@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,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
|
+
}
|
|
@@ -72,7 +72,8 @@ const CORE_FEISHU_SCOPES = new Set([
|
|
|
72
72
|
"im:message:send_as_bot",
|
|
73
73
|
]);
|
|
74
74
|
const CORE_FEISHU_EVENTS = new Set<string>(["im.message.receive_v1"]);
|
|
75
|
-
const
|
|
75
|
+
const ALL_GROUP_MESSAGE_SCOPES = new Set(["im:message.group_msg"]);
|
|
76
|
+
const MANUAL_FEISHU_SCOPES = ALL_GROUP_MESSAGE_SCOPES;
|
|
76
77
|
|
|
77
78
|
export interface FeishuAppConfiguration {
|
|
78
79
|
scopes: string[];
|
|
@@ -96,6 +97,7 @@ export interface FeishuConfigurationChallenge {
|
|
|
96
97
|
export interface EnsureFeishuAppConfigurationOptions {
|
|
97
98
|
fetch?: typeof globalThis.fetch;
|
|
98
99
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
100
|
+
respondToAllGroupMessages?: boolean;
|
|
99
101
|
signal?: AbortSignal;
|
|
100
102
|
manualPermissionSkipSignal?: AbortSignal;
|
|
101
103
|
optionalSkipSignal?: AbortSignal;
|
|
@@ -124,6 +126,9 @@ export async function ensureFeishuAppConfiguration(
|
|
|
124
126
|
const timeoutMs = positiveInteger(options.timeoutMs) ?? DEFAULT_COMPLETION_TIMEOUT_MS;
|
|
125
127
|
const optionalTimeoutMs = positiveInteger(options.optionalTimeoutMs)
|
|
126
128
|
?? DEFAULT_OPTIONAL_COMPLETION_TIMEOUT_MS;
|
|
129
|
+
const excludedScopes = options.respondToAllGroupMessages === false
|
|
130
|
+
? ALL_GROUP_MESSAGE_SCOPES
|
|
131
|
+
: new Set<string>();
|
|
127
132
|
|
|
128
133
|
let configuration: FeishuAppConfiguration;
|
|
129
134
|
try {
|
|
@@ -133,7 +138,7 @@ export async function ensureFeishuAppConfiguration(
|
|
|
133
138
|
configuration = emptyConfiguration();
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
const missing = missingFeishuAppConfiguration(configuration);
|
|
141
|
+
const missing = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
|
|
137
142
|
if (!hasMissingConfiguration(missing)) {
|
|
138
143
|
return {
|
|
139
144
|
status: "ready",
|
|
@@ -154,6 +159,8 @@ export async function ensureFeishuAppConfiguration(
|
|
|
154
159
|
pollIntervalMs,
|
|
155
160
|
optionalTimeoutMs,
|
|
156
161
|
options,
|
|
162
|
+
new Set(),
|
|
163
|
+
excludedScopes,
|
|
157
164
|
);
|
|
158
165
|
}
|
|
159
166
|
|
|
@@ -179,7 +186,7 @@ export async function ensureFeishuAppConfiguration(
|
|
|
179
186
|
|
|
180
187
|
try {
|
|
181
188
|
configuration = await readFeishuAppConfiguration(credentials, fetchImpl, options.signal);
|
|
182
|
-
const remaining = missingFeishuAppConfiguration(configuration);
|
|
189
|
+
const remaining = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
|
|
183
190
|
const unresolvedCore = coreMissingFeishuAppConfiguration(remaining);
|
|
184
191
|
if (!hasBlockingCoreConfiguration(unresolvedCore, options.manualPermissionSkipSignal)) {
|
|
185
192
|
return completeOptionalFeishuConfiguration(
|
|
@@ -192,12 +199,16 @@ export async function ensureFeishuAppConfiguration(
|
|
|
192
199
|
optionalTimeoutMs,
|
|
193
200
|
options,
|
|
194
201
|
skippedManualScopes(coreMissing, options.manualPermissionSkipSignal),
|
|
202
|
+
excludedScopes,
|
|
195
203
|
);
|
|
196
204
|
}
|
|
197
205
|
lastError = undefined;
|
|
198
206
|
} catch (error) {
|
|
199
207
|
if (options.signal?.aborted) throw abortError(options.signal);
|
|
200
|
-
const knownRemaining =
|
|
208
|
+
const knownRemaining = withoutIgnoredScopes(
|
|
209
|
+
missingFeishuAppConfiguration(configuration),
|
|
210
|
+
excludedScopes,
|
|
211
|
+
);
|
|
201
212
|
if (!hasBlockingCoreConfiguration(
|
|
202
213
|
coreMissingFeishuAppConfiguration(knownRemaining),
|
|
203
214
|
options.manualPermissionSkipSignal,
|
|
@@ -212,6 +223,7 @@ export async function ensureFeishuAppConfiguration(
|
|
|
212
223
|
optionalTimeoutMs,
|
|
213
224
|
options,
|
|
214
225
|
skippedManualScopes(coreMissing, options.manualPermissionSkipSignal),
|
|
226
|
+
excludedScopes,
|
|
215
227
|
);
|
|
216
228
|
}
|
|
217
229
|
lastError = error;
|
|
@@ -267,9 +279,10 @@ async function completeOptionalFeishuConfiguration(
|
|
|
267
279
|
timeoutMs: number,
|
|
268
280
|
options: EnsureFeishuAppConfigurationOptions,
|
|
269
281
|
ignoredScopes: ReadonlySet<string> = new Set(),
|
|
282
|
+
excludedScopes: ReadonlySet<string> = new Set(),
|
|
270
283
|
): Promise<EnsureFeishuAppConfigurationResult> {
|
|
271
284
|
let configuration = initialConfiguration;
|
|
272
|
-
let remaining = missingFeishuAppConfiguration(configuration);
|
|
285
|
+
let remaining = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
|
|
273
286
|
const requested = withoutIgnoredScopes(remaining, ignoredScopes);
|
|
274
287
|
if (hasMissingConfiguration(requested)) {
|
|
275
288
|
await requestMissingFeishuConfiguration(
|
|
@@ -289,6 +302,7 @@ async function completeOptionalFeishuConfiguration(
|
|
|
289
302
|
options.signal,
|
|
290
303
|
options.optionalSkipSignal,
|
|
291
304
|
ignoredScopes,
|
|
305
|
+
excludedScopes,
|
|
292
306
|
);
|
|
293
307
|
configuration = completed.configuration;
|
|
294
308
|
remaining = completed.remaining;
|
|
@@ -312,12 +326,13 @@ async function waitForOptionalFeishuConfiguration(
|
|
|
312
326
|
signal?: AbortSignal,
|
|
313
327
|
optionalSkipSignal?: AbortSignal,
|
|
314
328
|
ignoredScopes: ReadonlySet<string> = new Set(),
|
|
329
|
+
excludedScopes: ReadonlySet<string> = new Set(),
|
|
315
330
|
): Promise<{
|
|
316
331
|
configuration: FeishuAppConfiguration;
|
|
317
332
|
remaining: MissingFeishuAppConfiguration;
|
|
318
333
|
}> {
|
|
319
334
|
let configuration = initialConfiguration;
|
|
320
|
-
let remaining = missingFeishuAppConfiguration(configuration);
|
|
335
|
+
let remaining = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
|
|
321
336
|
let remainingMs = timeoutMs;
|
|
322
337
|
while (remainingMs > 0 && hasMissingConfiguration(withoutIgnoredScopes(remaining, ignoredScopes))) {
|
|
323
338
|
if (optionalSkipSignal?.aborted) break;
|
|
@@ -332,7 +347,7 @@ async function waitForOptionalFeishuConfiguration(
|
|
|
332
347
|
remainingMs -= waitMs;
|
|
333
348
|
try {
|
|
334
349
|
configuration = await readFeishuAppConfiguration(credentials, fetchImpl, signal);
|
|
335
|
-
remaining = missingFeishuAppConfiguration(configuration);
|
|
350
|
+
remaining = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
|
|
336
351
|
} catch (error) {
|
|
337
352
|
if (signal?.aborted) throw abortError(signal);
|
|
338
353
|
}
|