@keyou007/agent-bot 0.1.24-alpha.6 → 0.1.24-alpha.8

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 (50) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +6 -4
  3. package/README.zh.md +6 -4
  4. package/dist/codex/CodexEventMapper.js +1 -0
  5. package/dist/codex/CodexEventMapper.js.map +1 -1
  6. package/dist/codex/CodexProcessManager.js +3 -1
  7. package/dist/codex/CodexProcessManager.js.map +1 -1
  8. package/dist/codex/CodexRuntime.js +70 -8
  9. package/dist/codex/CodexRuntime.js.map +1 -1
  10. package/dist/codex/CodexVersion.js +2 -2
  11. package/dist/codex/CodexVersion.js.map +1 -1
  12. package/dist/codex/ProviderSwitch.js +1 -1
  13. package/dist/codex/ProviderSwitch.js.map +1 -1
  14. package/dist/codex/TraePlanEntry.js +70 -0
  15. package/dist/codex/TraePlanEntry.js.map +1 -0
  16. package/dist/feishu/CardRenderer.js +27 -1
  17. package/dist/feishu/CardRenderer.js.map +1 -1
  18. package/dist/index.js +16 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/proxy/ProxySessionController.js +5 -0
  21. package/dist/proxy/ProxySessionController.js.map +1 -1
  22. package/dist/supervision/SafeRestartNotifier.js +32 -1
  23. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  24. package/dist/supervision/SafeRestartScheduler.js +3 -0
  25. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  26. package/dist/terminal/TerminalCommandTracker.js +74 -0
  27. package/dist/terminal/TerminalCommandTracker.js.map +1 -0
  28. package/dist/terminal/TerminalProcess.js +25 -1
  29. package/dist/terminal/TerminalProcess.js.map +1 -1
  30. package/dist/terminal/TerminalServer.js +44 -7
  31. package/dist/terminal/TerminalServer.js.map +1 -1
  32. package/docs/technical-reference.md +2 -0
  33. package/docs/technical-reference.zh.md +2 -0
  34. package/npm-shrinkwrap.json +2 -2
  35. package/package.json +1 -1
  36. package/skills/agent-bot/SKILL.md +6 -4
  37. package/src/codex/CodexEventMapper.ts +1 -0
  38. package/src/codex/CodexProcessManager.ts +3 -1
  39. package/src/codex/CodexRuntime.ts +64 -8
  40. package/src/codex/CodexVersion.ts +2 -2
  41. package/src/codex/ProviderSwitch.ts +1 -1
  42. package/src/codex/TraePlanEntry.ts +63 -0
  43. package/src/feishu/CardRenderer.ts +30 -2
  44. package/src/index.ts +14 -3
  45. package/src/proxy/ProxySessionController.ts +4 -0
  46. package/src/supervision/SafeRestartNotifier.ts +34 -2
  47. package/src/supervision/SafeRestartScheduler.ts +7 -0
  48. package/src/terminal/TerminalCommandTracker.ts +74 -0
  49. package/src/terminal/TerminalProcess.ts +24 -2
  50. package/src/terminal/TerminalServer.ts +45 -7
@@ -3,8 +3,11 @@ import { createRequire } from "node:module";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import type { IPty } from "node-pty";
6
+ import { powerShellTrackingScript, TerminalCommandTracker, type TerminalCommandState } from "./TerminalCommandTracker.js";
6
7
 
7
- export type TerminalProcess = Pick<IPty, "write" | "resize" | "kill" | "onData" | "onExit">;
8
+ export type TerminalProcess = Pick<IPty, "write" | "resize" | "kill" | "onData" | "onExit"> & {
9
+ commandState?(): TerminalCommandState;
10
+ };
8
11
  export type TerminalSpawner = (cwd: string, cols: number, rows: number) => Promise<TerminalProcess>;
9
12
 
10
13
  export function terminalShell(
@@ -63,9 +66,28 @@ export const spawnTerminal: TerminalSpawner = async (cwd, cols, rows) => {
63
66
  prepareTerminalHelper(path.dirname(require.resolve("node-pty/package.json")));
64
67
  }
65
68
  const shell = terminalShell();
66
- return pty.spawn(shell.file, shell.args, {
69
+ const tracker = new TerminalCommandTracker();
70
+ const powershell = /^(pwsh|powershell)(\.exe)?$/iu.test(path.basename(shell.file));
71
+ const args = powershell ? [...shell.args, "-NoExit", "-EncodedCommand",
72
+ Buffer.from(powerShellTrackingScript(tracker.nonce), "utf16le").toString("base64")] : shell.args;
73
+ const processPty = pty.spawn(shell.file, args, {
67
74
  cwd, cols, rows, name: "xterm-256color", env: terminalEnvironment(), useConpty: true,
68
75
  // The bundled ConPTY closes via its input pipe, avoiding node-pty's console-list helper on kill.
69
76
  useConptyDll: process.platform === "win32",
70
77
  });
78
+ return {
79
+ write: (data) => processPty.write(data), resize: (cols, rows) => processPty.resize(cols, rows), kill: () => processPty.kill(),
80
+ onExit: (listener) => processPty.onExit(listener),
81
+ onData: (listener) => processPty.onData((data) => { const visible = tracker.consume(data); if (visible) listener(visible); }),
82
+ commandState: () => {
83
+ const tracked = tracker.snapshot();
84
+ if (tracked.status !== "unknown" || process.platform === "win32") return tracked;
85
+ try {
86
+ const name = processPty.process;
87
+ // Unix PTYs can report a foreground process even without shell integration.
88
+ if (name && path.basename(name) !== path.basename(shell.file)) return { status: "running", command: name, source: "process" };
89
+ } catch { /* An exited PTY may no longer have a foreground process. */ }
90
+ return tracked;
91
+ },
92
+ };
71
93
  };
@@ -12,6 +12,7 @@ import type { CardAction } from "../feishu/types.js";
12
12
  import { spawnTerminal, type TerminalProcess, type TerminalSpawner } from "./TerminalProcess.js";
13
13
  import { terminalPage, TERMINAL_CSS, TERMINAL_SCRIPT } from "./TerminalPage.js";
14
14
  import { TerminalLinks } from "./TerminalLinks.js";
15
+ import type { TerminalCommandState } from "./TerminalCommandTracker.js";
15
16
 
16
17
  const require = createRequire(import.meta.url);
17
18
  const AUTH_TTL = 5 * 60_000;
@@ -36,6 +37,11 @@ export interface TerminalCardView extends TerminalAuthorization {
36
37
  terminalId: string; reopenToken: string; reopenUrl: string; recovery: boolean; state: "pending" | "approved" | "running" | "closed";
37
38
  connected: boolean; message?: string;
38
39
  }
40
+ export interface ActiveTerminal {
41
+ terminalId: string; token: string; cwd: string;
42
+ state: "starting" | "running" | "closing"; connected: boolean;
43
+ active: boolean; foreground: boolean; command: TerminalCommandState;
44
+ }
39
45
  export interface TerminalServerOptions {
40
46
  stateDirectory: string;
41
47
  ownerId(): string | undefined;
@@ -133,6 +139,37 @@ export class TerminalServer {
133
139
  return [...this.sessions.values()].filter((s) => s.pty || s.starting).length;
134
140
  }
135
141
 
142
+ listActiveTerminals(): ActiveTerminal[] {
143
+ return [...this.sessions.values()].filter((s) => s.pty || s.starting).map((s) => {
144
+ const foreground = s.socket?.readyState === WebSocket.OPEN && (s.foregroundUntil ?? 0) > this.now();
145
+ let command: TerminalCommandState = { status: "unknown" };
146
+ if (s.state !== "closed") {
147
+ try { command = s.pty?.commandState?.() ?? command; }
148
+ catch { /* Command inspection must not prevent safe-restart activity checks. */ }
149
+ }
150
+ return {
151
+ terminalId: s.requestId, token: s.reopenToken, cwd: s.cwd,
152
+ state: s.state === "closed" ? "closing" : s.pty ? "running" : "starting",
153
+ connected: s.socket?.readyState === WebSocket.OPEN,
154
+ active: s.state !== "closed" && (foreground || command.status === "running" || this.now() - s.lastActivity < 30_000),
155
+ foreground, command,
156
+ };
157
+ });
158
+ }
159
+
160
+ async stopActiveTerminal(terminalId: string, userId: string): Promise<boolean> {
161
+ const ownerId = this.options.ownerId();
162
+ if (!ownerId || userId !== ownerId) throw new Error("仅 AgentBot 所有者可结束终端。");
163
+ const session = this.sessions.get(terminalId);
164
+ if (!session || session.state === "closed" || (!session.pty && !session.starting)) return false;
165
+ if (session.ownerId !== userId) throw new Error("终端所有者已变更,请刷新状态。");
166
+ // Keep blocking restart if the OS refuses to terminate the Shell.
167
+ if (session.pty) session.pty.kill();
168
+ this.disposeSession(session, "所有者已从安全重启卡片结束终端", true);
169
+ await this.cardUpdates;
170
+ return true;
171
+ }
172
+
136
173
  async resolveApproval(action: CardAction): Promise<string> {
137
174
  this.sweep();
138
175
  const id = String(action.value.requestId ?? "");
@@ -425,11 +462,13 @@ export class TerminalServer {
425
462
  this.assertCanStart();
426
463
  const starting = (async () => {
427
464
  const pty = await (this.options.spawn ?? spawnTerminal)(session.cwd, 100, 30);
465
+ session.pty = pty;
466
+ pty.onExit(({ exitCode }) => { session.pty = undefined; this.disposeSession(session, `Shell 已退出(${exitCode})`); });
428
467
  if (this.closed || this.options.canStart?.() === false || session.state === "closed"
429
468
  || session.ownerId !== this.options.ownerId() || this.now() >= session.expiresAt) {
430
- pty.kill(); this.disposeSession(session, "服务正在重启或授权已失效"); return;
469
+ this.disposeSession(session, "服务正在重启或授权已失效", true); pty.kill(); return;
431
470
  }
432
- session.pty = pty; session.state = "running"; session.lastActivity = this.now();
471
+ session.state = "running"; session.lastActivity = this.now();
433
472
  this.markDisconnected(session);
434
473
  pty.onData((data) => {
435
474
  if (session.state !== "running") return;
@@ -437,7 +476,6 @@ export class TerminalServer {
437
476
  session.output = (session.output + data).slice(-256 * 1024);
438
477
  if (session.socket) this.send(session.socket, { type: "data", data });
439
478
  });
440
- pty.onExit(({ exitCode }) => { session.pty = undefined; this.disposeSession(session, `Shell 已退出(${exitCode})`); });
441
479
  this.options.logger.info({ requestId: session.requestId }, "Interactive terminal started.");
442
480
  })();
443
481
  session.starting = starting;
@@ -469,16 +507,16 @@ export class TerminalServer {
469
507
  if (s.state === "running" && now - Math.max(s.lastActivity, s.foregroundUntil ?? 0) >= IDLE_TTL) {
470
508
  this.disposeSession(s, "连续 30 分钟无输入或输出,Shell 已自动关闭");
471
509
  }
472
- if (s.state === "closed" && !s.starting && now - s.lastActivity >= AUTH_TTL) this.sessions.delete(s.requestId);
510
+ if (s.state === "closed" && !s.pty && !s.starting && now - s.lastActivity >= AUTH_TTL) this.sessions.delete(s.requestId);
473
511
  }
474
512
  }
475
- private disposeSession(s: TerminalSession, message: string): void {
513
+ private disposeSession(s: TerminalSession, message: string, waitForExit = false): void {
476
514
  if (s.state === "closed") return;
477
515
  this.clearDisconnectTimer(s);
478
516
  s.state = "closed"; s.message = message; s.lastActivity = this.now();
479
- const pty = s.pty; s.pty = undefined;
517
+ const pty = s.pty; if (!waitForExit) s.pty = undefined;
480
518
  if (s.socket) { this.send(s.socket, { type: "exit", message }); s.socket.close(); s.socket = undefined; }
481
- try { pty?.kill(); } catch (error) { this.options.logger.warn({ error, requestId: s.requestId }, "Failed to close terminal process."); }
519
+ try { if (!waitForExit) pty?.kill(); } catch (error) { this.options.logger.warn({ error, requestId: s.requestId }, "Failed to close terminal process."); }
482
520
  s.output = "";
483
521
  for (const recovery of this.recoveries.values()) {
484
522
  if (recovery.targetId === s.requestId) this.closeRecovery(recovery, message);