@keyou007/agent-bot 0.1.14-alpha.0 → 0.1.14-alpha.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 +26 -0
- package/README.md +82 -55
- package/README.zh.md +82 -55
- package/config.example.yaml +5 -1
- package/dist/cli/AutostartManager.js +431 -0
- package/dist/cli/AutostartManager.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +17 -13
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/Initializer.js +35 -0
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js +552 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -0
- package/dist/cli/SelfUpdater.js +431 -0
- package/dist/cli/SelfUpdater.js.map +1 -0
- package/dist/cli/autostartOutput.js +63 -0
- package/dist/cli/autostartOutput.js.map +1 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +92 -18
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/taskGroupOptions.js +14 -11
- package/dist/cli/taskGroupOptions.js.map +1 -1
- package/dist/cli/taskListOutput.js +3 -0
- package/dist/cli/taskListOutput.js.map +1 -1
- package/dist/cli/taskTarget.js +99 -0
- package/dist/cli/taskTarget.js.map +1 -0
- package/dist/cli.js +643 -58
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +16 -5
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +20 -1
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/loadConfig.js +2 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +2 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +584 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +29 -8
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +25 -1
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +43 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +29 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/ownerAccess.js +7 -0
- package/dist/feishu/ownerAccess.js.map +1 -0
- package/dist/index.js +148 -19
- package/dist/index.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +19 -1
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +1444 -66
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +65 -16
- 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 +268 -67
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +26 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +17 -16
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +12 -7
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/replacementSupervisor.js +45 -4
- package/dist/supervision/replacementSupervisor.js.map +1 -1
- package/dist/supervisor.js +30 -18
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +33 -10
- package/docs/technical-reference.zh.md +33 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/skills/agent-bot/SKILL.md +107 -182
- package/src/cli/AutostartManager.ts +579 -0
- package/src/cli/FeishuAppConfiguration.ts +22 -7
- package/src/cli/Initializer.ts +57 -0
- package/src/cli/SelfUpdateRunner.ts +817 -0
- package/src/cli/SelfUpdater.ts +688 -0
- package/src/cli/autostartOutput.ts +75 -0
- package/src/cli/controlProtocol.ts +111 -1
- package/src/cli/help.ts +92 -18
- package/src/cli/taskGroupOptions.ts +27 -17
- package/src/cli/taskListOutput.ts +10 -0
- package/src/cli/taskTarget.ts +168 -0
- package/src/cli.ts +781 -65
- package/src/codex/CodexRuntime.ts +17 -5
- package/src/commands/CommandRouter.ts +19 -1
- package/src/commands/commandTypes.ts +3 -0
- package/src/config/loadConfig.ts +2 -0
- package/src/config/schema.ts +2 -0
- package/src/console/ConsoleTurnPresenter.ts +4 -1
- package/src/feishu/CardRenderer.ts +716 -16
- package/src/feishu/FeishuConnector.ts +44 -10
- package/src/feishu/FeishuMarkdown.ts +27 -1
- package/src/feishu/FeishuMessageClient.ts +58 -0
- package/src/feishu/FeishuTurnPresenter.ts +35 -2
- package/src/feishu/ownerAccess.ts +7 -0
- package/src/feishu/types.ts +7 -0
- package/src/index.ts +192 -22
- package/src/presentation/OutboundRouter.ts +28 -3
- package/src/proxy/ProxySessionController.ts +1707 -70
- package/src/runtime/types.ts +1 -0
- package/src/startup/StartupNotifier.ts +67 -17
- package/src/startup/startFeishu.ts +5 -3
- package/src/state/StateStore.ts +413 -65
- package/src/state/migrations.ts +26 -0
- package/src/supervision/SafeRestartNotifier.ts +17 -17
- package/src/supervision/SafeRestartScheduler.ts +13 -6
- package/src/supervision/replacementSupervisor.ts +50 -4
- package/src/supervisor.ts +35 -16
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
export const SELF_UPDATE_PLAN_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
export interface SelfUpdatePlan {
|
|
10
|
+
schemaVersion: typeof SELF_UPDATE_PLAN_VERSION;
|
|
11
|
+
packageName: string;
|
|
12
|
+
fromVersion: string;
|
|
13
|
+
toVersion: string;
|
|
14
|
+
packageRoot: string;
|
|
15
|
+
candidateTarball: string;
|
|
16
|
+
backupTarball: string;
|
|
17
|
+
backupPackageRoot: string;
|
|
18
|
+
lockPath: string;
|
|
19
|
+
lockToken: string;
|
|
20
|
+
pendingMarkerPath: string;
|
|
21
|
+
databasePath: string;
|
|
22
|
+
controlEndpoint: string;
|
|
23
|
+
workingDirectory: string;
|
|
24
|
+
restartService: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
notificationSessionId?: string;
|
|
27
|
+
resultPath: string;
|
|
28
|
+
logPath: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SelfUpdateResult {
|
|
32
|
+
status: "updated" | "rolled-back" | "failed";
|
|
33
|
+
fromVersion: string;
|
|
34
|
+
toVersion: string;
|
|
35
|
+
activeVersion?: string;
|
|
36
|
+
initialized?: boolean;
|
|
37
|
+
serviceReady?: boolean;
|
|
38
|
+
fallback?: "npm" | "backup";
|
|
39
|
+
error?: string;
|
|
40
|
+
rollbackError?: string;
|
|
41
|
+
completedAt: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CommandResult {
|
|
45
|
+
status: number;
|
|
46
|
+
stdout: string;
|
|
47
|
+
stderr: string;
|
|
48
|
+
error?: Error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DatabaseBackup {
|
|
52
|
+
directory: string;
|
|
53
|
+
entries: Array<{ sourcePath: string; backupPath: string; existed: boolean }>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SelfUpdateRunnerDependencies {
|
|
57
|
+
runNpm(args: string[], cwd: string): CommandResult;
|
|
58
|
+
validatePackage(
|
|
59
|
+
packageRoot: string,
|
|
60
|
+
packageName: string,
|
|
61
|
+
version: string,
|
|
62
|
+
): void;
|
|
63
|
+
initializeProfile(packageRoot: string, cwd: string): CommandResult;
|
|
64
|
+
startSupervisor(packageRoot: string, cwd: string): number | undefined;
|
|
65
|
+
waitForServer(endpoint: string, timeoutMs: number): Promise<boolean>;
|
|
66
|
+
stopServer(endpoint: string, supervisorPid?: number): Promise<void>;
|
|
67
|
+
waitForProcesses(pids: number[], timeoutMs: number): Promise<void>;
|
|
68
|
+
now(): Date;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function applySelfUpdatePlan(
|
|
72
|
+
plan: SelfUpdatePlan,
|
|
73
|
+
waitPids: number[] = [],
|
|
74
|
+
dependencies: SelfUpdateRunnerDependencies = defaultDependencies,
|
|
75
|
+
): Promise<SelfUpdateResult> {
|
|
76
|
+
validatePlan(plan);
|
|
77
|
+
assertUpdateLock(plan);
|
|
78
|
+
markUpdateRunning(plan);
|
|
79
|
+
fs.mkdirSync(path.dirname(plan.logPath), { recursive: true });
|
|
80
|
+
const log = (event: string, data: Record<string, unknown> = {}): void => {
|
|
81
|
+
fs.appendFileSync(
|
|
82
|
+
plan.logPath,
|
|
83
|
+
`${JSON.stringify({
|
|
84
|
+
timestamp: dependencies.now().toISOString(),
|
|
85
|
+
event,
|
|
86
|
+
...data,
|
|
87
|
+
})}\n`,
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
log("update_started", {
|
|
93
|
+
fromVersion: plan.fromVersion,
|
|
94
|
+
toVersion: plan.toVersion,
|
|
95
|
+
waitPids,
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
await dependencies.waitForProcesses(waitPids, 60_000);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return recoverBeforeInstallation(
|
|
101
|
+
plan,
|
|
102
|
+
errorMessage(error),
|
|
103
|
+
dependencies,
|
|
104
|
+
log,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let databaseBackup: DatabaseBackup;
|
|
109
|
+
try {
|
|
110
|
+
databaseBackup = createDatabaseBackup(plan);
|
|
111
|
+
log("database_backup_created", {
|
|
112
|
+
directory: databaseBackup.directory,
|
|
113
|
+
files: databaseBackup.entries.filter((entry) => entry.existed).length,
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return recoverBeforeInstallation(
|
|
117
|
+
plan,
|
|
118
|
+
`Could not back up Agent Bot state: ${errorMessage(error)}`,
|
|
119
|
+
dependencies,
|
|
120
|
+
log,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let startedSupervisorPid: number | undefined;
|
|
125
|
+
try {
|
|
126
|
+
installTarball(
|
|
127
|
+
plan.candidateTarball,
|
|
128
|
+
plan.workingDirectory,
|
|
129
|
+
dependencies,
|
|
130
|
+
log,
|
|
131
|
+
);
|
|
132
|
+
dependencies.validatePackage(
|
|
133
|
+
plan.packageRoot,
|
|
134
|
+
plan.packageName,
|
|
135
|
+
plan.toVersion,
|
|
136
|
+
);
|
|
137
|
+
initializeUpdatedProfile(plan, dependencies, log);
|
|
138
|
+
if (plan.restartService) {
|
|
139
|
+
startedSupervisorPid = dependencies.startSupervisor(
|
|
140
|
+
plan.packageRoot,
|
|
141
|
+
plan.workingDirectory,
|
|
142
|
+
);
|
|
143
|
+
if (!(await dependencies.waitForServer(plan.controlEndpoint, 60_000))) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"The updated Agent Bot service did not become ready within 60 seconds.",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const result: SelfUpdateResult = {
|
|
150
|
+
status: "updated",
|
|
151
|
+
fromVersion: plan.fromVersion,
|
|
152
|
+
toVersion: plan.toVersion,
|
|
153
|
+
activeVersion: plan.toVersion,
|
|
154
|
+
initialized: true,
|
|
155
|
+
...(plan.restartService ? { serviceReady: true } : {}),
|
|
156
|
+
completedAt: dependencies.now().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
writeResult(plan.resultPath, result);
|
|
159
|
+
log("update_completed", { ...result });
|
|
160
|
+
return result;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const message = errorMessage(error);
|
|
163
|
+
log("update_failed", { error: message });
|
|
164
|
+
if (plan.restartService) {
|
|
165
|
+
await dependencies
|
|
166
|
+
.stopServer(plan.controlEndpoint, startedSupervisorPid)
|
|
167
|
+
.catch((stopError: unknown) => {
|
|
168
|
+
log("updated_service_stop_failed", {
|
|
169
|
+
error: errorMessage(stopError),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return rollback(plan, message, databaseBackup, dependencies, log);
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
releaseUpdateLock(plan);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function recoverBeforeInstallation(
|
|
181
|
+
plan: SelfUpdatePlan,
|
|
182
|
+
waitError: string,
|
|
183
|
+
dependencies: SelfUpdateRunnerDependencies,
|
|
184
|
+
log: (event: string, data?: Record<string, unknown>) => void,
|
|
185
|
+
): Promise<SelfUpdateResult> {
|
|
186
|
+
log("update_aborted_before_install", { error: waitError });
|
|
187
|
+
if (plan.restartService) {
|
|
188
|
+
let ready = await dependencies.waitForServer(plan.controlEndpoint, 2_000);
|
|
189
|
+
if (!ready) {
|
|
190
|
+
process.env.AGENT_BOT_RESTART_REASON = `Agent Bot update aborted; running backup ${plan.fromVersion}`;
|
|
191
|
+
dependencies.validatePackage(
|
|
192
|
+
plan.backupPackageRoot,
|
|
193
|
+
plan.packageName,
|
|
194
|
+
plan.fromVersion,
|
|
195
|
+
);
|
|
196
|
+
dependencies.startSupervisor(
|
|
197
|
+
plan.backupPackageRoot,
|
|
198
|
+
plan.workingDirectory,
|
|
199
|
+
);
|
|
200
|
+
ready = await dependencies.waitForServer(plan.controlEndpoint, 60_000);
|
|
201
|
+
}
|
|
202
|
+
if (ready) {
|
|
203
|
+
const result: SelfUpdateResult = {
|
|
204
|
+
status: "rolled-back",
|
|
205
|
+
fromVersion: plan.fromVersion,
|
|
206
|
+
toVersion: plan.toVersion,
|
|
207
|
+
activeVersion: plan.fromVersion,
|
|
208
|
+
serviceReady: true,
|
|
209
|
+
fallback: "backup",
|
|
210
|
+
error: waitError,
|
|
211
|
+
completedAt: dependencies.now().toISOString(),
|
|
212
|
+
};
|
|
213
|
+
writeResult(plan.resultPath, result);
|
|
214
|
+
log("preinstall_recovery_completed", { ...result });
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const result: SelfUpdateResult = {
|
|
219
|
+
status: "failed",
|
|
220
|
+
fromVersion: plan.fromVersion,
|
|
221
|
+
toVersion: plan.toVersion,
|
|
222
|
+
error: waitError,
|
|
223
|
+
completedAt: dependencies.now().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
writeResult(plan.resultPath, result);
|
|
226
|
+
log("preinstall_recovery_failed", { ...result });
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function rollback(
|
|
231
|
+
plan: SelfUpdatePlan,
|
|
232
|
+
updateError: string,
|
|
233
|
+
databaseBackup: DatabaseBackup,
|
|
234
|
+
dependencies: SelfUpdateRunnerDependencies,
|
|
235
|
+
log: (event: string, data?: Record<string, unknown>) => void,
|
|
236
|
+
): Promise<SelfUpdateResult> {
|
|
237
|
+
let rollbackError: string | undefined;
|
|
238
|
+
try {
|
|
239
|
+
installTarball(
|
|
240
|
+
plan.backupTarball,
|
|
241
|
+
plan.workingDirectory,
|
|
242
|
+
dependencies,
|
|
243
|
+
log,
|
|
244
|
+
);
|
|
245
|
+
dependencies.validatePackage(
|
|
246
|
+
plan.packageRoot,
|
|
247
|
+
plan.packageName,
|
|
248
|
+
plan.fromVersion,
|
|
249
|
+
);
|
|
250
|
+
restoreDatabase(databaseBackup);
|
|
251
|
+
if (plan.restartService) {
|
|
252
|
+
process.env.AGENT_BOT_RESTART_REASON = `Agent Bot update failed; restored ${plan.fromVersion}`;
|
|
253
|
+
dependencies.startSupervisor(plan.packageRoot, plan.workingDirectory);
|
|
254
|
+
if (!(await dependencies.waitForServer(plan.controlEndpoint, 60_000))) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"The restored Agent Bot service did not become ready within 60 seconds.",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const result: SelfUpdateResult = {
|
|
261
|
+
status: "rolled-back",
|
|
262
|
+
fromVersion: plan.fromVersion,
|
|
263
|
+
toVersion: plan.toVersion,
|
|
264
|
+
activeVersion: plan.fromVersion,
|
|
265
|
+
...(plan.restartService ? { serviceReady: true } : {}),
|
|
266
|
+
fallback: "npm",
|
|
267
|
+
error: updateError,
|
|
268
|
+
completedAt: dependencies.now().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
writeResult(plan.resultPath, result);
|
|
271
|
+
log("rollback_completed", { ...result });
|
|
272
|
+
return result;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
rollbackError = errorMessage(error);
|
|
275
|
+
log("npm_rollback_failed", { error: rollbackError });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (plan.restartService) {
|
|
279
|
+
try {
|
|
280
|
+
dependencies.validatePackage(
|
|
281
|
+
plan.backupPackageRoot,
|
|
282
|
+
plan.packageName,
|
|
283
|
+
plan.fromVersion,
|
|
284
|
+
);
|
|
285
|
+
restoreDatabase(databaseBackup);
|
|
286
|
+
process.env.AGENT_BOT_RESTART_REASON = `Agent Bot update failed; running backup ${plan.fromVersion}`;
|
|
287
|
+
dependencies.startSupervisor(
|
|
288
|
+
plan.backupPackageRoot,
|
|
289
|
+
plan.workingDirectory,
|
|
290
|
+
);
|
|
291
|
+
if (!(await dependencies.waitForServer(plan.controlEndpoint, 60_000))) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"The backup Agent Bot service did not become ready within 60 seconds.",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const result: SelfUpdateResult = {
|
|
297
|
+
status: "rolled-back",
|
|
298
|
+
fromVersion: plan.fromVersion,
|
|
299
|
+
toVersion: plan.toVersion,
|
|
300
|
+
activeVersion: plan.fromVersion,
|
|
301
|
+
serviceReady: true,
|
|
302
|
+
fallback: "backup",
|
|
303
|
+
error: updateError,
|
|
304
|
+
rollbackError,
|
|
305
|
+
completedAt: dependencies.now().toISOString(),
|
|
306
|
+
};
|
|
307
|
+
writeResult(plan.resultPath, result);
|
|
308
|
+
log("backup_service_started", { ...result });
|
|
309
|
+
return result;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
rollbackError = `${rollbackError ?? "npm rollback failed"}; backup start failed: ${errorMessage(error)}`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const result: SelfUpdateResult = {
|
|
316
|
+
status: "failed",
|
|
317
|
+
fromVersion: plan.fromVersion,
|
|
318
|
+
toVersion: plan.toVersion,
|
|
319
|
+
error: updateError,
|
|
320
|
+
rollbackError,
|
|
321
|
+
completedAt: dependencies.now().toISOString(),
|
|
322
|
+
};
|
|
323
|
+
writeResult(plan.resultPath, result);
|
|
324
|
+
log("rollback_failed", { ...result });
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function installTarball(
|
|
329
|
+
tarball: string,
|
|
330
|
+
cwd: string,
|
|
331
|
+
dependencies: SelfUpdateRunnerDependencies,
|
|
332
|
+
log: (event: string, data?: Record<string, unknown>) => void,
|
|
333
|
+
): void {
|
|
334
|
+
const result = dependencies.runNpm(
|
|
335
|
+
["install", "--global", "--no-audit", "--no-fund", tarball],
|
|
336
|
+
cwd,
|
|
337
|
+
);
|
|
338
|
+
log("npm_install_finished", {
|
|
339
|
+
tarball,
|
|
340
|
+
status: result.status,
|
|
341
|
+
stderr: result.stderr.trim().slice(-4_000),
|
|
342
|
+
});
|
|
343
|
+
if (result.error) throw result.error;
|
|
344
|
+
if (result.status !== 0) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
result.stderr.trim() ||
|
|
347
|
+
result.stdout.trim() ||
|
|
348
|
+
`npm exited with ${result.status}.`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function initializeUpdatedProfile(
|
|
354
|
+
plan: SelfUpdatePlan,
|
|
355
|
+
dependencies: SelfUpdateRunnerDependencies,
|
|
356
|
+
log: (event: string, data?: Record<string, unknown>) => void,
|
|
357
|
+
): void {
|
|
358
|
+
const result = dependencies.initializeProfile(plan.packageRoot, plan.workingDirectory);
|
|
359
|
+
log("profile_initialization_finished", {
|
|
360
|
+
status: result.status,
|
|
361
|
+
stderr: result.stderr.trim().slice(-4_000),
|
|
362
|
+
});
|
|
363
|
+
if (result.error) throw result.error;
|
|
364
|
+
if (result.status !== 0) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
result.stderr.trim()
|
|
367
|
+
|| result.stdout.trim()
|
|
368
|
+
|| `agentbot init exited with ${result.status}.`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function validateInstalledPackage(
|
|
374
|
+
packageRoot: string,
|
|
375
|
+
packageName: string,
|
|
376
|
+
version: string,
|
|
377
|
+
): void {
|
|
378
|
+
const metadata = JSON.parse(
|
|
379
|
+
fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"),
|
|
380
|
+
) as {
|
|
381
|
+
name?: unknown;
|
|
382
|
+
version?: unknown;
|
|
383
|
+
};
|
|
384
|
+
if (metadata.name !== packageName || metadata.version !== version) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`Installed package identity mismatch: expected ${packageName}@${version}, got ${String(metadata.name)}@${String(metadata.version)}.`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const cliEntry = path.join(packageRoot, "dist", "cli.js");
|
|
390
|
+
const supervisorEntry = path.join(packageRoot, "dist", "supervisor.js");
|
|
391
|
+
for (const required of [cliEntry, supervisorEntry]) {
|
|
392
|
+
if (!fs.existsSync(required))
|
|
393
|
+
throw new Error(`Updated package is missing ${required}.`);
|
|
394
|
+
}
|
|
395
|
+
const versionResult = runNode([cliEntry, "--version"], packageRoot);
|
|
396
|
+
if (versionResult.status !== 0 || versionResult.stdout.trim() !== version) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Updated CLI validation failed: ${versionResult.stderr.trim() || versionResult.stdout.trim()}.`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const helpResult = runNode([cliEntry, "--help"], packageRoot);
|
|
402
|
+
if (helpResult.status !== 0 || !helpResult.stdout.includes("agentbot")) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`Updated CLI help validation failed: ${helpResult.stderr.trim() || helpResult.stdout.trim()}.`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function startSupervisor(packageRoot: string, cwd: string): number | undefined {
|
|
410
|
+
const child = spawn(
|
|
411
|
+
process.execPath,
|
|
412
|
+
[path.join(packageRoot, "dist", "supervisor.js")],
|
|
413
|
+
{
|
|
414
|
+
cwd,
|
|
415
|
+
detached: true,
|
|
416
|
+
windowsHide: true,
|
|
417
|
+
stdio: "ignore",
|
|
418
|
+
env: process.env,
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
child.unref();
|
|
422
|
+
return child.pid;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function initializeProfile(packageRoot: string, cwd: string): CommandResult {
|
|
426
|
+
return runNode([
|
|
427
|
+
path.join(packageRoot, "dist", "cli.js"),
|
|
428
|
+
"init",
|
|
429
|
+
"--skip-feishu",
|
|
430
|
+
"--json",
|
|
431
|
+
], cwd);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function waitForServer(
|
|
435
|
+
endpoint: string,
|
|
436
|
+
timeoutMs: number,
|
|
437
|
+
): Promise<boolean> {
|
|
438
|
+
const deadline = Date.now() + timeoutMs;
|
|
439
|
+
while (Date.now() < deadline) {
|
|
440
|
+
const response = await sendControlRequest(
|
|
441
|
+
endpoint,
|
|
442
|
+
{ action: "health" },
|
|
443
|
+
1_000,
|
|
444
|
+
).catch(() => undefined);
|
|
445
|
+
if (
|
|
446
|
+
response?.ok &&
|
|
447
|
+
isRecord(response.data) &&
|
|
448
|
+
response.data.ready !== false
|
|
449
|
+
)
|
|
450
|
+
return true;
|
|
451
|
+
await delay(500);
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function stopServer(
|
|
457
|
+
endpoint: string,
|
|
458
|
+
supervisorPid?: number,
|
|
459
|
+
): Promise<void> {
|
|
460
|
+
await sendControlRequest(endpoint, { action: "server_stop" }, 2_000).catch(
|
|
461
|
+
() => undefined,
|
|
462
|
+
);
|
|
463
|
+
const deadline = Date.now() + 10_000;
|
|
464
|
+
while (Date.now() < deadline) {
|
|
465
|
+
if (!(await endpointReachable(endpoint))) break;
|
|
466
|
+
await delay(250);
|
|
467
|
+
}
|
|
468
|
+
if (supervisorPid) {
|
|
469
|
+
try {
|
|
470
|
+
process.kill(supervisorPid, "SIGTERM");
|
|
471
|
+
} catch {
|
|
472
|
+
// The Supervisor may already have exited.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (supervisorPid) {
|
|
476
|
+
const processDeadline = Date.now() + 5_000;
|
|
477
|
+
while (processExists(supervisorPid) && Date.now() < processDeadline)
|
|
478
|
+
await delay(250);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function waitForProcesses(
|
|
483
|
+
pids: number[],
|
|
484
|
+
timeoutMs: number,
|
|
485
|
+
): Promise<void> {
|
|
486
|
+
const remaining = new Set(
|
|
487
|
+
pids.filter((pid) => Number.isInteger(pid) && pid > 0),
|
|
488
|
+
);
|
|
489
|
+
const deadline = Date.now() + timeoutMs;
|
|
490
|
+
while (remaining.size > 0 && Date.now() < deadline) {
|
|
491
|
+
for (const pid of remaining) {
|
|
492
|
+
if (!processExists(pid)) remaining.delete(pid);
|
|
493
|
+
}
|
|
494
|
+
if (remaining.size > 0) await delay(250);
|
|
495
|
+
}
|
|
496
|
+
if (remaining.size > 0) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`Timed out waiting for Agent Bot processes to exit: ${[...remaining].join(", ")}.`,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function processExists(pid: number): boolean {
|
|
504
|
+
try {
|
|
505
|
+
process.kill(pid, 0);
|
|
506
|
+
return true;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
return isRecord(error) && error.code === "EPERM";
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function sendControlRequest(
|
|
513
|
+
endpoint: string,
|
|
514
|
+
request: Record<string, unknown>,
|
|
515
|
+
timeoutMs: number,
|
|
516
|
+
): Promise<{ ok?: boolean; data?: unknown }> {
|
|
517
|
+
return new Promise((resolve, reject) => {
|
|
518
|
+
const socket = net.createConnection(endpoint);
|
|
519
|
+
let settled = false;
|
|
520
|
+
let input = "";
|
|
521
|
+
const finish = (operation: () => void): void => {
|
|
522
|
+
if (settled) return;
|
|
523
|
+
settled = true;
|
|
524
|
+
clearTimeout(timer);
|
|
525
|
+
socket.destroy();
|
|
526
|
+
operation();
|
|
527
|
+
};
|
|
528
|
+
const timer = setTimeout(
|
|
529
|
+
() => finish(() => reject(new Error("Control request timed out."))),
|
|
530
|
+
timeoutMs,
|
|
531
|
+
);
|
|
532
|
+
socket.setEncoding("utf8");
|
|
533
|
+
socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
534
|
+
socket.on("data", (chunk: string) => {
|
|
535
|
+
input += chunk;
|
|
536
|
+
const newline = input.indexOf("\n");
|
|
537
|
+
if (newline < 0) return;
|
|
538
|
+
try {
|
|
539
|
+
finish(() =>
|
|
540
|
+
resolve(
|
|
541
|
+
JSON.parse(input.slice(0, newline)) as {
|
|
542
|
+
ok?: boolean;
|
|
543
|
+
data?: unknown;
|
|
544
|
+
},
|
|
545
|
+
),
|
|
546
|
+
);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
finish(() => reject(error));
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
socket.once("error", (error) => finish(() => reject(error)));
|
|
552
|
+
socket.once("end", () =>
|
|
553
|
+
finish(() => reject(new Error("Control endpoint returned no response."))),
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function endpointReachable(endpoint: string): Promise<boolean> {
|
|
559
|
+
return sendControlRequest(endpoint, { action: "health" }, 500).then(
|
|
560
|
+
() => true,
|
|
561
|
+
() => false,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function runNpmCommand(args: string[], cwd: string): CommandResult {
|
|
566
|
+
const npmCli = resolveNpmCli();
|
|
567
|
+
if (npmCli) return runNode([npmCli, ...args], cwd);
|
|
568
|
+
const result = spawnSync(
|
|
569
|
+
process.platform === "win32" ? "npm.cmd" : "npm",
|
|
570
|
+
args,
|
|
571
|
+
{
|
|
572
|
+
cwd,
|
|
573
|
+
env: process.env,
|
|
574
|
+
encoding: "utf8",
|
|
575
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
576
|
+
shell: process.platform === "win32",
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
return commandResult(result);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function resolveNpmCli(): string | undefined {
|
|
583
|
+
const candidates = [
|
|
584
|
+
process.env.npm_execpath,
|
|
585
|
+
path.join(
|
|
586
|
+
path.dirname(process.execPath),
|
|
587
|
+
"node_modules",
|
|
588
|
+
"npm",
|
|
589
|
+
"bin",
|
|
590
|
+
"npm-cli.js",
|
|
591
|
+
),
|
|
592
|
+
process.env.APPDATA
|
|
593
|
+
? path.join(
|
|
594
|
+
process.env.APPDATA,
|
|
595
|
+
"npm",
|
|
596
|
+
"node_modules",
|
|
597
|
+
"npm",
|
|
598
|
+
"bin",
|
|
599
|
+
"npm-cli.js",
|
|
600
|
+
)
|
|
601
|
+
: undefined,
|
|
602
|
+
];
|
|
603
|
+
return candidates.find((candidate): candidate is string =>
|
|
604
|
+
Boolean(candidate && fs.existsSync(candidate)),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function runNode(args: string[], cwd: string): CommandResult {
|
|
609
|
+
return commandResult(
|
|
610
|
+
spawnSync(process.execPath, args, {
|
|
611
|
+
cwd,
|
|
612
|
+
env: process.env,
|
|
613
|
+
encoding: "utf8",
|
|
614
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function commandResult(result: ReturnType<typeof spawnSync>): CommandResult {
|
|
620
|
+
return {
|
|
621
|
+
status: result.status ?? (result.error ? 1 : 0),
|
|
622
|
+
stdout:
|
|
623
|
+
typeof result.stdout === "string"
|
|
624
|
+
? result.stdout
|
|
625
|
+
: (result.stdout?.toString("utf8") ?? ""),
|
|
626
|
+
stderr:
|
|
627
|
+
typeof result.stderr === "string"
|
|
628
|
+
? result.stderr
|
|
629
|
+
: (result.stderr?.toString("utf8") ?? ""),
|
|
630
|
+
...(result.error ? { error: result.error } : {}),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function validatePlan(plan: SelfUpdatePlan): void {
|
|
635
|
+
if (plan.schemaVersion !== SELF_UPDATE_PLAN_VERSION)
|
|
636
|
+
throw new Error("Unsupported Agent Bot update plan.");
|
|
637
|
+
for (const value of [
|
|
638
|
+
plan.packageName,
|
|
639
|
+
plan.fromVersion,
|
|
640
|
+
plan.toVersion,
|
|
641
|
+
plan.packageRoot,
|
|
642
|
+
plan.candidateTarball,
|
|
643
|
+
plan.backupTarball,
|
|
644
|
+
plan.backupPackageRoot,
|
|
645
|
+
plan.lockPath,
|
|
646
|
+
plan.lockToken,
|
|
647
|
+
plan.pendingMarkerPath,
|
|
648
|
+
plan.databasePath,
|
|
649
|
+
plan.controlEndpoint,
|
|
650
|
+
plan.workingDirectory,
|
|
651
|
+
plan.resultPath,
|
|
652
|
+
plan.logPath,
|
|
653
|
+
]) {
|
|
654
|
+
if (!value?.trim()) throw new Error("Agent Bot update plan is incomplete.");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function createDatabaseBackup(plan: SelfUpdatePlan): DatabaseBackup {
|
|
659
|
+
const directory = path.join(path.dirname(plan.resultPath), "database-backup");
|
|
660
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
661
|
+
const sourcePaths = [
|
|
662
|
+
plan.databasePath,
|
|
663
|
+
`${plan.databasePath}-wal`,
|
|
664
|
+
`${plan.databasePath}-shm`,
|
|
665
|
+
];
|
|
666
|
+
const entries = sourcePaths.map((sourcePath, index) => {
|
|
667
|
+
const existed = fs.existsSync(sourcePath);
|
|
668
|
+
const backupPath = path.join(
|
|
669
|
+
directory,
|
|
670
|
+
`${index}-${path.basename(sourcePath)}`,
|
|
671
|
+
);
|
|
672
|
+
if (existed) fs.copyFileSync(sourcePath, backupPath);
|
|
673
|
+
return { sourcePath, backupPath, existed };
|
|
674
|
+
});
|
|
675
|
+
fs.writeFileSync(
|
|
676
|
+
path.join(directory, "manifest.json"),
|
|
677
|
+
`${JSON.stringify({ entries }, null, 2)}\n`,
|
|
678
|
+
);
|
|
679
|
+
return { directory, entries };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function restoreDatabase(backup: DatabaseBackup): void {
|
|
683
|
+
for (const entry of backup.entries) {
|
|
684
|
+
fs.rmSync(entry.sourcePath, { force: true });
|
|
685
|
+
if (!entry.existed) continue;
|
|
686
|
+
fs.mkdirSync(path.dirname(entry.sourcePath), { recursive: true });
|
|
687
|
+
fs.copyFileSync(entry.backupPath, entry.sourcePath);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function assertUpdateLock(plan: SelfUpdatePlan): void {
|
|
692
|
+
const lock = JSON.parse(fs.readFileSync(plan.lockPath, "utf8")) as {
|
|
693
|
+
token?: unknown;
|
|
694
|
+
};
|
|
695
|
+
if (lock.token !== plan.lockToken)
|
|
696
|
+
throw new Error("Agent Bot update lock is not owned by this operation.");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function releaseUpdateLock(plan: SelfUpdatePlan): void {
|
|
700
|
+
try {
|
|
701
|
+
const lock = JSON.parse(fs.readFileSync(plan.lockPath, "utf8")) as {
|
|
702
|
+
token?: unknown;
|
|
703
|
+
};
|
|
704
|
+
if (lock.token === plan.lockToken)
|
|
705
|
+
fs.rmSync(plan.lockPath, { force: true });
|
|
706
|
+
} catch {
|
|
707
|
+
// A missing or replaced lock belongs to no recoverable update operation.
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const marker = JSON.parse(
|
|
711
|
+
fs.readFileSync(plan.pendingMarkerPath, "utf8"),
|
|
712
|
+
) as { token?: unknown };
|
|
713
|
+
if (marker.token === plan.lockToken)
|
|
714
|
+
fs.rmSync(plan.pendingMarkerPath, { force: true });
|
|
715
|
+
} catch {
|
|
716
|
+
// A missing or replaced marker belongs to no recoverable update operation.
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function markUpdateRunning(plan: SelfUpdatePlan): void {
|
|
721
|
+
fs.writeFileSync(
|
|
722
|
+
plan.pendingMarkerPath,
|
|
723
|
+
`${JSON.stringify(
|
|
724
|
+
{
|
|
725
|
+
token: plan.lockToken,
|
|
726
|
+
phase: "running",
|
|
727
|
+
planPath: path.join(path.dirname(plan.resultPath), "plan.json"),
|
|
728
|
+
runnerPid: process.pid,
|
|
729
|
+
startedAt: new Date().toISOString(),
|
|
730
|
+
},
|
|
731
|
+
null,
|
|
732
|
+
2,
|
|
733
|
+
)}\n`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function writeResult(resultPath: string, result: SelfUpdateResult): void {
|
|
738
|
+
fs.writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function readPlan(planPath: string): SelfUpdatePlan {
|
|
742
|
+
return JSON.parse(fs.readFileSync(planPath, "utf8")) as SelfUpdatePlan;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function writeFatalResult(
|
|
746
|
+
plan: SelfUpdatePlan | undefined,
|
|
747
|
+
error: unknown,
|
|
748
|
+
): void {
|
|
749
|
+
if (!plan) return;
|
|
750
|
+
const result: SelfUpdateResult = {
|
|
751
|
+
status: "failed",
|
|
752
|
+
fromVersion: plan.fromVersion,
|
|
753
|
+
toVersion: plan.toVersion,
|
|
754
|
+
error: errorMessage(error),
|
|
755
|
+
completedAt: new Date().toISOString(),
|
|
756
|
+
};
|
|
757
|
+
try {
|
|
758
|
+
writeResult(plan.resultPath, result);
|
|
759
|
+
} catch {
|
|
760
|
+
// There is no further recovery path if even the result file cannot be written.
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function parseWaitPids(args: string[]): number[] {
|
|
765
|
+
const pids: number[] = [];
|
|
766
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
767
|
+
if (args[index] !== "--wait-pid") continue;
|
|
768
|
+
const pid = Number(args[index + 1]);
|
|
769
|
+
if (Number.isInteger(pid) && pid > 0) pids.push(pid);
|
|
770
|
+
index += 1;
|
|
771
|
+
}
|
|
772
|
+
return pids;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
776
|
+
return Boolean(value) && typeof value === "object";
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function errorMessage(error: unknown): string {
|
|
780
|
+
return error instanceof Error ? error.message : String(error);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function delay(ms: number): Promise<void> {
|
|
784
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const defaultDependencies: SelfUpdateRunnerDependencies = {
|
|
788
|
+
runNpm: runNpmCommand,
|
|
789
|
+
validatePackage: validateInstalledPackage,
|
|
790
|
+
initializeProfile,
|
|
791
|
+
startSupervisor,
|
|
792
|
+
waitForServer,
|
|
793
|
+
stopServer,
|
|
794
|
+
waitForProcesses,
|
|
795
|
+
now: () => new Date(),
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const invokedDirectly =
|
|
799
|
+
process.argv[1] &&
|
|
800
|
+
path.resolve(process.argv[1]) ===
|
|
801
|
+
path.resolve(fileURLToPath(import.meta.url));
|
|
802
|
+
if (invokedDirectly) {
|
|
803
|
+
const planPath = process.argv[2];
|
|
804
|
+
let plan: SelfUpdatePlan | undefined;
|
|
805
|
+
void (async () => {
|
|
806
|
+
if (!planPath) throw new Error("Missing Agent Bot update plan path.");
|
|
807
|
+
plan = readPlan(planPath);
|
|
808
|
+
const result = await applySelfUpdatePlan(
|
|
809
|
+
plan,
|
|
810
|
+
parseWaitPids(process.argv.slice(3)),
|
|
811
|
+
);
|
|
812
|
+
if (result.status === "failed") process.exitCode = 1;
|
|
813
|
+
})().catch((error: unknown) => {
|
|
814
|
+
writeFatalResult(plan, error);
|
|
815
|
+
process.exitCode = 1;
|
|
816
|
+
});
|
|
817
|
+
}
|