@xiaoqiong0v0/opencode-ssh-tool 0.4.0 → 0.5.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 +22 -22
- package/dist/base-session.d.ts +128 -0
- package/dist/base-session.d.ts.map +1 -0
- package/dist/base-session.js +414 -0
- package/dist/base-session.js.map +1 -0
- package/dist/constants.d.ts +27 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +28 -3
- package/dist/constants.js.map +1 -1
- package/dist/history.d.ts +14 -5
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +25 -9
- package/dist/history.js.map +1 -1
- package/dist/i18n.d.ts +75 -227
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +65 -211
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -31
- package/dist/index.js.map +1 -1
- package/dist/last-match.d.ts +17 -0
- package/dist/last-match.d.ts.map +1 -0
- package/dist/last-match.js +63 -0
- package/dist/last-match.js.map +1 -0
- package/dist/local-session.d.ts +9 -91
- package/dist/local-session.d.ts.map +1 -1
- package/dist/local-session.js +46 -308
- package/dist/local-session.js.map +1 -1
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +5 -0
- package/dist/log.js.map +1 -0
- package/dist/server.d.ts +32 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +417 -349
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +15 -108
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +69 -361
- package/dist/session.js.map +1 -1
- package/dist/shell-adapter.d.ts +20 -0
- package/dist/shell-adapter.d.ts.map +1 -0
- package/dist/shell-adapter.js +70 -0
- package/dist/shell-adapter.js.map +1 -0
- package/dist/ssh-auth.d.ts.map +1 -1
- package/dist/ssh-auth.js +1 -2
- package/dist/ssh-auth.js.map +1 -1
- package/dist/utils.d.ts +25 -24
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +279 -45
- package/dist/utils.js.map +1 -1
- package/dist/web/app.css +43 -0
- package/dist/web/app.js +2 -0
- package/dist/web/fonts/CaskaydiaCoveNerdFontMono-Regular.ttf +0 -0
- package/dist/web/index.html +28 -0
- package/package.json +6 -3
package/dist/i18n.js
CHANGED
|
@@ -15,133 +15,14 @@ export function getLang(configured = "en") {
|
|
|
15
15
|
}
|
|
16
16
|
/** 工具描述与参数说明字典 */
|
|
17
17
|
export const T = {
|
|
18
|
-
ssh_connect: {
|
|
19
|
-
en: "Open a long-lived SSH connection with a PTY shell in the current session. Reconnecting the same session closes the previous connection first. Returns a connection id.",
|
|
20
|
-
zh: "在当前会话建立长驻 SSH 连接并打开 PTY shell;同一会话重复连接会先关闭旧连接。返回连接标识。",
|
|
21
|
-
},
|
|
22
|
-
ssh_connect_args: {
|
|
23
|
-
host: { en: "Target host or IP", zh: "目标主机 host / IP" },
|
|
24
|
-
user: { en: "Login username", zh: "登录用户名" },
|
|
25
|
-
port: { en: "Port, default 22", zh: "端口,默认 22" },
|
|
26
|
-
name: {
|
|
27
|
-
en: "Terminal name within the session, default 'default'. Creating with an existing name closes the old one first. Use distinct names to keep multiple terminals.",
|
|
28
|
-
zh: "会话内的终端名,默认 'default'。同名创建会先关闭旧终端。用不同名称可保持多个终端。",
|
|
29
|
-
},
|
|
30
|
-
password: {
|
|
31
|
-
en: "Password for password auth (optional). Supports plain text or 'file:<abs path>' to read from a file (avoids exposing it in context). If omitted, falls back to SSH key / agent / SSH_PASSWORD env.",
|
|
32
|
-
zh: "密码认证密码(可选)。支持明文或 'file:<绝对路径>' 从文件读取(避免暴露在上下文中)。省略时回退到 SSH 私钥 / agent / 环境变量 SSH_PASSWORD。",
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
term_exec: {
|
|
36
|
-
en: "Execute a command in an SSH, local or container terminal (by terminal name), preserving cwd and environment. Commands outside the read-only allowlist require user approval.",
|
|
37
|
-
zh: "在 SSH / 本地 / 容器终端(按终端名)执行命令,保留 cwd 与环境变量;白名单外的命令需用户审批。",
|
|
38
|
-
},
|
|
39
|
-
term_exec_args: {
|
|
40
|
-
command: { en: "Shell command to execute", zh: "要执行的 shell 命令" },
|
|
41
|
-
waitResult: {
|
|
42
|
-
en: "Wait for the command to finish and return its output. Default false: submit async, return immediately; read results later via term_read.",
|
|
43
|
-
zh: "是否等待命令完成并返回其输出。默认 false:异步提交,立即返回;稍后通过 term_read 读取结果。",
|
|
44
|
-
},
|
|
45
|
-
name: {
|
|
46
|
-
en: "Target terminal name, default 'default'. Must match a name from ssh_connect.",
|
|
47
|
-
zh: "目标终端名,默认 'default'。需与 ssh_connect 创建的名称一致。",
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
18
|
submitted: {
|
|
51
|
-
en: "Command submitted (async). Running in background: check
|
|
52
|
-
zh: "命令已异步提交,后台执行中:用
|
|
53
|
-
},
|
|
54
|
-
term_read: {
|
|
55
|
-
en: "Read output of an SSH, local or container terminal: buffered (unconsumed, for interactive/polling) or history (completed command+output pairs, first N or last N). Returns browser URL for the full scrollable viewer when the HTTP server is on.",
|
|
56
|
-
zh: "读取 SSH / 本地 / 容器终端输出:缓冲(未消费,交互/轮询用)或历史(已完成的命令+输出对,前 N 条或后 N 条)。HTTP 服务开启时返回浏览器查看完整记录的地址。",
|
|
57
|
-
},
|
|
58
|
-
term_send: {
|
|
59
|
-
en: "Send text or keystrokes to an SSH, local or container terminal for interactive prompts (sudo password, confirmations, interrupts). Escapes: \\r or \\n = Enter, \\x03 = Ctrl-C, \\x04 = Ctrl-D, \\x1a = Ctrl-Z, \\x1b = Esc. After sending, read the result with term_read.",
|
|
60
|
-
zh: "向 SSH / 本地 / 容器终端发送文本或按键,用于交互提示(sudo 密码、确认、中断等)。转义:\\r 或 \\n = 回车,\\x03 = Ctrl-C,\\x04 = Ctrl-D,\\x1a = Ctrl-Z,\\x1b = Esc。发送后用 term_read 读取结果。",
|
|
61
|
-
},
|
|
62
|
-
term_send_args: {
|
|
63
|
-
text: {
|
|
64
|
-
en: "Text or key sequence to send. Use \\r or \\n for Enter, \\x03 for Ctrl-C, \\x1b for Esc. For sudo password prompts send the password followed by \\r.",
|
|
65
|
-
zh: "要发送的文本或按键序列。回车用 \\r 或 \\n,Ctrl-C 用 \\x03,Esc 用 \\x1b。sudo 密码提示时发送密码后跟 \\r。",
|
|
66
|
-
},
|
|
67
|
-
name: {
|
|
68
|
-
en: "Target terminal name, default 'default'.",
|
|
69
|
-
zh: "目标终端名,默认 'default'。",
|
|
70
|
-
},
|
|
19
|
+
en: "Command submitted (async). Running in background: check term_cli status for completion, then term_cli read to read output.",
|
|
20
|
+
zh: "命令已异步提交,后台执行中:用 term_cli status 查完成状态,再用 term_cli read 读取输出。",
|
|
71
21
|
},
|
|
72
22
|
send_ok: {
|
|
73
23
|
en: "Sent: {text}",
|
|
74
24
|
zh: "已发送:{text}",
|
|
75
25
|
},
|
|
76
|
-
send_title: {
|
|
77
|
-
en: "SSH send",
|
|
78
|
-
zh: "SSH 发送",
|
|
79
|
-
},
|
|
80
|
-
term_read_args: {
|
|
81
|
-
source: {
|
|
82
|
-
en: "Where to read from: 'buffer' (unconsumed live output) or 'history' (completed pairs, default).",
|
|
83
|
-
zh: "读取来源:'buffer'(实时未消费输出)或 'history'(已完成消息对,默认)。",
|
|
84
|
-
},
|
|
85
|
-
lines: { en: "Max lines to read from buffer, all if omitted", zh: "读取缓冲行数上限,默认全部" },
|
|
86
|
-
direction: {
|
|
87
|
-
en: "Which end to take (history): 'tail' (last N, default) or 'head' (first N). At most N pairs.",
|
|
88
|
-
zh: "取哪一端(history):'tail'(最后 N 条,默认)或 'head'(前 N 条)。最多 N 条。",
|
|
89
|
-
},
|
|
90
|
-
limit: {
|
|
91
|
-
en: "Number of pairs to return (history, default 10). Capped at total pairs.",
|
|
92
|
-
zh: "返回的对数(history,默认 10)。不超过总对数。",
|
|
93
|
-
},
|
|
94
|
-
includeCommand: {
|
|
95
|
-
en: "Whether to include the command line in history output. Default false (output only).",
|
|
96
|
-
zh: "是否在 history 结果中包含命令行。默认 false(只返回输出)。",
|
|
97
|
-
},
|
|
98
|
-
name: {
|
|
99
|
-
en: "Target terminal name, default 'default'.",
|
|
100
|
-
zh: "目标终端名,默认 'default'。",
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
term_status: {
|
|
104
|
-
en: "Check SSH/local/container terminal state (busy, pending, connection) by terminal name (all terminals if omitted) and the HTTP server state (URL/port/session count). Use busy to poll completion instead of reading animated output.",
|
|
105
|
-
zh: "按终端名检查 SSH / 本地 / 容器终端状态(busy、未消费量、连接;省略则列出全部终端)及 HTTP 服务状态(URL/端口/会话数)。用 busy 轮询完成情况,避免读取动画中间态。",
|
|
106
|
-
},
|
|
107
|
-
term_status_args: {
|
|
108
|
-
name: {
|
|
109
|
-
en: "Terminal name to check; omit to list all terminals.",
|
|
110
|
-
zh: "要检查的终端名;省略则列出全部终端。",
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
term_disconnect: {
|
|
114
|
-
en: "Close the terminal of a name (default 'default') for an SSH, local or container session, or all terminals when name is omitted, and clean up session state.",
|
|
115
|
-
zh: "关闭指定名称(默认 'default')的 SSH / 本地 / 容器终端,name 省略则关闭全部终端,并清理会话态。",
|
|
116
|
-
},
|
|
117
|
-
term_disconnect_args: {
|
|
118
|
-
name: {
|
|
119
|
-
en: "Terminal name to disconnect; omit to disconnect all terminals.",
|
|
120
|
-
zh: "要断开的终端名;省略则断开全部终端。",
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
local_connect: {
|
|
124
|
-
en: "Start a local/container PTY terminal (e.g. pwsh, bash, docker exec -it <container> sh) in the current session. Same-session reconnect closes the previous one. Uses Bun.Terminal (runs in the opencode Bun runtime).",
|
|
125
|
-
zh: "在当前会话启动本地/容器 PTY 终端(如 pwsh、bash、docker exec -it <容器> sh);同一会话重复启动会先关闭旧的。基于 Bun.Terminal(opencode Bun 运行时内运行)。",
|
|
126
|
-
},
|
|
127
|
-
local_connect_args: {
|
|
128
|
-
command: {
|
|
129
|
-
en: "Command to launch, e.g. pwsh / bash / docker exec -it <container> sh",
|
|
130
|
-
zh: "要启动的命令,如 pwsh / bash / docker exec -it <容器> sh",
|
|
131
|
-
},
|
|
132
|
-
name: {
|
|
133
|
-
en: "Terminal name within the session, default 'default'. Creating with an existing name closes the old one first.",
|
|
134
|
-
zh: "会话内的终端名,默认 'default'。同名创建会先关闭旧终端。",
|
|
135
|
-
},
|
|
136
|
-
cwd: {
|
|
137
|
-
en: "Working directory, defaults to plugin cwd.",
|
|
138
|
-
zh: "工作目录,默认插件所在目录。",
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
local_connect_title_ok: {
|
|
142
|
-
en: "Local terminal started",
|
|
143
|
-
zh: "本地终端已启动",
|
|
144
|
-
},
|
|
145
26
|
local_connect_ok: {
|
|
146
27
|
en: "Started: {cmd} (terminal {name})",
|
|
147
28
|
zh: "已启动:{cmd}(终端 {name})",
|
|
@@ -150,18 +31,19 @@ export const T = {
|
|
|
150
31
|
en: "Local terminal start failed",
|
|
151
32
|
zh: "本地终端启动失败",
|
|
152
33
|
},
|
|
153
|
-
|
|
154
|
-
en: "Unified CLI
|
|
155
|
-
zh: "
|
|
34
|
+
term_cli: {
|
|
35
|
+
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.",
|
|
36
|
+
zh: "统一终端命令行工具:SSH(远程)/ 本地 shell(wsl、pwsh、bash)/ 容器(docker exec)会话。对任何本地可启动的终端——WSL、Docker 容器或本地 shell——应使用 'local' 子命令,而不是用 bash 直接执行。子命令: connect / local / exec / read / send / status / disconnect / help。用 'help' 查看完整用法。",
|
|
156
37
|
},
|
|
157
|
-
|
|
158
|
-
en: "Full command line string, e.g. 'exec \"ls -la\" -w'
|
|
159
|
-
zh: "完整命令行字符串,如 'exec \"ls -la\" -w'
|
|
38
|
+
term_cli_args: {
|
|
39
|
+
en: "Full command line string, e.g. 'local \"wsl bash\"' / 'exec \"ls -la\" -w' / 'connect user@host'; defaults to 'help' when empty.",
|
|
40
|
+
zh: "完整命令行字符串,如 'local \"wsl bash\"' / 'exec \"ls -la\" -w' / 'connect user@host';空时默认 'help'。",
|
|
160
41
|
},
|
|
161
|
-
|
|
162
|
-
en: `
|
|
42
|
+
term_cli_help: {
|
|
43
|
+
en: `term_cli unified CLI tool: manage SSH / local (wsl, pwsh, bash) / container (docker exec) terminal sessions.
|
|
44
|
+
Use 'local' for any terminal started on this machine (WSL, Docker containers, local shells).
|
|
163
45
|
|
|
164
|
-
Usage:
|
|
46
|
+
Usage: term_cli <subcommand> [args]
|
|
165
47
|
|
|
166
48
|
Subcommands:
|
|
167
49
|
connect <target> [-u user] [-n name] [-p password|file:path]
|
|
@@ -172,7 +54,7 @@ Subcommands:
|
|
|
172
54
|
exec "<command>" [-n name] [-w]
|
|
173
55
|
run a command in the terminal (default: submit async and return immediately; -w waits for result)
|
|
174
56
|
read [-n name] [-s buffer|history] [-l limit] [--head] [--include-command]
|
|
175
|
-
read terminal output: buffer (
|
|
57
|
+
read terminal output: buffer (current snapshot, consumed) / history (default tail last N)
|
|
176
58
|
send "<text>" [-n name]
|
|
177
59
|
send text/keystrokes (\\r Enter, \\x03 Ctrl-C, \\x04 Ctrl-D, \\x1a Ctrl-Z, \\x1b Esc)
|
|
178
60
|
status [-n name]
|
|
@@ -183,19 +65,20 @@ Subcommands:
|
|
|
183
65
|
show this help
|
|
184
66
|
|
|
185
67
|
Examples:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
68
|
+
term_cli connect root@192.168.1.10
|
|
69
|
+
term_cli connect user@host -p file:C:\\secrets\\pw.txt
|
|
70
|
+
term_cli local "docker exec -it myctr bash" -n db
|
|
71
|
+
term_cli exec "ls -la" -w
|
|
72
|
+
term_cli exec "cd /var/log && tail -50 syslog"
|
|
73
|
+
term_cli send "mypass\\r" -n default
|
|
74
|
+
term_cli read -n db -s buffer
|
|
75
|
+
term_cli status
|
|
76
|
+
term_cli disconnect -n db
|
|
195
77
|
`,
|
|
196
|
-
zh: `
|
|
78
|
+
zh: `term_cli 统一命令行工具:管理 SSH / 本地(wsl、pwsh、bash)/ 容器(docker exec)终端会话。
|
|
79
|
+
本机可启动的终端(WSL、Docker 容器、本地 shell)一律用 'local' 子命令。
|
|
197
80
|
|
|
198
|
-
用法:
|
|
81
|
+
用法: term_cli <子命令> [参数]
|
|
199
82
|
|
|
200
83
|
子命令:
|
|
201
84
|
connect <target> [-u user] [-n name] [-p password|file:path]
|
|
@@ -206,7 +89,7 @@ Examples:
|
|
|
206
89
|
exec "<command>" [-n name] [-w]
|
|
207
90
|
在指定终端执行命令(默认异步提交立即返回;-w 同步等待结果)
|
|
208
91
|
read [-n name] [-s buffer|history] [-l limit] [--head] [--include-command]
|
|
209
|
-
读终端输出:buffer
|
|
92
|
+
读终端输出:buffer 实时快照(读取后清空)/ history 历史快照(默认 tail 后 N 条)
|
|
210
93
|
send "<text>" [-n name]
|
|
211
94
|
发送文本/按键(\\r 回车、\\x03 Ctrl-C、\\x04 Ctrl-D、\\x1a Ctrl-Z、\\x1b Esc)
|
|
212
95
|
status [-n name]
|
|
@@ -217,15 +100,15 @@ Examples:
|
|
|
217
100
|
显示本帮助
|
|
218
101
|
|
|
219
102
|
示例:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
103
|
+
term_cli connect root@192.168.1.10
|
|
104
|
+
term_cli connect user@host -p file:C:\\secrets\\pw.txt
|
|
105
|
+
term_cli local "docker exec -it myctr bash" -n db
|
|
106
|
+
term_cli exec "ls -la" -w
|
|
107
|
+
term_cli exec "cd /var/log && tail -50 syslog"
|
|
108
|
+
term_cli send "mypass\\r" -n default
|
|
109
|
+
term_cli read -n db -s buffer
|
|
110
|
+
term_cli status
|
|
111
|
+
term_cli disconnect -n db
|
|
229
112
|
`,
|
|
230
113
|
},
|
|
231
114
|
denied: {
|
|
@@ -286,8 +169,8 @@ Examples:
|
|
|
286
169
|
zh: "危险命令,已拒绝执行。",
|
|
287
170
|
},
|
|
288
171
|
not_connected: {
|
|
289
|
-
en: "No
|
|
290
|
-
zh: "
|
|
172
|
+
en: "No active terminal. Use term_cli connect (SSH) or term_cli local first.",
|
|
173
|
+
zh: "无活动终端。请先用 term_cli connect(SSH)或 term_cli local 建立。",
|
|
291
174
|
},
|
|
292
175
|
rejected_connect: {
|
|
293
176
|
en: "Connection rejected.",
|
|
@@ -298,12 +181,8 @@ Examples:
|
|
|
298
181
|
zh: "终端名非法。仅允许字母、数字、下划线、中划线、点(最多 64 字符),不得含路径分隔符。",
|
|
299
182
|
},
|
|
300
183
|
no_sessions: {
|
|
301
|
-
en: "No active
|
|
302
|
-
zh: "
|
|
303
|
-
},
|
|
304
|
-
connect_title_ok: {
|
|
305
|
-
en: "SSH connected",
|
|
306
|
-
zh: "SSH 已连接",
|
|
184
|
+
en: "No active terminals.",
|
|
185
|
+
zh: "当前没有活动的终端会话。",
|
|
307
186
|
},
|
|
308
187
|
connect_ok: {
|
|
309
188
|
en: "Connected: {user}@{host}:{port} (session {sid}, terminal {name})",
|
|
@@ -317,41 +196,21 @@ Examples:
|
|
|
317
196
|
en: "Unknown error",
|
|
318
197
|
zh: "未知错误",
|
|
319
198
|
},
|
|
320
|
-
exec_title: {
|
|
321
|
-
en: "SSH exec: {cmd}",
|
|
322
|
-
zh: "SSH 执行: {cmd}",
|
|
323
|
-
},
|
|
324
199
|
exec_failed: {
|
|
325
200
|
en: "Execution failed",
|
|
326
201
|
zh: "执行失败",
|
|
327
202
|
},
|
|
328
|
-
session_output_title: {
|
|
329
|
-
en: "SSH session output",
|
|
330
|
-
zh: "SSH 会话输出",
|
|
331
|
-
},
|
|
332
203
|
session_disconnected: {
|
|
333
|
-
en: "
|
|
334
|
-
zh: "
|
|
204
|
+
en: "Terminal disconnected",
|
|
205
|
+
zh: "终端已断开",
|
|
335
206
|
},
|
|
336
207
|
status_busy_hint: {
|
|
337
|
-
en: "\nHint: a command is still running. Wait and retry, or call
|
|
338
|
-
zh: "\n提示:仍有命令在执行,请等待后重试或调用
|
|
208
|
+
en: "\nHint: a command is still running. Wait and retry, or call term_cli read for partial output.",
|
|
209
|
+
zh: "\n提示:仍有命令在执行,请等待后重试或调用 term_cli read 获取部分输出。",
|
|
339
210
|
},
|
|
340
211
|
status_idle_hint: {
|
|
341
|
-
en: "\nHint: no command running. You can safely execute a new command or call
|
|
342
|
-
zh: "\n提示:无命令在执行,可安全执行新命令或调用
|
|
343
|
-
},
|
|
344
|
-
status_title: {
|
|
345
|
-
en: "SSH session status",
|
|
346
|
-
zh: "SSH 会话状态",
|
|
347
|
-
},
|
|
348
|
-
server_title: {
|
|
349
|
-
en: "SSH HTTP server",
|
|
350
|
-
zh: "SSH HTTP 服务",
|
|
351
|
-
},
|
|
352
|
-
server_info: {
|
|
353
|
-
en: "HTTP server enabled\nurl: {url}\nport: {port}\nactive sessions: {n}\nOpen {url} in a browser to view terminal records (scrollable, auto-refreshing).",
|
|
354
|
-
zh: "HTTP 服务已启用\nurl: {url}\nport: {port}\n活跃会话: {n}\n浏览器访问 {url} 查看终端记录(可滚动、自动刷新)。",
|
|
212
|
+
en: "\nHint: no command running. You can safely execute a new command or call term_cli read for remaining output.",
|
|
213
|
+
zh: "\n提示:无命令在执行,可安全执行新命令或调用 term_cli read 读取剩余输出。",
|
|
355
214
|
},
|
|
356
215
|
browser_full_record: {
|
|
357
216
|
en: "\nBrowser full record: {url}",
|
|
@@ -362,12 +221,12 @@ Examples:
|
|
|
362
221
|
zh: "\n(HTTP 服务未启用)",
|
|
363
222
|
},
|
|
364
223
|
history_title: {
|
|
365
|
-
en: "
|
|
366
|
-
zh: "
|
|
224
|
+
en: "Terminal history (last {n} / total {total})",
|
|
225
|
+
zh: "终端历史(最后 {n} 条 / 共 {total} 条)",
|
|
367
226
|
},
|
|
368
227
|
history_title_head: {
|
|
369
|
-
en: "
|
|
370
|
-
zh: "
|
|
228
|
+
en: "Terminal history (first {n} / total {total})",
|
|
229
|
+
zh: "终端历史(前 {n} 条 / 共 {total} 条)",
|
|
371
230
|
},
|
|
372
231
|
web_title: { en: "Terminal Records", zh: "终端记录" },
|
|
373
232
|
web_loading: { en: "Loading...", zh: "加载中..." },
|
|
@@ -378,37 +237,32 @@ Examples:
|
|
|
378
237
|
web_local: { en: "local", zh: "本地" },
|
|
379
238
|
web_commands: { en: "commands", zh: "条命令" },
|
|
380
239
|
web_auto_refresh: { en: "auto-refresh every 2s", zh: "每 2s 自动刷新" },
|
|
381
|
-
web_no_session: { en: "No
|
|
240
|
+
web_no_session: { en: "No terminal. Use term_cli connect (SSH) or term_cli local first.", zh: "无终端会话,请先用 term_cli connect(SSH)或 term_cli local 建立。" },
|
|
382
241
|
web_running: { en: "[running] ", zh: "[运行中] " },
|
|
383
242
|
web_new_messages: { en: "↓ New messages", zh: "↓ 新消息" },
|
|
243
|
+
web_delete_terminal: { en: "Delete disconnected terminal", zh: "删除已断开终端" },
|
|
244
|
+
web_cmd_placeholder: { en: "Enter command (Shift+Enter new line)", zh: "输入命令(Shift+Enter 换行)" },
|
|
245
|
+
web_send_ctrlc: { en: "Keystroke", zh: "快捷键" },
|
|
384
246
|
disconnected_ok: {
|
|
385
|
-
en: "
|
|
386
|
-
zh: "
|
|
247
|
+
en: "Terminal closed ({host}).",
|
|
248
|
+
zh: "终端已断开({host})。",
|
|
249
|
+
},
|
|
250
|
+
disconnected_ok_local: {
|
|
251
|
+
en: "Terminal closed ({program}).",
|
|
252
|
+
zh: "终端已断开({program})。",
|
|
387
253
|
},
|
|
388
254
|
disconnected_all_ok: {
|
|
389
|
-
en: "All
|
|
390
|
-
zh: "
|
|
255
|
+
en: "All terminals closed (last host {host}).",
|
|
256
|
+
zh: "已断开全部终端(最后主机 {host})。",
|
|
257
|
+
},
|
|
258
|
+
disconnected_all_ok_local: {
|
|
259
|
+
en: "All terminals closed (last program {program}).",
|
|
260
|
+
zh: "已断开全部终端(最后程序 {program})。",
|
|
391
261
|
},
|
|
392
262
|
err_not_connected: {
|
|
393
263
|
en: "Not connected",
|
|
394
264
|
zh: "未连接",
|
|
395
265
|
},
|
|
396
|
-
err_busy: {
|
|
397
|
-
en: "Previous command still running, poll with term_status first",
|
|
398
|
-
zh: "上一条命令仍在执行,请先 term_status 轮询",
|
|
399
|
-
},
|
|
400
|
-
err_shell_open: {
|
|
401
|
-
en: "Shell open failed",
|
|
402
|
-
zh: "shell 打开失败",
|
|
403
|
-
},
|
|
404
|
-
err_no_such_session: {
|
|
405
|
-
en: "Session not found or disconnected",
|
|
406
|
-
zh: "会话不存在或已断开",
|
|
407
|
-
},
|
|
408
|
-
truncated: {
|
|
409
|
-
en: "... [output truncated, {n} chars total]",
|
|
410
|
-
zh: "... [输出已截断,共 {n} 字符]",
|
|
411
|
-
},
|
|
412
266
|
};
|
|
413
267
|
/** 取某语言下的一条文案 */
|
|
414
268
|
export function tr(key, lang) {
|
package/dist/i18n.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAE/B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAOzC;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,UAAU,GAAS,IAAI;IAC7C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACjC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC7B,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC7B,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,kBAAkB;AAClB,MAAM,CAAC,MAAM,CAAC,GAAG;IACf,
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAE/B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAOzC;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,UAAU,GAAS,IAAI;IAC7C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACjC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC7B,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC7B,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,kBAAkB;AAClB,MAAM,CAAC,MAAM,CAAC,GAAG;IACf,SAAS,EAAE;QACT,EAAE,EAAE,4HAA4H;QAChI,EAAE,EAAE,8DAA8D;KACnE;IACD,OAAO,EAAE;QACP,EAAE,EAAE,cAAc;QAClB,EAAE,EAAE,YAAY;KACjB;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,QAAQ,EAAE;QACR,EAAE,EAAE,uXAAuX;QAC3X,EAAE,EAAE,2NAA2N;KAChO;IACD,aAAa,EAAE;QACb,EAAE,EAAE,kIAAkI;QACtI,EAAE,EAAE,2FAA2F;KAChG;IACD,aAAa,EAAE;QACb,EAAE,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCP;QACG,EAAE,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCP;KACE;IACD,MAAM,EAAE;QACN,EAAE,EAAE,iBAAiB;QACrB,EAAE,EAAE,QAAQ;KACb;IACD,cAAc,EAAE;QACd,EAAE,EAAE,0BAA0B;QAC9B,EAAE,EAAE,sBAAsB;KAC3B;IACD,iBAAiB,EAAE;QACjB,EAAE,EAAE,mEAAmE;QACvE,EAAE,EAAE,gEAAgE;KACrE;IACD,kBAAkB,EAAE;QAClB,EAAE,EAAE,oBAAoB;QACxB,EAAE,EAAE,gBAAgB;KACrB;IACD,eAAe,EAAE;QACf,EAAE,EAAE,+CAA+C;QACnD,EAAE,EAAE,4CAA4C;KACjD;IACD,cAAc,EAAE;QACd,EAAE,EAAE,0CAA0C;QAC9C,EAAE,EAAE,uCAAuC;KAC5C;IACD,cAAc,EAAE;QACd,EAAE,EAAE,kCAAkC;QACtC,EAAE,EAAE,+BAA+B;KACpC;IACD,gBAAgB,EAAE;QAChB,EAAE,EAAE,gBAAgB;QACpB,EAAE,EAAE,MAAM;KACX;IACD,aAAa,EAAE;QACb,EAAE,EAAE,mBAAmB;QACvB,EAAE,EAAE,QAAQ;KACb;IACD,WAAW,EAAE;QACX,EAAE,EAAE,wBAAwB;QAC5B,EAAE,EAAE,aAAa;KAClB;IACD,OAAO,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE;IACjC,OAAO,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE;IACjC,YAAY,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,KAAK,EAAE;IAC5C,OAAO,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE;IACjC,UAAU,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE;IACxC,QAAQ,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE;IACnC,OAAO,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE;IACjC,UAAU,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE;IACvC,cAAc,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,EAAE;IAChD,eAAe,EAAE,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE;IAClD,cAAc,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,SAAS,EAAE;IACnD,kBAAkB,EAAE,EAAE,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,EAAE;IACxD,WAAW,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE;IAC1C,aAAa,EAAE;QACb,EAAE,EAAE,wCAAwC;QAC5C,EAAE,EAAE,aAAa;KAClB;IACD,aAAa,EAAE;QACb,EAAE,EAAE,yEAAyE;QAC7E,EAAE,EAAE,qDAAqD;KAC1D;IACD,gBAAgB,EAAE;QAChB,EAAE,EAAE,sBAAsB;QAC1B,EAAE,EAAE,SAAS;KACd;IACD,YAAY,EAAE;QACZ,EAAE,EAAE,2GAA2G;QAC/G,EAAE,EAAE,8CAA8C;KACnD;IACD,WAAW,EAAE;QACX,EAAE,EAAE,sBAAsB;QAC1B,EAAE,EAAE,cAAc;KACnB;IACD,UAAU,EAAE;QACV,EAAE,EAAE,kEAAkE;QACtE,EAAE,EAAE,oDAAoD;KACzD;IACD,kBAAkB,EAAE;QAClB,EAAE,EAAE,uBAAuB;QAC3B,EAAE,EAAE,UAAU;KACf;IACD,aAAa,EAAE;QACb,EAAE,EAAE,eAAe;QACnB,EAAE,EAAE,MAAM;KACX;IACD,WAAW,EAAE;QACX,EAAE,EAAE,kBAAkB;QACtB,EAAE,EAAE,MAAM;KACX;IACD,oBAAoB,EAAE;QACpB,EAAE,EAAE,uBAAuB;QAC3B,EAAE,EAAE,OAAO;KACZ;IACD,gBAAgB,EAAE;QAChB,EAAE,EAAE,+FAA+F;QACnG,EAAE,EAAE,8CAA8C;KACnD;IACD,gBAAgB,EAAE;QAChB,EAAE,EAAE,8GAA8G;QAClH,EAAE,EAAE,+CAA+C;KACpD;IACD,mBAAmB,EAAE;QACnB,EAAE,EAAE,8BAA8B;QAClC,EAAE,EAAE,mBAAmB;KACxB;IACD,kBAAkB,EAAE;QAClB,EAAE,EAAE,6BAA6B;QACjC,EAAE,EAAE,gBAAgB;KACrB;IACD,aAAa,EAAE;QACb,EAAE,EAAE,6CAA6C;QACjD,EAAE,EAAE,8BAA8B;KACnC;IACD,kBAAkB,EAAE;QAClB,EAAE,EAAE,8CAA8C;QAClD,EAAE,EAAE,6BAA6B;KAClC;IACD,SAAS,EAAE,EAAE,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM,EAAE;IACjD,WAAW,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE;IAC/C,eAAe,EAAE,EAAE,EAAE,EAAE,wBAAwB,EAAE,EAAE,EAAE,QAAQ,EAAE;IAC/D,gBAAgB,EAAE,EAAE,EAAE,EAAE,mCAAmC,EAAE,EAAE,EAAE,WAAW,EAAE;IAC9E,QAAQ,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE;IAClC,aAAa,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,EAAE;IAC5C,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE;IACpC,YAAY,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE;IAC3C,gBAAgB,EAAE,EAAE,EAAE,EAAE,uBAAuB,EAAE,EAAE,EAAE,WAAW,EAAE;IAClE,cAAc,EAAE,EAAE,EAAE,EAAE,kEAAkE,EAAE,EAAE,EAAE,qDAAqD,EAAE;IACrJ,WAAW,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE;IAC/C,gBAAgB,EAAE,EAAE,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,OAAO,EAAE;IACvD,mBAAmB,EAAE,EAAE,EAAE,EAAE,8BAA8B,EAAE,EAAE,EAAE,SAAS,EAAE;IAC5E,mBAAmB,EAAE,EAAE,EAAE,EAAE,sCAAsC,EAAE,EAAE,EAAE,sBAAsB,EAAE;IAC7F,cAAc,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,KAAK,EAAE;IAC9C,eAAe,EAAE;QACf,EAAE,EAAE,2BAA2B;QAC/B,EAAE,EAAE,gBAAgB;KACrB;IACD,qBAAqB,EAAE;QACrB,EAAE,EAAE,8BAA8B;QAClC,EAAE,EAAE,mBAAmB;KACxB;IACD,mBAAmB,EAAE;QACnB,EAAE,EAAE,0CAA0C;QAC9C,EAAE,EAAE,uBAAuB;KAC5B;IACD,yBAAyB,EAAE;QACzB,EAAE,EAAE,gDAAgD;QACpD,EAAE,EAAE,0BAA0B;KAC/B;IACD,iBAAiB,EAAE;QACjB,EAAE,EAAE,eAAe;QACnB,EAAE,EAAE,KAAK;KACV;CACO,CAAA;AAEV,iBAAiB;AACjB,MAAM,UAAU,EAAE,CAAC,GAAY,EAAE,IAAU;IACzC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAA;AACrB,CAAC"}
|
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;AAgNvD,eAAO,MAAM,eAAe,EAAE,MAyU7B,CAAA;eAGc,eAAe"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
3
|
import stringArgv from "string-argv";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
-
import
|
|
5
|
+
import log from "./log.js";
|
|
6
6
|
import { CACHE_DIR } from "./constants.js";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
@@ -10,13 +10,13 @@ import { rmSync } from "node:fs";
|
|
|
10
10
|
import { loadConfig } from "./config.js";
|
|
11
11
|
import { SessionHistory } from "./history.js";
|
|
12
12
|
import { T, getLang, tr } from "./i18n.js";
|
|
13
|
+
import { toModelText } from "./utils.js";
|
|
13
14
|
import { createDecider } from "./permission.js";
|
|
14
15
|
import { SshSession } from "./session.js";
|
|
15
16
|
import { LocalSession } from "./local-session.js";
|
|
16
17
|
import {} from "./server.js";
|
|
17
18
|
import { ensureServer } from "./server-manager.js";
|
|
18
19
|
import { writeSessionState, removeSessionState, removeAllSessionStates } from "./session-store.js";
|
|
19
|
-
const log = createLogger("opencode-ssh-tool");
|
|
20
20
|
/** 默认终端名(不传 name 时用) */
|
|
21
21
|
const DEFAULT_NAME = "default";
|
|
22
22
|
/** 终端名合法字符(字母/数字/下划线/中划线/点),防止路径穿越 */
|
|
@@ -68,13 +68,18 @@ function getSessionMap(sessionID) {
|
|
|
68
68
|
function getSession(sessionID, name = DEFAULT_NAME) {
|
|
69
69
|
return getSessionMap(sessionID)?.get(name);
|
|
70
70
|
}
|
|
71
|
-
/** 扁平化会话条目列表(供 HTTP
|
|
71
|
+
/** 扁平化会话条目列表(供 HTTP 服务展示,SSH + 本地/容器,kind 区分历史目录与展示) */
|
|
72
72
|
function listSessionEntries() {
|
|
73
73
|
const entries = [];
|
|
74
74
|
for (const [sessionID, map] of sshSessions) {
|
|
75
75
|
const meta = sessionMeta.get(sessionID);
|
|
76
76
|
for (const [name, session] of map)
|
|
77
|
-
entries.push({ sessionID, name, session, title: meta?.title, directory: meta?.directory });
|
|
77
|
+
entries.push({ sessionID, name, session, kind: "ssh", title: meta?.title, directory: meta?.directory });
|
|
78
|
+
}
|
|
79
|
+
for (const [sessionID, map] of localSessions) {
|
|
80
|
+
const meta = sessionMeta.get(sessionID);
|
|
81
|
+
for (const [name, session] of map)
|
|
82
|
+
entries.push({ sessionID, name, session, kind: "local", title: meta?.title, directory: meta?.directory });
|
|
78
83
|
}
|
|
79
84
|
return entries;
|
|
80
85
|
}
|
|
@@ -193,6 +198,27 @@ function cleanupAllSessions(sessionID) {
|
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
export const OpenCodeSshTool = async () => {
|
|
201
|
+
// 父子会话关系(sessionID → parentID;根的 parentID 为 undefined)
|
|
202
|
+
// 通过 session.created/updated 事件构建,用于根系查找(主子会话共享终端)
|
|
203
|
+
const childMap = new Map();
|
|
204
|
+
/**
|
|
205
|
+
* 沿 parentID 链找到根会话 ID(主子会话共享存储key)
|
|
206
|
+
* 输出异常时回退回原 sessionID
|
|
207
|
+
*/
|
|
208
|
+
const findRoot = (sessionID) => {
|
|
209
|
+
let cur = sessionID;
|
|
210
|
+
const visited = new Set();
|
|
211
|
+
while (cur && !visited.has(cur)) {
|
|
212
|
+
visited.add(cur);
|
|
213
|
+
const parent = childMap.get(cur);
|
|
214
|
+
if (parent !== undefined && parent !== null) {
|
|
215
|
+
cur = parent;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
return cur;
|
|
221
|
+
};
|
|
196
222
|
log.loaded();
|
|
197
223
|
// 加载配置并启动 HTTP 终端记录服务(默认开启;端口 0=自动分配)
|
|
198
224
|
const cfg = loadConfig();
|
|
@@ -223,9 +249,9 @@ export const OpenCodeSshTool = async () => {
|
|
|
223
249
|
else {
|
|
224
250
|
log.info("HTTP 服务未启用(配置 server.enabled=false)");
|
|
225
251
|
}
|
|
226
|
-
// ===== CLI 风格单工具(
|
|
252
|
+
// ===== CLI 风格单工具(term_cli)子命令实现 =====
|
|
227
253
|
/** 完整用法(模型自学手册) */
|
|
228
|
-
const HELP_TEXT = tr("
|
|
254
|
+
const HELP_TEXT = tr("term_cli_help", lang);
|
|
229
255
|
/** connect:SSH 连接(target=user@host[:port]) */
|
|
230
256
|
async function doConnect(args, ctx) {
|
|
231
257
|
const { values, positionals } = parseArgs({ args, options: { name: { type: "string", short: "n" }, password: { type: "string", short: "p" }, user: { type: "string", short: "u" } }, allowPositionals: true });
|
|
@@ -321,8 +347,10 @@ export const OpenCodeSshTool = async () => {
|
|
|
321
347
|
return tr("denied", lang);
|
|
322
348
|
}
|
|
323
349
|
}
|
|
350
|
+
const name = values.name ?? DEFAULT_NAME;
|
|
351
|
+
syncSessionState(ctx.sessionID, name);
|
|
324
352
|
const result = values.waitResult ? await session.exec(command) : await session.submit(command);
|
|
325
|
-
syncSessionState(ctx.sessionID,
|
|
353
|
+
syncSessionState(ctx.sessionID, name);
|
|
326
354
|
log.tool("term_exec", { command, ok: result.ok, submitted: result.submitted, interactive: result.interactive, running: result.running });
|
|
327
355
|
if (result.submitted)
|
|
328
356
|
return tr("submitted", lang);
|
|
@@ -345,7 +373,7 @@ export const OpenCodeSshTool = async () => {
|
|
|
345
373
|
const direction = values.head ? "head" : "tail";
|
|
346
374
|
const selected = direction === "tail" ? all.slice(-limit) : all.slice(0, limit);
|
|
347
375
|
const includeCommand = values.includeCommand ?? false;
|
|
348
|
-
const text = selected.map((p) => (includeCommand ? `$ ${p.command}\n
|
|
376
|
+
const text = selected.map((p) => (includeCommand ? `$ ${p.command}\n` : "") + toModelText(history.readOutput(p))).join("\n");
|
|
349
377
|
const browserLine = httpUrl ? tr("browser_full_record", lang).replace("{url}", httpUrl) : tr("server_not_enabled", lang);
|
|
350
378
|
return `${tr(direction === "tail" ? "history_title" : "history_title_head", lang).replace("{n}", String(selected.length)).replace("{total}", String(all.length))}\n${text}${browserLine}`;
|
|
351
379
|
}
|
|
@@ -417,10 +445,13 @@ export const OpenCodeSshTool = async () => {
|
|
|
417
445
|
if ((!smap || smap.size === 0) && (!lmap || lmap.size === 0))
|
|
418
446
|
return tr("no_sessions", lang);
|
|
419
447
|
if (!values.name) {
|
|
420
|
-
const
|
|
448
|
+
const firstSshHost = [...(smap?.values() ?? [])][0]?.getStatus().host;
|
|
449
|
+
const firstLocalProgram = [...(lmap?.values() ?? [])][0]?.getStatus().program;
|
|
421
450
|
cleanupAllSessions(ctx.sessionID);
|
|
422
451
|
cleanupAllLocalSessions(ctx.sessionID);
|
|
423
|
-
|
|
452
|
+
if (firstSshHost)
|
|
453
|
+
return tr("disconnected_all_ok", lang).replace("{host}", firstSshHost);
|
|
454
|
+
return tr("disconnected_all_ok_local", lang).replace("{program}", firstLocalProgram ?? "-");
|
|
424
455
|
}
|
|
425
456
|
const ssh = getSession(ctx.sessionID, values.name);
|
|
426
457
|
if (ssh) {
|
|
@@ -432,7 +463,7 @@ export const OpenCodeSshTool = async () => {
|
|
|
432
463
|
if (local) {
|
|
433
464
|
const program = local.getStatus().program;
|
|
434
465
|
cleanupLocalSession(ctx.sessionID, values.name);
|
|
435
|
-
return tr("
|
|
466
|
+
return tr("disconnected_ok_local", lang).replace("{program}", program ?? "-");
|
|
436
467
|
}
|
|
437
468
|
return tr("no_sessions", lang);
|
|
438
469
|
}
|
|
@@ -442,52 +473,73 @@ export const OpenCodeSshTool = async () => {
|
|
|
442
473
|
const [cmd, ...rest] = tokens;
|
|
443
474
|
if (!cmd || cmd === "help")
|
|
444
475
|
return HELP_TEXT;
|
|
476
|
+
// 统一按根会话 ID 路由(主子会话共享终端)
|
|
477
|
+
const rootID = await findRoot(ctx.sessionID);
|
|
478
|
+
const rootCtx = { ...ctx, sessionID: rootID };
|
|
445
479
|
switch (cmd) {
|
|
446
|
-
case "connect": return doConnect(rest,
|
|
447
|
-
case "local": return doLocal(rest,
|
|
448
|
-
case "exec": return doExec(rest,
|
|
449
|
-
case "read": return doRead(rest,
|
|
450
|
-
case "send": return doSend(rest,
|
|
451
|
-
case "status": return doStatus(rest,
|
|
452
|
-
case "disconnect": return doDisconnect(rest,
|
|
480
|
+
case "connect": return doConnect(rest, rootCtx);
|
|
481
|
+
case "local": return doLocal(rest, rootCtx);
|
|
482
|
+
case "exec": return doExec(rest, rootCtx);
|
|
483
|
+
case "read": return doRead(rest, rootCtx);
|
|
484
|
+
case "send": return doSend(rest, rootCtx);
|
|
485
|
+
case "status": return doStatus(rest, rootCtx);
|
|
486
|
+
case "disconnect": return doDisconnect(rest, rootCtx);
|
|
453
487
|
default: return `${tr("cli_unknown", lang).replace("{cmd}", cmd)}\n\n${HELP_TEXT}`;
|
|
454
488
|
}
|
|
455
489
|
}
|
|
490
|
+
// 记录哪些根会话已收到过"来自根自身"的标题(防止子会话标题覆盖根标题)
|
|
491
|
+
const rootTitleOwned = new Set();
|
|
456
492
|
return {
|
|
457
493
|
event: async ({ event }) => {
|
|
458
494
|
// 会话创建/更新时记录标题与目录(供 HTTP 页面显示会话名称)
|
|
459
495
|
if (event.type === "session.created" || event.type === "session.updated") {
|
|
460
496
|
const info = event.properties?.info;
|
|
461
497
|
if (info?.id) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
498
|
+
// 记录父子关系(用于根系查找)
|
|
499
|
+
if (info.parentID !== undefined)
|
|
500
|
+
childMap.set(info.id, info.parentID);
|
|
501
|
+
else if (!childMap.has(info.id))
|
|
502
|
+
childMap.set(info.id, undefined);
|
|
503
|
+
// 标题仅来自根会话自身;子会话不覆盖根标题
|
|
504
|
+
const root = findRoot(info.id);
|
|
505
|
+
if (info.id === root) {
|
|
506
|
+
sessionMeta.set(root, { title: info.title ?? "", directory: info.directory ?? "" });
|
|
507
|
+
rootTitleOwned.add(root);
|
|
508
|
+
}
|
|
509
|
+
else if (!rootTitleOwned.has(root) && !sessionMeta.has(root)) {
|
|
510
|
+
sessionMeta.set(root, { title: info.title ?? "", directory: info.directory ?? "" });
|
|
511
|
+
}
|
|
512
|
+
// 有活动终端时同步状态文件
|
|
513
|
+
const map = getSessionMap(root);
|
|
465
514
|
if (map)
|
|
466
515
|
for (const name of map.keys())
|
|
467
|
-
syncSessionState(
|
|
516
|
+
syncSessionState(root, name);
|
|
468
517
|
}
|
|
469
518
|
}
|
|
470
|
-
// 会话删除时清理:关闭全部终端连接 + 删缓存目录 + 删元信息
|
|
471
519
|
if (event.type === "session.deleted") {
|
|
472
520
|
const props = event.properties;
|
|
473
|
-
// sessionID 直接挂在 properties 层(完整事件);info.id 为兜底
|
|
474
521
|
const sid = props?.sessionID ?? props?.info?.id;
|
|
475
522
|
if (sid) {
|
|
476
|
-
|
|
477
|
-
|
|
523
|
+
childMap.delete(sid);
|
|
524
|
+
const root = findRoot(sid);
|
|
525
|
+
if (root === sid) {
|
|
526
|
+
cleanupAllSessions(sid);
|
|
527
|
+
cleanupAllLocalSessions(sid);
|
|
528
|
+
}
|
|
478
529
|
sessionMeta.delete(sid);
|
|
479
|
-
|
|
530
|
+
rootTitleOwned.delete(sid);
|
|
531
|
+
log.info(`会话 ${sid} 删除,根 ${root} 清理`);
|
|
480
532
|
}
|
|
481
533
|
}
|
|
482
534
|
},
|
|
483
535
|
tool: {
|
|
484
|
-
|
|
485
|
-
description: T.
|
|
536
|
+
term_cli: tool({
|
|
537
|
+
description: T.term_cli[lang],
|
|
486
538
|
args: {
|
|
487
|
-
args: tool.schema.string().optional().describe(T.
|
|
539
|
+
args: tool.schema.string().optional().describe(T.term_cli_args[lang]),
|
|
488
540
|
},
|
|
489
541
|
async execute(args, context) {
|
|
490
|
-
return { title: "
|
|
542
|
+
return { title: "term_cli", output: await handleCliCommand(args.args ?? "help", context) };
|
|
491
543
|
},
|
|
492
544
|
}),
|
|
493
545
|
},
|