@keyou007/agent-bot 0.1.12 → 0.1.13-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -2
- package/CHANGELOG.md +18 -0
- package/README.md +32 -13
- package/README.zh.md +32 -13
- package/config.example.yaml +0 -24
- package/dist/cli/AgentPrerequisites.js +262 -0
- package/dist/cli/AgentPrerequisites.js.map +1 -0
- package/dist/cli/DefaultAgentSelection.js +47 -0
- package/dist/cli/DefaultAgentSelection.js.map +1 -0
- package/dist/cli/Initializer.js +165 -2
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/ServerStarter.js +17 -2
- package/dist/cli/ServerStarter.js.map +1 -1
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +16 -6
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.js +233 -4
- package/dist/cli.js.map +1 -1
- package/dist/codex/AppServerConnection.js +3 -3
- package/dist/codex/AppServerConnection.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +4 -4
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +28 -16
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/FeishuUserOpenIdStore.js +76 -0
- package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
- package/dist/config/loadConfig.js +3 -24
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/console/ConsoleConnector.js +2 -2
- package/dist/console/ConsoleConnector.js.map +1 -1
- package/dist/feishu/CardRenderer.js +90 -30
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/index.js +104 -24
- package/dist/index.js.map +1 -1
- package/dist/presentation/turnGraph.js +36 -0
- package/dist/presentation/turnGraph.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +321 -98
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/AcpRuntimeAdapter.js +3 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/startup/StartupNotifier.js +16 -7
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -2
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/state/StateStore.js +169 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +21 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +46 -3
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +65 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/replacementSupervisor.js +27 -2
- package/dist/supervision/replacementSupervisor.js.map +1 -1
- package/dist/supervisor.js +27 -2
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +42 -20
- package/docs/technical-reference.zh.md +42 -20
- package/npm-shrinkwrap.json +2 -2
- package/package.json +7 -3
- package/skills/agent-bot/SKILL.md +21 -9
- package/src/cli/AgentPrerequisites.ts +377 -0
- package/src/cli/DefaultAgentSelection.ts +56 -0
- package/src/cli/Initializer.ts +204 -3
- package/src/cli/ServerStarter.ts +25 -2
- package/src/cli/controlProtocol.ts +6 -1
- package/src/cli/help.ts +16 -6
- package/src/cli.ts +350 -5
- package/src/codex/AppServerConnection.ts +3 -3
- package/src/codex/CodexProcessManager.ts +4 -4
- package/src/codex/CodexRuntime.ts +38 -18
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/config/FeishuUserOpenIdStore.ts +81 -0
- package/src/config/loadConfig.ts +3 -24
- package/src/console/ConsoleConnector.ts +2 -2
- package/src/feishu/CardRenderer.ts +117 -30
- package/src/index.ts +162 -22
- package/src/presentation/turnGraph.ts +48 -0
- package/src/proxy/ProxySessionController.ts +375 -107
- package/src/runtime/AcpRuntimeAdapter.ts +3 -1
- package/src/startup/StartupNotifier.ts +23 -7
- package/src/startup/startFeishu.ts +3 -2
- package/src/state/StateStore.ts +221 -0
- package/src/state/migrations.ts +21 -0
- package/src/supervision/SafeRestartNotifier.ts +53 -4
- package/src/supervision/SafeRestartScheduler.ts +78 -7
- package/src/supervision/replacementSupervisor.ts +27 -1
- package/src/supervisor.ts +33 -2
package/src/cli/ServerStarter.ts
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
import { refreshedSystemEnvironment } from "../supervision/systemEnvironment.js";
|
|
12
12
|
import { controlEndpoint } from "./controlProtocol.js";
|
|
13
13
|
import { cliText } from "./i18n.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
isServerReachable,
|
|
16
|
+
isServerRunning,
|
|
17
|
+
sendControlRequest,
|
|
18
|
+
} from "./LocalControlClient.js";
|
|
15
19
|
|
|
16
20
|
export interface ServerStartResult {
|
|
17
21
|
status: "started" | "already-running";
|
|
@@ -19,6 +23,7 @@ export interface ServerStartResult {
|
|
|
19
23
|
|
|
20
24
|
export type InitializationServerResult =
|
|
21
25
|
| ServerStartResult
|
|
26
|
+
| { status: "restart-scheduled" }
|
|
22
27
|
| { status: "skipped"; reason: "feishu-skipped" };
|
|
23
28
|
|
|
24
29
|
export interface ServerStarterDependencies {
|
|
@@ -33,6 +38,7 @@ const SERVER_START_TIMEOUT_MS = 45_000;
|
|
|
33
38
|
interface InitializationServerDependencies {
|
|
34
39
|
loadConfig(configPath?: string): AppConfig;
|
|
35
40
|
startServer(config: AppConfig): Promise<ServerStartResult>;
|
|
41
|
+
restartServer(config: AppConfig): Promise<void>;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
export async function startInitializedServer(
|
|
@@ -42,7 +48,11 @@ export async function startInitializedServer(
|
|
|
42
48
|
if (options.skipFeishu) {
|
|
43
49
|
return { status: "skipped", reason: "feishu-skipped" };
|
|
44
50
|
}
|
|
45
|
-
|
|
51
|
+
const config = dependencies.loadConfig(options.configPath);
|
|
52
|
+
const result = await dependencies.startServer(config);
|
|
53
|
+
if (result.status !== "already-running") return result;
|
|
54
|
+
await dependencies.restartServer(config);
|
|
55
|
+
return { status: "restart-scheduled" };
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
export async function startServer(
|
|
@@ -116,6 +126,19 @@ const defaultDependencies: ServerStarterDependencies = {
|
|
|
116
126
|
const defaultInitializationDependencies: InitializationServerDependencies = {
|
|
117
127
|
loadConfig,
|
|
118
128
|
startServer: (config) => startServer(config),
|
|
129
|
+
restartServer: async (config) => {
|
|
130
|
+
const response = await sendControlRequest(controlEndpoint(config.storage.sqlitePath), {
|
|
131
|
+
action: "server_restart",
|
|
132
|
+
mode: "safe",
|
|
133
|
+
reason: "Agent Bot 初始化完成,需要加载当前版本",
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
throw new Error(response.message?.trim() || cliText(
|
|
137
|
+
"Could not schedule the Agent Bot restart after initialization.",
|
|
138
|
+
"初始化完成后无法安排 Agent Bot 重启。",
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
},
|
|
119
142
|
};
|
|
120
143
|
|
|
121
144
|
async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
|
|
@@ -6,7 +6,12 @@ import type { SessionRecord } from "../state/StateStore.js";
|
|
|
6
6
|
|
|
7
7
|
export type ControlRequest =
|
|
8
8
|
| { action: "health" }
|
|
9
|
-
| {
|
|
9
|
+
| {
|
|
10
|
+
action: "server_restart";
|
|
11
|
+
mode: "safe" | "immediate";
|
|
12
|
+
reason: string;
|
|
13
|
+
notificationSessionId?: string;
|
|
14
|
+
}
|
|
10
15
|
| { action: "server_stop" }
|
|
11
16
|
| { action: "task_status"; localSessionId: string }
|
|
12
17
|
| { action: "task_stop"; localSessionId: string }
|
package/src/cli/help.ts
CHANGED
|
@@ -3,18 +3,18 @@ import { cliLanguage, type CliLanguage } from "./i18n.js";
|
|
|
3
3
|
export function renderCliHelp(version: string, language: CliLanguage = cliLanguage): string {
|
|
4
4
|
if (language === "zh") {
|
|
5
5
|
return `Agent Bot ${version}
|
|
6
|
-
通过飞书使用本地 Codex
|
|
6
|
+
通过飞书使用本地 Codex、TraeX 和兼容 ACP 的 Agent。
|
|
7
7
|
|
|
8
8
|
用法:
|
|
9
9
|
agentbot [选项] <命令>
|
|
10
10
|
|
|
11
11
|
常用命令:
|
|
12
|
-
init
|
|
12
|
+
init 检查并选择 Agent,初始化飞书应用和服务
|
|
13
13
|
init --reset 重置并重新配置显式指定的 Profile
|
|
14
14
|
console 打开本地控制台
|
|
15
15
|
server start 在后台启动 Agent Bot
|
|
16
16
|
server status 显示服务、飞书 App ID 和安全重启状态
|
|
17
|
-
server restart
|
|
17
|
+
server restart [--task <任务>] 安排安全重启并将状态发回指定任务会话
|
|
18
18
|
server stop 停止 Agent Bot
|
|
19
19
|
task list 列出任务
|
|
20
20
|
task status <任务> 显示任务状态
|
|
@@ -39,6 +39,11 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
|
|
|
39
39
|
--skip-feishu 仅初始化 Console 环境
|
|
40
40
|
--reconfigure-feishu 仅替换现有飞书凭据
|
|
41
41
|
|
|
42
|
+
重启选项:
|
|
43
|
+
--task <任务> 将安全重启状态发回任务所在会话
|
|
44
|
+
--reason <原因> 设置重启原因
|
|
45
|
+
--immediate 立即重启,可能中断任务
|
|
46
|
+
|
|
42
47
|
示例:
|
|
43
48
|
agentbot init
|
|
44
49
|
agentbot --profile ~/.agent-bot init --reset
|
|
@@ -51,18 +56,18 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
return `Agent Bot ${version}
|
|
54
|
-
Use
|
|
59
|
+
Use Codex, TraeX, and compatible ACP agents from Lark/Feishu.
|
|
55
60
|
|
|
56
61
|
Usage:
|
|
57
62
|
agentbot [options] <command>
|
|
58
63
|
|
|
59
64
|
Common commands:
|
|
60
|
-
init
|
|
65
|
+
init Check and select an Agent; initialize the Lark app and service
|
|
61
66
|
init --reset Reset and reconfigure an explicit profile
|
|
62
67
|
console Open the local console
|
|
63
68
|
server start Start Agent Bot in the background
|
|
64
69
|
server status Show the server, Lark App ID, and safe-restart status
|
|
65
|
-
server restart
|
|
70
|
+
server restart [--task <task>] Schedule a safe restart and return status to the task's conversation
|
|
66
71
|
server stop Stop Agent Bot
|
|
67
72
|
task list List tasks
|
|
68
73
|
task status <task> Show task status
|
|
@@ -87,6 +92,11 @@ Init options:
|
|
|
87
92
|
--skip-feishu Initialize for Console only
|
|
88
93
|
--reconfigure-feishu Replace only the existing Lark credentials
|
|
89
94
|
|
|
95
|
+
Restart options:
|
|
96
|
+
--task <task> Return safe-restart status to the task's conversation
|
|
97
|
+
--reason <reason> Set the restart reason
|
|
98
|
+
--immediate Restart immediately and possibly interrupt tasks
|
|
99
|
+
|
|
90
100
|
Examples:
|
|
91
101
|
agentbot init
|
|
92
102
|
agentbot --profile ~/.agent-bot init --reset
|
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
5
6
|
import { loadConfig, loadConfigWithoutEnvironmentMutation } from "./config/loadConfig.js";
|
|
6
7
|
import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.js";
|
|
7
8
|
import {
|
|
@@ -14,8 +15,10 @@ import {
|
|
|
14
15
|
acquireInitializationLock,
|
|
15
16
|
cleanupFeishuCredentialTemporaryFiles,
|
|
16
17
|
initializeAgentBot,
|
|
18
|
+
readConfiguredAgentSelection,
|
|
17
19
|
readFeishuCredentials,
|
|
18
20
|
shouldCreateFeishuApp,
|
|
21
|
+
writeDefaultAgent,
|
|
19
22
|
writeFeishuCredentials,
|
|
20
23
|
type InitializationResult,
|
|
21
24
|
type InitializationStatus,
|
|
@@ -38,6 +41,18 @@ import {
|
|
|
38
41
|
} from "./cli/FeishuAppConfiguration.js";
|
|
39
42
|
import { renderCliHelp } from "./cli/help.js";
|
|
40
43
|
import { cliLanguage, cliText, localizeCliErrorMessage } from "./cli/i18n.js";
|
|
44
|
+
import {
|
|
45
|
+
inspectSupportedAgent,
|
|
46
|
+
inspectSupportedAgents,
|
|
47
|
+
runSupportedAgentMaintenance,
|
|
48
|
+
type SupportedAgentInspection,
|
|
49
|
+
} from "./cli/AgentPrerequisites.js";
|
|
50
|
+
import {
|
|
51
|
+
parseMaintenanceSelection,
|
|
52
|
+
resolveDefaultAgentChoice,
|
|
53
|
+
selectableDefaultAgents,
|
|
54
|
+
type SelectableDefaultAgent,
|
|
55
|
+
} from "./cli/DefaultAgentSelection.js";
|
|
41
56
|
import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatus.js";
|
|
42
57
|
import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
|
|
43
58
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
@@ -50,6 +65,8 @@ import {
|
|
|
50
65
|
type ServerStartResult,
|
|
51
66
|
} from "./cli/ServerStarter.js";
|
|
52
67
|
import { taskChatRoute } from "./cli/taskChatRoute.js";
|
|
68
|
+
import { isThreadContextKey } from "./feishu/contextKey.js";
|
|
69
|
+
import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
|
|
53
70
|
import {
|
|
54
71
|
parseTaskForkGroupOptions,
|
|
55
72
|
parseTaskNewGroupOptions,
|
|
@@ -112,14 +129,27 @@ async function main(input: string[]): Promise<void> {
|
|
|
112
129
|
type FeishuInitializationStatus = "created" | "existing" | "skipped";
|
|
113
130
|
|
|
114
131
|
interface InitCommandResult extends InitializationResult {
|
|
132
|
+
agents: InitializedAgent[];
|
|
133
|
+
defaultAgent: {
|
|
134
|
+
name: string;
|
|
135
|
+
status: "selected" | "existing";
|
|
136
|
+
};
|
|
115
137
|
feishu: {
|
|
116
138
|
status: FeishuInitializationStatus;
|
|
117
139
|
appId?: string;
|
|
140
|
+
userOpenIdStatus?: "configured" | "pending";
|
|
118
141
|
configuration?: EnsureFeishuAppConfigurationResult;
|
|
119
142
|
};
|
|
120
143
|
server: InitializationServerResult;
|
|
121
144
|
}
|
|
122
145
|
|
|
146
|
+
interface InitializedAgent extends SupportedAgentInspection {
|
|
147
|
+
assistance?: {
|
|
148
|
+
status: "completed" | "skipped" | "unavailable" | "failed";
|
|
149
|
+
error?: string;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
async function initCommand(
|
|
124
154
|
input: string[],
|
|
125
155
|
configPath: string | undefined,
|
|
@@ -135,13 +165,15 @@ async function initCommand(
|
|
|
135
165
|
if (options.reset) await assertResetProfileServerStopped(configPath);
|
|
136
166
|
paths = initializeAgentBot({ configPath, reset: options.reset });
|
|
137
167
|
if (!options.json) printInitializationPaths(paths);
|
|
168
|
+
const agents = await initializeSupportedAgents(options.json);
|
|
169
|
+
const defaultAgent = await selectDefaultAgent(paths.config.path, agents, options.json);
|
|
138
170
|
initializationLock ??= acquireInitializationLock(paths.home.path);
|
|
139
171
|
cleanupFeishuCredentialTemporaryFiles(paths.env.path);
|
|
140
172
|
const feishu = await initializeFeishu(
|
|
141
173
|
paths,
|
|
142
174
|
options,
|
|
143
175
|
);
|
|
144
|
-
initialized = { ...paths, feishu };
|
|
176
|
+
initialized = { ...paths, agents, defaultAgent, feishu };
|
|
145
177
|
} finally {
|
|
146
178
|
initializationLock?.release();
|
|
147
179
|
}
|
|
@@ -156,6 +188,259 @@ async function initCommand(
|
|
|
156
188
|
else printInitializationServerResult(server);
|
|
157
189
|
}
|
|
158
190
|
|
|
191
|
+
async function initializeSupportedAgents(json: boolean): Promise<InitializedAgent[]> {
|
|
192
|
+
const output = json ? process.stderr : process.stdout;
|
|
193
|
+
output.write(cliText(
|
|
194
|
+
"\nAgent setup\nChecking Codex and TraeX...\n",
|
|
195
|
+
"\nAgent 设置\n正在检查 Codex 和 TraeX...\n",
|
|
196
|
+
));
|
|
197
|
+
const inspections = await inspectSupportedAgents();
|
|
198
|
+
for (const inspection of inspections) printAgentInspection(inspection, output);
|
|
199
|
+
const actionable = inspections.filter((inspection) => inspection.action);
|
|
200
|
+
if (actionable.length === 0) return inspections;
|
|
201
|
+
|
|
202
|
+
output.write(cliText("\nAvailable actions:\n", "\n可执行的操作:\n"));
|
|
203
|
+
for (const [index, inspection] of actionable.entries()) {
|
|
204
|
+
output.write(cliText(
|
|
205
|
+
` ${index + 1}. ${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}: ${inspection.action?.command}\n`,
|
|
206
|
+
` ${index + 1}. ${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action?.command}\n`,
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let selected = new Set<number>();
|
|
211
|
+
if (!process.stdin.isTTY) {
|
|
212
|
+
output.write(cliText(
|
|
213
|
+
"No interactive terminal is available. Run the commands above manually if needed.\n",
|
|
214
|
+
"当前没有交互式终端,如有需要请手动执行上面的命令。\n",
|
|
215
|
+
));
|
|
216
|
+
} else {
|
|
217
|
+
selected = new Set(await promptForAgentMaintenance(actionable.length));
|
|
218
|
+
if (selected.size === 0) {
|
|
219
|
+
output.write(cliText("No Agent changes selected.\n", "已跳过 Agent 安装和升级。\n"));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const initialized: InitializedAgent[] = [];
|
|
224
|
+
for (const inspection of inspections) {
|
|
225
|
+
if (!inspection.action) {
|
|
226
|
+
initialized.push(inspection);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const actionIndex = actionable.indexOf(inspection);
|
|
230
|
+
if (!selected.has(actionIndex)) {
|
|
231
|
+
initialized.push({
|
|
232
|
+
...inspection,
|
|
233
|
+
assistance: { status: process.stdin.isTTY ? "skipped" : "unavailable" },
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
output.write(cliText(
|
|
239
|
+
`\nRunning ${inspection.name} ${inspection.action.kind}: ${inspection.action.command}\n`,
|
|
240
|
+
`\n正在${inspection.action.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action.command}\n`,
|
|
241
|
+
));
|
|
242
|
+
const maintenance = await runSupportedAgentMaintenance(inspection.id, inspection.action.kind);
|
|
243
|
+
if (maintenance.status !== 0 || maintenance.error) {
|
|
244
|
+
const error = maintenance.error ?? cliText(
|
|
245
|
+
`Command exited with code ${maintenance.status ?? "unknown"}.`,
|
|
246
|
+
`命令退出码为 ${maintenance.status ?? "未知"}。`,
|
|
247
|
+
);
|
|
248
|
+
output.write(cliText(
|
|
249
|
+
`${inspection.name} ${inspection.action.kind} did not complete; initialization will continue.\n`,
|
|
250
|
+
`${inspection.name}${inspection.action.kind === "install" ? "安装" : "升级"}未完成,初始化将继续。\n`,
|
|
251
|
+
));
|
|
252
|
+
initialized.push({ ...inspection, assistance: { status: "failed", error } });
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
refreshCurrentProcessPath();
|
|
257
|
+
const refreshed = await inspectSupportedAgent(inspection.id);
|
|
258
|
+
if (refreshed.installedVersion) {
|
|
259
|
+
output.write(cliText(
|
|
260
|
+
`${inspection.name} is ready (${refreshed.installedVersion}).\n`,
|
|
261
|
+
`${inspection.name} 已就绪(${refreshed.installedVersion})。\n`,
|
|
262
|
+
));
|
|
263
|
+
} else {
|
|
264
|
+
output.write(cliText(
|
|
265
|
+
`${inspection.name} command completed, but the CLI is not visible in this process. Open a new terminal and run agentbot init again.\n`,
|
|
266
|
+
`${inspection.name} 命令已完成,但当前进程仍无法找到该 CLI。请打开新终端后重新运行 agentbot init。\n`,
|
|
267
|
+
));
|
|
268
|
+
}
|
|
269
|
+
initialized.push({ ...refreshed, assistance: { status: "completed" } });
|
|
270
|
+
}
|
|
271
|
+
return initialized;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function selectDefaultAgent(
|
|
275
|
+
configPath: string,
|
|
276
|
+
inspections: InitializedAgent[],
|
|
277
|
+
json: boolean,
|
|
278
|
+
): Promise<InitCommandResult["defaultAgent"]> {
|
|
279
|
+
const output = json ? process.stderr : process.stdout;
|
|
280
|
+
const configured = readConfiguredAgentSelection(configPath);
|
|
281
|
+
const choices = selectableDefaultAgents(configured.agents, inspections);
|
|
282
|
+
if (!process.stdin.isTTY) {
|
|
283
|
+
if (!configured.defaultAgent || !configured.agents.some((agent) => agent.name === configured.defaultAgent)) {
|
|
284
|
+
throw new Error(cliText(
|
|
285
|
+
"A default Agent is not configured. Run agentbot init in an interactive terminal and select one.",
|
|
286
|
+
"尚未配置默认 Agent。请在交互式终端中运行 agentbot init 并选择一个 Agent。",
|
|
287
|
+
));
|
|
288
|
+
}
|
|
289
|
+
output.write(cliText(
|
|
290
|
+
`Default Agent: ${configured.defaultAgent} (kept because no interactive terminal is available)\n`,
|
|
291
|
+
`默认 Agent:${configured.defaultAgent}(当前没有交互式终端,已保留现有设置)\n`,
|
|
292
|
+
));
|
|
293
|
+
return { name: configured.defaultAgent, status: "existing" };
|
|
294
|
+
}
|
|
295
|
+
if (choices.length === 0) {
|
|
296
|
+
throw new Error(cliText(
|
|
297
|
+
"No available configured Agent can be selected. Install Codex or TraeX, then run agentbot init again.",
|
|
298
|
+
"没有可选择的已配置 Agent。请安装 Codex 或 TraeX,然后重新运行 agentbot init。",
|
|
299
|
+
));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
output.write(cliText("\nSelect the default Agent:\n", "\n请选择默认 Agent:\n"));
|
|
303
|
+
for (const [index, choice] of choices.entries()) {
|
|
304
|
+
const version = choice.installedVersion ? ` (${choice.installedVersion})` : "";
|
|
305
|
+
const current = choice.name === configured.defaultAgent
|
|
306
|
+
? cliText(" [current]", " [当前]")
|
|
307
|
+
: "";
|
|
308
|
+
output.write(` ${index + 1}. ${choice.name} - ${choice.title}${version}${current}\n`);
|
|
309
|
+
}
|
|
310
|
+
const selected = choices[await promptForDefaultAgent(choices, configured.defaultAgent)];
|
|
311
|
+
if (!selected) throw new Error(cliText("Default Agent selection failed.", "默认 Agent 选择失败。"));
|
|
312
|
+
const updated = writeDefaultAgent(configPath, selected.name);
|
|
313
|
+
output.write(updated
|
|
314
|
+
? cliText(`Saved default Agent: ${selected.name}\n`, `已保存默认 Agent:${selected.name}\n`)
|
|
315
|
+
: cliText(`Default Agent unchanged: ${selected.name}\n`, `默认 Agent 保持不变:${selected.name}\n`));
|
|
316
|
+
return { name: selected.name, status: updated ? "selected" : "existing" };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function promptForAgentMaintenance(choiceCount: number): Promise<number[]> {
|
|
320
|
+
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
let settled = false;
|
|
323
|
+
const finish = (selection: number[]): void => {
|
|
324
|
+
settled = true;
|
|
325
|
+
readline.close();
|
|
326
|
+
resolve(selection);
|
|
327
|
+
};
|
|
328
|
+
const ask = (): void => {
|
|
329
|
+
readline.question(cliText(
|
|
330
|
+
"Enter action numbers separated by commas, type all, or press Enter to skip: ",
|
|
331
|
+
"请输入要执行的操作编号(多个用逗号分隔),输入 all 选择全部,直接回车跳过:",
|
|
332
|
+
), (answer) => {
|
|
333
|
+
const selection = parseMaintenanceSelection(answer, choiceCount);
|
|
334
|
+
if (selection) {
|
|
335
|
+
finish(selection);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
process.stderr.write(cliText(
|
|
339
|
+
`Enter numbers from 1 to ${choiceCount}, all, or press Enter to skip.\n`,
|
|
340
|
+
`请输入 1 到 ${choiceCount} 之间的编号、all,或直接回车跳过。\n`,
|
|
341
|
+
));
|
|
342
|
+
ask();
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
listenForPromptCancellation(readline, () => {
|
|
346
|
+
settled = true;
|
|
347
|
+
reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
|
|
348
|
+
}, () => settled);
|
|
349
|
+
ask();
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function promptForDefaultAgent(
|
|
354
|
+
choices: SelectableDefaultAgent[],
|
|
355
|
+
currentAgent?: string,
|
|
356
|
+
): Promise<number> {
|
|
357
|
+
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
let settled = false;
|
|
360
|
+
const finish = (index: number): void => {
|
|
361
|
+
settled = true;
|
|
362
|
+
readline.close();
|
|
363
|
+
resolve(index);
|
|
364
|
+
};
|
|
365
|
+
const ask = (): void => {
|
|
366
|
+
const currentAvailable = choices.some((choice) => choice.name === currentAgent);
|
|
367
|
+
readline.question(cliText(
|
|
368
|
+
`Enter a number or Agent name${currentAvailable ? ` [${currentAgent}]` : ""}: `,
|
|
369
|
+
`请输入编号或 Agent 标准名${currentAvailable ? ` [${currentAgent}]` : ""}:`,
|
|
370
|
+
), (answer) => {
|
|
371
|
+
const index = resolveDefaultAgentChoice(answer, choices, currentAgent);
|
|
372
|
+
if (index !== undefined) {
|
|
373
|
+
finish(index);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
process.stderr.write(cliText(
|
|
377
|
+
`Enter a number from 1 to ${choices.length} or an Agent standard name.\n`,
|
|
378
|
+
`请输入 1 到 ${choices.length} 之间的编号,或 Agent 标准名。\n`,
|
|
379
|
+
));
|
|
380
|
+
ask();
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
listenForPromptCancellation(readline, () => {
|
|
384
|
+
settled = true;
|
|
385
|
+
reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
|
|
386
|
+
}, () => settled);
|
|
387
|
+
ask();
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function listenForPromptCancellation(
|
|
392
|
+
readline: ReturnType<typeof createInterface>,
|
|
393
|
+
cancel: () => void,
|
|
394
|
+
isSettled: () => boolean,
|
|
395
|
+
): void {
|
|
396
|
+
readline.once("SIGINT", () => {
|
|
397
|
+
if (!isSettled()) cancel();
|
|
398
|
+
readline.close();
|
|
399
|
+
});
|
|
400
|
+
readline.once("close", () => {
|
|
401
|
+
if (!isSettled()) cancel();
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function printAgentInspection(
|
|
406
|
+
inspection: SupportedAgentInspection,
|
|
407
|
+
output: NodeJS.WriteStream,
|
|
408
|
+
): void {
|
|
409
|
+
if (inspection.state === "missing") {
|
|
410
|
+
output.write(cliText(
|
|
411
|
+
` ${inspection.name}: not installed\n`,
|
|
412
|
+
` ${inspection.name}:未安装\n`,
|
|
413
|
+
));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (inspection.state === "outdated") {
|
|
417
|
+
output.write(cliText(
|
|
418
|
+
` ${inspection.name}: ${inspection.installedVersion}; update available (${inspection.latestVersion})\n`,
|
|
419
|
+
` ${inspection.name}:${inspection.installedVersion};可升级到 ${inspection.latestVersion}\n`,
|
|
420
|
+
));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (inspection.latestCheckFailed) {
|
|
424
|
+
output.write(cliText(
|
|
425
|
+
` ${inspection.name}: ${inspection.installedVersion}; latest-version check unavailable\n`,
|
|
426
|
+
` ${inspection.name}:${inspection.installedVersion};无法检查最新版本\n`,
|
|
427
|
+
));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
output.write(cliText(
|
|
431
|
+
` ${inspection.name}: ${inspection.installedVersion} (up to date)\n`,
|
|
432
|
+
` ${inspection.name}:${inspection.installedVersion}(已是最新)\n`,
|
|
433
|
+
));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function refreshCurrentProcessPath(): void {
|
|
437
|
+
const refreshed = refreshedSystemEnvironment();
|
|
438
|
+
if (!refreshed.refreshed) return;
|
|
439
|
+
const pathEntry = Object.entries(refreshed.environment)
|
|
440
|
+
.find(([name]) => name.toLowerCase() === "path");
|
|
441
|
+
if (pathEntry?.[1]) process.env.PATH = pathEntry[1];
|
|
442
|
+
}
|
|
443
|
+
|
|
159
444
|
async function initializeFeishu(
|
|
160
445
|
paths: InitializationResult,
|
|
161
446
|
options: InitCommandOptions,
|
|
@@ -165,8 +450,14 @@ async function initializeFeishu(
|
|
|
165
450
|
return {
|
|
166
451
|
status: "skipped",
|
|
167
452
|
...(existing.appId ? { appId: existing.appId } : {}),
|
|
453
|
+
...(existing.appId && existing.appSecret
|
|
454
|
+
? { userOpenIdStatus: existing.userOpenId ? "configured" as const : "pending" as const }
|
|
455
|
+
: {}),
|
|
168
456
|
};
|
|
169
457
|
}
|
|
458
|
+
if (!options.json) {
|
|
459
|
+
process.stdout.write(cliText("\nLark setup\n", "\n飞书设置\n"));
|
|
460
|
+
}
|
|
170
461
|
|
|
171
462
|
const controller = new AbortController();
|
|
172
463
|
const onInterrupt = (): void => controller.abort(new Error(cliText(
|
|
@@ -202,8 +493,8 @@ async function initializeFeishu(
|
|
|
202
493
|
|
|
203
494
|
if (!options.json) {
|
|
204
495
|
process.stdout.write(cliText(
|
|
205
|
-
"
|
|
206
|
-
"
|
|
496
|
+
"Checking app permissions, events, and callbacks...\n",
|
|
497
|
+
"正在检查应用权限、事件和回调...\n",
|
|
207
498
|
));
|
|
208
499
|
}
|
|
209
500
|
const manualPermissionSkip = new AbortController();
|
|
@@ -227,6 +518,7 @@ async function initializeFeishu(
|
|
|
227
518
|
return {
|
|
228
519
|
status,
|
|
229
520
|
appId: credentials.appId,
|
|
521
|
+
userOpenIdStatus: credentials.userOpenId ? "configured" : "pending",
|
|
230
522
|
configuration,
|
|
231
523
|
};
|
|
232
524
|
} finally {
|
|
@@ -373,12 +665,14 @@ async function serverCommand(input: string[]): Promise<void> {
|
|
|
373
665
|
}
|
|
374
666
|
if (action === "restart") {
|
|
375
667
|
const immediate = rest.includes("--immediate") || rest.includes("--force");
|
|
668
|
+
const notificationSessionId = resolveRestartNotificationSessionId(config.storage.sqlitePath, rest);
|
|
376
669
|
const reason = optionValue(rest, "--reason")
|
|
377
670
|
?? (immediate ? "通过 Agent Bot CLI 立即重启" : "通过 Agent Bot CLI 安全重启");
|
|
378
671
|
ensureOk(await sendControlRequest(endpoint, {
|
|
379
672
|
action: "server_restart",
|
|
380
673
|
mode: immediate ? "immediate" : "safe",
|
|
381
674
|
reason,
|
|
675
|
+
notificationSessionId,
|
|
382
676
|
}));
|
|
383
677
|
process.stdout.write(immediate
|
|
384
678
|
? cliText("Immediate restart requested.\n", "已请求立即重启。\n")
|
|
@@ -388,6 +682,42 @@ async function serverCommand(input: string[]): Promise<void> {
|
|
|
388
682
|
throw new Error(cliText(`Unknown server command: ${action}`, `未知的 server 命令:${action}`));
|
|
389
683
|
}
|
|
390
684
|
|
|
685
|
+
function resolveRestartNotificationSessionId(sqlitePath: string, args: string[]): string | undefined {
|
|
686
|
+
const store = new StateStore(sqlitePath);
|
|
687
|
+
try {
|
|
688
|
+
const sessions = store.listAllSessions();
|
|
689
|
+
if (args.includes("--task")) {
|
|
690
|
+
const reference = optionValue(args, "--task")?.trim();
|
|
691
|
+
if (!reference || reference.startsWith("--")) throw new Error(cliText(
|
|
692
|
+
"server restart --task requires a task number or task ID.",
|
|
693
|
+
"server restart --task 需要任务序号或任务 ID。",
|
|
694
|
+
));
|
|
695
|
+
const session = resolveTask(sessions, reference);
|
|
696
|
+
if (
|
|
697
|
+
isThreadContextKey(session.contextKey)
|
|
698
|
+
&& !store.findLatestMessageIdForSession(session.localSessionId, session.contextKey)
|
|
699
|
+
&& !store.findLatestMessageIdForContext(session.contextKey)
|
|
700
|
+
) {
|
|
701
|
+
throw new Error(cliText(
|
|
702
|
+
"The task topic has no message anchor for restart notifications. Send /restart in that topic instead.",
|
|
703
|
+
"该任务话题没有可用于重启通知的消息锚点,请直接在该话题中发送 /restart。",
|
|
704
|
+
));
|
|
705
|
+
}
|
|
706
|
+
return session.localSessionId;
|
|
707
|
+
}
|
|
708
|
+
const runningContexts = new Set(
|
|
709
|
+
sessions.filter((session) => session.status === "running").map((session) => session.contextKey),
|
|
710
|
+
);
|
|
711
|
+
if (runningContexts.size > 1) throw new Error(cliText(
|
|
712
|
+
"Multiple conversations are running. Pass --task <task> so restart notifications return to the requester.",
|
|
713
|
+
"当前有多个会话正在运行。请传入 --task <任务>,确保重启通知返回发起会话。",
|
|
714
|
+
));
|
|
715
|
+
return undefined;
|
|
716
|
+
} finally {
|
|
717
|
+
store.close();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
391
721
|
async function taskCommand(input: string[]): Promise<void> {
|
|
392
722
|
const [action = "list", ...rest] = input;
|
|
393
723
|
const config = loadConfig();
|
|
@@ -624,8 +954,8 @@ function printSkillStatus(status: SkillRegistrationStatus): void {
|
|
|
624
954
|
|
|
625
955
|
function printInitializationPaths(result: InitializationResult): void {
|
|
626
956
|
process.stdout.write(cliText(
|
|
627
|
-
"
|
|
628
|
-
"
|
|
957
|
+
"Profile setup\n",
|
|
958
|
+
"Profile 设置\n",
|
|
629
959
|
));
|
|
630
960
|
if (result.reset) {
|
|
631
961
|
process.stdout.write(`${cliText("Reset backup: ", "重置备份:")}${result.reset.backupPath}\n`);
|
|
@@ -642,6 +972,7 @@ function printInitializationResult(result: Omit<InitCommandResult, "server">): v
|
|
|
642
972
|
"\nAgent Bot initialization completed.\n",
|
|
643
973
|
"\nAgent Bot 初始化完成。\n",
|
|
644
974
|
));
|
|
975
|
+
process.stdout.write(`${cliText("Default Agent: ", "默认 Agent:")}${result.defaultAgent.name}\n`);
|
|
645
976
|
if (result.feishu.status === "created") {
|
|
646
977
|
process.stdout.write(cliText(
|
|
647
978
|
`Lark app: created and credentials saved (${result.feishu.appId})\n`,
|
|
@@ -658,6 +989,12 @@ function printInitializationResult(result: Omit<InitCommandResult, "server">): v
|
|
|
658
989
|
"飞书应用:已跳过;请使用 Console 模式或稍后重新运行 init。\n",
|
|
659
990
|
));
|
|
660
991
|
}
|
|
992
|
+
if (result.feishu.userOpenIdStatus === "pending") {
|
|
993
|
+
process.stdout.write(cliText(
|
|
994
|
+
"Lark user: pending; send the bot a direct message to save FEISHU_USER_OPEN_ID\n",
|
|
995
|
+
"飞书用户:等待补全;请私聊机器人以保存 FEISHU_USER_OPEN_ID\n",
|
|
996
|
+
));
|
|
997
|
+
}
|
|
661
998
|
if (result.feishu.configuration?.status === "updated") {
|
|
662
999
|
const added = result.feishu.configuration.added;
|
|
663
1000
|
process.stdout.write(cliText(
|
|
@@ -697,6 +1034,13 @@ function printInitializationServerResult(result: InitCommandResult["server"]): v
|
|
|
697
1034
|
));
|
|
698
1035
|
return;
|
|
699
1036
|
}
|
|
1037
|
+
if (result.status === "restart-scheduled") {
|
|
1038
|
+
process.stdout.write(cliText(
|
|
1039
|
+
"Agent Bot server: safe restart scheduled to load the current version.\n",
|
|
1040
|
+
"Agent Bot 服务:已安排安全重启,以加载当前版本。\n",
|
|
1041
|
+
));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
700
1044
|
printServerStartResult(result);
|
|
701
1045
|
}
|
|
702
1046
|
|
|
@@ -852,6 +1196,7 @@ function feishuFeatureWarnings(missing: FeishuConfigurationChallenge["missing"])
|
|
|
852
1196
|
|
|
853
1197
|
function initializationStatusLabel(status: InitializationStatus): string {
|
|
854
1198
|
if (status === "created") return cliText("created", "已创建");
|
|
1199
|
+
if (status === "updated") return cliText("updated with missing settings", "已补全缺失配置");
|
|
855
1200
|
if (status === "reset") return cliText("reset from template", "已从模板重置");
|
|
856
1201
|
return cliText("already exists; unchanged", "已存在,未修改");
|
|
857
1202
|
}
|
|
@@ -108,7 +108,7 @@ export class AppServerConnection {
|
|
|
108
108
|
try {
|
|
109
109
|
message = JSON.parse(line) as AppServerMessage;
|
|
110
110
|
} catch (error) {
|
|
111
|
-
this.logger.warn({ line, error }, "Ignoring non-JSON
|
|
111
|
+
this.logger.warn({ line, error }, "Ignoring non-JSON App Server stdout line.");
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
this.handleMessage(message);
|
|
@@ -128,7 +128,7 @@ export class AppServerConnection {
|
|
|
128
128
|
for (const listener of this.notificationListeners) listener(message.method, message.params);
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
|
-
this.logger.warn({ message }, "Ignoring unknown
|
|
131
|
+
this.logger.warn({ message }, "Ignoring unknown App Server message shape.");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
private handleResponse(message: AppServerResponse): void {
|
|
@@ -139,7 +139,7 @@ export class AppServerConnection {
|
|
|
139
139
|
responseId: message.id,
|
|
140
140
|
responseKind: "error" in message ? "error" : "result",
|
|
141
141
|
},
|
|
142
|
-
"Received
|
|
142
|
+
"Received App Server response for unknown request.",
|
|
143
143
|
);
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
@@ -27,15 +27,15 @@ export class CodexProcessManager implements AppServerClientProvider {
|
|
|
27
27
|
const client = new AppServerConnection(child, this.logger.child({ component: "codex-app-server" }));
|
|
28
28
|
this.client = client;
|
|
29
29
|
readline.createInterface({ input: child.stderr, crlfDelay: Infinity }).on("line", (line) => {
|
|
30
|
-
this.logger.debug({ line }, "
|
|
30
|
+
this.logger.debug({ line }, "App Server stderr.");
|
|
31
31
|
});
|
|
32
|
-
child.once("error", (error) => this.logger.error({ error }, "
|
|
32
|
+
child.once("error", (error) => this.logger.error({ error }, "App Server process error."));
|
|
33
33
|
child.once("exit", (code, signal) => {
|
|
34
|
-
this.logger.warn({ code, signal }, "
|
|
34
|
+
this.logger.warn({ code, signal }, "App Server process exited.");
|
|
35
35
|
client.close();
|
|
36
36
|
if (this.client === client) this.client = undefined;
|
|
37
37
|
if (this.child === child) this.child = undefined;
|
|
38
|
-
const error = new Error(`
|
|
38
|
+
const error = new Error(`App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
|
|
39
39
|
for (const listener of this.disconnectListeners) listener(error);
|
|
40
40
|
});
|
|
41
41
|
await client.request("initialize", {
|