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

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 (63) hide show
  1. package/CHANGELOG.md +11 -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/InitializationWelcome.js +116 -0
  9. package/dist/cli/InitializationWelcome.js.map +1 -0
  10. package/dist/cli/help.js +4 -2
  11. package/dist/cli/help.js.map +1 -1
  12. package/dist/cli/serverStatus.js +27 -0
  13. package/dist/cli/serverStatus.js.map +1 -1
  14. package/dist/cli/taskListOutput.js +72 -0
  15. package/dist/cli/taskListOutput.js.map +1 -0
  16. package/dist/cli.js +103 -56
  17. package/dist/cli.js.map +1 -1
  18. package/dist/codex/CodexProcessManager.js +34 -4
  19. package/dist/codex/CodexProcessManager.js.map +1 -1
  20. package/dist/codex/CodexRuntime.js +3 -0
  21. package/dist/codex/CodexRuntime.js.map +1 -1
  22. package/dist/feishu/CardRenderer.js +164 -2
  23. package/dist/feishu/CardRenderer.js.map +1 -1
  24. package/dist/feishu/FeishuConnector.js +16 -5
  25. package/dist/feishu/FeishuConnector.js.map +1 -1
  26. package/dist/index.js +23 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/proxy/ProxySessionController.js +157 -59
  29. package/dist/proxy/ProxySessionController.js.map +1 -1
  30. package/dist/runtime/AcpRuntimeAdapter.js +3 -0
  31. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  32. package/dist/runtime/agentEnvironment.js +24 -4
  33. package/dist/runtime/agentEnvironment.js.map +1 -1
  34. package/dist/runtime/createAgentRuntimeRegistry.js +7 -3
  35. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
  36. package/dist/state/StateStore.js +12 -9
  37. package/dist/state/StateStore.js.map +1 -1
  38. package/dist/supervision/SafeRestartNotifier.js +1 -9
  39. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  40. package/docs/technical-reference.md +12 -4
  41. package/docs/technical-reference.zh.md +12 -4
  42. package/npm-shrinkwrap.json +2 -2
  43. package/package.json +1 -1
  44. package/skills/agent-bot/SKILL.md +53 -22
  45. package/src/acp/AcpProcessManager.ts +16 -3
  46. package/src/acp/AcpSessionManager.ts +14 -0
  47. package/src/cli/InitializationWelcome.ts +160 -0
  48. package/src/cli/help.ts +4 -2
  49. package/src/cli/serverStatus.ts +28 -0
  50. package/src/cli/taskListOutput.ts +91 -0
  51. package/src/cli.ts +140 -55
  52. package/src/codex/CodexProcessManager.ts +38 -3
  53. package/src/codex/CodexRuntime.ts +6 -0
  54. package/src/feishu/CardRenderer.ts +204 -1
  55. package/src/feishu/FeishuConnector.ts +13 -4
  56. package/src/index.ts +35 -3
  57. package/src/proxy/ProxySessionController.ts +186 -59
  58. package/src/runtime/AcpRuntimeAdapter.ts +5 -0
  59. package/src/runtime/agentEnvironment.ts +41 -3
  60. package/src/runtime/createAgentRuntimeRegistry.ts +18 -3
  61. package/src/runtime/types.ts +5 -0
  62. package/src/state/StateStore.ts +12 -9
  63. package/src/supervision/SafeRestartNotifier.ts +1 -11
package/src/cli.ts CHANGED
@@ -56,6 +56,13 @@ import {
56
56
  import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatus.js";
57
57
  import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
58
58
  import { readPackageVersion } from "./cli/packageVersion.js";
59
+ import {
60
+ readInitializationReceipt,
61
+ resolveInitializationWelcomeKind,
62
+ sendInitializationWelcome,
63
+ writeInitializationReceipt,
64
+ type InitializationWelcomeResult,
65
+ } from "./cli/InitializationWelcome.js";
59
66
  import { applyExplicitProfile, parseGlobalOptions } from "./cli/profile.js";
60
67
  import { printVerificationQrAndLink } from "./cli/VerificationOutput.js";
61
68
  import {
@@ -65,6 +72,12 @@ import {
65
72
  type ServerStartResult,
66
73
  } from "./cli/ServerStarter.js";
67
74
  import { taskChatRoute } from "./cli/taskChatRoute.js";
75
+ import {
76
+ currentAppServerThreadIds,
77
+ formatTaskList,
78
+ resolveCurrentTask,
79
+ taskStateLabel,
80
+ } from "./cli/taskListOutput.js";
68
81
  import { isThreadContextKey } from "./feishu/contextKey.js";
69
82
  import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
70
83
  import {
@@ -141,6 +154,7 @@ interface InitCommandResult extends InitializationResult {
141
154
  configuration?: EnsureFeishuAppConfigurationResult;
142
155
  };
143
156
  server: InitializationServerResult;
157
+ welcome: InitializationWelcomeResult;
144
158
  }
145
159
 
146
160
  interface InitializedAgent extends SupportedAgentInspection {
@@ -156,11 +170,13 @@ async function initCommand(
156
170
  explicitProfile: boolean,
157
171
  ): Promise<void> {
158
172
  const options = parseInitCommandOptions(input, explicitProfile);
173
+ const version = readPackageVersion();
174
+ const previousInitialization = readInitializationReceipt(agentBotHome());
159
175
  let initializationLock = options.reset
160
176
  ? acquireInitializationLock(agentBotHome())
161
177
  : undefined;
162
178
  let paths: InitializationResult;
163
- let initialized: Omit<InitCommandResult, "server">;
179
+ let initialized: Omit<InitCommandResult, "server" | "welcome">;
164
180
  try {
165
181
  if (options.reset) await assertResetProfileServerStopped(configPath);
166
182
  paths = initializeAgentBot({ configPath, reset: options.reset });
@@ -183,9 +199,46 @@ async function initCommand(
183
199
  process.stdout.write(cliText("\nStarting Agent Bot server...\n", "\n正在启动 Agent Bot 服务...\n"));
184
200
  }
185
201
  const server = await startInitializedServer({ skipFeishu: options.skipFeishu, configPath });
186
- const result: InitCommandResult = { ...initialized, server };
202
+ const welcomeKind = resolveInitializationWelcomeKind({
203
+ firstInitialization: paths.config.status === "created",
204
+ previousVersion: previousInitialization?.version,
205
+ currentVersion: version,
206
+ });
207
+ let welcome: InitializationWelcomeResult;
208
+ if (options.skipFeishu) {
209
+ welcome = { status: "skipped", kind: welcomeKind, reason: "feishu-skipped" };
210
+ } else {
211
+ try {
212
+ welcome = await sendInitializationWelcome({
213
+ configPath,
214
+ language: cliLanguage,
215
+ version,
216
+ previousVersion: previousInitialization?.version,
217
+ kind: welcomeKind,
218
+ activationPending: server.status === "restart-scheduled",
219
+ });
220
+ } catch (error) {
221
+ welcome = {
222
+ status: "failed",
223
+ kind: welcomeKind,
224
+ message: error instanceof Error ? error.message : String(error),
225
+ };
226
+ }
227
+ }
228
+ try {
229
+ writeInitializationReceipt(paths.home.path, version);
230
+ } catch (error) {
231
+ process.stderr.write(`${cliText(
232
+ "Warning: Could not save initialization version metadata: ",
233
+ "警告:无法保存初始化版本信息:",
234
+ )}${error instanceof Error ? error.message : String(error)}\n`);
235
+ }
236
+ const result: InitCommandResult = { ...initialized, server, welcome };
187
237
  if (options.json) printJson(result);
188
- else printInitializationServerResult(server);
238
+ else {
239
+ printInitializationServerResult(server);
240
+ printInitializationWelcomeResult(welcome);
241
+ }
189
242
  }
190
243
 
191
244
  async function initializeSupportedAgents(json: boolean): Promise<InitializedAgent[]> {
@@ -724,6 +777,16 @@ async function taskCommand(input: string[]): Promise<void> {
724
777
  const store = new StateStore(config.storage.sqlitePath);
725
778
  try {
726
779
  const allSessions = store.listAllSessions();
780
+ if (action === "current") {
781
+ const unsupported = rest.filter((value) => value !== "--json");
782
+ if (unsupported.length > 0) throw new Error(cliText(
783
+ `task current does not accept arguments: ${unsupported.join(" ")}`,
784
+ `task current 不接受这些参数:${unsupported.join(" ")}`,
785
+ ));
786
+ const session = resolveCurrentTaskFromEnvironment(allSessions);
787
+ await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
788
+ return;
789
+ }
727
790
  if (action === "prompt" || action === "send") {
728
791
  const [reference, ...promptParts] = rest;
729
792
  if (!reference) throw new Error(cliText(
@@ -809,28 +872,7 @@ async function taskCommand(input: string[]): Promise<void> {
809
872
  return;
810
873
  }
811
874
  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);
875
+ await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
834
876
  return;
835
877
  }
836
878
  const endpoint = controlEndpoint(config.storage.sqlitePath);
@@ -862,6 +904,53 @@ async function taskCommand(input: string[]): Promise<void> {
862
904
  }
863
905
  }
864
906
 
907
+ function resolveCurrentTaskFromEnvironment(sessions: SessionRecord[]): SessionRecord {
908
+ const resolution = resolveCurrentTask(sessions, currentAppServerThreadIds(process.env));
909
+ if (resolution.status === "found") return resolution.session;
910
+ if (resolution.status === "missing-thread-id") throw new Error(cliText(
911
+ "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.",
912
+ "无法检测当前任务:CODEX_THREAD_ID 和 TRAECLI_THREAD_ID 均未设置。请从 Codex 或 TraeX 任务中运行此命令。",
913
+ ));
914
+ if (resolution.status === "not-found") throw new Error(cliText(
915
+ `No AgentBot task matches the current App Server Thread ID (${resolution.threadIds.join(", ")}). Check that the correct Profile is selected.`,
916
+ `没有 AgentBot 任务匹配当前 App Server Thread ID(${resolution.threadIds.join(",")})。请检查是否选择了正确的 Profile。`,
917
+ ));
918
+ throw new Error(cliText(
919
+ "The current task is ambiguous because multiple injected Thread IDs match AgentBot tasks. Use task status <task> with an explicit AgentBot task ID.",
920
+ "当前任务存在歧义:多个注入的 Thread ID 匹配到了 AgentBot 任务。请使用明确的 AgentBot 任务 ID 执行 task status <任务>。",
921
+ ));
922
+ }
923
+
924
+ async function outputTaskStatus(
925
+ sqlitePath: string,
926
+ store: StateStore,
927
+ session: SessionRecord,
928
+ json: boolean,
929
+ ): Promise<void> {
930
+ const endpoint = controlEndpoint(sqlitePath);
931
+ let live: TaskStatusControlData | undefined;
932
+ let response: ControlResponse | undefined;
933
+ try {
934
+ response = await sendControlRequest(endpoint, {
935
+ action: "task_status",
936
+ localSessionId: session.localSessionId,
937
+ });
938
+ } catch {
939
+ // The server can be stopped or still run an older control protocol
940
+ // while a safe restart is pending. Preserve the local read-only fallback.
941
+ }
942
+ if (response) {
943
+ ensureOk(response);
944
+ live = response.data as TaskStatusControlData;
945
+ }
946
+ const statusSession = live?.session ?? session;
947
+ const snapshot = live?.snapshot
948
+ ?? (statusSession.lastTurnId ? store.getTurnSnapshot(statusSession.lastTurnId) : undefined);
949
+ const result = { ...statusSession, snapshot, ...(live?.remote ? { remote: live.remote } : {}) };
950
+ if (json) printJson(result);
951
+ else printTaskStatus(statusSession, snapshot, live?.remote);
952
+ }
953
+
865
954
  function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
866
955
  const context = optionValue(args, "--context");
867
956
  const status = optionValue(args, "--status");
@@ -896,19 +985,12 @@ function resolveTask(sessions: SessionRecord[], reference: string): SessionRecor
896
985
  }
897
986
 
898
987
  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
- }
988
+ process.stdout.write(formatTaskList(sessions, cliLanguage, currentAppServerThreadIds(process.env)));
908
989
  }
909
990
 
910
991
  function printTaskStatus(session: SessionRecord, snapshot: unknown, remote?: TaskStatusControlData["remote"]): void {
911
992
  process.stdout.write(`${cliText("Title: ", "标题:")}${session.title ?? cliText("Untitled task", "未命名任务")}\n`);
993
+ process.stdout.write(`${cliText("Agent: ", "Agent:")}${session.agentName}\n`);
912
994
  process.stdout.write(`${cliText("Task ID: ", "任务 ID:")}${session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId}\n`);
913
995
  process.stdout.write(`${cliText("Local ID: ", "本地 ID:")}${session.localSessionId}\n`);
914
996
  process.stdout.write(`${cliText("Context: ", "上下文:")}${session.contextKey}\n`);
@@ -967,7 +1049,7 @@ function printInitializationPaths(result: InitializationResult): void {
967
1049
  process.stdout.write(`${cliText("Log directory: ", "日志目录:")}${result.logs.path} (${initializationStatusLabel(result.logs.status)})\n`);
968
1050
  }
969
1051
 
970
- function printInitializationResult(result: Omit<InitCommandResult, "server">): void {
1052
+ function printInitializationResult(result: Omit<InitCommandResult, "server" | "welcome">): void {
971
1053
  process.stdout.write(cliText(
972
1054
  "\nAgent Bot initialization completed.\n",
973
1055
  "\nAgent Bot 初始化完成。\n",
@@ -1044,6 +1126,29 @@ function printInitializationServerResult(result: InitCommandResult["server"]): v
1044
1126
  printServerStartResult(result);
1045
1127
  }
1046
1128
 
1129
+ function printInitializationWelcomeResult(result: InitializationWelcomeResult): void {
1130
+ if (result.status === "sent") {
1131
+ process.stdout.write(cliText(
1132
+ "Welcome card: sent to your Lark private chat.\n",
1133
+ "欢迎卡片:已发送到你的飞书私聊。\n",
1134
+ ));
1135
+ return;
1136
+ }
1137
+ if (result.status === "failed") {
1138
+ process.stderr.write(cliText(
1139
+ `Warning: The private welcome card could not be sent: ${result.message}\n`,
1140
+ `警告:无法发送私聊欢迎卡片:${result.message}\n`,
1141
+ ));
1142
+ return;
1143
+ }
1144
+ if (result.reason === "missing-user-open-id") {
1145
+ process.stderr.write(cliText(
1146
+ "Warning: The private welcome card was skipped because FEISHU_USER_OPEN_ID is not available. Send the bot a private message, then run agentbot init again.\n",
1147
+ "警告:尚未获得 FEISHU_USER_OPEN_ID,无法发送私聊欢迎卡片。请先私聊机器人,再次运行 agentbot init。\n",
1148
+ ));
1149
+ }
1150
+ }
1151
+
1047
1152
  function printServerStartResult(result: ServerStartResult): void {
1048
1153
  process.stdout.write(
1049
1154
  result.status === "already-running"
@@ -1201,26 +1306,6 @@ function initializationStatusLabel(status: InitializationStatus): string {
1201
1306
  return cliText("already exists; unchanged", "已存在,未修改");
1202
1307
  }
1203
1308
 
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
1309
  function ensureOk(response: ControlResponse): void {
1225
1310
  if (response.ok) return;
1226
1311
  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", {
@@ -35,6 +35,27 @@ export interface StartupStatusView {
35
35
  };
36
36
  }
37
37
 
38
+ export type InitializationWelcomeKind = "first" | "upgrade" | "refresh";
39
+
40
+ export interface InitializationWelcomeFeature {
41
+ icon: string;
42
+ title: string;
43
+ description: string;
44
+ }
45
+
46
+ export interface InitializationWelcomeView {
47
+ kind: InitializationWelcomeKind;
48
+ language: "en" | "zh";
49
+ version: string;
50
+ previousVersion?: string;
51
+ activationPending?: boolean;
52
+ defaultAgentName: string;
53
+ defaultAgentTitle: string;
54
+ availableAgents: string[];
55
+ logoPath: string;
56
+ features: InitializationWelcomeFeature[];
57
+ }
58
+
38
59
  export interface SafeRestartStatusView {
39
60
  scheduleId: number;
40
61
  reason: string;
@@ -139,6 +160,18 @@ export interface TaskListCardAction {
139
160
  value: Record<string, string>;
140
161
  }
141
162
 
163
+ export interface HelpCardCommand {
164
+ text: string;
165
+ action?: TaskListCardAction;
166
+ usage?: string;
167
+ description: string;
168
+ }
169
+
170
+ export interface HelpCardSection {
171
+ title: string;
172
+ commands: HelpCardCommand[];
173
+ }
174
+
142
175
  export interface TaskListCardEntry {
143
176
  lines: string[];
144
177
  actions?: TaskListCardAction[];
@@ -667,6 +700,128 @@ export class CardRenderer {
667
700
  return sectionCard("Agent Bot 已启动", [markdown(lines.join("\n"))], "green");
668
701
  }
669
702
 
703
+ renderInitializationWelcome(view: InitializationWelcomeView): Record<string, unknown> {
704
+ const chinese = view.language === "zh";
705
+ const title = view.kind === "first"
706
+ ? (chinese ? "欢迎使用 Agent Bot" : "Welcome to Agent Bot")
707
+ : view.kind === "upgrade"
708
+ ? (chinese ? "Agent Bot 已更新" : "Agent Bot is updated")
709
+ : (chinese ? "Agent Bot 已准备就绪" : "Agent Bot is ready");
710
+ const subtitle = view.activationPending
711
+ ? (chinese ? "配置已完成,安全重启后生效" : "Configured and pending a safe restart")
712
+ : view.kind === "first"
713
+ ? (chinese ? "本地 Agent 已接入飞书" : "Your local Agents are now available in Lark")
714
+ : view.kind === "upgrade"
715
+ ? (chinese ? `新版本 ${view.version} 已生效` : `Version ${view.version} is now active`)
716
+ : (chinese ? "初始化配置已刷新" : "Initialization settings were refreshed");
717
+ const activationNote = view.activationPending
718
+ ? (chinese ? "当前任务完成并安全重启后生效。" : "It will take effect after active work finishes and the safe restart completes.")
719
+ : undefined;
720
+ const intro = view.kind === "first"
721
+ ? (chinese
722
+ ? "**初始化完成**\n从现在起,你可以直接在飞书里把任务交给本机 Agent,并随时查看进度、切换任务或创建分支。"
723
+ : "**Setup complete**\nYou can now work with local Agents from Lark, follow progress, switch tasks, and branch conversations.")
724
+ : view.kind === "upgrade"
725
+ ? (chinese
726
+ ? `**升级完成**\n${view.previousVersion ? `${inlineCode(view.previousVersion)} → ` : ""}${inlineCode(view.version)} 已准备好。${activationNote ?? "下面是本版值得关注的能力。"}`
727
+ : `**Upgrade complete**\n${view.previousVersion ? `${inlineCode(view.previousVersion)} → ` : ""}${inlineCode(view.version)} is ready. ${activationNote ?? "Here is what is new."}`)
728
+ : (chinese
729
+ ? `**配置刷新完成**\n${inlineCode(view.version)} 已重新检查配置、Agent 和飞书连接。${activationNote ?? ""}`
730
+ : `**Configuration refreshed**\n${inlineCode(view.version)} rechecked your configuration, Agents, and Lark connection. ${activationNote ?? ""}`);
731
+ const logo = {
732
+ ...localCardImage(view.logoPath, "Agent Bot logo"),
733
+ preview: false,
734
+ };
735
+ const featureColumns = view.features.slice(0, 4).map((feature) => ({
736
+ tag: "column",
737
+ width: "weighted",
738
+ weight: 1,
739
+ vertical_align: "top",
740
+ elements: [markdown([
741
+ `${feature.icon} **${feature.title}**`,
742
+ `<font color='grey'>${feature.description}</font>`,
743
+ ].join("\n"))],
744
+ }));
745
+ const featureRows = [0, 2].flatMap((start) => {
746
+ const columns = featureColumns.slice(start, start + 2);
747
+ return columns.length > 0
748
+ ? [{
749
+ tag: "column_set",
750
+ flex_mode: "none",
751
+ horizontal_spacing: "16px",
752
+ vertical_align: "top",
753
+ columns,
754
+ }]
755
+ : [];
756
+ });
757
+ const availableAgents = view.availableAgents.length > 0
758
+ ? view.availableAgents.map((agent) => inlineCode(agent)).join(" · ")
759
+ : inlineCode(view.defaultAgentName);
760
+ return {
761
+ schema: "2.0",
762
+ config: {
763
+ update_multi: true,
764
+ width_mode: "fill",
765
+ },
766
+ header: {
767
+ template: view.kind === "first" ? "turquoise" : view.kind === "upgrade" ? "blue" : "green",
768
+ title: { tag: "plain_text", content: title },
769
+ subtitle: { tag: "plain_text", content: subtitle },
770
+ padding: "12px 12px 12px 12px",
771
+ },
772
+ body: {
773
+ direction: "vertical",
774
+ vertical_spacing: "12px",
775
+ padding: "12px 12px 12px 12px",
776
+ elements: [
777
+ {
778
+ tag: "column_set",
779
+ flex_mode: "none",
780
+ horizontal_spacing: "16px",
781
+ vertical_align: "center",
782
+ columns: [
783
+ {
784
+ tag: "column",
785
+ width: "weighted",
786
+ weight: 1,
787
+ vertical_align: "center",
788
+ elements: [logo],
789
+ },
790
+ {
791
+ tag: "column",
792
+ width: "weighted",
793
+ weight: 3,
794
+ vertical_align: "center",
795
+ elements: [markdown(intro)],
796
+ },
797
+ ],
798
+ },
799
+ { tag: "hr" },
800
+ markdown(chinese
801
+ ? `**${view.kind === "upgrade" ? "本版亮点" : "你可以这样使用"}**`
802
+ : `**${view.kind === "upgrade" ? "What's new" : "What you can do"}**`),
803
+ ...featureRows,
804
+ { tag: "hr" },
805
+ markdown(chinese
806
+ ? "> 直接发送消息即可开始;发送 `/new` 创建新任务;发送 `/help` 查看全部命令。"
807
+ : "> Send a message to begin, `/new` for a new task, or `/help` for all commands."),
808
+ {
809
+ ...markdown(chinese
810
+ ? `**版本** ${inlineCode(view.version)} · **默认 Agent** ${inlineCode(view.defaultAgentTitle)} · **可用 Agent** ${availableAgents}`
811
+ : `**Version** ${inlineCode(view.version)} · **Default Agent** ${inlineCode(view.defaultAgentTitle)} · **Available** ${availableAgents}`),
812
+ text_size: "notation",
813
+ },
814
+ {
815
+ ...markdown(chinese
816
+ ? "📋 [查看更新日志](https://github.com/keyou/agent-bot/blob/master/CHANGELOG.md)"
817
+ : "📋 [View Changelog](https://github.com/keyou/agent-bot/blob/master/CHANGELOG.md)"),
818
+ text_size: "notation",
819
+ },
820
+ ],
821
+ },
822
+ };
823
+ }
824
+
670
825
  renderTurn(state: TurnViewState): Record<string, unknown> {
671
826
  const elements = renderTurnElements(state, "hidden");
672
827
  if (isTurnStoppable(state.status)) {
@@ -794,6 +949,19 @@ export class CardRenderer {
794
949
  return sectionCard(title, elements);
795
950
  }
796
951
 
952
+ renderHelpCard(
953
+ title: string,
954
+ introLines: string[],
955
+ sections: HelpCardSection[],
956
+ ): Record<string, unknown> {
957
+ const elements: Record<string, unknown>[] = [markdown(introLines.join("\n"))];
958
+ for (const section of sections) {
959
+ elements.push({ tag: "hr" }, markdown(`**${section.title}**`));
960
+ elements.push(...section.commands.map(helpCommandRow));
961
+ }
962
+ return sectionCard(title, elements);
963
+ }
964
+
797
965
  renderTaskListCard(
798
966
  title: string,
799
967
  sectionTitle: string,
@@ -813,10 +981,12 @@ export class CardRenderer {
813
981
  if (index < entries.length - 1) elements.push({ tag: "hr" });
814
982
  });
815
983
  }
984
+ if (footerActions.length > 0) {
985
+ elements.push({ tag: "hr" }, taskActionRow(footerActions));
986
+ }
816
987
  if (footerLines.length > 0) {
817
988
  elements.push({ tag: "hr" }, markdown(footerLines.join("\n")));
818
989
  }
819
- if (footerActions.length > 0) elements.push(taskActionRow(footerActions));
820
990
  return {
821
991
  schema: "2.0",
822
992
  config: {
@@ -1588,6 +1758,39 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
1588
1758
  };
1589
1759
  }
1590
1760
 
1761
+ function helpCommandRow(command: HelpCardCommand): Record<string, unknown> {
1762
+ const details = [
1763
+ command.usage ? `**${command.usage}**` : undefined,
1764
+ command.description,
1765
+ ].filter((line): line is string => Boolean(line)).join(" ");
1766
+ return {
1767
+ tag: "column_set",
1768
+ flex_mode: "none",
1769
+ horizontal_spacing: "12px",
1770
+ vertical_align: "top",
1771
+ margin: "2px 0 0 0",
1772
+ columns: [
1773
+ {
1774
+ tag: "column",
1775
+ width: "auto",
1776
+ vertical_align: "top",
1777
+ elements: [
1778
+ command.action
1779
+ ? taskActionElement(command.action)
1780
+ : markdown(`**${escapeCardHtml(command.text)}**`),
1781
+ ],
1782
+ },
1783
+ {
1784
+ tag: "column",
1785
+ width: "weighted",
1786
+ weight: 1,
1787
+ vertical_align: "top",
1788
+ elements: [markdown(details)],
1789
+ },
1790
+ ],
1791
+ };
1792
+ }
1793
+
1591
1794
  function turnCard(
1592
1795
  title: string,
1593
1796
  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 {