@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,90 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export interface ProjectlessWorkspace {
|
|
7
|
+
cwd: string;
|
|
8
|
+
outputDirectory: string;
|
|
9
|
+
workspaceRoot: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createProjectlessWorkspace(options: {
|
|
13
|
+
prompt?: string;
|
|
14
|
+
directoryName?: string;
|
|
15
|
+
homeDirectory?: string;
|
|
16
|
+
now?: Date;
|
|
17
|
+
} = {}): ProjectlessWorkspace {
|
|
18
|
+
const workspaceRoot = path.join(options.homeDirectory ?? os.homedir(), "Documents", "Codex");
|
|
19
|
+
const dateDirectory = path.join(workspaceRoot, formatDate(options.now ?? new Date()));
|
|
20
|
+
ensureRealDirectory(workspaceRoot);
|
|
21
|
+
ensureRealDirectory(dateDirectory);
|
|
22
|
+
|
|
23
|
+
const baseName = projectlessDirectoryName(options.directoryName, options.prompt);
|
|
24
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
25
|
+
const name = attempt === 0 ? baseName : `${baseName}-${attempt + 1}`;
|
|
26
|
+
const workspace = tryCreateWorkspace(dateDirectory, name, workspaceRoot);
|
|
27
|
+
if (workspace) return workspace;
|
|
28
|
+
}
|
|
29
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
30
|
+
const suffix = randomUUID().replaceAll("-", "").slice(0, 8);
|
|
31
|
+
const workspace = tryCreateWorkspace(dateDirectory, `${baseName}-${suffix}`, workspaceRoot);
|
|
32
|
+
if (workspace) return workspace;
|
|
33
|
+
}
|
|
34
|
+
throw new Error("Unable to create a unique Codex projectless task directory.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function detectProjectlessWorkspace(cwd: string): ProjectlessWorkspace | undefined {
|
|
38
|
+
const normalized = path.resolve(cwd);
|
|
39
|
+
const match = normalized.match(/^(.*[\\/]Documents[\\/]Codex)[\\/]\d{4}-\d{2}-\d{2}[\\/][a-z0-9][a-z0-9-]*[\\/]*$/i);
|
|
40
|
+
if (!match?.[1]) return undefined;
|
|
41
|
+
return {
|
|
42
|
+
cwd: normalized,
|
|
43
|
+
outputDirectory: path.join(normalized, "outputs"),
|
|
44
|
+
workspaceRoot: match[1],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function projectlessDirectoryName(directoryName?: string, prompt?: string): string {
|
|
49
|
+
const words = (directoryName ?? prompt)?.toLowerCase().match(/[a-z0-9]+/g);
|
|
50
|
+
if (!words?.length) return "new-chat";
|
|
51
|
+
return (directoryName === undefined ? words.slice(0, 6) : words).join("-").slice(0, 80);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tryCreateWorkspace(
|
|
55
|
+
dateDirectory: string,
|
|
56
|
+
directoryName: string,
|
|
57
|
+
workspaceRoot: string,
|
|
58
|
+
): ProjectlessWorkspace | undefined {
|
|
59
|
+
const cwd = path.join(dateDirectory, directoryName);
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(cwd);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (isAlreadyExists(error)) return undefined;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const outputDirectory = path.join(cwd, "outputs");
|
|
67
|
+
fs.mkdirSync(outputDirectory);
|
|
68
|
+
fs.mkdirSync(path.join(cwd, "work"));
|
|
69
|
+
return { cwd, outputDirectory, workspaceRoot };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ensureRealDirectory(directory: string): void {
|
|
73
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
74
|
+
const metadata = fs.lstatSync(directory);
|
|
75
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
76
|
+
throw new Error(`Codex projectless task directory must be a real directory: ${directory}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatDate(date: Date): string {
|
|
81
|
+
return [
|
|
82
|
+
date.getFullYear(),
|
|
83
|
+
String(date.getMonth() + 1).padStart(2, "0"),
|
|
84
|
+
String(date.getDate()).padStart(2, "0"),
|
|
85
|
+
].join("-");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isAlreadyExists(error: unknown): boolean {
|
|
89
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
90
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type AppServerRequestId = string | number;
|
|
2
|
+
|
|
3
|
+
export interface AppServerRequest {
|
|
4
|
+
id: AppServerRequestId;
|
|
5
|
+
method: string;
|
|
6
|
+
params?: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AppServerNotification {
|
|
10
|
+
method: string;
|
|
11
|
+
params?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AppServerSuccess {
|
|
15
|
+
id: AppServerRequestId;
|
|
16
|
+
result: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AppServerFailure {
|
|
20
|
+
id: AppServerRequestId;
|
|
21
|
+
error: { code: number; message: string; data?: unknown };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AppServerResponse = AppServerSuccess | AppServerFailure;
|
|
25
|
+
export type AppServerMessage = AppServerRequest | AppServerNotification | AppServerResponse;
|
|
26
|
+
|
|
27
|
+
export function isResponse(message: AppServerMessage): message is AppServerResponse {
|
|
28
|
+
return "id" in message && !("method" in message) && ("result" in message || "error" in message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isRequest(message: AppServerMessage): message is AppServerRequest {
|
|
32
|
+
return "id" in message && "method" in message;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isNotification(message: AppServerMessage): message is AppServerNotification {
|
|
36
|
+
return !("id" in message) && "method" in message;
|
|
37
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Command } from "./commandTypes.js";
|
|
2
|
+
|
|
3
|
+
export class CommandRouter {
|
|
4
|
+
parse(text: string): Command {
|
|
5
|
+
const trimmed = text.trim();
|
|
6
|
+
if (trimmed.startsWith("!") || trimmed.startsWith("!")) {
|
|
7
|
+
const command = trimmed.slice(1).trim();
|
|
8
|
+
if (!command) throw new Error("请输入要执行的命令,例如:! ls 或 !ls");
|
|
9
|
+
return { type: "shell", command };
|
|
10
|
+
}
|
|
11
|
+
if (!trimmed.startsWith("/")) {
|
|
12
|
+
return { type: "prompt", text };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const [rawCommand, ...args] = splitArgs(trimmed);
|
|
16
|
+
const command = rawCommand.slice(1).toLowerCase();
|
|
17
|
+
|
|
18
|
+
switch (command) {
|
|
19
|
+
case "agents":
|
|
20
|
+
return { type: "agent" };
|
|
21
|
+
case "new":
|
|
22
|
+
return parseNewCommand(args);
|
|
23
|
+
case "newgroup":
|
|
24
|
+
return parseNewGroupCommand(args);
|
|
25
|
+
case "forkgroup":
|
|
26
|
+
return {
|
|
27
|
+
type: "forkgroup",
|
|
28
|
+
title: args.join(" ").trim() || undefined,
|
|
29
|
+
};
|
|
30
|
+
case "fork":
|
|
31
|
+
if (args.length > 1) throw new Error("/fork 只接受一个可选的任务序号或任务 ID。");
|
|
32
|
+
return args[0] ? { type: "fork", sessionId: args[0] } : { type: "fork" };
|
|
33
|
+
case "title":
|
|
34
|
+
if (args.length === 0) throw new Error("请输入新标题,例如:/title 修复会话列表。");
|
|
35
|
+
return { type: "title", title: args.join(" ") };
|
|
36
|
+
case "ask":
|
|
37
|
+
return { type: "ask", text: trimmed.slice(rawCommand.length).trim() };
|
|
38
|
+
case "queue":
|
|
39
|
+
case "nosteer": {
|
|
40
|
+
const prompt = trimmed.slice(rawCommand.length).trim();
|
|
41
|
+
if (!prompt) throw new Error(`请输入要排队的 Prompt,例如:/${command} 完成后再运行全部测试。`);
|
|
42
|
+
return { type: "nosteer", text: prompt };
|
|
43
|
+
}
|
|
44
|
+
case "sessions":
|
|
45
|
+
return { type: "sessions", searchTerm: trimmed.slice(rawCommand.length).trim() || undefined };
|
|
46
|
+
case "switch":
|
|
47
|
+
return args[0] ? { type: "switch", sessionId: args[0] } : { type: "switch" };
|
|
48
|
+
case "agent":
|
|
49
|
+
return args[0] ? { type: "agent", agent: args[0] } : { type: "agent" };
|
|
50
|
+
case "use":
|
|
51
|
+
return requireArg(args[0], "agent name", (agent) => ({ type: "use", agent, cwd: args[1] }));
|
|
52
|
+
case "stop":
|
|
53
|
+
return { type: "stop" };
|
|
54
|
+
case "status":
|
|
55
|
+
return args[0] ? { type: "status", sessionId: args[0] } : { type: "status" };
|
|
56
|
+
case "goal": {
|
|
57
|
+
if (args.length === 0) return { type: "goal", action: "show" };
|
|
58
|
+
const action = args[0]!.toLowerCase();
|
|
59
|
+
if (action === "pause" || action === "resume" || action === "clear") {
|
|
60
|
+
if (args.length > 1) throw new Error(`/goal ${action} 不接受额外参数。`);
|
|
61
|
+
return { type: "goal", action };
|
|
62
|
+
}
|
|
63
|
+
if (action === "edit") {
|
|
64
|
+
const objective = args.slice(1).join(" ").trim();
|
|
65
|
+
if (!objective) throw new Error("请输入修改后的 Goal,例如:/goal edit 完成迁移并通过全部测试。");
|
|
66
|
+
return { type: "goal", action: "edit", objective };
|
|
67
|
+
}
|
|
68
|
+
return { type: "goal", action: "set", objective: args.join(" ") };
|
|
69
|
+
}
|
|
70
|
+
case "restart":
|
|
71
|
+
return { type: "restart" };
|
|
72
|
+
case "modes":
|
|
73
|
+
return { type: "modes" };
|
|
74
|
+
case "mode":
|
|
75
|
+
return requireArg(args[0], "mode value", (value) => ({ type: "mode", value }));
|
|
76
|
+
case "model":
|
|
77
|
+
return { type: "model", model: args[0] };
|
|
78
|
+
case "thinking":
|
|
79
|
+
return { type: "thinking", effort: args[0] };
|
|
80
|
+
case "permissions": {
|
|
81
|
+
const mode = args[0];
|
|
82
|
+
if (mode !== undefined && mode !== "auto" && mode !== "confirm") {
|
|
83
|
+
throw new Error("权限模式只能是 auto 或 confirm。");
|
|
84
|
+
}
|
|
85
|
+
return { type: "permissions", mode };
|
|
86
|
+
}
|
|
87
|
+
case "help":
|
|
88
|
+
return { type: "help" };
|
|
89
|
+
default:
|
|
90
|
+
throw new Error(`未知命令:${rawCommand}。发送 /help 查看可用命令。`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requireArg<T>(value: string | undefined, name: string, create: (value: string) => T): T {
|
|
96
|
+
if (!value) {
|
|
97
|
+
throw new Error(`Missing ${name}.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return create(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseNewCommand(args: string[]): Extract<Command, { type: "new" }> {
|
|
104
|
+
const titleParts: string[] = [];
|
|
105
|
+
let cwd: string | undefined;
|
|
106
|
+
let projectless = false;
|
|
107
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
108
|
+
const argument = args[index]!;
|
|
109
|
+
if (argument === "--nodir") {
|
|
110
|
+
if (projectless) throw new Error("/new 只能指定一次 --nodir。");
|
|
111
|
+
if (cwd !== undefined) throw new Error("/new 的 --dir 和 --nodir 不能同时使用。");
|
|
112
|
+
projectless = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (argument !== "--dir") {
|
|
116
|
+
titleParts.push(argument);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (projectless) throw new Error("/new 的 --dir 和 --nodir 不能同时使用。");
|
|
120
|
+
if (cwd !== undefined) throw new Error("/new 只能指定一次 --dir。");
|
|
121
|
+
const directory = args[index + 1];
|
|
122
|
+
if (!directory || directory === "--dir") {
|
|
123
|
+
throw new Error("请在 --dir 后指定工作目录,例如:/new 修复会话列表 --dir D:\\dev\\project。");
|
|
124
|
+
}
|
|
125
|
+
cwd = directory;
|
|
126
|
+
index += 1;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
type: "new",
|
|
130
|
+
title: titleParts.join(" ").trim() || undefined,
|
|
131
|
+
cwd,
|
|
132
|
+
...(projectless ? { projectless: true } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseNewGroupCommand(args: string[]): Extract<Command, { type: "newgroup" }> {
|
|
137
|
+
if (args.includes("--dir")) {
|
|
138
|
+
throw new Error("/newgroup 只创建飞书群,不支持 --dir;请在新群中使用 /new --dir <cwd> 创建任务。");
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
type: "newgroup",
|
|
142
|
+
title: args.join(" ").trim() || undefined,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function splitArgs(input: string): string[] {
|
|
147
|
+
const args: string[] = [];
|
|
148
|
+
let current = "";
|
|
149
|
+
let quote: string | undefined;
|
|
150
|
+
|
|
151
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
152
|
+
const char = input[index];
|
|
153
|
+
if ((char === '"' || char === "'") && !quote) {
|
|
154
|
+
quote = char;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (char === quote) {
|
|
159
|
+
quote = undefined;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (/\s/.test(char) && !quote) {
|
|
164
|
+
if (current) {
|
|
165
|
+
args.push(current);
|
|
166
|
+
current = "";
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
current += char;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (current) {
|
|
175
|
+
args.push(current);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return args;
|
|
179
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PermissionMode } from "../runtime/types.js";
|
|
2
|
+
|
|
3
|
+
export type Command =
|
|
4
|
+
| { type: "shell"; command: string }
|
|
5
|
+
| { type: "new"; title?: string; cwd?: string; projectless?: boolean }
|
|
6
|
+
| { type: "newgroup"; title?: string }
|
|
7
|
+
| { type: "forkgroup"; title?: string }
|
|
8
|
+
| { type: "fork"; sessionId?: string }
|
|
9
|
+
| { type: "title"; title: string }
|
|
10
|
+
| { type: "ask"; text: string }
|
|
11
|
+
| { type: "nosteer"; text: string }
|
|
12
|
+
| { type: "sessions"; searchTerm?: string }
|
|
13
|
+
| { type: "switch"; sessionId?: string }
|
|
14
|
+
| { type: "agent"; agent?: string }
|
|
15
|
+
| { type: "use"; agent: string; cwd?: string }
|
|
16
|
+
| { type: "stop" }
|
|
17
|
+
| { type: "status"; sessionId?: string }
|
|
18
|
+
| { type: "goal"; action: "show" }
|
|
19
|
+
| { type: "goal"; action: "set" | "edit"; objective: string }
|
|
20
|
+
| { type: "goal"; action: "pause" | "resume" | "clear" }
|
|
21
|
+
| { type: "restart" }
|
|
22
|
+
| { type: "modes" }
|
|
23
|
+
| { type: "mode"; value: string }
|
|
24
|
+
| { type: "model"; model?: string }
|
|
25
|
+
| { type: "thinking"; effort?: string }
|
|
26
|
+
| { type: "permissions"; mode?: PermissionMode }
|
|
27
|
+
| { type: "help" }
|
|
28
|
+
| { type: "prompt"; text: string };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { config as loadDotEnv } from "dotenv";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { appConfigSchema, type AppConfig } from "./schema.js";
|
|
6
|
+
import { defaultConfigPath, defaultDotEnvPath, resolveUserPath } from "./paths.js";
|
|
7
|
+
|
|
8
|
+
const ENV_PATTERN = /\$\{([A-Z0-9_]+)\}/gi;
|
|
9
|
+
const DEFAULT_CONFIG = `feishu:
|
|
10
|
+
transport: "auto"
|
|
11
|
+
appId: "\${FEISHU_APP_ID}"
|
|
12
|
+
appSecret: "\${FEISHU_APP_SECRET}"
|
|
13
|
+
useConsoleWhenMissingCredentials: true
|
|
14
|
+
|
|
15
|
+
console:
|
|
16
|
+
enabled: true
|
|
17
|
+
|
|
18
|
+
agents:
|
|
19
|
+
codex:
|
|
20
|
+
kind: "codex"
|
|
21
|
+
title: "Codex"
|
|
22
|
+
command: "codex"
|
|
23
|
+
args:
|
|
24
|
+
- "app-server"
|
|
25
|
+
- "--enable"
|
|
26
|
+
- "goals"
|
|
27
|
+
- "--listen"
|
|
28
|
+
- "stdio://"
|
|
29
|
+
env: {}
|
|
30
|
+
|
|
31
|
+
coco:
|
|
32
|
+
title: "Coco"
|
|
33
|
+
command: "coco"
|
|
34
|
+
args:
|
|
35
|
+
- "acp"
|
|
36
|
+
- "serve"
|
|
37
|
+
env: {}
|
|
38
|
+
|
|
39
|
+
coco-yolo:
|
|
40
|
+
title: "Coco YOLO"
|
|
41
|
+
command: "coco"
|
|
42
|
+
args:
|
|
43
|
+
- "acp"
|
|
44
|
+
- "serve"
|
|
45
|
+
- "--yolo"
|
|
46
|
+
env: {}
|
|
47
|
+
|
|
48
|
+
example:
|
|
49
|
+
title: "Example ACP Agent"
|
|
50
|
+
command: "node"
|
|
51
|
+
args:
|
|
52
|
+
- "./examples/example-acp-agent.js"
|
|
53
|
+
env: {}
|
|
54
|
+
|
|
55
|
+
defaults:
|
|
56
|
+
agent: "codex"
|
|
57
|
+
cwd: "."
|
|
58
|
+
|
|
59
|
+
storage:
|
|
60
|
+
sqlitePath: "./data/agent-bot.sqlite"
|
|
61
|
+
|
|
62
|
+
logging:
|
|
63
|
+
level: "info"
|
|
64
|
+
path: "./logs/agent-bot.log"
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
export function loadConfig(
|
|
68
|
+
configPath?: string,
|
|
69
|
+
): AppConfig {
|
|
70
|
+
loadDotEnv({ path: defaultDotEnvPath(), quiet: true });
|
|
71
|
+
|
|
72
|
+
const configuredPath = firstNonBlank(configPath, process.env.AGENT_BOT_CONFIG);
|
|
73
|
+
const absolutePath = resolveUserPath(configuredPath ?? defaultConfigPath());
|
|
74
|
+
if (!fs.existsSync(absolutePath)) {
|
|
75
|
+
if (configuredPath) throw new Error(`Config file does not exist: ${absolutePath}`);
|
|
76
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
77
|
+
fs.writeFileSync(absolutePath, DEFAULT_CONFIG);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const raw = fs.readFileSync(absolutePath, "utf8");
|
|
81
|
+
const expanded = expandEnv(raw);
|
|
82
|
+
const parsed = YAML.parse(expanded) as unknown;
|
|
83
|
+
const config = appConfigSchema.parse(parsed);
|
|
84
|
+
|
|
85
|
+
const defaultAgent = config.defaults.agent ?? Object.keys(config.agents)[0];
|
|
86
|
+
if (!config.agents[defaultAgent]) {
|
|
87
|
+
throw new Error(`Default agent "${defaultAgent}" is not configured.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const configDirectory = path.dirname(absolutePath);
|
|
91
|
+
const sqlitePath = resolveUserPath(config.storage.sqlitePath, configDirectory);
|
|
92
|
+
return {
|
|
93
|
+
...config,
|
|
94
|
+
defaults: {
|
|
95
|
+
...config.defaults,
|
|
96
|
+
agent: defaultAgent,
|
|
97
|
+
cwd: path.resolve(config.defaults.cwd),
|
|
98
|
+
},
|
|
99
|
+
storage: {
|
|
100
|
+
sqlitePath,
|
|
101
|
+
},
|
|
102
|
+
logging: {
|
|
103
|
+
...config.logging,
|
|
104
|
+
path: resolveUserPath(config.logging.path, configDirectory),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function expandEnv(raw: string): string {
|
|
110
|
+
return raw.replace(ENV_PATTERN, (_match, name: string) => process.env[name] ?? "");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function firstNonBlank(...values: Array<string | undefined>): string | undefined {
|
|
114
|
+
return values.find((value) => value !== undefined && value.trim().length > 0);
|
|
115
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const AGENT_BOT_HOME_ENV = "AGENT_BOT_HOME";
|
|
5
|
+
export const DEFAULT_CONFIG_FILE = "config.yaml";
|
|
6
|
+
export const DEFAULT_ENV_FILE = ".env";
|
|
7
|
+
export const DEFAULT_SQLITE_PATH = "./data/agent-bot.sqlite";
|
|
8
|
+
export const DEFAULT_LOG_PATH = "./logs/agent-bot.log";
|
|
9
|
+
|
|
10
|
+
export function agentBotHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
11
|
+
return path.resolve(expandHome(env[AGENT_BOT_HOME_ENV] ?? path.join(os.homedir(), ".agent-bot")));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function defaultConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
15
|
+
return path.join(agentBotHome(env), DEFAULT_CONFIG_FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function defaultDotEnvPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
19
|
+
return path.join(agentBotHome(env), DEFAULT_ENV_FILE);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function defaultSqlitePath(env: NodeJS.ProcessEnv = process.env): string {
|
|
23
|
+
return path.join(agentBotHome(env), "data", "agent-bot.sqlite");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveUserPath(input: string, baseDirectory = process.cwd()): string {
|
|
27
|
+
const expanded = expandHome(input);
|
|
28
|
+
return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(baseDirectory, expanded);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function expandHome(input: string): string {
|
|
32
|
+
if (input === "~") return os.homedir();
|
|
33
|
+
if (input.startsWith("~/") || input.startsWith("~\\")) {
|
|
34
|
+
return path.join(os.homedir(), input.slice(2));
|
|
35
|
+
}
|
|
36
|
+
return input;
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_LOG_PATH, DEFAULT_SQLITE_PATH } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
const envRecordSchema = z.record(z.string(), z.string()).default({});
|
|
5
|
+
|
|
6
|
+
export const agentConfigSchema = z.object({
|
|
7
|
+
kind: z.enum(["acp", "codex"]).default("acp"),
|
|
8
|
+
title: z.string().min(1),
|
|
9
|
+
command: z.string().min(1),
|
|
10
|
+
args: z.array(z.string()).default([]),
|
|
11
|
+
env: envRecordSchema,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const appConfigSchema = z.object({
|
|
15
|
+
console: z
|
|
16
|
+
.object({
|
|
17
|
+
enabled: z.boolean().default(true),
|
|
18
|
+
})
|
|
19
|
+
.default({ enabled: true }),
|
|
20
|
+
feishu: z.object({
|
|
21
|
+
transport: z.enum(["auto", "sdk", "console"]).default("auto"),
|
|
22
|
+
appId: z.string().optional(),
|
|
23
|
+
appSecret: z.string().optional(),
|
|
24
|
+
useConsoleWhenMissingCredentials: z.boolean().default(true),
|
|
25
|
+
}),
|
|
26
|
+
agents: z.record(z.string().min(1), agentConfigSchema).refine(
|
|
27
|
+
(agents) => Object.keys(agents).length > 0,
|
|
28
|
+
"At least one agent must be configured.",
|
|
29
|
+
),
|
|
30
|
+
defaults: z
|
|
31
|
+
.object({
|
|
32
|
+
agent: z.string().optional(),
|
|
33
|
+
cwd: z.string().default("."),
|
|
34
|
+
})
|
|
35
|
+
.default({ cwd: "." }),
|
|
36
|
+
storage: z.object({
|
|
37
|
+
sqlitePath: z.string().default(DEFAULT_SQLITE_PATH),
|
|
38
|
+
}).default({ sqlitePath: DEFAULT_SQLITE_PATH }),
|
|
39
|
+
logging: z.object({
|
|
40
|
+
level: z.enum(["trace", "debug", "info", "warn", "error", "fatal"]).default("info"),
|
|
41
|
+
path: z.string().optional().default(DEFAULT_LOG_PATH),
|
|
42
|
+
}).default({ level: "info", path: DEFAULT_LOG_PATH }),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type AgentConfig = z.infer<typeof agentConfigSchema>;
|
|
46
|
+
export type AppConfig = z.infer<typeof appConfigSchema>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import type { Logger } from "pino";
|
|
3
|
+
import type { FeishuEventHandler, IncomingMessage } from "../feishu/types.js";
|
|
4
|
+
|
|
5
|
+
export class ConsoleConnector {
|
|
6
|
+
private reader?: readline.Interface;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly handler: FeishuEventHandler,
|
|
10
|
+
private readonly logger: Logger,
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
start(): void {
|
|
14
|
+
if (this.reader) return;
|
|
15
|
+
this.logger.info("Starting local Codex test console.");
|
|
16
|
+
console.log("Codex console ready. Type /help or enter a prompt.");
|
|
17
|
+
const reader = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "agent-bot> " });
|
|
18
|
+
this.reader = reader;
|
|
19
|
+
reader.prompt();
|
|
20
|
+
reader.on("line", (line) => {
|
|
21
|
+
const message: IncomingMessage = {
|
|
22
|
+
messageId: `console-${Date.now()}`,
|
|
23
|
+
contextKey: "console:local",
|
|
24
|
+
userId: "local",
|
|
25
|
+
text: line,
|
|
26
|
+
};
|
|
27
|
+
void this.handler.onMessage(message).catch((error) => {
|
|
28
|
+
this.logger.error({ error }, "Console message handling failed.");
|
|
29
|
+
}).finally(() => reader.prompt());
|
|
30
|
+
});
|
|
31
|
+
reader.on("close", () => {
|
|
32
|
+
if (this.reader === reader) this.reader = undefined;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
stop(): void {
|
|
37
|
+
this.reader?.close();
|
|
38
|
+
this.reader = undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AgentEvent } from "../runtime/types.js";
|
|
2
|
+
import type { FeishuOutbound } from "../feishu/types.js";
|
|
3
|
+
import type { TurnPresenter } from "../presentation/OutboundRouter.js";
|
|
4
|
+
|
|
5
|
+
export class ConsoleTurnPresenter implements TurnPresenter {
|
|
6
|
+
private readonly contexts = new Map<string, string>();
|
|
7
|
+
private readonly delivered = new Set<string>();
|
|
8
|
+
|
|
9
|
+
constructor(private readonly outbound: FeishuOutbound) {}
|
|
10
|
+
|
|
11
|
+
registerSession(sessionId: string, contextKey: string): void {
|
|
12
|
+
this.contexts.set(sessionId, contextKey);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
updateSessionTitle(): void {}
|
|
16
|
+
|
|
17
|
+
unregisterSession(sessionId: string): void {
|
|
18
|
+
this.contexts.delete(sessionId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async startPendingTurn(_sessionId: string, contextKey: string): Promise<void> {
|
|
22
|
+
await this.outbound.sendText(contextKey, "Connecting to Codex...");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async failPendingTurn(sessionId: string, message: string): Promise<void> {
|
|
26
|
+
const contextKey = this.contexts.get(sessionId);
|
|
27
|
+
if (contextKey) await this.outbound.sendText(contextKey, `Codex failed to start: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async appendSteerMessage(): Promise<void> {}
|
|
31
|
+
|
|
32
|
+
async onEvent(event: AgentEvent): Promise<void> {
|
|
33
|
+
const contextKey = this.contexts.get(event.sessionId);
|
|
34
|
+
if (!contextKey) return;
|
|
35
|
+
if (event.type === "turn_started") {
|
|
36
|
+
await this.outbound.sendText(contextKey, `Codex started (${event.turnId})`);
|
|
37
|
+
} else if (event.type === "progress") {
|
|
38
|
+
await this.outbound.sendText(contextKey, event.text);
|
|
39
|
+
} else if (event.type === "tool_started") {
|
|
40
|
+
await this.outbound.sendText(contextKey, `> ${event.tool.title}`);
|
|
41
|
+
} else if (event.type === "approval_requested") {
|
|
42
|
+
await this.outbound.sendText(contextKey, `Approval required: ${event.request.title}. Use Feishu or /permissions auto.`);
|
|
43
|
+
} else if (event.type === "turn_completed" && !this.delivered.has(event.turnId)) {
|
|
44
|
+
this.delivered.add(event.turnId);
|
|
45
|
+
await this.outbound.sendMarkdown(contextKey, event.finalResponse || "(completed without text)");
|
|
46
|
+
} else if (event.type === "turn_failed") {
|
|
47
|
+
await this.outbound.sendText(contextKey, `Codex failed: ${event.message}`);
|
|
48
|
+
} else if (event.type === "turn_cancelled") {
|
|
49
|
+
await this.outbound.sendText(contextKey, "Codex stopped.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async showDetails(contextKey: string, turnId: string): Promise<void> {
|
|
54
|
+
await this.outbound.sendText(contextKey, `Turn details are available in Feishu (${turnId}).`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async showActivityPage(contextKey: string, turnId: string): Promise<void> {
|
|
58
|
+
await this.outbound.sendText(contextKey, `Turn activity pages are available in Feishu (${turnId}).`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async resumeDelivery(): Promise<void> {}
|
|
62
|
+
|
|
63
|
+
async flushAll(): Promise<void> {}
|
|
64
|
+
}
|