@keyou007/agent-bot 0.1.24-alpha.5 → 0.1.24-alpha.7

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +29 -3
  3. package/README.zh.md +29 -3
  4. package/SECURITY.md +12 -0
  5. package/config.example.yaml +5 -0
  6. package/dist/cli/controlProtocol.js.map +1 -1
  7. package/dist/cli/serverStatus.js +1 -0
  8. package/dist/cli/serverStatus.js.map +1 -1
  9. package/dist/cli.js +6 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/codex/CodexEventMapper.js +1 -0
  12. package/dist/codex/CodexEventMapper.js.map +1 -1
  13. package/dist/codex/CodexRuntime.js +247 -120
  14. package/dist/codex/CodexRuntime.js.map +1 -1
  15. package/dist/codex/ProviderSwitch.js +12 -13
  16. package/dist/codex/ProviderSwitch.js.map +1 -1
  17. package/dist/codex/TraePlanEntry.js +70 -0
  18. package/dist/codex/TraePlanEntry.js.map +1 -0
  19. package/dist/config/AgentExecutionDefaultsStore.js +4 -0
  20. package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
  21. package/dist/config/loadConfig.js +4 -0
  22. package/dist/config/loadConfig.js.map +1 -1
  23. package/dist/config/schema.js +3 -0
  24. package/dist/config/schema.js.map +1 -1
  25. package/dist/feishu/CardRenderer.js +92 -8
  26. package/dist/feishu/CardRenderer.js.map +1 -1
  27. package/dist/feishu/FeishuConnector.js +1 -0
  28. package/dist/feishu/FeishuConnector.js.map +1 -1
  29. package/dist/feishu/FeishuTurnPresenter.js +121 -17
  30. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  31. package/dist/index.js +57 -7
  32. package/dist/index.js.map +1 -1
  33. package/dist/local-files/LocalFileViewerServer.js +51 -4
  34. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  35. package/dist/local-files/TurnPreviewPage.js +7 -2
  36. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  37. package/dist/presentation/TurnStateReducer.js +41 -6
  38. package/dist/presentation/TurnStateReducer.js.map +1 -1
  39. package/dist/proxy/ProxySessionController.js +620 -214
  40. package/dist/proxy/ProxySessionController.js.map +1 -1
  41. package/dist/proxy/SessionOperationQueue.js +14 -0
  42. package/dist/proxy/SessionOperationQueue.js.map +1 -0
  43. package/dist/runtime/AcpRuntimeAdapter.js +4 -2
  44. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  45. package/dist/state/StateStore.js +22 -0
  46. package/dist/state/StateStore.js.map +1 -1
  47. package/dist/state/TurnPreviewJournal.js +19 -7
  48. package/dist/state/TurnPreviewJournal.js.map +1 -1
  49. package/dist/state/migrations.js +6 -0
  50. package/dist/state/migrations.js.map +1 -1
  51. package/dist/supervision/SafeRestartNotifier.js +33 -1
  52. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  53. package/dist/supervision/SafeRestartScheduler.js +22 -6
  54. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  55. package/dist/terminal/TerminalCards.js +13 -0
  56. package/dist/terminal/TerminalCards.js.map +1 -0
  57. package/dist/terminal/TerminalCommandTracker.js +74 -0
  58. package/dist/terminal/TerminalCommandTracker.js.map +1 -0
  59. package/dist/terminal/TerminalLinks.js +76 -0
  60. package/dist/terminal/TerminalLinks.js.map +1 -0
  61. package/dist/terminal/TerminalPage.js +212 -0
  62. package/dist/terminal/TerminalPage.js.map +1 -0
  63. package/dist/terminal/TerminalProcess.js +95 -0
  64. package/dist/terminal/TerminalProcess.js.map +1 -0
  65. package/dist/terminal/TerminalServer.js +676 -0
  66. package/dist/terminal/TerminalServer.js.map +1 -0
  67. package/docs/technical-reference.md +48 -0
  68. package/docs/technical-reference.zh.md +48 -0
  69. package/npm-shrinkwrap.json +55 -5
  70. package/package.json +11 -4
  71. package/skills/agent-bot/SKILL.md +11 -2
  72. package/src/cli/controlProtocol.ts +2 -1
  73. package/src/cli/serverStatus.ts +1 -0
  74. package/src/cli.ts +6 -1
  75. package/src/codex/CodexEventMapper.ts +1 -0
  76. package/src/codex/CodexRuntime.ts +244 -128
  77. package/src/codex/ProviderSwitch.ts +11 -12
  78. package/src/codex/TraePlanEntry.ts +63 -0
  79. package/src/config/AgentExecutionDefaultsStore.ts +2 -0
  80. package/src/config/loadConfig.ts +4 -0
  81. package/src/config/schema.ts +5 -1
  82. package/src/feishu/CardRenderer.ts +109 -8
  83. package/src/feishu/FeishuConnector.ts +1 -0
  84. package/src/feishu/FeishuTurnPresenter.ts +108 -16
  85. package/src/feishu/types.ts +1 -0
  86. package/src/index.ts +56 -8
  87. package/src/local-files/LocalFileViewerServer.ts +48 -4
  88. package/src/local-files/TurnPreviewPage.ts +7 -2
  89. package/src/presentation/TurnStateReducer.ts +46 -7
  90. package/src/presentation/turnViewTypes.ts +5 -0
  91. package/src/proxy/ProxySessionController.ts +629 -211
  92. package/src/proxy/SessionOperationQueue.ts +13 -0
  93. package/src/runtime/AcpRuntimeAdapter.ts +5 -3
  94. package/src/runtime/types.ts +8 -3
  95. package/src/state/StateStore.ts +38 -1
  96. package/src/state/TurnPreviewJournal.ts +19 -7
  97. package/src/state/migrations.ts +6 -0
  98. package/src/supervision/SafeRestartNotifier.ts +35 -2
  99. package/src/supervision/SafeRestartScheduler.ts +25 -6
  100. package/src/terminal/TerminalCards.ts +18 -0
  101. package/src/terminal/TerminalCommandTracker.ts +74 -0
  102. package/src/terminal/TerminalLinks.ts +61 -0
  103. package/src/terminal/TerminalPage.ts +213 -0
  104. package/src/terminal/TerminalProcess.ts +93 -0
  105. package/src/terminal/TerminalServer.ts +551 -0
@@ -0,0 +1,212 @@
1
+ export function terminalPage(base, recovery = false) {
2
+ base = base.replaceAll("&", "&amp;").replaceAll(String.fromCharCode(34), "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <title>AgentBot Terminal</title><link rel="stylesheet" href="${base}/assets/xterm.css"><link rel="stylesheet" href="${base}/assets/terminal.css">
5
+ <script defer src="${base}/assets/xterm.js"></script><script defer src="${base}/assets/fit.js"></script><script defer src="${base}/assets/terminal.js"></script></head>
6
+ <body data-base="${base}" data-recovery="${recovery}"><header><strong>AgentBot <span>Terminal</span></strong><div id="status" role="status">尚未连接</div><button id="connect">请求连接</button><button id="interrupt" disabled>Ctrl+C</button><button id="close" disabled>关闭终端</button></header>
7
+ <section id="welcome"><h1>在浏览器中使用本机终端</h1><p>命令在 AgentBot 所在电脑上执行,拥有其系统用户权限。不会自动执行或接管原卡片中的命令。</p><p>点击请求连接,然后返回命令所在的飞书会话或话题,由 AgentBot 所有者核对验证码并授权。不要批准非本人发起的请求。</p><button id="welcome-connect" type="button">请求连接</button><p id="code"></p></section><div id="terminal"></div>
8
+ <footer id="details">前台连接时不因空闲关闭;后台空闲 30 分钟或网页断开 3 分钟后自动关闭 Shell · 成功重连取消断开计时并重置空闲计时 · 可从原会话的终端管理卡打开或关闭终端</footer></body></html>`;
9
+ }
10
+ export const TERMINAL_CSS = `
11
+ #welcome-connect{padding:10px 20px;font:inherit}
12
+ :root{color-scheme:dark;font:14px system-ui,sans-serif;background:#10141b;color:#dbe4f1}*{box-sizing:border-box}body{margin:0;height:100dvh;display:flex;flex-direction:column}header{display:flex;gap:12px;align-items:center;padding:12px 18px;background:#191f2b;border-bottom:1px solid #303a4d;flex-wrap:wrap}header strong{font-size:17px}header strong span{font-weight:400;color:#9aacc7}#status{flex:1;color:#adbfdb;font-size:13px}button{background:#25334a;border:1px solid #415775;border-radius:7px;padding:8px 12px;color:#e5efff;cursor:pointer}button:hover{background:#304661}button:disabled{opacity:.45;cursor:default}#close{border-color:#86505a}#welcome{margin:40px auto;max-width:780px;padding:0 24px;line-height:1.8}h1{font-size:25px}#code{font-size:24px;letter-spacing:3px;color:#84c1ff}#terminal{flex:1;min-height:0;padding:12px 14px;display:none}.xterm{height:100%}footer{font-size:12px;color:#8f9eb7;padding:9px 18px;border-top:1px solid #293142}body.connected #welcome{display:none}body.connected #terminal{display:block}@media(max-width:650px){header{padding:10px;gap:8px}#status{min-width:100%}footer{padding:7px 10px}#terminal{padding:6px}}
13
+ `;
14
+ export const TERMINAL_SCRIPT = String.raw `(() => {
15
+ const base = document.body.dataset.base;
16
+ const status = document.getElementById('status');
17
+ const connect = document.getElementById('connect');
18
+ const welcomeConnect = document.getElementById('welcome-connect');
19
+ const close = document.getElementById('close');
20
+ const interrupt = document.getElementById('interrupt');
21
+ const storageKey = 'agentbot-terminal:' + location.pathname;
22
+ const recovery = document.body.dataset.recovery === 'true';
23
+ if (recovery) document.querySelector('#welcome h1').textContent = '恢复已有终端';
24
+ let session, ws, term, observer, retryTimer, socketTimer, checking, visibilityTimer;
25
+ let socketReady = false;
26
+ let epoch = 0, failures = 0, stopped = false, pageHidden = false, offline = false, busy = false;
27
+ try { session = JSON.parse(sessionStorage.getItem(storageKey) || 'null'); } catch {}
28
+ const awake = () => !pageHidden && !document.hidden && !offline;
29
+ const post = async (route, data, controller = new AbortController()) => {
30
+ const timer = setTimeout(() => controller.abort(), 10000);
31
+ try {
32
+ const response = await fetch(base + '/api/' + route, { method:'POST', credentials:'omit', cache:'no-store',
33
+ signal:controller.signal, headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) });
34
+ let value;
35
+ try { value = await response.json(); } catch {
36
+ throw Object.assign(new Error('服务返回了非 JSON 响应'), {httpStatus:response.status});
37
+ }
38
+ if (!response.ok) throw Object.assign(new Error(value.error || '请求失败'), {httpStatus:response.status});
39
+ return value;
40
+ } finally { clearTimeout(timer); }
41
+ };
42
+ function message(error) {
43
+ if (error?.name === 'AbortError') return '请求超时';
44
+ if (!error?.httpStatus && (error?.name === 'TypeError' || error?.message === 'Failed to fetch')) return '网络暂时不可用';
45
+ return error?.message || String(error);
46
+ }
47
+ function buttons(retry = false) {
48
+ for (const button of [connect, welcomeConnect]) {
49
+ button.textContent = session ? '重新连接' : recovery ? '请求恢复连接' : '请求连接';
50
+ button.disabled = busy || (!!session && !retry);
51
+ }
52
+ close.disabled = busy || !session;
53
+ }
54
+ function invalidate() {
55
+ epoch++; clearTimeout(retryTimer); clearTimeout(socketTimer);
56
+ clearTimeout(visibilityTimer); socketReady = false;
57
+ checking?.abort(); checking = undefined;
58
+ const old = ws; ws = undefined; old?.close(); interrupt.disabled = true;
59
+ if (term) term.options.disableStdin = true;
60
+ }
61
+ function forget() {
62
+ session = undefined; sessionStorage.removeItem(storageKey);
63
+ document.getElementById('code').textContent = ''; buttons(true);
64
+ }
65
+ function end(text) { stopped = true; invalidate(); forget(); status.textContent = text; }
66
+ function fail(text) { stopped = true; invalidate(); status.textContent = text; buttons(true); }
67
+ function schedule(delay) {
68
+ clearTimeout(retryTimer);
69
+ if (!stopped && awake() && session) retryTimer = setTimeout(check, delay);
70
+ }
71
+ function retry(stage, error) {
72
+ if (stopped || !session) return;
73
+ const seconds = Math.min(10, Math.pow(2, Math.min(failures++, 4)));
74
+ status.textContent = stage + ':' + message(error) + ',' + seconds + ' 秒后自动重试';
75
+ buttons(true); schedule(seconds * 1000);
76
+ }
77
+ function sendSize(cols, rows) {
78
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({type:'resize',cols:Math.max(2,Math.min(500,cols)),rows:Math.max(2,Math.min(200,rows))}));
79
+ }
80
+ function reportVisibility() {
81
+ clearTimeout(visibilityTimer);
82
+ if (stopped || !socketReady || ws?.readyState !== WebSocket.OPEN) return;
83
+ const foreground = awake();
84
+ ws.send(JSON.stringify({type:'visibility',foreground}));
85
+ if (foreground) visibilityTimer = setTimeout(reportVisibility, 30000);
86
+ }
87
+ function openSocket() {
88
+ if (stopped || !awake() || ws || !session) return;
89
+ const current = session, generation = epoch;
90
+ const url = new URL(base + '/socket', location.origin); url.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
91
+ let socket;
92
+ try { socket = new WebSocket(url); } catch (error) { retry('WebSocket 连接失败', error); return; }
93
+ ws = socket; status.textContent = '授权已通过,正在连接终端…'; buttons();
94
+ const valid = () => ws === socket && generation === epoch && session === current && !stopped;
95
+ socketTimer = setTimeout(() => {
96
+ if (!valid()) return;
97
+ invalidate(); retry('WebSocket 连接超时', new Error('请检查网络是否支持 WebSocket'));
98
+ }, 15000);
99
+ socket.onopen = () => { if (valid()) socket.send(JSON.stringify({type:'auth',requestId:current.requestId,browserKey:current.browserKey})); };
100
+ socket.onmessage = (event) => {
101
+ if (!valid()) return;
102
+ try {
103
+ const result = JSON.parse(event.data);
104
+ if (result.type === 'ready') {
105
+ clearTimeout(socketTimer);
106
+ socketReady = true; reportVisibility();
107
+ document.body.classList.add('connected');
108
+ observer?.disconnect(); term?.dispose();
109
+ const terminal = new Terminal({cursorBlink:true,fontSize:14,scrollback:5000,disableStdin:result.replay === true,theme:{background:'#10141b'}});
110
+ term = terminal;
111
+ const active = () => valid() && term === terminal;
112
+ terminal.attachCustomKeyEventHandler((event) => {
113
+ if (event.altKey || event.shiftKey || event.ctrlKey === event.metaKey) return true;
114
+ const key = event.key.toLowerCase();
115
+ // Let the browser dispatch native clipboard events, including on HTTP.
116
+ // xterm handles the selected text and bracketed paste without sending ^C/^V.
117
+ return !(key === 'v' || (key === 'c' && terminal.hasSelection()));
118
+ });
119
+ const fit = new FitAddon.FitAddon(); terminal.loadAddon(fit); terminal.open(document.getElementById('terminal'));
120
+ terminal.onData((data) => {
121
+ if (active() && !terminal.options.disableStdin && socket.readyState === WebSocket.OPEN) for (let i=0; i<data.length; i+=4096) socket.send(JSON.stringify({type:'input',data:data.slice(i,i+4096)}));
122
+ });
123
+ terminal.onResize(({cols,rows}) => { if (active()) sendSize(cols,rows); });
124
+ observer = new ResizeObserver(() => { if (active()) fit.fit(); });
125
+ observer.observe(document.getElementById('terminal'));
126
+ fit.fit(); sendSize(terminal.cols,terminal.rows); failures = 0; interrupt.disabled = true;
127
+ status.textContent = '正在恢复终端画面…';
128
+ const restored = () => {
129
+ if (!active()) return;
130
+ terminal.options.disableStdin = false; terminal.focus();
131
+ status.textContent = '已连接 · ' + result.cwd; interrupt.disabled = false; buttons();
132
+ };
133
+ // xterm parses asynchronously. Keep replay silent until its write callback,
134
+ // before subsequently queued live output is parsed. Startup output is live.
135
+ if (result.output) terminal.write(result.output, restored); else restored();
136
+ } else if (result.type === 'data') term?.write(result.data);
137
+ else if (result.type === 'exit') end(result.message || '终端已结束');
138
+ else if (result.type === 'error') fail((result.stage === 'spawn' ? 'Shell 启动失败:' : '终端连接被拒绝:') + result.message);
139
+ } catch (error) { fail('终端界面初始化或消息处理失败:' + message(error)); }
140
+ };
141
+ socket.onclose = (event) => {
142
+ if (!valid()) return;
143
+ if (event.reason === 'Reconnected from another page') { fail('该终端已在另一页面连接,点击重新连接可恢复。'); return; }
144
+ invalidate(); retry('WebSocket 已断开', new Error('连接关闭' + (event.code ? '(' + event.code + ')' : '')));
145
+ };
146
+ socket.onerror = () => {
147
+ if (!valid()) return;
148
+ invalidate(); retry('WebSocket 连接失败', new Error('请检查网络是否支持 WebSocket'));
149
+ };
150
+ }
151
+ async function check() {
152
+ if (stopped || !awake() || !session || checking || ws || busy) return;
153
+ clearTimeout(retryTimer);
154
+ const current = session, generation = epoch, controller = new AbortController(); checking = controller;
155
+ try {
156
+ const result = await post('status', current, controller);
157
+ if (generation !== epoch || current !== session || stopped || !awake()) return;
158
+ if (result.state === 'approved' || result.state === 'running') openSocket();
159
+ else if (result.state === 'pending') { failures = 0; status.textContent = '等待原会话中的所有者授权'; buttons(); schedule(1500); }
160
+ else end(result.message || '授权已失效,请重新请求连接');
161
+ } catch (error) {
162
+ if (generation !== epoch || current !== session || stopped || !awake()) return;
163
+ if ([400,401,404,410].includes(error.httpStatus)) end('授权状态查询失败:' + message(error) + '。请重新请求连接。');
164
+ else if (error.httpStatus === 403) fail('授权状态查询被拒绝:' + message(error));
165
+ else retry('暂时无法查询授权状态', error);
166
+ } finally { if (checking === controller) checking = undefined; }
167
+ }
168
+ connect.onclick = welcomeConnect.onclick = async () => {
169
+ if (busy) return;
170
+ stopped = false; failures = 0; invalidate();
171
+ if (session) { status.textContent = '正在恢复已有终端授权…'; buttons(); check(); return; }
172
+ busy = true; buttons(); status.textContent = '正在请求飞书授权…';
173
+ try {
174
+ document.body.classList.remove('connected');
175
+ if (!globalThis.crypto?.getRandomValues) throw new Error('浏览器不支持安全随机数,请使用新版浏览器。');
176
+ const browserKey = Array.from(crypto.getRandomValues(new Uint8Array(32)), (n) => n.toString(16).padStart(2,'0')).join('');
177
+ const result = await post('request', {launch:location.pathname,browserKey});
178
+ session = {requestId:result.requestId,browserKey,code:result.code}; sessionStorage.setItem(storageKey,JSON.stringify(session));
179
+ document.getElementById('code').textContent = '验证码:' + result.code;
180
+ status.textContent = awake() ? '等待原会话中的所有者授权' : '返回网页后将自动查询授权状态';
181
+ } catch (error) { status.textContent = '请求授权失败:' + message(error); }
182
+ finally { busy = false; buttons(!session); }
183
+ check();
184
+ };
185
+ interrupt.onclick = () => { if (ws?.readyState === WebSocket.OPEN && !interrupt.disabled) ws.send(JSON.stringify({type:'input',data:String.fromCharCode(3)})); };
186
+ close.onclick = async () => {
187
+ if (busy || !session || !confirm('关闭该终端和 Shell?已脱离终端的后台进程可能继续运行。')) return;
188
+ busy = true; stopped = true; invalidate(); buttons();
189
+ try { await post('close',session); end('终端已关闭'); }
190
+ catch (error) {
191
+ if ([400,401,404,410].includes(error.httpStatus)) end('终端已失效或结束');
192
+ else status.textContent = '关闭请求失败:' + message(error) + ';未确认关闭,请重试';
193
+ } finally { busy = false; buttons(true); }
194
+ };
195
+ function suspend() {
196
+ invalidate();
197
+ if (session && !stopped) { status.textContent = '连接已暂停,返回网页或网络恢复后自动重连'; buttons(true); }
198
+ }
199
+ function resume() {
200
+ if (stopped || !awake() || busy || !session) return;
201
+ clearTimeout(retryTimer); check();
202
+ }
203
+ window.addEventListener('pagehide', () => { pageHidden = true; suspend(); });
204
+ window.addEventListener('pageshow', () => { pageHidden = false; resume(); });
205
+ document.addEventListener('visibilitychange', () => { reportVisibility(); if (document.hidden) { if (!ws) suspend(); } else resume(); });
206
+ window.addEventListener('offline', () => { offline = true; suspend(); });
207
+ window.addEventListener('online', () => { offline = false; resume(); });
208
+ window.addEventListener('focus', resume);
209
+ if (session?.code) document.getElementById('code').textContent = '验证码:' + session.code;
210
+ buttons(); check();
211
+ })();`;
212
+ //# sourceMappingURL=TerminalPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TerminalPage.js","sourceRoot":"","sources":["../../src/terminal/TerminalPage.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,QAAQ,GAAG,KAAK;IACzD,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACnI,OAAO;+DACsD,IAAI,mDAAmD,IAAI;qBACrG,IAAI,iDAAiD,IAAI,+CAA+C,IAAI;mBAC9G,IAAI,oBAAoB,QAAQ;;iIAE8E,CAAC;AAClI,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG;;;CAG3B,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAqMnC,CAAC"}
@@ -0,0 +1,95 @@
1
+ import fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { powerShellTrackingScript, TerminalCommandTracker } from "./TerminalCommandTracker.js";
6
+ export function terminalShell(platform = process.platform, env = process.env, exists = fs.existsSync, loginShell = os.userInfo().shell) {
7
+ if (platform === "win32") {
8
+ const directories = (env.PATH ?? env.Path ?? "").split(";").filter(Boolean);
9
+ const pwsh = directories.map((dir) => path.win32.join(dir, "pwsh.exe")).find(exists);
10
+ if (pwsh)
11
+ return { file: pwsh, args: ["-NoLogo"] };
12
+ const windows = env.SystemRoot ?? "C:/Windows";
13
+ const powershell = path.win32.join(windows, "System32/WindowsPowerShell/v1.0/powershell.exe");
14
+ return exists(powershell) ? { file: powershell, args: ["-NoLogo"] }
15
+ : { file: path.win32.join(windows, "System32/cmd.exe"), args: [] };
16
+ }
17
+ const file = [env.SHELL, loginShell, platform === "darwin" ? "/bin/zsh" : "/bin/bash", "/bin/sh"]
18
+ .find((candidate) => Boolean(candidate && path.posix.isAbsolute(candidate) && exists(candidate)));
19
+ if (!file)
20
+ throw new Error("No interactive Shell is available.");
21
+ return { file, args: ["-i"] };
22
+ }
23
+ export function terminalEnvironment(env = process.env) {
24
+ return {
25
+ ...Object.fromEntries(Object.entries(env).filter(([key]) => !/^(FEISHU_|AGENT_BOT(?:_|$)|NODE_OPTIONS$)/i.test(key))),
26
+ TERM: "xterm-256color", COLORTERM: "truecolor",
27
+ };
28
+ }
29
+ export function prepareTerminalHelper(packageRoot, arch = process.arch, filesystem = fs) {
30
+ // node-pty 1.1.0 ships macOS spawn-helper binaries without executable bits.
31
+ for (const directory of ["build/Release", "build/Debug", `prebuilds/darwin-${arch}`]) {
32
+ const helper = path.join(packageRoot, directory, "spawn-helper");
33
+ if (!filesystem.existsSync(helper))
34
+ continue;
35
+ const stat = filesystem.statSync(helper);
36
+ if (!stat.isFile())
37
+ throw new Error(`Invalid terminal helper: ${helper}`);
38
+ if ((stat.mode & 0o111) === 0o111)
39
+ continue;
40
+ try {
41
+ filesystem.chmodSync(helper, (stat.mode & 0o777) | 0o111);
42
+ }
43
+ catch (cause) {
44
+ throw new Error(`Terminal helper is not executable: ${helper}. Reinstall node-pty or restore its executable permission.`, { cause });
45
+ }
46
+ }
47
+ }
48
+ export const spawnTerminal = async (cwd, cols, rows) => {
49
+ if (process.platform === "win32" && Number(os.release().split(".")[2] ?? 0) < 18309) {
50
+ throw new Error("网页终端需要支持 ConPTY 的 Windows 10 1903 / Windows Server 2022 或更新系统。");
51
+ }
52
+ if (!fs.statSync(cwd).isDirectory())
53
+ throw new Error("Terminal working directory is not a directory.");
54
+ let pty;
55
+ try {
56
+ pty = await import("node-pty");
57
+ }
58
+ catch {
59
+ throw new Error("交互终端组件 node-pty 不可用,请重新安装 AgentBot 的可选依赖(可能需要本机编译工具)。");
60
+ }
61
+ if (process.platform === "darwin") {
62
+ const require = createRequire(import.meta.url);
63
+ prepareTerminalHelper(path.dirname(require.resolve("node-pty/package.json")));
64
+ }
65
+ const shell = terminalShell();
66
+ const tracker = new TerminalCommandTracker();
67
+ const powershell = /^(pwsh|powershell)(\.exe)?$/iu.test(path.basename(shell.file));
68
+ const args = powershell ? [...shell.args, "-NoExit", "-EncodedCommand",
69
+ Buffer.from(powerShellTrackingScript(tracker.nonce), "utf16le").toString("base64")] : shell.args;
70
+ const processPty = pty.spawn(shell.file, args, {
71
+ cwd, cols, rows, name: "xterm-256color", env: terminalEnvironment(), useConpty: true,
72
+ // The bundled ConPTY closes via its input pipe, avoiding node-pty's console-list helper on kill.
73
+ useConptyDll: process.platform === "win32",
74
+ });
75
+ return {
76
+ write: (data) => processPty.write(data), resize: (cols, rows) => processPty.resize(cols, rows), kill: () => processPty.kill(),
77
+ onExit: (listener) => processPty.onExit(listener),
78
+ onData: (listener) => processPty.onData((data) => { const visible = tracker.consume(data); if (visible)
79
+ listener(visible); }),
80
+ commandState: () => {
81
+ const tracked = tracker.snapshot();
82
+ if (tracked.status !== "unknown" || process.platform === "win32")
83
+ return tracked;
84
+ try {
85
+ const name = processPty.process;
86
+ // Unix PTYs can report a foreground process even without shell integration.
87
+ if (name && path.basename(name) !== path.basename(shell.file))
88
+ return { status: "running", command: name, source: "process" };
89
+ }
90
+ catch { /* An exited PTY may no longer have a foreground process. */ }
91
+ return tracked;
92
+ },
93
+ };
94
+ };
95
+ //# sourceMappingURL=TerminalProcess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TerminalProcess.js","sourceRoot":"","sources":["../../src/terminal/TerminalProcess.ts"],"names":[],"mappings":"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"}