@keyou007/agent-bot 0.1.1 → 0.1.3
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/.env.example +3 -1
- package/CHANGELOG.md +15 -0
- package/README.md +23 -6
- package/README.zh.md +23 -6
- package/config.example.yaml +1 -2
- package/dist/cli/FeishuAppRegistration.js +12 -2
- package/dist/cli/FeishuAppRegistration.js.map +1 -1
- package/dist/cli/Initializer.js +164 -14
- 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/ServerStarter.js +69 -0
- package/dist/cli/ServerStarter.js.map +1 -0
- package/dist/cli/help.js +6 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/profile.js +40 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli.js +93 -89
- package/dist/cli.js.map +1 -1
- package/dist/config/loadConfig.js +7 -4
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/paths.js +1 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schema.js +2 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +21 -3
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +8 -11
- 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 +20 -8
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +18 -3
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +13 -2
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -1
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +23 -7
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +20 -2
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +209 -0
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -0
- package/dist/supervisor.js +54 -16
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +28 -14
- package/docs/technical-reference.zh.md +28 -14
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +24 -4
- package/src/cli/FeishuAppRegistration.ts +14 -1
- package/src/cli/Initializer.ts +174 -14
- package/src/cli/LocalControlClient.ts +12 -1
- package/src/cli/ServerStarter.ts +106 -0
- package/src/cli/help.ts +6 -2
- package/src/cli/profile.ts +59 -0
- package/src/cli.ts +114 -84
- package/src/config/loadConfig.ts +12 -4
- package/src/config/paths.ts +1 -0
- package/src/config/schema.ts +2 -1
- package/src/feishu/CardRenderer.ts +25 -3
- package/src/feishu/FeishuConnector.ts +8 -12
- package/src/feishu/transport.ts +7 -16
- package/src/index.ts +31 -8
- package/src/proxy/ProxySessionController.ts +18 -3
- package/src/startup/StartupNotifier.ts +21 -4
- package/src/startup/startFeishu.ts +2 -0
- package/src/supervision/SafeRestartNotifier.ts +22 -7
- package/src/supervision/SafeRestartScheduler.ts +27 -3
- package/src/supervision/SupervisorDiagnostics.ts +268 -0
- package/src/supervisor.ts +53 -15
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { loadConfig } from "../config/loadConfig.js";
|
|
4
|
+
import type { AppConfig } from "../config/schema.js";
|
|
5
|
+
import { requireServerFeishuTransport } from "../feishu/transport.js";
|
|
6
|
+
import {
|
|
7
|
+
nodeDiagnosticReportArguments,
|
|
8
|
+
prepareCrashReportDirectory,
|
|
9
|
+
resolveSupervisorDiagnosticsPaths,
|
|
10
|
+
} from "../supervision/SupervisorDiagnostics.js";
|
|
11
|
+
import { controlEndpoint } from "./controlProtocol.js";
|
|
12
|
+
import { isServerReachable, isServerRunning } from "./LocalControlClient.js";
|
|
13
|
+
|
|
14
|
+
export interface ServerStartResult {
|
|
15
|
+
status: "started" | "already-running";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type InitializationServerResult =
|
|
19
|
+
| ServerStartResult
|
|
20
|
+
| { status: "skipped"; reason: "feishu-skipped" };
|
|
21
|
+
|
|
22
|
+
export interface ServerStarterDependencies {
|
|
23
|
+
isRunning(endpoint: string): Promise<boolean>;
|
|
24
|
+
isReachable(endpoint: string): Promise<boolean>;
|
|
25
|
+
waitUntilRunning(endpoint: string, timeoutMs: number): Promise<boolean>;
|
|
26
|
+
spawnSupervisor(config: AppConfig): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SERVER_START_TIMEOUT_MS = 45_000;
|
|
30
|
+
|
|
31
|
+
interface InitializationServerDependencies {
|
|
32
|
+
loadConfig(configPath?: string): AppConfig;
|
|
33
|
+
startServer(config: AppConfig): Promise<ServerStartResult>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function startInitializedServer(
|
|
37
|
+
options: { skipFeishu: boolean; configPath?: string },
|
|
38
|
+
dependencies: InitializationServerDependencies = defaultInitializationDependencies,
|
|
39
|
+
): Promise<InitializationServerResult> {
|
|
40
|
+
if (options.skipFeishu) {
|
|
41
|
+
return { status: "skipped", reason: "feishu-skipped" };
|
|
42
|
+
}
|
|
43
|
+
return dependencies.startServer(dependencies.loadConfig(options.configPath));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function startServer(
|
|
47
|
+
config: AppConfig,
|
|
48
|
+
dependencies: ServerStarterDependencies = defaultDependencies,
|
|
49
|
+
): Promise<ServerStartResult> {
|
|
50
|
+
requireServerFeishuTransport(config.feishu);
|
|
51
|
+
const endpoint = controlEndpoint(config.storage.sqlitePath);
|
|
52
|
+
|
|
53
|
+
if (await dependencies.isRunning(endpoint)) {
|
|
54
|
+
return { status: "already-running" };
|
|
55
|
+
}
|
|
56
|
+
if (await dependencies.isReachable(endpoint)) {
|
|
57
|
+
const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
|
|
58
|
+
if (!running) {
|
|
59
|
+
throw new Error("agent-bot server 已启动,但未能连接飞书机器人。请检查日志。");
|
|
60
|
+
}
|
|
61
|
+
return { status: "started" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
dependencies.spawnSupervisor(config);
|
|
65
|
+
const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
|
|
66
|
+
if (!running) {
|
|
67
|
+
throw new Error("已启动 Supervisor,但 server 未在 45 秒内连接飞书机器人。请检查日志。");
|
|
68
|
+
}
|
|
69
|
+
return { status: "started" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const defaultDependencies: ServerStarterDependencies = {
|
|
73
|
+
isRunning: isServerRunning,
|
|
74
|
+
isReachable: isServerReachable,
|
|
75
|
+
waitUntilRunning: waitForServer,
|
|
76
|
+
spawnSupervisor: (config) => {
|
|
77
|
+
const entry = fileURLToPath(new URL("../supervisor.js", import.meta.url));
|
|
78
|
+
const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
|
|
79
|
+
prepareCrashReportDirectory(reportDirectory);
|
|
80
|
+
const child = spawn(process.execPath, [
|
|
81
|
+
...nodeDiagnosticReportArguments(reportDirectory),
|
|
82
|
+
entry,
|
|
83
|
+
], {
|
|
84
|
+
cwd: process.cwd(),
|
|
85
|
+
detached: true,
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
stdio: "ignore",
|
|
88
|
+
env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 agent-bot CLI 启动" },
|
|
89
|
+
});
|
|
90
|
+
child.unref();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const defaultInitializationDependencies: InitializationServerDependencies = {
|
|
95
|
+
loadConfig,
|
|
96
|
+
startServer: (config) => startServer(config),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
|
|
100
|
+
const deadline = Date.now() + timeoutMs;
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
if (await isServerRunning(endpoint)) return true;
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -25,7 +25,7 @@ function renderChineseHelp(version: string): string {
|
|
|
25
25
|
agent-bot [选项] <命令>
|
|
26
26
|
|
|
27
27
|
常用命令:
|
|
28
|
-
init
|
|
28
|
+
init 初始化配置和飞书应用,并启动服务
|
|
29
29
|
console 打开本地 Console
|
|
30
30
|
server start 在后台启动 Agent Bot
|
|
31
31
|
server status 查看服务和安全重启状态
|
|
@@ -41,6 +41,7 @@ function renderChineseHelp(version: string): string {
|
|
|
41
41
|
skills install 安装内置 Skill
|
|
42
42
|
|
|
43
43
|
选项:
|
|
44
|
+
--profile <directory> 使用指定目录中的独立 Profile
|
|
44
45
|
--config <path> 使用指定配置文件
|
|
45
46
|
--json 在支持的命令中输出 JSON
|
|
46
47
|
-h, --help 显示帮助
|
|
@@ -48,6 +49,7 @@ function renderChineseHelp(version: string): string {
|
|
|
48
49
|
|
|
49
50
|
示例:
|
|
50
51
|
agent-bot init
|
|
52
|
+
agent-bot --profile ~/.agent-bot-rescue init
|
|
51
53
|
agent-bot server start
|
|
52
54
|
agent-bot server status
|
|
53
55
|
agent-bot task list
|
|
@@ -63,7 +65,7 @@ Usage:
|
|
|
63
65
|
agent-bot [options] <command>
|
|
64
66
|
|
|
65
67
|
Common commands:
|
|
66
|
-
init Initialize
|
|
68
|
+
init Initialize the Lark app and start the service
|
|
67
69
|
console Open the local console
|
|
68
70
|
server start Start Agent Bot in the background
|
|
69
71
|
server status Show server and safe-restart status
|
|
@@ -79,6 +81,7 @@ Common commands:
|
|
|
79
81
|
skills install Install the built-in Skill
|
|
80
82
|
|
|
81
83
|
Options:
|
|
84
|
+
--profile <directory> Use an isolated profile directory
|
|
82
85
|
--config <path> Use a specific config file
|
|
83
86
|
--json Print JSON when supported
|
|
84
87
|
-h, --help Show help
|
|
@@ -86,6 +89,7 @@ Options:
|
|
|
86
89
|
|
|
87
90
|
Examples:
|
|
88
91
|
agent-bot init
|
|
92
|
+
agent-bot --profile ~/.agent-bot-rescue init
|
|
89
93
|
agent-bot server start
|
|
90
94
|
agent-bot server status
|
|
91
95
|
agent-bot task list
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
AGENT_BOT_EXPLICIT_PROFILE_ENV,
|
|
4
|
+
AGENT_BOT_HOME_ENV,
|
|
5
|
+
DEFAULT_CONFIG_FILE,
|
|
6
|
+
resolveUserPath,
|
|
7
|
+
} from "../config/paths.js";
|
|
8
|
+
|
|
9
|
+
export interface ParsedGlobalOptions {
|
|
10
|
+
args: string[];
|
|
11
|
+
configPath?: string;
|
|
12
|
+
profilePath?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AppliedProfile {
|
|
16
|
+
homePath: string;
|
|
17
|
+
configPath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseGlobalOptions(input: string[]): ParsedGlobalOptions {
|
|
21
|
+
const args = [...input];
|
|
22
|
+
const configPath = removeOption(args, "--config");
|
|
23
|
+
const profilePath = removeOption(args, "--profile");
|
|
24
|
+
if (configPath && profilePath) {
|
|
25
|
+
throw new Error("--profile 和 --config 不能同时使用。Profile 固定使用其目录下的 config.yaml。");
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
args,
|
|
29
|
+
...(configPath ? { configPath } : {}),
|
|
30
|
+
...(profilePath ? { profilePath } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function applyExplicitProfile(
|
|
35
|
+
profilePath: string,
|
|
36
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
37
|
+
baseDirectory = process.cwd(),
|
|
38
|
+
): AppliedProfile {
|
|
39
|
+
const homePath = resolveUserPath(profilePath, baseDirectory);
|
|
40
|
+
const configPath = path.join(homePath, DEFAULT_CONFIG_FILE);
|
|
41
|
+
env[AGENT_BOT_HOME_ENV] = homePath;
|
|
42
|
+
env.AGENT_BOT_CONFIG = configPath;
|
|
43
|
+
env[AGENT_BOT_EXPLICIT_PROFILE_ENV] = "1";
|
|
44
|
+
delete env.FEISHU_APP_ID;
|
|
45
|
+
delete env.FEISHU_APP_SECRET;
|
|
46
|
+
delete env.FEISHU_USER_OPEN_ID;
|
|
47
|
+
return { homePath, configPath };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function removeOption(args: string[], option: string): string | undefined {
|
|
51
|
+
const indexes = args.flatMap((value, index) => value === option ? [index] : []);
|
|
52
|
+
if (indexes.length > 1) throw new Error(`${option} 只能指定一次。`);
|
|
53
|
+
const index = indexes[0];
|
|
54
|
+
if (index === undefined) return undefined;
|
|
55
|
+
const value = args[index + 1];
|
|
56
|
+
if (!value || value.startsWith("--")) throw new Error(`${option} 需要目录或文件路径。`);
|
|
57
|
+
args.splice(index, 2);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { 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,
|
|
6
|
+
import { sendControlRequest, isServerReachable } 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,
|
|
@@ -29,8 +32,20 @@ import {
|
|
|
29
32
|
import { renderCliHelp } from "./cli/help.js";
|
|
30
33
|
import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
|
|
31
34
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
35
|
+
import { applyExplicitProfile, parseGlobalOptions } from "./cli/profile.js";
|
|
36
|
+
import {
|
|
37
|
+
startInitializedServer,
|
|
38
|
+
startServer,
|
|
39
|
+
type InitializationServerResult,
|
|
40
|
+
type ServerStartResult,
|
|
41
|
+
} from "./cli/ServerStarter.js";
|
|
32
42
|
import { taskChatRoute } from "./cli/taskChatRoute.js";
|
|
33
43
|
import { StateStore, type SessionRecord } from "./state/StateStore.js";
|
|
44
|
+
import {
|
|
45
|
+
nodeDiagnosticReportArguments,
|
|
46
|
+
prepareCrashReportDirectory,
|
|
47
|
+
resolveSupervisorDiagnosticsPaths,
|
|
48
|
+
} from "./supervision/SupervisorDiagnostics.js";
|
|
34
49
|
|
|
35
50
|
const args = process.argv.slice(2);
|
|
36
51
|
|
|
@@ -41,7 +56,8 @@ void main(args).catch((error: unknown) => {
|
|
|
41
56
|
|
|
42
57
|
async function main(input: string[]): Promise<void> {
|
|
43
58
|
const parsed = parseGlobalOptions(input);
|
|
44
|
-
if (parsed.
|
|
59
|
+
if (parsed.profilePath) applyExplicitProfile(parsed.profilePath);
|
|
60
|
+
else if (parsed.configPath) process.env.AGENT_BOT_CONFIG = parsed.configPath;
|
|
45
61
|
const [command, ...rest] = parsed.args;
|
|
46
62
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
47
63
|
printHelp();
|
|
@@ -82,6 +98,7 @@ interface InitCommandResult extends InitializationResult {
|
|
|
82
98
|
appId?: string;
|
|
83
99
|
configuration?: EnsureFeishuAppConfigurationResult;
|
|
84
100
|
};
|
|
101
|
+
server: InitializationServerResult;
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
async function initCommand(input: string[], configPath?: string): Promise<void> {
|
|
@@ -97,57 +114,80 @@ async function initCommand(input: string[], configPath?: string): Promise<void>
|
|
|
97
114
|
|
|
98
115
|
const paths = initializeAgentBot({ configPath });
|
|
99
116
|
if (!json) printInitializationPaths(paths);
|
|
117
|
+
const initializationLock = acquireInitializationLock(paths.home.path);
|
|
118
|
+
|
|
119
|
+
let initialized: Omit<InitCommandResult, "server">;
|
|
120
|
+
try {
|
|
121
|
+
cleanupFeishuCredentialTemporaryFiles(paths.env.path);
|
|
122
|
+
const feishu = await initializeFeishu(
|
|
123
|
+
paths,
|
|
124
|
+
{ json, skipFeishu, reconfigureFeishu },
|
|
125
|
+
);
|
|
126
|
+
initialized = { ...paths, feishu };
|
|
127
|
+
} finally {
|
|
128
|
+
initializationLock.release();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!json) printInitializationResult(initialized);
|
|
132
|
+
if (!json && !skipFeishu) process.stdout.write("\n正在启动 Agent Bot server...\n");
|
|
133
|
+
const server = await startInitializedServer({ skipFeishu, configPath });
|
|
134
|
+
const result: InitCommandResult = { ...initialized, server };
|
|
135
|
+
if (json) printJson(result);
|
|
136
|
+
else printInitializationServerResult(server);
|
|
137
|
+
}
|
|
100
138
|
|
|
139
|
+
async function initializeFeishu(
|
|
140
|
+
paths: InitializationResult,
|
|
141
|
+
options: { json: boolean; skipFeishu: boolean; reconfigureFeishu: boolean },
|
|
142
|
+
): Promise<InitCommandResult["feishu"]> {
|
|
101
143
|
const existing = readFeishuCredentials(paths.env.path);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
feishu = {
|
|
144
|
+
if (options.skipFeishu) {
|
|
145
|
+
return {
|
|
105
146
|
status: "skipped",
|
|
106
147
|
...(existing.appId ? { appId: existing.appId } : {}),
|
|
107
148
|
};
|
|
108
|
-
}
|
|
109
|
-
const controller = new AbortController();
|
|
110
|
-
const onInterrupt = (): void => controller.abort(new Error("已取消初始化。"));
|
|
111
|
-
process.once("SIGINT", onInterrupt);
|
|
112
|
-
try {
|
|
113
|
-
let credentials: FeishuAppCredentials;
|
|
114
|
-
let status: Exclude<FeishuInitializationStatus, "skipped">;
|
|
115
|
-
if (existing.status === "configured" && !reconfigureFeishu) {
|
|
116
|
-
credentials = {
|
|
117
|
-
appId: existing.appId!,
|
|
118
|
-
appSecret: existing.appSecret!,
|
|
119
|
-
};
|
|
120
|
-
status = "existing";
|
|
121
|
-
} else {
|
|
122
|
-
if (existing.status === "incomplete" && !reconfigureFeishu) {
|
|
123
|
-
throw new Error(`检测到不完整的飞书凭证:请补全 ${paths.env.path},或使用 --reconfigure-feishu 重新创建应用。`);
|
|
124
|
-
}
|
|
125
|
-
credentials = await registerFeishuApp({
|
|
126
|
-
signal: controller.signal,
|
|
127
|
-
onVerification: (challenge) => printFeishuVerification(challenge, json),
|
|
128
|
-
});
|
|
129
|
-
writeFeishuCredentials(paths.env.path, credentials);
|
|
130
|
-
status = "created";
|
|
131
|
-
}
|
|
149
|
+
}
|
|
132
150
|
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const onInterrupt = (): void => controller.abort(new Error("已取消初始化。"));
|
|
153
|
+
process.once("SIGINT", onInterrupt);
|
|
154
|
+
process.once("SIGTERM", onInterrupt);
|
|
155
|
+
try {
|
|
156
|
+
let credentials: FeishuAppCredentials;
|
|
157
|
+
let status: Exclude<FeishuInitializationStatus, "skipped">;
|
|
158
|
+
if (!shouldCreateFeishuApp(existing, options.reconfigureFeishu)) {
|
|
159
|
+
credentials = {
|
|
160
|
+
appId: existing.appId!,
|
|
161
|
+
appSecret: existing.appSecret!,
|
|
162
|
+
userOpenId: existing.userOpenId,
|
|
163
|
+
};
|
|
164
|
+
status = "existing";
|
|
165
|
+
} else {
|
|
166
|
+
if (existing.status === "incomplete" && !options.json) {
|
|
167
|
+
process.stdout.write("\n检测到未完整保存的飞书凭据,将重新创建机器人。\n");
|
|
168
|
+
}
|
|
169
|
+
credentials = await registerFeishuApp({
|
|
135
170
|
signal: controller.signal,
|
|
136
|
-
onVerification: (challenge) =>
|
|
171
|
+
onVerification: (challenge) => printFeishuVerification(challenge, options.json),
|
|
137
172
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
appId: credentials.appId,
|
|
141
|
-
configuration,
|
|
142
|
-
};
|
|
143
|
-
} finally {
|
|
144
|
-
process.removeListener("SIGINT", onInterrupt);
|
|
173
|
+
writeFeishuCredentials(paths.env.path, credentials);
|
|
174
|
+
status = "created";
|
|
145
175
|
}
|
|
146
|
-
}
|
|
147
176
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
if (!options.json) process.stdout.write("\n正在检查飞书应用权限、事件和回调配置...\n");
|
|
178
|
+
const configuration = await ensureFeishuAppConfiguration(credentials, {
|
|
179
|
+
signal: controller.signal,
|
|
180
|
+
onVerification: (challenge) => printFeishuConfigurationVerification(challenge, options.json),
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
status,
|
|
184
|
+
appId: credentials.appId,
|
|
185
|
+
configuration,
|
|
186
|
+
};
|
|
187
|
+
} finally {
|
|
188
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
189
|
+
process.removeListener("SIGTERM", onInterrupt);
|
|
190
|
+
}
|
|
151
191
|
}
|
|
152
192
|
|
|
153
193
|
function skillsCommand(input: string[]): void {
|
|
@@ -196,11 +236,16 @@ async function consoleCommand(input: string[]): Promise<void> {
|
|
|
196
236
|
const config = loadConfig();
|
|
197
237
|
const endpoint = controlEndpoint(config.storage.sqlitePath);
|
|
198
238
|
const force = input.includes("--force");
|
|
199
|
-
if (!force && await
|
|
239
|
+
if (!force && await isServerReachable(endpoint)) {
|
|
200
240
|
throw new Error("agent-bot server 正在运行。为避免争用同一任务状态,请先停止 server,或明确使用 --force。");
|
|
201
241
|
}
|
|
202
242
|
const entry = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
203
|
-
const
|
|
243
|
+
const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
|
|
244
|
+
prepareCrashReportDirectory(reportDirectory);
|
|
245
|
+
const result = spawnSync(process.execPath, [
|
|
246
|
+
...nodeDiagnosticReportArguments(reportDirectory),
|
|
247
|
+
entry,
|
|
248
|
+
], {
|
|
204
249
|
cwd: process.cwd(),
|
|
205
250
|
stdio: "inherit",
|
|
206
251
|
env: { ...process.env, AGENT_BOT_CONSOLE_ONLY: "1" },
|
|
@@ -227,22 +272,7 @@ async function serverCommand(input: string[]): Promise<void> {
|
|
|
227
272
|
return;
|
|
228
273
|
}
|
|
229
274
|
if (action === "start") {
|
|
230
|
-
|
|
231
|
-
process.stdout.write("agent-bot server 已在运行。\n");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const entry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
|
|
235
|
-
const child = spawn(process.execPath, [entry], {
|
|
236
|
-
cwd: process.cwd(),
|
|
237
|
-
detached: true,
|
|
238
|
-
windowsHide: true,
|
|
239
|
-
stdio: "ignore",
|
|
240
|
-
env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 agent-bot CLI 启动" },
|
|
241
|
-
});
|
|
242
|
-
child.unref();
|
|
243
|
-
const running = await waitForServer(endpoint, 30_000);
|
|
244
|
-
if (!running) throw new Error("已启动 Supervisor,但 server 未在 30 秒内就绪。请检查日志。");
|
|
245
|
-
process.stdout.write("agent-bot server 已启动。\n");
|
|
275
|
+
printServerStartResult(await startServer(config));
|
|
246
276
|
return;
|
|
247
277
|
}
|
|
248
278
|
if (action === "stop") {
|
|
@@ -347,16 +377,6 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
347
377
|
}
|
|
348
378
|
}
|
|
349
379
|
|
|
350
|
-
function parseGlobalOptions(input: string[]): { configPath?: string; args: string[] } {
|
|
351
|
-
const args = [...input];
|
|
352
|
-
const configIndex = args.indexOf("--config");
|
|
353
|
-
if (configIndex < 0) return { args };
|
|
354
|
-
const configPath = args[configIndex + 1];
|
|
355
|
-
if (!configPath) throw new Error("--config 需要配置文件路径。");
|
|
356
|
-
args.splice(configIndex, 2);
|
|
357
|
-
return { configPath, args };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
380
|
function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
|
|
361
381
|
const context = optionValue(args, "--context");
|
|
362
382
|
const status = optionValue(args, "--status");
|
|
@@ -422,7 +442,11 @@ function printServerStatus(data: unknown): void {
|
|
|
422
442
|
const activity = value.activity && typeof value.activity === "object"
|
|
423
443
|
? value.activity as Record<string, unknown>
|
|
424
444
|
: {};
|
|
425
|
-
process.stdout.write(
|
|
445
|
+
process.stdout.write(
|
|
446
|
+
value.ready === false
|
|
447
|
+
? "agent-bot server:启动中(正在连接飞书机器人)\n"
|
|
448
|
+
: "agent-bot server:运行中\n",
|
|
449
|
+
);
|
|
426
450
|
process.stdout.write(`PID:${value.pid ?? "-"}\n`);
|
|
427
451
|
process.stdout.write(`启动时间:${value.startedAt ?? "-"}\n`);
|
|
428
452
|
process.stdout.write(`Supervisor:${value.supervised ? "已启用" : "未启用"}\n`);
|
|
@@ -450,7 +474,7 @@ function printInitializationPaths(result: InitializationResult): void {
|
|
|
450
474
|
process.stdout.write(`日志目录:${result.logs.path}(${initializationStatusLabel(result.logs.status)})\n`);
|
|
451
475
|
}
|
|
452
476
|
|
|
453
|
-
function printInitializationResult(result: InitCommandResult): void {
|
|
477
|
+
function printInitializationResult(result: Omit<InitCommandResult, "server">): void {
|
|
454
478
|
process.stdout.write("\nAgent Bot 初始化完成。\n");
|
|
455
479
|
if (result.feishu.status === "created") {
|
|
456
480
|
process.stdout.write(`飞书应用:已创建并保存凭证(${result.feishu.appId})\n`);
|
|
@@ -480,7 +504,22 @@ function printInitializationResult(result: InitCommandResult): void {
|
|
|
480
504
|
process.stdout.write("飞书配置:权限、事件和回调均已就绪\n");
|
|
481
505
|
}
|
|
482
506
|
process.stdout.write(`配置文件:${result.config.path}\n`);
|
|
483
|
-
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function printInitializationServerResult(result: InitCommandResult["server"]): void {
|
|
510
|
+
if (result.status === "skipped") {
|
|
511
|
+
process.stdout.write("Agent Bot server:已跳过(未配置飞书,可使用 Console 模式)\n");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
printServerStartResult(result);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function printServerStartResult(result: ServerStartResult): void {
|
|
518
|
+
process.stdout.write(
|
|
519
|
+
result.status === "already-running"
|
|
520
|
+
? "agent-bot server 已在运行。\n"
|
|
521
|
+
: "agent-bot server 已启动。\n",
|
|
522
|
+
);
|
|
484
523
|
}
|
|
485
524
|
|
|
486
525
|
function printFeishuVerification(challenge: FeishuAppRegistrationChallenge, json: boolean): void {
|
|
@@ -586,15 +625,6 @@ function optionValue(args: string[], name: string): string | undefined {
|
|
|
586
625
|
return index >= 0 ? args[index + 1] : undefined;
|
|
587
626
|
}
|
|
588
627
|
|
|
589
|
-
async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
|
|
590
|
-
const deadline = Date.now() + timeoutMs;
|
|
591
|
-
while (Date.now() < deadline) {
|
|
592
|
-
if (await isServerRunning(endpoint)) return true;
|
|
593
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
594
|
-
}
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
628
|
function printJson(value: unknown): void {
|
|
599
629
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
600
630
|
}
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -3,14 +3,18 @@ import path from "node:path";
|
|
|
3
3
|
import { config as loadDotEnv } from "dotenv";
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import { appConfigSchema, type AppConfig } from "./schema.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AGENT_BOT_EXPLICIT_PROFILE_ENV,
|
|
8
|
+
defaultConfigPath,
|
|
9
|
+
defaultDotEnvPath,
|
|
10
|
+
resolveUserPath,
|
|
11
|
+
} from "./paths.js";
|
|
7
12
|
|
|
8
13
|
const ENV_PATTERN = /\$\{([A-Z0-9_]+)\}/gi;
|
|
9
14
|
const DEFAULT_CONFIG = `feishu:
|
|
10
|
-
transport: "auto"
|
|
11
15
|
appId: "\${FEISHU_APP_ID}"
|
|
12
16
|
appSecret: "\${FEISHU_APP_SECRET}"
|
|
13
|
-
|
|
17
|
+
userOpenId: "\${FEISHU_USER_OPEN_ID}"
|
|
14
18
|
|
|
15
19
|
console:
|
|
16
20
|
enabled: true
|
|
@@ -67,7 +71,11 @@ logging:
|
|
|
67
71
|
export function loadConfig(
|
|
68
72
|
configPath?: string,
|
|
69
73
|
): AppConfig {
|
|
70
|
-
loadDotEnv({
|
|
74
|
+
loadDotEnv({
|
|
75
|
+
path: defaultDotEnvPath(),
|
|
76
|
+
quiet: true,
|
|
77
|
+
override: process.env[AGENT_BOT_EXPLICIT_PROFILE_ENV] === "1",
|
|
78
|
+
});
|
|
71
79
|
|
|
72
80
|
const configuredPath = firstNonBlank(configPath, process.env.AGENT_BOT_CONFIG);
|
|
73
81
|
const absolutePath = resolveUserPath(configuredPath ?? defaultConfigPath());
|
package/src/config/paths.ts
CHANGED
|
@@ -2,6 +2,7 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
export const AGENT_BOT_HOME_ENV = "AGENT_BOT_HOME";
|
|
5
|
+
export const AGENT_BOT_EXPLICIT_PROFILE_ENV = "AGENT_BOT_EXPLICIT_PROFILE";
|
|
5
6
|
export const DEFAULT_CONFIG_FILE = "config.yaml";
|
|
6
7
|
export const DEFAULT_ENV_FILE = ".env";
|
|
7
8
|
export const DEFAULT_SQLITE_PATH = "./data/agent-bot.sqlite";
|
package/src/config/schema.ts
CHANGED
|
@@ -21,7 +21,8 @@ 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
|
-
|
|
24
|
+
userOpenId: z.string().optional(),
|
|
25
|
+
useConsoleWhenMissingCredentials: z.boolean().default(false),
|
|
25
26
|
}),
|
|
26
27
|
agents: z.record(z.string().min(1), agentConfigSchema).refine(
|
|
27
28
|
(agents) => Object.keys(agents).length > 0,
|
|
@@ -33,8 +33,9 @@ export interface StartupStatusView {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface SafeRestartStatusView {
|
|
36
|
+
scheduleId: number;
|
|
36
37
|
reason: string;
|
|
37
|
-
phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting";
|
|
38
|
+
phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled";
|
|
38
39
|
remainingMs?: number;
|
|
39
40
|
pendingFinalDeliveries: number;
|
|
40
41
|
waitingTasks: Array<{
|
|
@@ -270,6 +271,8 @@ export class CardRenderer {
|
|
|
270
271
|
? `${Math.max(0, Math.ceil((view.remainingMs ?? 0) / 1_000))}s`
|
|
271
272
|
: view.phase === "restarting"
|
|
272
273
|
? "0s"
|
|
274
|
+
: view.phase === "cancelled"
|
|
275
|
+
? "已取消"
|
|
273
276
|
: "等待阻塞项清空后开始";
|
|
274
277
|
const status = view.phase === "waiting_tasks"
|
|
275
278
|
? "🟠 等待任务完成"
|
|
@@ -277,7 +280,9 @@ export class CardRenderer {
|
|
|
277
280
|
? "🟠 等待最终结果投递"
|
|
278
281
|
: view.phase === "countdown"
|
|
279
282
|
? "🟡 空闲确认中"
|
|
280
|
-
:
|
|
283
|
+
: view.phase === "restarting"
|
|
284
|
+
? "🔄 正在重启"
|
|
285
|
+
: "⚪ 已取消";
|
|
281
286
|
const lines = [
|
|
282
287
|
`**状态**:${status}`,
|
|
283
288
|
`**重启原因**:${inlineCode(view.reason)}`,
|
|
@@ -296,7 +301,24 @@ export class CardRenderer {
|
|
|
296
301
|
} else {
|
|
297
302
|
elements.push({ tag: "hr" }, markdown("**当前等待的任务**:无"));
|
|
298
303
|
}
|
|
299
|
-
|
|
304
|
+
if (view.phase !== "restarting" && view.phase !== "cancelled") {
|
|
305
|
+
elements.push(
|
|
306
|
+
{ tag: "hr" },
|
|
307
|
+
taskActionRow([{
|
|
308
|
+
text: "Cancel",
|
|
309
|
+
value: {
|
|
310
|
+
action: "safe_restart_cancel",
|
|
311
|
+
scheduleId: String(view.scheduleId),
|
|
312
|
+
},
|
|
313
|
+
}]),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const template = view.phase === "restarting"
|
|
317
|
+
? "blue"
|
|
318
|
+
: view.phase === "cancelled"
|
|
319
|
+
? "grey"
|
|
320
|
+
: "orange";
|
|
321
|
+
return sectionCard("Agent Bot 安全重启", elements, template);
|
|
300
322
|
}
|
|
301
323
|
|
|
302
324
|
renderStartupStatus(view: StartupStatusView): Record<string, unknown> {
|
|
@@ -25,7 +25,6 @@ export class FeishuConnector {
|
|
|
25
25
|
|
|
26
26
|
private async startFeishuWs(appId: string, appSecret: string): Promise<void> {
|
|
27
27
|
const lark = (await import("@larksuiteoapi/node-sdk")) as Record<string, any>;
|
|
28
|
-
const wsClient = new lark.WSClient({ appId, appSecret });
|
|
29
28
|
const eventDispatcher = new lark.EventDispatcher({}).register({
|
|
30
29
|
"im.message.receive_v1": async (data: unknown) => {
|
|
31
30
|
const message = toIncomingMessage(data);
|
|
@@ -77,8 +76,11 @@ export class FeishuConnector {
|
|
|
77
76
|
};
|
|
78
77
|
},
|
|
79
78
|
});
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const wsClient = new lark.WSClient({
|
|
80
|
+
appId,
|
|
81
|
+
appSecret,
|
|
82
|
+
});
|
|
83
|
+
await wsClient.start({ eventDispatcher });
|
|
82
84
|
this.logger.info("Feishu WebSocket connector started.");
|
|
83
85
|
}
|
|
84
86
|
}
|
|
@@ -86,12 +88,8 @@ export class FeishuConnector {
|
|
|
86
88
|
function toChatUpdatedEvent(data: unknown): ChatUpdatedEvent | undefined {
|
|
87
89
|
const event = getFeishuEvent(data);
|
|
88
90
|
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;
|
|
91
|
+
const beforeName = typeof event?.before_change?.name === "string" ? event.before_change.name : undefined;
|
|
92
|
+
const afterName = typeof event?.after_change?.name === "string" ? event.after_change.name : undefined;
|
|
95
93
|
if (!chatId || !afterName || beforeName === afterName) return undefined;
|
|
96
94
|
return {
|
|
97
95
|
chatId,
|
|
@@ -118,9 +116,7 @@ function toIncomingMessage(data: unknown): IncomingMessage | undefined {
|
|
|
118
116
|
const parentMessageId = typeof message.parent_id === "string" && message.parent_id ? message.parent_id : undefined;
|
|
119
117
|
const threadContext = Boolean(chatId && threadId);
|
|
120
118
|
const senderId =
|
|
121
|
-
event.sender?.sender_id?.open_id ??
|
|
122
|
-
event.sender?.sender_id?.user_id ??
|
|
123
|
-
event.sender?.sender_id?.union_id;
|
|
119
|
+
event.sender?.sender_id?.open_id ?? event.sender?.sender_id?.user_id ?? event.sender?.sender_id?.union_id;
|
|
124
120
|
|
|
125
121
|
if (!chatId && !senderId) {
|
|
126
122
|
return undefined;
|