@keyou007/agent-bot 0.1.14-alpha.1 → 0.1.14-alpha.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/CHANGELOG.md +19 -1
- package/README.md +48 -15
- package/README.zh.md +48 -15
- package/config.example.yaml +1 -1
- package/dist/cli/AutostartManager.js +431 -0
- package/dist/cli/AutostartManager.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +21 -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 +94 -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 +661 -58
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +8 -2
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +16 -1
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +266 -38
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +25 -1
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +16 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +42 -6
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +136 -2
- package/dist/index.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +7 -0
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +21 -7
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +537 -13
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +53 -0
- package/dist/state/StateStore.js.map +1 -1
- package/docs/technical-reference.md +17 -7
- package/docs/technical-reference.zh.md +17 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/skills/agent-bot/SKILL.md +109 -188
- package/src/cli/AutostartManager.ts +579 -0
- package/src/cli/FeishuAppConfiguration.ts +26 -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 +119 -1
- package/src/cli/help.ts +94 -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 +809 -65
- package/src/codex/CodexRuntime.ts +8 -2
- package/src/commands/CommandRouter.ts +14 -1
- package/src/commands/commandTypes.ts +2 -0
- package/src/feishu/CardRenderer.ts +313 -35
- package/src/feishu/FeishuMarkdown.ts +27 -1
- package/src/feishu/FeishuMessageClient.ts +33 -0
- package/src/feishu/FeishuTurnPresenter.ts +43 -6
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +174 -2
- package/src/presentation/OutboundRouter.ts +7 -0
- package/src/presentation/TurnStateReducer.ts +21 -7
- package/src/proxy/ProxySessionController.ts +661 -16
- package/src/runtime/types.ts +1 -0
- package/src/state/StateStore.ts +56 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import type { AppConfig } from "../config/schema.js";
|
|
8
|
+
import {
|
|
9
|
+
AGENT_BOT_EXPLICIT_PROFILE_ENV,
|
|
10
|
+
AGENT_BOT_HOME_ENV,
|
|
11
|
+
agentBotHome,
|
|
12
|
+
defaultConfigPath,
|
|
13
|
+
resolveUserPath,
|
|
14
|
+
} from "../config/paths.js";
|
|
15
|
+
import {
|
|
16
|
+
nodeDiagnosticReportArguments,
|
|
17
|
+
resolveSupervisorDiagnosticsPaths,
|
|
18
|
+
} from "../supervision/SupervisorDiagnostics.js";
|
|
19
|
+
|
|
20
|
+
export type SupportedAutostartPlatform = "win32" | "darwin" | "linux";
|
|
21
|
+
|
|
22
|
+
export interface AutostartContext {
|
|
23
|
+
profilePath: string;
|
|
24
|
+
configPath: string;
|
|
25
|
+
nodePath: string;
|
|
26
|
+
supervisorPath: string;
|
|
27
|
+
crashReportDirectory: string;
|
|
28
|
+
pathValue?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AutostartPlan {
|
|
32
|
+
platform: SupportedAutostartPlatform;
|
|
33
|
+
name: string;
|
|
34
|
+
profilePath: string;
|
|
35
|
+
configPath: string;
|
|
36
|
+
bootstrapPath: string;
|
|
37
|
+
bootstrapContent: string;
|
|
38
|
+
definitionPath: string;
|
|
39
|
+
definitionContent?: string;
|
|
40
|
+
command: string;
|
|
41
|
+
commandArguments: string[];
|
|
42
|
+
windowsFallbackPath?: string;
|
|
43
|
+
windowsFallbackContent?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AutostartRegistrationStatus {
|
|
47
|
+
supported: boolean;
|
|
48
|
+
platform: NodeJS.Platform;
|
|
49
|
+
profilePath: string;
|
|
50
|
+
configPath: string;
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
loaded?: boolean;
|
|
53
|
+
linger?: boolean;
|
|
54
|
+
name?: string;
|
|
55
|
+
definitionPath?: string;
|
|
56
|
+
trigger?: "login" | "boot";
|
|
57
|
+
mechanism?: "task-scheduler" | "startup-folder" | "launch-agent" | "systemd";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface EnableAutostartOptions {
|
|
61
|
+
linger?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface CommandResult {
|
|
65
|
+
status: number | null;
|
|
66
|
+
stdout: string;
|
|
67
|
+
stderr: string;
|
|
68
|
+
error?: Error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface AutostartDependencies {
|
|
72
|
+
platform: NodeJS.Platform;
|
|
73
|
+
homeDirectory: string;
|
|
74
|
+
username: string;
|
|
75
|
+
uid?: number;
|
|
76
|
+
appDataDirectory?: string;
|
|
77
|
+
run(command: string, args: string[]): CommandResult;
|
|
78
|
+
exists(filePath: string): boolean;
|
|
79
|
+
write(filePath: string, content: string): void;
|
|
80
|
+
remove(filePath: string): void;
|
|
81
|
+
removeDirectoryIfEmpty(directoryPath: string): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type AutostartDependencyOverrides = Partial<AutostartDependencies>;
|
|
85
|
+
|
|
86
|
+
export class AutostartManager {
|
|
87
|
+
readonly plan?: AutostartPlan;
|
|
88
|
+
private readonly dependencies: AutostartDependencies;
|
|
89
|
+
|
|
90
|
+
constructor(
|
|
91
|
+
private readonly context: AutostartContext,
|
|
92
|
+
overrides: AutostartDependencyOverrides = {},
|
|
93
|
+
) {
|
|
94
|
+
this.dependencies = { ...defaultDependencies(), ...overrides };
|
|
95
|
+
if (isSupportedPlatform(this.dependencies.platform)) {
|
|
96
|
+
this.plan = createAutostartPlan(
|
|
97
|
+
context,
|
|
98
|
+
this.dependencies.platform,
|
|
99
|
+
this.dependencies.homeDirectory,
|
|
100
|
+
this.dependencies.appDataDirectory,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
enable(options: EnableAutostartOptions = {}): AutostartRegistrationStatus {
|
|
106
|
+
const plan = this.requirePlan();
|
|
107
|
+
if (options.linger && plan.platform !== "linux") {
|
|
108
|
+
throw new Error("--linger is supported only on Linux.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.dependencies.write(plan.bootstrapPath, plan.bootstrapContent);
|
|
112
|
+
if (plan.definitionContent !== undefined) {
|
|
113
|
+
this.dependencies.write(plan.definitionPath, plan.definitionContent);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (plan.platform === "win32") {
|
|
117
|
+
const task = this.dependencies.run("schtasks.exe", [
|
|
118
|
+
"/Create",
|
|
119
|
+
"/TN",
|
|
120
|
+
plan.name,
|
|
121
|
+
"/SC",
|
|
122
|
+
"ONLOGON",
|
|
123
|
+
"/TR",
|
|
124
|
+
windowsCommandLine(plan.command, plan.commandArguments),
|
|
125
|
+
"/RL",
|
|
126
|
+
"LIMITED",
|
|
127
|
+
"/F",
|
|
128
|
+
]);
|
|
129
|
+
if (!task.error && task.status === 0) {
|
|
130
|
+
if (plan.windowsFallbackPath) this.dependencies.remove(plan.windowsFallbackPath);
|
|
131
|
+
} else if (plan.windowsFallbackPath && plan.windowsFallbackContent) {
|
|
132
|
+
this.dependencies.write(plan.windowsFallbackPath, plan.windowsFallbackContent);
|
|
133
|
+
} else {
|
|
134
|
+
this.throwCommandError(task, "Could not register the Windows startup task");
|
|
135
|
+
}
|
|
136
|
+
} else if (plan.platform === "linux") {
|
|
137
|
+
this.runChecked("systemctl", ["--user", "daemon-reload"], "Could not reload systemd user units");
|
|
138
|
+
this.runChecked("systemctl", ["--user", "enable", plan.name], "Could not enable the systemd user unit");
|
|
139
|
+
if (options.linger) {
|
|
140
|
+
this.runChecked(
|
|
141
|
+
"loginctl",
|
|
142
|
+
["enable-linger", this.dependencies.username],
|
|
143
|
+
"Could not enable systemd user lingering",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return this.status();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
disable(): AutostartRegistrationStatus {
|
|
152
|
+
const plan = this.requirePlan();
|
|
153
|
+
const current = this.status();
|
|
154
|
+
|
|
155
|
+
if (plan.platform === "win32") {
|
|
156
|
+
const taskRegistered = this.dependencies.run("schtasks.exe", ["/Query", "/TN", plan.name]).status === 0;
|
|
157
|
+
if (taskRegistered) {
|
|
158
|
+
this.runChecked(
|
|
159
|
+
"schtasks.exe",
|
|
160
|
+
["/Delete", "/TN", plan.name, "/F"],
|
|
161
|
+
"Could not remove the Windows startup task",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (plan.windowsFallbackPath) this.dependencies.remove(plan.windowsFallbackPath);
|
|
165
|
+
} else if (plan.platform === "linux") {
|
|
166
|
+
if (current.enabled) {
|
|
167
|
+
this.runChecked(
|
|
168
|
+
"systemctl",
|
|
169
|
+
["--user", "disable", plan.name],
|
|
170
|
+
"Could not disable the systemd user unit",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
this.dependencies.remove(plan.definitionPath);
|
|
174
|
+
this.runChecked("systemctl", ["--user", "daemon-reload"], "Could not reload systemd user units");
|
|
175
|
+
} else if (plan.platform === "darwin") {
|
|
176
|
+
this.dependencies.remove(plan.definitionPath);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.dependencies.remove(plan.bootstrapPath);
|
|
180
|
+
this.dependencies.removeDirectoryIfEmpty(path.dirname(plan.bootstrapPath));
|
|
181
|
+
return this.status();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
status(): AutostartRegistrationStatus {
|
|
185
|
+
const platform = this.dependencies.platform;
|
|
186
|
+
if (!this.plan) {
|
|
187
|
+
return {
|
|
188
|
+
supported: false,
|
|
189
|
+
platform,
|
|
190
|
+
profilePath: this.context.profilePath,
|
|
191
|
+
configPath: this.context.configPath,
|
|
192
|
+
enabled: false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const plan = this.plan;
|
|
197
|
+
if (plan.platform === "win32") {
|
|
198
|
+
const task = this.dependencies.run("schtasks.exe", ["/Query", "/TN", plan.name]);
|
|
199
|
+
const fallback = plan.windowsFallbackPath
|
|
200
|
+
? this.dependencies.exists(plan.windowsFallbackPath)
|
|
201
|
+
: false;
|
|
202
|
+
return this.registrationStatus(task.status === 0 || fallback, {
|
|
203
|
+
mechanism: task.status === 0 ? "task-scheduler" : fallback ? "startup-folder" : undefined,
|
|
204
|
+
definitionPath: fallback ? plan.windowsFallbackPath : plan.definitionPath,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (plan.platform === "darwin") {
|
|
209
|
+
const domain = `gui/${this.dependencies.uid ?? 0}/${plan.name}`;
|
|
210
|
+
const loaded = this.dependencies.run("launchctl", ["print", domain]).status === 0;
|
|
211
|
+
return this.registrationStatus(this.dependencies.exists(plan.definitionPath), {
|
|
212
|
+
loaded,
|
|
213
|
+
mechanism: "launch-agent",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const enabled = this.dependencies.run("systemctl", ["--user", "is-enabled", plan.name]).status === 0;
|
|
218
|
+
const loaded = this.dependencies.run("systemctl", ["--user", "is-active", plan.name]).status === 0;
|
|
219
|
+
const lingerResult = this.dependencies.run("loginctl", [
|
|
220
|
+
"show-user",
|
|
221
|
+
this.dependencies.username,
|
|
222
|
+
"--property=Linger",
|
|
223
|
+
"--value",
|
|
224
|
+
]);
|
|
225
|
+
const linger = lingerResult.status === 0 && lingerResult.stdout.trim().toLowerCase() === "yes";
|
|
226
|
+
return this.registrationStatus(enabled, {
|
|
227
|
+
loaded,
|
|
228
|
+
linger,
|
|
229
|
+
trigger: linger ? "boot" : "login",
|
|
230
|
+
mechanism: "systemd",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private requirePlan(): AutostartPlan {
|
|
235
|
+
if (!this.plan) {
|
|
236
|
+
throw new Error(`Autostart is not supported on ${this.dependencies.platform}.`);
|
|
237
|
+
}
|
|
238
|
+
return this.plan;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private registrationStatus(
|
|
242
|
+
enabled: boolean,
|
|
243
|
+
details: Pick<
|
|
244
|
+
AutostartRegistrationStatus,
|
|
245
|
+
"loaded" | "linger" | "trigger" | "mechanism" | "definitionPath"
|
|
246
|
+
> = {},
|
|
247
|
+
): AutostartRegistrationStatus {
|
|
248
|
+
const plan = this.requirePlan();
|
|
249
|
+
return {
|
|
250
|
+
supported: true,
|
|
251
|
+
platform: plan.platform,
|
|
252
|
+
profilePath: plan.profilePath,
|
|
253
|
+
configPath: plan.configPath,
|
|
254
|
+
enabled,
|
|
255
|
+
name: plan.name,
|
|
256
|
+
definitionPath: details.definitionPath ?? plan.definitionPath,
|
|
257
|
+
trigger: details.trigger ?? "login",
|
|
258
|
+
...(details.mechanism === undefined ? {} : { mechanism: details.mechanism }),
|
|
259
|
+
...(details.loaded === undefined ? {} : { loaded: details.loaded }),
|
|
260
|
+
...(details.linger === undefined ? {} : { linger: details.linger }),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private runChecked(command: string, args: string[], description: string): void {
|
|
265
|
+
const result = this.dependencies.run(command, args);
|
|
266
|
+
if (!result.error && result.status === 0) return;
|
|
267
|
+
this.throwCommandError(result, description);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private throwCommandError(result: CommandResult, description: string): never {
|
|
271
|
+
const detail = result.error?.message || result.stderr.trim() || result.stdout.trim()
|
|
272
|
+
|| `exit code ${result.status ?? "unknown"}`;
|
|
273
|
+
throw new Error(`${description}: ${detail}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function createCurrentAutostartManager(
|
|
278
|
+
config: AppConfig,
|
|
279
|
+
overrides: AutostartDependencyOverrides = {},
|
|
280
|
+
): AutostartManager {
|
|
281
|
+
const configuredPath = process.env.AGENT_BOT_CONFIG?.trim();
|
|
282
|
+
const profilePath = agentBotHome();
|
|
283
|
+
const configPath = configuredPath ? resolveUserPath(configuredPath) : defaultConfigPath();
|
|
284
|
+
return new AutostartManager({
|
|
285
|
+
profilePath,
|
|
286
|
+
configPath,
|
|
287
|
+
nodePath: process.execPath,
|
|
288
|
+
supervisorPath: fileURLToPath(new URL("../supervisor.js", import.meta.url)),
|
|
289
|
+
crashReportDirectory: resolveSupervisorDiagnosticsPaths(config).crashReportDirectory,
|
|
290
|
+
pathValue: process.env.PATH,
|
|
291
|
+
}, overrides);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function createAutostartPlan(
|
|
295
|
+
context: AutostartContext,
|
|
296
|
+
platform: SupportedAutostartPlatform,
|
|
297
|
+
homeDirectory: string,
|
|
298
|
+
appDataDirectory?: string,
|
|
299
|
+
): AutostartPlan {
|
|
300
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
301
|
+
const profilePath = pathApi.resolve(context.profilePath);
|
|
302
|
+
const configPath = pathApi.resolve(context.configPath);
|
|
303
|
+
const bootstrapPath = pathApi.join(profilePath, "autostart", "supervisor-bootstrap.mjs");
|
|
304
|
+
const identity = createHash("sha256")
|
|
305
|
+
.update(`${platform}\0${profilePath}\0${configPath}`)
|
|
306
|
+
.digest("hex")
|
|
307
|
+
.slice(0, 10);
|
|
308
|
+
const bootstrapContent = renderBootstrap({ ...context, profilePath, configPath }, platform);
|
|
309
|
+
const commandArguments = [
|
|
310
|
+
...nodeDiagnosticReportArguments(context.crashReportDirectory),
|
|
311
|
+
bootstrapPath,
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
if (platform === "win32") {
|
|
315
|
+
const profileName = sanitizeName(pathApi.basename(profilePath).replace(/^\.+/, "") || "main");
|
|
316
|
+
const name = `AgentBot-${profileName}-${identity}`;
|
|
317
|
+
const fallbackPath = pathApi.join(
|
|
318
|
+
appDataDirectory ?? pathApi.join(homeDirectory, "AppData", "Roaming"),
|
|
319
|
+
"Microsoft",
|
|
320
|
+
"Windows",
|
|
321
|
+
"Start Menu",
|
|
322
|
+
"Programs",
|
|
323
|
+
"Startup",
|
|
324
|
+
`${name}.vbs`,
|
|
325
|
+
);
|
|
326
|
+
return {
|
|
327
|
+
platform,
|
|
328
|
+
name,
|
|
329
|
+
profilePath,
|
|
330
|
+
configPath,
|
|
331
|
+
bootstrapPath,
|
|
332
|
+
bootstrapContent,
|
|
333
|
+
definitionPath: bootstrapPath,
|
|
334
|
+
command: context.nodePath,
|
|
335
|
+
commandArguments,
|
|
336
|
+
windowsFallbackPath: fallbackPath,
|
|
337
|
+
windowsFallbackContent: renderWindowsStartupScript(context.nodePath, commandArguments),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (platform === "darwin") {
|
|
342
|
+
const name = `com.keyou.agent-bot.${identity}`;
|
|
343
|
+
const definitionPath = pathApi.join(homeDirectory, "Library", "LaunchAgents", `${name}.plist`);
|
|
344
|
+
const stdoutPath = pathApi.join(profilePath, "logs", "autostart.stdout.log");
|
|
345
|
+
const stderrPath = pathApi.join(profilePath, "logs", "autostart.stderr.log");
|
|
346
|
+
return {
|
|
347
|
+
platform,
|
|
348
|
+
name,
|
|
349
|
+
profilePath,
|
|
350
|
+
configPath,
|
|
351
|
+
bootstrapPath,
|
|
352
|
+
bootstrapContent,
|
|
353
|
+
definitionPath,
|
|
354
|
+
definitionContent: renderLaunchAgent({
|
|
355
|
+
name,
|
|
356
|
+
command: context.nodePath,
|
|
357
|
+
commandArguments,
|
|
358
|
+
workingDirectory: profilePath,
|
|
359
|
+
stdoutPath,
|
|
360
|
+
stderrPath,
|
|
361
|
+
}),
|
|
362
|
+
command: context.nodePath,
|
|
363
|
+
commandArguments,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const name = `agent-bot-${identity}.service`;
|
|
368
|
+
const definitionPath = pathApi.join(homeDirectory, ".config", "systemd", "user", name);
|
|
369
|
+
return {
|
|
370
|
+
platform,
|
|
371
|
+
name,
|
|
372
|
+
profilePath,
|
|
373
|
+
configPath,
|
|
374
|
+
bootstrapPath,
|
|
375
|
+
bootstrapContent,
|
|
376
|
+
definitionPath,
|
|
377
|
+
definitionContent: renderSystemdUnit({
|
|
378
|
+
profilePath,
|
|
379
|
+
command: context.nodePath,
|
|
380
|
+
commandArguments,
|
|
381
|
+
}),
|
|
382
|
+
command: context.nodePath,
|
|
383
|
+
commandArguments,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function renderBootstrap(context: AutostartContext, platform: SupportedAutostartPlatform): string {
|
|
388
|
+
return [
|
|
389
|
+
`process.env.${AGENT_BOT_HOME_ENV} = ${JSON.stringify(context.profilePath)};`,
|
|
390
|
+
`process.env.AGENT_BOT_CONFIG = ${JSON.stringify(context.configPath)};`,
|
|
391
|
+
`process.env.${AGENT_BOT_EXPLICIT_PROFILE_ENV} = "1";`,
|
|
392
|
+
...(context.pathValue?.trim()
|
|
393
|
+
? [`process.env.PATH = ${JSON.stringify(context.pathValue)};`]
|
|
394
|
+
: []),
|
|
395
|
+
"delete process.env.FEISHU_APP_ID;",
|
|
396
|
+
"delete process.env.FEISHU_APP_SECRET;",
|
|
397
|
+
"delete process.env.FEISHU_USER_OPEN_ID;",
|
|
398
|
+
`process.chdir(${JSON.stringify(context.profilePath)});`,
|
|
399
|
+
`await import(${JSON.stringify(fileUrlForPlatform(context.supervisorPath, platform))});`,
|
|
400
|
+
"",
|
|
401
|
+
].join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function fileUrlForPlatform(filePath: string, platform: SupportedAutostartPlatform): string {
|
|
405
|
+
if (platform !== "win32") return pathToFileURL(filePath).href;
|
|
406
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
407
|
+
if (normalized.startsWith("//")) {
|
|
408
|
+
const [host = "", ...segments] = normalized.slice(2).split("/");
|
|
409
|
+
return `file://${host}/${segments.map(encodeURIComponent).join("/")}`;
|
|
410
|
+
}
|
|
411
|
+
const drivePath = /^([A-Za-z]):\/(.*)$/.exec(normalized);
|
|
412
|
+
if (!drivePath) return pathToFileURL(filePath).href;
|
|
413
|
+
return `file:///${drivePath[1]}:/${(drivePath[2] ?? "").split("/").map(encodeURIComponent).join("/")}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function renderLaunchAgent(input: {
|
|
417
|
+
name: string;
|
|
418
|
+
command: string;
|
|
419
|
+
commandArguments: string[];
|
|
420
|
+
workingDirectory: string;
|
|
421
|
+
stdoutPath: string;
|
|
422
|
+
stderrPath: string;
|
|
423
|
+
}): string {
|
|
424
|
+
const argumentsXml = [input.command, ...input.commandArguments]
|
|
425
|
+
.map((value) => ` <string>${escapeXml(value)}</string>`)
|
|
426
|
+
.join("\n");
|
|
427
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
428
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
429
|
+
<plist version="1.0">
|
|
430
|
+
<dict>
|
|
431
|
+
<key>Label</key>
|
|
432
|
+
<string>${escapeXml(input.name)}</string>
|
|
433
|
+
<key>ProgramArguments</key>
|
|
434
|
+
<array>
|
|
435
|
+
${argumentsXml}
|
|
436
|
+
</array>
|
|
437
|
+
<key>WorkingDirectory</key>
|
|
438
|
+
<string>${escapeXml(input.workingDirectory)}</string>
|
|
439
|
+
<key>RunAtLoad</key>
|
|
440
|
+
<true/>
|
|
441
|
+
<key>KeepAlive</key>
|
|
442
|
+
<false/>
|
|
443
|
+
<key>ProcessType</key>
|
|
444
|
+
<string>Background</string>
|
|
445
|
+
<key>StandardOutPath</key>
|
|
446
|
+
<string>${escapeXml(input.stdoutPath)}</string>
|
|
447
|
+
<key>StandardErrorPath</key>
|
|
448
|
+
<string>${escapeXml(input.stderrPath)}</string>
|
|
449
|
+
</dict>
|
|
450
|
+
</plist>
|
|
451
|
+
`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function renderSystemdUnit(input: {
|
|
455
|
+
profilePath: string;
|
|
456
|
+
command: string;
|
|
457
|
+
commandArguments: string[];
|
|
458
|
+
}): string {
|
|
459
|
+
const commandLine = [input.command, ...input.commandArguments].map(systemdQuote).join(" ");
|
|
460
|
+
return `[Unit]
|
|
461
|
+
Description=Agent Bot Supervisor
|
|
462
|
+
Wants=network-online.target
|
|
463
|
+
After=network-online.target
|
|
464
|
+
|
|
465
|
+
[Service]
|
|
466
|
+
Type=simple
|
|
467
|
+
WorkingDirectory=${systemdQuote(input.profilePath)}
|
|
468
|
+
ExecStart=${commandLine}
|
|
469
|
+
Restart=no
|
|
470
|
+
|
|
471
|
+
[Install]
|
|
472
|
+
WantedBy=default.target
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function defaultDependencies(): AutostartDependencies {
|
|
477
|
+
return {
|
|
478
|
+
platform: process.platform,
|
|
479
|
+
homeDirectory: os.homedir(),
|
|
480
|
+
username: currentUsername(),
|
|
481
|
+
uid: process.getuid?.(),
|
|
482
|
+
appDataDirectory: process.env.APPDATA?.trim(),
|
|
483
|
+
run: (command, args) => {
|
|
484
|
+
const result = spawnSync(command, args, {
|
|
485
|
+
encoding: "utf8",
|
|
486
|
+
windowsHide: true,
|
|
487
|
+
timeout: 15_000,
|
|
488
|
+
});
|
|
489
|
+
return {
|
|
490
|
+
status: result.status,
|
|
491
|
+
stdout: result.stdout ?? "",
|
|
492
|
+
stderr: result.stderr ?? "",
|
|
493
|
+
...(result.error ? { error: result.error } : {}),
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
exists: (filePath) => fs.existsSync(filePath),
|
|
497
|
+
write: (filePath, content) => writePrivateFile(filePath, content),
|
|
498
|
+
remove: (filePath) => fs.rmSync(filePath, { force: true }),
|
|
499
|
+
removeDirectoryIfEmpty: (directoryPath) => {
|
|
500
|
+
try {
|
|
501
|
+
fs.rmdirSync(directoryPath);
|
|
502
|
+
} catch {
|
|
503
|
+
// The directory is shared with a newer registration or contains user files.
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function writePrivateFile(filePath: string, content: string): void {
|
|
510
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
511
|
+
fs.writeFileSync(filePath, content, { encoding: "utf8", mode: 0o600 });
|
|
512
|
+
try {
|
|
513
|
+
fs.chmodSync(filePath, 0o600);
|
|
514
|
+
} catch {
|
|
515
|
+
// Windows and some mounted filesystems do not implement POSIX modes.
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function currentUsername(): string {
|
|
520
|
+
try {
|
|
521
|
+
return os.userInfo().username;
|
|
522
|
+
} catch {
|
|
523
|
+
return process.env.USER?.trim() || process.env.USERNAME?.trim() || "";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isSupportedPlatform(platform: NodeJS.Platform): platform is SupportedAutostartPlatform {
|
|
528
|
+
return platform === "win32" || platform === "darwin" || platform === "linux";
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function sanitizeName(value: string): string {
|
|
532
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "main";
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function escapeXml(value: string): string {
|
|
536
|
+
return value
|
|
537
|
+
.replaceAll("&", "&")
|
|
538
|
+
.replaceAll("<", "<")
|
|
539
|
+
.replaceAll(">", ">")
|
|
540
|
+
.replaceAll("\"", """)
|
|
541
|
+
.replaceAll("'", "'");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function systemdQuote(value: string): string {
|
|
545
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("%", "%%")}"`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function windowsCommandLine(command: string, args: string[]): string {
|
|
549
|
+
return [command, ...args].map(quoteWindowsArgument).join(" ");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function renderWindowsStartupScript(command: string, args: string[]): string {
|
|
553
|
+
const commandLine = windowsCommandLine(command, args).replaceAll("\"", "\"\"");
|
|
554
|
+
return [
|
|
555
|
+
"Set shell = CreateObject(\"WScript.Shell\")",
|
|
556
|
+
`shell.Run "${commandLine}", 0, False`,
|
|
557
|
+
"",
|
|
558
|
+
].join("\r\n");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function quoteWindowsArgument(value: string): string {
|
|
562
|
+
if (value && !/[\s"]/u.test(value)) return value;
|
|
563
|
+
let result = "\"";
|
|
564
|
+
let backslashes = 0;
|
|
565
|
+
for (const character of value) {
|
|
566
|
+
if (character === "\\") {
|
|
567
|
+
backslashes += 1;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (character === "\"") {
|
|
571
|
+
result += "\\".repeat(backslashes * 2 + 1) + "\"";
|
|
572
|
+
backslashes = 0;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
result += "\\".repeat(backslashes) + character;
|
|
576
|
+
backslashes = 0;
|
|
577
|
+
}
|
|
578
|
+
return `${result}${"\\".repeat(backslashes * 2)}\"`;
|
|
579
|
+
}
|