@keyou007/agent-bot 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +22 -10
  3. package/README.zh.md +22 -10
  4. package/assets/agent-bot-logo.png +0 -0
  5. package/dist/acp/AcpSessionManager.js +78 -46
  6. package/dist/acp/AcpSessionManager.js.map +1 -1
  7. package/dist/cli/FeishuAppConfiguration.js +84 -22
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/FeishuAppRegistration.js +12 -9
  10. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  11. package/dist/cli/Initializer.js +7 -6
  12. package/dist/cli/Initializer.js.map +1 -1
  13. package/dist/cli/LocalControlClient.js +3 -2
  14. package/dist/cli/LocalControlClient.js.map +1 -1
  15. package/dist/cli/LocalControlServer.js +2 -1
  16. package/dist/cli/LocalControlServer.js.map +1 -1
  17. package/dist/cli/OptionalAuthorizationSkip.js +27 -6
  18. package/dist/cli/OptionalAuthorizationSkip.js.map +1 -1
  19. package/dist/cli/ServerStarter.js +12 -3
  20. package/dist/cli/ServerStarter.js.map +1 -1
  21. package/dist/cli/SkillRegistry.js +5 -4
  22. package/dist/cli/SkillRegistry.js.map +1 -1
  23. package/dist/cli/VerificationOutput.js +2 -1
  24. package/dist/cli/VerificationOutput.js.map +1 -1
  25. package/dist/cli/help.js +48 -1
  26. package/dist/cli/help.js.map +1 -1
  27. package/dist/cli/i18n.js +27 -0
  28. package/dist/cli/i18n.js.map +1 -0
  29. package/dist/cli/initOptions.js +5 -4
  30. package/dist/cli/initOptions.js.map +1 -1
  31. package/dist/cli/packageVersion.js +2 -1
  32. package/dist/cli/packageVersion.js.map +1 -1
  33. package/dist/cli/profile.js +4 -3
  34. package/dist/cli/profile.js.map +1 -1
  35. package/dist/cli/serverStatus.js +15 -14
  36. package/dist/cli/serverStatus.js.map +1 -1
  37. package/dist/cli/taskChatRoute.js +3 -2
  38. package/dist/cli/taskChatRoute.js.map +1 -1
  39. package/dist/cli.js +165 -107
  40. package/dist/cli.js.map +1 -1
  41. package/dist/codex/AppServerConnection.js +15 -1
  42. package/dist/codex/AppServerConnection.js.map +1 -1
  43. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  44. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  45. package/dist/codex/CodexProcessManager.js +1 -0
  46. package/dist/codex/CodexProcessManager.js.map +1 -1
  47. package/dist/codex/CodexRuntime.js +77 -2
  48. package/dist/codex/CodexRuntime.js.map +1 -1
  49. package/dist/commands/CommandRouter.js +108 -47
  50. package/dist/commands/CommandRouter.js.map +1 -1
  51. package/dist/console/ConsoleTurnPresenter.js +1 -1
  52. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  53. package/dist/feishu/CardRenderer.js +345 -11
  54. package/dist/feishu/CardRenderer.js.map +1 -1
  55. package/dist/feishu/FeishuMessageClient.js +51 -5
  56. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  57. package/dist/feishu/FeishuTurnPresenter.js +8 -3
  58. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  59. package/dist/index.js +18 -17
  60. package/dist/index.js.map +1 -1
  61. package/dist/proxy/ProxySessionController.js +595 -205
  62. package/dist/proxy/ProxySessionController.js.map +1 -1
  63. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  64. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  65. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  66. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  67. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  68. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  69. package/dist/startup/SessionMetadataHydrator.js +2 -2
  70. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  71. package/dist/startup/StartupNotifier.js +1 -0
  72. package/dist/startup/StartupNotifier.js.map +1 -1
  73. package/dist/state/StateStore.js +21 -15
  74. package/dist/state/StateStore.js.map +1 -1
  75. package/dist/supervision/SafeRestartScheduler.js +5 -0
  76. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  77. package/docs/technical-reference.md +27 -6
  78. package/docs/technical-reference.zh.md +27 -6
  79. package/npm-shrinkwrap.json +2 -2
  80. package/package.json +2 -1
  81. package/skills/agent-bot/SKILL.md +19 -9
  82. package/src/acp/AcpSessionManager.ts +73 -54
  83. package/src/cli/FeishuAppConfiguration.ts +130 -18
  84. package/src/cli/FeishuAppRegistration.ts +24 -9
  85. package/src/cli/Initializer.ts +25 -6
  86. package/src/cli/LocalControlClient.ts +9 -2
  87. package/src/cli/LocalControlServer.ts +2 -1
  88. package/src/cli/OptionalAuthorizationSkip.ts +51 -6
  89. package/src/cli/ServerStarter.ts +20 -3
  90. package/src/cli/SkillRegistry.ts +17 -4
  91. package/src/cli/VerificationOutput.ts +4 -1
  92. package/src/cli/help.ts +50 -1
  93. package/src/cli/i18n.ts +38 -0
  94. package/src/cli/initOptions.ts +17 -4
  95. package/src/cli/packageVersion.ts +5 -1
  96. package/src/cli/profile.ts +13 -3
  97. package/src/cli/serverStatus.ts +19 -14
  98. package/src/cli/taskChatRoute.ts +9 -2
  99. package/src/cli.ts +290 -103
  100. package/src/codex/AppServerConnection.ts +21 -1
  101. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  102. package/src/codex/CodexProcessManager.ts +1 -0
  103. package/src/codex/CodexRuntime.ts +91 -2
  104. package/src/commands/CommandRouter.ts +123 -46
  105. package/src/commands/commandTypes.ts +6 -11
  106. package/src/console/ConsoleTurnPresenter.ts +1 -1
  107. package/src/feishu/CardRenderer.ts +418 -11
  108. package/src/feishu/FeishuMessageClient.ts +66 -3
  109. package/src/feishu/FeishuTurnPresenter.ts +7 -2
  110. package/src/index.ts +21 -22
  111. package/src/proxy/ProxySessionController.ts +712 -208
  112. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  113. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  114. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  115. package/src/runtime/types.ts +18 -0
  116. package/src/startup/SessionMetadataHydrator.ts +2 -2
  117. package/src/startup/StartupNotifier.ts +1 -0
  118. package/src/state/StateStore.ts +29 -15
  119. package/src/supervision/SafeRestartScheduler.ts +5 -0
@@ -1,5 +1,6 @@
1
1
  import { gzipSync } from "node:zlib";
2
2
  import type { FeishuAppCredentials } from "./FeishuAppRegistration.js";
3
+ import { cliText } from "./i18n.js";
3
4
 
4
5
  const FEISHU_OPEN_BASE_URL = "https://open.feishu.cn";
5
6
  const TENANT_TOKEN_PATH = "/open-apis/auth/v3/tenant_access_token/internal";
@@ -71,6 +72,7 @@ const CORE_FEISHU_SCOPES = new Set([
71
72
  "im:message:send_as_bot",
72
73
  ]);
73
74
  const CORE_FEISHU_EVENTS = new Set<string>(["im.message.receive_v1"]);
75
+ const MANUAL_FEISHU_SCOPES = new Set(["im:message.group_msg"]);
74
76
 
75
77
  export interface FeishuAppConfiguration {
76
78
  scopes: string[];
@@ -85,6 +87,7 @@ export interface MissingFeishuAppConfiguration {
85
87
  }
86
88
 
87
89
  export interface FeishuConfigurationChallenge {
90
+ kind: "launcher" | "manual_scope";
88
91
  verificationUrl: string;
89
92
  missing: MissingFeishuAppConfiguration;
90
93
  blocking: boolean;
@@ -94,6 +97,7 @@ export interface EnsureFeishuAppConfigurationOptions {
94
97
  fetch?: typeof globalThis.fetch;
95
98
  sleep?: (milliseconds: number) => Promise<void>;
96
99
  signal?: AbortSignal;
100
+ manualPermissionSkipSignal?: AbortSignal;
97
101
  optionalSkipSignal?: AbortSignal;
98
102
  pollIntervalMs?: number;
99
103
  timeoutMs?: number;
@@ -157,15 +161,27 @@ export async function ensureFeishuAppConfiguration(
157
161
 
158
162
  let remainingMs = timeoutMs;
159
163
  let lastError: unknown;
164
+ let refreshImmediately = options.manualPermissionSkipSignal?.aborted === true;
160
165
  while (remainingMs > 0) {
161
166
  const waitMs = Math.min(pollIntervalMs, remainingMs);
162
- await sleepWithAbort(sleep, waitMs, options.signal);
163
- remainingMs -= waitMs;
167
+ if (!refreshImmediately) {
168
+ const waitResult = options.manualPermissionSkipSignal?.aborted
169
+ ? await sleepWithAbort(sleep, waitMs, options.signal).then(() => "elapsed" as const)
170
+ : await sleepWithOptionalSkip(
171
+ sleep,
172
+ waitMs,
173
+ options.signal,
174
+ options.manualPermissionSkipSignal,
175
+ );
176
+ if (waitResult === "elapsed") remainingMs -= waitMs;
177
+ }
178
+ refreshImmediately = false;
164
179
 
165
180
  try {
166
181
  configuration = await readFeishuAppConfiguration(credentials, fetchImpl, options.signal);
167
182
  const remaining = missingFeishuAppConfiguration(configuration);
168
- if (!hasMissingConfiguration(coreMissingFeishuAppConfiguration(remaining))) {
183
+ const unresolvedCore = coreMissingFeishuAppConfiguration(remaining);
184
+ if (!hasBlockingCoreConfiguration(unresolvedCore, options.manualPermissionSkipSignal)) {
169
185
  return completeOptionalFeishuConfiguration(
170
186
  credentials,
171
187
  configuration,
@@ -175,17 +191,40 @@ export async function ensureFeishuAppConfiguration(
175
191
  pollIntervalMs,
176
192
  optionalTimeoutMs,
177
193
  options,
194
+ skippedManualScopes(coreMissing, options.manualPermissionSkipSignal),
178
195
  );
179
196
  }
180
197
  lastError = undefined;
181
198
  } catch (error) {
182
199
  if (options.signal?.aborted) throw abortError(options.signal);
200
+ const knownRemaining = missingFeishuAppConfiguration(configuration);
201
+ if (!hasBlockingCoreConfiguration(
202
+ coreMissingFeishuAppConfiguration(knownRemaining),
203
+ options.manualPermissionSkipSignal,
204
+ )) {
205
+ return completeOptionalFeishuConfiguration(
206
+ credentials,
207
+ configuration,
208
+ missing,
209
+ fetchImpl,
210
+ sleep,
211
+ pollIntervalMs,
212
+ optionalTimeoutMs,
213
+ options,
214
+ skippedManualScopes(coreMissing, options.manualPermissionSkipSignal),
215
+ );
216
+ }
183
217
  lastError = error;
184
218
  }
185
219
  }
186
220
 
187
- const detail = lastError instanceof Error ? ` Last check failed: ${lastError.message}` : "";
188
- throw new Error(`Timed out waiting for the core Lark scopes and message event. Run agent-bot init again.${detail}`);
221
+ const detail = lastError instanceof Error
222
+ ? cliText(` Last check failed: ${lastError.message}`, ` 上次检查失败:${lastError.message}`)
223
+ : "";
224
+ throw new Error(cliText(
225
+ `Timed out waiting for the core Lark scopes and message event. Run agent-bot init again.${detail}`,
226
+ `等待飞书核心权限和消息事件超时。请重新运行 agent-bot init。${detail}`,
227
+ ));
189
228
  }
190
229
 
191
230
  async function requestMissingFeishuConfiguration(
@@ -194,8 +233,28 @@ async function requestMissingFeishuConfiguration(
194
233
  blocking: boolean,
195
234
  onVerification: EnsureFeishuAppConfigurationOptions["onVerification"],
196
235
  ): Promise<void> {
197
- const verificationUrl = buildFeishuConfigurationUrl(appId, missing);
198
- await onVerification?.({ verificationUrl, missing, blocking });
236
+ const launcherMissing: MissingFeishuAppConfiguration = {
237
+ scopes: missing.scopes.filter((scope) => !MANUAL_FEISHU_SCOPES.has(scope)),
238
+ events: missing.events,
239
+ callbacks: missing.callbacks,
240
+ };
241
+ if (hasMissingConfiguration(launcherMissing)) {
242
+ await onVerification?.({
243
+ kind: "launcher",
244
+ verificationUrl: buildFeishuConfigurationUrl(appId, launcherMissing),
245
+ missing: launcherMissing,
246
+ blocking,
247
+ });
248
+ }
249
+
250
+ for (const scope of missing.scopes.filter((value) => MANUAL_FEISHU_SCOPES.has(value))) {
251
+ await onVerification?.({
252
+ kind: "manual_scope",
253
+ verificationUrl: buildFeishuPermissionManagementUrl(appId, scope),
254
+ missing: { scopes: [scope], events: [], callbacks: [] },
255
+ blocking,
256
+ });
257
+ }
199
258
  }
200
259
 
201
260
  async function completeOptionalFeishuConfiguration(
@@ -207,13 +266,15 @@ async function completeOptionalFeishuConfiguration(
207
266
  pollIntervalMs: number,
208
267
  timeoutMs: number,
209
268
  options: EnsureFeishuAppConfigurationOptions,
269
+ ignoredScopes: ReadonlySet<string> = new Set(),
210
270
  ): Promise<EnsureFeishuAppConfigurationResult> {
211
271
  let configuration = initialConfiguration;
212
272
  let remaining = missingFeishuAppConfiguration(configuration);
213
- if (hasMissingConfiguration(remaining)) {
273
+ const requested = withoutIgnoredScopes(remaining, ignoredScopes);
274
+ if (hasMissingConfiguration(requested)) {
214
275
  await requestMissingFeishuConfiguration(
215
276
  credentials.appId,
216
- remaining,
277
+ requested,
217
278
  false,
218
279
  options.onVerification,
219
280
  );
@@ -227,6 +288,7 @@ async function completeOptionalFeishuConfiguration(
227
288
  timeoutMs,
228
289
  options.signal,
229
290
  options.optionalSkipSignal,
291
+ ignoredScopes,
230
292
  );
231
293
  configuration = completed.configuration;
232
294
  remaining = completed.remaining;
@@ -249,6 +311,7 @@ async function waitForOptionalFeishuConfiguration(
249
311
  timeoutMs: number,
250
312
  signal?: AbortSignal,
251
313
  optionalSkipSignal?: AbortSignal,
314
+ ignoredScopes: ReadonlySet<string> = new Set(),
252
315
  ): Promise<{
253
316
  configuration: FeishuAppConfiguration;
254
317
  remaining: MissingFeishuAppConfiguration;
@@ -256,7 +319,7 @@ async function waitForOptionalFeishuConfiguration(
256
319
  let configuration = initialConfiguration;
257
320
  let remaining = missingFeishuAppConfiguration(configuration);
258
321
  let remainingMs = timeoutMs;
259
- while (remainingMs > 0 && hasMissingConfiguration(remaining)) {
322
+ while (remainingMs > 0 && hasMissingConfiguration(withoutIgnoredScopes(remaining, ignoredScopes))) {
260
323
  if (optionalSkipSignal?.aborted) break;
261
324
  const waitMs = Math.min(pollIntervalMs, remainingMs);
262
325
  const waitResult = await sleepWithOptionalSkip(
@@ -346,6 +409,14 @@ export function buildFeishuConfigurationUrl(appId: string, missing: MissingFeish
346
409
  return url.toString();
347
410
  }
348
411
 
412
+ export function buildFeishuPermissionManagementUrl(appId: string, scope: string): string {
413
+ const url = new URL(`/app/${encodeURIComponent(appId)}/auth`, FEISHU_OPEN_BASE_URL);
414
+ url.searchParams.set("q", scope);
415
+ url.searchParams.set("op_from", "openapi");
416
+ url.searchParams.set("token_type", "tenant");
417
+ return url.toString();
418
+ }
419
+
349
420
  async function readFeishuAppConfiguration(
350
421
  credentials: FeishuAppCredentials,
351
422
  fetchImpl: typeof globalThis.fetch,
@@ -360,14 +431,14 @@ async function readFeishuAppConfiguration(
360
431
  `${FEISHU_OPEN_BASE_URL}${APPLICATION_PATH_PREFIX}/${appId}?lang=zh_cn`,
361
432
  { headers },
362
433
  signal,
363
- "Read Lark app configuration",
434
+ cliText("Read Lark app configuration", "读取飞书应用配置"),
364
435
  ),
365
436
  fetchJson(
366
437
  fetchImpl,
367
438
  `${FEISHU_OPEN_BASE_URL}${APPLICATION_PATH_PREFIX}/${appId}/app_versions?lang=zh_cn&page_size=2`,
368
439
  { headers },
369
440
  signal,
370
- "Read Lark app versions",
441
+ cliText("Read Lark app versions", "读取飞书应用版本"),
371
442
  ),
372
443
  ]);
373
444
 
@@ -415,10 +486,13 @@ async function getTenantAccessToken(
415
486
  }),
416
487
  },
417
488
  signal,
418
- "Get the Lark tenant_access_token",
489
+ cliText("Get the Lark tenant_access_token", "获取飞书 tenant_access_token"),
419
490
  );
420
491
  const token = readString(payload, "tenant_access_token");
421
- if (!token) throw new Error("The Lark tenant_access_token response is missing the access token.");
492
+ if (!token) throw new Error(cliText(
493
+ "The Lark tenant_access_token response is missing the access token.",
494
+ "飞书 tenant_access_token 响应中缺少访问令牌。",
495
+ ));
422
496
  return token;
423
497
  }
424
498
 
@@ -439,12 +513,18 @@ async function fetchJson(
439
513
  const response = await fetchImpl(url, { ...init, signal: controller.signal });
440
514
  const payload = (await response.json().catch(() => undefined)) as unknown;
441
515
  if (!isJsonObject(payload)) {
442
- throw new Error(`${operation} returned an invalid response (HTTP ${response.status}).`);
516
+ throw new Error(cliText(
517
+ `${operation} returned an invalid response (HTTP ${response.status}).`,
518
+ `${operation}返回了无效响应(HTTP ${response.status})。`,
519
+ ));
443
520
  }
444
521
  const code = readNumber(payload, "code");
445
522
  if (!response.ok || code !== 0) {
446
523
  throw new FeishuConfigurationApiError(
447
- `${operation} failed: ${readString(payload, "msg") || response.statusText || `HTTP ${response.status}`}`,
524
+ cliText(
525
+ `${operation} failed: ${readString(payload, "msg") || response.statusText || `HTTP ${response.status}`}`,
526
+ `${operation}失败:${readString(payload, "msg") || response.statusText || `HTTP ${response.status}`}`,
527
+ ),
448
528
  code,
449
529
  response.status,
450
530
  );
@@ -452,7 +532,10 @@ async function fetchJson(
452
532
  return payload;
453
533
  } catch (error) {
454
534
  if (parentSignal?.aborted) throw abortError(parentSignal);
455
- if (controller.signal.aborted) throw new Error(`${operation} timed out.`);
535
+ if (controller.signal.aborted) throw new Error(cliText(
536
+ `${operation} timed out.`,
537
+ `${operation}超时。`,
538
+ ));
456
539
  throw error;
457
540
  } finally {
458
541
  clearTimeout(timeout);
@@ -484,6 +567,33 @@ function hasMissingConfiguration(missing: MissingFeishuAppConfiguration): boolea
484
567
  return missing.scopes.length > 0 || missing.events.length > 0 || missing.callbacks.length > 0;
485
568
  }
486
569
 
570
+ function hasBlockingCoreConfiguration(
571
+ missing: MissingFeishuAppConfiguration,
572
+ manualPermissionSkipSignal?: AbortSignal,
573
+ ): boolean {
574
+ if (!manualPermissionSkipSignal?.aborted) return hasMissingConfiguration(missing);
575
+ return hasMissingConfiguration(withoutIgnoredScopes(missing, MANUAL_FEISHU_SCOPES));
576
+ }
577
+
578
+ function skippedManualScopes(
579
+ initialCoreMissing: MissingFeishuAppConfiguration,
580
+ manualPermissionSkipSignal?: AbortSignal,
581
+ ): ReadonlySet<string> {
582
+ if (!manualPermissionSkipSignal?.aborted) return new Set();
583
+ return new Set(initialCoreMissing.scopes.filter((scope) => MANUAL_FEISHU_SCOPES.has(scope)));
584
+ }
585
+
586
+ function withoutIgnoredScopes(
587
+ missing: MissingFeishuAppConfiguration,
588
+ ignoredScopes: ReadonlySet<string>,
589
+ ): MissingFeishuAppConfiguration {
590
+ return {
591
+ scopes: missing.scopes.filter((scope) => !ignoredScopes.has(scope)),
592
+ events: missing.events,
593
+ callbacks: missing.callbacks,
594
+ };
595
+ }
596
+
487
597
  function coreMissingFeishuAppConfiguration(missing: MissingFeishuAppConfiguration): MissingFeishuAppConfiguration {
488
598
  return {
489
599
  scopes: missing.scopes.filter((scope) => CORE_FEISHU_SCOPES.has(scope)),
@@ -554,7 +664,9 @@ function positiveInteger(value: number | undefined): number | undefined {
554
664
  }
555
665
 
556
666
  function abortError(signal: AbortSignal): Error {
557
- return signal.reason instanceof Error ? signal.reason : new Error("Lark app configuration was cancelled.");
667
+ return signal.reason instanceof Error
668
+ ? signal.reason
669
+ : new Error(cliText("Lark app configuration was cancelled.", "飞书应用配置已取消。"));
558
670
  }
559
671
 
560
672
  function delay(milliseconds: number): Promise<void> {
@@ -41,12 +41,15 @@ export async function registerFeishuApp(options: FeishuAppRegistrationOptions =
41
41
  },
42
42
  options.signal,
43
43
  );
44
- throwRegistrationError(begin, "Could not start Lark app creation");
44
+ throwRegistrationError(begin, cliText("Could not start Lark app creation", "无法开始创建飞书应用"));
45
45
 
46
46
  const deviceCode = readString(begin, "device_code");
47
47
  const userCode = readString(begin, "user_code");
48
48
  if (!deviceCode || !userCode) {
49
- throw new Error("The Lark app creation response is missing device_code or user_code.");
49
+ throw new Error(cliText(
50
+ "The Lark app creation response is missing device_code or user_code.",
51
+ "飞书应用创建响应中缺少 device_code 或 user_code。",
52
+ ));
50
53
  }
51
54
 
52
55
  const expiresIn =
@@ -113,16 +116,22 @@ export async function registerFeishuApp(options: FeishuAppRegistrationOptions =
113
116
  intervalSeconds = Math.min(intervalSeconds + 5, MAX_POLL_INTERVAL_SECONDS);
114
117
  continue;
115
118
  case "access_denied":
116
- throw new Error("Lark app creation was cancelled.");
119
+ throw new Error(cliText("Lark app creation was cancelled.", "飞书应用创建已取消。"));
117
120
  case "expired_token":
118
121
  case "invalid_grant":
119
- throw new Error("The Lark app creation link expired. Run agent-bot init again.");
122
+ throw new Error(cliText(
123
+ "The Lark app creation link expired. Run agent-bot init again.",
124
+ "飞书应用创建链接已过期,请重新运行 agent-bot init。",
125
+ ));
120
126
  default:
121
- throwRegistrationError(payload, "Lark app creation failed");
127
+ throwRegistrationError(payload, cliText("Lark app creation failed", "飞书应用创建失败"));
122
128
  }
123
129
  }
124
130
 
125
- throw new Error("Timed out waiting for Lark app creation. Run agent-bot init again.");
131
+ throw new Error(cliText(
132
+ "Timed out waiting for Lark app creation. Run agent-bot init again.",
133
+ "等待飞书应用创建超时,请重新运行 agent-bot init。",
134
+ ));
126
135
  }
127
136
 
128
137
  function buildVerificationUrl(userCode: string): string {
@@ -152,7 +161,10 @@ async function postRegistration(
152
161
  });
153
162
  const payload = (await response.json().catch(() => undefined)) as unknown;
154
163
  if (!isRecord(payload)) {
155
- throw new Error(`The Lark app registration endpoint returned an invalid response (HTTP ${response.status}).`);
164
+ throw new Error(cliText(
165
+ `The Lark app registration endpoint returned an invalid response (HTTP ${response.status}).`,
166
+ `飞书应用注册接口返回了无效响应(HTTP ${response.status})。`,
167
+ ));
156
168
  }
157
169
  return payload;
158
170
  } finally {
@@ -165,7 +177,7 @@ function throwRegistrationError(payload: RegistrationPayload, prefix: string): v
165
177
  const code = readString(payload, "error");
166
178
  if (!code) return;
167
179
  const description = readString(payload, "error_description") || code;
168
- throw new Error(`${prefix}: ${description}`);
180
+ throw new Error(`${prefix}${cliText(": ", ":")}${description}`);
169
181
  }
170
182
 
171
183
  function readString(value: RegistrationPayload, key: string): string {
@@ -189,7 +201,9 @@ function isRecord(value: unknown): value is RegistrationPayload {
189
201
  }
190
202
 
191
203
  function abortError(signal: AbortSignal): Error {
192
- return signal.reason instanceof Error ? signal.reason : new Error("Lark app creation was cancelled.");
204
+ return signal.reason instanceof Error
205
+ ? signal.reason
206
+ : new Error(cliText("Lark app creation was cancelled.", "飞书应用创建已取消。"));
193
207
  }
194
208
 
195
209
  function delay(milliseconds: number): Promise<void> {
@@ -218,3 +232,4 @@ async function sleepWithAbort(
218
232
  if (onAbort) signal.removeEventListener("abort", onAbort);
219
233
  }
220
234
  }
235
+ import { cliText } from "./i18n.js";
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { cliText } from "./i18n.js";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { parse as parseDotEnv } from "dotenv";
7
8
  import {
@@ -136,7 +137,10 @@ export function writeFeishuCredentials(envPath: string, credentials: FeishuCrede
136
137
  || persisted.appSecret !== credentials.appSecret
137
138
  || persisted.userOpenId !== credentials.userOpenId
138
139
  ) {
139
- throw new Error("Lark credential verification failed after writing the environment file.");
140
+ throw new Error(cliText(
141
+ "Lark credential verification failed after writing the environment file.",
142
+ "写入环境文件后,飞书凭据校验失败。",
143
+ ));
140
144
  }
141
145
  } finally {
142
146
  if (temporaryFile !== undefined) fs.closeSync(temporaryFile);
@@ -183,13 +187,19 @@ export function acquireInitializationLock(lockDirectory: string): Initialization
183
187
  if (created) fs.rmSync(lockPath, { force: true });
184
188
  if (!isFileExistsError(error)) throw error;
185
189
  if (!isStaleInitializationLock(lockPath)) {
186
- throw new Error(`Another agent-bot init process is running (lock file: ${lockPath}).`);
190
+ throw new Error(cliText(
191
+ `Another agent-bot init process is running (lock file: ${lockPath}).`,
192
+ `另一个 agent-bot init 进程正在运行(锁文件:${lockPath})。`,
193
+ ));
187
194
  }
188
195
  fs.rmSync(lockPath, { force: true });
189
196
  }
190
197
  }
191
198
 
192
- throw new Error(`Could not acquire the initialization lock: ${lockPath}`);
199
+ throw new Error(cliText(
200
+ `Could not acquire the initialization lock: ${lockPath}`,
201
+ `无法获取初始化锁:${lockPath}`,
202
+ ));
193
203
  }
194
204
 
195
205
  export function cleanupFeishuCredentialTemporaryFiles(envPath: string): number {
@@ -209,7 +219,10 @@ export function cleanupFeishuCredentialTemporaryFiles(envPath: string): number {
209
219
 
210
220
  function assertTemplate(templatePath: string): void {
211
221
  if (!fs.statSync(templatePath, { throwIfNoEntry: false })?.isFile()) {
212
- throw new Error(`Initialization template does not exist: ${templatePath}`);
222
+ throw new Error(cliText(
223
+ `Initialization template does not exist: ${templatePath}`,
224
+ `初始化模板不存在:${templatePath}`,
225
+ ));
213
226
  }
214
227
  }
215
228
 
@@ -243,7 +256,10 @@ function resetProfileContents(
243
256
  || path.dirname(path.resolve(configPath)) !== resolvedHome
244
257
  || path.dirname(path.resolve(envPath)) !== resolvedHome
245
258
  ) {
246
- throw new Error("--reset only supports a profile-owned config.yaml and .env.");
259
+ throw new Error(cliText(
260
+ "--reset only supports a profile-owned config.yaml and .env.",
261
+ "--reset 仅支持由 Profile 管理的 config.yaml 和 .env。",
262
+ ));
247
263
  }
248
264
  const targets = [
249
265
  { name: "config.yaml", sourcePath: configPath },
@@ -304,7 +320,10 @@ function upsertDotEnvValue(contents: string, key: string, value: string): string
304
320
 
305
321
  function assertDotEnvValue(key: string, value: string): void {
306
322
  if (!value.trim() || /[\r\n]/.test(value)) {
307
- throw new Error(`${key} is not a valid single-line environment variable value.`);
323
+ throw new Error(cliText(
324
+ `${key} is not a valid single-line environment variable value.`,
325
+ `${key} 不是有效的单行环境变量值。`,
326
+ ));
308
327
  }
309
328
  }
310
329
 
@@ -1,5 +1,6 @@
1
1
  import net from "node:net";
2
2
  import type { ControlRequest, ControlResponse } from "./controlProtocol.js";
3
+ import { cliText } from "./i18n.js";
3
4
 
4
5
  export async function sendControlRequest(
5
6
  endpoint: string,
@@ -17,7 +18,10 @@ export async function sendControlRequest(
17
18
  socket.destroy();
18
19
  operation();
19
20
  };
20
- const timer = setTimeout(() => finish(() => reject(new Error("Timed out connecting to the Agent Bot control endpoint."))), timeoutMs);
21
+ const timer = setTimeout(() => finish(() => reject(new Error(cliText(
22
+ "Timed out connecting to the Agent Bot control endpoint.",
23
+ "连接 Agent Bot 控制端点超时。",
24
+ )))), timeoutMs);
21
25
  socket.setEncoding("utf8");
22
26
  socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
23
27
  socket.on("data", (chunk: string) => {
@@ -33,7 +37,10 @@ export async function sendControlRequest(
33
37
  });
34
38
  socket.once("error", (error) => finish(() => reject(error)));
35
39
  socket.once("end", () => {
36
- if (!settled) finish(() => reject(new Error("The Agent Bot control endpoint returned no response.")));
40
+ if (!settled) finish(() => reject(new Error(cliText(
41
+ "The Agent Bot control endpoint returned no response.",
42
+ "Agent Bot 控制端点未返回响应。",
43
+ ))));
37
44
  });
38
45
  });
39
46
  }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import net from "node:net";
3
3
  import type { ControlRequest, ControlResponse } from "./controlProtocol.js";
4
+ import { cliText } from "./i18n.js";
4
5
 
5
6
  export type ControlRequestHandler = (request: ControlRequest) => Promise<ControlResponse>;
6
7
 
@@ -61,7 +62,7 @@ export class LocalControlServer {
61
62
  function parseRequest(line: string): ControlRequest {
62
63
  const value = JSON.parse(line) as unknown;
63
64
  if (!value || typeof value !== "object" || !("action" in value) || typeof value.action !== "string") {
64
- throw new Error("Invalid local control request.");
65
+ throw new Error(cliText("Invalid local control request.", "无效的本地控制请求。"));
65
66
  }
66
67
  return value as ControlRequest;
67
68
  }
@@ -1,4 +1,5 @@
1
1
  import { createInterface, type Interface } from "node:readline";
2
+ import { cliText } from "./i18n.js";
2
3
 
3
4
  export interface OptionalAuthorizationSkipListener {
4
5
  close(): void;
@@ -6,6 +7,45 @@ export interface OptionalAuthorizationSkipListener {
6
7
 
7
8
  export function listenForOptionalAuthorizationSkip(
8
9
  onSkip: () => void,
10
+ ): OptionalAuthorizationSkipListener {
11
+ return listenForAuthorizationSkip(onSkip, {
12
+ prompt: cliText(
13
+ "Skip optional authorization? Enter Y and press Enter to skip.\n",
14
+ "是否跳过可选授权?输入 Y 并按回车键跳过。\n",
15
+ ),
16
+ retry: cliText(
17
+ "Enter Y to skip optional authorization, or keep waiting.\n",
18
+ "输入 Y 跳过可选授权,或继续等待。\n",
19
+ ),
20
+ skipped: cliText(
21
+ "Optional authorization skipped. Initialization will continue.\n",
22
+ "已跳过可选授权,初始化将继续。\n",
23
+ ),
24
+ });
25
+ }
26
+
27
+ export function listenForManualPermissionSkip(
28
+ onSkip: () => void,
29
+ ): OptionalAuthorizationSkipListener {
30
+ return listenForAuthorizationSkip(onSkip, {
31
+ prompt: cliText(
32
+ "Skip waiting for this permission? Enter Y and press Enter to skip.\n",
33
+ "是否跳过等待此权限?输入 Y 并按回车键跳过。\n",
34
+ ),
35
+ retry: cliText(
36
+ "Enter Y to skip this permission, or keep waiting.\n",
37
+ "输入 Y 跳过此权限,或继续等待。\n",
38
+ ),
39
+ skipped: cliText(
40
+ "Permission wait skipped. Initialization will continue with limited group-message functionality.\n",
41
+ "已跳过权限等待,初始化将继续,但群消息功能受限。\n",
42
+ ),
43
+ });
44
+ }
45
+
46
+ function listenForAuthorizationSkip(
47
+ onSkip: () => void,
48
+ messages: { prompt: string; retry: string; skipped: string },
9
49
  ): OptionalAuthorizationSkipListener {
10
50
  if (!process.stdin.isTTY) return { close: () => undefined };
11
51
 
@@ -13,19 +53,24 @@ export function listenForOptionalAuthorizationSkip(
13
53
  input: process.stdin,
14
54
  output: process.stderr,
15
55
  });
56
+ let closed = false;
57
+ const close = (): void => {
58
+ if (closed) return;
59
+ closed = true;
60
+ closeReadline(readline, onLine);
61
+ };
16
62
  const onLine = (value: string): void => {
17
63
  if (!isOptionalAuthorizationSkip(value)) {
18
- process.stderr.write("Enter Y to skip optional authorization, or keep waiting.\n");
64
+ process.stderr.write(messages.retry);
19
65
  return;
20
66
  }
21
- process.stderr.write("Optional authorization skipped. Initialization will continue.\n");
67
+ process.stderr.write(messages.skipped);
68
+ close();
22
69
  onSkip();
23
70
  };
24
71
  readline.on("line", onLine);
25
- process.stderr.write("Skip optional authorization? Enter Y and press Enter to skip.\n");
26
- return {
27
- close: () => closeReadline(readline, onLine),
28
- };
72
+ process.stderr.write(messages.prompt);
73
+ return { close };
29
74
  }
30
75
 
31
76
  export function isOptionalAuthorizationSkip(value: string): boolean {
@@ -9,6 +9,7 @@ import {
9
9
  resolveSupervisorDiagnosticsPaths,
10
10
  } from "../supervision/SupervisorDiagnostics.js";
11
11
  import { controlEndpoint } from "./controlProtocol.js";
12
+ import { cliText } from "./i18n.js";
12
13
  import { isServerReachable, isServerRunning } from "./LocalControlClient.js";
13
14
 
14
15
  export interface ServerStartResult {
@@ -47,7 +48,17 @@ export async function startServer(
47
48
  config: AppConfig,
48
49
  dependencies: ServerStarterDependencies = defaultDependencies,
49
50
  ): Promise<ServerStartResult> {
50
- requireServerFeishuTransport(config.feishu);
51
+ try {
52
+ requireServerFeishuTransport(config.feishu);
53
+ } catch (error) {
54
+ if (error instanceof Error && /Lark bot is not configured/.test(error.message)) {
55
+ throw new Error(cliText(
56
+ "The Lark bot is not configured. Run agent-bot init first, or use agent-bot console for local-only testing.",
57
+ "尚未配置飞书机器人。请先运行 agent-bot init,或使用 agent-bot console 进行纯本地测试。",
58
+ ));
59
+ }
60
+ throw error;
61
+ }
51
62
  const endpoint = controlEndpoint(config.storage.sqlitePath);
52
63
 
53
64
  if (await dependencies.isRunning(endpoint)) {
@@ -56,7 +67,10 @@ export async function startServer(
56
67
  if (await dependencies.isReachable(endpoint)) {
57
68
  const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
58
69
  if (!running) {
59
- throw new Error("The Agent Bot server started but could not connect to Lark. Check the logs.");
70
+ throw new Error(cliText(
71
+ "The Agent Bot server started but could not connect to Lark. Check the logs.",
72
+ "Agent Bot 服务已启动,但无法连接飞书。请检查日志。",
73
+ ));
60
74
  }
61
75
  return { status: "started" };
62
76
  }
@@ -64,7 +78,10 @@ export async function startServer(
64
78
  dependencies.spawnSupervisor(config);
65
79
  const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
66
80
  if (!running) {
67
- throw new Error("The Supervisor started, but the server did not connect to Lark within 45 seconds. Check the logs.");
81
+ throw new Error(cliText(
82
+ "The Supervisor started, but the server did not connect to Lark within 45 seconds. Check the logs.",
83
+ "Supervisor 已启动,但服务未能在 45 秒内连接飞书。请检查日志。",
84
+ ));
68
85
  }
69
86
  return { status: "started" };
70
87
  }