@keyou007/agent-bot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +675 -0
- package/README.md +187 -0
- package/README.zh.md +187 -0
- package/SECURITY.md +13 -0
- package/config.example.yaml +56 -0
- package/dist/acp/AcpJsonRpcConnection.js +155 -0
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
- package/dist/acp/AcpProcessManager.js +65 -0
- package/dist/acp/AcpProcessManager.js.map +1 -0
- package/dist/acp/AcpSessionManager.js +164 -0
- package/dist/acp/AcpSessionManager.js.map +1 -0
- package/dist/acp/acpTypes.js +10 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +349 -0
- package/dist/cli/FeishuAppConfiguration.js.map +1 -0
- package/dist/cli/FeishuAppRegistration.js +160 -0
- package/dist/cli/FeishuAppRegistration.js.map +1 -0
- package/dist/cli/Initializer.js +112 -0
- package/dist/cli/Initializer.js.map +1 -0
- package/dist/cli/LocalControlClient.js +47 -0
- package/dist/cli/LocalControlClient.js.map +1 -0
- package/dist/cli/LocalControlServer.js +66 -0
- package/dist/cli/LocalControlServer.js.map +1 -0
- package/dist/cli/SkillRegistry.js +126 -0
- package/dist/cli/SkillRegistry.js.map +1 -0
- package/dist/cli/controlProtocol.js +10 -0
- package/dist/cli/controlProtocol.js.map +1 -0
- package/dist/cli/taskChatRoute.js +23 -0
- package/dist/cli/taskChatRoute.js.map +1 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/AppServerConnection.js +131 -0
- package/dist/codex/AppServerConnection.js.map +1 -0
- package/dist/codex/CodexEventMapper.js +296 -0
- package/dist/codex/CodexEventMapper.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +124 -0
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
- package/dist/codex/CodexProcessManager.js +64 -0
- package/dist/codex/CodexProcessManager.js.map +1 -0
- package/dist/codex/CodexRuntime.js +825 -0
- package/dist/codex/CodexRuntime.js.map +1 -0
- package/dist/codex/ProjectlessWorkspace.js +74 -0
- package/dist/codex/ProjectlessWorkspace.js.map +1 -0
- package/dist/codex/protocol.js +10 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/commands/CommandRouter.js +176 -0
- package/dist/commands/CommandRouter.js.map +1 -0
- package/dist/commands/commandTypes.js +2 -0
- package/dist/commands/commandTypes.js.map +1 -0
- package/dist/config/loadConfig.js +107 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/paths.js +32 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.js +38 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/console/ConsoleConnector.js +39 -0
- package/dist/console/ConsoleConnector.js.map +1 -0
- package/dist/console/ConsoleTurnPresenter.js +60 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -0
- package/dist/feishu/CardRenderer.js +972 -0
- package/dist/feishu/CardRenderer.js.map +1 -0
- package/dist/feishu/CardUpdateScheduler.js +148 -0
- package/dist/feishu/CardUpdateScheduler.js.map +1 -0
- package/dist/feishu/ConsoleFeishuClient.js +18 -0
- package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +257 -0
- package/dist/feishu/FeishuConnector.js.map +1 -0
- package/dist/feishu/FeishuMarkdown.js +37 -0
- package/dist/feishu/FeishuMarkdown.js.map +1 -0
- package/dist/feishu/FeishuMessageClient.js +459 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -0
- package/dist/feishu/FeishuTurnPresenter.js +425 -0
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
- package/dist/feishu/GroupAvatarGenerator.js +86 -0
- package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
- package/dist/feishu/LocalCardImage.js +13 -0
- package/dist/feishu/LocalCardImage.js.map +1 -0
- package/dist/feishu/LocalImageMarkdown.js +89 -0
- package/dist/feishu/LocalImageMarkdown.js.map +1 -0
- package/dist/feishu/contextKey.js +12 -0
- package/dist/feishu/contextKey.js.map +1 -0
- package/dist/feishu/transport.js +14 -0
- package/dist/feishu/transport.js.map +1 -0
- package/dist/feishu/types.js +2 -0
- package/dist/feishu/types.js.map +1 -0
- package/dist/index.js +213 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.js +22 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +127 -0
- package/dist/presentation/OutboundRouter.js.map +1 -0
- package/dist/presentation/TurnStateReducer.js +307 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -0
- package/dist/presentation/splitMarkdown.js +341 -0
- package/dist/presentation/splitMarkdown.js.map +1 -0
- package/dist/presentation/turnViewTypes.js +2 -0
- package/dist/presentation/turnViewTypes.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +2819 -0
- package/dist/proxy/ProxySessionController.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +271 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
- package/dist/runtime/AgentRuntimeRegistry.js +18 -0
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
- package/dist/runtime/agentEnvironment.js +11 -0
- package/dist/runtime/agentEnvironment.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +46 -0
- package/dist/startup/SessionMetadataHydrator.js.map +1 -0
- package/dist/startup/StartupNotifier.js +78 -0
- package/dist/startup/StartupNotifier.js.map +1 -0
- package/dist/startup/startFeishu.js +6 -0
- package/dist/startup/startFeishu.js.map +1 -0
- package/dist/state/StateStore.js +893 -0
- package/dist/state/StateStore.js.map +1 -0
- package/dist/state/migrations.js +129 -0
- package/dist/state/migrations.js.map +1 -0
- package/dist/supervision/SafeRestartNotifier.js +116 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -0
- package/dist/supervision/SafeRestartScheduler.js +123 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -0
- package/dist/supervision/replacementSupervisor.js +8 -0
- package/dist/supervision/replacementSupervisor.js.map +1 -0
- package/dist/supervision/restartPolicy.js +16 -0
- package/dist/supervision/restartPolicy.js.map +1 -0
- package/dist/supervisor.js +94 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/utils/executeShellCommand.js +76 -0
- package/dist/utils/executeShellCommand.js.map +1 -0
- package/dist/utils/id.js +5 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/markdown.js +29 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/spawnCommand.js +21 -0
- package/dist/utils/spawnCommand.js.map +1 -0
- package/dist/utils/taskTitle.js +10 -0
- package/dist/utils/taskTitle.js.map +1 -0
- package/docs/technical-reference.md +294 -0
- package/docs/technical-reference.zh.md +294 -0
- package/npm-shrinkwrap.json +4525 -0
- package/package.json +87 -0
- package/skills/agent-bot/SKILL.md +141 -0
- package/skills/agent-bot/agents/openai.yaml +4 -0
- package/src/acp/AcpJsonRpcConnection.ts +212 -0
- package/src/acp/AcpProcessManager.ts +87 -0
- package/src/acp/AcpSessionManager.ts +227 -0
- package/src/acp/acpTypes.ts +99 -0
- package/src/cli/FeishuAppConfiguration.ts +473 -0
- package/src/cli/FeishuAppRegistration.ts +207 -0
- package/src/cli/Initializer.ts +159 -0
- package/src/cli/LocalControlClient.ts +48 -0
- package/src/cli/LocalControlServer.ts +67 -0
- package/src/cli/SkillRegistry.ts +151 -0
- package/src/cli/controlProtocol.ts +34 -0
- package/src/cli/taskChatRoute.ts +31 -0
- package/src/cli.ts +630 -0
- package/src/codex/AppServerConnection.ts +159 -0
- package/src/codex/CodexEventMapper.ts +351 -0
- package/src/codex/CodexLocalActivityDetector.ts +148 -0
- package/src/codex/CodexProcessManager.ts +63 -0
- package/src/codex/CodexRuntime.ts +1025 -0
- package/src/codex/ProjectlessWorkspace.ts +90 -0
- package/src/codex/protocol.ts +37 -0
- package/src/commands/CommandRouter.ts +179 -0
- package/src/commands/commandTypes.ts +28 -0
- package/src/config/loadConfig.ts +115 -0
- package/src/config/paths.ts +37 -0
- package/src/config/schema.ts +46 -0
- package/src/console/ConsoleConnector.ts +40 -0
- package/src/console/ConsoleTurnPresenter.ts +64 -0
- package/src/feishu/CardRenderer.ts +1157 -0
- package/src/feishu/CardUpdateScheduler.ts +165 -0
- package/src/feishu/ConsoleFeishuClient.ts +22 -0
- package/src/feishu/FeishuConnector.ts +277 -0
- package/src/feishu/FeishuMarkdown.ts +47 -0
- package/src/feishu/FeishuMessageClient.ts +649 -0
- package/src/feishu/FeishuTurnPresenter.ts +539 -0
- package/src/feishu/GroupAvatarGenerator.ts +102 -0
- package/src/feishu/LocalCardImage.ts +13 -0
- package/src/feishu/LocalImageMarkdown.ts +92 -0
- package/src/feishu/contextKey.ts +14 -0
- package/src/feishu/transport.ts +24 -0
- package/src/feishu/types.ts +83 -0
- package/src/index.ts +228 -0
- package/src/logging/logger.ts +28 -0
- package/src/presentation/OutboundRouter.ts +205 -0
- package/src/presentation/TurnStateReducer.ts +341 -0
- package/src/presentation/splitMarkdown.ts +358 -0
- package/src/presentation/turnViewTypes.ts +54 -0
- package/src/proxy/ProxySessionController.ts +3311 -0
- package/src/runtime/AcpRuntimeAdapter.ts +324 -0
- package/src/runtime/AgentRuntimeRegistry.ts +20 -0
- package/src/runtime/agentEnvironment.ts +14 -0
- package/src/runtime/types.ts +215 -0
- package/src/startup/SessionMetadataHydrator.ts +46 -0
- package/src/startup/StartupNotifier.ts +92 -0
- package/src/startup/startFeishu.ts +19 -0
- package/src/state/StateStore.ts +1249 -0
- package/src/state/migrations.ts +128 -0
- package/src/supervision/SafeRestartNotifier.ts +122 -0
- package/src/supervision/SafeRestartScheduler.ts +149 -0
- package/src/supervision/replacementSupervisor.ts +10 -0
- package/src/supervision/restartPolicy.ts +19 -0
- package/src/supervisor.ts +106 -0
- package/src/utils/executeShellCommand.ts +97 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/markdown.ts +25 -0
- package/src/utils/spawnCommand.ts +35 -0
- package/src/utils/taskTitle.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse as parseDotEnv } from "dotenv";
|
|
5
|
+
import {
|
|
6
|
+
agentBotHome,
|
|
7
|
+
defaultConfigPath,
|
|
8
|
+
defaultDotEnvPath,
|
|
9
|
+
resolveUserPath,
|
|
10
|
+
} from "../config/paths.js";
|
|
11
|
+
|
|
12
|
+
export type InitializationStatus = "created" | "existing";
|
|
13
|
+
|
|
14
|
+
export interface InitializedPath {
|
|
15
|
+
path: string;
|
|
16
|
+
status: InitializationStatus;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface InitializationResult {
|
|
20
|
+
home: InitializedPath;
|
|
21
|
+
config: InitializedPath;
|
|
22
|
+
env: InitializedPath;
|
|
23
|
+
data: InitializedPath;
|
|
24
|
+
logs: InitializedPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface InitializationOptions {
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
configPath?: string;
|
|
30
|
+
configTemplatePath?: string;
|
|
31
|
+
envTemplatePath?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type FeishuCredentialStatus = "configured" | "missing" | "incomplete";
|
|
35
|
+
|
|
36
|
+
export interface FeishuCredentialState {
|
|
37
|
+
status: FeishuCredentialStatus;
|
|
38
|
+
appId?: string;
|
|
39
|
+
appSecret?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FeishuCredentials {
|
|
43
|
+
appId: string;
|
|
44
|
+
appSecret: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_CONFIG_TEMPLATE = fileURLToPath(new URL("../../config.example.yaml", import.meta.url));
|
|
48
|
+
const DEFAULT_ENV_TEMPLATE = fileURLToPath(new URL("../../.env.example", import.meta.url));
|
|
49
|
+
|
|
50
|
+
export function initializeAgentBot(options: InitializationOptions = {}): InitializationResult {
|
|
51
|
+
const env = options.env ?? process.env;
|
|
52
|
+
const configTemplatePath = path.resolve(options.configTemplatePath ?? DEFAULT_CONFIG_TEMPLATE);
|
|
53
|
+
const envTemplatePath = path.resolve(options.envTemplatePath ?? DEFAULT_ENV_TEMPLATE);
|
|
54
|
+
assertTemplate(configTemplatePath);
|
|
55
|
+
assertTemplate(envTemplatePath);
|
|
56
|
+
|
|
57
|
+
const homePath = agentBotHome(env);
|
|
58
|
+
const configuredPath = firstNonBlank(options.configPath, env.AGENT_BOT_CONFIG);
|
|
59
|
+
const configPath = resolveUserPath(configuredPath ?? defaultConfigPath(env));
|
|
60
|
+
const envPath = defaultDotEnvPath(env);
|
|
61
|
+
const configDirectory = path.dirname(configPath);
|
|
62
|
+
|
|
63
|
+
const home = ensureDirectory(homePath);
|
|
64
|
+
fs.mkdirSync(configDirectory, { recursive: true });
|
|
65
|
+
|
|
66
|
+
let config: InitializedPath;
|
|
67
|
+
if (fs.existsSync(configPath)) {
|
|
68
|
+
config = { path: configPath, status: "existing" };
|
|
69
|
+
} else {
|
|
70
|
+
fs.copyFileSync(configTemplatePath, configPath, fs.constants.COPYFILE_EXCL);
|
|
71
|
+
config = { path: configPath, status: "created" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const envFile = copyIfMissing(envTemplatePath, envPath);
|
|
75
|
+
restrictFilePermissions(envFile.path);
|
|
76
|
+
const data = ensureDirectory(path.join(configDirectory, "data"));
|
|
77
|
+
const logs = ensureDirectory(path.join(configDirectory, "logs"));
|
|
78
|
+
|
|
79
|
+
return { home, config, env: envFile, data, logs };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readFeishuCredentials(envPath: string, env: NodeJS.ProcessEnv = process.env): FeishuCredentialState {
|
|
83
|
+
const fileValues = fs.existsSync(envPath) ? parseDotEnv(fs.readFileSync(envPath, "utf8")) : {};
|
|
84
|
+
const appId = firstNonBlank(env.FEISHU_APP_ID, fileValues.FEISHU_APP_ID);
|
|
85
|
+
const appSecret = firstNonBlank(env.FEISHU_APP_SECRET, fileValues.FEISHU_APP_SECRET);
|
|
86
|
+
if (appId && appSecret) return { status: "configured", appId, appSecret };
|
|
87
|
+
if (appId || appSecret) return { status: "incomplete", appId, appSecret };
|
|
88
|
+
return { status: "missing" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function writeFeishuCredentials(envPath: string, credentials: FeishuCredentials): void {
|
|
92
|
+
assertDotEnvValue("FEISHU_APP_ID", credentials.appId);
|
|
93
|
+
assertDotEnvValue("FEISHU_APP_SECRET", credentials.appSecret);
|
|
94
|
+
const original = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
95
|
+
let updated = upsertDotEnvValue(original, "FEISHU_APP_ID", credentials.appId);
|
|
96
|
+
updated = upsertDotEnvValue(updated, "FEISHU_APP_SECRET", credentials.appSecret);
|
|
97
|
+
|
|
98
|
+
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
99
|
+
const temporaryPath = `${envPath}.${process.pid}.${Date.now()}.tmp`;
|
|
100
|
+
try {
|
|
101
|
+
fs.writeFileSync(temporaryPath, updated, { encoding: "utf8", mode: 0o600 });
|
|
102
|
+
fs.renameSync(temporaryPath, envPath);
|
|
103
|
+
restrictFilePermissions(envPath);
|
|
104
|
+
} finally {
|
|
105
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assertTemplate(templatePath: string): void {
|
|
110
|
+
if (!fs.statSync(templatePath, { throwIfNoEntry: false })?.isFile()) {
|
|
111
|
+
throw new Error(`Initialization template does not exist: ${templatePath}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ensureDirectory(directoryPath: string): InitializedPath {
|
|
116
|
+
const status = fs.existsSync(directoryPath) ? "existing" : "created";
|
|
117
|
+
fs.mkdirSync(directoryPath, { recursive: true });
|
|
118
|
+
return { path: directoryPath, status };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function copyIfMissing(sourcePath: string, targetPath: string): InitializedPath {
|
|
122
|
+
if (fs.existsSync(targetPath)) return { path: targetPath, status: "existing" };
|
|
123
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
124
|
+
fs.copyFileSync(sourcePath, targetPath, fs.constants.COPYFILE_EXCL);
|
|
125
|
+
return { path: targetPath, status: "created" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function upsertDotEnvValue(contents: string, key: string, value: string): string {
|
|
129
|
+
const newline = contents.includes("\r\n") ? "\r\n" : "\n";
|
|
130
|
+
const lines = contents.length > 0 ? contents.split(/\r?\n/) : [];
|
|
131
|
+
const matcher = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=`);
|
|
132
|
+
const index = lines.findIndex((line) => matcher.test(line));
|
|
133
|
+
if (index >= 0) {
|
|
134
|
+
lines[index] = `${key}=${value}`;
|
|
135
|
+
} else {
|
|
136
|
+
while (lines.length > 0 && lines.at(-1) === "") lines.pop();
|
|
137
|
+
lines.push(`${key}=${value}`);
|
|
138
|
+
}
|
|
139
|
+
while (lines.length > 0 && lines.at(-1) === "") lines.pop();
|
|
140
|
+
return `${lines.join(newline)}${newline}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function assertDotEnvValue(key: string, value: string): void {
|
|
144
|
+
if (!value.trim() || /[\r\n]/.test(value)) {
|
|
145
|
+
throw new Error(`${key} 不是有效的单行环境变量值。`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function restrictFilePermissions(filePath: string): void {
|
|
150
|
+
try {
|
|
151
|
+
fs.chmodSync(filePath, 0o600);
|
|
152
|
+
} catch {
|
|
153
|
+
// Windows and some mounted filesystems do not implement POSIX modes.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function firstNonBlank(...values: Array<string | undefined>): string | undefined {
|
|
158
|
+
return values.find((value) => value !== undefined && value.trim().length > 0);
|
|
159
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import type { ControlRequest, ControlResponse } from "./controlProtocol.js";
|
|
3
|
+
|
|
4
|
+
export async function sendControlRequest(
|
|
5
|
+
endpoint: string,
|
|
6
|
+
request: ControlRequest,
|
|
7
|
+
timeoutMs = 5_000,
|
|
8
|
+
): Promise<ControlResponse> {
|
|
9
|
+
return new Promise<ControlResponse>((resolve, reject) => {
|
|
10
|
+
const socket = net.createConnection(endpoint);
|
|
11
|
+
let settled = false;
|
|
12
|
+
let input = "";
|
|
13
|
+
const finish = (operation: () => void): void => {
|
|
14
|
+
if (settled) return;
|
|
15
|
+
settled = true;
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
socket.destroy();
|
|
18
|
+
operation();
|
|
19
|
+
};
|
|
20
|
+
const timer = setTimeout(() => finish(() => reject(new Error("连接 Agent Bot 控制端点超时。"))), timeoutMs);
|
|
21
|
+
socket.setEncoding("utf8");
|
|
22
|
+
socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
23
|
+
socket.on("data", (chunk: string) => {
|
|
24
|
+
input += chunk;
|
|
25
|
+
const newline = input.indexOf("\n");
|
|
26
|
+
if (newline < 0) return;
|
|
27
|
+
try {
|
|
28
|
+
const response = JSON.parse(input.slice(0, newline)) as ControlResponse;
|
|
29
|
+
finish(() => resolve(response));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
finish(() => reject(error));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
socket.once("error", (error) => finish(() => reject(error)));
|
|
35
|
+
socket.once("end", () => {
|
|
36
|
+
if (!settled) finish(() => reject(new Error("Agent Bot 控制端点未返回结果。")));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function isServerRunning(endpoint: string): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
const response = await sendControlRequest(endpoint, { action: "health" }, 1_000);
|
|
44
|
+
return response.ok;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import type { ControlRequest, ControlResponse } from "./controlProtocol.js";
|
|
4
|
+
|
|
5
|
+
export type ControlRequestHandler = (request: ControlRequest) => Promise<ControlResponse>;
|
|
6
|
+
|
|
7
|
+
export class LocalControlServer {
|
|
8
|
+
private server?: net.Server;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly endpoint: string,
|
|
12
|
+
private readonly handleRequest: ControlRequestHandler,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
async start(): Promise<void> {
|
|
16
|
+
if (this.server) return;
|
|
17
|
+
if (process.platform !== "win32") fs.rmSync(this.endpoint, { force: true });
|
|
18
|
+
const server = net.createServer((socket) => {
|
|
19
|
+
socket.setEncoding("utf8");
|
|
20
|
+
let input = "";
|
|
21
|
+
socket.on("data", (chunk: string) => {
|
|
22
|
+
input += chunk;
|
|
23
|
+
const newline = input.indexOf("\n");
|
|
24
|
+
if (newline < 0) return;
|
|
25
|
+
const line = input.slice(0, newline);
|
|
26
|
+
input = "";
|
|
27
|
+
void this.respond(socket, line);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
await new Promise<void>((resolve, reject) => {
|
|
31
|
+
const onError = (error: Error) => reject(error);
|
|
32
|
+
server.once("error", onError);
|
|
33
|
+
server.listen(this.endpoint, () => {
|
|
34
|
+
server.off("error", onError);
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
this.server = server;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async close(): Promise<void> {
|
|
42
|
+
const server = this.server;
|
|
43
|
+
this.server = undefined;
|
|
44
|
+
if (server) {
|
|
45
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
46
|
+
}
|
|
47
|
+
if (process.platform !== "win32") fs.rmSync(this.endpoint, { force: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async respond(socket: net.Socket, line: string): Promise<void> {
|
|
51
|
+
let response: ControlResponse;
|
|
52
|
+
try {
|
|
53
|
+
response = await this.handleRequest(parseRequest(line));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
response = { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
56
|
+
}
|
|
57
|
+
socket.end(`${JSON.stringify(response)}\n`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseRequest(line: string): ControlRequest {
|
|
62
|
+
const value = JSON.parse(line) as unknown;
|
|
63
|
+
if (!value || typeof value !== "object" || !("action" in value) || typeof value.action !== "string") {
|
|
64
|
+
throw new Error("无效的本地控制请求。");
|
|
65
|
+
}
|
|
66
|
+
return value as ControlRequest;
|
|
67
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const SKILL_NAME = "agent-bot";
|
|
7
|
+
const MARKER_FILE = ".agent-bot-managed.json";
|
|
8
|
+
|
|
9
|
+
interface RegistrationMarker {
|
|
10
|
+
schemaVersion: 1;
|
|
11
|
+
managedBy: "agent-bot";
|
|
12
|
+
installedAt: string;
|
|
13
|
+
sourceDigest: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SkillRegistrationStatus {
|
|
17
|
+
sourcePath: string;
|
|
18
|
+
skillsRoot: string;
|
|
19
|
+
targetPath: string;
|
|
20
|
+
registered: boolean;
|
|
21
|
+
managed: boolean;
|
|
22
|
+
upToDate: boolean;
|
|
23
|
+
sourceDigest: string;
|
|
24
|
+
installedDigest?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveSystemSkillsRoot(
|
|
28
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
29
|
+
homeDirectory = os.homedir(),
|
|
30
|
+
): string {
|
|
31
|
+
return path.resolve(env.AGENT_BOT_SKILLS_DIR ?? path.join(homeDirectory, ".agents", "skills"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class SkillRegistry {
|
|
35
|
+
readonly sourcePath: string;
|
|
36
|
+
readonly skillsRoot: string;
|
|
37
|
+
readonly targetPath: string;
|
|
38
|
+
|
|
39
|
+
constructor(sourcePath: string, skillsRoot = resolveSystemSkillsRoot()) {
|
|
40
|
+
this.sourcePath = path.resolve(sourcePath);
|
|
41
|
+
this.skillsRoot = path.resolve(skillsRoot);
|
|
42
|
+
this.targetPath = path.join(this.skillsRoot, SKILL_NAME);
|
|
43
|
+
this.assertSafeTarget();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
status(): SkillRegistrationStatus {
|
|
47
|
+
this.assertValidSource();
|
|
48
|
+
const registered = fs.existsSync(path.join(this.targetPath, "SKILL.md"));
|
|
49
|
+
const managed = registered && this.readMarker()?.managedBy === "agent-bot";
|
|
50
|
+
const sourceDigest = digestDirectory(this.sourcePath);
|
|
51
|
+
const installedDigest = registered ? digestDirectory(this.targetPath) : undefined;
|
|
52
|
+
return {
|
|
53
|
+
sourcePath: this.sourcePath,
|
|
54
|
+
skillsRoot: this.skillsRoot,
|
|
55
|
+
targetPath: this.targetPath,
|
|
56
|
+
registered,
|
|
57
|
+
managed,
|
|
58
|
+
upToDate: managed && installedDigest === sourceDigest,
|
|
59
|
+
sourceDigest,
|
|
60
|
+
installedDigest,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
install(): { updated: boolean; status: SkillRegistrationStatus } {
|
|
65
|
+
this.assertValidSource();
|
|
66
|
+
const existing = fs.existsSync(this.targetPath);
|
|
67
|
+
if (existing && this.readMarker()?.managedBy !== "agent-bot") {
|
|
68
|
+
throw new Error(`目标目录已存在且不由 Agent Bot 管理:${this.targetPath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const before = existing ? this.status() : undefined;
|
|
72
|
+
if (before?.upToDate) return { updated: false, status: before };
|
|
73
|
+
|
|
74
|
+
fs.mkdirSync(this.skillsRoot, { recursive: true });
|
|
75
|
+
const temporaryPath = path.join(this.skillsRoot, `.${SKILL_NAME}.install-${process.pid}-${randomUUID()}`);
|
|
76
|
+
try {
|
|
77
|
+
fs.cpSync(this.sourcePath, temporaryPath, { recursive: true, errorOnExist: true });
|
|
78
|
+
const marker: RegistrationMarker = {
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
managedBy: "agent-bot",
|
|
81
|
+
installedAt: new Date().toISOString(),
|
|
82
|
+
sourceDigest: digestDirectory(this.sourcePath),
|
|
83
|
+
};
|
|
84
|
+
fs.writeFileSync(path.join(temporaryPath, MARKER_FILE), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
85
|
+
if (existing) this.removeManagedTarget();
|
|
86
|
+
fs.renameSync(temporaryPath, this.targetPath);
|
|
87
|
+
} finally {
|
|
88
|
+
if (fs.existsSync(temporaryPath)) fs.rmSync(temporaryPath, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
return { updated: true, status: this.status() };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
uninstall(): boolean {
|
|
94
|
+
if (!fs.existsSync(this.targetPath)) return false;
|
|
95
|
+
if (this.readMarker()?.managedBy !== "agent-bot") {
|
|
96
|
+
throw new Error(`拒绝删除不由 Agent Bot 管理的目录:${this.targetPath}`);
|
|
97
|
+
}
|
|
98
|
+
this.removeManagedTarget();
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private assertValidSource(): void {
|
|
103
|
+
if (!fs.existsSync(path.join(this.sourcePath, "SKILL.md"))) {
|
|
104
|
+
throw new Error(`找不到内置 Agent Bot Skill:${this.sourcePath}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private assertSafeTarget(): void {
|
|
109
|
+
if (path.dirname(this.targetPath) !== this.skillsRoot || path.basename(this.targetPath) !== SKILL_NAME) {
|
|
110
|
+
throw new Error(`不安全的 Skill 目标路径:${this.targetPath}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private readMarker(): RegistrationMarker | undefined {
|
|
115
|
+
try {
|
|
116
|
+
const marker = JSON.parse(fs.readFileSync(path.join(this.targetPath, MARKER_FILE), "utf8")) as Partial<RegistrationMarker>;
|
|
117
|
+
if (marker.schemaVersion !== 1 || marker.managedBy !== "agent-bot") return undefined;
|
|
118
|
+
return marker as RegistrationMarker;
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private removeManagedTarget(): void {
|
|
125
|
+
this.assertSafeTarget();
|
|
126
|
+
fs.rmSync(this.targetPath, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function digestDirectory(directory: string): string {
|
|
131
|
+
const hash = createHash("sha256");
|
|
132
|
+
for (const file of listFiles(directory)) {
|
|
133
|
+
const relativePath = path.relative(directory, file).replaceAll(path.sep, "/");
|
|
134
|
+
if (relativePath === MARKER_FILE) continue;
|
|
135
|
+
hash.update(relativePath);
|
|
136
|
+
hash.update("\0");
|
|
137
|
+
hash.update(fs.readFileSync(file));
|
|
138
|
+
hash.update("\0");
|
|
139
|
+
}
|
|
140
|
+
return hash.digest("hex");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function listFiles(directory: string): string[] {
|
|
144
|
+
const files: string[] = [];
|
|
145
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
146
|
+
const entryPath = path.join(directory, entry.name);
|
|
147
|
+
if (entry.isDirectory()) files.push(...listFiles(entryPath));
|
|
148
|
+
else if (entry.isFile()) files.push(entryPath);
|
|
149
|
+
}
|
|
150
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
151
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { TurnViewState } from "../presentation/turnViewTypes.js";
|
|
4
|
+
import type { RemoteSessionSummary } from "../runtime/types.js";
|
|
5
|
+
import type { SessionRecord } from "../state/StateStore.js";
|
|
6
|
+
|
|
7
|
+
export type ControlRequest =
|
|
8
|
+
| { action: "health" }
|
|
9
|
+
| { action: "server_restart"; mode: "safe" | "immediate"; reason: string }
|
|
10
|
+
| { action: "server_stop" }
|
|
11
|
+
| { action: "task_status"; localSessionId: string }
|
|
12
|
+
| { action: "task_stop"; localSessionId: string }
|
|
13
|
+
| { action: "task_title"; localSessionId: string; title: string }
|
|
14
|
+
| { action: "task_prompt"; localSessionId: string; text: string };
|
|
15
|
+
|
|
16
|
+
export interface ControlResponse {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
message?: string;
|
|
19
|
+
data?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TaskStatusControlData {
|
|
23
|
+
session: SessionRecord;
|
|
24
|
+
snapshot?: TurnViewState;
|
|
25
|
+
remote?: RemoteSessionSummary;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function controlEndpoint(sqlitePath: string): string {
|
|
29
|
+
const resolvedPath = path.resolve(sqlitePath);
|
|
30
|
+
const key = createHash("sha256").update(resolvedPath.toLowerCase()).digest("hex").slice(0, 16);
|
|
31
|
+
return process.platform === "win32"
|
|
32
|
+
? `\\\\.\\pipe\\agent-bot-${key}`
|
|
33
|
+
: path.join(path.dirname(resolvedPath), `.agent-bot-${key}.sock`);
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
|
|
2
|
+
import type { SessionRecord } from "../state/StateStore.js";
|
|
3
|
+
|
|
4
|
+
const CHAT_CONTEXT_PREFIX = "chat_id:";
|
|
5
|
+
const THREAD_CONTEXT_MARKER = ":thread_id:";
|
|
6
|
+
|
|
7
|
+
export interface TaskChatRoute {
|
|
8
|
+
taskId: string;
|
|
9
|
+
chatId: string;
|
|
10
|
+
contextKey: string;
|
|
11
|
+
threadId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function taskChatRoute(session: SessionRecord): TaskChatRoute {
|
|
15
|
+
const contextKey = session.contextKey;
|
|
16
|
+
const baseContextKey = baseChatContextKey(contextKey);
|
|
17
|
+
if (!baseContextKey.startsWith(CHAT_CONTEXT_PREFIX)) {
|
|
18
|
+
throw new Error(`任务没有绑定飞书会话:${session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId}`);
|
|
19
|
+
}
|
|
20
|
+
const chatId = baseContextKey.slice(CHAT_CONTEXT_PREFIX.length);
|
|
21
|
+
if (!chatId) throw new Error(`任务的飞书会话 ID 无效:${contextKey}`);
|
|
22
|
+
const threadId = isThreadContextKey(contextKey)
|
|
23
|
+
? contextKey.slice(contextKey.indexOf(THREAD_CONTEXT_MARKER) + THREAD_CONTEXT_MARKER.length)
|
|
24
|
+
: undefined;
|
|
25
|
+
return {
|
|
26
|
+
taskId: session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId,
|
|
27
|
+
chatId,
|
|
28
|
+
contextKey,
|
|
29
|
+
...(threadId ? { threadId } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|