@xiaoqiong0v0/opencode-ssh-tool 0.6.18 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -3
- package/dist/agent.d.ts +7 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +1 -0
- package/dist/agent.js.map +1 -1
- package/dist/constants.d.ts +43 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +45 -1
- package/dist/constants.js.map +1 -1
- package/dist/i18n.d.ts +142 -8
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +118 -8
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +237 -12
- package/dist/index.js.map +1 -1
- package/dist/path-utils.d.ts +8 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/path-utils.js +17 -0
- package/dist/path-utils.js.map +1 -0
- package/dist/payload.d.ts.map +1 -1
- package/dist/payload.js +2 -15
- package/dist/payload.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +123 -1
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +59 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +215 -1
- package/dist/session.js.map +1 -1
- package/dist/transfer-events.d.ts +41 -0
- package/dist/transfer-events.d.ts.map +1 -0
- package/dist/transfer-events.js +79 -0
- package/dist/transfer-events.js.map +1 -0
- package/dist/transfer.d.ts +121 -0
- package/dist/transfer.d.ts.map +1 -0
- package/dist/transfer.js +1001 -0
- package/dist/transfer.js.map +1 -0
- package/dist/web/app.css +47 -1
- package/dist/web/app.js +10 -10
- package/dist/web/index.html +11 -0
- package/package.json +56 -56
package/dist/i18n.js
CHANGED
|
@@ -59,12 +59,12 @@ export const T = {
|
|
|
59
59
|
zh: "service 的 PATH 找不到可执行文件:{cmd}。请改用不含空格的全路径,或 \"powershell\"。",
|
|
60
60
|
},
|
|
61
61
|
term_cli: {
|
|
62
|
-
en: "Unified terminal CLI: SSH (remote) / local shell (wsl, pwsh, bash) / container (docker exec) sessions. For ANY terminal you already have or can start locally — WSL, Docker containers, or local shells — use the 'local' subcommand INSTEAD of running commands via bash. Subcommands: connect / local / exec / read / send / status / disconnect / help. Use 'help' for full usage. Long-running, stdin-reading or interactive commands: use the default submit (returns immediately) and then status / read
|
|
63
|
-
zh: "统一终端命令行工具:SSH(远程)/ 本地 shell(wsl、pwsh、bash)/ 容器(docker exec)会话。对任何本地可启动的终端——WSL、Docker 容器或本地 shell——应使用 'local' 子命令,而不是用 bash 直接执行。子命令: connect / local / exec / read / send / status / disconnect / help。用 'help' 查看完整用法。可能长时间运行、需要 stdin 或交互的命令:用默认提交(立即返回)再用 status / read
|
|
62
|
+
en: "Unified terminal CLI: SSH (remote) / local shell (wsl, pwsh, bash) / container (docker exec) sessions. For ANY terminal you already have or can start locally — WSL, Docker containers, or local shells — use the 'local' subcommand INSTEAD of running commands via bash. Subcommands: connect / local / exec / read / send / upload / download / status / disconnect / help. Use 'help' for full usage. Long-running, stdin-reading or interactive commands: use the default submit (returns immediately) and then status / read. NEVER add -w to servers, watchers, tail -f, builds, installs, scans, REPLs or commands waiting for input: -w blocks until completion and can hang. upload/download move regular files over SFTP and require an SSH session (local/container sessions are rejected).",
|
|
63
|
+
zh: "统一终端命令行工具:SSH(远程)/ 本地 shell(wsl、pwsh、bash)/ 容器(docker exec)会话。对任何本地可启动的终端——WSL、Docker 容器或本地 shell——应使用 'local' 子命令,而不是用 bash 直接执行。子命令: connect / local / exec / read / send / upload / download / status / disconnect / help。用 'help' 查看完整用法。可能长时间运行、需要 stdin 或交互的命令:用默认提交(立即返回)再用 status / read。禁止对 server/watch/tail -f/构建/安装/扫描/REPL/等待输入的命令加 -w:-w 会一直等到结束,可能永久阻塞。upload/download 通过 SFTP 传普通文件,仅支持 SSH 会话(本地/容器会话会明确拒绝)。",
|
|
64
64
|
},
|
|
65
65
|
term_cli_args: {
|
|
66
|
-
en: "Full command line string, e.g. 'local \"wsl bash\"' / 'exec \"ls -la\"
|
|
67
|
-
zh: "完整命令行字符串,如 'local \"wsl bash\"' / 'exec \"ls -la\"
|
|
66
|
+
en: "Full command line string, e.g. 'local \"wsl bash\"' / 'exec \"ls -la\"' / 'connect user@host' / 'upload ./a.txt ~/a.txt'; defaults to 'help' when empty.",
|
|
67
|
+
zh: "完整命令行字符串,如 'local \"wsl bash\"' / 'exec \"ls -la\"' / 'connect user@host' / 'upload ./a.txt ~/a.txt';空时默认 'help'。",
|
|
68
68
|
},
|
|
69
69
|
term_cli_help: {
|
|
70
70
|
en: `term_cli unified CLI tool: manage SSH / local (wsl, pwsh, bash) / container (docker exec) terminal sessions.
|
|
@@ -79,11 +79,18 @@ Subcommands:
|
|
|
79
79
|
local "<command>" [-n name] [-c cwd]
|
|
80
80
|
start a local/container terminal (e.g. "pwsh" / "docker exec -it <container> sh")
|
|
81
81
|
exec "<command>" [--base64 | --file <path>] [-n name] [-w]
|
|
82
|
-
run a command in the terminal (default: submit and return immediately without waiting for the result; -w waits until the command actually finishes — use it only for commands expected to finish quickly; for long-running / stdin-reading / interactive
|
|
82
|
+
run a command in the terminal (default: submit and return immediately without waiting for the result; -w waits until the command actually finishes — use it only for commands expected to finish quickly; NEVER use -w for long-running / stdin-reading / interactive / server / watch / tail -f / build / install / scan / REPL commands, because -w keeps waiting as long as the command runs and can hang indefinitely; use the default and poll with status/read)
|
|
83
83
|
read [-n name] [-s buffer|history] [-l limit] [--head] [--includeCommand]
|
|
84
84
|
read terminal output: buffer (current snapshot, consumed) / history (default tail last N)
|
|
85
85
|
send "<text>" [--base64 | --file <path>] [-n name]
|
|
86
86
|
send text/keystrokes (\\xHH any hex byte, \\t Tab, \\r/\\n Enter, \\x03 Ctrl-C, \\x04 Ctrl-D, \\x1a Ctrl-Z, \\x1b Esc)
|
|
87
|
+
upload <local> <remote> [-n name] [--overwrite] [--mkdir]
|
|
88
|
+
upload a regular file from this machine (plugin host) to the SSH session over SFTP
|
|
89
|
+
local supports ~; remote supports absolute or home-relative paths and ~/ (expanded by SFTP, no shell; ~user unsupported); Windows drive paths must use forward slashes (C:/dir/file)
|
|
90
|
+
fails if the target exists unless --overwrite; parents are not created unless --mkdir
|
|
91
|
+
SSH sessions only; directories and symlinks are rejected; max 256 MiB
|
|
92
|
+
download <remote> <local> [-n name] [--overwrite] [--mkdir]
|
|
93
|
+
download a regular file over SFTP; same path/overwrite/mkdir/size rules
|
|
87
94
|
status [-n name]
|
|
88
95
|
terminal state (busy/pending/connection); omit name to list all
|
|
89
96
|
disconnect [-n name]
|
|
@@ -101,12 +108,14 @@ Examples:
|
|
|
101
108
|
term_cli connect root@192.168.1.10
|
|
102
109
|
term_cli connect user@host -p file:C:\\secrets\\pw.txt
|
|
103
110
|
term_cli local "docker exec -it myctr bash" -n db
|
|
104
|
-
term_cli exec "ls -la"
|
|
111
|
+
term_cli exec "ls -la"
|
|
105
112
|
term_cli exec "cd /var/log && tail -50 syslog"
|
|
106
113
|
term_cli exec -- "-x"
|
|
107
114
|
term_cli exec --base64 "bHMK"
|
|
108
115
|
term_cli send --file ./payload.txt -n db
|
|
109
116
|
term_cli send "mypass\\r" -n default
|
|
117
|
+
term_cli upload ./dist/app.tgz /opt/app/app.tgz --overwrite
|
|
118
|
+
term_cli download ~/logs/app.log ./app.log --mkdir
|
|
110
119
|
term_cli read -n db -s buffer
|
|
111
120
|
term_cli status
|
|
112
121
|
term_cli disconnect -n db
|
|
@@ -123,11 +132,18 @@ Examples:
|
|
|
123
132
|
local "<command>" [-n name] [-c cwd]
|
|
124
133
|
启动本地/容器终端(如 "pwsh" / "docker exec -it <容器> sh")
|
|
125
134
|
exec "<command>" [--base64 | --file <path>] [-n name] [-w]
|
|
126
|
-
在指定终端执行命令(默认:提交后立即返回、不等结果;-w 等到命令真正结束 ——
|
|
135
|
+
在指定终端执行命令(默认:提交后立即返回、不等结果;-w 等到命令真正结束 —— 只适用于预期很快结束的命令;**禁止**对长时间运行 / 需要 stdin / 交互 / server / watch / tail -f / 构建 / 安装 / 扫描 / REPL 命令加 -w:只要命令没结束 -w 就会一直等,可能永久阻塞;这些命令请用默认模式,再用 status/read 轮询)
|
|
127
136
|
read [-n name] [-s buffer|history] [-l limit] [--head] [--includeCommand]
|
|
128
137
|
读终端输出:buffer 实时快照(读取后清空)/ history 历史快照(默认 tail 后 N 条)
|
|
129
138
|
send "<text>" [--base64 | --file <path>] [-n name]
|
|
130
139
|
发送文本/按键(\\xHH 任意十六进制字节、\\t Tab、\\r/\\n 回车、\\x03 Ctrl-C、\\x04 Ctrl-D、\\x1a Ctrl-Z、\\x1b Esc)
|
|
140
|
+
upload <local> <remote> [-n name] [--overwrite] [--mkdir]
|
|
141
|
+
通过 SFTP 把本机(插件进程)普通文件上传到 SSH 会话
|
|
142
|
+
local 支持 ~;remote 支持绝对路径、相对 home 路径与 ~/(由 SFTP 展开,不经过 shell;不支持 ~user);Windows 盘符路径必须用正斜杠(C:/dir/file)
|
|
143
|
+
目标已存在默认失败,需加 --overwrite;默认不建父目录,需加 --mkdir
|
|
144
|
+
仅支持 SSH 会话;目录与符号链接会被拒绝;单文件上限 256 MiB
|
|
145
|
+
download <remote> <local> [-n name] [--overwrite] [--mkdir]
|
|
146
|
+
通过 SFTP 下载普通文件;路径/覆盖/建目录/大小规则相同
|
|
131
147
|
status [-n name]
|
|
132
148
|
终端状态(busy/pending/连接);省略 name 列出全部
|
|
133
149
|
disconnect [-n name]
|
|
@@ -145,12 +161,14 @@ Examples:
|
|
|
145
161
|
term_cli connect root@192.168.1.10
|
|
146
162
|
term_cli connect user@host -p file:C:\\secrets\\pw.txt
|
|
147
163
|
term_cli local "docker exec -it myctr bash" -n db
|
|
148
|
-
term_cli exec "ls -la"
|
|
164
|
+
term_cli exec "ls -la"
|
|
149
165
|
term_cli exec "cd /var/log && tail -50 syslog"
|
|
150
166
|
term_cli exec -- "-x"
|
|
151
167
|
term_cli exec --base64 "bHMK"
|
|
152
168
|
term_cli send --file ./payload.txt -n db
|
|
153
169
|
term_cli send "mypass\\r" -n default
|
|
170
|
+
term_cli upload ./dist/app.tgz /opt/app/app.tgz --overwrite
|
|
171
|
+
term_cli download ~/logs/app.log ./app.log --mkdir
|
|
154
172
|
term_cli read -n db -s buffer
|
|
155
173
|
term_cli status
|
|
156
174
|
term_cli disconnect -n db
|
|
@@ -208,6 +226,90 @@ Examples:
|
|
|
208
226
|
en: "Cannot read file: {path}",
|
|
209
227
|
zh: "无法读取文件:{path}",
|
|
210
228
|
},
|
|
229
|
+
cli_need_transfer_paths: {
|
|
230
|
+
en: "Need a local path and a remote path",
|
|
231
|
+
zh: "需要本地路径和远端路径",
|
|
232
|
+
},
|
|
233
|
+
cli_upload_usage: {
|
|
234
|
+
en: "Usage: upload <local> <remote> [-n name] [--overwrite] [--mkdir]",
|
|
235
|
+
zh: "用法: upload <local> <remote> [-n name] [--overwrite] [--mkdir]",
|
|
236
|
+
},
|
|
237
|
+
cli_download_usage: {
|
|
238
|
+
en: "Usage: download <remote> <local> [-n name] [--overwrite] [--mkdir]",
|
|
239
|
+
zh: "用法: download <remote> <local> [-n name] [--overwrite] [--mkdir]",
|
|
240
|
+
},
|
|
241
|
+
upload_ok: {
|
|
242
|
+
en: "Uploaded {local} -> {remote} ({bytes} bytes, {ms} ms)",
|
|
243
|
+
zh: "已上传 {local} -> {remote}({bytes} 字节,{ms} ms)",
|
|
244
|
+
},
|
|
245
|
+
download_ok: {
|
|
246
|
+
en: "Downloaded {remote} -> {local} ({bytes} bytes, {ms} ms)",
|
|
247
|
+
zh: "已下载 {remote} -> {local}({bytes} 字节,{ms} ms)",
|
|
248
|
+
},
|
|
249
|
+
transfer_denied: {
|
|
250
|
+
en: "Transfer denied by user.",
|
|
251
|
+
zh: "用户已拒绝传输。",
|
|
252
|
+
},
|
|
253
|
+
transfer_err_exists: {
|
|
254
|
+
en: "{path} already exists. Add --overwrite to replace it (no auto-rename/backup).",
|
|
255
|
+
zh: "{path} 已存在。需要覆盖请加 --overwrite(不会自动改名或备份)。",
|
|
256
|
+
},
|
|
257
|
+
transfer_err_local_missing: {
|
|
258
|
+
en: "Local file not found: {path}",
|
|
259
|
+
zh: "本地文件不存在:{path}",
|
|
260
|
+
},
|
|
261
|
+
transfer_err_local_is_dir: {
|
|
262
|
+
en: "Local path is a directory (only regular files are supported): {path}",
|
|
263
|
+
zh: "本地路径是目录(只支持普通文件):{path}",
|
|
264
|
+
},
|
|
265
|
+
transfer_err_remote_missing: {
|
|
266
|
+
en: "Remote file not found: {path}",
|
|
267
|
+
zh: "远端文件不存在:{path}",
|
|
268
|
+
},
|
|
269
|
+
transfer_err_remote_is_dir: {
|
|
270
|
+
en: "Remote path is a directory (only regular files are supported): {path}",
|
|
271
|
+
zh: "远端路径是目录(只支持普通文件):{path}",
|
|
272
|
+
},
|
|
273
|
+
transfer_err_symlink: {
|
|
274
|
+
en: "Symbolic links are not supported: {path}",
|
|
275
|
+
zh: "不支持符号链接:{path}",
|
|
276
|
+
},
|
|
277
|
+
transfer_err_parent_missing: {
|
|
278
|
+
en: "Parent directory does not exist: {path}. Add --mkdir to create it.",
|
|
279
|
+
zh: "父目录不存在:{path}。需要创建请加 --mkdir。",
|
|
280
|
+
},
|
|
281
|
+
transfer_err_too_large: {
|
|
282
|
+
en: "File is too large for v1 transfer: {path} (limit {limit}).",
|
|
283
|
+
zh: "文件超过 v1 传输上限:{path}(上限 {limit})。",
|
|
284
|
+
},
|
|
285
|
+
transfer_err_sftp_unavailable: {
|
|
286
|
+
en: "SFTP is unavailable on this SSH session. Reconnect with term_cli connect and retry.",
|
|
287
|
+
zh: "该 SSH 会话的 SFTP 不可用。请用 term_cli connect 重新连接后重试。",
|
|
288
|
+
},
|
|
289
|
+
transfer_err_timeout: {
|
|
290
|
+
en: "Transfer timed out ({seconds}s). The SFTP channel was aborted; no new rename/commit is started, but an already-issued rename may still run. A partially written file may remain at the target path or in a .part/.bak temp file.",
|
|
291
|
+
zh: "传输超时({seconds}s)。已中止 SFTP 通道,不再主动发起新的 rename/commit,但已发出的 rename 仍可能被执行;目标路径或 `.part`/`.bak` 临时文件可能残留半截内容。",
|
|
292
|
+
},
|
|
293
|
+
transfer_err_io: {
|
|
294
|
+
en: "Transfer failed: {reason}",
|
|
295
|
+
zh: "传输失败:{reason}",
|
|
296
|
+
},
|
|
297
|
+
transfer_err_unsupported: {
|
|
298
|
+
en: "Only regular files are supported (not {path}).",
|
|
299
|
+
zh: "只支持普通文件({path} 不是普通文件)。",
|
|
300
|
+
},
|
|
301
|
+
transfer_err_tilde_user: {
|
|
302
|
+
en: "~user paths are not supported; use ~/ (current user) or an absolute path: {path}",
|
|
303
|
+
zh: "不支持 ~user 路径;请用 ~/(当前用户)或绝对路径:{path}",
|
|
304
|
+
},
|
|
305
|
+
transfer_err_remote_path_backslash: {
|
|
306
|
+
en: "Remote Windows paths must use forward slashes (e.g. C:/dir/file); backslash drive paths are rejected: {path}",
|
|
307
|
+
zh: "远端 Windows 路径必须用正斜杠(如 C:/dir/file);反斜杠盘符路径已拒绝:{path}",
|
|
308
|
+
},
|
|
309
|
+
transfer_err_ssh_only: {
|
|
310
|
+
en: "File transfer is only supported on SSH sessions (term_cli connect); local/container sessions cannot transfer files.",
|
|
311
|
+
zh: "文件传输仅支持 SSH 会话(term_cli connect);本地/容器会话不能传文件。",
|
|
312
|
+
},
|
|
211
313
|
cli_unknown: {
|
|
212
314
|
en: "Unknown command: {cmd}",
|
|
213
315
|
zh: "未知命令: {cmd}",
|
|
@@ -299,10 +401,18 @@ Examples:
|
|
|
299
401
|
zh: "终端历史(前 {n} 条 / 共 {total} 条)",
|
|
300
402
|
},
|
|
301
403
|
web_title: { en: "Terminal Records", zh: "终端记录" },
|
|
404
|
+
web_close: { en: "Close", zh: "关闭" },
|
|
302
405
|
web_loading: { en: "Loading...", zh: "加载中..." },
|
|
303
406
|
web_session_gone: { en: "Session not found or disconnected", zh: "会话不存在或已断开" },
|
|
304
407
|
web_time: { en: "Time", zh: "时间" },
|
|
305
408
|
web_terminals: { en: "terminals", zh: "终端" },
|
|
409
|
+
web_transfers: { en: "Transfers", zh: "传输任务" },
|
|
410
|
+
web_transfers_empty: { en: "No transfers for this terminal.", zh: "当前终端暂无传输任务。" },
|
|
411
|
+
web_transfer_upload: { en: "Upload", zh: "上传" },
|
|
412
|
+
web_transfer_download: { en: "Download", zh: "下载" },
|
|
413
|
+
web_transfer_running: { en: "running", zh: "传输中" },
|
|
414
|
+
web_transfer_done: { en: "done", zh: "完成" },
|
|
415
|
+
web_transfer_failed: { en: "failed", zh: "失败" },
|
|
306
416
|
web_local: { en: "local", zh: "本地" },
|
|
307
417
|
web_commands: { en: "commands", zh: "条命令" },
|
|
308
418
|
web_no_session: { en: "No terminal. Use term_cli connect (SSH) or term_cli local first.", zh: "无终端会话,请先用 term_cli connect(SSH)或 term_cli local 建立。" },
|
package/dist/i18n.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,6DAA6D;AAE7D,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAOvD;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,GAAuB,EAAE,UAAgB;IACzD,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,GAAG,CAAA;IAC5C,OAAO,UAAU,CAAA;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,UAAU,GAAS,IAAI;IACjD,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,CAAA;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,UAAU,GAAS,IAAI;IAChD,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,UAAU,CAAC,CAAA;AACxD,CAAC;AAED,kBAAkB;AAClB,MAAM,CAAC,MAAM,CAAC,GAAG;IACf,SAAS,EAAE;QACT,EAAE,EAAE,0LAA0L;QAC9L,EAAE,EAAE,6EAA6E;KAClF;IACD,OAAO,EAAE;QACP,EAAE,EAAE,cAAc;QAClB,EAAE,EAAE,YAAY;KACjB;IACD,cAAc,EAAE;QACd,EAAE,EAAE,iDAAiD;QACrD,EAAE,EAAE,iCAAiC;KACtC;IACD,gBAAgB,EAAE;QAChB,EAAE,EAAE,kCAAkC;QACtC,EAAE,EAAE,sBAAsB;KAC3B;IACD,wBAAwB,EAAE;QACxB,EAAE,EAAE,6BAA6B;QACjC,EAAE,EAAE,UAAU;KACf;IACD,iBAAiB,EAAE;QACjB,EAAE,EAAE,6IAA6I;QACjJ,EAAE,EAAE,uFAAuF;KAC5F;IACD,qBAAqB,EAAE;QACrB,EAAE,EAAE,gGAAgG;QACpG,EAAE,EAAE,6DAA6D;KAClE;IACD,QAAQ,EAAE;QACR,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAwRvD,eAAO,MAAM,eAAe,EAAE,MA8nB7B,CAAA;eAGc,eAAe"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// 插件入口:导出 5 个 SSH 工具 + 会话生命周期管理 + HTTP 终端记录服务
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import log, { describeSend, infoDedupShared, sweepDedupDir } from "./log.js";
|
|
5
6
|
import { parseCliArgs } from "./cli-args.js";
|
|
6
7
|
import { PayloadError, normalizeExecText, resolvePayload } from "./payload.js";
|
|
7
|
-
import { CACHE_DIR } from "./constants.js";
|
|
8
|
+
import { CACHE_DIR, MAX_TRANSFER_BYTES, TRANSFER_PROGRESS_THROTTLE_MS, TRANSFER_TIMEOUT_MS } from "./constants.js";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
import { rmSync } from "node:fs";
|
|
@@ -13,8 +14,9 @@ import { SessionHistory } from "./history.js";
|
|
|
13
14
|
import { T, getToolLang, getWebLang, tr } from "./i18n.js";
|
|
14
15
|
import { toModelText } from "./utils.js";
|
|
15
16
|
import { createDecider } from "./permission.js";
|
|
16
|
-
import { SshSession } from "./session.js";
|
|
17
|
+
import { runSshTransfer, SshSession } from "./session.js";
|
|
17
18
|
import { LocalSession } from "./local-session.js";
|
|
19
|
+
import { TransferError, parseTransferPositionals } from "./transfer.js";
|
|
18
20
|
import {} from "./server.js";
|
|
19
21
|
import { ensureServer } from "./server-manager.js";
|
|
20
22
|
import { startAgent } from "./agent.js";
|
|
@@ -24,6 +26,25 @@ import { precheckBeforeExec } from "./precheck.js";
|
|
|
24
26
|
const DEFAULT_NAME = "default";
|
|
25
27
|
/** 终端名合法字符(字母/数字/下划线/中划线/点),防止路径穿越 */
|
|
26
28
|
const NAME_RE = /^[A-Za-z0-9_.-]+$/;
|
|
29
|
+
/** 传输错误码 → i18n 文案键(展示文案不在 transfer.ts 中硬编码) */
|
|
30
|
+
const TRANSFER_ERROR_KEYS = {
|
|
31
|
+
exists: "transfer_err_exists",
|
|
32
|
+
local_missing: "transfer_err_local_missing",
|
|
33
|
+
local_is_dir: "transfer_err_local_is_dir",
|
|
34
|
+
remote_missing: "transfer_err_remote_missing",
|
|
35
|
+
remote_is_dir: "transfer_err_remote_is_dir",
|
|
36
|
+
symlink: "transfer_err_symlink",
|
|
37
|
+
parent_missing: "transfer_err_parent_missing",
|
|
38
|
+
too_large: "transfer_err_too_large",
|
|
39
|
+
sftp_unavailable: "transfer_err_sftp_unavailable",
|
|
40
|
+
timeout: "transfer_err_timeout",
|
|
41
|
+
io: "transfer_err_io",
|
|
42
|
+
unsupported: "transfer_err_unsupported",
|
|
43
|
+
ssh_only: "transfer_err_ssh_only",
|
|
44
|
+
denied: "transfer_denied",
|
|
45
|
+
tilde_user: "transfer_err_tilde_user",
|
|
46
|
+
remote_path_backslash: "transfer_err_remote_path_backslash",
|
|
47
|
+
};
|
|
27
48
|
/**
|
|
28
49
|
* 校验并规整终端名(防路径穿越:拒绝 ../、绝对路径、路径分隔符)
|
|
29
50
|
* @param name 原始终端名
|
|
@@ -355,8 +376,14 @@ export const OpenCodeSshTool = async () => {
|
|
|
355
376
|
catch {
|
|
356
377
|
return tr("rejected_connect", toolLang);
|
|
357
378
|
}
|
|
358
|
-
|
|
359
|
-
|
|
379
|
+
// 先占住会话表再 await 连接:两个不同名终端并行 connect 时,
|
|
380
|
+
// 不能各自在 await 后 new Map() 覆盖对方(会丢终端并触发状态文件孤儿清理)。
|
|
381
|
+
let map = getSessionMap(ctx.sessionID);
|
|
382
|
+
if (!map) {
|
|
383
|
+
map = new Map();
|
|
384
|
+
sshSessions.set(ctx.sessionID, map);
|
|
385
|
+
}
|
|
386
|
+
const old = map.get(name);
|
|
360
387
|
if (old) {
|
|
361
388
|
old.close();
|
|
362
389
|
map.delete(name);
|
|
@@ -368,9 +395,7 @@ export const OpenCodeSshTool = async () => {
|
|
|
368
395
|
session.close();
|
|
369
396
|
return `${tr("connect_title_fail", toolLang)}: ${result.error ?? tr("unknown_error", toolLang)}`;
|
|
370
397
|
}
|
|
371
|
-
|
|
372
|
-
sshSessions.set(ctx.sessionID, new Map());
|
|
373
|
-
sshSessions.get(ctx.sessionID).set(name, session);
|
|
398
|
+
map.set(name, session);
|
|
374
399
|
hookBusyState(session, ctx.sessionID, name);
|
|
375
400
|
syncSessionState(ctx.sessionID, name, "connect");
|
|
376
401
|
refreshAgent();
|
|
@@ -385,8 +410,13 @@ export const OpenCodeSshTool = async () => {
|
|
|
385
410
|
const name = values.name ?? DEFAULT_NAME;
|
|
386
411
|
if (sanitizeName(name) === null)
|
|
387
412
|
return tr("invalid_name", toolLang);
|
|
388
|
-
|
|
389
|
-
|
|
413
|
+
// 同样先占住会话表,避免并行 local 连接互相覆盖。
|
|
414
|
+
let map = getLocalSessionMap(ctx.sessionID);
|
|
415
|
+
if (!map) {
|
|
416
|
+
map = new Map();
|
|
417
|
+
localSessions.set(ctx.sessionID, map);
|
|
418
|
+
}
|
|
419
|
+
const old = map.get(name);
|
|
390
420
|
if (old) {
|
|
391
421
|
old.close();
|
|
392
422
|
map.delete(name);
|
|
@@ -398,9 +428,7 @@ export const OpenCodeSshTool = async () => {
|
|
|
398
428
|
session.close();
|
|
399
429
|
return `${tr("local_connect_title_fail", toolLang)}: ${result.error ?? tr("unknown_error", toolLang)}`;
|
|
400
430
|
}
|
|
401
|
-
|
|
402
|
-
localSessions.set(ctx.sessionID, new Map());
|
|
403
|
-
localSessions.get(ctx.sessionID).set(name, session);
|
|
431
|
+
map.set(name, session);
|
|
404
432
|
hookBusyState(session, ctx.sessionID, name);
|
|
405
433
|
syncSessionState(ctx.sessionID, name, "local");
|
|
406
434
|
refreshAgent();
|
|
@@ -546,6 +574,201 @@ export const OpenCodeSshTool = async () => {
|
|
|
546
574
|
return tr("send_ok", toolLang).replace("{text}", text.slice(0, 60));
|
|
547
575
|
return tr("send_ok_source", toolLang).replace("{source}", payload.source).replace("{length}", String(text.length));
|
|
548
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* 传输错误 → 本地化可操作文案(路径/上限/超时秒数由常量填充,不硬编码)
|
|
579
|
+
* @param err 传输错误
|
|
580
|
+
* @returns 本地化文案
|
|
581
|
+
*/
|
|
582
|
+
function transferErrorText(err) {
|
|
583
|
+
const template = tr(TRANSFER_ERROR_KEYS[err.code], toolLang);
|
|
584
|
+
const limitMiB = Math.round(MAX_TRANSFER_BYTES / (1024 * 1024));
|
|
585
|
+
return template
|
|
586
|
+
.replace("{path}", err.path ?? "-")
|
|
587
|
+
.replace("{reason}", err.detail ?? "")
|
|
588
|
+
.replace("{limit}", `${limitMiB} MiB`)
|
|
589
|
+
.replace("{seconds}", String(Math.round(TRANSFER_TIMEOUT_MS / 1000)));
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* upload / download 共用实现:解析参数 → 仅 SSH 会话 → 权限审批 → SFTP 传输 → 回执
|
|
593
|
+
* 传输内容不进入工具输出/日志;日志只记方向、路径、字节、耗时、ok、错误码。
|
|
594
|
+
* @param direction 传输方向
|
|
595
|
+
* @param args 子命令参数
|
|
596
|
+
* @param ctx 执行上下文
|
|
597
|
+
* @returns 回执或错误文案
|
|
598
|
+
*/
|
|
599
|
+
async function doTransfer(direction, args, ctx) {
|
|
600
|
+
const usage = tr(direction === "upload" ? "cli_upload_usage" : "cli_download_usage", toolLang);
|
|
601
|
+
const { values, positionals } = parseArgs({ args, options: { name: { type: "string", short: "n" }, overwrite: { type: "boolean" }, mkdir: { type: "boolean" } }, allowPositionals: true });
|
|
602
|
+
const pair = parseTransferPositionals(positionals);
|
|
603
|
+
if (!pair)
|
|
604
|
+
return `${tr("cli_need_transfer_paths", toolLang)}\n${usage}\n\n${HELP_TEXT}`;
|
|
605
|
+
const { first, second } = pair;
|
|
606
|
+
const name = values.name ?? DEFAULT_NAME;
|
|
607
|
+
if (sanitizeName(name) === null)
|
|
608
|
+
return tr("invalid_name", toolLang);
|
|
609
|
+
const session = resolveSession(ctx.sessionID, name);
|
|
610
|
+
if (!session)
|
|
611
|
+
return tr("not_connected", toolLang);
|
|
612
|
+
const permission = direction === "upload" ? "file_upload" : "file_download";
|
|
613
|
+
const overwrite = values.overwrite === true;
|
|
614
|
+
const mkdir = values.mkdir === true;
|
|
615
|
+
const logKey = direction === "upload" ? "file_upload" : "file_download";
|
|
616
|
+
/** 当前传输任务(审批通过、run 开始时创建;拒绝/非 SSH 不创建,因此不会被伪造上报) */
|
|
617
|
+
let transferTask = null;
|
|
618
|
+
/** 最近一次进度回调值(字节/总数),完成或失败时用于补齐最终值 */
|
|
619
|
+
let lastBytes = 0;
|
|
620
|
+
let lastTotal = 0;
|
|
621
|
+
/** 进度节流的尾沿定时器(窗口内多次回调合并为最新一帧) */
|
|
622
|
+
let progressTimer = null;
|
|
623
|
+
/** 上次实际发出进度事件的时刻(毫秒) */
|
|
624
|
+
let lastEmitAt = 0;
|
|
625
|
+
/** 发送传输任务元数据(无 agent/未连接时静默丢弃;只发元数据,不含内容) */
|
|
626
|
+
const emitTransfer = (task) => { agent?.transfer(task); };
|
|
627
|
+
/** 清除待发的进度尾沿定时器 */
|
|
628
|
+
const clearProgressTimer = () => {
|
|
629
|
+
if (progressTimer) {
|
|
630
|
+
clearTimeout(progressTimer);
|
|
631
|
+
progressTimer = null;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
/** 立即发送当前进度(以最新回调值为准) */
|
|
635
|
+
const flushProgress = () => {
|
|
636
|
+
progressTimer = null;
|
|
637
|
+
if (!transferTask || transferTask.state !== "running")
|
|
638
|
+
return;
|
|
639
|
+
lastEmitAt = Date.now();
|
|
640
|
+
emitTransfer({ ...transferTask, bytes: lastBytes, total: lastTotal });
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* 进度回调(由 transfer.ts 在每块数据后调用):按 TRANSFER_PROGRESS_THROTTLE_MS 节流;
|
|
644
|
+
* 首次超过窗口立即发,否则挂一个尾沿定时器,保证窗口内最新值最终被发出。
|
|
645
|
+
* @param bytes 已传字节数
|
|
646
|
+
* @param total 总字节数
|
|
647
|
+
*/
|
|
648
|
+
const onProgress = (bytes, total) => {
|
|
649
|
+
lastBytes = bytes;
|
|
650
|
+
lastTotal = total;
|
|
651
|
+
if (!transferTask || transferTask.state !== "running")
|
|
652
|
+
return;
|
|
653
|
+
const elapsed = Date.now() - lastEmitAt;
|
|
654
|
+
if (elapsed >= TRANSFER_PROGRESS_THROTTLE_MS) {
|
|
655
|
+
flushProgress();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (progressTimer === null) {
|
|
659
|
+
progressTimer = setTimeout(flushProgress, TRANSFER_PROGRESS_THROTTLE_MS - elapsed);
|
|
660
|
+
if (typeof progressTimer.unref === "function")
|
|
661
|
+
progressTimer.unref();
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
/** 取当前未结束的任务(跨异步回调读取,避免 TS 把闭包内赋值的变量误窄化为 null) */
|
|
665
|
+
const runningTask = () => transferTask && transferTask.state === "running" ? transferTask : null;
|
|
666
|
+
try {
|
|
667
|
+
// runSshTransfer 固定顺序:instanceof SshSession → ctx.ask → 实际传输
|
|
668
|
+
const result = await runSshTransfer({
|
|
669
|
+
session,
|
|
670
|
+
ask: () => ctx.ask({
|
|
671
|
+
permission,
|
|
672
|
+
patterns: [first, second],
|
|
673
|
+
// overwrite/mkdir 参与 always 键:避免"总是允许"普通上传后,同路径 --overwrite 不再询问
|
|
674
|
+
always: [`${permission}:${first}:${second}:ow=${overwrite ? 1 : 0}:mkdir=${mkdir ? 1 : 0}`],
|
|
675
|
+
metadata: { title: `${first} -> ${second}`, overwrite, mkdir },
|
|
676
|
+
}),
|
|
677
|
+
run: async (ssh) => {
|
|
678
|
+
// 审批通过、真正开始传输时才建任务并上报 running(拒绝/非 SSH 不会走到这里)
|
|
679
|
+
const startedAt = Date.now();
|
|
680
|
+
transferTask = {
|
|
681
|
+
id: randomUUID(),
|
|
682
|
+
sessionID: ctx.sessionID,
|
|
683
|
+
name,
|
|
684
|
+
direction,
|
|
685
|
+
localPath: direction === "upload" ? first : second,
|
|
686
|
+
remotePath: direction === "upload" ? second : first,
|
|
687
|
+
state: "running",
|
|
688
|
+
bytes: 0,
|
|
689
|
+
total: 0,
|
|
690
|
+
startedAt,
|
|
691
|
+
};
|
|
692
|
+
lastBytes = 0;
|
|
693
|
+
lastTotal = 0;
|
|
694
|
+
lastEmitAt = startedAt;
|
|
695
|
+
emitTransfer({ ...transferTask });
|
|
696
|
+
return direction === "upload"
|
|
697
|
+
? ssh.uploadFile({ localPath: first, remotePath: second, overwrite, mkdir, onProgress })
|
|
698
|
+
: ssh.downloadFile({ remotePath: first, localPath: second, overwrite, mkdir, onProgress });
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
const activeTask = runningTask();
|
|
702
|
+
if (activeTask) {
|
|
703
|
+
clearProgressTimer();
|
|
704
|
+
// 完成补最终值:路径/字节取实际结果;total 优先用进度回调拿到的 stat 大小
|
|
705
|
+
const finished = {
|
|
706
|
+
...activeTask,
|
|
707
|
+
localPath: result.localPath,
|
|
708
|
+
remotePath: result.remotePath,
|
|
709
|
+
state: "done",
|
|
710
|
+
bytes: result.bytes,
|
|
711
|
+
total: lastTotal || result.bytes,
|
|
712
|
+
endedAt: Date.now(),
|
|
713
|
+
};
|
|
714
|
+
transferTask = finished;
|
|
715
|
+
emitTransfer(finished);
|
|
716
|
+
}
|
|
717
|
+
log.tool(logKey, {
|
|
718
|
+
sessionID: ctx.sessionID,
|
|
719
|
+
name,
|
|
720
|
+
local: result.localPath,
|
|
721
|
+
remote: result.remotePath,
|
|
722
|
+
bytes: result.bytes,
|
|
723
|
+
duration: result.duration,
|
|
724
|
+
ok: true,
|
|
725
|
+
});
|
|
726
|
+
return tr(direction === "upload" ? "upload_ok" : "download_ok", toolLang)
|
|
727
|
+
.replace("{local}", result.localPath)
|
|
728
|
+
.replace("{remote}", result.remotePath)
|
|
729
|
+
.replace("{bytes}", String(result.bytes))
|
|
730
|
+
.replace("{ms}", String(result.duration));
|
|
731
|
+
}
|
|
732
|
+
catch (e) {
|
|
733
|
+
const err = e instanceof TransferError ? e : new TransferError("io", undefined, e instanceof Error ? e.message : String(e));
|
|
734
|
+
const activeTask = runningTask();
|
|
735
|
+
if (activeTask) {
|
|
736
|
+
clearProgressTimer();
|
|
737
|
+
const failed = {
|
|
738
|
+
...activeTask,
|
|
739
|
+
state: "failed",
|
|
740
|
+
bytes: lastBytes,
|
|
741
|
+
total: lastTotal,
|
|
742
|
+
endedAt: Date.now(),
|
|
743
|
+
errorCode: err.code,
|
|
744
|
+
};
|
|
745
|
+
transferTask = failed;
|
|
746
|
+
emitTransfer(failed);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
clearProgressTimer();
|
|
750
|
+
}
|
|
751
|
+
// 失败详情(含回滚失败时的 .bak 路径)同时进日志,便于事后定位
|
|
752
|
+
log.tool(logKey, {
|
|
753
|
+
sessionID: ctx.sessionID,
|
|
754
|
+
name,
|
|
755
|
+
local: first,
|
|
756
|
+
remote: second,
|
|
757
|
+
ok: false,
|
|
758
|
+
code: err.code,
|
|
759
|
+
...(err.detail ? { reason: err.detail } : {}),
|
|
760
|
+
});
|
|
761
|
+
return transferErrorText(err);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/** upload:本机文件 → SSH 远端(SFTP) */
|
|
765
|
+
function doUpload(args, ctx) {
|
|
766
|
+
return doTransfer("upload", args, ctx);
|
|
767
|
+
}
|
|
768
|
+
/** download:SSH 远端 → 本机(SFTP) */
|
|
769
|
+
function doDownload(args, ctx) {
|
|
770
|
+
return doTransfer("download", args, ctx);
|
|
771
|
+
}
|
|
549
772
|
/** status:终端状态 */
|
|
550
773
|
async function doStatus(args, ctx) {
|
|
551
774
|
const { values } = parseArgs({ args, options: { name: { type: "string", short: "n" } }, allowPositionals: true });
|
|
@@ -638,6 +861,8 @@ export const OpenCodeSshTool = async () => {
|
|
|
638
861
|
case "exec": return doExec(rest, rootCtx);
|
|
639
862
|
case "read": return doRead(rest, rootCtx);
|
|
640
863
|
case "send": return doSend(rest, rootCtx);
|
|
864
|
+
case "upload": return doUpload(rest, rootCtx);
|
|
865
|
+
case "download": return doDownload(rest, rootCtx);
|
|
641
866
|
case "status": return doStatus(rest, rootCtx);
|
|
642
867
|
case "disconnect": return doDisconnect(rest, rootCtx);
|
|
643
868
|
default: return `${tr("cli_unknown", toolLang).replace("{cmd}", cmd)}\n\n${HELP_TEXT}`;
|