@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.
- package/CHANGELOG.md +11 -0
- package/README.md +7 -1
- package/README.zh.md +7 -1
- package/config.example.yaml +0 -2
- package/dist/cli/Initializer.js +155 -11
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/LocalControlClient.js +14 -1
- package/dist/cli/LocalControlClient.js.map +1 -1
- package/dist/cli/help.js +91 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/packageVersion.js +10 -0
- package/dist/cli/packageVersion.js.map +1 -0
- package/dist/cli.js +77 -83
- package/dist/cli.js.map +1 -1
- package/dist/config/loadConfig.js +0 -2
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +79 -13
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/transport.js +4 -11
- package/dist/feishu/transport.js.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -1
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/supervisor.js +7 -4
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +19 -18
- package/docs/technical-reference.zh.md +19 -18
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/skills/agent-bot/SKILL.md +4 -1
- package/src/cli/Initializer.ts +164 -11
- package/src/cli/LocalControlClient.ts +12 -1
- package/src/cli/help.ts +99 -0
- package/src/cli/packageVersion.ts +14 -0
- package/src/cli.ts +84 -79
- package/src/config/loadConfig.ts +0 -2
- package/src/config/schema.ts +1 -1
- package/src/feishu/FeishuConnector.ts +89 -12
- package/src/feishu/transport.ts +7 -16
- package/src/index.ts +14 -3
- package/src/startup/startFeishu.ts +2 -0
- package/src/supervisor.ts +7 -4
package/src/cli/help.ts
ADDED
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
feishu = {
|
|
125
|
+
if (options.skipFeishu) {
|
|
126
|
+
return {
|
|
99
127
|
status: "skipped",
|
|
100
128
|
...(existing.appId ? { appId: existing.appId } : {}),
|
|
101
129
|
};
|
|
102
|
-
}
|
|
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
|
-
|
|
128
|
-
|
|
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) =>
|
|
151
|
+
onVerification: (challenge) => printFeishuVerification(challenge, options.json),
|
|
131
152
|
});
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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,
|
|
238
|
-
if (!running) throw new Error("已启动 Supervisor,但 server 未在
|
|
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(
|
|
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(
|
|
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
|
}
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -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
|
package/src/config/schema.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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;
|
package/src/feishu/transport.ts
CHANGED
|
@@ -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
|
-
"
|
|
5
|
+
"appId" | "appSecret"
|
|
8
6
|
>;
|
|
9
7
|
|
|
10
|
-
export function
|
|
11
|
-
if (config.
|
|
12
|
-
|
|
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 {
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
70
|
+
if (await isServerReachable(serverEndpoint)) {
|
|
68
71
|
writeSupervisorLog("duplicate_supervisor_stopped", { endpoint: serverEndpoint });
|
|
69
72
|
process.exit(0);
|
|
70
73
|
return;
|