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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -18,7 +18,8 @@ import {
18
18
  readConfiguredAgentSelection,
19
19
  readFeishuCredentials,
20
20
  shouldCreateFeishuApp,
21
- writeDefaultAgent,
21
+ shouldConfigureAgentsDuringInitialization,
22
+ writeConfiguredAgentSelection,
22
23
  writeFeishuCredentials,
23
24
  type InitializationResult,
24
25
  type InitializationStatus,
@@ -49,6 +50,7 @@ import {
49
50
  } from "./cli/AgentPrerequisites.js";
50
51
  import {
51
52
  parseMaintenanceSelection,
53
+ resolveAgentConfigurationChoices,
52
54
  resolveDefaultAgentChoice,
53
55
  selectableDefaultAgents,
54
56
  type SelectableDefaultAgent,
@@ -56,6 +58,13 @@ import {
56
58
  import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatus.js";
57
59
  import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
58
60
  import { readPackageVersion } from "./cli/packageVersion.js";
61
+ import {
62
+ readInitializationReceipt,
63
+ resolveInitializationWelcomeKind,
64
+ sendInitializationWelcome,
65
+ writeInitializationReceipt,
66
+ type InitializationWelcomeResult,
67
+ } from "./cli/InitializationWelcome.js";
59
68
  import { applyExplicitProfile, parseGlobalOptions } from "./cli/profile.js";
60
69
  import { printVerificationQrAndLink } from "./cli/VerificationOutput.js";
61
70
  import {
@@ -136,6 +145,7 @@ type FeishuInitializationStatus = "created" | "existing" | "skipped";
136
145
 
137
146
  interface InitCommandResult extends InitializationResult {
138
147
  agents: InitializedAgent[];
148
+ configuredAgents: string[];
139
149
  defaultAgent: {
140
150
  name: string;
141
151
  status: "selected" | "existing";
@@ -147,9 +157,11 @@ interface InitCommandResult extends InitializationResult {
147
157
  configuration?: EnsureFeishuAppConfigurationResult;
148
158
  };
149
159
  server: InitializationServerResult;
160
+ welcome: InitializationWelcomeResult;
150
161
  }
151
162
 
152
163
  interface InitializedAgent extends SupportedAgentInspection {
164
+ configured?: boolean;
153
165
  assistance?: {
154
166
  status: "completed" | "skipped" | "unavailable" | "failed";
155
167
  error?: string;
@@ -162,24 +174,37 @@ async function initCommand(
162
174
  explicitProfile: boolean,
163
175
  ): Promise<void> {
164
176
  const options = parseInitCommandOptions(input, explicitProfile);
177
+ const version = readPackageVersion();
178
+ const previousInitialization = readInitializationReceipt(agentBotHome());
165
179
  let initializationLock = options.reset
166
180
  ? acquireInitializationLock(agentBotHome())
167
181
  : undefined;
168
182
  let paths: InitializationResult;
169
- let initialized: Omit<InitCommandResult, "server">;
183
+ let initialized: Omit<InitCommandResult, "server" | "welcome">;
170
184
  try {
171
185
  if (options.reset) await assertResetProfileServerStopped(configPath);
172
186
  paths = initializeAgentBot({ configPath, reset: options.reset });
173
187
  if (!options.json) printInitializationPaths(paths);
174
- const agents = await initializeSupportedAgents(options.json);
175
- const defaultAgent = await selectDefaultAgent(paths.config.path, agents, options.json);
188
+ const inspectedAgents = await initializeSupportedAgents(options.json);
189
+ const defaultAgent = await configureAgentsAndDefault(
190
+ paths.config.path,
191
+ inspectedAgents,
192
+ options.json,
193
+ shouldConfigureAgentsDuringInitialization(paths.config.status),
194
+ );
195
+ const configuredAgents = readConfiguredAgentSelection(paths.config.path).agents.map((agent) => agent.name);
196
+ const configuredAgentNames = new Set(configuredAgents);
197
+ const agents = inspectedAgents.map((agent) => ({
198
+ ...agent,
199
+ configured: configuredAgentNames.has(agent.id),
200
+ }));
176
201
  initializationLock ??= acquireInitializationLock(paths.home.path);
177
202
  cleanupFeishuCredentialTemporaryFiles(paths.env.path);
178
203
  const feishu = await initializeFeishu(
179
204
  paths,
180
205
  options,
181
206
  );
182
- initialized = { ...paths, agents, defaultAgent, feishu };
207
+ initialized = { ...paths, agents, configuredAgents, defaultAgent, feishu };
183
208
  } finally {
184
209
  initializationLock?.release();
185
210
  }
@@ -189,9 +214,45 @@ async function initCommand(
189
214
  process.stdout.write(cliText("\nStarting Agent Bot server...\n", "\n正在启动 Agent Bot 服务...\n"));
190
215
  }
191
216
  const server = await startInitializedServer({ skipFeishu: options.skipFeishu, configPath });
192
- const result: InitCommandResult = { ...initialized, server };
217
+ const welcomeKind = resolveInitializationWelcomeKind({
218
+ firstInitialization: paths.config.status === "created",
219
+ previousVersion: previousInitialization?.version,
220
+ currentVersion: version,
221
+ });
222
+ let welcome: InitializationWelcomeResult;
223
+ if (options.skipFeishu) {
224
+ welcome = { status: "skipped", kind: welcomeKind, reason: "feishu-skipped" };
225
+ } else {
226
+ try {
227
+ welcome = await sendInitializationWelcome({
228
+ configPath,
229
+ version,
230
+ previousVersion: previousInitialization?.version,
231
+ kind: welcomeKind,
232
+ activationPending: server.status === "restart-scheduled",
233
+ });
234
+ } catch (error) {
235
+ welcome = {
236
+ status: "failed",
237
+ kind: welcomeKind,
238
+ message: error instanceof Error ? error.message : String(error),
239
+ };
240
+ }
241
+ }
242
+ try {
243
+ writeInitializationReceipt(paths.home.path, version);
244
+ } catch (error) {
245
+ process.stderr.write(`${cliText(
246
+ "Warning: Could not save initialization version metadata: ",
247
+ "警告:无法保存初始化版本信息:",
248
+ )}${error instanceof Error ? error.message : String(error)}\n`);
249
+ }
250
+ const result: InitCommandResult = { ...initialized, server, welcome };
193
251
  if (options.json) printJson(result);
194
- else printInitializationServerResult(server);
252
+ else {
253
+ printInitializationServerResult(server);
254
+ printInitializationWelcomeResult(welcome);
255
+ }
195
256
  }
196
257
 
197
258
  async function initializeSupportedAgents(json: boolean): Promise<InitializedAgent[]> {
@@ -277,49 +338,81 @@ async function initializeSupportedAgents(json: boolean): Promise<InitializedAgen
277
338
  return initialized;
278
339
  }
279
340
 
280
- async function selectDefaultAgent(
341
+ async function configureAgentsAndDefault(
281
342
  configPath: string,
282
343
  inspections: InitializedAgent[],
283
344
  json: boolean,
345
+ configureAgents: boolean,
284
346
  ): Promise<InitCommandResult["defaultAgent"]> {
285
347
  const output = json ? process.stderr : process.stdout;
286
348
  const configured = readConfiguredAgentSelection(configPath);
287
- const choices = selectableDefaultAgents(configured.agents, inspections);
288
- if (!process.stdin.isTTY) {
289
- if (!configured.defaultAgent || !configured.agents.some((agent) => agent.name === configured.defaultAgent)) {
349
+ const configuredDefaultIsValid = Boolean(
350
+ configured.defaultAgent
351
+ && configured.agents.some((agent) => agent.name === configured.defaultAgent),
352
+ );
353
+ if (!configureAgents) {
354
+ if (!configuredDefaultIsValid) {
290
355
  throw new Error(cliText(
291
- "A default Agent is not configured. Run agentbot init in an interactive terminal and select one.",
292
- "尚未配置默认 Agent。请在交互式终端中运行 agentbot init 并选择一个 Agent。",
356
+ "The existing Profile has no valid default Agent. Set defaults.agent in config.yaml, then run agentbot init again.",
357
+ "现有 Profile 没有有效的默认 Agent。请在 config.yaml 中设置 defaults.agent,然后重新运行 agentbot init。",
293
358
  ));
294
359
  }
295
360
  output.write(cliText(
296
- `Default Agent: ${configured.defaultAgent} (kept because no interactive terminal is available)\n`,
297
- `默认 Agent:${configured.defaultAgent}(当前没有交互式终端,已保留现有设置)\n`,
361
+ `Default Agent: ${configured.defaultAgent} (unchanged)\n`,
362
+ `默认 Agent:${configured.defaultAgent}(保持不变)\n`,
298
363
  ));
299
- return { name: configured.defaultAgent, status: "existing" };
364
+ return { name: configured.defaultAgent!, status: "existing" };
300
365
  }
366
+
367
+ const choices = selectableDefaultAgents(configured.agents, inspections);
301
368
  if (choices.length === 0) {
302
369
  throw new Error(cliText(
303
- "No available configured Agent can be selected. Install Codex or TraeX, then run agentbot init again.",
304
- "没有可选择的已配置 Agent。请安装 Codex 或 TraeX,然后重新运行 agentbot init。",
370
+ "No installed supported Agent was detected. Install Codex or TraeX, then run agentbot init again.",
371
+ "没有检测到已安装且受支持的 Agent。请安装 Codex 或 TraeX,然后重新运行 agentbot init。",
305
372
  ));
306
373
  }
307
374
 
308
- output.write(cliText("\nSelect the default Agent:\n", "\n请选择默认 Agent:\n"));
309
- for (const [index, choice] of choices.entries()) {
310
- const version = choice.installedVersion ? ` (${choice.installedVersion})` : "";
311
- const current = choice.name === configured.defaultAgent
312
- ? cliText(" [current]", " [当前]")
313
- : "";
314
- output.write(` ${index + 1}. ${choice.name} - ${choice.title}${version}${current}\n`);
315
- }
316
- const selected = choices[await promptForDefaultAgent(choices, configured.defaultAgent)];
317
- if (!selected) throw new Error(cliText("Default Agent selection failed.", "默认 Agent 选择失败。"));
318
- const updated = writeDefaultAgent(configPath, selected.name);
319
- output.write(updated
320
- ? cliText(`Saved default Agent: ${selected.name}\n`, `已保存默认 Agent:${selected.name}\n`)
321
- : cliText(`Default Agent unchanged: ${selected.name}\n`, `默认 Agent 保持不变:${selected.name}\n`));
322
- return { name: selected.name, status: updated ? "selected" : "existing" };
375
+ let selectedAgents = choices;
376
+ if (process.stdin.isTTY && choices.length > 1) {
377
+ output.write(cliText("\nSelect Agents to configure:\n", "\n请选择要配置的 Agent:\n"));
378
+ for (const [index, choice] of choices.entries()) {
379
+ output.write(` ${index + 1}. ${choice.name} - ${choice.title} (${choice.installedVersion})\n`);
380
+ }
381
+ const selectedIndexes = await promptForConfiguredAgents(choices);
382
+ selectedAgents = selectedIndexes.flatMap((index) => choices[index] ? [choices[index]!] : []);
383
+ }
384
+
385
+ const selectedNames = selectedAgents.map((agent) => agent.name);
386
+ output.write(cliText(
387
+ `Configured Agents: ${selectedNames.join(", ")}\n`,
388
+ `已配置 Agent:${selectedNames.join("、")}\n`,
389
+ ));
390
+
391
+ let selectedDefault = selectedAgents[0]!;
392
+ if (selectedAgents.length > 1) {
393
+ if (process.stdin.isTTY) {
394
+ output.write(cliText("\nSelect the default Agent:\n", "\n请选择默认 Agent:\n"));
395
+ for (const [index, choice] of selectedAgents.entries()) {
396
+ const current = choice.name === configured.defaultAgent
397
+ ? cliText(" [current]", " [当前]")
398
+ : "";
399
+ output.write(` ${index + 1}. ${choice.name} - ${choice.title} (${choice.installedVersion})${current}\n`);
400
+ }
401
+ selectedDefault = selectedAgents[
402
+ await promptForDefaultAgent(selectedAgents, configured.defaultAgent)
403
+ ]!;
404
+ } else {
405
+ selectedDefault = selectedAgents.find((agent) => agent.name === configured.defaultAgent)
406
+ ?? selectedAgents[0]!;
407
+ }
408
+ }
409
+
410
+ const updated = writeConfiguredAgentSelection(configPath, selectedNames, selectedDefault.name);
411
+ output.write(cliText(
412
+ `Default Agent: ${selectedDefault.name}${process.stdin.isTTY ? "" : " (selected automatically)"}\n`,
413
+ `默认 Agent:${selectedDefault.name}${process.stdin.isTTY ? "" : "(已自动选择)"}\n`,
414
+ ));
415
+ return { name: selectedDefault.name, status: updated ? "selected" : "existing" };
323
416
  }
324
417
 
325
418
  function promptForAgentMaintenance(choiceCount: number): Promise<number[]> {
@@ -356,6 +449,40 @@ function promptForAgentMaintenance(choiceCount: number): Promise<number[]> {
356
449
  });
357
450
  }
358
451
 
452
+ function promptForConfiguredAgents(choices: SelectableDefaultAgent[]): Promise<number[]> {
453
+ const readline = createInterface({ input: process.stdin, output: process.stderr });
454
+ return new Promise((resolve, reject) => {
455
+ let settled = false;
456
+ const finish = (selection: number[]): void => {
457
+ settled = true;
458
+ readline.close();
459
+ resolve(selection);
460
+ };
461
+ const ask = (): void => {
462
+ readline.question(cliText(
463
+ "Enter numbers or Agent names separated by commas, type all, or press Enter for all: ",
464
+ "请输入 Agent 编号或标准名(多个用逗号分隔),输入 all 或直接回车选择全部:",
465
+ ), (answer) => {
466
+ const selection = resolveAgentConfigurationChoices(answer, choices);
467
+ if (selection) {
468
+ finish(selection);
469
+ return;
470
+ }
471
+ process.stderr.write(cliText(
472
+ `Select at least one Agent using numbers from 1 to ${choices.length} or standard names.\n`,
473
+ `请使用 1 到 ${choices.length} 之间的编号或标准名,至少选择一个 Agent。\n`,
474
+ ));
475
+ ask();
476
+ });
477
+ };
478
+ listenForPromptCancellation(readline, () => {
479
+ settled = true;
480
+ reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
481
+ }, () => settled);
482
+ ask();
483
+ });
484
+ }
485
+
359
486
  function promptForDefaultAgent(
360
487
  choices: SelectableDefaultAgent[],
361
488
  currentAgent?: string,
@@ -1002,11 +1129,12 @@ function printInitializationPaths(result: InitializationResult): void {
1002
1129
  process.stdout.write(`${cliText("Log directory: ", "日志目录:")}${result.logs.path} (${initializationStatusLabel(result.logs.status)})\n`);
1003
1130
  }
1004
1131
 
1005
- function printInitializationResult(result: Omit<InitCommandResult, "server">): void {
1132
+ function printInitializationResult(result: Omit<InitCommandResult, "server" | "welcome">): void {
1006
1133
  process.stdout.write(cliText(
1007
1134
  "\nAgent Bot initialization completed.\n",
1008
1135
  "\nAgent Bot 初始化完成。\n",
1009
1136
  ));
1137
+ process.stdout.write(`${cliText("Configured Agents: ", "已配置 Agent:")}${result.configuredAgents.join(", ")}\n`);
1010
1138
  process.stdout.write(`${cliText("Default Agent: ", "默认 Agent:")}${result.defaultAgent.name}\n`);
1011
1139
  if (result.feishu.status === "created") {
1012
1140
  process.stdout.write(cliText(
@@ -1079,6 +1207,29 @@ function printInitializationServerResult(result: InitCommandResult["server"]): v
1079
1207
  printServerStartResult(result);
1080
1208
  }
1081
1209
 
1210
+ function printInitializationWelcomeResult(result: InitializationWelcomeResult): void {
1211
+ if (result.status === "sent") {
1212
+ process.stdout.write(cliText(
1213
+ "Welcome card: sent to your Lark private chat.\n",
1214
+ "欢迎卡片:已发送到你的飞书私聊。\n",
1215
+ ));
1216
+ return;
1217
+ }
1218
+ if (result.status === "failed") {
1219
+ process.stderr.write(cliText(
1220
+ `Warning: The private welcome card could not be sent: ${result.message}\n`,
1221
+ `警告:无法发送私聊欢迎卡片:${result.message}\n`,
1222
+ ));
1223
+ return;
1224
+ }
1225
+ if (result.reason === "missing-user-open-id") {
1226
+ process.stderr.write(cliText(
1227
+ "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",
1228
+ "警告:尚未获得 FEISHU_USER_OPEN_ID,无法发送私聊欢迎卡片。请先私聊机器人,再次运行 agentbot init。\n",
1229
+ ));
1230
+ }
1231
+ }
1232
+
1082
1233
  function printServerStartResult(result: ServerStartResult): void {
1083
1234
  process.stdout.write(
1084
1235
  result.status === "already-running"
@@ -35,6 +35,26 @@ 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
+ version: string;
49
+ previousVersion?: string;
50
+ activationPending?: boolean;
51
+ defaultAgentName: string;
52
+ defaultAgentTitle: string;
53
+ availableAgents: string[];
54
+ logoPath: string;
55
+ features: InitializationWelcomeFeature[];
56
+ }
57
+
38
58
  export interface SafeRestartStatusView {
39
59
  scheduleId: number;
40
60
  reason: string;
@@ -679,6 +699,113 @@ export class CardRenderer {
679
699
  return sectionCard("Agent Bot 已启动", [markdown(lines.join("\n"))], "green");
680
700
  }
681
701
 
702
+ renderInitializationWelcome(view: InitializationWelcomeView): Record<string, unknown> {
703
+ const title = view.kind === "first"
704
+ ? "欢迎使用 Agent Bot"
705
+ : view.kind === "upgrade"
706
+ ? "Agent Bot 已更新"
707
+ : "Agent Bot 已准备就绪";
708
+ const subtitle = view.activationPending
709
+ ? "配置已完成,安全重启后生效"
710
+ : view.kind === "first"
711
+ ? "本地 Agent 已接入飞书"
712
+ : view.kind === "upgrade"
713
+ ? `新版本 ${view.version} 已生效`
714
+ : "初始化配置已刷新";
715
+ const activationNote = view.activationPending
716
+ ? "当前任务完成并安全重启后生效。"
717
+ : undefined;
718
+ const intro = view.kind === "first"
719
+ ? "**初始化完成**\n从现在起,你可以直接在飞书里把任务交给本机 Agent,并随时查看进度、切换任务或创建分支。"
720
+ : view.kind === "upgrade"
721
+ ? `**升级完成**\n${view.previousVersion ? `${inlineCode(view.previousVersion)} → ` : ""}${inlineCode(view.version)} 已准备好。${activationNote ?? "下面是本版值得关注的能力。"}`
722
+ : `**配置刷新完成**\n${inlineCode(view.version)} 已重新检查配置、Agent 和飞书连接。${activationNote ?? ""}`;
723
+ const logo = {
724
+ ...localCardImage(view.logoPath, "Agent Bot logo"),
725
+ preview: false,
726
+ };
727
+ const featureColumns = view.features.slice(0, 4).map((feature) => ({
728
+ tag: "column",
729
+ width: "weighted",
730
+ weight: 1,
731
+ vertical_align: "top",
732
+ elements: [markdown([
733
+ `${feature.icon} **${feature.title}**`,
734
+ `<font color='grey'>${feature.description}</font>`,
735
+ ].join("\n"))],
736
+ }));
737
+ const featureRows = [0, 2].flatMap((start) => {
738
+ const columns = featureColumns.slice(start, start + 2);
739
+ return columns.length > 0
740
+ ? [{
741
+ tag: "column_set",
742
+ flex_mode: "none",
743
+ horizontal_spacing: "16px",
744
+ vertical_align: "top",
745
+ columns,
746
+ }]
747
+ : [];
748
+ });
749
+ const availableAgents = view.availableAgents.length > 0
750
+ ? view.availableAgents.map((agent) => inlineCode(agent)).join(" · ")
751
+ : inlineCode(view.defaultAgentName);
752
+ return {
753
+ schema: "2.0",
754
+ config: {
755
+ update_multi: true,
756
+ width_mode: "fill",
757
+ },
758
+ header: {
759
+ template: view.kind === "first" ? "turquoise" : view.kind === "upgrade" ? "blue" : "green",
760
+ title: { tag: "plain_text", content: title },
761
+ subtitle: { tag: "plain_text", content: subtitle },
762
+ padding: "12px 12px 12px 12px",
763
+ },
764
+ body: {
765
+ direction: "vertical",
766
+ vertical_spacing: "12px",
767
+ padding: "12px 12px 12px 12px",
768
+ elements: [
769
+ {
770
+ tag: "column_set",
771
+ flex_mode: "none",
772
+ horizontal_spacing: "16px",
773
+ vertical_align: "center",
774
+ columns: [
775
+ {
776
+ tag: "column",
777
+ width: "weighted",
778
+ weight: 1,
779
+ vertical_align: "center",
780
+ elements: [logo],
781
+ },
782
+ {
783
+ tag: "column",
784
+ width: "weighted",
785
+ weight: 3,
786
+ vertical_align: "center",
787
+ elements: [markdown(intro)],
788
+ },
789
+ ],
790
+ },
791
+ { tag: "hr" },
792
+ markdown(`**${view.kind === "upgrade" ? "本版亮点" : "你可以这样使用"}**`),
793
+ ...featureRows,
794
+ { tag: "hr" },
795
+ markdown("> 直接发送消息即可开始;发送 `/new` 创建新任务;发送 `/help` 查看全部命令。"),
796
+ {
797
+ ...markdown(`**版本** ${inlineCode(view.version)} · **默认 Agent** ${inlineCode(view.defaultAgentTitle)} · **可用 Agent** ${availableAgents}`),
798
+ text_size: "notation",
799
+ },
800
+ {
801
+ ...markdown("📋 [查看更新日志](https://github.com/keyou/agent-bot/blob/master/CHANGELOG.md)"),
802
+ text_size: "notation",
803
+ },
804
+ ],
805
+ },
806
+ };
807
+ }
808
+
682
809
  renderTurn(state: TurnViewState): Record<string, unknown> {
683
810
  const elements = renderTurnElements(state, "hidden");
684
811
  if (isTurnStoppable(state.status)) {
@@ -161,17 +161,7 @@ export class SafeRestartNotifier {
161
161
  }
162
162
 
163
163
  private collectNotificationTargets(status: SafeRestartStatus): void {
164
- const requestedTargets: RestartNotificationTarget[] = status.notificationTargets
165
- ?? this.store.listChatContexts("p2p").map(({ contextKey }) => ({ contextKey }));
166
- const recentTargets: RestartNotificationTarget[] = this.store.listRecentlyActiveChatContexts(
167
- new Date(Date.now() - 60_000),
168
- )
169
- .filter((chat) => !isThreadContextKey(chat.contextKey))
170
- .map(({ contextKey }) => ({ contextKey }));
171
- for (const target of [
172
- ...requestedTargets,
173
- ...recentTargets,
174
- ]) {
164
+ for (const target of status.notificationTargets ?? []) {
175
165
  const contextKey = target.contextKey.trim();
176
166
  if (!contextKey) continue;
177
167
  const existing = this.notificationTargets.get(contextKey);