@keyou007/agent-bot 0.1.0 → 0.1.2

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 (45) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +7 -1
  3. package/README.zh.md +7 -1
  4. package/config.example.yaml +0 -2
  5. package/dist/cli/Initializer.js +155 -11
  6. package/dist/cli/Initializer.js.map +1 -1
  7. package/dist/cli/LocalControlClient.js +14 -1
  8. package/dist/cli/LocalControlClient.js.map +1 -1
  9. package/dist/cli/help.js +91 -0
  10. package/dist/cli/help.js.map +1 -0
  11. package/dist/cli/packageVersion.js +10 -0
  12. package/dist/cli/packageVersion.js.map +1 -0
  13. package/dist/cli.js +77 -83
  14. package/dist/cli.js.map +1 -1
  15. package/dist/config/loadConfig.js +0 -2
  16. package/dist/config/loadConfig.js.map +1 -1
  17. package/dist/config/schema.js +1 -1
  18. package/dist/config/schema.js.map +1 -1
  19. package/dist/feishu/FeishuConnector.js +79 -13
  20. package/dist/feishu/FeishuConnector.js.map +1 -1
  21. package/dist/feishu/transport.js +4 -11
  22. package/dist/feishu/transport.js.map +1 -1
  23. package/dist/index.js +7 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/startup/startFeishu.js +2 -1
  26. package/dist/startup/startFeishu.js.map +1 -1
  27. package/dist/supervisor.js +7 -4
  28. package/dist/supervisor.js.map +1 -1
  29. package/docs/technical-reference.md +19 -18
  30. package/docs/technical-reference.zh.md +19 -18
  31. package/npm-shrinkwrap.json +2 -2
  32. package/package.json +3 -2
  33. package/skills/agent-bot/SKILL.md +4 -1
  34. package/src/cli/Initializer.ts +164 -11
  35. package/src/cli/LocalControlClient.ts +12 -1
  36. package/src/cli/help.ts +99 -0
  37. package/src/cli/packageVersion.ts +14 -0
  38. package/src/cli.ts +84 -79
  39. package/src/config/loadConfig.ts +0 -2
  40. package/src/config/schema.ts +1 -1
  41. package/src/feishu/FeishuConnector.ts +89 -12
  42. package/src/feishu/transport.ts +7 -16
  43. package/src/index.ts +14 -3
  44. package/src/startup/startFeishu.ts +2 -0
  45. package/src/supervisor.ts +7 -4
@@ -0,0 +1,99 @@
1
+ export type CliLanguage = "en" | "zh";
2
+
3
+ export function resolveCliLanguage(
4
+ env: NodeJS.ProcessEnv = process.env,
5
+ systemLocale = Intl.DateTimeFormat().resolvedOptions().locale,
6
+ ): CliLanguage {
7
+ const configuredLocale = [
8
+ env.LC_ALL,
9
+ env.LC_MESSAGES,
10
+ env.LANGUAGE,
11
+ env.LANG,
12
+ ].find((value) => value?.trim());
13
+ return isChineseLocale(configuredLocale ?? systemLocale) ? "zh" : "en";
14
+ }
15
+
16
+ export function renderCliHelp(version: string, language = resolveCliLanguage()): string {
17
+ return language === "zh" ? renderChineseHelp(version) : renderEnglishHelp(version);
18
+ }
19
+
20
+ function renderChineseHelp(version: string): string {
21
+ return `Agent Bot ${version}
22
+ 通过飞书/Lark 使用本机 Codex 与 ACP Agent。
23
+
24
+ 用法:
25
+ agent-bot [选项] <命令>
26
+
27
+ 常用命令:
28
+ init 初始化配置、数据目录和飞书应用
29
+ console 打开本地 Console
30
+ server start 在后台启动 Agent Bot
31
+ server status 查看服务和安全重启状态
32
+ server restart 安排安全重启
33
+ server stop 停止 Agent Bot
34
+ task list 列出任务
35
+ task status <task> 查看任务状态
36
+ task chat <task> 查看任务绑定的飞书会话
37
+ task prompt <task> <prompt> 向指定任务发送 Prompt
38
+ task stop <task> 停止指定任务
39
+ task title <task> <title> 修改任务标题
40
+ skills status 查看内置 Skill 状态
41
+ skills install 安装内置 Skill
42
+
43
+ 选项:
44
+ --config <path> 使用指定配置文件
45
+ --json 在支持的命令中输出 JSON
46
+ -h, --help 显示帮助
47
+ -v, --version 显示版本
48
+
49
+ 示例:
50
+ agent-bot init
51
+ agent-bot server start
52
+ agent-bot server status
53
+ agent-bot task list
54
+ agent-bot server restart --reason "升级 Agent Bot"
55
+ `;
56
+ }
57
+
58
+ function renderEnglishHelp(version: string): string {
59
+ return `Agent Bot ${version}
60
+ Use local Codex and ACP agents from Lark/Feishu.
61
+
62
+ Usage:
63
+ agent-bot [options] <command>
64
+
65
+ Common commands:
66
+ init Initialize config, data directories, and the Lark app
67
+ console Open the local console
68
+ server start Start Agent Bot in the background
69
+ server status Show server and safe-restart status
70
+ server restart Schedule a safe restart
71
+ server stop Stop Agent Bot
72
+ task list List tasks
73
+ task status <task> Show task status
74
+ task chat <task> Show the task's Lark chat binding
75
+ task prompt <task> <prompt> Send a Prompt to a task
76
+ task stop <task> Stop a task
77
+ task title <task> <title> Rename a task
78
+ skills status Show the built-in Skill status
79
+ skills install Install the built-in Skill
80
+
81
+ Options:
82
+ --config <path> Use a specific config file
83
+ --json Print JSON when supported
84
+ -h, --help Show help
85
+ -v, --version Show version
86
+
87
+ Examples:
88
+ agent-bot init
89
+ agent-bot server start
90
+ agent-bot server status
91
+ agent-bot task list
92
+ agent-bot server restart --reason "Update Agent Bot"
93
+ `;
94
+ }
95
+
96
+ function isChineseLocale(value: string): boolean {
97
+ const locale = value.trim().split(":")[0]?.split(/[.@]/)[0]?.replaceAll("_", "-").toLowerCase();
98
+ return locale === "zh" || Boolean(locale?.startsWith("zh-"));
99
+ }
@@ -0,0 +1,14 @@
1
+ import fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export function readPackageVersion(
5
+ packageJsonUrl = new URL("../../package.json", import.meta.url),
6
+ ): string {
7
+ const metadata = JSON.parse(
8
+ fs.readFileSync(fileURLToPath(packageJsonUrl), "utf8"),
9
+ ) as { version?: unknown };
10
+ if (typeof metadata.version !== "string" || !metadata.version.trim()) {
11
+ throw new Error("package.json 中缺少有效的 version。");
12
+ }
13
+ return metadata.version;
14
+ }
package/src/cli.ts CHANGED
@@ -3,15 +3,18 @@ import { spawn, spawnSync } from "node:child_process";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import qrcode from "qrcode-terminal";
5
5
  import { loadConfig } from "./config/loadConfig.js";
6
- import { sendControlRequest, isServerRunning } from "./cli/LocalControlClient.js";
6
+ import { sendControlRequest, isServerReachable, isServerRunning } from "./cli/LocalControlClient.js";
7
7
  import {
8
8
  controlEndpoint,
9
9
  type ControlResponse,
10
10
  type TaskStatusControlData,
11
11
  } from "./cli/controlProtocol.js";
12
12
  import {
13
+ acquireInitializationLock,
14
+ cleanupFeishuCredentialTemporaryFiles,
13
15
  initializeAgentBot,
14
16
  readFeishuCredentials,
17
+ shouldCreateFeishuApp,
15
18
  writeFeishuCredentials,
16
19
  type InitializationResult,
17
20
  type InitializationStatus,
@@ -26,7 +29,10 @@ import {
26
29
  type EnsureFeishuAppConfigurationResult,
27
30
  type FeishuConfigurationChallenge,
28
31
  } from "./cli/FeishuAppConfiguration.js";
32
+ import { renderCliHelp } from "./cli/help.js";
33
+ import { requireServerFeishuTransport } from "./feishu/transport.js";
29
34
  import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
35
+ import { readPackageVersion } from "./cli/packageVersion.js";
30
36
  import { taskChatRoute } from "./cli/taskChatRoute.js";
31
37
  import { StateStore, type SessionRecord } from "./state/StateStore.js";
32
38
 
@@ -45,6 +51,10 @@ async function main(input: string[]): Promise<void> {
45
51
  printHelp();
46
52
  return;
47
53
  }
54
+ if (command === "--version" || command === "-v") {
55
+ process.stdout.write(`${readPackageVersion()}\n`);
56
+ return;
57
+ }
48
58
  if (command === "init") {
49
59
  await initCommand(rest, parsed.configPath);
50
60
  return;
@@ -91,57 +101,73 @@ async function initCommand(input: string[], configPath?: string): Promise<void>
91
101
 
92
102
  const paths = initializeAgentBot({ configPath });
93
103
  if (!json) printInitializationPaths(paths);
104
+ const initializationLock = acquireInitializationLock(paths.home.path);
94
105
 
106
+ try {
107
+ cleanupFeishuCredentialTemporaryFiles(paths.env.path);
108
+ const feishu = await initializeFeishu(
109
+ paths,
110
+ { json, skipFeishu, reconfigureFeishu },
111
+ );
112
+ const result: InitCommandResult = { ...paths, feishu };
113
+ if (json) printJson(result);
114
+ else printInitializationResult(result);
115
+ } finally {
116
+ initializationLock.release();
117
+ }
118
+ }
119
+
120
+ async function initializeFeishu(
121
+ paths: InitializationResult,
122
+ options: { json: boolean; skipFeishu: boolean; reconfigureFeishu: boolean },
123
+ ): Promise<InitCommandResult["feishu"]> {
95
124
  const existing = readFeishuCredentials(paths.env.path);
96
- let feishu: InitCommandResult["feishu"];
97
- if (skipFeishu) {
98
- feishu = {
125
+ if (options.skipFeishu) {
126
+ return {
99
127
  status: "skipped",
100
128
  ...(existing.appId ? { appId: existing.appId } : {}),
101
129
  };
102
- } else {
103
- const controller = new AbortController();
104
- const onInterrupt = (): void => controller.abort(new Error("已取消初始化。"));
105
- process.once("SIGINT", onInterrupt);
106
- try {
107
- let credentials: FeishuAppCredentials;
108
- let status: Exclude<FeishuInitializationStatus, "skipped">;
109
- if (existing.status === "configured" && !reconfigureFeishu) {
110
- credentials = {
111
- appId: existing.appId!,
112
- appSecret: existing.appSecret!,
113
- };
114
- status = "existing";
115
- } else {
116
- if (existing.status === "incomplete" && !reconfigureFeishu) {
117
- throw new Error(`检测到不完整的飞书凭证:请补全 ${paths.env.path},或使用 --reconfigure-feishu 重新创建应用。`);
118
- }
119
- credentials = await registerFeishuApp({
120
- signal: controller.signal,
121
- onVerification: (challenge) => printFeishuVerification(challenge, json),
122
- });
123
- writeFeishuCredentials(paths.env.path, credentials);
124
- status = "created";
125
- }
130
+ }
126
131
 
127
- if (!json) process.stdout.write("\n正在检查飞书应用权限、事件和回调配置...\n");
128
- const configuration = await ensureFeishuAppConfiguration(credentials, {
132
+ const controller = new AbortController();
133
+ const onInterrupt = (): void => controller.abort(new Error("已取消初始化。"));
134
+ process.once("SIGINT", onInterrupt);
135
+ process.once("SIGTERM", onInterrupt);
136
+ try {
137
+ let credentials: FeishuAppCredentials;
138
+ let status: Exclude<FeishuInitializationStatus, "skipped">;
139
+ if (!shouldCreateFeishuApp(existing, options.reconfigureFeishu)) {
140
+ credentials = {
141
+ appId: existing.appId!,
142
+ appSecret: existing.appSecret!,
143
+ };
144
+ status = "existing";
145
+ } else {
146
+ if (existing.status === "incomplete" && !options.json) {
147
+ process.stdout.write("\n检测到未完整保存的飞书凭据,将重新创建机器人。\n");
148
+ }
149
+ credentials = await registerFeishuApp({
129
150
  signal: controller.signal,
130
- onVerification: (challenge) => printFeishuConfigurationVerification(challenge, json),
151
+ onVerification: (challenge) => printFeishuVerification(challenge, options.json),
131
152
  });
132
- feishu = {
133
- status,
134
- appId: credentials.appId,
135
- configuration,
136
- };
137
- } finally {
138
- process.removeListener("SIGINT", onInterrupt);
153
+ writeFeishuCredentials(paths.env.path, credentials);
154
+ status = "created";
139
155
  }
140
- }
141
156
 
142
- const result: InitCommandResult = { ...paths, feishu };
143
- if (json) printJson(result);
144
- else printInitializationResult(result);
157
+ if (!options.json) process.stdout.write("\n正在检查飞书应用权限、事件和回调配置...\n");
158
+ const configuration = await ensureFeishuAppConfiguration(credentials, {
159
+ signal: controller.signal,
160
+ onVerification: (challenge) => printFeishuConfigurationVerification(challenge, options.json),
161
+ });
162
+ return {
163
+ status,
164
+ appId: credentials.appId,
165
+ configuration,
166
+ };
167
+ } finally {
168
+ process.removeListener("SIGINT", onInterrupt);
169
+ process.removeListener("SIGTERM", onInterrupt);
170
+ }
145
171
  }
146
172
 
147
173
  function skillsCommand(input: string[]): void {
@@ -190,7 +216,7 @@ async function consoleCommand(input: string[]): Promise<void> {
190
216
  const config = loadConfig();
191
217
  const endpoint = controlEndpoint(config.storage.sqlitePath);
192
218
  const force = input.includes("--force");
193
- if (!force && await isServerRunning(endpoint)) {
219
+ if (!force && await isServerReachable(endpoint)) {
194
220
  throw new Error("agent-bot server 正在运行。为避免争用同一任务状态,请先停止 server,或明确使用 --force。");
195
221
  }
196
222
  const entry = fileURLToPath(new URL("./index.js", import.meta.url));
@@ -221,10 +247,17 @@ async function serverCommand(input: string[]): Promise<void> {
221
247
  return;
222
248
  }
223
249
  if (action === "start") {
250
+ requireServerFeishuTransport(config.feishu);
224
251
  if (await isServerRunning(endpoint)) {
225
252
  process.stdout.write("agent-bot server 已在运行。\n");
226
253
  return;
227
254
  }
255
+ if (await isServerReachable(endpoint)) {
256
+ const running = await waitForServer(endpoint, 45_000);
257
+ if (!running) throw new Error("agent-bot server 已启动,但未能连接飞书机器人。请检查日志。");
258
+ process.stdout.write("agent-bot server 已启动。\n");
259
+ return;
260
+ }
228
261
  const entry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
229
262
  const child = spawn(process.execPath, [entry], {
230
263
  cwd: process.cwd(),
@@ -234,8 +267,8 @@ async function serverCommand(input: string[]): Promise<void> {
234
267
  env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 agent-bot CLI 启动" },
235
268
  });
236
269
  child.unref();
237
- const running = await waitForServer(endpoint, 30_000);
238
- if (!running) throw new Error("已启动 Supervisor,但 server 未在 30 秒内就绪。请检查日志。");
270
+ const running = await waitForServer(endpoint, 45_000);
271
+ if (!running) throw new Error("已启动 Supervisor,但 server 未在 45 秒内连接飞书机器人。请检查日志。");
239
272
  process.stdout.write("agent-bot server 已启动。\n");
240
273
  return;
241
274
  }
@@ -416,7 +449,11 @@ function printServerStatus(data: unknown): void {
416
449
  const activity = value.activity && typeof value.activity === "object"
417
450
  ? value.activity as Record<string, unknown>
418
451
  : {};
419
- process.stdout.write("agent-bot server:运行中\n");
452
+ process.stdout.write(
453
+ value.ready === false
454
+ ? "agent-bot server:启动中(正在连接飞书机器人)\n"
455
+ : "agent-bot server:运行中\n",
456
+ );
420
457
  process.stdout.write(`PID:${value.pid ?? "-"}\n`);
421
458
  process.stdout.write(`启动时间:${value.startedAt ?? "-"}\n`);
422
459
  process.stdout.write(`Supervisor:${value.supervised ? "已启用" : "未启用"}\n`);
@@ -594,37 +631,5 @@ function printJson(value: unknown): void {
594
631
  }
595
632
 
596
633
  function printHelp(): void {
597
- process.stdout.write(`Agent Bot 命令行工具
598
-
599
- 用法:
600
- agent-bot [--config <path>] init [--json] [--skip-feishu | --reconfigure-feishu]
601
- agent-bot [--config <path>] console [--force]
602
- agent-bot [--config <path>] server status [--json]
603
- agent-bot [--config <path>] server start
604
- agent-bot [--config <path>] server stop
605
- agent-bot [--config <path>] server restart [--safe | --immediate] [--reason <text>]
606
- agent-bot [--config <path>] task list [--context <key>] [--status <status>] [--json]
607
- agent-bot [--config <path>] task chat <序号|任务ID> [--json]
608
- agent-bot [--config <path>] task status <序号|任务ID> [--json]
609
- agent-bot [--config <path>] task stop <序号|任务ID>
610
- agent-bot [--config <path>] task title <序号|任务ID> <新标题>
611
- agent-bot [--config <path>] task prompt <序号|任务ID> <prompt>
612
- agent-bot skills status [--json] [--target <skills目录>]
613
- agent-bot skills install|register [--json] [--target <skills目录>]
614
- agent-bot skills uninstall|unregister [--json] [--target <skills目录>]
615
- agent-bot skills path [--json] [--target <skills目录>]
616
-
617
- 说明:
618
- 默认用户目录是 ~/.agent-bot;配置为 ~/.agent-bot/config.yaml,环境变量为 ~/.agent-bot/.env。
619
- 可用 AGENT_BOT_HOME 修改用户目录,或用 --config 指向某个配置文件。
620
- init 创建用户目录和运行文件;创建或读取飞书凭证后会审计并补齐已发布的权限、事件和回调。
621
- 仅核心消息能力缺失会阻塞初始化;非核心配置缺失只提醒受影响功能。
622
- --skip-feishu 跳过飞书创建及配置审计并保留 Console 模式;--reconfigure-feishu 明确替换已有飞书凭证。
623
- server restart 默认执行安全重启;等待全部任务完成、结果投递完成且连续 15 秒无新消息。
624
- --immediate(或 --force)跳过空闲等待并立即重启 worker。
625
- task 序号来自 task list 当前排序;任务管理操作通过运行中 server 执行。
626
- task chat 从本地任务路由中读取飞书会话 ID;话题任务使用 --json 可同时查看 threadId。
627
- task prompt 不切换任务;机器人先在原会话显示 Prompt,再提交任务,响应继续发送到该会话。
628
- skills 默认注册到系统通用 ~/.agents/skills 目录;AGENT_BOT_SKILLS_DIR 可修改默认目录。
629
- `);
634
+ process.stdout.write(renderCliHelp(readPackageVersion()));
630
635
  }
@@ -7,10 +7,8 @@ import { defaultConfigPath, defaultDotEnvPath, resolveUserPath } from "./paths.j
7
7
 
8
8
  const ENV_PATTERN = /\$\{([A-Z0-9_]+)\}/gi;
9
9
  const DEFAULT_CONFIG = `feishu:
10
- transport: "auto"
11
10
  appId: "\${FEISHU_APP_ID}"
12
11
  appSecret: "\${FEISHU_APP_SECRET}"
13
- useConsoleWhenMissingCredentials: true
14
12
 
15
13
  console:
16
14
  enabled: true
@@ -21,7 +21,7 @@ export const appConfigSchema = z.object({
21
21
  transport: z.enum(["auto", "sdk", "console"]).default("auto"),
22
22
  appId: z.string().optional(),
23
23
  appSecret: z.string().optional(),
24
- useConsoleWhenMissingCredentials: z.boolean().default(true),
24
+ useConsoleWhenMissingCredentials: z.boolean().default(false),
25
25
  }),
26
26
  agents: z.record(z.string().min(1), agentConfigSchema).refine(
27
27
  (agents) => Object.keys(agents).length > 0,
@@ -3,11 +3,18 @@ import type { AppConfig } from "../config/schema.js";
3
3
  import { threadContextKey } from "./contextKey.js";
4
4
  import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
5
5
 
6
+ const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
7
+
8
+ interface FeishuConnectorOptions {
9
+ connectTimeoutMs?: number;
10
+ }
11
+
6
12
  export class FeishuConnector {
7
13
  constructor(
8
14
  private readonly config: AppConfig,
9
15
  private readonly handler: FeishuEventHandler,
10
16
  private readonly logger: Logger,
17
+ private readonly options: FeishuConnectorOptions = {},
11
18
  ) {}
12
19
 
13
20
  async start(): Promise<void> {
@@ -25,7 +32,6 @@ export class FeishuConnector {
25
32
 
26
33
  private async startFeishuWs(appId: string, appSecret: string): Promise<void> {
27
34
  const lark = (await import("@larksuiteoapi/node-sdk")) as Record<string, any>;
28
- const wsClient = new lark.WSClient({ appId, appSecret });
29
35
  const eventDispatcher = new lark.EventDispatcher({}).register({
30
36
  "im.message.receive_v1": async (data: unknown) => {
31
37
  const message = toIncomingMessage(data);
@@ -77,21 +83,94 @@ export class FeishuConnector {
77
83
  };
78
84
  },
79
85
  });
86
+ const connected = createConnectionWaiter(this.logger, this.options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);
87
+ try {
88
+ const wsClient = new lark.WSClient({
89
+ appId,
90
+ appSecret,
91
+ logger: connected.sdkLogger,
92
+ loggerLevel: lark.LoggerLevel.debug,
93
+ });
94
+ await wsClient.start({ eventDispatcher });
95
+ await connected.promise;
96
+ this.logger.info("Feishu WebSocket connector connected.");
97
+ } finally {
98
+ connected.dispose();
99
+ }
100
+ }
101
+ }
80
102
 
81
- wsClient.start({ eventDispatcher });
82
- this.logger.info("Feishu WebSocket connector started.");
103
+ function createConnectionWaiter(
104
+ logger: Logger,
105
+ timeoutMs: number,
106
+ ): {
107
+ promise: Promise<void>;
108
+ sdkLogger: {
109
+ error: (...messages: unknown[]) => void;
110
+ warn: (...messages: unknown[]) => void;
111
+ info: (...messages: unknown[]) => void;
112
+ debug: (...messages: unknown[]) => void;
113
+ trace: (...messages: unknown[]) => void;
114
+ };
115
+ dispose: () => void;
116
+ } {
117
+ let resolveConnected!: () => void;
118
+ let rejectConnected!: (error: Error) => void;
119
+ let lastError: string | undefined;
120
+ let settled = false;
121
+ const promise = new Promise<void>((resolve, reject) => {
122
+ resolveConnected = resolve;
123
+ rejectConnected = reject;
124
+ });
125
+ const timer = setTimeout(() => {
126
+ if (settled) return;
127
+ settled = true;
128
+ const detail = lastError ? ` Last SDK error: ${lastError}` : "";
129
+ rejectConnected(new Error(`Timed out after ${timeoutMs}ms waiting for the Feishu WebSocket connection.${detail}`));
130
+ }, timeoutMs);
131
+ const logValues = (messages: unknown[]): string => messages.map(formatSdkLogValue).join(" ");
132
+ const sdkLogger = {
133
+ error: (...messages: unknown[]) => {
134
+ lastError = logValues(messages);
135
+ logger.error({ component: "feishu-websocket", sdkMessage: lastError }, "Feishu SDK error.");
136
+ },
137
+ warn: (...messages: unknown[]) => {
138
+ logger.warn({ component: "feishu-websocket", sdkMessage: logValues(messages) }, "Feishu SDK warning.");
139
+ },
140
+ info: (...messages: unknown[]) => {
141
+ logger.debug({ component: "feishu-websocket", sdkMessage: logValues(messages) }, "Feishu SDK status.");
142
+ },
143
+ debug: (...messages: unknown[]) => {
144
+ if (!messages.some((message) => message === "ws connect success")) return;
145
+ if (settled) return;
146
+ settled = true;
147
+ clearTimeout(timer);
148
+ resolveConnected();
149
+ },
150
+ trace: () => undefined,
151
+ };
152
+ return {
153
+ promise,
154
+ sdkLogger,
155
+ dispose: () => clearTimeout(timer),
156
+ };
157
+ }
158
+
159
+ function formatSdkLogValue(value: unknown): string {
160
+ if (typeof value === "string") return value;
161
+ if (value instanceof Error) return value.message;
162
+ try {
163
+ return JSON.stringify(value);
164
+ } catch {
165
+ return String(value);
83
166
  }
84
167
  }
85
168
 
86
169
  function toChatUpdatedEvent(data: unknown): ChatUpdatedEvent | undefined {
87
170
  const event = getFeishuEvent(data);
88
171
  const chatId = typeof event?.chat_id === "string" ? event.chat_id : undefined;
89
- const beforeName = typeof event?.before_change?.name === "string"
90
- ? event.before_change.name
91
- : undefined;
92
- const afterName = typeof event?.after_change?.name === "string"
93
- ? event.after_change.name
94
- : undefined;
172
+ const beforeName = typeof event?.before_change?.name === "string" ? event.before_change.name : undefined;
173
+ const afterName = typeof event?.after_change?.name === "string" ? event.after_change.name : undefined;
95
174
  if (!chatId || !afterName || beforeName === afterName) return undefined;
96
175
  return {
97
176
  chatId,
@@ -118,9 +197,7 @@ function toIncomingMessage(data: unknown): IncomingMessage | undefined {
118
197
  const parentMessageId = typeof message.parent_id === "string" && message.parent_id ? message.parent_id : undefined;
119
198
  const threadContext = Boolean(chatId && threadId);
120
199
  const senderId =
121
- event.sender?.sender_id?.open_id ??
122
- event.sender?.sender_id?.user_id ??
123
- event.sender?.sender_id?.union_id;
200
+ event.sender?.sender_id?.open_id ?? event.sender?.sender_id?.user_id ?? event.sender?.sender_id?.union_id;
124
201
 
125
202
  if (!chatId && !senderId) {
126
203
  return undefined;
@@ -1,24 +1,15 @@
1
1
  import type { AppConfig } from "../config/schema.js";
2
2
 
3
- export type FeishuTransportMode = "sdk" | "console";
4
-
5
3
  type FeishuTransportConfig = Pick<
6
4
  AppConfig["feishu"],
7
- "transport" | "appId" | "appSecret" | "useConsoleWhenMissingCredentials"
5
+ "appId" | "appSecret"
8
6
  >;
9
7
 
10
- export function resolveFeishuTransport(config: FeishuTransportConfig): FeishuTransportMode {
11
- if (config.transport === "console") {
12
- return "console";
13
- }
14
-
15
- const hasCredentials = Boolean(config.appId && config.appSecret);
16
- if (config.transport === "sdk" || !config.useConsoleWhenMissingCredentials) {
17
- if (!hasCredentials) {
18
- throw new Error("Feishu appId/appSecret are required.");
19
- }
20
- return "sdk";
8
+ export function requireServerFeishuTransport(config: FeishuTransportConfig): "sdk" {
9
+ if (!config.appId || !config.appSecret) {
10
+ throw new Error(
11
+ "飞书机器人尚未配置。请先运行 agent-bot init 完成初始化;仅需本地调试时请使用 agent-bot console。",
12
+ );
21
13
  }
22
-
23
- return hasCredentials ? "sdk" : "console";
14
+ return "sdk";
24
15
  }
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ import { ConsoleFeishuClient } from "./feishu/ConsoleFeishuClient.js";
15
15
  import { FeishuConnector } from "./feishu/FeishuConnector.js";
16
16
  import { FeishuMessageClient } from "./feishu/FeishuMessageClient.js";
17
17
  import { FeishuTurnPresenter } from "./feishu/FeishuTurnPresenter.js";
18
- import { resolveFeishuTransport } from "./feishu/transport.js";
18
+ import { requireServerFeishuTransport } from "./feishu/transport.js";
19
19
  import { createLogger } from "./logging/logger.js";
20
20
  import { OutboundRouter, type OutboundRoute } from "./presentation/OutboundRouter.js";
21
21
  import { ProxySessionController } from "./proxy/ProxySessionController.js";
@@ -36,6 +36,7 @@ const startupReason = process.env.AGENT_BOT_RESTART_REASON?.trim()
36
36
  || (supervised ? "Supervisor 启动" : "直接启动");
37
37
  const consoleOnly = process.env.AGENT_BOT_CONSOLE_ONLY === "1";
38
38
  const config = loadConfig();
39
+ const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.feishu);
39
40
  const logger = createLogger(config);
40
41
  const store = new StateStore(config.storage.sqlitePath);
41
42
 
@@ -52,13 +53,13 @@ const codexProcessManager = new CodexProcessManager(
52
53
  const codexRuntime = new CodexRuntime(codexProcessManager, logger);
53
54
  const runtimes = new AgentRuntimeRegistry({ acp: acpRuntime, codex: codexRuntime });
54
55
 
55
- const transport = consoleOnly ? "console" : resolveFeishuTransport(config.feishu);
56
56
  const routes: OutboundRoute[] = [];
57
57
  let feishuConnector: FeishuConnector | undefined;
58
58
  let consoleConnector: ConsoleConnector | undefined;
59
59
  let startupNotifier: StartupNotifier | undefined;
60
60
  let safeRestartNotifier: SafeRestartNotifier | undefined;
61
61
  let controlServer: LocalControlServer | undefined;
62
+ let serverReady = false;
62
63
 
63
64
  const feishuOutbound = transport === "sdk" ? new FeishuMessageClient(config, logger) : undefined;
64
65
  if (feishuOutbound) {
@@ -123,10 +124,18 @@ const startControlServer = async (): Promise<void> => {
123
124
  };
124
125
 
125
126
  if (feishuConnector && startupNotifier) {
126
- await startFeishu(feishuConnector, startupNotifier, processStartedAt, startupReason, startControlServer);
127
+ await startFeishu(
128
+ feishuConnector,
129
+ startupNotifier,
130
+ processStartedAt,
131
+ startupReason,
132
+ startControlServer,
133
+ () => { serverReady = true; },
134
+ );
127
135
  } else {
128
136
  await startControlServer();
129
137
  await feishuConnector?.start();
138
+ serverReady = true;
130
139
  }
131
140
  consoleConnector?.start();
132
141
 
@@ -156,6 +165,8 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
156
165
  return {
157
166
  ok: true,
158
167
  data: {
168
+ ready: serverReady,
169
+ phase: serverReady ? "ready" : "connecting_feishu",
159
170
  pid: process.pid,
160
171
  startedAt: processStartedAt.toISOString(),
161
172
  supervised,
@@ -12,8 +12,10 @@ export async function startFeishu(
12
12
  startedAt: Date,
13
13
  restartReason: string,
14
14
  prepare?: () => Promise<void>,
15
+ onConnected?: () => void,
15
16
  ): Promise<void> {
16
17
  await prepare?.();
17
18
  await connector.start();
19
+ onConnected?.();
18
20
  await notifier.notify(startedAt, restartReason);
19
21
  }
package/src/supervisor.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
- import { isServerRunning } from "./cli/LocalControlClient.js";
3
+ import { isServerReachable } from "./cli/LocalControlClient.js";
4
4
  import { controlEndpoint } from "./cli/controlProtocol.js";
5
5
  import { loadConfig } from "./config/loadConfig.js";
6
+ import { requireServerFeishuTransport } from "./feishu/transport.js";
6
7
  import {
7
8
  crashRestartDelayMs,
8
9
  describeRestartReason,
@@ -13,7 +14,9 @@ import {
13
14
  } from "./supervision/restartPolicy.js";
14
15
 
15
16
  const childEntry = fileURLToPath(new URL("./index.js", import.meta.url));
16
- const sqlitePath = loadConfig().storage.sqlitePath;
17
+ const config = loadConfig();
18
+ requireServerFeishuTransport(config.feishu);
19
+ const sqlitePath = config.storage.sqlitePath;
17
20
  const serverEndpoint = controlEndpoint(sqlitePath);
18
21
  let child: ChildProcess | undefined;
19
22
  let restartTimer: NodeJS.Timeout | undefined;
@@ -23,7 +26,7 @@ let nextStartReason = process.env.AGENT_BOT_RESTART_REASON?.trim() || "Superviso
23
26
 
24
27
  async function startChild(): Promise<void> {
25
28
  if (stopping || child) return;
26
- if (await isServerRunning(serverEndpoint)) {
29
+ if (await isServerReachable(serverEndpoint)) {
27
30
  writeSupervisorLog("existing_server_detected", { endpoint: serverEndpoint });
28
31
  process.exit(0);
29
32
  return;
@@ -64,7 +67,7 @@ async function handleChildExit(
64
67
  process.exit(0);
65
68
  return;
66
69
  }
67
- if (await isServerRunning(serverEndpoint)) {
70
+ if (await isServerReachable(serverEndpoint)) {
68
71
  writeSupervisorLog("duplicate_supervisor_stopped", { endpoint: serverEndpoint });
69
72
  process.exit(0);
70
73
  return;