@keyou007/agent-bot 0.1.13-alpha.1 → 0.1.13-alpha.2

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 (57) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +106 -103
  3. package/README.zh.md +76 -73
  4. package/dist/acp/AcpProcessManager.js +8 -3
  5. package/dist/acp/AcpProcessManager.js.map +1 -1
  6. package/dist/acp/AcpSessionManager.js +15 -0
  7. package/dist/acp/AcpSessionManager.js.map +1 -1
  8. package/dist/cli/help.js +4 -2
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/serverStatus.js +27 -0
  11. package/dist/cli/serverStatus.js.map +1 -1
  12. package/dist/cli/taskListOutput.js +72 -0
  13. package/dist/cli/taskListOutput.js.map +1 -0
  14. package/dist/cli.js +49 -54
  15. package/dist/cli.js.map +1 -1
  16. package/dist/codex/CodexProcessManager.js +34 -4
  17. package/dist/codex/CodexProcessManager.js.map +1 -1
  18. package/dist/codex/CodexRuntime.js +3 -0
  19. package/dist/codex/CodexRuntime.js.map +1 -1
  20. package/dist/feishu/CardRenderer.js +43 -2
  21. package/dist/feishu/CardRenderer.js.map +1 -1
  22. package/dist/feishu/FeishuConnector.js +16 -5
  23. package/dist/feishu/FeishuConnector.js.map +1 -1
  24. package/dist/index.js +23 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/proxy/ProxySessionController.js +157 -59
  27. package/dist/proxy/ProxySessionController.js.map +1 -1
  28. package/dist/runtime/AcpRuntimeAdapter.js +3 -0
  29. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  30. package/dist/runtime/agentEnvironment.js +24 -4
  31. package/dist/runtime/agentEnvironment.js.map +1 -1
  32. package/dist/runtime/createAgentRuntimeRegistry.js +7 -3
  33. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
  34. package/dist/state/StateStore.js +12 -9
  35. package/dist/state/StateStore.js.map +1 -1
  36. package/docs/technical-reference.md +9 -3
  37. package/docs/technical-reference.zh.md +9 -3
  38. package/npm-shrinkwrap.json +2 -2
  39. package/package.json +1 -1
  40. package/skills/agent-bot/SKILL.md +53 -22
  41. package/src/acp/AcpProcessManager.ts +16 -3
  42. package/src/acp/AcpSessionManager.ts +14 -0
  43. package/src/cli/help.ts +4 -2
  44. package/src/cli/serverStatus.ts +28 -0
  45. package/src/cli/taskListOutput.ts +91 -0
  46. package/src/cli.ts +66 -51
  47. package/src/codex/CodexProcessManager.ts +38 -3
  48. package/src/codex/CodexRuntime.ts +6 -0
  49. package/src/feishu/CardRenderer.ts +61 -1
  50. package/src/feishu/FeishuConnector.ts +13 -4
  51. package/src/index.ts +35 -3
  52. package/src/proxy/ProxySessionController.ts +186 -59
  53. package/src/runtime/AcpRuntimeAdapter.ts +5 -0
  54. package/src/runtime/agentEnvironment.ts +41 -3
  55. package/src/runtime/createAgentRuntimeRegistry.ts +18 -3
  56. package/src/runtime/types.ts +5 -0
  57. package/src/state/StateStore.ts +12 -9
@@ -3,7 +3,10 @@ import readline from "node:readline";
3
3
  import type { Logger } from "pino";
4
4
  import type { AgentConfig } from "../config/schema.js";
5
5
  import { AcpJsonRpcConnection } from "./AcpJsonRpcConnection.js";
6
- import { agentBotEnvironment } from "../runtime/agentEnvironment.js";
6
+ import {
7
+ agentBotEnvironment,
8
+ type AgentEnvironmentContext,
9
+ } from "../runtime/agentEnvironment.js";
7
10
  import { spawnStdioCommand } from "../utils/spawnCommand.js";
8
11
 
9
12
  export interface ManagedAcpProcess {
@@ -16,14 +19,24 @@ export interface ManagedAcpProcess {
16
19
  export class AcpProcessManager {
17
20
  private readonly processes = new Map<string, ManagedAcpProcess>();
18
21
 
19
- constructor(private readonly logger: Logger) {}
22
+ constructor(
23
+ private readonly logger: Logger,
24
+ private readonly environmentContext: () => AgentEnvironmentContext = () => ({}),
25
+ ) {}
20
26
 
21
27
  start(processKey: string, agentName: string, agent: AgentConfig): ManagedAcpProcess {
22
28
  if (this.processes.has(processKey)) {
23
29
  throw new Error(`ACP process already exists: ${processKey}`);
24
30
  }
25
31
 
26
- const child = spawnStdioCommand(agent.command, agent.args, agentBotEnvironment(process.env, agent.env));
32
+ const child = spawnStdioCommand(
33
+ agent.command,
34
+ agent.args,
35
+ agentBotEnvironment(process.env, agent.env, {
36
+ ...this.environmentContext(),
37
+ agentName,
38
+ }),
39
+ );
27
40
 
28
41
  const childLogger = this.logger.child({ processKey, agentName });
29
42
  const connection = new AcpJsonRpcConnection(child, childLogger);
@@ -40,6 +40,7 @@ export class AcpSessionManager {
40
40
  private readonly sessions = new Map<string, RuntimeSession>();
41
41
  private readonly sessionsByAcpId = new Map<string, RuntimeSession>();
42
42
  private processStart?: Promise<ManagedAcpProcess>;
43
+ private processVersion?: string;
43
44
 
44
45
  constructor(
45
46
  private readonly agentName: string,
@@ -52,6 +53,15 @@ export class AcpSessionManager {
52
53
  return this.sessions.get(localSessionId);
53
54
  }
54
55
 
56
+ getProcessInfo(): { pid?: number; version?: string } {
57
+ const pid = this.processManager.get(this.agentName)?.child.pid;
58
+ if (!pid) return {};
59
+ return {
60
+ pid,
61
+ ...(this.processVersion ? { version: this.processVersion } : {}),
62
+ };
63
+ }
64
+
55
65
  async create(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
56
66
  if (input.agentName !== this.agentName) throw new Error(`Unexpected agent: ${input.agentName}`);
57
67
 
@@ -133,6 +143,7 @@ export class AcpSessionManager {
133
143
  this.sessions.clear();
134
144
  this.sessionsByAcpId.clear();
135
145
  this.processStart = undefined;
146
+ this.processVersion = undefined;
136
147
  this.processManager.stopAll();
137
148
  }
138
149
 
@@ -181,6 +192,7 @@ export class AcpSessionManager {
181
192
  }
182
193
 
183
194
  private async startProcess(): Promise<ManagedAcpProcess> {
195
+ this.processVersion = undefined;
184
196
  const managed = this.processManager.start(this.agentName, this.agentName, this.agent);
185
197
  try {
186
198
  const connection = managed.connection;
@@ -210,6 +222,8 @@ export class AcpSessionManager {
210
222
  version: "0.1.0",
211
223
  },
212
224
  });
225
+ const version = initializeResult.agentInfo?.version?.trim();
226
+ if (version) this.processVersion = version;
213
227
  if (initializeResult.authMethods?.length) {
214
228
  const method = initializeResult.authMethods[0];
215
229
  this.logger.info({ methodId: method.id }, "Authenticating ACP agent with first advertised method.");
package/src/cli/help.ts CHANGED
@@ -13,10 +13,11 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
13
13
  init --reset 重置并重新配置显式指定的 Profile
14
14
  console 打开本地控制台
15
15
  server start 在后台启动 Agent Bot
16
- server status 显示服务、飞书 App ID 和安全重启状态
16
+ server status 显示服务、飞书 App ID、Agent 进程和安全重启状态
17
17
  server restart [--task <任务>] 安排安全重启并将状态发回指定任务会话
18
18
  server stop 停止 Agent Bot
19
19
  task list 列出任务
20
+ task current [--json] 显示当前调用任务的详情
20
21
  task status <任务> 显示任务状态
21
22
  task chat <任务> 显示任务绑定的飞书会话
22
23
  task prompt <任务> <提示词> 向任务发送提示词
@@ -66,10 +67,11 @@ Common commands:
66
67
  init --reset Reset and reconfigure an explicit profile
67
68
  console Open the local console
68
69
  server start Start Agent Bot in the background
69
- server status Show the server, Lark App ID, and safe-restart status
70
+ server status Show the server, Lark App ID, Agent processes, and safe-restart status
70
71
  server restart [--task <task>] Schedule a safe restart and return status to the task's conversation
71
72
  server stop Stop Agent Bot
72
73
  task list List tasks
74
+ task current [--json] Show details for the task invoking the CLI
73
75
  task status <task> Show task status
74
76
  task chat <task> Show the task's Lark chat binding
75
77
  task prompt <task> <prompt> Send a Prompt to a task
@@ -29,16 +29,44 @@ export function formatServerStatus(data: unknown, language: CliLanguage = cliLan
29
29
  language,
30
30
  )
31
31
  : cliText("not scheduled", "未计划", language);
32
+ const agents = formatAgentProcesses(value.agents, language);
32
33
  return [
33
34
  `${cliText("Agent Bot server: ", "Agent Bot 服务:", language)}${state}`,
34
35
  `${cliText("Lark App ID: ", "飞书 App ID:", language)}${value.feishuAppId ?? cliText("not configured", "未配置", language)}`,
35
36
  `${cliText("PID: ", "PID:", language)}${value.pid ?? "-"}`,
36
37
  `${cliText("Started at: ", "启动时间:", language)}${value.startedAt ?? "-"}`,
37
38
  `${cliText("Supervisor: ", "Supervisor:", language)}${value.supervised ? cliText("enabled", "已启用", language) : cliText("disabled", "未启用", language)}`,
39
+ ...agents,
38
40
  `${cliText("Running tasks: ", "运行中任务:", language)}${activity.runningSessions ?? 0}`,
39
41
  `${cliText("Pending final deliveries: ", "待发送最终结果:", language)}${activity.pendingFinalDeliveries ?? 0}`,
40
42
  `${cliText("Safe restart: ", "安全重启:", language)}${safeRestart}`,
41
43
  "",
42
44
  ].join("\n");
43
45
  }
46
+
47
+ function formatAgentProcesses(value: unknown, language: CliLanguage): string[] {
48
+ const agents = Array.isArray(value)
49
+ ? value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object")
50
+ : [];
51
+ if (agents.length === 0) {
52
+ return [`${cliText("Agents: ", "Agent 进程:", language)}-`];
53
+ }
54
+ return [
55
+ cliText("Agents:", "Agent 进程:", language),
56
+ ...agents.map((agent) => {
57
+ const name = typeof agent.name === "string" && agent.name.trim() ? agent.name.trim() : "-";
58
+ const title = typeof agent.title === "string" && agent.title.trim() ? agent.title.trim() : undefined;
59
+ const label = title && title !== name ? `${name} (${title})` : name;
60
+ const pid = typeof agent.pid === "number" && Number.isInteger(agent.pid) && agent.pid > 0
61
+ ? String(agent.pid)
62
+ : "-";
63
+ const version = typeof agent.version === "string" && agent.version.trim()
64
+ ? agent.version.trim()
65
+ : "-";
66
+ return language === "zh"
67
+ ? ` ${label}:PID ${pid},版本 ${version}`
68
+ : ` ${label}: PID ${pid}, version ${version}`;
69
+ }),
70
+ ];
71
+ }
44
72
  import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
@@ -0,0 +1,91 @@
1
+ import type { SessionRecord } from "../state/StateStore.js";
2
+ import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
3
+
4
+ export type CurrentTaskResolution =
5
+ | { status: "found"; session: SessionRecord }
6
+ | { status: "missing-thread-id" }
7
+ | { status: "not-found"; threadIds: string[] }
8
+ | { status: "ambiguous"; sessions: SessionRecord[]; threadIds: string[] };
9
+
10
+ export function formatTaskList(
11
+ sessions: SessionRecord[],
12
+ language: CliLanguage = cliLanguage,
13
+ currentThreadIds: readonly string[] = [],
14
+ ): string {
15
+ if (sessions.length === 0) return cliText("No tasks.\n", "没有任务。\n", language);
16
+
17
+ const current = resolveCurrentTask(sessions, currentThreadIds);
18
+ const currentLocalSessionId = current.status === "found" ? current.session.localSessionId : undefined;
19
+ return sessions.map((session, index) => {
20
+ const title = session.title ?? cliText("Untitled task", "未命名任务", language);
21
+ const current = session.localSessionId === currentLocalSessionId
22
+ ? ` [${cliText("Current", "当前任务", language)}]`
23
+ : "";
24
+ const nativeId = nativeTaskIdLabel(session, language);
25
+ return [
26
+ `${index + 1}.${current} [${taskStateLabel(session.status, language)}/${taskStateLabel(session.lastTurnStatus, language)}] ${title}`,
27
+ ` ${cliText("Agent: ", "Agent:", language)}${session.agentName} · ${cliText("AgentBot task ID: ", "AgentBot 任务 ID:", language)}${session.localSessionId}`,
28
+ ` ${nativeId ? `${nativeId} · ` : ""}${session.contextKey} · ${session.updatedAt}`,
29
+ ].join("\n");
30
+ }).join("\n") + "\n";
31
+ }
32
+
33
+ export function currentAppServerThreadIds(env: NodeJS.ProcessEnv): string[] {
34
+ return [...new Set([
35
+ env.CODEX_THREAD_ID?.trim(),
36
+ env.TRAECLI_THREAD_ID?.trim(),
37
+ ].filter((value): value is string => Boolean(value)))];
38
+ }
39
+
40
+ export function resolveCurrentTask(
41
+ sessions: SessionRecord[],
42
+ currentThreadIds: readonly string[],
43
+ ): CurrentTaskResolution {
44
+ const threadIds = [...new Set(currentThreadIds.map((value) => value.trim()).filter(Boolean))];
45
+ if (threadIds.length === 0) return { status: "missing-thread-id" };
46
+
47
+ const candidates = new Set(threadIds);
48
+ const matches = sessions.filter((session) => session.remoteSessionId && candidates.has(session.remoteSessionId));
49
+ if (matches.length === 0) return { status: "not-found", threadIds };
50
+ if (matches.length === 1) return { status: "found", session: matches[0]! };
51
+
52
+ const running = matches.filter((session) =>
53
+ session.status === "running" || session.lastTurnStatus === "running");
54
+ if (running.length === 1) return { status: "found", session: running[0]! };
55
+ return { status: "ambiguous", sessions: matches, threadIds };
56
+ }
57
+
58
+ export function taskStateLabel(
59
+ status: string | undefined,
60
+ language: CliLanguage = cliLanguage,
61
+ ): string {
62
+ if (!status) return "-";
63
+ const chinese: Record<string, string> = {
64
+ starting: "启动中",
65
+ ready: "就绪",
66
+ running: "运行中",
67
+ closed: "已关闭",
68
+ failed: "失败",
69
+ completed: "已完成",
70
+ cancelled: "已取消",
71
+ interrupted: "已中断",
72
+ inProgress: "进行中",
73
+ active: "活动中",
74
+ idle: "空闲",
75
+ not_loaded: "未加载",
76
+ error: "错误",
77
+ };
78
+ return language === "zh" ? chinese[status] ?? status : status;
79
+ }
80
+
81
+ function nativeTaskIdLabel(session: SessionRecord, language: CliLanguage): string | undefined {
82
+ if (session.runtimeKind === "acp" || session.acpSessionId) {
83
+ const sessionId = session.acpSessionId ?? session.remoteSessionId;
84
+ return sessionId
85
+ ? `${cliText("ACP session ID: ", "ACP Session ID:", language)}${sessionId}`
86
+ : undefined;
87
+ }
88
+ return session.remoteSessionId
89
+ ? `${cliText("App Server thread ID: ", "App Server 原生任务 ID(Thread ID):", language)}${session.remoteSessionId}`
90
+ : undefined;
91
+ }
package/src/cli.ts CHANGED
@@ -65,6 +65,12 @@ import {
65
65
  type ServerStartResult,
66
66
  } from "./cli/ServerStarter.js";
67
67
  import { taskChatRoute } from "./cli/taskChatRoute.js";
68
+ import {
69
+ currentAppServerThreadIds,
70
+ formatTaskList,
71
+ resolveCurrentTask,
72
+ taskStateLabel,
73
+ } from "./cli/taskListOutput.js";
68
74
  import { isThreadContextKey } from "./feishu/contextKey.js";
69
75
  import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
70
76
  import {
@@ -724,6 +730,16 @@ async function taskCommand(input: string[]): Promise<void> {
724
730
  const store = new StateStore(config.storage.sqlitePath);
725
731
  try {
726
732
  const allSessions = store.listAllSessions();
733
+ if (action === "current") {
734
+ const unsupported = rest.filter((value) => value !== "--json");
735
+ if (unsupported.length > 0) throw new Error(cliText(
736
+ `task current does not accept arguments: ${unsupported.join(" ")}`,
737
+ `task current 不接受这些参数:${unsupported.join(" ")}`,
738
+ ));
739
+ const session = resolveCurrentTaskFromEnvironment(allSessions);
740
+ await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
741
+ return;
742
+ }
727
743
  if (action === "prompt" || action === "send") {
728
744
  const [reference, ...promptParts] = rest;
729
745
  if (!reference) throw new Error(cliText(
@@ -809,28 +825,7 @@ async function taskCommand(input: string[]): Promise<void> {
809
825
  return;
810
826
  }
811
827
  if (action === "status") {
812
- const endpoint = controlEndpoint(config.storage.sqlitePath);
813
- let live: TaskStatusControlData | undefined;
814
- let response: ControlResponse | undefined;
815
- try {
816
- response = await sendControlRequest(endpoint, {
817
- action: "task_status",
818
- localSessionId: session.localSessionId,
819
- });
820
- } catch {
821
- // The server can be stopped or still run an older control protocol
822
- // while a safe restart is pending. Preserve the local read-only fallback.
823
- }
824
- if (response) {
825
- ensureOk(response);
826
- live = response.data as TaskStatusControlData;
827
- }
828
- const statusSession = live?.session ?? session;
829
- const snapshot = live?.snapshot
830
- ?? (statusSession.lastTurnId ? store.getTurnSnapshot(statusSession.lastTurnId) : undefined);
831
- const result = { ...statusSession, snapshot, ...(live?.remote ? { remote: live.remote } : {}) };
832
- if (rest.includes("--json")) printJson(result);
833
- else printTaskStatus(statusSession, snapshot, live?.remote);
828
+ await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
834
829
  return;
835
830
  }
836
831
  const endpoint = controlEndpoint(config.storage.sqlitePath);
@@ -862,6 +857,53 @@ async function taskCommand(input: string[]): Promise<void> {
862
857
  }
863
858
  }
864
859
 
860
+ function resolveCurrentTaskFromEnvironment(sessions: SessionRecord[]): SessionRecord {
861
+ const resolution = resolveCurrentTask(sessions, currentAppServerThreadIds(process.env));
862
+ if (resolution.status === "found") return resolution.session;
863
+ if (resolution.status === "missing-thread-id") throw new Error(cliText(
864
+ "The current task cannot be detected because neither CODEX_THREAD_ID nor TRAECLI_THREAD_ID is set. Run this command from a Codex or TraeX task.",
865
+ "无法检测当前任务:CODEX_THREAD_ID 和 TRAECLI_THREAD_ID 均未设置。请从 Codex 或 TraeX 任务中运行此命令。",
866
+ ));
867
+ if (resolution.status === "not-found") throw new Error(cliText(
868
+ `No AgentBot task matches the current App Server Thread ID (${resolution.threadIds.join(", ")}). Check that the correct Profile is selected.`,
869
+ `没有 AgentBot 任务匹配当前 App Server Thread ID(${resolution.threadIds.join(",")})。请检查是否选择了正确的 Profile。`,
870
+ ));
871
+ throw new Error(cliText(
872
+ "The current task is ambiguous because multiple injected Thread IDs match AgentBot tasks. Use task status <task> with an explicit AgentBot task ID.",
873
+ "当前任务存在歧义:多个注入的 Thread ID 匹配到了 AgentBot 任务。请使用明确的 AgentBot 任务 ID 执行 task status <任务>。",
874
+ ));
875
+ }
876
+
877
+ async function outputTaskStatus(
878
+ sqlitePath: string,
879
+ store: StateStore,
880
+ session: SessionRecord,
881
+ json: boolean,
882
+ ): Promise<void> {
883
+ const endpoint = controlEndpoint(sqlitePath);
884
+ let live: TaskStatusControlData | undefined;
885
+ let response: ControlResponse | undefined;
886
+ try {
887
+ response = await sendControlRequest(endpoint, {
888
+ action: "task_status",
889
+ localSessionId: session.localSessionId,
890
+ });
891
+ } catch {
892
+ // The server can be stopped or still run an older control protocol
893
+ // while a safe restart is pending. Preserve the local read-only fallback.
894
+ }
895
+ if (response) {
896
+ ensureOk(response);
897
+ live = response.data as TaskStatusControlData;
898
+ }
899
+ const statusSession = live?.session ?? session;
900
+ const snapshot = live?.snapshot
901
+ ?? (statusSession.lastTurnId ? store.getTurnSnapshot(statusSession.lastTurnId) : undefined);
902
+ const result = { ...statusSession, snapshot, ...(live?.remote ? { remote: live.remote } : {}) };
903
+ if (json) printJson(result);
904
+ else printTaskStatus(statusSession, snapshot, live?.remote);
905
+ }
906
+
865
907
  function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
866
908
  const context = optionValue(args, "--context");
867
909
  const status = optionValue(args, "--status");
@@ -896,19 +938,12 @@ function resolveTask(sessions: SessionRecord[], reference: string): SessionRecor
896
938
  }
897
939
 
898
940
  function printTaskList(sessions: SessionRecord[]): void {
899
- if (sessions.length === 0) {
900
- process.stdout.write(cliText("No tasks.\n", "没有任务。\n"));
901
- return;
902
- }
903
- for (const [index, session] of sessions.entries()) {
904
- const id = session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId;
905
- process.stdout.write(`${index + 1}. [${taskStateLabel(session.status)}/${taskStateLabel(session.lastTurnStatus)}] ${session.title ?? cliText("Untitled task", "未命名任务")}\n`);
906
- process.stdout.write(` ${id} · ${session.contextKey} · ${session.updatedAt}\n`);
907
- }
941
+ process.stdout.write(formatTaskList(sessions, cliLanguage, currentAppServerThreadIds(process.env)));
908
942
  }
909
943
 
910
944
  function printTaskStatus(session: SessionRecord, snapshot: unknown, remote?: TaskStatusControlData["remote"]): void {
911
945
  process.stdout.write(`${cliText("Title: ", "标题:")}${session.title ?? cliText("Untitled task", "未命名任务")}\n`);
946
+ process.stdout.write(`${cliText("Agent: ", "Agent:")}${session.agentName}\n`);
912
947
  process.stdout.write(`${cliText("Task ID: ", "任务 ID:")}${session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId}\n`);
913
948
  process.stdout.write(`${cliText("Local ID: ", "本地 ID:")}${session.localSessionId}\n`);
914
949
  process.stdout.write(`${cliText("Context: ", "上下文:")}${session.contextKey}\n`);
@@ -1201,26 +1236,6 @@ function initializationStatusLabel(status: InitializationStatus): string {
1201
1236
  return cliText("already exists; unchanged", "已存在,未修改");
1202
1237
  }
1203
1238
 
1204
- function taskStateLabel(status: string | undefined): string {
1205
- if (!status) return "-";
1206
- const chinese: Record<string, string> = {
1207
- starting: "启动中",
1208
- ready: "就绪",
1209
- running: "运行中",
1210
- closed: "已关闭",
1211
- failed: "失败",
1212
- completed: "已完成",
1213
- cancelled: "已取消",
1214
- interrupted: "已中断",
1215
- inProgress: "进行中",
1216
- active: "活动中",
1217
- idle: "空闲",
1218
- not_loaded: "未加载",
1219
- error: "错误",
1220
- };
1221
- return cliLanguage === "zh" ? chinese[status] ?? status : status;
1222
- }
1223
-
1224
1239
  function ensureOk(response: ControlResponse): void {
1225
1240
  if (response.ok) return;
1226
1241
  const message = response.message?.trim();
@@ -5,12 +5,17 @@ import readline from "node:readline";
5
5
  import type { Logger } from "pino";
6
6
  import { AppServerConnection } from "./AppServerConnection.js";
7
7
  import type { AppServerClient, AppServerClientProvider } from "./CodexRuntime.js";
8
- import { agentBotEnvironment } from "../runtime/agentEnvironment.js";
8
+ import {
9
+ agentBotEnvironment,
10
+ type AgentEnvironmentContext,
11
+ } from "../runtime/agentEnvironment.js";
12
+ import type { AgentProcessInfo } from "../runtime/types.js";
9
13
  import { spawnStdioCommand } from "../utils/spawnCommand.js";
10
14
 
11
15
  export class CodexProcessManager implements AppServerClientProvider {
12
16
  private client?: AppServerConnection;
13
17
  private child?: ChildProcessWithoutNullStreams;
18
+ private version?: string;
14
19
  private readonly disconnectListeners = new Set<(error: Error) => void>();
15
20
 
16
21
  constructor(
@@ -18,11 +23,17 @@ export class CodexProcessManager implements AppServerClientProvider {
18
23
  private readonly args: string[],
19
24
  private readonly env: Record<string, string>,
20
25
  private readonly logger: Logger,
26
+ private readonly environmentContext: () => AgentEnvironmentContext = () => ({}),
21
27
  ) {}
22
28
 
23
29
  async getClient(): Promise<AppServerClient> {
24
30
  if (this.client) return this.client;
25
- const child = spawnStdioCommand(this.command, this.args, agentBotEnvironment(process.env, this.env));
31
+ this.version = undefined;
32
+ const child = spawnStdioCommand(
33
+ this.command,
34
+ this.args,
35
+ agentBotEnvironment(process.env, this.env, this.environmentContext()),
36
+ );
26
37
  this.child = child;
27
38
  const client = new AppServerConnection(child, this.logger.child({ component: "codex-app-server" }));
28
39
  this.client = client;
@@ -38,14 +49,24 @@ export class CodexProcessManager implements AppServerClientProvider {
38
49
  const error = new Error(`App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
39
50
  for (const listener of this.disconnectListeners) listener(error);
40
51
  });
41
- await client.request("initialize", {
52
+ const initializeResult = await client.request("initialize", {
42
53
  clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
43
54
  capabilities: { experimentalApi: true },
44
55
  });
56
+ this.version = initializedAgentVersion(initializeResult);
45
57
  client.notify("initialized", {});
46
58
  return client;
47
59
  }
48
60
 
61
+ getProcessInfo(): AgentProcessInfo {
62
+ const pid = this.child?.pid;
63
+ if (!pid) return {};
64
+ return {
65
+ pid,
66
+ ...(this.version ? { version: this.version } : {}),
67
+ };
68
+ }
69
+
49
70
  getCodexHome(): string {
50
71
  return this.env.CODEX_HOME ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
51
72
  }
@@ -62,3 +83,17 @@ export class CodexProcessManager implements AppServerClientProvider {
62
83
  return () => this.disconnectListeners.delete(listener);
63
84
  }
64
85
  }
86
+
87
+ function initializedAgentVersion(value: unknown): string | undefined {
88
+ if (!value || typeof value !== "object") return undefined;
89
+ const result = value as Record<string, unknown>;
90
+ const serverInfo = result.serverInfo && typeof result.serverInfo === "object"
91
+ ? result.serverInfo as Record<string, unknown>
92
+ : undefined;
93
+ for (const candidate of [serverInfo?.version, result.version, result.userAgent]) {
94
+ if (typeof candidate !== "string") continue;
95
+ const match = /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b/u.exec(candidate);
96
+ if (match?.[1]) return match[1];
97
+ }
98
+ return undefined;
99
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Logger } from "pino";
2
2
  import type {
3
3
  AgentRuntime,
4
+ AgentProcessInfo,
4
5
  ApprovalDecision,
5
6
  CreateRuntimeSessionInput,
6
7
  ForkRuntimeSessionInput,
@@ -55,6 +56,7 @@ export interface AppServerClient {
55
56
 
56
57
  export interface AppServerClientProvider {
57
58
  getClient(): Promise<AppServerClient>;
59
+ getProcessInfo?(): AgentProcessInfo;
58
60
  getCodexHome?(): string;
59
61
  onDisconnect?(listener: (error: Error) => void): () => void;
60
62
  close(): void;
@@ -100,6 +102,10 @@ export class CodexRuntime implements AgentRuntime {
100
102
  return this.sessions.get(localSessionId);
101
103
  }
102
104
 
105
+ getProcessInfo(): AgentProcessInfo {
106
+ return this.provider.getProcessInfo?.() ?? {};
107
+ }
108
+
103
109
  async createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
104
110
  const client = await this.client();
105
111
  const response = await client.request<ThreadResponse>("thread/start", {
@@ -139,6 +139,18 @@ export interface TaskListCardAction {
139
139
  value: Record<string, string>;
140
140
  }
141
141
 
142
+ export interface HelpCardCommand {
143
+ text: string;
144
+ action?: TaskListCardAction;
145
+ usage?: string;
146
+ description: string;
147
+ }
148
+
149
+ export interface HelpCardSection {
150
+ title: string;
151
+ commands: HelpCardCommand[];
152
+ }
153
+
142
154
  export interface TaskListCardEntry {
143
155
  lines: string[];
144
156
  actions?: TaskListCardAction[];
@@ -794,6 +806,19 @@ export class CardRenderer {
794
806
  return sectionCard(title, elements);
795
807
  }
796
808
 
809
+ renderHelpCard(
810
+ title: string,
811
+ introLines: string[],
812
+ sections: HelpCardSection[],
813
+ ): Record<string, unknown> {
814
+ const elements: Record<string, unknown>[] = [markdown(introLines.join("\n"))];
815
+ for (const section of sections) {
816
+ elements.push({ tag: "hr" }, markdown(`**${section.title}**`));
817
+ elements.push(...section.commands.map(helpCommandRow));
818
+ }
819
+ return sectionCard(title, elements);
820
+ }
821
+
797
822
  renderTaskListCard(
798
823
  title: string,
799
824
  sectionTitle: string,
@@ -813,10 +838,12 @@ export class CardRenderer {
813
838
  if (index < entries.length - 1) elements.push({ tag: "hr" });
814
839
  });
815
840
  }
841
+ if (footerActions.length > 0) {
842
+ elements.push({ tag: "hr" }, taskActionRow(footerActions));
843
+ }
816
844
  if (footerLines.length > 0) {
817
845
  elements.push({ tag: "hr" }, markdown(footerLines.join("\n")));
818
846
  }
819
- if (footerActions.length > 0) elements.push(taskActionRow(footerActions));
820
847
  return {
821
848
  schema: "2.0",
822
849
  config: {
@@ -1588,6 +1615,39 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
1588
1615
  };
1589
1616
  }
1590
1617
 
1618
+ function helpCommandRow(command: HelpCardCommand): Record<string, unknown> {
1619
+ const details = [
1620
+ command.usage ? `**${command.usage}**` : undefined,
1621
+ command.description,
1622
+ ].filter((line): line is string => Boolean(line)).join(" ");
1623
+ return {
1624
+ tag: "column_set",
1625
+ flex_mode: "none",
1626
+ horizontal_spacing: "12px",
1627
+ vertical_align: "top",
1628
+ margin: "2px 0 0 0",
1629
+ columns: [
1630
+ {
1631
+ tag: "column",
1632
+ width: "auto",
1633
+ vertical_align: "top",
1634
+ elements: [
1635
+ command.action
1636
+ ? taskActionElement(command.action)
1637
+ : markdown(`**${escapeCardHtml(command.text)}**`),
1638
+ ],
1639
+ },
1640
+ {
1641
+ tag: "column",
1642
+ width: "weighted",
1643
+ weight: 1,
1644
+ vertical_align: "top",
1645
+ elements: [markdown(details)],
1646
+ },
1647
+ ],
1648
+ };
1649
+ }
1650
+
1591
1651
  function turnCard(
1592
1652
  title: string,
1593
1653
  template: string,
@@ -5,6 +5,7 @@ import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
5
5
  import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
6
6
 
7
7
  type BotOpenIdResolver = (appId: string, appSecret: string) => Promise<string>;
8
+ type BotOpenIdListener = (botOpenId: string) => void;
8
9
 
9
10
  export class FeishuConnector {
10
11
  constructor(
@@ -12,6 +13,7 @@ export class FeishuConnector {
12
13
  private readonly handler: FeishuEventHandler,
13
14
  private readonly logger: Logger,
14
15
  private readonly botOpenIdResolver: BotOpenIdResolver = resolveFeishuBotOpenId,
16
+ private readonly onBotOpenId?: BotOpenIdListener,
15
17
  ) {}
16
18
 
17
19
  async start(): Promise<void> {
@@ -21,10 +23,17 @@ export class FeishuConnector {
21
23
  }
22
24
 
23
25
  const respondToAllGroupMessages = this.config.feishu.respondToAllGroupMessages !== false;
24
- const botOpenId = respondToAllGroupMessages
25
- ? undefined
26
- : await this.botOpenIdResolver(appId, appSecret);
27
- await this.startFeishuWs(appId, appSecret, botOpenId);
26
+ let botOpenId: string | undefined;
27
+ if (!respondToAllGroupMessages || this.onBotOpenId) {
28
+ try {
29
+ botOpenId = await this.botOpenIdResolver(appId, appSecret);
30
+ this.onBotOpenId?.(botOpenId);
31
+ } catch (error) {
32
+ if (!respondToAllGroupMessages) throw error;
33
+ this.logger.warn({ error }, "Failed to resolve the Lark bot Open ID for the Agent environment.");
34
+ }
35
+ }
36
+ await this.startFeishuWs(appId, appSecret, respondToAllGroupMessages ? undefined : botOpenId);
28
37
  }
29
38
 
30
39
  stop(): void {