` 也在 Chrome 149 stable 复现,提 Chromium [#521205128](https://issues.chromium.org/issues/521205128) → 纯内核 bug,非本项目代码。**应用层无法修**:丢失的那次按键在 Chromium 内部被吞,不触发任何 JS 事件(无 keydown/beforeinput/compositionend),拦不到。**不在 CodeMirror 加 inputHandler/keydown hack**:作者本人只提 issue 未给 workaround,硬 hack 引新问题。当前环境:WebView2 Runtime = Edge = 149.0.4022.62(Chromium 149,2026-06-02 stable),通道覆盖未设(stable 正常)。**根因**(2026-06-12 上游确认):Chromium 的 TSFTextStore 在 IME 活动时仍跑 autocorrect 检测,吞掉标点;修复 CL [7917332](https://chromium-review.googlesource.com/c/chromium/src/+/7917332)(Pranav Modi/MS)标题 "Skip autocorrect detection when IME is active in TSFTextStore"。
+- **状态**:🚧 上游已修复待发布(2026-06-12 在 Chrome 151.0.7888.0 / Win11 验证有效,已加 verified 标签;接受现状不应用层修)
+- **📋 待办**:修复落 **Chrome 151**(非 150)。等 Edge/WebView2 Runtime 升到 151 stable(Chrome 150 stable ~2026-06-29,151 stable 约 2026-07 下旬;急用可装 Edge Beta 151 提前验)→ 更新后回归验证编辑器中文标点。参考 [CodeMirror 论坛 #9741](https://discuss.codemirror.net/t/chinese-ime-punctuation-input-loses-every-other-keypress-requires-2-presses-per-character/9741)。
+
+---
+
+## 维护说明
+
+本文档为初始整理,覆盖 7 个历史会话 + 当前会话,共 ~90 条去重决策。
+
+**压缩信号**:当前 ~19 功能域、超 300 行。后续触发以下任一条件时压缩:
+- 老 Sprint 决策超 3 个 → 归档到 `功能决策记录-归档.md`
+- 某功能域 > 10 条 → 单独拆文件
+- 已稳定决策合并精简,只留结论 + 关键原因
+
+**更新优先于新增**:决策点已存在就改原条,演进用 `→` 串联,不留重复版本。
diff --git a/frontend/bindings/u-desk/app.ts b/frontend/bindings/u-desk/app.ts
index d7e29b6..f960a49 100644
--- a/frontend/bindings/u-desk/app.ts
+++ b/frontend/bindings/u-desk/app.ts
@@ -53,6 +53,13 @@ export function ClearCache(): $CancellablePromise
{
return $Call.ByID(1413834504);
}
+/**
+ * CopyPath 复制文件或目录(递归),通过 transfer-progress 事件推送进度
+ */
+export function CopyPath(src: string, dst: string): $CancellablePromise {
+ return $Call.ByID(1297703531, src, dst);
+}
+
/**
* CreateDir 创建目录
*/
@@ -116,6 +123,16 @@ export function EmptyRecycleBin(): $CancellablePromise {
return $Call.ByID(4176312624);
}
+/**
+ * EnsureDrawioViewer 确保本地已缓存 drawio viewer(首次预览时下载),前端打开 .drawio 前调用。
+ * 通过 drawio-download-progress 事件推送下载进度。
+ */
+export function EnsureDrawioViewer(): $CancellablePromise<{ [_ in string]?: any }> {
+ return $Call.ByID(1345697171).then(($result: any) => {
+ return $$createType2($result);
+ });
+}
+
/**
* ExportPDF 导出PDF文件
*/
@@ -318,6 +335,13 @@ export function LoadConnectionProfiles(): $CancellablePromise<{ [_ in string]?:
});
}
+/**
+ * MovePath 移动文件或目录(同盘重命名,跨盘复制后删除)
+ */
+export function MovePath(src: string, dst: string): $CancellablePromise {
+ return $Call.ByID(2028899675, src, dst);
+}
+
/**
* OpenPath 使用系统默认程序打开文件或目录
*/
@@ -451,6 +475,13 @@ export function OssWriteFile(connID: string, key: string, content: string): $Can
return $Call.ByID(39773277, connID, key, content);
}
+/**
+ * PickDirectory 打开系统目录选择器,返回选中目录(取消时返回空串)
+ */
+export function PickDirectory(): $CancellablePromise {
+ return $Call.ByID(1347829059);
+}
+
/**
* ReadFile 读取文件
*/
@@ -597,6 +628,13 @@ export function SftpDownloadSiteForPreview(connID: string, remotePath: string):
return $Call.ByID(1591575570, connID, remotePath);
}
+/**
+ * SftpDownloadToFile 下载远程文件/目录到用户指定目录(流式无上限,重名自动副本),返回本地落地路径
+ */
+export function SftpDownloadToFile(connID: string, remotePath: string, localDstDir: string): $CancellablePromise {
+ return $Call.ByID(2935422351, connID, remotePath, localDstDir);
+}
+
/**
* SftpDownloadToTemp 下载远程文件到本地临时目录(用于预览)
*/
@@ -663,6 +701,13 @@ export function SftpRenamePath(req: $models.SftpRenamePathRequest): $Cancellable
});
}
+/**
+ * SftpUploadFromFile 上传本地文件/目录到远程目录(流式,目录递归,重名自动副本)
+ */
+export function SftpUploadFromFile(connID: string, localPath: string, remoteDir: string): $CancellablePromise {
+ return $Call.ByID(3304594335, connID, localPath, remoteDir);
+}
+
/**
* SftpWriteBase64File SFTP 写入 base64 编码的二进制文件(粘贴图片等)
*/
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e9de754..1cd434c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -459,361 +459,130 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
- "cpu": [
- "ppc64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/android-arm": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
- "cpu": [
- "arm"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
- "cpu": [
- "arm64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/android-x64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
- "cpu": [
- "arm64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
- "cpu": [
- "arm64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
- "cpu": [
- "arm"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
- "cpu": [
- "arm64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
- "cpu": [
- "ia32"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
- "cpu": [
- "loong64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
- "cpu": [
- "mips64el"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
- "cpu": [
- "ppc64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
- "cpu": [
- "riscv64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
- "cpu": [
- "s390x"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
- "cpu": [
- "arm64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
- "cpu": [
- "arm64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.7",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "optional": true
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
@@ -1386,102 +1155,46 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.60.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
"integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
- "cpu": [
- "s390x"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "optional": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.60.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
"integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "optional": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.60.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
"integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "optional": true
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.60.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
"integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
- "cpu": [
- "x64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
+ "optional": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.60.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
"integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
- "cpu": [
- "arm64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
+ "optional": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.60.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
"integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
- "cpu": [
- "arm64"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "optional": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.60.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
"integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
- "cpu": [
- "ia32"
- ],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "optional": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.2",
@@ -3009,19 +2722,10 @@
}
},
"node_modules/fsevents": {
- "version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
+ "optional": true
},
"node_modules/glob-parent": {
"version": "5.1.2",
diff --git a/frontend/src/api/connection-manager.ts b/frontend/src/api/connection-manager.ts
index 584ad64..be832e9 100644
--- a/frontend/src/api/connection-manager.ts
+++ b/frontend/src/api/connection-manager.ts
@@ -18,7 +18,8 @@ export type ConnectionType = 'local' | 'remote' | 'sftp' | 'oss'
export type OssProvider = 'qiniu' | 'aliyun'
export interface ConnectionProfile {
- id: string | number
+ /** 统一为字符串: 本地固定 'local-default',其余为数据库自增 id 的十进制字符串 */
+ id: string
name: string
host: string
port: number
@@ -58,6 +59,8 @@ class ConnectionManagerImpl {
private _activeId: string | null = null
private _state: ConnectionState = 'disconnected'
private _stateChangeCallbacks: ((state: ConnectionState) => void)[] = []
+ /** 每个 profile 的独立连接状态(连接菜单圆点用) */
+ private _profileStates = new Map()
private _sysInfoChangeCallbacks: ((profileId: string, info: SystemInfo) => void)[] = []
private _sysInfoCache = new Map()
@@ -91,8 +94,8 @@ class ConnectionManagerImpl {
try {
const list = await LoadConnectionProfiles()
if (list && list.length > 0) {
- this._profiles = list.map((p: any) => {
- const camel = snakeToCamel(p)
+ this._profiles = list.map((p: any): ConnectionProfile => {
+ const camel = snakeToCamel(p) as ConnectionProfile
return {
...camel,
id: String(p.id),
@@ -117,18 +120,18 @@ class ConnectionManagerImpl {
if (autoConnect !== 'false') {
for (const p of this._profiles) {
if (p.type !== 'local') {
- this.buildAndPool(String(p.id), p).catch(() => {})
+ this.buildAndPool(String(p.id), p).catch(e => console.warn('[连接管理器] 自动连接失败:', p.name, e))
}
}
}
}, 500)
}
- /** 保存/更新单个 profile 到 SQLite */
- private async persistProfile(profile: ConnectionProfile) {
- if (profile.type === 'local') return
+ /** 保存/更新单个 profile 到 SQLite,返回数据库真实 id(新建时) */
+ private async persistProfile(profile: ConnectionProfile): Promise {
+ if (profile.type === 'local') return undefined
const id = profile.id !== 'local-default' ? Number(profile.id) : 0
- await SaveConnectionProfile({
+ const result = await SaveConnectionProfile({
id: id > 0 ? id : undefined,
name: profile.name,
host: profile.host,
@@ -146,6 +149,8 @@ class ConnectionManagerImpl {
endpoint: profile.endpoint || '',
lastConnected: profile.lastConnected ? Math.floor(profile.lastConnected / 1000) : undefined,
})
+ const realId = Number((result as any)?.id ?? 0)
+ return realId > 0 ? realId : undefined
}
/** 从 SQLite 删除 profile */
@@ -159,6 +164,17 @@ class ConnectionManagerImpl {
this.notifyChange()
}
+ /** 更新单个 profile 的连接状态(对齐 Rust 版,供连接菜单圆点着色) */
+ private setProfileState(profileId: string, state: ConnectionState, error?: string) {
+ this._profileStates.set(profileId, { state, error })
+ this.notifyChange()
+ }
+
+ getProfileState(profileId: string): { state: ConnectionState; error?: string } {
+ if (profileId === 'local-default') return { state: 'connected' }
+ return this._profileStates.get(profileId) ?? { state: 'disconnected' }
+ }
+
private notifyChange() {
this._stateChangeCallbacks.forEach(cb => cb(this._state))
}
@@ -252,7 +268,7 @@ class ConnectionManagerImpl {
}
}
}
- } catch (e) { info._error = true; info._errorMsg = e instanceof Error ? e.message : String(e) }
+ } catch (e) { info._error = true; info._errorMsg = errMsg(e) }
this._sysInfoCache.set(profileId, info)
this._sysInfoChangeCallbacks.forEach(cb => cb(profileId, info))
@@ -268,11 +284,32 @@ class ConnectionManagerImpl {
const profile = this._profiles.find(p => p.id === profileId)
if (!profile) return Promise.reject(new Error(`连接配置不存在: ${profileId}`))
- // 池中已有,直接复用
+ this.setProfileState(profileId, 'connecting')
+
+ // 池中已有,探测有效性(探测加超时:半开死连接上的网络探测会无限挂起,超时按死连接处理走重建)
if (this._pool.has(profileId)) {
- this._activeId = profileId
- this.setState('connected')
- return
+ const t = this._pool.get(profileId)!
+ if (t.isAlive) {
+ let probeTimer: ReturnType | undefined
+ const alive = await Promise.race([
+ t.isAlive().catch(() => false),
+ new Promise(resolve => { probeTimer = setTimeout(() => resolve(false), 2500) }),
+ ])
+ if (probeTimer) clearTimeout(probeTimer)
+ if (alive) {
+ this._activeId = profileId
+ this.setProfileState(profileId, 'connected')
+ this.setState('connected')
+ return
+ }
+ } else {
+ this._activeId = profileId
+ this.setProfileState(profileId, 'connected')
+ this.setState('connected')
+ return
+ }
+ // 连接已死,清理后重建
+ this.disconnectProfile(profileId)
}
// 先设 activeId(确保回调读到正确的 activeProfile),失败时回退
@@ -282,6 +319,7 @@ class ConnectionManagerImpl {
await this.buildAndPool(profileId, profile)
} catch (err) {
this._activeId = prevActiveId
+ this.setProfileState(profileId, 'error', err instanceof Error ? err.message : String(err))
this.notifyChange()
throw err
}
@@ -294,6 +332,7 @@ class ConnectionManagerImpl {
if (t instanceof SftpTransport) { t.disconnect() }
if (t instanceof OssTransport) { t.disconnect() }
this._pool.delete(profileId)
+ this.setProfileState(profileId, 'disconnected')
}
/** 断开所有远程连接(保留 local) */
@@ -313,10 +352,26 @@ class ConnectionManagerImpl {
this.disconnectAll()
}
+ /** 查找同名且同地址(OSS 比对 bucket)的既有条目,local 不参与查重;查重谓词唯一实现,供对话框预判与 addProfile 共用避免两处漂移 */
+ findDuplicateProfile(profile: Pick): ConnectionProfile | undefined {
+ return this._profiles.find(p =>
+ p.type !== 'local' && p.name === profile.name &&
+ (profile.type === 'oss' ? p.bucket === profile.bucket : p.host === profile.host && p.port === profile.port)
+ )
+ }
+
async addProfile(profile: Omit): Promise {
+ // 防重复: 同名且同地址的配置已存在时直接返回既有条目(表单双击/重试等场景)
+ const dup = this.findDuplicateProfile(profile)
+ if (dup) return dup
const newProfile: ConnectionProfile = { ...profile, id: crypto.randomUUID() }
this._profiles.push(newProfile)
- await this.persistProfile(newProfile)
+ // 持久化后回填数据库真实 id(自增主键),否则后续 updateProfile
+ // 会因 UUID 无法转数字而每次都 INSERT 新行 → 列表出现重复服务器
+ const realId = await this.persistProfile(newProfile)
+ if (realId !== undefined) {
+ newProfile.id = String(realId)
+ }
this.notifyChange()
return newProfile
}
@@ -354,25 +409,30 @@ class ConnectionManagerImpl {
private async buildAndPool(profileId: string, profile: ConnectionProfile): Promise {
if (profile.type === 'local') {
this._pool.set(profileId, new WailsTransport())
+ this.setProfileState(profileId, 'connected')
this.setState('connected')
return
}
if (profile.type === 'sftp') {
if (!profile.password && !profile.keyPath) {
+ this.setProfileState(profileId, 'error', '未配置密码或私钥')
this.setState('error')
return
}
+ this.setProfileState(profileId, 'connecting')
this.setState('connecting')
try {
const t = new SftpTransport(profile)
await t.connect()
this._pool.set(profileId, t)
+ this.setProfileState(profileId, 'connected')
this.setState('connected')
this.updateProfile(profileId, { lastConnected: Date.now() })
this.fetchSystemInfo(profileId).catch(() => {})
} catch (err) {
this._pool.delete(profileId)
+ this.setProfileState(profileId, 'error', errMsg(err))
this.setState('error')
throw err
}
@@ -381,15 +441,18 @@ class ConnectionManagerImpl {
// OSS
if (profile.type === 'oss') {
+ this.setProfileState(profileId, 'connecting')
this.setState('connecting')
try {
const t = new OssTransport(profile)
await t.connect()
this._pool.set(profileId, t)
+ this.setProfileState(profileId, 'connected')
this.setState('connected')
this.updateProfile(profileId, { lastConnected: Date.now() })
} catch (err) {
this._pool.delete(profileId)
+ this.setProfileState(profileId, 'error', errMsg(err))
this.setState('error')
throw err
}
@@ -397,22 +460,26 @@ class ConnectionManagerImpl {
}
// remote / http agent
+ this.setProfileState(profileId, 'connecting')
this.setState('connecting')
try {
const t = new HttpTransport(profile.host, profile.port, profile.token)
this._pool.set(profileId, t)
// 验证连接可用性
t.getFileInfo('/').then(() => {
+ this.setProfileState(profileId, 'connected')
if (this._activeId !== profileId) return
this.setState('connected')
this.updateProfile(profileId, { lastConnected: Date.now() })
this.fetchSystemInfo(profileId).catch(() => {})
- }).catch(() => {
+ }).catch((e) => {
+ this.setProfileState(profileId, 'error', errMsg(e))
if (this._activeId !== profileId) return
this.setState('error')
})
} catch (err) {
this._pool.delete(profileId)
+ this.setProfileState(profileId, 'error', errMsg(err))
this.setState('error')
throw err
}
@@ -421,6 +488,11 @@ class ConnectionManagerImpl {
export const connectionManager = new ConnectionManagerImpl()
+/** 任意抛出值转错误文案 */
+function errMsg(e: unknown): string {
+ return e instanceof Error ? e.message : String(e)
+}
+
/** snake_case → camelCase */
function snakeToCamel(obj: Record): Record {
const result: Record = {}
diff --git a/frontend/src/api/http-transport.ts b/frontend/src/api/http-transport.ts
index 91af236..4bda45e 100644
--- a/frontend/src/api/http-transport.ts
+++ b/frontend/src/api/http-transport.ts
@@ -10,9 +10,10 @@ export class HttpTransport implements FsTransport {
private baseUrl: string
private token: string
- constructor(host: string, port: number, token: string) {
+ constructor(host: string, port: number | undefined, token: string) {
const scheme = port === 443 ? 'https' : 'http'
- this.baseUrl = `${scheme}://${host}${port === 80 || port === 443 ? '' : ':' + port}`
+ const portSuffix = (!port || port === 80 || port === 443) ? '' : `:${port}`
+ this.baseUrl = `${scheme}://${host}${portSuffix}`
this.token = token
}
@@ -82,11 +83,32 @@ export class HttpTransport implements FsTransport {
return this.request('PATCH', '/api/v1/fs/rename', { path: oldPath }, { new_path: newPath })
}
+ // ====== 复制/移动/传输(不支持)======
+
+ async copyPath(_src: string, _dst: string): Promise {
+ throw new Error('远程模式暂不支持复制/粘贴')
+ }
+
+ async movePath(_src: string, _dst: string): Promise {
+ throw new Error('远程模式暂不支持剪切/粘贴')
+ }
+
+ async downloadToFile(_path: string, _localDstDir: string): Promise {
+ throw new Error('远程模式暂不支持下载到本机')
+ }
+
+ async uploadFromFile(_localPath: string, _remoteDir: string): Promise {
+ throw new Error('远程模式暂不支持上传')
+ }
+
async listZipContents(zipPath: string): Promise {
// Wave 3 实现
throw new Error('ZIP 操作在远程模式暂未实现')
}
+ async watchFile(_path: string): Promise { /* 远程 HTTP 模式不支持文件监听 */ }
+ async unwatchFile(): Promise { /* 远程 HTTP 模式不支持文件监听 */ }
+
async extractFileFromZip(_zipPath: string, _filePath: string): Promise {
throw new Error('ZIP 操作在远程模式暂未实现')
}
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts
index dd5a947..76ab290 100644
--- a/frontend/src/api/index.ts
+++ b/frontend/src/api/index.ts
@@ -2,5 +2,4 @@
* API 统一导出
*/
-export * from './types'
export * from './system'
diff --git a/frontend/src/api/oss-transport.ts b/frontend/src/api/oss-transport.ts
index 74d733d..77c2db4 100644
--- a/frontend/src/api/oss-transport.ts
+++ b/frontend/src/api/oss-transport.ts
@@ -5,6 +5,7 @@
import type {
FsTransport, FileItem, FileOperationResult, DetectTypeResult,
} from './transport'
+import { transformFileList, joinPath, probeAlive } from './transport'
import type { ConnectionProfile } from './connection-manager'
import {
OssConnect, OssDisconnect, OssListDir, OssReadFile,
@@ -13,14 +14,6 @@ import {
OssGetCommonPaths, OssWriteBase64File, OssGetSignedURL,
} from '@bindings/u-desk/app'
-function transformFile(file: any): FileItem {
- return { ...file, is_dir: file.is_dir, mod_time: file.mod_time }
-}
-
-function transformFileList(files: any[]): FileItem[] {
- return files.map(transformFile)
-}
-
export class OssTransport implements FsTransport {
private profile: ConnectionProfile
private connID: string | null = null
@@ -40,6 +33,10 @@ export class OssTransport implements FsTransport {
return result
}
+ async isAlive(): Promise {
+ return probeAlive(this.connID, id => OssListDir(id, ''))
+ }
+
get sessionId(): string | null { return this.connID }
async disconnect(): Promise {
@@ -84,13 +81,11 @@ export class OssTransport implements FsTransport {
// ====== 文件操作 ======
async createFile(dirPath: string, filename: string): Promise {
- const fullPath = dirPath.replace(/\/$/, '') + '/' + filename
- return OssCreateFile(this.requireConn(), fullPath)
+ return OssCreateFile(this.requireConn(), joinPath(dirPath, filename))
}
async createDir(parentPath: string, dirname: string): Promise {
- const fullPath = parentPath.replace(/\/$/, '') + '/' + dirname
- return OssCreateDir(this.requireConn(), fullPath)
+ return OssCreateDir(this.requireConn(), joinPath(parentPath, dirname))
}
async deletePath(path: string): Promise {
@@ -105,6 +100,24 @@ export class OssTransport implements FsTransport {
})
}
+ // ====== 复制/移动/传输(不支持)======
+
+ async copyPath(_src: string, _dst: string): Promise {
+ throw new Error('OSS 面板暂不支持复制/粘贴')
+ }
+
+ async movePath(_src: string, _dst: string): Promise {
+ throw new Error('OSS 面板暂不支持剪切/粘贴')
+ }
+
+ async downloadToFile(_path: string, _localDstDir: string): Promise {
+ throw new Error('OSS 下载到本机暂未实现')
+ }
+
+ async uploadFromFile(_localPath: string, _remoteDir: string): Promise {
+ throw new Error('OSS 上传暂未实现')
+ }
+
// ====== ZIP 操作(不支持)======
async listZipContents(_zipPath: string): Promise {
@@ -177,4 +190,7 @@ export class OssTransport implements FsTransport {
async getSignedUrl(key: string): Promise {
return OssGetSignedURL(this.requireConn(), key)
}
+
+ async watchFile(_path: string): Promise { /* OSS 模式不支持文件监听 */ }
+ async unwatchFile(): Promise { /* OSS 模式不支持文件监听 */ }
}
diff --git a/frontend/src/api/sftp-transport.ts b/frontend/src/api/sftp-transport.ts
index 67ebf35..1fead36 100644
--- a/frontend/src/api/sftp-transport.ts
+++ b/frontend/src/api/sftp-transport.ts
@@ -5,22 +5,16 @@
import type {
FsTransport, FileItem, FileOperationResult, DetectTypeResult,
} from './transport'
+import { transformFileList, joinPath, probeAlive } from './transport'
import type { ConnectionProfile } from './connection-manager'
import {
SftpConnect, SftpDisconnect, SftpListDir, SftpReadFile,
SftpWriteFile, SftpGetFileInfo, SftpCreateDir, SftpCreateFile,
SftpDeletePath, SftpRenamePath, SftpDownloadToTemp, SftpDownloadSiteForPreview,
+ SftpDownloadToFile, SftpUploadFromFile,
SftpGetCommonPaths, SftpWriteBase64File,
} from '@bindings/u-desk/app'
-function transformFile(file: any): FileItem {
- return { ...file, is_dir: file.is_dir, mod_time: file.mod_time }
-}
-
-function transformFileList(files: any[]): FileItem[] {
- return files.map(transformFile)
-}
-
export class SftpTransport implements FsTransport {
private profile: ConnectionProfile
private connID: string | null = null
@@ -42,6 +36,10 @@ export class SftpTransport implements FsTransport {
return result
}
+ async isAlive(): Promise {
+ return probeAlive(this.connID, id => SftpGetFileInfo(id, '/'))
+ }
+
get sessionId(): string | null { return this.connID }
async disconnect(): Promise {
@@ -90,13 +88,11 @@ export class SftpTransport implements FsTransport {
// ====== 文件操作 ======
async createFile(dirPath: string, filename: string): Promise {
- const fullPath = dirPath.replace(/\/$/, '') + '/' + filename
- return SftpCreateFile(this.requireConn(), fullPath)
+ return SftpCreateFile(this.requireConn(), joinPath(dirPath, filename))
}
async createDir(parentPath: string, dirname: string): Promise {
- const fullPath = parentPath.replace(/\/$/, '') + '/' + dirname
- return SftpCreateDir(this.requireConn(), fullPath)
+ return SftpCreateDir(this.requireConn(), joinPath(parentPath, dirname))
}
async deletePath(path: string): Promise {
@@ -111,6 +107,28 @@ export class SftpTransport implements FsTransport {
})
}
+ // ====== 复制/移动(粘贴仅本地面板支持)======
+
+ async copyPath(_src: string, _dst: string): Promise {
+ throw new Error('SFTP 面板暂不支持粘贴,可使用拖拽上传')
+ }
+
+ async movePath(_src: string, _dst: string): Promise {
+ throw new Error('SFTP 面板暂不支持粘贴,可使用拖拽上传')
+ }
+
+ // ====== 传输 ======
+
+ /** 下载远程文件/目录到用户指定目录(流式无上限,重名自动副本),返回本地落地路径 */
+ async downloadToFile(remotePath: string, localDstDir: string): Promise {
+ return SftpDownloadToFile(this.requireConn(), remotePath, localDstDir)
+ }
+
+ /** 上传本地文件/目录到远程目录(流式,目录递归) */
+ async uploadFromFile(localPath: string, remoteDir: string): Promise {
+ await SftpUploadFromFile(this.requireConn(), localPath, remoteDir)
+ }
+
// ====== ZIP 操作(不支持)======
async listZipContents(_zipPath: string): Promise {
@@ -179,4 +197,7 @@ export class SftpTransport implements FsTransport {
async downloadSiteForPreview(remotePath: string): Promise {
return SftpDownloadSiteForPreview(this.requireConn(), remotePath)
}
+
+ async watchFile(_path: string): Promise { /* SFTP 模式不支持文件监听 */ }
+ async unwatchFile(): Promise { /* SFTP 模式不支持文件监听 */ }
}
diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts
index 38933c2..19925b4 100644
--- a/frontend/src/api/system.ts
+++ b/frontend/src/api/system.ts
@@ -3,18 +3,17 @@
* 本地模式走 Wails IPC,远程模式走 HTTP REST API
*/
-import type { File } from './types'
+import type { FileItem } from '@/types/file-system'
import { connectionManager } from './connection-manager'
+import { PickDirectory } from '../wailsjs/v3-bindings/u-desk/app'
-/**
- * 转换后端文件数据格式(蛇形 → 驼峰)
- */
-function transformFile(file: any): File {
+/** 转换后端蛇形字段为前端驼峰格式 */
+function toFileItem(file: any): FileItem {
return { ...file, isDir: file.is_dir, modified_time: file.mod_time }
}
-function transformFileList(files: any[]): File[] {
- return files.map(transformFile)
+function toFileItemList(files: any[]): FileItem[] {
+ return files.map(toFileItem)
}
const t = () => connectionManager.getTransport()
@@ -23,21 +22,21 @@ export async function getSystemInfo() { return t().getFileInfo('/') }
export async function getCPUInfo() {
if (connectionManager.isRemote()) return {}
- try { return await (window.go?.main?.App?.GetCPUInfo?.()) ?? {} } catch { return {} }
+ try { return (await window.go?.main?.App?.GetCPUInfo?.()) ?? {} } catch { return {} }
}
export async function getMemoryInfo() {
if (connectionManager.isRemote()) return {}
- try { return await (window.go?.main?.App?.GetMemoryInfo?.()) ?? {} } catch { return {} }
+ try { return (await window.go?.main?.App?.GetMemoryInfo?.()) ?? {} } catch { return {} }
}
export async function getDiskInfo() {
if (connectionManager.isRemote()) return {}
- try { return await (window.go?.main?.App?.GetDiskInfo?.()) ?? {} } catch { return {} }
+ try { return (await window.go?.main?.App?.GetDiskInfo?.()) ?? {} } catch { return {} }
}
-export async function listDir(path: string): Promise {
- return transformFileList(await t().listDir(path))
+export async function listDir(path: string): Promise {
+ return toFileItemList(await t().listDir(path))
}
export async function readFile(path: string): Promise {
@@ -69,12 +68,31 @@ export async function renamePath(oldPath: string, newPath: string): Promise
return t().renamePath(oldPath, String(newPath))
}
-export async function getEnvVars(): Promise> {
- try { return await (window.go?.main?.App?.GetEnvVars?.()) ?? {} } catch { return {} }
+// 复制/移动(本机面板粘贴;进度经 transfer-progress 事件推送)
+export async function copyPath(src: string, dst: string): Promise {
+ await t().copyPath(src, dst)
}
-export async function listZipContents(zipPath: string): Promise {
- return transformFileList(await t().listZipContents(zipPath))
+export async function movePath(src: string, dst: string): Promise {
+ await t().movePath(src, dst)
+}
+
+// 下载远程文件/目录到本机指定目录(SFTP),返回本地落地路径
+export async function downloadToFile(path: string, localDstDir: string): Promise {
+ return t().downloadToFile(path, localDstDir)
+}
+
+// 上传本地文件/目录到远程目录(SFTP)
+export async function uploadFromFile(localPath: string, remoteDir: string): Promise {
+ await t().uploadFromFile(localPath, remoteDir)
+}
+
+export async function getEnvVars(): Promise> {
+ try { return (await window.go?.main?.App?.GetEnvVars?.()) ?? {} } catch { return {} }
+}
+
+export async function listZipContents(zipPath: string): Promise {
+ return toFileItemList(await t().listZipContents(zipPath))
}
export async function extractFileFromZip(zipPath: string, filePath: string): Promise {
@@ -85,8 +103,8 @@ export async function extractFileFromZipToTemp(zipPath: string, filePath: string
return t().extractFileFromZipToTemp(zipPath, filePath)
}
-export async function getZipFileInfo(zipPath: string, filePath: string): Promise {
- return transformFile(await t().getZipFileInfo(zipPath, filePath))
+export async function getZipFileInfo(zipPath: string, filePath: string): Promise {
+ return toFileItem(await t().getZipFileInfo(zipPath, filePath))
}
export async function openPath(path: string): Promise {
@@ -109,19 +127,16 @@ export async function getCommonPaths() {
return t().getCommonPaths()
}
-// 文件监听(仅本地模式,直接调用 Wails 绑定)
+// 打开系统目录选择器(取消返回空串)
+export async function pickDirectory(): Promise {
+ return PickDirectory()
+}
+
+// 文件监听(通过 Transport 层,本地模式走 Wails IPC,远程模式空操作)
export async function watchFile(path: string): Promise {
- if (connectionManager.isRemote()) return
- try {
- const { WatchFile } = await import('../wailsjs/v3-bindings/u-desk/app')
- await WatchFile(path)
- } catch { /* 忽略绑定未生成的场景 */ }
+ await t().watchFile(path)
}
export async function unwatchFile(): Promise {
- if (connectionManager.isRemote()) return
- try {
- const { UnwatchFile } = await import('../wailsjs/v3-bindings/u-desk/app')
- await UnwatchFile()
- } catch { /* 忽略 */ }
+ await t().unwatchFile()
}
diff --git a/frontend/src/api/transport.ts b/frontend/src/api/transport.ts
index cf9108d..db87ef3 100644
--- a/frontend/src/api/transport.ts
+++ b/frontend/src/api/transport.ts
@@ -34,6 +34,9 @@ export type DetectTypeResult = {
}
export interface FsTransport {
+ /** 检测连接是否仍然存活 */
+ isAlive?(): Promise
+
// 文件列表与信息
listDir(path: string): Promise
getFileInfo(path: string): Promise>
@@ -49,6 +52,14 @@ export interface FsTransport {
deletePath(path: string): Promise
renamePath(oldPath: string, newPath: string): Promise
+ // 复制/移动(本机面板粘贴;SFTP/OSS/HTTP 暂不支持)
+ copyPath(src: string, dst: string): Promise
+ movePath(src: string, dst: string): Promise
+
+ // 传输(SFTP 下载到本机 / 本地文件上传到 SFTP;进度经 transfer-progress 事件推送)
+ downloadToFile(path: string, localDstDir: string): Promise
+ uploadFromFile(localPath: string, remoteDir: string): Promise
+
// ZIP 操作(Wave 3)
listZipContents(zipPath: string): Promise
extractFileFromZip(zipPath: string, filePath: string): Promise
@@ -76,4 +87,42 @@ export interface FsTransport {
downloadSiteForPreview?(path: string): Promise
/** 获取预签名 URL(仅 OSS 实现,其他返回空串) */
getSignedUrl?(path: string): Promise
+
+ // 文件监听(仅本地 Wails 模式有效)
+ watchFile(path: string): Promise
+ unwatchFile(): Promise
+}
+
+// ========== 共享工具函数 ==========
+
+// 说明:后端蛇形 → 前端驼峰(isDir/modified_time)的统一转换在 system.ts 的
+// toFileItem/toFileItemList 完成;下面两个函数仅做【类型适配】(运行时恒等),
+// 把绑定层返回的松散类型断言为 FileItem,供各 transport 复用。
+
+/** 类型适配:断言后端文件数据为 FileItem(运行时恒等) */
+export function transformFile(file: any): FileItem {
+ return { ...file, is_dir: file.is_dir, mod_time: file.mod_time }
+}
+
+export function transformFileList(files: any[]): FileItem[] {
+ return files.map(transformFile)
+}
+
+/** 路径拼接:确保父路径以 / 结尾后拼接文件名 */
+export function joinPath(parent: string, name: string): string {
+ return parent.replace(/\/$/, '') + '/' + name
+}
+
+/** 连接存活探测:无会话 id 或探测调用失败均视为死连接(SFTP/OSS 共用) */
+export async function probeAlive(
+ connId: string | null,
+ probe: (id: string) => Promise,
+): Promise {
+ if (!connId) return false
+ try {
+ await probe(connId)
+ return true
+ } catch {
+ return false
+ }
}
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
deleted file mode 100644
index bd64a8a..0000000
--- a/frontend/src/api/types.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * API 类型定义
- */
-
-// 系统信息
-export interface SystemInfo {
- os: string
- arch: string
- version: string
-}
-
-export interface CPU {
- model: string
- cores: number
- usage: number
-}
-
-export interface Memory {
- total: number
- used: number
- free: number
- usage: number
-}
-
-export interface Disk {
- path: string
- total: number
- used: number
- free: number
- usage: number
-}
-
-export interface File {
- name: string
- path: string
- size: number
- isDir: boolean
- modified?: string
- modified_time?: string
-}
diff --git a/frontend/src/api/wails-transport.ts b/frontend/src/api/wails-transport.ts
index 5807e94..b68f653 100644
--- a/frontend/src/api/wails-transport.ts
+++ b/frontend/src/api/wails-transport.ts
@@ -3,25 +3,20 @@
*/
import type { FsTransport, FileItem, FileOperationResult, DetectTypeResult } from './transport'
+import { transformFile, transformFileList, joinPath } from './transport'
import {
ListDir, GetFileInfo, ReadFile, WriteFile, SaveBase64File,
- CreateFile, CreateDir, DeletePath, RenamePath,
+ CreateFile, CreateDir, DeletePath, RenamePath, CopyPath, MovePath,
ListZipContents, ExtractFileFromZip, ExtractFileFromZipToTemp, GetZipFileInfo,
OpenPath, GetFileServerURL, ResolveShortcut, DetectFileTypeByContent,
GetCommonPaths, GetRecycleBinEntries, RestoreFromRecycleBin,
- DeletePermanently, EmptyRecycleBin
+ DeletePermanently, EmptyRecycleBin,
+ WatchFile, UnwatchFile
} from '../wailsjs/v3-bindings/u-desk/app'
-function transformFile(file: any): FileItem {
- return { ...file, is_dir: file.is_dir, mod_time: file.mod_time }
-}
-
-function transformFileList(files: any[]): FileItem[] {
- return files.map(transformFile)
-}
-
export class WailsTransport implements FsTransport {
async listDir(path: string): Promise {
+ // 蛇形→驼峰转换统一由 system.ts 的 toFileItemList 完成,此处仅类型适配
return transformFileList(await ListDir(path))
}
@@ -43,13 +38,11 @@ export class WailsTransport implements FsTransport {
}
async createFile(dirPath: string, filename: string): Promise {
- const fullPath = dirPath.replace(/\/$/, '') + '/' + filename
- return CreateFile(fullPath)
+ return CreateFile(joinPath(dirPath, filename))
}
async createDir(parentPath: string, dirname: string): Promise {
- const fullPath = parentPath.replace(/\/$/, '') + '/' + dirname
- return CreateDir(fullPath)
+ return CreateDir(joinPath(parentPath, dirname))
}
async deletePath(path: string): Promise {
@@ -60,6 +53,22 @@ export class WailsTransport implements FsTransport {
return RenamePath({ oldPath: String(oldPath), newPath: String(newPath) })
}
+ async copyPath(src: string, dst: string): Promise {
+ await CopyPath(String(src), String(dst))
+ }
+
+ async movePath(src: string, dst: string): Promise {
+ await MovePath(String(src), String(dst))
+ }
+
+ async downloadToFile(path: string, _localDstDir: string): Promise {
+ return path // 本地文件无需下载
+ }
+
+ async uploadFromFile(_localPath: string, _remoteDir: string): Promise {
+ // 本地面板无上传语义(外部文件拖入走 copyPath)
+ }
+
async listZipContents(zipPath: string): Promise {
return transformFileList(await ListZipContents(zipPath))
}
@@ -73,7 +82,7 @@ export class WailsTransport implements FsTransport {
}
async getZipFileInfo(zipPath: string, filePath: string): Promise {
- return transform(await GetZipFileInfo(zipPath, filePath))
+ return transformFile(await GetZipFileInfo(zipPath, filePath))
}
async openPath(path: string): Promise {
@@ -120,4 +129,12 @@ export class WailsTransport implements FsTransport {
async downloadForPreview(path: string): Promise {
return path
}
+
+ async watchFile(path: string): Promise {
+ await WatchFile(path)
+ }
+
+ async unwatchFile(): Promise {
+ await UnwatchFile()
+ }
}
diff --git a/frontend/src/components/FileSystem/components/ConnectionDialog.vue b/frontend/src/components/FileSystem/components/ConnectionDialog.vue
index ec6f078..d8e767b 100644
--- a/frontend/src/components/FileSystem/components/ConnectionDialog.vue
+++ b/frontend/src/components/FileSystem/components/ConnectionDialog.vue
@@ -145,9 +145,12 @@ watch(() => props.visible, (val) => {
})
watch(() => form.type, (t) => {
- if (t === 'sftp') form.port = 22
- else if (t === 'remote') form.port = 9876
- else form.port = 0
+ // 仅当前端口是其它类型的默认值(而非用户自定义值)时才重置,避免切类型丢失自定义端口
+ const DEFAULT_PORTS: Record = { sftp: 22, remote: 9876, oss: 0 }
+ const isOtherDefault = Object.entries(DEFAULT_PORTS).some(([, v]) => form.port === v)
+ if (isOtherDefault) {
+ form.port = DEFAULT_PORTS[t] ?? 0
+ }
})
async function handleOk(): Promise {
@@ -166,8 +169,10 @@ async function handleOk(): Promise {
await connectionManager.updateProfile(editingId.value, { ...form })
Message.success('已更新')
} else {
+ // addProfile 遇同名同地址的已有配置会静默复用,提前查重以区分提示
+ const existed = !!connectionManager.findDuplicateProfile(form)
await connectionManager.addProfile({ ...form, type: form.type })
- Message.success('已添加')
+ Message.success(existed ? '已存在同名配置,已复用' : '已添加')
}
emit('update:visible', false)
return true
diff --git a/frontend/src/components/FileSystem/components/ContextMenu.vue b/frontend/src/components/FileSystem/components/ContextMenu.vue
index 9d1cd48..80ca4a3 100644
--- a/frontend/src/components/FileSystem/components/ContextMenu.vue
+++ b/frontend/src/components/FileSystem/components/ContextMenu.vue
@@ -18,6 +18,16 @@
新建文件夹
+
@@ -34,13 +44,35 @@
+