@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/dist/cli.js 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 { controlEndpoint, } from "./cli/controlProtocol.js";
@@ -10,11 +9,13 @@ import { acquireInitializationLock, cleanupFeishuCredentialTemporaryFiles, initi
10
9
  import { parseInitCommandOptions } from "./cli/initOptions.js";
11
10
  import { listenForManualPermissionSkip, listenForOptionalAuthorizationSkip, } from "./cli/OptionalAuthorizationSkip.js";
12
11
  import { registerFeishuApp, } from "./cli/FeishuAppRegistration.js";
13
- import { ensureFeishuAppConfiguration, } from "./cli/FeishuAppConfiguration.js";
12
+ import { ensureFeishuAppConfiguration, ensureFeishuGroupMessagePermission, } from "./cli/FeishuAppConfiguration.js";
13
+ import { feishuAffectedFeatures, formatFeishuConfigurationFeatureIntroduction, } from "./cli/FeishuConfigurationFeatures.js";
14
14
  import { renderCliHelp } from "./cli/help.js";
15
15
  import { cliLanguage, cliText, localizeCliErrorMessage } from "./cli/i18n.js";
16
- import { inspectSupportedAgent, inspectSupportedAgents, runSupportedAgentMaintenance, } from "./cli/AgentPrerequisites.js";
17
- import { parseMaintenanceSelection, resolveAgentConfigurationChoices, resolveDefaultAgentChoice, selectableDefaultAgents, } from "./cli/DefaultAgentSelection.js";
16
+ import { inspectSupportedAgent, inspectSupportedAgents, runSupportedAgentMaintenance, selectAgentMaintenanceActions, } from "./cli/AgentPrerequisites.js";
17
+ import { selectableDefaultAgents, } from "./cli/DefaultAgentSelection.js";
18
+ import { InitializationPromptCancelledError, InitUi, shouldUseInteractiveInitialization, } from "./cli/InitUi.js";
18
19
  import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatus.js";
19
20
  import { resolveSystemSkillsRoot, SkillRegistry } from "./cli/SkillRegistry.js";
20
21
  import { readPackageVersion } from "./cli/packageVersion.js";
@@ -36,6 +37,10 @@ import { createCurrentAutostartManager } from "./cli/AutostartManager.js";
36
37
  import { formatAutostartStatus, } from "./cli/autostartOutput.js";
37
38
  const args = process.argv.slice(2);
38
39
  void main(args).catch((error) => {
40
+ if (error instanceof InitializationPromptCancelledError) {
41
+ process.exitCode = 130;
42
+ return;
43
+ }
39
44
  const message = error instanceof Error ? error.message : String(error);
40
45
  process.stderr.write(`${cliText("Error: ", "错误:")}${localizeCliErrorMessage(message)}\n`);
41
46
  process.exitCode = 1;
@@ -151,6 +156,10 @@ async function updateCommand(input) {
151
156
  async function initCommand(input, configPath) {
152
157
  const options = parseInitCommandOptions(input);
153
158
  const version = readPackageVersion();
159
+ const ui = new InitUi({
160
+ interactive: shouldUseInteractiveInitialization(options.json),
161
+ });
162
+ ui.start(cliText(`AgentBot ${version} setup`, `AgentBot ${version} 初始化`));
154
163
  const previousInitialization = readInitializationReceipt(agentBotHome());
155
164
  let initializationLock = options.reset
156
165
  ? acquireInitializationLock(agentBotHome())
@@ -162,10 +171,9 @@ async function initCommand(input, configPath) {
162
171
  await assertResetProfileServerStopped(configPath);
163
172
  paths = initializeAgentBot({ configPath, reset: options.reset });
164
173
  if (!options.json)
165
- printInitializationPaths(paths);
166
- const inspectedAgents = await initializeSupportedAgents(options.json);
167
- const defaultAgent = await configureAgentsAndDefault(paths.config.path, inspectedAgents, options.json, shouldConfigureAgentsDuringInitialization(paths.config.status));
168
- const groupMessages = await configureGroupMessageResponse(paths.config.path, options.json, shouldConfigureGroupMessagesDuringInitialization(paths.config.status));
174
+ printInitializationPaths(paths, ui);
175
+ const inspectedAgents = await initializeSupportedAgents(options.json, ui);
176
+ const defaultAgent = await configureAgentsAndDefault(paths.config.path, inspectedAgents, options.json, shouldConfigureAgentsDuringInitialization(paths.config.status), ui);
169
177
  const configuredAgents = readConfiguredAgentSelection(paths.config.path).agents.map((agent) => agent.name);
170
178
  const configuredAgentNames = new Set(configuredAgents);
171
179
  const agents = inspectedAgents.map((agent) => ({
@@ -174,7 +182,10 @@ async function initCommand(input, configPath) {
174
182
  }));
175
183
  initializationLock ??= acquireInitializationLock(paths.home.path);
176
184
  cleanupFeishuCredentialTemporaryFiles(paths.env.path);
177
- const feishu = await initializeFeishu(paths, options, groupMessages.mode === "all");
185
+ const feishuContext = await initializeFeishu(paths, options, ui);
186
+ const groupMessages = await configureGroupMessageResponse(paths.config.path, options.json, shouldConfigureGroupMessagesDuringInitialization(paths.config.status), ui);
187
+ const feishu = await completeFeishuGroupMessageSetup(feishuContext, groupMessages.mode, ui);
188
+ ui.step(cliText("Installing the AgentBot Skill", "正在安装 AgentBot Skill"));
178
189
  const skill = bundledSkillRegistry().install();
179
190
  initialized = { ...paths, agents, configuredAgents, defaultAgent, groupMessages, feishu, skill };
180
191
  }
@@ -182,9 +193,12 @@ async function initCommand(input, configPath) {
182
193
  initializationLock?.release();
183
194
  }
184
195
  if (!options.json)
185
- printInitializationResult(initialized);
196
+ printInitializationResult(initialized, ui);
186
197
  if (!options.json && !options.skipFeishu) {
187
- process.stdout.write(cliText("\nStarting Agent Bot server...\n", "\n正在启动 Agent Bot 服务...\n"));
198
+ if (ui.interactive)
199
+ ui.step(cliText("Starting AgentBot", "正在启动 AgentBot"));
200
+ else
201
+ process.stdout.write(cliText("\nStarting Agent Bot server...\n", "\n正在启动 Agent Bot 服务...\n"));
188
202
  }
189
203
  const server = await startInitializedServer({ skipFeishu: options.skipFeishu, configPath });
190
204
  const welcomeKind = resolveInitializationWelcomeKind({
@@ -224,32 +238,50 @@ async function initCommand(input, configPath) {
224
238
  if (options.json)
225
239
  printJson(result);
226
240
  else {
227
- printInitializationServerResult(server);
228
- printInitializationWelcomeResult(welcome);
241
+ printInitializationServerResult(server, ui);
242
+ printInitializationWelcomeResult(welcome, ui);
243
+ ui.finish(cliText("AgentBot initialization completed", "AgentBot 初始化完成"));
229
244
  }
230
245
  }
231
- async function initializeSupportedAgents(json) {
246
+ async function initializeSupportedAgents(json, ui) {
232
247
  const output = json ? process.stderr : process.stdout;
233
- output.write(cliText("\nAgent setup\nChecking Codex and TraeX...\n", "\nAgent 设置\n正在检查 Codex 和 TraeX...\n"));
248
+ if (ui.interactive)
249
+ ui.step(cliText("Checking installed Agents", "正在检查已安装的 Agent"));
250
+ else {
251
+ output.write(cliText("\nAgent setup\nChecking Codex and TraeX...\n", "\nAgent 设置\n正在检查 Codex 和 TraeX...\n"));
252
+ }
234
253
  const inspections = await inspectSupportedAgents();
235
- for (const inspection of inspections)
236
- printAgentInspection(inspection, output);
237
- const actionable = inspections.filter((inspection) => inspection.action);
254
+ for (const inspection of inspections) {
255
+ if (ui.interactive)
256
+ printInteractiveAgentInspection(inspection, ui);
257
+ else
258
+ printAgentInspection(inspection, output);
259
+ }
260
+ const actionable = selectAgentMaintenanceActions(inspections);
238
261
  if (actionable.length === 0)
239
262
  return inspections;
240
- output.write(cliText("\nAvailable actions:\n", "\n可执行的操作:\n"));
241
- for (const [index, inspection] of actionable.entries()) {
242
- output.write(cliText(` ${index + 1}. ${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}: ${inspection.action?.command}\n`, ` ${index + 1}. ${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action?.command}\n`));
243
- }
244
263
  let selected = new Set();
245
- if (!process.stdin.isTTY) {
246
- output.write(cliText("No interactive terminal is available. Run the commands above manually if needed.\n", "当前没有交互式终端,如有需要请手动执行上面的命令。\n"));
264
+ if (ui.interactive) {
265
+ selected = new Set(await ui.multiselect({
266
+ message: cliText("Choose Agent maintenance actions", "请选择 Agent 安装或升级操作"),
267
+ options: actionable.map((inspection, index) => ({
268
+ value: index,
269
+ label: cliText(`${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}`, `${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}`),
270
+ hint: inspection.action?.command,
271
+ })),
272
+ initialValues: [],
273
+ required: false,
274
+ }));
275
+ if (selected.size === 0) {
276
+ ui.info(cliText("Agent maintenance skipped", "已跳过 Agent 安装和升级"));
277
+ }
247
278
  }
248
279
  else {
249
- selected = new Set(await promptForAgentMaintenance(actionable.length));
250
- if (selected.size === 0) {
251
- output.write(cliText("No Agent changes selected.\n", "已跳过 Agent 安装和升级。\n"));
280
+ output.write(cliText("\nAvailable actions:\n", "\n可执行的操作:\n"));
281
+ for (const [index, inspection] of actionable.entries()) {
282
+ output.write(cliText(` ${index + 1}. ${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}: ${inspection.action?.command}\n`, ` ${index + 1}. ${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action?.command}\n`));
252
283
  }
284
+ output.write(cliText("No interactive terminal is available. Run the commands above manually if needed.\n", "当前没有交互式终端,如有需要请手动执行上面的命令。\n"));
253
285
  }
254
286
  const initialized = [];
255
287
  for (const inspection of inspections) {
@@ -261,31 +293,47 @@ async function initializeSupportedAgents(json) {
261
293
  if (!selected.has(actionIndex)) {
262
294
  initialized.push({
263
295
  ...inspection,
264
- assistance: { status: process.stdin.isTTY ? "skipped" : "unavailable" },
296
+ assistance: { status: ui.interactive ? "skipped" : "unavailable" },
265
297
  });
266
298
  continue;
267
299
  }
268
- output.write(cliText(`\nRunning ${inspection.name} ${inspection.action.kind}: ${inspection.action.command}\n`, `\n正在${inspection.action.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action.command}\n`));
300
+ const actionMessage = cliText(`Running ${inspection.name} ${inspection.action.kind}: ${inspection.action.command}`, `正在${inspection.action.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action.command}`);
301
+ if (ui.interactive)
302
+ ui.step(actionMessage);
303
+ else
304
+ output.write(`\n${actionMessage}\n`);
269
305
  const maintenance = await runSupportedAgentMaintenance(inspection.id, inspection.action.kind);
270
306
  if (maintenance.status !== 0 || maintenance.error) {
271
307
  const error = maintenance.error ?? cliText(`Command exited with code ${maintenance.status ?? "unknown"}.`, `命令退出码为 ${maintenance.status ?? "未知"}。`);
272
- output.write(cliText(`${inspection.name} ${inspection.action.kind} did not complete; initialization will continue.\n`, `${inspection.name}${inspection.action.kind === "install" ? "安装" : "升级"}未完成,初始化将继续。\n`));
308
+ const warning = cliText(`${inspection.name} ${inspection.action.kind} did not complete; initialization will continue.`, `${inspection.name}${inspection.action.kind === "install" ? "安装" : "升级"}未完成,初始化将继续。`);
309
+ if (ui.interactive)
310
+ ui.warn(warning);
311
+ else
312
+ output.write(`${warning}\n`);
273
313
  initialized.push({ ...inspection, assistance: { status: "failed", error } });
274
314
  continue;
275
315
  }
276
316
  refreshCurrentProcessPath();
277
317
  const refreshed = await inspectSupportedAgent(inspection.id);
278
318
  if (refreshed.installedVersion) {
279
- output.write(cliText(`${inspection.name} is ready (${refreshed.installedVersion}).\n`, `${inspection.name} 已就绪(${refreshed.installedVersion})。\n`));
319
+ const ready = cliText(`${inspection.name} is ready (${refreshed.installedVersion}).`, `${inspection.name} 已就绪(${refreshed.installedVersion})。`);
320
+ if (ui.interactive)
321
+ ui.success(ready);
322
+ else
323
+ output.write(`${ready}\n`);
280
324
  }
281
325
  else {
282
- output.write(cliText(`${inspection.name} command completed, but the CLI is not visible in this process. Open a new terminal and run agentbot init again.\n`, `${inspection.name} 命令已完成,但当前进程仍无法找到该 CLI。请打开新终端后重新运行 agentbot init。\n`));
326
+ const warning = cliText(`${inspection.name} command completed, but the CLI is not visible in this process. Open a new terminal and run agentbot init again.`, `${inspection.name} 命令已完成,但当前进程仍无法找到该 CLI。请打开新终端后重新运行 agentbot init。`);
327
+ if (ui.interactive)
328
+ ui.warn(warning);
329
+ else
330
+ output.write(`${warning}\n`);
283
331
  }
284
332
  initialized.push({ ...refreshed, assistance: { status: "completed" } });
285
333
  }
286
334
  return initialized;
287
335
  }
288
- async function configureAgentsAndDefault(configPath, inspections, json, configureAgents) {
336
+ async function configureAgentsAndDefault(configPath, inspections, json, configureAgents, ui) {
289
337
  const output = json ? process.stderr : process.stdout;
290
338
  const configured = readConfiguredAgentSelection(configPath);
291
339
  const configuredDefaultIsValid = Boolean(configured.defaultAgent
@@ -294,7 +342,11 @@ async function configureAgentsAndDefault(configPath, inspections, json, configur
294
342
  if (!configuredDefaultIsValid) {
295
343
  throw new Error(cliText("The existing Profile has no valid default Agent. Set defaults.agent in config.yaml, then run agentbot init again.", "现有 Profile 没有有效的默认 Agent。请在 config.yaml 中设置 defaults.agent,然后重新运行 agentbot init。"));
296
344
  }
297
- output.write(cliText(`Default Agent: ${configured.defaultAgent} (unchanged)\n`, `默认 Agent:${configured.defaultAgent}(保持不变)\n`));
345
+ const message = cliText(`Default Agent: ${configured.defaultAgent} (unchanged)`, `默认 Agent:${configured.defaultAgent}(保持不变)`);
346
+ if (ui.interactive)
347
+ ui.info(message);
348
+ else
349
+ output.write(`${message}\n`);
298
350
  return { name: configured.defaultAgent, status: "existing" };
299
351
  }
300
352
  const choices = selectableDefaultAgents(configured.agents, inspections);
@@ -302,27 +354,39 @@ async function configureAgentsAndDefault(configPath, inspections, json, configur
302
354
  throw new Error(cliText("No installed supported Agent was detected. Install Codex or TraeX, then run agentbot init again.", "没有检测到已安装且受支持的 Agent。请安装 Codex 或 TraeX,然后重新运行 agentbot init。"));
303
355
  }
304
356
  let selectedAgents = choices;
305
- if (process.stdin.isTTY && choices.length > 1) {
306
- output.write(cliText("\nSelect Agents to configure:\n", "\n请选择要配置的 Agent:\n"));
307
- for (const [index, choice] of choices.entries()) {
308
- output.write(` ${index + 1}. ${choice.name} - ${choice.title} (${choice.installedVersion})\n`);
309
- }
310
- const selectedIndexes = await promptForConfiguredAgents(choices);
357
+ if (ui.interactive && choices.length > 1) {
358
+ const selectedIndexes = await ui.multiselect({
359
+ message: cliText("Choose Agents to configure", "请选择要配置的 Agent"),
360
+ options: choices.map((choice, index) => ({
361
+ value: index,
362
+ label: `${choice.name} - ${choice.title}`,
363
+ hint: choice.installedVersion,
364
+ })),
365
+ initialValues: choices.map((_, index) => index),
366
+ required: true,
367
+ });
311
368
  selectedAgents = selectedIndexes.flatMap((index) => choices[index] ? [choices[index]] : []);
312
369
  }
313
370
  const selectedNames = selectedAgents.map((agent) => agent.name);
314
- output.write(cliText(`Configured Agents: ${selectedNames.join(", ")}\n`, `已配置 Agent:${selectedNames.join("、")}\n`));
371
+ const configuredMessage = cliText(`Configured Agents: ${selectedNames.join(", ")}`, `已配置 Agent:${selectedNames.join("、")}`);
372
+ if (ui.interactive)
373
+ ui.success(configuredMessage);
374
+ else
375
+ output.write(`${configuredMessage}\n`);
315
376
  let selectedDefault = selectedAgents[0];
316
377
  if (selectedAgents.length > 1) {
317
- if (process.stdin.isTTY) {
318
- output.write(cliText("\nSelect the default Agent:\n", "\n请选择默认 Agent:\n"));
319
- for (const [index, choice] of selectedAgents.entries()) {
320
- const current = choice.name === configured.defaultAgent
321
- ? cliText(" [current]", " [当前]")
322
- : "";
323
- output.write(` ${index + 1}. ${choice.name} - ${choice.title} (${choice.installedVersion})${current}\n`);
324
- }
325
- selectedDefault = selectedAgents[await promptForDefaultAgent(selectedAgents, configured.defaultAgent)];
378
+ if (ui.interactive) {
379
+ const currentIndex = selectedAgents.findIndex((choice) => choice.name === configured.defaultAgent);
380
+ const selectedIndex = await ui.select({
381
+ message: cliText("Choose the default Agent", "请选择默认 Agent"),
382
+ options: selectedAgents.map((choice, index) => ({
383
+ value: index,
384
+ label: `${choice.name} - ${choice.title}`,
385
+ hint: choice.installedVersion,
386
+ })),
387
+ initialValue: currentIndex >= 0 ? currentIndex : 0,
388
+ });
389
+ selectedDefault = selectedAgents[selectedIndex];
326
390
  }
327
391
  else {
328
392
  selectedDefault = selectedAgents.find((agent) => agent.name === configured.defaultAgent)
@@ -330,149 +394,66 @@ async function configureAgentsAndDefault(configPath, inspections, json, configur
330
394
  }
331
395
  }
332
396
  const updated = writeConfiguredAgentSelection(configPath, selectedNames, selectedDefault.name);
333
- output.write(cliText(`Default Agent: ${selectedDefault.name}${process.stdin.isTTY ? "" : " (selected automatically)"}\n`, `默认 Agent:${selectedDefault.name}${process.stdin.isTTY ? "" : "(已自动选择)"}\n`));
397
+ const defaultMessage = cliText(`Default Agent: ${selectedDefault.name}${ui.interactive ? "" : " (selected automatically)"}`, `默认 Agent:${selectedDefault.name}${ui.interactive ? "" : "(已自动选择)"}`);
398
+ if (ui.interactive)
399
+ ui.success(defaultMessage);
400
+ else
401
+ output.write(`${defaultMessage}\n`);
334
402
  return { name: selectedDefault.name, status: updated ? "selected" : "existing" };
335
403
  }
336
- async function configureGroupMessageResponse(configPath, json, configure) {
404
+ async function configureGroupMessageResponse(configPath, json, configure, ui) {
337
405
  const output = json ? process.stderr : process.stdout;
338
406
  if (!configure) {
339
- return { mode: readGroupMessageResponseMode(configPath), status: "existing" };
407
+ const mode = readGroupMessageResponseMode(configPath);
408
+ if (ui.interactive) {
409
+ ui.info(cliText(`Group message response: ${mode === "all" ? "receive all group messages" : "require an explicit @ mention"} (unchanged)`, `群消息响应方式:${mode === "all" ? "接收所有群消息" : "必须明确 @ 机器人"}(保持不变)`));
410
+ }
411
+ return { mode, status: "existing" };
340
412
  }
341
413
  let mode = "mention-only";
342
- if (process.stdin.isTTY) {
343
- output.write(cliText("\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", "\n请选择 Agent Bot 响应群消息的方式:\n 1. 接收所有群消息\n 无需 @ 机器人即可响应,需要额外开通飞书权限。\n 2. 明确 @ 机器人才能接收消息\n 不会申请“免 @ 机器人”所需的额外权限。\n"));
344
- mode = await promptForGroupMessageResponseMode();
414
+ if (ui.interactive) {
415
+ mode = await ui.select({
416
+ message: cliText("Choose how AgentBot responds to group messages", "请选择 AgentBot 响应群消息的方式"),
417
+ options: [
418
+ {
419
+ value: "mention-only",
420
+ label: cliText("Require an explicit @ mention", "必须明确 @ 机器人"),
421
+ hint: cliText("Recommended; no additional manual permission is required", "推荐;不需要额外手动申请权限"),
422
+ },
423
+ {
424
+ value: "all",
425
+ label: cliText("Receive all group messages", "接收所有群消息"),
426
+ hint: cliText("Respond without @; requires a final manual Lark permission step", "无需 @ 即可响应;最后需要手动开通飞书权限"),
427
+ },
428
+ ],
429
+ initialValue: "mention-only",
430
+ });
345
431
  }
346
432
  else {
347
433
  output.write(cliText("No interactive terminal is available. Group messages will require an explicit @ mention, and the all-group-message permission will not be requested.\n", "当前没有交互式终端。群消息将要求明确 @ 机器人,并且不会申请接收所有群消息的额外权限。\n"));
348
434
  }
349
435
  writeGroupMessageResponseMode(configPath, mode);
350
- output.write(cliText(`Group message response: ${mode === "all" ? "receive all group messages" : "require an explicit @ mention"}\n`, `群消息响应方式:${mode === "all" ? "接收所有群消息" : "必须明确 @ 机器人"}\n`));
436
+ const message = cliText(`Group message response: ${mode === "all" ? "receive all group messages" : "require an explicit @ mention"}`, `群消息响应方式:${mode === "all" ? "接收所有群消息" : "必须明确 @ 机器人"}`);
437
+ if (ui.interactive)
438
+ ui.success(message);
439
+ else
440
+ output.write(`${message}\n`);
351
441
  return { mode, status: "selected" };
352
442
  }
353
- function promptForGroupMessageResponseMode() {
354
- const readline = createInterface({ input: process.stdin, output: process.stderr });
355
- return new Promise((resolve, reject) => {
356
- let settled = false;
357
- const finish = (mode) => {
358
- settled = true;
359
- readline.close();
360
- resolve(mode);
361
- };
362
- const ask = () => {
363
- readline.question(cliText("Select 1 or 2 [2]: ", "请选择 1 或 2 [2]:"), (answer) => {
364
- const normalized = answer.trim().toLowerCase();
365
- if (normalized === "1") {
366
- finish("all");
367
- return;
368
- }
369
- if (normalized === "" || normalized === "2") {
370
- finish("mention-only");
371
- return;
372
- }
373
- process.stderr.write(cliText("Enter 1 to receive all group messages, or 2 to require an @ mention.\n", "请输入 1 接收所有群消息,或输入 2 要求明确 @ 机器人。\n"));
374
- ask();
375
- });
376
- };
377
- listenForPromptCancellation(readline, () => {
378
- settled = true;
379
- reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
380
- }, () => settled);
381
- ask();
382
- });
383
- }
384
- function promptForAgentMaintenance(choiceCount) {
385
- const readline = createInterface({ input: process.stdin, output: process.stderr });
386
- return new Promise((resolve, reject) => {
387
- let settled = false;
388
- const finish = (selection) => {
389
- settled = true;
390
- readline.close();
391
- resolve(selection);
392
- };
393
- const ask = () => {
394
- readline.question(cliText("Enter action numbers separated by commas, type all, or press Enter to skip: ", "请输入要执行的操作编号(多个用逗号分隔),输入 all 选择全部,直接回车跳过:"), (answer) => {
395
- const selection = parseMaintenanceSelection(answer, choiceCount);
396
- if (selection) {
397
- finish(selection);
398
- return;
399
- }
400
- process.stderr.write(cliText(`Enter numbers from 1 to ${choiceCount}, all, or press Enter to skip.\n`, `请输入 1 到 ${choiceCount} 之间的编号、all,或直接回车跳过。\n`));
401
- ask();
402
- });
403
- };
404
- listenForPromptCancellation(readline, () => {
405
- settled = true;
406
- reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
407
- }, () => settled);
408
- ask();
409
- });
410
- }
411
- function promptForConfiguredAgents(choices) {
412
- const readline = createInterface({ input: process.stdin, output: process.stderr });
413
- return new Promise((resolve, reject) => {
414
- let settled = false;
415
- const finish = (selection) => {
416
- settled = true;
417
- readline.close();
418
- resolve(selection);
419
- };
420
- const ask = () => {
421
- readline.question(cliText("Enter numbers or Agent names separated by commas, type all, or press Enter for all: ", "请输入 Agent 编号或标准名(多个用逗号分隔),输入 all 或直接回车选择全部:"), (answer) => {
422
- const selection = resolveAgentConfigurationChoices(answer, choices);
423
- if (selection) {
424
- finish(selection);
425
- return;
426
- }
427
- process.stderr.write(cliText(`Select at least one Agent using numbers from 1 to ${choices.length} or standard names.\n`, `请使用 1 到 ${choices.length} 之间的编号或标准名,至少选择一个 Agent。\n`));
428
- ask();
429
- });
430
- };
431
- listenForPromptCancellation(readline, () => {
432
- settled = true;
433
- reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
434
- }, () => settled);
435
- ask();
436
- });
437
- }
438
- function promptForDefaultAgent(choices, currentAgent) {
439
- const readline = createInterface({ input: process.stdin, output: process.stderr });
440
- return new Promise((resolve, reject) => {
441
- let settled = false;
442
- const finish = (index) => {
443
- settled = true;
444
- readline.close();
445
- resolve(index);
446
- };
447
- const ask = () => {
448
- const currentAvailable = choices.some((choice) => choice.name === currentAgent);
449
- readline.question(cliText(`Enter a number or Agent name${currentAvailable ? ` [${currentAgent}]` : ""}: `, `请输入编号或 Agent 标准名${currentAvailable ? ` [${currentAgent}]` : ""}:`), (answer) => {
450
- const index = resolveDefaultAgentChoice(answer, choices, currentAgent);
451
- if (index !== undefined) {
452
- finish(index);
453
- return;
454
- }
455
- process.stderr.write(cliText(`Enter a number from 1 to ${choices.length} or an Agent standard name.\n`, `请输入 1 到 ${choices.length} 之间的编号,或 Agent 标准名。\n`));
456
- ask();
457
- });
458
- };
459
- listenForPromptCancellation(readline, () => {
460
- settled = true;
461
- reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
462
- }, () => settled);
463
- ask();
464
- });
465
- }
466
- function listenForPromptCancellation(readline, cancel, isSettled) {
467
- readline.once("SIGINT", () => {
468
- if (!isSettled())
469
- cancel();
470
- readline.close();
471
- });
472
- readline.once("close", () => {
473
- if (!isSettled())
474
- cancel();
475
- });
443
+ function printInteractiveAgentInspection(inspection, ui) {
444
+ if (inspection.state === "missing") {
445
+ ui.info(cliText(`${inspection.name}: not installed`, `${inspection.name}:未安装`));
446
+ return;
447
+ }
448
+ if (inspection.state === "outdated") {
449
+ ui.warn(cliText(`${inspection.name}: ${inspection.installedVersion}; update available (${inspection.latestVersion})`, `${inspection.name}:${inspection.installedVersion};可升级到 ${inspection.latestVersion}`));
450
+ return;
451
+ }
452
+ if (inspection.latestCheckFailed) {
453
+ ui.warn(cliText(`${inspection.name}: ${inspection.installedVersion}; latest-version check unavailable`, `${inspection.name}:${inspection.installedVersion};无法检查最新版本`));
454
+ return;
455
+ }
456
+ ui.success(cliText(`${inspection.name}: ${inspection.installedVersion} (up to date)`, `${inspection.name}:${inspection.installedVersion}(已是最新)`));
476
457
  }
477
458
  function printAgentInspection(inspection, output) {
478
459
  if (inspection.state === "missing") {
@@ -498,25 +479,24 @@ function refreshCurrentProcessPath() {
498
479
  if (pathEntry?.[1])
499
480
  process.env.PATH = pathEntry[1];
500
481
  }
501
- async function initializeFeishu(paths, options, respondToAllGroupMessages) {
482
+ async function initializeFeishu(paths, options, ui) {
502
483
  const existing = readFeishuCredentials(paths.env.path);
503
484
  if (options.skipFeishu) {
504
485
  return {
505
- status: "skipped",
506
- ...(existing.appId ? { appId: existing.appId } : {}),
507
- ...(existing.appId && existing.appSecret
508
- ? { userOpenIdStatus: existing.userOpenId ? "configured" : "pending" }
509
- : {}),
486
+ result: {
487
+ status: "skipped",
488
+ ...(existing.appId ? { appId: existing.appId } : {}),
489
+ ...(existing.appId && existing.appSecret
490
+ ? { userOpenIdStatus: existing.userOpenId ? "configured" : "pending" }
491
+ : {}),
492
+ },
510
493
  };
511
494
  }
512
- if (!options.json) {
495
+ if (ui.interactive)
496
+ ui.step(cliText("Creating or connecting the Lark bot", "正在创建或连接飞书机器人"));
497
+ else if (!options.json)
513
498
  process.stdout.write(cliText("\nLark setup\n", "\n飞书设置\n"));
514
- }
515
- const controller = new AbortController();
516
- const onInterrupt = () => controller.abort(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
517
- process.once("SIGINT", onInterrupt);
518
- process.once("SIGTERM", onInterrupt);
519
- try {
499
+ return withInitializationCancellation(async (signal) => {
520
500
  let credentials;
521
501
  let status;
522
502
  if (!shouldCreateFeishuApp(existing, options.reconfigureFeishu)) {
@@ -526,31 +506,85 @@ async function initializeFeishu(paths, options, respondToAllGroupMessages) {
526
506
  userOpenId: existing.userOpenId,
527
507
  };
528
508
  status = "existing";
509
+ if (ui.interactive) {
510
+ ui.success(cliText(`Using the existing Lark bot (${credentials.appId})`, `正在使用已有飞书机器人(${credentials.appId})`));
511
+ }
529
512
  }
530
513
  else {
531
514
  if (existing.status === "incomplete" && !options.json) {
532
- process.stdout.write(cliText("\nIncomplete Lark credentials were found. A new bot will be created.\n", "\n发现不完整的飞书凭据,将创建一个新机器人。\n"));
515
+ const message = cliText("Incomplete Lark credentials were found. A new bot will be created.", "发现不完整的飞书凭据,将创建一个新机器人。");
516
+ if (ui.interactive)
517
+ ui.warn(message);
518
+ else
519
+ process.stdout.write(`\n${message}\n`);
533
520
  }
534
521
  credentials = await registerFeishuApp({
535
- signal: controller.signal,
536
- onVerification: (challenge) => printFeishuVerification(challenge),
522
+ signal,
523
+ onVerification: (challenge) => printFeishuVerification(challenge, ui),
537
524
  });
538
525
  writeFeishuCredentials(paths.env.path, credentials);
539
526
  status = "created";
527
+ if (ui.interactive) {
528
+ ui.success(cliText(`Lark bot created (${credentials.appId})`, `飞书机器人已创建(${credentials.appId})`));
529
+ }
530
+ }
531
+ const checking = cliText("Checking one-click Lark permissions, events, and callbacks", "正在检查可一键授权的飞书权限、事件和回调");
532
+ if (ui.interactive)
533
+ ui.step(checking);
534
+ else if (!options.json)
535
+ process.stdout.write(`${checking}...\n`);
536
+ const configuration = await runFeishuConfigurationInteraction(signal, ui, (interaction) => ensureFeishuAppConfiguration(credentials, {
537
+ ...interaction,
538
+ respondToAllGroupMessages: false,
539
+ }));
540
+ if (ui.interactive) {
541
+ if (configuration.status === "partial") {
542
+ ui.warn(cliText("Some optional one-click Lark configuration was skipped or is not active yet", "部分可选的一键飞书配置已跳过或尚未生效"));
543
+ }
544
+ else {
545
+ ui.success(cliText("One-click Lark permissions, events, and callbacks are ready", "可一键授权的飞书权限、事件和回调已就绪"));
546
+ }
540
547
  }
541
- if (!options.json) {
542
- process.stdout.write(cliText("Checking app permissions, events, and callbacks...\n", "正在检查应用权限、事件和回调...\n"));
548
+ return {
549
+ credentials,
550
+ result: {
551
+ status,
552
+ appId: credentials.appId,
553
+ userOpenIdStatus: credentials.userOpenId ? "configured" : "pending",
554
+ configuration,
555
+ },
556
+ };
557
+ });
558
+ }
559
+ async function completeFeishuGroupMessageSetup(context, mode, ui) {
560
+ if (mode !== "all" || !context.credentials)
561
+ return context.result;
562
+ ui.step(cliText("Configuring permission for group messages without @ mentions", "正在配置群消息免 @ 权限"));
563
+ const groupConfiguration = await withInitializationCancellation((signal) => runFeishuConfigurationInteraction(signal, ui, (interaction) => ensureFeishuGroupMessagePermission(context.credentials, interaction)));
564
+ if (ui.interactive) {
565
+ if (groupConfiguration.status === "partial") {
566
+ ui.warn(cliText("The all-group-message permission is not active; group messages still require an @ mention", "接收所有群消息权限尚未生效;群消息仍需 @ 机器人"));
567
+ }
568
+ else {
569
+ ui.success(cliText("Group messages can be received without @ mentions", "已可接收未 @ 机器人的群消息"));
543
570
  }
544
- const manualPermissionSkip = new AbortController();
545
- const optionalSkip = new AbortController();
546
- let skipListener;
547
- const configuration = await ensureFeishuAppConfiguration(credentials, {
548
- respondToAllGroupMessages,
549
- signal: controller.signal,
571
+ }
572
+ return {
573
+ ...context.result,
574
+ configuration: mergeFeishuConfigurationResults(context.result.configuration, groupConfiguration),
575
+ };
576
+ }
577
+ async function runFeishuConfigurationInteraction(signal, ui, operation) {
578
+ const manualPermissionSkip = new AbortController();
579
+ const optionalSkip = new AbortController();
580
+ let skipListener;
581
+ try {
582
+ return await operation({
583
+ signal,
550
584
  manualPermissionSkipSignal: manualPermissionSkip.signal,
551
585
  optionalSkipSignal: optionalSkip.signal,
552
586
  onVerification: (challenge) => {
553
- printFeishuConfigurationVerification(challenge);
587
+ printFeishuConfigurationVerification(challenge, ui);
554
588
  if (challenge.kind === "manual_scope" && challenge.blocking) {
555
589
  skipListener?.close();
556
590
  skipListener = listenForManualPermissionSkip(() => manualPermissionSkip.abort());
@@ -560,19 +594,46 @@ async function initializeFeishu(paths, options, respondToAllGroupMessages) {
560
594
  skipListener = listenForOptionalAuthorizationSkip(() => optionalSkip.abort());
561
595
  }
562
596
  },
563
- }).finally(() => skipListener?.close());
564
- return {
565
- status,
566
- appId: credentials.appId,
567
- userOpenIdStatus: credentials.userOpenId ? "configured" : "pending",
568
- configuration,
569
- };
597
+ });
598
+ }
599
+ finally {
600
+ skipListener?.close();
601
+ }
602
+ }
603
+ async function withInitializationCancellation(operation) {
604
+ const controller = new AbortController();
605
+ const onInterrupt = () => controller.abort(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
606
+ process.once("SIGINT", onInterrupt);
607
+ process.once("SIGTERM", onInterrupt);
608
+ try {
609
+ return await operation(controller.signal);
570
610
  }
571
611
  finally {
572
612
  process.removeListener("SIGINT", onInterrupt);
573
613
  process.removeListener("SIGTERM", onInterrupt);
574
614
  }
575
615
  }
616
+ function mergeFeishuConfigurationResults(first, second) {
617
+ if (!first)
618
+ return second;
619
+ const added = mergeFeishuConfigurationSets(first.added, second.added);
620
+ const remaining = mergeFeishuConfigurationSets(first.remaining, second.remaining);
621
+ const addedCount = added.scopes.length + added.events.length + added.callbacks.length;
622
+ const remainingCount = remaining.scopes.length + remaining.events.length + remaining.callbacks.length;
623
+ return {
624
+ status: remainingCount > 0 ? "partial" : addedCount > 0 ? "updated" : "ready",
625
+ configuration: second.configuration,
626
+ added,
627
+ remaining,
628
+ };
629
+ }
630
+ function mergeFeishuConfigurationSets(first, second) {
631
+ return {
632
+ scopes: [...new Set([...first.scopes, ...second.scopes])],
633
+ events: [...new Set([...first.events, ...second.events])],
634
+ callbacks: [...new Set([...first.callbacks, ...second.callbacks])],
635
+ };
636
+ }
576
637
  async function assertResetProfileServerStopped(configPath) {
577
638
  const endpoints = new Set([controlEndpoint(defaultSqlitePath())]);
578
639
  const selectedConfigPath = configPath ?? process.env.AGENT_BOT_CONFIG;
@@ -1448,7 +1509,21 @@ function printSkillStatus(status) {
1448
1509
  : cliText("update required", "需要更新")}\n`);
1449
1510
  }
1450
1511
  }
1451
- function printInitializationPaths(result) {
1512
+ function printInitializationPaths(result, ui) {
1513
+ if (ui.interactive) {
1514
+ const lines = [
1515
+ ...(result.reset
1516
+ ? [`${cliText("Reset backup", "重置备份")}: ${result.reset.backupPath}`]
1517
+ : []),
1518
+ `${cliText("Home directory", "主目录")}: ${result.home.path} (${initializationStatusLabel(result.home.status)})`,
1519
+ `${cliText("Config file", "配置文件")}: ${result.config.path} (${initializationStatusLabel(result.config.status)})`,
1520
+ `${cliText("Environment file", "环境文件")}: ${result.env.path} (${initializationStatusLabel(result.env.status)})`,
1521
+ `${cliText("Data directory", "数据目录")}: ${result.data.path} (${initializationStatusLabel(result.data.status)})`,
1522
+ `${cliText("Log directory", "日志目录")}: ${result.logs.path} (${initializationStatusLabel(result.logs.status)})`,
1523
+ ];
1524
+ ui.note(cliText("Profile", "Profile"), lines.join("\n"));
1525
+ return;
1526
+ }
1452
1527
  process.stdout.write(cliText("Profile setup\n", "Profile 设置\n"));
1453
1528
  if (result.reset) {
1454
1529
  process.stdout.write(`${cliText("Reset backup: ", "重置备份:")}${result.reset.backupPath}\n`);
@@ -1459,7 +1534,38 @@ function printInitializationPaths(result) {
1459
1534
  process.stdout.write(`${cliText("Data directory: ", "数据目录:")}${result.data.path} (${initializationStatusLabel(result.data.status)})\n`);
1460
1535
  process.stdout.write(`${cliText("Log directory: ", "日志目录:")}${result.logs.path} (${initializationStatusLabel(result.logs.status)})\n`);
1461
1536
  }
1462
- function printInitializationResult(result) {
1537
+ function printInitializationResult(result, ui) {
1538
+ if (ui.interactive) {
1539
+ const feishuStatus = result.feishu.status === "created"
1540
+ ? cliText(`created (${result.feishu.appId})`, `已创建(${result.feishu.appId})`)
1541
+ : result.feishu.status === "existing"
1542
+ ? cliText(`existing (${result.feishu.appId})`, `已配置(${result.feishu.appId})`)
1543
+ : cliText("skipped", "已跳过");
1544
+ const configuration = result.feishu.configuration;
1545
+ const configurationStatus = configuration?.status === "partial"
1546
+ ? cliText("partially configured", "部分配置未完成")
1547
+ : configuration
1548
+ ? cliText("ready", "已就绪")
1549
+ : cliText("not checked", "未检查");
1550
+ ui.note(cliText("Initialization summary", "初始化摘要"), [
1551
+ `${cliText("Configured Agents", "已配置 Agent")}: ${result.configuredAgents.join(", ")}`,
1552
+ `${cliText("Default Agent", "默认 Agent")}: ${result.defaultAgent.name}`,
1553
+ `${cliText("Group messages", "群消息")}: ${result.groupMessages.mode === "all"
1554
+ ? cliText("receive all", "接收所有消息")
1555
+ : cliText("require @ mention", "需要 @ 机器人")}`,
1556
+ `${cliText("Lark bot", "飞书机器人")}: ${feishuStatus}`,
1557
+ `${cliText("Lark configuration", "飞书配置")}: ${configurationStatus}`,
1558
+ `${cliText("AgentBot Skill", "AgentBot Skill")}: ${result.skill.status.targetPath}`,
1559
+ `${cliText("Config file", "配置文件")}: ${result.config.path}`,
1560
+ ].join("\n"));
1561
+ if (configuration?.status === "partial") {
1562
+ ui.warn(cliText("Initialization continued with some unavailable Lark features.", "部分飞书功能尚不可用,但初始化已继续。"));
1563
+ for (const feature of feishuAffectedFeatures(configuration.remaining)) {
1564
+ ui.warn(`${cliText("Affected feature", "受影响功能")}: ${feature}`);
1565
+ }
1566
+ }
1567
+ return;
1568
+ }
1463
1569
  process.stdout.write(cliText("\nAgent Bot initialization completed.\n", "\nAgent Bot 初始化完成。\n"));
1464
1570
  process.stdout.write(`${cliText("Configured Agents: ", "已配置 Agent:")}${result.configuredAgents.join(", ")}\n`);
1465
1571
  process.stdout.write(`${cliText("Default Agent: ", "默认 Agent:")}${result.defaultAgent.name}\n`);
@@ -1499,28 +1605,55 @@ function printInitializationResult(result) {
1499
1605
  }
1500
1606
  process.stdout.write(`${cliText("Config file: ", "配置文件:")}${result.config.path}\n`);
1501
1607
  }
1502
- function printInitializationServerResult(result) {
1608
+ function printInitializationServerResult(result, ui) {
1503
1609
  if (result.status === "skipped") {
1504
- process.stdout.write(cliText("Agent Bot server: skipped (Lark is not configured; Console mode is available)\n", "Agent Bot 服务:已跳过(未配置飞书;可使用 Console 模式)\n"));
1610
+ const message = cliText("Agent Bot server: skipped (Lark is not configured; Console mode is available)", "Agent Bot 服务:已跳过(未配置飞书;可使用 Console 模式)");
1611
+ if (ui.interactive)
1612
+ ui.warn(message);
1613
+ else
1614
+ process.stdout.write(`${message}\n`);
1505
1615
  return;
1506
1616
  }
1507
1617
  if (result.status === "restart-scheduled") {
1508
- process.stdout.write(cliText("Agent Bot server: safe restart scheduled to load the current version.\n", "Agent Bot 服务:已安排安全重启,以加载当前版本。\n"));
1618
+ const message = cliText("Agent Bot server: safe restart scheduled to load the current version.", "Agent Bot 服务:已安排安全重启,以加载当前版本。");
1619
+ if (ui.interactive)
1620
+ ui.success(message);
1621
+ else
1622
+ process.stdout.write(`${message}\n`);
1509
1623
  return;
1510
1624
  }
1511
- printServerStartResult(result);
1625
+ if (ui.interactive) {
1626
+ ui.success(result.status === "already-running"
1627
+ ? cliText("Agent Bot server is already running.", "Agent Bot 服务已在运行。")
1628
+ : cliText("Agent Bot server started.", "Agent Bot 服务已启动。"));
1629
+ }
1630
+ else {
1631
+ printServerStartResult(result);
1632
+ }
1512
1633
  }
1513
- function printInitializationWelcomeResult(result) {
1634
+ function printInitializationWelcomeResult(result, ui) {
1514
1635
  if (result.status === "sent") {
1515
- process.stdout.write(cliText("Welcome card: sent to your Lark private chat.\n", "欢迎卡片:已发送到你的飞书私聊。\n"));
1636
+ const message = cliText("Welcome card: sent to your Lark private chat.", "欢迎卡片:已发送到你的飞书私聊。");
1637
+ if (ui.interactive)
1638
+ ui.success(message);
1639
+ else
1640
+ process.stdout.write(`${message}\n`);
1516
1641
  return;
1517
1642
  }
1518
1643
  if (result.status === "failed") {
1519
- process.stderr.write(cliText(`Warning: The private welcome card could not be sent: ${result.message}\n`, `警告:无法发送私聊欢迎卡片:${result.message}\n`));
1644
+ const message = cliText(`The private welcome card could not be sent: ${result.message}`, `无法发送私聊欢迎卡片:${result.message}`);
1645
+ if (ui.interactive)
1646
+ ui.warn(message);
1647
+ else
1648
+ process.stderr.write(`${cliText("Warning: ", "警告:")}${message}\n`);
1520
1649
  return;
1521
1650
  }
1522
1651
  if (result.reason === "missing-user-open-id") {
1523
- process.stderr.write(cliText("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", "警告:尚未获得 FEISHU_USER_OPEN_ID,无法发送私聊欢迎卡片。请先私聊机器人,再次运行 agentbot init。\n"));
1652
+ const message = cliText("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.", "尚未获得 FEISHU_USER_OPEN_ID,无法发送私聊欢迎卡片。请先私聊机器人,再次运行 agentbot init。");
1653
+ if (ui.interactive)
1654
+ ui.warn(message);
1655
+ else
1656
+ process.stderr.write(`${cliText("Warning: ", "警告:")}${message}\n`);
1524
1657
  }
1525
1658
  }
1526
1659
  function printServerStartResult(result) {
@@ -1528,24 +1661,43 @@ function printServerStartResult(result) {
1528
1661
  ? cliText("Agent Bot server is already running.\n", "Agent Bot 服务已在运行。\n")
1529
1662
  : cliText("Agent Bot server started.\n", "Agent Bot 服务已启动。\n"));
1530
1663
  }
1531
- function printFeishuVerification(challenge) {
1664
+ function printFeishuVerification(challenge, ui) {
1665
+ if (ui.interactive) {
1666
+ ui.note(cliText("Create Lark bot", "创建飞书机器人"), cliText("Open the link below and confirm creation. AgentBot will save the App ID and App Secret after Lark returns them.", "请打开下方链接并确认创建。飞书返回 App ID 和 App Secret 后,AgentBot 会自动保存。"));
1667
+ }
1532
1668
  printVerificationLink({
1533
1669
  verificationUrl: challenge.verificationUrl,
1534
1670
  });
1535
- process.stderr.write(cliText(`The link expires in about ${Math.ceil(challenge.expiresIn / 60)} minutes. Waiting for confirmation...\n`, `链接将在约 ${Math.ceil(challenge.expiresIn / 60)} 分钟后过期,正在等待确认...\n`));
1671
+ const waiting = cliText(`The link expires in about ${Math.ceil(challenge.expiresIn / 60)} minutes. Waiting for confirmation...`, `链接将在约 ${Math.ceil(challenge.expiresIn / 60)} 分钟后过期,正在等待确认...`);
1672
+ if (ui.interactive)
1673
+ ui.info(waiting);
1674
+ else
1675
+ process.stderr.write(`${waiting}\n`);
1536
1676
  }
1537
- function printFeishuConfigurationVerification(challenge) {
1538
- process.stderr.write(challenge.kind === "manual_scope"
1539
- ? cliText("\nThe following Lark permission must be added manually in Developer Console:\n", "\n必须在飞书开发者后台手动添加以下权限:\n")
1540
- : cliText("\nThe Lark app is missing the following configuration:\n", "\n飞书应用缺少以下配置:\n"));
1541
- if (challenge.missing.scopes.length > 0) {
1542
- process.stderr.write(`${cliText("Scopes: ", "权限:")}${challenge.missing.scopes.join(", ")}\n`);
1543
- }
1544
- if (challenge.missing.events.length > 0) {
1545
- process.stderr.write(`${cliText("Events: ", "事件:")}${challenge.missing.events.join(", ")}\n`);
1677
+ function printFeishuConfigurationVerification(challenge, ui) {
1678
+ const title = challenge.kind === "manual_scope"
1679
+ ? cliText("Manual Lark permission", "手动开通飞书权限")
1680
+ : cliText("Lark authorization required", "需要飞书授权");
1681
+ const details = [
1682
+ ...(challenge.missing.scopes.length > 0
1683
+ ? [`${cliText("Scopes", "权限")}: ${challenge.missing.scopes.join(", ")}`]
1684
+ : []),
1685
+ ...(challenge.missing.events.length > 0
1686
+ ? [`${cliText("Events", "事件")}: ${challenge.missing.events.join(", ")}`]
1687
+ : []),
1688
+ ...(challenge.missing.callbacks.length > 0
1689
+ ? [`${cliText("Callbacks", "回调")}: ${challenge.missing.callbacks.join(", ")}`]
1690
+ : []),
1691
+ formatFeishuConfigurationFeatureIntroduction(challenge.missing).trim(),
1692
+ ].filter(Boolean).join("\n");
1693
+ if (ui.interactive) {
1694
+ ui.note(title, details);
1546
1695
  }
1547
- if (challenge.missing.callbacks.length > 0) {
1548
- process.stderr.write(`${cliText("Callbacks: ", "回调:")}${challenge.missing.callbacks.join(", ")}\n`);
1696
+ else {
1697
+ process.stderr.write(challenge.kind === "manual_scope"
1698
+ ? cliText("\nThe following Lark permission must be added manually in Developer Console:\n", "\n必须在飞书开发者后台手动添加以下权限:\n")
1699
+ : cliText("\nThe Lark app is missing the following configuration:\n", "\n飞书应用缺少以下配置:\n"));
1700
+ process.stderr.write(`${details}\n`);
1549
1701
  }
1550
1702
  printVerificationLink({
1551
1703
  verificationUrl: challenge.verificationUrl,
@@ -1554,17 +1706,26 @@ function printFeishuConfigurationVerification(challenge) {
1554
1706
  : undefined,
1555
1707
  });
1556
1708
  if (challenge.kind === "manual_scope") {
1557
- process.stderr.write(cliText("Add the filtered permission, publish the app version, and complete tenant approval if required.\n", "请添加已筛选的权限、发布应用版本,并在需要时完成租户管理员审批。\n"));
1709
+ const guidance = cliText("Add the filtered permission, publish the app version, and complete tenant approval if required.", "请添加已筛选的权限、发布应用版本,并在需要时完成租户管理员审批。");
1710
+ if (ui.interactive)
1711
+ ui.info(guidance);
1712
+ else
1713
+ process.stderr.write(`${guidance}\n`);
1558
1714
  }
1715
+ let waiting;
1559
1716
  if (challenge.kind === "manual_scope") {
1560
- process.stderr.write(cliText("Waiting up to 5 minutes for this permission to appear in the published app version...\n", "将等待最多 5 分钟,直到该权限出现在已发布的应用版本中...\n"));
1717
+ waiting = cliText("Waiting up to 5 minutes for this permission to appear in the published app version...", "将等待最多 5 分钟,直到该权限出现在已发布的应用版本中...");
1561
1718
  }
1562
1719
  else if (challenge.blocking) {
1563
- process.stderr.write(cliText("Waiting for the core scopes and message event to become active...\n", "正在等待核心权限和消息事件生效...\n"));
1720
+ waiting = cliText("Waiting for the core scopes and message event to become active...", "正在等待核心权限和消息事件生效...");
1564
1721
  }
1565
1722
  else {
1566
- process.stderr.write(cliText("Waiting up to 5 minutes for these optional items to become active...\n", "将等待最多 5 分钟,直到这些可选配置生效...\n"));
1723
+ waiting = cliText("Waiting up to 5 minutes for these optional items to become active...", "将等待最多 5 分钟,直到这些可选配置生效...");
1567
1724
  }
1725
+ if (ui.interactive)
1726
+ ui.info(waiting);
1727
+ else
1728
+ process.stderr.write(`${waiting}\n`);
1568
1729
  }
1569
1730
  function printFeishuConfigurationWarnings(missing) {
1570
1731
  process.stdout.write(cliText("Warning: some Lark configuration is not active, so some features may be unavailable.\n", "警告:部分飞书配置尚未生效,某些功能可能不可用。\n"));
@@ -1577,41 +1738,10 @@ function printFeishuConfigurationWarnings(missing) {
1577
1738
  if (missing.callbacks.length > 0) {
1578
1739
  process.stdout.write(`${cliText("Inactive callbacks: ", "未生效回调:")}${missing.callbacks.join(", ")}\n`);
1579
1740
  }
1580
- for (const feature of feishuFeatureWarnings(missing)) {
1741
+ for (const feature of feishuAffectedFeatures(missing)) {
1581
1742
  process.stdout.write(`${cliText("Affected feature: ", "受影响功能:")}${feature}\n`);
1582
1743
  }
1583
1744
  }
1584
- function feishuFeatureWarnings(missing) {
1585
- const scopes = new Set(missing.scopes);
1586
- const events = new Set(missing.events);
1587
- const callbacks = new Set(missing.callbacks);
1588
- const warnings = [];
1589
- if (scopes.has("im:message.group_msg")) {
1590
- warnings.push(cliText("responding to ordinary group messages that do not mention the bot", "响应未 @ 机器人的普通群消息"));
1591
- }
1592
- if (scopes.has("im:chat:create")) {
1593
- warnings.push(cliText("creating Lark groups with /newgroup and /forkgroup", "使用 /newgroup 和 /forkgroup 创建飞书群"));
1594
- }
1595
- if (scopes.has("im:chat:read") || events.has("im.chat.updated_v1")) {
1596
- warnings.push(cliText("synchronizing Agent Bot task titles after Lark group renames", "飞书群重命名后同步 Agent Bot 任务标题"));
1597
- }
1598
- if (scopes.has("im:message.reactions:write_only")) {
1599
- warnings.push(cliText("showing message-processing status with reactions", "使用 Reaction 显示消息处理状态"));
1600
- }
1601
- if (scopes.has("im:message:readonly")) {
1602
- warnings.push(cliText("reading images from user messages", "读取用户消息中的图片"));
1603
- }
1604
- if (scopes.has("im:resource")) {
1605
- warnings.push(cliText("uploading images, sending local images, or setting group avatars", "上传图片、发送本地图片或设置群头像"));
1606
- }
1607
- if (scopes.has("im:message:update")) {
1608
- warnings.push(cliText("updating sent progress cards", "更新已发送的进度卡片"));
1609
- }
1610
- if (callbacks.has("card.action.trigger")) {
1611
- warnings.push(cliText("card buttons and interactive actions", "卡片按钮和交互操作"));
1612
- }
1613
- return warnings;
1614
- }
1615
1745
  function initializationStatusLabel(status) {
1616
1746
  if (status === "created")
1617
1747
  return cliText("created", "已创建");