@keyou007/agent-bot 0.1.15 → 0.1.16-alpha.1

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
@@ -2,7 +2,6 @@
2
2
  import fs from "node:fs";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
- import { createInterface } from "node:readline";
6
5
  import { loadConfig, loadConfigWithoutEnvironmentMutation } from "./config/loadConfig.js";
7
6
  import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.js";
8
7
  import {
@@ -50,24 +49,31 @@ import {
50
49
  } from "./cli/FeishuAppRegistration.js";
51
50
  import {
52
51
  ensureFeishuAppConfiguration,
52
+ ensureFeishuGroupMessagePermission,
53
53
  type EnsureFeishuAppConfigurationResult,
54
54
  type FeishuConfigurationChallenge,
55
55
  } from "./cli/FeishuAppConfiguration.js";
56
+ import {
57
+ feishuAffectedFeatures,
58
+ formatFeishuConfigurationFeatureIntroduction,
59
+ } from "./cli/FeishuConfigurationFeatures.js";
56
60
  import { renderCliHelp } from "./cli/help.js";
57
61
  import { cliLanguage, cliText, localizeCliErrorMessage } from "./cli/i18n.js";
58
62
  import {
59
63
  inspectSupportedAgent,
60
64
  inspectSupportedAgents,
61
65
  runSupportedAgentMaintenance,
66
+ selectAgentMaintenanceActions,
62
67
  type SupportedAgentInspection,
63
68
  } from "./cli/AgentPrerequisites.js";
64
69
  import {
65
- parseMaintenanceSelection,
66
- resolveAgentConfigurationChoices,
67
- resolveDefaultAgentChoice,
68
70
  selectableDefaultAgents,
69
- type SelectableDefaultAgent,
70
71
  } from "./cli/DefaultAgentSelection.js";
72
+ import {
73
+ InitializationPromptCancelledError,
74
+ InitUi,
75
+ shouldUseInteractiveInitialization,
76
+ } from "./cli/InitUi.js";
71
77
  import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatus.js";
72
78
  import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
73
79
  import { readPackageVersion } from "./cli/packageVersion.js";
@@ -129,6 +135,10 @@ import {
129
135
  const args = process.argv.slice(2);
130
136
 
131
137
  void main(args).catch((error: unknown) => {
138
+ if (error instanceof InitializationPromptCancelledError) {
139
+ process.exitCode = 130;
140
+ return;
141
+ }
132
142
  const message = error instanceof Error ? error.message : String(error);
133
143
  process.stderr.write(`${cliText("Error: ", "错误:")}${localizeCliErrorMessage(message)}\n`);
134
144
  process.exitCode = 1;
@@ -291,12 +301,21 @@ interface InitializedAgent extends SupportedAgentInspection {
291
301
  };
292
302
  }
293
303
 
304
+ interface FeishuInitializationContext {
305
+ result: InitCommandResult["feishu"];
306
+ credentials?: FeishuAppCredentials;
307
+ }
308
+
294
309
  async function initCommand(
295
310
  input: string[],
296
311
  configPath: string | undefined,
297
312
  ): Promise<void> {
298
313
  const options = parseInitCommandOptions(input);
299
314
  const version = readPackageVersion();
315
+ const ui = new InitUi({
316
+ interactive: shouldUseInteractiveInitialization(options.json),
317
+ });
318
+ ui.start(cliText(`AgentBot ${version} setup`, `AgentBot ${version} 初始化`));
300
319
  const previousInitialization = readInitializationReceipt(agentBotHome());
301
320
  let initializationLock = options.reset
302
321
  ? acquireInitializationLock(agentBotHome())
@@ -306,18 +325,14 @@ async function initCommand(
306
325
  try {
307
326
  if (options.reset) await assertResetProfileServerStopped(configPath);
308
327
  paths = initializeAgentBot({ configPath, reset: options.reset });
309
- if (!options.json) printInitializationPaths(paths);
310
- const inspectedAgents = await initializeSupportedAgents(options.json);
328
+ if (!options.json) printInitializationPaths(paths, ui);
329
+ const inspectedAgents = await initializeSupportedAgents(options.json, ui);
311
330
  const defaultAgent = await configureAgentsAndDefault(
312
331
  paths.config.path,
313
332
  inspectedAgents,
314
333
  options.json,
315
334
  shouldConfigureAgentsDuringInitialization(paths.config.status),
316
- );
317
- const groupMessages = await configureGroupMessageResponse(
318
- paths.config.path,
319
- options.json,
320
- shouldConfigureGroupMessagesDuringInitialization(paths.config.status),
335
+ ui,
321
336
  );
322
337
  const configuredAgents = readConfiguredAgentSelection(paths.config.path).agents.map((agent) => agent.name);
323
338
  const configuredAgentNames = new Set(configuredAgents);
@@ -327,20 +342,33 @@ async function initCommand(
327
342
  }));
328
343
  initializationLock ??= acquireInitializationLock(paths.home.path);
329
344
  cleanupFeishuCredentialTemporaryFiles(paths.env.path);
330
- const feishu = await initializeFeishu(
345
+ const feishuContext = await initializeFeishu(
331
346
  paths,
332
347
  options,
333
- groupMessages.mode === "all",
348
+ ui,
349
+ );
350
+ const groupMessages = await configureGroupMessageResponse(
351
+ paths.config.path,
352
+ options.json,
353
+ shouldConfigureGroupMessagesDuringInitialization(paths.config.status),
354
+ ui,
355
+ );
356
+ const feishu = await completeFeishuGroupMessageSetup(
357
+ feishuContext,
358
+ groupMessages.mode,
359
+ ui,
334
360
  );
361
+ ui.step(cliText("Installing the AgentBot Skill", "正在安装 AgentBot Skill"));
335
362
  const skill = bundledSkillRegistry().install();
336
363
  initialized = { ...paths, agents, configuredAgents, defaultAgent, groupMessages, feishu, skill };
337
364
  } finally {
338
365
  initializationLock?.release();
339
366
  }
340
367
 
341
- if (!options.json) printInitializationResult(initialized);
368
+ if (!options.json) printInitializationResult(initialized, ui);
342
369
  if (!options.json && !options.skipFeishu) {
343
- process.stdout.write(cliText("\nStarting Agent Bot server...\n", "\n正在启动 Agent Bot 服务...\n"));
370
+ if (ui.interactive) ui.step(cliText("Starting AgentBot", "正在启动 AgentBot"));
371
+ else process.stdout.write(cliText("\nStarting Agent Bot server...\n", "\n正在启动 Agent Bot 服务...\n"));
344
372
  }
345
373
  const server = await startInitializedServer({ skipFeishu: options.skipFeishu, configPath });
346
374
  const welcomeKind = resolveInitializationWelcomeKind({
@@ -379,41 +407,59 @@ async function initCommand(
379
407
  const result: InitCommandResult = { ...initialized, server, welcome };
380
408
  if (options.json) printJson(result);
381
409
  else {
382
- printInitializationServerResult(server);
383
- printInitializationWelcomeResult(welcome);
410
+ printInitializationServerResult(server, ui);
411
+ printInitializationWelcomeResult(welcome, ui);
412
+ ui.finish(cliText("AgentBot initialization completed", "AgentBot 初始化完成"));
384
413
  }
385
414
  }
386
415
 
387
- async function initializeSupportedAgents(json: boolean): Promise<InitializedAgent[]> {
416
+ async function initializeSupportedAgents(json: boolean, ui: InitUi): Promise<InitializedAgent[]> {
388
417
  const output = json ? process.stderr : process.stdout;
389
- output.write(cliText(
390
- "\nAgent setup\nChecking Codex and TraeX...\n",
391
- "\nAgent 设置\n正在检查 Codex 和 TraeX...\n",
392
- ));
393
- const inspections = await inspectSupportedAgents();
394
- for (const inspection of inspections) printAgentInspection(inspection, output);
395
- const actionable = inspections.filter((inspection) => inspection.action);
396
- if (actionable.length === 0) return inspections;
397
-
398
- output.write(cliText("\nAvailable actions:\n", "\n可执行的操作:\n"));
399
- for (const [index, inspection] of actionable.entries()) {
418
+ if (ui.interactive) ui.step(cliText("Checking installed Agents", "正在检查已安装的 Agent"));
419
+ else {
400
420
  output.write(cliText(
401
- ` ${index + 1}. ${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}: ${inspection.action?.command}\n`,
402
- ` ${index + 1}. ${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action?.command}\n`,
421
+ "\nAgent setup\nChecking Codex and TraeX...\n",
422
+ "\nAgent 设置\n正在检查 Codex 和 TraeX...\n",
403
423
  ));
404
424
  }
425
+ const inspections = await inspectSupportedAgents();
426
+ for (const inspection of inspections) {
427
+ if (ui.interactive) printInteractiveAgentInspection(inspection, ui);
428
+ else printAgentInspection(inspection, output);
429
+ }
430
+ const actionable = selectAgentMaintenanceActions(inspections);
431
+ if (actionable.length === 0) return inspections;
405
432
 
406
433
  let selected = new Set<number>();
407
- if (!process.stdin.isTTY) {
434
+ if (ui.interactive) {
435
+ selected = new Set(await ui.multiselect({
436
+ message: cliText("Choose Agent maintenance actions", "请选择 Agent 安装或升级操作"),
437
+ options: actionable.map((inspection, index) => ({
438
+ value: index,
439
+ label: cliText(
440
+ `${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}`,
441
+ `${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}`,
442
+ ),
443
+ hint: inspection.action?.command,
444
+ })),
445
+ initialValues: [],
446
+ required: false,
447
+ }));
448
+ if (selected.size === 0) {
449
+ ui.info(cliText("Agent maintenance skipped", "已跳过 Agent 安装和升级"));
450
+ }
451
+ } else {
452
+ output.write(cliText("\nAvailable actions:\n", "\n可执行的操作:\n"));
453
+ for (const [index, inspection] of actionable.entries()) {
454
+ output.write(cliText(
455
+ ` ${index + 1}. ${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}: ${inspection.action?.command}\n`,
456
+ ` ${index + 1}. ${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action?.command}\n`,
457
+ ));
458
+ }
408
459
  output.write(cliText(
409
460
  "No interactive terminal is available. Run the commands above manually if needed.\n",
410
461
  "当前没有交互式终端,如有需要请手动执行上面的命令。\n",
411
462
  ));
412
- } else {
413
- selected = new Set(await promptForAgentMaintenance(actionable.length));
414
- if (selected.size === 0) {
415
- output.write(cliText("No Agent changes selected.\n", "已跳过 Agent 安装和升级。\n"));
416
- }
417
463
  }
418
464
 
419
465
  const initialized: InitializedAgent[] = [];
@@ -426,25 +472,29 @@ async function initializeSupportedAgents(json: boolean): Promise<InitializedAgen
426
472
  if (!selected.has(actionIndex)) {
427
473
  initialized.push({
428
474
  ...inspection,
429
- assistance: { status: process.stdin.isTTY ? "skipped" : "unavailable" },
475
+ assistance: { status: ui.interactive ? "skipped" : "unavailable" },
430
476
  });
431
477
  continue;
432
478
  }
433
479
 
434
- output.write(cliText(
435
- `\nRunning ${inspection.name} ${inspection.action.kind}: ${inspection.action.command}\n`,
436
- `\n正在${inspection.action.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action.command}\n`,
437
- ));
480
+ const actionMessage = cliText(
481
+ `Running ${inspection.name} ${inspection.action.kind}: ${inspection.action.command}`,
482
+ `正在${inspection.action.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action.command}`,
483
+ );
484
+ if (ui.interactive) ui.step(actionMessage);
485
+ else output.write(`\n${actionMessage}\n`);
438
486
  const maintenance = await runSupportedAgentMaintenance(inspection.id, inspection.action.kind);
439
487
  if (maintenance.status !== 0 || maintenance.error) {
440
488
  const error = maintenance.error ?? cliText(
441
489
  `Command exited with code ${maintenance.status ?? "unknown"}.`,
442
490
  `命令退出码为 ${maintenance.status ?? "未知"}。`,
443
491
  );
444
- output.write(cliText(
445
- `${inspection.name} ${inspection.action.kind} did not complete; initialization will continue.\n`,
446
- `${inspection.name}${inspection.action.kind === "install" ? "安装" : "升级"}未完成,初始化将继续。\n`,
447
- ));
492
+ const warning = cliText(
493
+ `${inspection.name} ${inspection.action.kind} did not complete; initialization will continue.`,
494
+ `${inspection.name}${inspection.action.kind === "install" ? "安装" : "升级"}未完成,初始化将继续。`,
495
+ );
496
+ if (ui.interactive) ui.warn(warning);
497
+ else output.write(`${warning}\n`);
448
498
  initialized.push({ ...inspection, assistance: { status: "failed", error } });
449
499
  continue;
450
500
  }
@@ -452,15 +502,19 @@ async function initializeSupportedAgents(json: boolean): Promise<InitializedAgen
452
502
  refreshCurrentProcessPath();
453
503
  const refreshed = await inspectSupportedAgent(inspection.id);
454
504
  if (refreshed.installedVersion) {
455
- output.write(cliText(
456
- `${inspection.name} is ready (${refreshed.installedVersion}).\n`,
457
- `${inspection.name} 已就绪(${refreshed.installedVersion})。\n`,
458
- ));
505
+ const ready = cliText(
506
+ `${inspection.name} is ready (${refreshed.installedVersion}).`,
507
+ `${inspection.name} 已就绪(${refreshed.installedVersion})。`,
508
+ );
509
+ if (ui.interactive) ui.success(ready);
510
+ else output.write(`${ready}\n`);
459
511
  } else {
460
- output.write(cliText(
461
- `${inspection.name} command completed, but the CLI is not visible in this process. Open a new terminal and run agentbot init again.\n`,
462
- `${inspection.name} 命令已完成,但当前进程仍无法找到该 CLI。请打开新终端后重新运行 agentbot init。\n`,
463
- ));
512
+ const warning = cliText(
513
+ `${inspection.name} command completed, but the CLI is not visible in this process. Open a new terminal and run agentbot init again.`,
514
+ `${inspection.name} 命令已完成,但当前进程仍无法找到该 CLI。请打开新终端后重新运行 agentbot init。`,
515
+ );
516
+ if (ui.interactive) ui.warn(warning);
517
+ else output.write(`${warning}\n`);
464
518
  }
465
519
  initialized.push({ ...refreshed, assistance: { status: "completed" } });
466
520
  }
@@ -472,6 +526,7 @@ async function configureAgentsAndDefault(
472
526
  inspections: InitializedAgent[],
473
527
  json: boolean,
474
528
  configureAgents: boolean,
529
+ ui: InitUi,
475
530
  ): Promise<InitCommandResult["defaultAgent"]> {
476
531
  const output = json ? process.stderr : process.stdout;
477
532
  const configured = readConfiguredAgentSelection(configPath);
@@ -486,10 +541,12 @@ async function configureAgentsAndDefault(
486
541
  "现有 Profile 没有有效的默认 Agent。请在 config.yaml 中设置 defaults.agent,然后重新运行 agentbot init。",
487
542
  ));
488
543
  }
489
- output.write(cliText(
490
- `Default Agent: ${configured.defaultAgent} (unchanged)\n`,
491
- `默认 Agent:${configured.defaultAgent}(保持不变)\n`,
492
- ));
544
+ const message = cliText(
545
+ `Default Agent: ${configured.defaultAgent} (unchanged)`,
546
+ `默认 Agent:${configured.defaultAgent}(保持不变)`,
547
+ );
548
+ if (ui.interactive) ui.info(message);
549
+ else output.write(`${message}\n`);
493
550
  return { name: configured.defaultAgent!, status: "existing" };
494
551
  }
495
552
 
@@ -502,34 +559,42 @@ async function configureAgentsAndDefault(
502
559
  }
503
560
 
504
561
  let selectedAgents = choices;
505
- if (process.stdin.isTTY && choices.length > 1) {
506
- output.write(cliText("\nSelect Agents to configure:\n", "\n请选择要配置的 Agent:\n"));
507
- for (const [index, choice] of choices.entries()) {
508
- output.write(` ${index + 1}. ${choice.name} - ${choice.title} (${choice.installedVersion})\n`);
509
- }
510
- const selectedIndexes = await promptForConfiguredAgents(choices);
562
+ if (ui.interactive && choices.length > 1) {
563
+ const selectedIndexes = await ui.multiselect({
564
+ message: cliText("Choose Agents to configure", "请选择要配置的 Agent"),
565
+ options: choices.map((choice, index) => ({
566
+ value: index,
567
+ label: `${choice.name} - ${choice.title}`,
568
+ hint: choice.installedVersion,
569
+ })),
570
+ initialValues: choices.map((_, index) => index),
571
+ required: true,
572
+ });
511
573
  selectedAgents = selectedIndexes.flatMap((index) => choices[index] ? [choices[index]!] : []);
512
574
  }
513
575
 
514
576
  const selectedNames = selectedAgents.map((agent) => agent.name);
515
- output.write(cliText(
516
- `Configured Agents: ${selectedNames.join(", ")}\n`,
517
- `已配置 Agent:${selectedNames.join("、")}\n`,
518
- ));
577
+ const configuredMessage = cliText(
578
+ `Configured Agents: ${selectedNames.join(", ")}`,
579
+ `已配置 Agent:${selectedNames.join("、")}`,
580
+ );
581
+ if (ui.interactive) ui.success(configuredMessage);
582
+ else output.write(`${configuredMessage}\n`);
519
583
 
520
584
  let selectedDefault = selectedAgents[0]!;
521
585
  if (selectedAgents.length > 1) {
522
- if (process.stdin.isTTY) {
523
- output.write(cliText("\nSelect the default Agent:\n", "\n请选择默认 Agent:\n"));
524
- for (const [index, choice] of selectedAgents.entries()) {
525
- const current = choice.name === configured.defaultAgent
526
- ? cliText(" [current]", " [当前]")
527
- : "";
528
- output.write(` ${index + 1}. ${choice.name} - ${choice.title} (${choice.installedVersion})${current}\n`);
529
- }
530
- selectedDefault = selectedAgents[
531
- await promptForDefaultAgent(selectedAgents, configured.defaultAgent)
532
- ]!;
586
+ if (ui.interactive) {
587
+ const currentIndex = selectedAgents.findIndex((choice) => choice.name === configured.defaultAgent);
588
+ const selectedIndex = await ui.select({
589
+ message: cliText("Choose the default Agent", "请选择默认 Agent"),
590
+ options: selectedAgents.map((choice, index) => ({
591
+ value: index,
592
+ label: `${choice.name} - ${choice.title}`,
593
+ hint: choice.installedVersion,
594
+ })),
595
+ initialValue: currentIndex >= 0 ? currentIndex : 0,
596
+ });
597
+ selectedDefault = selectedAgents[selectedIndex]!;
533
598
  } else {
534
599
  selectedDefault = selectedAgents.find((agent) => agent.name === configured.defaultAgent)
535
600
  ?? selectedAgents[0]!;
@@ -537,10 +602,12 @@ async function configureAgentsAndDefault(
537
602
  }
538
603
 
539
604
  const updated = writeConfiguredAgentSelection(configPath, selectedNames, selectedDefault.name);
540
- output.write(cliText(
541
- `Default Agent: ${selectedDefault.name}${process.stdin.isTTY ? "" : " (selected automatically)"}\n`,
542
- `默认 Agent:${selectedDefault.name}${process.stdin.isTTY ? "" : "(已自动选择)"}\n`,
543
- ));
605
+ const defaultMessage = cliText(
606
+ `Default Agent: ${selectedDefault.name}${ui.interactive ? "" : " (selected automatically)"}`,
607
+ `默认 Agent:${selectedDefault.name}${ui.interactive ? "" : "(已自动选择)"}`,
608
+ );
609
+ if (ui.interactive) ui.success(defaultMessage);
610
+ else output.write(`${defaultMessage}\n`);
544
611
  return { name: selectedDefault.name, status: updated ? "selected" : "existing" };
545
612
  }
546
613
 
@@ -548,19 +615,47 @@ async function configureGroupMessageResponse(
548
615
  configPath: string,
549
616
  json: boolean,
550
617
  configure: boolean,
618
+ ui: InitUi,
551
619
  ): Promise<InitCommandResult["groupMessages"]> {
552
620
  const output = json ? process.stderr : process.stdout;
553
621
  if (!configure) {
554
- return { mode: readGroupMessageResponseMode(configPath), status: "existing" };
622
+ const mode = readGroupMessageResponseMode(configPath);
623
+ if (ui.interactive) {
624
+ ui.info(cliText(
625
+ `Group message response: ${mode === "all" ? "receive all group messages" : "require an explicit @ mention"} (unchanged)`,
626
+ `群消息响应方式:${mode === "all" ? "接收所有群消息" : "必须明确 @ 机器人"}(保持不变)`,
627
+ ));
628
+ }
629
+ return { mode, status: "existing" };
555
630
  }
556
631
 
557
632
  let mode: GroupMessageResponseMode = "mention-only";
558
- if (process.stdin.isTTY) {
559
- output.write(cliText(
560
- "\nChoose how Agent Bot responds to group messages:\n 1. Receive all group messages\n The bot can respond without an @ mention. This requires an additional Lark permission.\n 2. Require an explicit @ mention\n The additional all-group-message permission will not be requested.\n",
561
- "\n请选择 Agent Bot 响应群消息的方式:\n 1. 接收所有群消息\n 无需 @ 机器人即可响应,需要额外开通飞书权限。\n 2. 明确 @ 机器人才能接收消息\n 不会申请“免 @ 机器人”所需的额外权限。\n",
562
- ));
563
- mode = await promptForGroupMessageResponseMode();
633
+ if (ui.interactive) {
634
+ mode = await ui.select({
635
+ message: cliText(
636
+ "Choose how AgentBot responds to group messages",
637
+ "请选择 AgentBot 响应群消息的方式",
638
+ ),
639
+ options: [
640
+ {
641
+ value: "mention-only",
642
+ label: cliText("Require an explicit @ mention", "必须明确 @ 机器人"),
643
+ hint: cliText(
644
+ "Recommended; no additional manual permission is required",
645
+ "推荐;不需要额外手动申请权限",
646
+ ),
647
+ },
648
+ {
649
+ value: "all",
650
+ label: cliText("Receive all group messages", "接收所有群消息"),
651
+ hint: cliText(
652
+ "Respond without @; requires a final manual Lark permission step",
653
+ "无需 @ 即可响应;最后需要手动开通飞书权限",
654
+ ),
655
+ },
656
+ ],
657
+ initialValue: "mention-only",
658
+ });
564
659
  } else {
565
660
  output.write(cliText(
566
661
  "No interactive terminal is available. Group messages will require an explicit @ mention, and the all-group-message permission will not be requested.\n",
@@ -569,169 +664,44 @@ async function configureGroupMessageResponse(
569
664
  }
570
665
 
571
666
  writeGroupMessageResponseMode(configPath, mode);
572
- output.write(cliText(
573
- `Group message response: ${mode === "all" ? "receive all group messages" : "require an explicit @ mention"}\n`,
574
- `群消息响应方式:${mode === "all" ? "接收所有群消息" : "必须明确 @ 机器人"}\n`,
575
- ));
667
+ const message = cliText(
668
+ `Group message response: ${mode === "all" ? "receive all group messages" : "require an explicit @ mention"}`,
669
+ `群消息响应方式:${mode === "all" ? "接收所有群消息" : "必须明确 @ 机器人"}`,
670
+ );
671
+ if (ui.interactive) ui.success(message);
672
+ else output.write(`${message}\n`);
576
673
  return { mode, status: "selected" };
577
674
  }
578
675
 
579
- function promptForGroupMessageResponseMode(): Promise<GroupMessageResponseMode> {
580
- const readline = createInterface({ input: process.stdin, output: process.stderr });
581
- return new Promise((resolve, reject) => {
582
- let settled = false;
583
- const finish = (mode: GroupMessageResponseMode): void => {
584
- settled = true;
585
- readline.close();
586
- resolve(mode);
587
- };
588
- const ask = (): void => {
589
- readline.question(cliText(
590
- "Select 1 or 2 [2]: ",
591
- "请选择 1 或 2 [2]:",
592
- ), (answer) => {
593
- const normalized = answer.trim().toLowerCase();
594
- if (normalized === "1") {
595
- finish("all");
596
- return;
597
- }
598
- if (normalized === "" || normalized === "2") {
599
- finish("mention-only");
600
- return;
601
- }
602
- process.stderr.write(cliText(
603
- "Enter 1 to receive all group messages, or 2 to require an @ mention.\n",
604
- "请输入 1 接收所有群消息,或输入 2 要求明确 @ 机器人。\n",
605
- ));
606
- ask();
607
- });
608
- };
609
- listenForPromptCancellation(readline, () => {
610
- settled = true;
611
- reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
612
- }, () => settled);
613
- ask();
614
- });
615
- }
616
-
617
- function promptForAgentMaintenance(choiceCount: number): Promise<number[]> {
618
- const readline = createInterface({ input: process.stdin, output: process.stderr });
619
- return new Promise((resolve, reject) => {
620
- let settled = false;
621
- const finish = (selection: number[]): void => {
622
- settled = true;
623
- readline.close();
624
- resolve(selection);
625
- };
626
- const ask = (): void => {
627
- readline.question(cliText(
628
- "Enter action numbers separated by commas, type all, or press Enter to skip: ",
629
- "请输入要执行的操作编号(多个用逗号分隔),输入 all 选择全部,直接回车跳过:",
630
- ), (answer) => {
631
- const selection = parseMaintenanceSelection(answer, choiceCount);
632
- if (selection) {
633
- finish(selection);
634
- return;
635
- }
636
- process.stderr.write(cliText(
637
- `Enter numbers from 1 to ${choiceCount}, all, or press Enter to skip.\n`,
638
- `请输入 1 到 ${choiceCount} 之间的编号、all,或直接回车跳过。\n`,
639
- ));
640
- ask();
641
- });
642
- };
643
- listenForPromptCancellation(readline, () => {
644
- settled = true;
645
- reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
646
- }, () => settled);
647
- ask();
648
- });
649
- }
650
-
651
- function promptForConfiguredAgents(choices: SelectableDefaultAgent[]): Promise<number[]> {
652
- const readline = createInterface({ input: process.stdin, output: process.stderr });
653
- return new Promise((resolve, reject) => {
654
- let settled = false;
655
- const finish = (selection: number[]): void => {
656
- settled = true;
657
- readline.close();
658
- resolve(selection);
659
- };
660
- const ask = (): void => {
661
- readline.question(cliText(
662
- "Enter numbers or Agent names separated by commas, type all, or press Enter for all: ",
663
- "请输入 Agent 编号或标准名(多个用逗号分隔),输入 all 或直接回车选择全部:",
664
- ), (answer) => {
665
- const selection = resolveAgentConfigurationChoices(answer, choices);
666
- if (selection) {
667
- finish(selection);
668
- return;
669
- }
670
- process.stderr.write(cliText(
671
- `Select at least one Agent using numbers from 1 to ${choices.length} or standard names.\n`,
672
- `请使用 1 到 ${choices.length} 之间的编号或标准名,至少选择一个 Agent。\n`,
673
- ));
674
- ask();
675
- });
676
- };
677
- listenForPromptCancellation(readline, () => {
678
- settled = true;
679
- reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
680
- }, () => settled);
681
- ask();
682
- });
683
- }
684
-
685
- function promptForDefaultAgent(
686
- choices: SelectableDefaultAgent[],
687
- currentAgent?: string,
688
- ): Promise<number> {
689
- const readline = createInterface({ input: process.stdin, output: process.stderr });
690
- return new Promise((resolve, reject) => {
691
- let settled = false;
692
- const finish = (index: number): void => {
693
- settled = true;
694
- readline.close();
695
- resolve(index);
696
- };
697
- const ask = (): void => {
698
- const currentAvailable = choices.some((choice) => choice.name === currentAgent);
699
- readline.question(cliText(
700
- `Enter a number or Agent name${currentAvailable ? ` [${currentAgent}]` : ""}: `,
701
- `请输入编号或 Agent 标准名${currentAvailable ? ` [${currentAgent}]` : ""}:`,
702
- ), (answer) => {
703
- const index = resolveDefaultAgentChoice(answer, choices, currentAgent);
704
- if (index !== undefined) {
705
- finish(index);
706
- return;
707
- }
708
- process.stderr.write(cliText(
709
- `Enter a number from 1 to ${choices.length} or an Agent standard name.\n`,
710
- `请输入 1 到 ${choices.length} 之间的编号,或 Agent 标准名。\n`,
711
- ));
712
- ask();
713
- });
714
- };
715
- listenForPromptCancellation(readline, () => {
716
- settled = true;
717
- reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
718
- }, () => settled);
719
- ask();
720
- });
721
- }
722
-
723
- function listenForPromptCancellation(
724
- readline: ReturnType<typeof createInterface>,
725
- cancel: () => void,
726
- isSettled: () => boolean,
676
+ function printInteractiveAgentInspection(
677
+ inspection: SupportedAgentInspection,
678
+ ui: InitUi,
727
679
  ): void {
728
- readline.once("SIGINT", () => {
729
- if (!isSettled()) cancel();
730
- readline.close();
731
- });
732
- readline.once("close", () => {
733
- if (!isSettled()) cancel();
734
- });
680
+ if (inspection.state === "missing") {
681
+ ui.info(cliText(
682
+ `${inspection.name}: not installed`,
683
+ `${inspection.name}:未安装`,
684
+ ));
685
+ return;
686
+ }
687
+ if (inspection.state === "outdated") {
688
+ ui.warn(cliText(
689
+ `${inspection.name}: ${inspection.installedVersion}; update available (${inspection.latestVersion})`,
690
+ `${inspection.name}:${inspection.installedVersion};可升级到 ${inspection.latestVersion}`,
691
+ ));
692
+ return;
693
+ }
694
+ if (inspection.latestCheckFailed) {
695
+ ui.warn(cliText(
696
+ `${inspection.name}: ${inspection.installedVersion}; latest-version check unavailable`,
697
+ `${inspection.name}:${inspection.installedVersion};无法检查最新版本`,
698
+ ));
699
+ return;
700
+ }
701
+ ui.success(cliText(
702
+ `${inspection.name}: ${inspection.installedVersion} (up to date)`,
703
+ `${inspection.name}:${inspection.installedVersion}(已是最新)`,
704
+ ));
735
705
  }
736
706
 
737
707
  function printAgentInspection(
@@ -776,30 +746,24 @@ function refreshCurrentProcessPath(): void {
776
746
  async function initializeFeishu(
777
747
  paths: InitializationResult,
778
748
  options: InitCommandOptions,
779
- respondToAllGroupMessages: boolean,
780
- ): Promise<InitCommandResult["feishu"]> {
749
+ ui: InitUi,
750
+ ): Promise<FeishuInitializationContext> {
781
751
  const existing = readFeishuCredentials(paths.env.path);
782
752
  if (options.skipFeishu) {
783
753
  return {
784
- status: "skipped",
785
- ...(existing.appId ? { appId: existing.appId } : {}),
786
- ...(existing.appId && existing.appSecret
787
- ? { userOpenIdStatus: existing.userOpenId ? "configured" as const : "pending" as const }
788
- : {}),
754
+ result: {
755
+ status: "skipped",
756
+ ...(existing.appId ? { appId: existing.appId } : {}),
757
+ ...(existing.appId && existing.appSecret
758
+ ? { userOpenIdStatus: existing.userOpenId ? "configured" as const : "pending" as const }
759
+ : {}),
760
+ },
789
761
  };
790
762
  }
791
- if (!options.json) {
792
- process.stdout.write(cliText("\nLark setup\n", "\n飞书设置\n"));
793
- }
763
+ if (ui.interactive) ui.step(cliText("Creating or connecting the Lark bot", "正在创建或连接飞书机器人"));
764
+ else if (!options.json) process.stdout.write(cliText("\nLark setup\n", "\n飞书设置\n"));
794
765
 
795
- const controller = new AbortController();
796
- const onInterrupt = (): void => controller.abort(new Error(cliText(
797
- "Initialization was cancelled.",
798
- "初始化已取消。",
799
- )));
800
- process.once("SIGINT", onInterrupt);
801
- process.once("SIGTERM", onInterrupt);
802
- try {
766
+ return withInitializationCancellation(async (signal) => {
803
767
  let credentials: FeishuAppCredentials;
804
768
  let status: Exclude<FeishuInitializationStatus, "skipped">;
805
769
  if (!shouldCreateFeishuApp(existing, options.reconfigureFeishu)) {
@@ -809,37 +773,134 @@ async function initializeFeishu(
809
773
  userOpenId: existing.userOpenId,
810
774
  };
811
775
  status = "existing";
776
+ if (ui.interactive) {
777
+ ui.success(cliText(
778
+ `Using the existing Lark bot (${credentials.appId})`,
779
+ `正在使用已有飞书机器人(${credentials.appId})`,
780
+ ));
781
+ }
812
782
  } else {
813
783
  if (existing.status === "incomplete" && !options.json) {
814
- process.stdout.write(cliText(
815
- "\nIncomplete Lark credentials were found. A new bot will be created.\n",
816
- "\n发现不完整的飞书凭据,将创建一个新机器人。\n",
817
- ));
784
+ const message = cliText(
785
+ "Incomplete Lark credentials were found. A new bot will be created.",
786
+ "发现不完整的飞书凭据,将创建一个新机器人。",
787
+ );
788
+ if (ui.interactive) ui.warn(message);
789
+ else process.stdout.write(`\n${message}\n`);
818
790
  }
819
791
  credentials = await registerFeishuApp({
820
- signal: controller.signal,
821
- onVerification: (challenge) => printFeishuVerification(challenge),
792
+ signal,
793
+ onVerification: (challenge) => printFeishuVerification(challenge, ui),
822
794
  });
823
795
  writeFeishuCredentials(paths.env.path, credentials);
824
796
  status = "created";
797
+ if (ui.interactive) {
798
+ ui.success(cliText(
799
+ `Lark bot created (${credentials.appId})`,
800
+ `飞书机器人已创建(${credentials.appId})`,
801
+ ));
802
+ }
825
803
  }
826
804
 
827
- if (!options.json) {
828
- process.stdout.write(cliText(
829
- "Checking app permissions, events, and callbacks...\n",
830
- "正在检查应用权限、事件和回调...\n",
805
+ const checking = cliText(
806
+ "Checking one-click Lark permissions, events, and callbacks",
807
+ "正在检查可一键授权的飞书权限、事件和回调",
808
+ );
809
+ if (ui.interactive) ui.step(checking);
810
+ else if (!options.json) process.stdout.write(`${checking}...\n`);
811
+ const configuration = await runFeishuConfigurationInteraction(
812
+ signal,
813
+ ui,
814
+ (interaction) => ensureFeishuAppConfiguration(credentials, {
815
+ ...interaction,
816
+ respondToAllGroupMessages: false,
817
+ }),
818
+ );
819
+ if (ui.interactive) {
820
+ if (configuration.status === "partial") {
821
+ ui.warn(cliText(
822
+ "Some optional one-click Lark configuration was skipped or is not active yet",
823
+ "部分可选的一键飞书配置已跳过或尚未生效",
824
+ ));
825
+ } else {
826
+ ui.success(cliText(
827
+ "One-click Lark permissions, events, and callbacks are ready",
828
+ "可一键授权的飞书权限、事件和回调已就绪",
829
+ ));
830
+ }
831
+ }
832
+ return {
833
+ credentials,
834
+ result: {
835
+ status,
836
+ appId: credentials.appId,
837
+ userOpenIdStatus: credentials.userOpenId ? "configured" : "pending",
838
+ configuration,
839
+ },
840
+ };
841
+ });
842
+ }
843
+
844
+ async function completeFeishuGroupMessageSetup(
845
+ context: FeishuInitializationContext,
846
+ mode: GroupMessageResponseMode,
847
+ ui: InitUi,
848
+ ): Promise<InitCommandResult["feishu"]> {
849
+ if (mode !== "all" || !context.credentials) return context.result;
850
+
851
+ ui.step(cliText(
852
+ "Configuring permission for group messages without @ mentions",
853
+ "正在配置群消息免 @ 权限",
854
+ ));
855
+ const groupConfiguration = await withInitializationCancellation((signal) =>
856
+ runFeishuConfigurationInteraction(
857
+ signal,
858
+ ui,
859
+ (interaction) => ensureFeishuGroupMessagePermission(context.credentials!, interaction),
860
+ )
861
+ );
862
+ if (ui.interactive) {
863
+ if (groupConfiguration.status === "partial") {
864
+ ui.warn(cliText(
865
+ "The all-group-message permission is not active; group messages still require an @ mention",
866
+ "接收所有群消息权限尚未生效;群消息仍需 @ 机器人",
867
+ ));
868
+ } else {
869
+ ui.success(cliText(
870
+ "Group messages can be received without @ mentions",
871
+ "已可接收未 @ 机器人的群消息",
831
872
  ));
832
873
  }
833
- const manualPermissionSkip = new AbortController();
834
- const optionalSkip = new AbortController();
835
- let skipListener: OptionalAuthorizationSkipListener | undefined;
836
- const configuration = await ensureFeishuAppConfiguration(credentials, {
837
- respondToAllGroupMessages,
838
- signal: controller.signal,
874
+ }
875
+ return {
876
+ ...context.result,
877
+ configuration: mergeFeishuConfigurationResults(
878
+ context.result.configuration,
879
+ groupConfiguration,
880
+ ),
881
+ };
882
+ }
883
+
884
+ async function runFeishuConfigurationInteraction(
885
+ signal: AbortSignal,
886
+ ui: InitUi,
887
+ operation: (options: {
888
+ signal: AbortSignal;
889
+ manualPermissionSkipSignal: AbortSignal;
890
+ optionalSkipSignal: AbortSignal;
891
+ onVerification: (challenge: FeishuConfigurationChallenge) => void;
892
+ }) => Promise<EnsureFeishuAppConfigurationResult>,
893
+ ): Promise<EnsureFeishuAppConfigurationResult> {
894
+ const manualPermissionSkip = new AbortController();
895
+ const optionalSkip = new AbortController();
896
+ let skipListener: OptionalAuthorizationSkipListener | undefined;
897
+ try {
898
+ return await operation({
899
+ signal,
839
900
  manualPermissionSkipSignal: manualPermissionSkip.signal,
840
901
  optionalSkipSignal: optionalSkip.signal,
841
902
  onVerification: (challenge) => {
842
- printFeishuConfigurationVerification(challenge);
903
+ printFeishuConfigurationVerification(challenge, ui);
843
904
  if (challenge.kind === "manual_scope" && challenge.blocking) {
844
905
  skipListener?.close();
845
906
  skipListener = listenForManualPermissionSkip(() => manualPermissionSkip.abort());
@@ -848,19 +909,58 @@ async function initializeFeishu(
848
909
  skipListener = listenForOptionalAuthorizationSkip(() => optionalSkip.abort());
849
910
  }
850
911
  },
851
- }).finally(() => skipListener?.close());
852
- return {
853
- status,
854
- appId: credentials.appId,
855
- userOpenIdStatus: credentials.userOpenId ? "configured" : "pending",
856
- configuration,
857
- };
912
+ });
913
+ } finally {
914
+ skipListener?.close();
915
+ }
916
+ }
917
+
918
+ async function withInitializationCancellation<T>(
919
+ operation: (signal: AbortSignal) => Promise<T>,
920
+ ): Promise<T> {
921
+ const controller = new AbortController();
922
+ const onInterrupt = (): void => controller.abort(new Error(cliText(
923
+ "Initialization was cancelled.",
924
+ "初始化已取消。",
925
+ )));
926
+ process.once("SIGINT", onInterrupt);
927
+ process.once("SIGTERM", onInterrupt);
928
+ try {
929
+ return await operation(controller.signal);
858
930
  } finally {
859
931
  process.removeListener("SIGINT", onInterrupt);
860
932
  process.removeListener("SIGTERM", onInterrupt);
861
933
  }
862
934
  }
863
935
 
936
+ function mergeFeishuConfigurationResults(
937
+ first: EnsureFeishuAppConfigurationResult | undefined,
938
+ second: EnsureFeishuAppConfigurationResult,
939
+ ): EnsureFeishuAppConfigurationResult {
940
+ if (!first) return second;
941
+ const added = mergeFeishuConfigurationSets(first.added, second.added);
942
+ const remaining = mergeFeishuConfigurationSets(first.remaining, second.remaining);
943
+ const addedCount = added.scopes.length + added.events.length + added.callbacks.length;
944
+ const remainingCount = remaining.scopes.length + remaining.events.length + remaining.callbacks.length;
945
+ return {
946
+ status: remainingCount > 0 ? "partial" : addedCount > 0 ? "updated" : "ready",
947
+ configuration: second.configuration,
948
+ added,
949
+ remaining,
950
+ };
951
+ }
952
+
953
+ function mergeFeishuConfigurationSets(
954
+ first: FeishuConfigurationChallenge["missing"],
955
+ second: FeishuConfigurationChallenge["missing"],
956
+ ): FeishuConfigurationChallenge["missing"] {
957
+ return {
958
+ scopes: [...new Set([...first.scopes, ...second.scopes])],
959
+ events: [...new Set([...first.events, ...second.events])],
960
+ callbacks: [...new Set([...first.callbacks, ...second.callbacks])],
961
+ };
962
+ }
963
+
864
964
  async function assertResetProfileServerStopped(configPath?: string): Promise<void> {
865
965
  const endpoints = new Set<string>([controlEndpoint(defaultSqlitePath())]);
866
966
  const selectedConfigPath = configPath ?? process.env.AGENT_BOT_CONFIG;
@@ -1850,7 +1950,21 @@ function printSkillStatus(status: SkillRegistrationStatus): void {
1850
1950
  }
1851
1951
  }
1852
1952
 
1853
- function printInitializationPaths(result: InitializationResult): void {
1953
+ function printInitializationPaths(result: InitializationResult, ui: InitUi): void {
1954
+ if (ui.interactive) {
1955
+ const lines = [
1956
+ ...(result.reset
1957
+ ? [`${cliText("Reset backup", "重置备份")}: ${result.reset.backupPath}`]
1958
+ : []),
1959
+ `${cliText("Home directory", "主目录")}: ${result.home.path} (${initializationStatusLabel(result.home.status)})`,
1960
+ `${cliText("Config file", "配置文件")}: ${result.config.path} (${initializationStatusLabel(result.config.status)})`,
1961
+ `${cliText("Environment file", "环境文件")}: ${result.env.path} (${initializationStatusLabel(result.env.status)})`,
1962
+ `${cliText("Data directory", "数据目录")}: ${result.data.path} (${initializationStatusLabel(result.data.status)})`,
1963
+ `${cliText("Log directory", "日志目录")}: ${result.logs.path} (${initializationStatusLabel(result.logs.status)})`,
1964
+ ];
1965
+ ui.note(cliText("Profile", "Profile"), lines.join("\n"));
1966
+ return;
1967
+ }
1854
1968
  process.stdout.write(cliText(
1855
1969
  "Profile setup\n",
1856
1970
  "Profile 设置\n",
@@ -1865,7 +1979,44 @@ function printInitializationPaths(result: InitializationResult): void {
1865
1979
  process.stdout.write(`${cliText("Log directory: ", "日志目录:")}${result.logs.path} (${initializationStatusLabel(result.logs.status)})\n`);
1866
1980
  }
1867
1981
 
1868
- function printInitializationResult(result: Omit<InitCommandResult, "server" | "welcome">): void {
1982
+ function printInitializationResult(
1983
+ result: Omit<InitCommandResult, "server" | "welcome">,
1984
+ ui: InitUi,
1985
+ ): void {
1986
+ if (ui.interactive) {
1987
+ const feishuStatus = result.feishu.status === "created"
1988
+ ? cliText(`created (${result.feishu.appId})`, `已创建(${result.feishu.appId})`)
1989
+ : result.feishu.status === "existing"
1990
+ ? cliText(`existing (${result.feishu.appId})`, `已配置(${result.feishu.appId})`)
1991
+ : cliText("skipped", "已跳过");
1992
+ const configuration = result.feishu.configuration;
1993
+ const configurationStatus = configuration?.status === "partial"
1994
+ ? cliText("partially configured", "部分配置未完成")
1995
+ : configuration
1996
+ ? cliText("ready", "已就绪")
1997
+ : cliText("not checked", "未检查");
1998
+ ui.note(cliText("Initialization summary", "初始化摘要"), [
1999
+ `${cliText("Configured Agents", "已配置 Agent")}: ${result.configuredAgents.join(", ")}`,
2000
+ `${cliText("Default Agent", "默认 Agent")}: ${result.defaultAgent.name}`,
2001
+ `${cliText("Group messages", "群消息")}: ${result.groupMessages.mode === "all"
2002
+ ? cliText("receive all", "接收所有消息")
2003
+ : cliText("require @ mention", "需要 @ 机器人")}`,
2004
+ `${cliText("Lark bot", "飞书机器人")}: ${feishuStatus}`,
2005
+ `${cliText("Lark configuration", "飞书配置")}: ${configurationStatus}`,
2006
+ `${cliText("AgentBot Skill", "AgentBot Skill")}: ${result.skill.status.targetPath}`,
2007
+ `${cliText("Config file", "配置文件")}: ${result.config.path}`,
2008
+ ].join("\n"));
2009
+ if (configuration?.status === "partial") {
2010
+ ui.warn(cliText(
2011
+ "Initialization continued with some unavailable Lark features.",
2012
+ "部分飞书功能尚不可用,但初始化已继续。",
2013
+ ));
2014
+ for (const feature of feishuAffectedFeatures(configuration.remaining)) {
2015
+ ui.warn(`${cliText("Affected feature", "受影响功能")}: ${feature}`);
2016
+ }
2017
+ }
2018
+ return;
2019
+ }
1869
2020
  process.stdout.write(cliText(
1870
2021
  "\nAgent Bot initialization completed.\n",
1871
2022
  "\nAgent Bot 初始化完成。\n",
@@ -1932,44 +2083,60 @@ function printInitializationResult(result: Omit<InitCommandResult, "server" | "w
1932
2083
  process.stdout.write(`${cliText("Config file: ", "配置文件:")}${result.config.path}\n`);
1933
2084
  }
1934
2085
 
1935
- function printInitializationServerResult(result: InitCommandResult["server"]): void {
2086
+ function printInitializationServerResult(result: InitCommandResult["server"], ui: InitUi): void {
1936
2087
  if (result.status === "skipped") {
1937
- process.stdout.write(cliText(
1938
- "Agent Bot server: skipped (Lark is not configured; Console mode is available)\n",
1939
- "Agent Bot 服务:已跳过(未配置飞书;可使用 Console 模式)\n",
1940
- ));
2088
+ const message = cliText(
2089
+ "Agent Bot server: skipped (Lark is not configured; Console mode is available)",
2090
+ "Agent Bot 服务:已跳过(未配置飞书;可使用 Console 模式)",
2091
+ );
2092
+ if (ui.interactive) ui.warn(message);
2093
+ else process.stdout.write(`${message}\n`);
1941
2094
  return;
1942
2095
  }
1943
2096
  if (result.status === "restart-scheduled") {
1944
- process.stdout.write(cliText(
1945
- "Agent Bot server: safe restart scheduled to load the current version.\n",
1946
- "Agent Bot 服务:已安排安全重启,以加载当前版本。\n",
1947
- ));
2097
+ const message = cliText(
2098
+ "Agent Bot server: safe restart scheduled to load the current version.",
2099
+ "Agent Bot 服务:已安排安全重启,以加载当前版本。",
2100
+ );
2101
+ if (ui.interactive) ui.success(message);
2102
+ else process.stdout.write(`${message}\n`);
1948
2103
  return;
1949
2104
  }
1950
- printServerStartResult(result);
2105
+ if (ui.interactive) {
2106
+ ui.success(result.status === "already-running"
2107
+ ? cliText("Agent Bot server is already running.", "Agent Bot 服务已在运行。")
2108
+ : cliText("Agent Bot server started.", "Agent Bot 服务已启动。"));
2109
+ } else {
2110
+ printServerStartResult(result);
2111
+ }
1951
2112
  }
1952
2113
 
1953
- function printInitializationWelcomeResult(result: InitializationWelcomeResult): void {
2114
+ function printInitializationWelcomeResult(result: InitializationWelcomeResult, ui: InitUi): void {
1954
2115
  if (result.status === "sent") {
1955
- process.stdout.write(cliText(
1956
- "Welcome card: sent to your Lark private chat.\n",
1957
- "欢迎卡片:已发送到你的飞书私聊。\n",
1958
- ));
2116
+ const message = cliText(
2117
+ "Welcome card: sent to your Lark private chat.",
2118
+ "欢迎卡片:已发送到你的飞书私聊。",
2119
+ );
2120
+ if (ui.interactive) ui.success(message);
2121
+ else process.stdout.write(`${message}\n`);
1959
2122
  return;
1960
2123
  }
1961
2124
  if (result.status === "failed") {
1962
- process.stderr.write(cliText(
1963
- `Warning: The private welcome card could not be sent: ${result.message}\n`,
1964
- `警告:无法发送私聊欢迎卡片:${result.message}\n`,
1965
- ));
2125
+ const message = cliText(
2126
+ `The private welcome card could not be sent: ${result.message}`,
2127
+ `无法发送私聊欢迎卡片:${result.message}`,
2128
+ );
2129
+ if (ui.interactive) ui.warn(message);
2130
+ else process.stderr.write(`${cliText("Warning: ", "警告:")}${message}\n`);
1966
2131
  return;
1967
2132
  }
1968
2133
  if (result.reason === "missing-user-open-id") {
1969
- process.stderr.write(cliText(
1970
- "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",
1971
- "警告:尚未获得 FEISHU_USER_OPEN_ID,无法发送私聊欢迎卡片。请先私聊机器人,再次运行 agentbot init。\n",
1972
- ));
2134
+ const message = cliText(
2135
+ "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.",
2136
+ "尚未获得 FEISHU_USER_OPEN_ID,无法发送私聊欢迎卡片。请先私聊机器人,再次运行 agentbot init。",
2137
+ );
2138
+ if (ui.interactive) ui.warn(message);
2139
+ else process.stderr.write(`${cliText("Warning: ", "警告:")}${message}\n`);
1973
2140
  }
1974
2141
  }
1975
2142
 
@@ -1981,38 +2148,61 @@ function printServerStartResult(result: ServerStartResult): void {
1981
2148
  );
1982
2149
  }
1983
2150
 
1984
- function printFeishuVerification(challenge: FeishuAppRegistrationChallenge): void {
2151
+ function printFeishuVerification(challenge: FeishuAppRegistrationChallenge, ui: InitUi): void {
2152
+ if (ui.interactive) {
2153
+ ui.note(
2154
+ cliText("Create Lark bot", "创建飞书机器人"),
2155
+ cliText(
2156
+ "Open the link below and confirm creation. AgentBot will save the App ID and App Secret after Lark returns them.",
2157
+ "请打开下方链接并确认创建。飞书返回 App ID 和 App Secret 后,AgentBot 会自动保存。",
2158
+ ),
2159
+ );
2160
+ }
1985
2161
  printVerificationLink(
1986
2162
  {
1987
2163
  verificationUrl: challenge.verificationUrl,
1988
2164
  },
1989
2165
  );
1990
- process.stderr.write(cliText(
1991
- `The link expires in about ${Math.ceil(challenge.expiresIn / 60)} minutes. Waiting for confirmation...\n`,
1992
- `链接将在约 ${Math.ceil(challenge.expiresIn / 60)} 分钟后过期,正在等待确认...\n`,
1993
- ));
2166
+ const waiting = cliText(
2167
+ `The link expires in about ${Math.ceil(challenge.expiresIn / 60)} minutes. Waiting for confirmation...`,
2168
+ `链接将在约 ${Math.ceil(challenge.expiresIn / 60)} 分钟后过期,正在等待确认...`,
2169
+ );
2170
+ if (ui.interactive) ui.info(waiting);
2171
+ else process.stderr.write(`${waiting}\n`);
1994
2172
  }
1995
2173
 
1996
2174
  function printFeishuConfigurationVerification(
1997
2175
  challenge: FeishuConfigurationChallenge,
2176
+ ui: InitUi,
1998
2177
  ): void {
1999
- process.stderr.write(challenge.kind === "manual_scope"
2000
- ? cliText(
2001
- "\nThe following Lark permission must be added manually in Developer Console:\n",
2002
- "\n必须在飞书开发者后台手动添加以下权限:\n",
2003
- )
2004
- : cliText(
2005
- "\nThe Lark app is missing the following configuration:\n",
2006
- "\n飞书应用缺少以下配置:\n",
2007
- ));
2008
- if (challenge.missing.scopes.length > 0) {
2009
- process.stderr.write(`${cliText("Scopes: ", "权限:")}${challenge.missing.scopes.join(", ")}\n`);
2010
- }
2011
- if (challenge.missing.events.length > 0) {
2012
- process.stderr.write(`${cliText("Events: ", "事件:")}${challenge.missing.events.join(", ")}\n`);
2013
- }
2014
- if (challenge.missing.callbacks.length > 0) {
2015
- process.stderr.write(`${cliText("Callbacks: ", "回调:")}${challenge.missing.callbacks.join(", ")}\n`);
2178
+ const title = challenge.kind === "manual_scope"
2179
+ ? cliText("Manual Lark permission", "手动开通飞书权限")
2180
+ : cliText("Lark authorization required", "需要飞书授权");
2181
+ const details = [
2182
+ ...(challenge.missing.scopes.length > 0
2183
+ ? [`${cliText("Scopes", "权限")}: ${challenge.missing.scopes.join(", ")}`]
2184
+ : []),
2185
+ ...(challenge.missing.events.length > 0
2186
+ ? [`${cliText("Events", "事件")}: ${challenge.missing.events.join(", ")}`]
2187
+ : []),
2188
+ ...(challenge.missing.callbacks.length > 0
2189
+ ? [`${cliText("Callbacks", "回调")}: ${challenge.missing.callbacks.join(", ")}`]
2190
+ : []),
2191
+ formatFeishuConfigurationFeatureIntroduction(challenge.missing).trim(),
2192
+ ].filter(Boolean).join("\n");
2193
+ if (ui.interactive) {
2194
+ ui.note(title, details);
2195
+ } else {
2196
+ process.stderr.write(challenge.kind === "manual_scope"
2197
+ ? cliText(
2198
+ "\nThe following Lark permission must be added manually in Developer Console:\n",
2199
+ "\n必须在飞书开发者后台手动添加以下权限:\n",
2200
+ )
2201
+ : cliText(
2202
+ "\nThe Lark app is missing the following configuration:\n",
2203
+ "\n飞书应用缺少以下配置:\n",
2204
+ ));
2205
+ process.stderr.write(`${details}\n`);
2016
2206
  }
2017
2207
  printVerificationLink(
2018
2208
  {
@@ -2023,27 +2213,32 @@ function printFeishuConfigurationVerification(
2023
2213
  },
2024
2214
  );
2025
2215
  if (challenge.kind === "manual_scope") {
2026
- process.stderr.write(cliText(
2027
- "Add the filtered permission, publish the app version, and complete tenant approval if required.\n",
2028
- "请添加已筛选的权限、发布应用版本,并在需要时完成租户管理员审批。\n",
2029
- ));
2216
+ const guidance = cliText(
2217
+ "Add the filtered permission, publish the app version, and complete tenant approval if required.",
2218
+ "请添加已筛选的权限、发布应用版本,并在需要时完成租户管理员审批。",
2219
+ );
2220
+ if (ui.interactive) ui.info(guidance);
2221
+ else process.stderr.write(`${guidance}\n`);
2030
2222
  }
2223
+ let waiting: string;
2031
2224
  if (challenge.kind === "manual_scope") {
2032
- process.stderr.write(cliText(
2033
- "Waiting up to 5 minutes for this permission to appear in the published app version...\n",
2034
- "将等待最多 5 分钟,直到该权限出现在已发布的应用版本中...\n",
2035
- ));
2225
+ waiting = cliText(
2226
+ "Waiting up to 5 minutes for this permission to appear in the published app version...",
2227
+ "将等待最多 5 分钟,直到该权限出现在已发布的应用版本中...",
2228
+ );
2036
2229
  } else if (challenge.blocking) {
2037
- process.stderr.write(cliText(
2038
- "Waiting for the core scopes and message event to become active...\n",
2039
- "正在等待核心权限和消息事件生效...\n",
2040
- ));
2230
+ waiting = cliText(
2231
+ "Waiting for the core scopes and message event to become active...",
2232
+ "正在等待核心权限和消息事件生效...",
2233
+ );
2041
2234
  } else {
2042
- process.stderr.write(cliText(
2043
- "Waiting up to 5 minutes for these optional items to become active...\n",
2044
- "将等待最多 5 分钟,直到这些可选配置生效...\n",
2045
- ));
2235
+ waiting = cliText(
2236
+ "Waiting up to 5 minutes for these optional items to become active...",
2237
+ "将等待最多 5 分钟,直到这些可选配置生效...",
2238
+ );
2046
2239
  }
2240
+ if (ui.interactive) ui.info(waiting);
2241
+ else process.stderr.write(`${waiting}\n`);
2047
2242
  }
2048
2243
 
2049
2244
  function printFeishuConfigurationWarnings(missing: FeishuConfigurationChallenge["missing"]): void {
@@ -2060,58 +2255,11 @@ function printFeishuConfigurationWarnings(missing: FeishuConfigurationChallenge[
2060
2255
  if (missing.callbacks.length > 0) {
2061
2256
  process.stdout.write(`${cliText("Inactive callbacks: ", "未生效回调:")}${missing.callbacks.join(", ")}\n`);
2062
2257
  }
2063
- for (const feature of feishuFeatureWarnings(missing)) {
2258
+ for (const feature of feishuAffectedFeatures(missing)) {
2064
2259
  process.stdout.write(`${cliText("Affected feature: ", "受影响功能:")}${feature}\n`);
2065
2260
  }
2066
2261
  }
2067
2262
 
2068
- function feishuFeatureWarnings(missing: FeishuConfigurationChallenge["missing"]): string[] {
2069
- const scopes = new Set(missing.scopes);
2070
- const events = new Set(missing.events);
2071
- const callbacks = new Set(missing.callbacks);
2072
- const warnings: string[] = [];
2073
- if (scopes.has("im:message.group_msg")) {
2074
- warnings.push(cliText(
2075
- "responding to ordinary group messages that do not mention the bot",
2076
- "响应未 @ 机器人的普通群消息",
2077
- ));
2078
- }
2079
- if (scopes.has("im:chat:create")) {
2080
- warnings.push(cliText(
2081
- "creating Lark groups with /newgroup and /forkgroup",
2082
- "使用 /newgroup 和 /forkgroup 创建飞书群",
2083
- ));
2084
- }
2085
- if (scopes.has("im:chat:read") || events.has("im.chat.updated_v1")) {
2086
- warnings.push(cliText(
2087
- "synchronizing Agent Bot task titles after Lark group renames",
2088
- "飞书群重命名后同步 Agent Bot 任务标题",
2089
- ));
2090
- }
2091
- if (scopes.has("im:message.reactions:write_only")) {
2092
- warnings.push(cliText(
2093
- "showing message-processing status with reactions",
2094
- "使用 Reaction 显示消息处理状态",
2095
- ));
2096
- }
2097
- if (scopes.has("im:message:readonly")) {
2098
- warnings.push(cliText("reading images from user messages", "读取用户消息中的图片"));
2099
- }
2100
- if (scopes.has("im:resource")) {
2101
- warnings.push(cliText(
2102
- "uploading images, sending local images, or setting group avatars",
2103
- "上传图片、发送本地图片或设置群头像",
2104
- ));
2105
- }
2106
- if (scopes.has("im:message:update")) {
2107
- warnings.push(cliText("updating sent progress cards", "更新已发送的进度卡片"));
2108
- }
2109
- if (callbacks.has("card.action.trigger")) {
2110
- warnings.push(cliText("card buttons and interactive actions", "卡片按钮和交互操作"));
2111
- }
2112
- return warnings;
2113
- }
2114
-
2115
2263
  function initializationStatusLabel(status: InitializationStatus): string {
2116
2264
  if (status === "created") return cliText("created", "已创建");
2117
2265
  if (status === "updated") return cliText("updated with missing settings", "已补全缺失配置");