@keyou007/agent-bot 0.1.13-alpha.1 → 0.1.13-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 +6 -0
- package/README.md +106 -103
- package/README.zh.md +76 -73
- package/dist/acp/AcpProcessManager.js +8 -3
- package/dist/acp/AcpProcessManager.js.map +1 -1
- package/dist/acp/AcpSessionManager.js +15 -0
- package/dist/acp/AcpSessionManager.js.map +1 -1
- package/dist/cli/help.js +4 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/serverStatus.js +27 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli/taskListOutput.js +72 -0
- package/dist/cli/taskListOutput.js.map +1 -0
- package/dist/cli.js +49 -54
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +34 -4
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +3 -0
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/feishu/CardRenderer.js +43 -2
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +16 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +157 -59
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/AcpRuntimeAdapter.js +3 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/runtime/agentEnvironment.js +24 -4
- package/dist/runtime/agentEnvironment.js.map +1 -1
- package/dist/runtime/createAgentRuntimeRegistry.js +7 -3
- package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
- package/dist/state/StateStore.js +12 -9
- package/dist/state/StateStore.js.map +1 -1
- package/docs/technical-reference.md +9 -3
- package/docs/technical-reference.zh.md +9 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +53 -22
- package/src/acp/AcpProcessManager.ts +16 -3
- package/src/acp/AcpSessionManager.ts +14 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/serverStatus.ts +28 -0
- package/src/cli/taskListOutput.ts +91 -0
- package/src/cli.ts +66 -51
- package/src/codex/CodexProcessManager.ts +38 -3
- package/src/codex/CodexRuntime.ts +6 -0
- package/src/feishu/CardRenderer.ts +61 -1
- package/src/feishu/FeishuConnector.ts +13 -4
- package/src/index.ts +35 -3
- package/src/proxy/ProxySessionController.ts +186 -59
- package/src/runtime/AcpRuntimeAdapter.ts +5 -0
- package/src/runtime/agentEnvironment.ts +41 -3
- package/src/runtime/createAgentRuntimeRegistry.ts +18 -3
- package/src/runtime/types.ts +5 -0
- package/src/state/StateStore.ts +12 -9
|
@@ -3,7 +3,10 @@ import readline from "node:readline";
|
|
|
3
3
|
import type { Logger } from "pino";
|
|
4
4
|
import type { AgentConfig } from "../config/schema.js";
|
|
5
5
|
import { AcpJsonRpcConnection } from "./AcpJsonRpcConnection.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
agentBotEnvironment,
|
|
8
|
+
type AgentEnvironmentContext,
|
|
9
|
+
} from "../runtime/agentEnvironment.js";
|
|
7
10
|
import { spawnStdioCommand } from "../utils/spawnCommand.js";
|
|
8
11
|
|
|
9
12
|
export interface ManagedAcpProcess {
|
|
@@ -16,14 +19,24 @@ export interface ManagedAcpProcess {
|
|
|
16
19
|
export class AcpProcessManager {
|
|
17
20
|
private readonly processes = new Map<string, ManagedAcpProcess>();
|
|
18
21
|
|
|
19
|
-
constructor(
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly logger: Logger,
|
|
24
|
+
private readonly environmentContext: () => AgentEnvironmentContext = () => ({}),
|
|
25
|
+
) {}
|
|
20
26
|
|
|
21
27
|
start(processKey: string, agentName: string, agent: AgentConfig): ManagedAcpProcess {
|
|
22
28
|
if (this.processes.has(processKey)) {
|
|
23
29
|
throw new Error(`ACP process already exists: ${processKey}`);
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
const child = spawnStdioCommand(
|
|
32
|
+
const child = spawnStdioCommand(
|
|
33
|
+
agent.command,
|
|
34
|
+
agent.args,
|
|
35
|
+
agentBotEnvironment(process.env, agent.env, {
|
|
36
|
+
...this.environmentContext(),
|
|
37
|
+
agentName,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
27
40
|
|
|
28
41
|
const childLogger = this.logger.child({ processKey, agentName });
|
|
29
42
|
const connection = new AcpJsonRpcConnection(child, childLogger);
|
|
@@ -40,6 +40,7 @@ export class AcpSessionManager {
|
|
|
40
40
|
private readonly sessions = new Map<string, RuntimeSession>();
|
|
41
41
|
private readonly sessionsByAcpId = new Map<string, RuntimeSession>();
|
|
42
42
|
private processStart?: Promise<ManagedAcpProcess>;
|
|
43
|
+
private processVersion?: string;
|
|
43
44
|
|
|
44
45
|
constructor(
|
|
45
46
|
private readonly agentName: string,
|
|
@@ -52,6 +53,15 @@ export class AcpSessionManager {
|
|
|
52
53
|
return this.sessions.get(localSessionId);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
getProcessInfo(): { pid?: number; version?: string } {
|
|
57
|
+
const pid = this.processManager.get(this.agentName)?.child.pid;
|
|
58
|
+
if (!pid) return {};
|
|
59
|
+
return {
|
|
60
|
+
pid,
|
|
61
|
+
...(this.processVersion ? { version: this.processVersion } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
async create(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
56
66
|
if (input.agentName !== this.agentName) throw new Error(`Unexpected agent: ${input.agentName}`);
|
|
57
67
|
|
|
@@ -133,6 +143,7 @@ export class AcpSessionManager {
|
|
|
133
143
|
this.sessions.clear();
|
|
134
144
|
this.sessionsByAcpId.clear();
|
|
135
145
|
this.processStart = undefined;
|
|
146
|
+
this.processVersion = undefined;
|
|
136
147
|
this.processManager.stopAll();
|
|
137
148
|
}
|
|
138
149
|
|
|
@@ -181,6 +192,7 @@ export class AcpSessionManager {
|
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
private async startProcess(): Promise<ManagedAcpProcess> {
|
|
195
|
+
this.processVersion = undefined;
|
|
184
196
|
const managed = this.processManager.start(this.agentName, this.agentName, this.agent);
|
|
185
197
|
try {
|
|
186
198
|
const connection = managed.connection;
|
|
@@ -210,6 +222,8 @@ export class AcpSessionManager {
|
|
|
210
222
|
version: "0.1.0",
|
|
211
223
|
},
|
|
212
224
|
});
|
|
225
|
+
const version = initializeResult.agentInfo?.version?.trim();
|
|
226
|
+
if (version) this.processVersion = version;
|
|
213
227
|
if (initializeResult.authMethods?.length) {
|
|
214
228
|
const method = initializeResult.authMethods[0];
|
|
215
229
|
this.logger.info({ methodId: method.id }, "Authenticating ACP agent with first advertised method.");
|
package/src/cli/help.ts
CHANGED
|
@@ -13,10 +13,11 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
|
|
|
13
13
|
init --reset 重置并重新配置显式指定的 Profile
|
|
14
14
|
console 打开本地控制台
|
|
15
15
|
server start 在后台启动 Agent Bot
|
|
16
|
-
server status 显示服务、飞书 App ID
|
|
16
|
+
server status 显示服务、飞书 App ID、Agent 进程和安全重启状态
|
|
17
17
|
server restart [--task <任务>] 安排安全重启并将状态发回指定任务会话
|
|
18
18
|
server stop 停止 Agent Bot
|
|
19
19
|
task list 列出任务
|
|
20
|
+
task current [--json] 显示当前调用任务的详情
|
|
20
21
|
task status <任务> 显示任务状态
|
|
21
22
|
task chat <任务> 显示任务绑定的飞书会话
|
|
22
23
|
task prompt <任务> <提示词> 向任务发送提示词
|
|
@@ -66,10 +67,11 @@ Common commands:
|
|
|
66
67
|
init --reset Reset and reconfigure an explicit profile
|
|
67
68
|
console Open the local console
|
|
68
69
|
server start Start Agent Bot in the background
|
|
69
|
-
server status Show the server, Lark App ID, and safe-restart status
|
|
70
|
+
server status Show the server, Lark App ID, Agent processes, and safe-restart status
|
|
70
71
|
server restart [--task <task>] Schedule a safe restart and return status to the task's conversation
|
|
71
72
|
server stop Stop Agent Bot
|
|
72
73
|
task list List tasks
|
|
74
|
+
task current [--json] Show details for the task invoking the CLI
|
|
73
75
|
task status <task> Show task status
|
|
74
76
|
task chat <task> Show the task's Lark chat binding
|
|
75
77
|
task prompt <task> <prompt> Send a Prompt to a task
|
package/src/cli/serverStatus.ts
CHANGED
|
@@ -29,16 +29,44 @@ export function formatServerStatus(data: unknown, language: CliLanguage = cliLan
|
|
|
29
29
|
language,
|
|
30
30
|
)
|
|
31
31
|
: cliText("not scheduled", "未计划", language);
|
|
32
|
+
const agents = formatAgentProcesses(value.agents, language);
|
|
32
33
|
return [
|
|
33
34
|
`${cliText("Agent Bot server: ", "Agent Bot 服务:", language)}${state}`,
|
|
34
35
|
`${cliText("Lark App ID: ", "飞书 App ID:", language)}${value.feishuAppId ?? cliText("not configured", "未配置", language)}`,
|
|
35
36
|
`${cliText("PID: ", "PID:", language)}${value.pid ?? "-"}`,
|
|
36
37
|
`${cliText("Started at: ", "启动时间:", language)}${value.startedAt ?? "-"}`,
|
|
37
38
|
`${cliText("Supervisor: ", "Supervisor:", language)}${value.supervised ? cliText("enabled", "已启用", language) : cliText("disabled", "未启用", language)}`,
|
|
39
|
+
...agents,
|
|
38
40
|
`${cliText("Running tasks: ", "运行中任务:", language)}${activity.runningSessions ?? 0}`,
|
|
39
41
|
`${cliText("Pending final deliveries: ", "待发送最终结果:", language)}${activity.pendingFinalDeliveries ?? 0}`,
|
|
40
42
|
`${cliText("Safe restart: ", "安全重启:", language)}${safeRestart}`,
|
|
41
43
|
"",
|
|
42
44
|
].join("\n");
|
|
43
45
|
}
|
|
46
|
+
|
|
47
|
+
function formatAgentProcesses(value: unknown, language: CliLanguage): string[] {
|
|
48
|
+
const agents = Array.isArray(value)
|
|
49
|
+
? value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object")
|
|
50
|
+
: [];
|
|
51
|
+
if (agents.length === 0) {
|
|
52
|
+
return [`${cliText("Agents: ", "Agent 进程:", language)}-`];
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
cliText("Agents:", "Agent 进程:", language),
|
|
56
|
+
...agents.map((agent) => {
|
|
57
|
+
const name = typeof agent.name === "string" && agent.name.trim() ? agent.name.trim() : "-";
|
|
58
|
+
const title = typeof agent.title === "string" && agent.title.trim() ? agent.title.trim() : undefined;
|
|
59
|
+
const label = title && title !== name ? `${name} (${title})` : name;
|
|
60
|
+
const pid = typeof agent.pid === "number" && Number.isInteger(agent.pid) && agent.pid > 0
|
|
61
|
+
? String(agent.pid)
|
|
62
|
+
: "-";
|
|
63
|
+
const version = typeof agent.version === "string" && agent.version.trim()
|
|
64
|
+
? agent.version.trim()
|
|
65
|
+
: "-";
|
|
66
|
+
return language === "zh"
|
|
67
|
+
? ` ${label}:PID ${pid},版本 ${version}`
|
|
68
|
+
: ` ${label}: PID ${pid}, version ${version}`;
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
44
72
|
import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { SessionRecord } from "../state/StateStore.js";
|
|
2
|
+
import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
|
|
3
|
+
|
|
4
|
+
export type CurrentTaskResolution =
|
|
5
|
+
| { status: "found"; session: SessionRecord }
|
|
6
|
+
| { status: "missing-thread-id" }
|
|
7
|
+
| { status: "not-found"; threadIds: string[] }
|
|
8
|
+
| { status: "ambiguous"; sessions: SessionRecord[]; threadIds: string[] };
|
|
9
|
+
|
|
10
|
+
export function formatTaskList(
|
|
11
|
+
sessions: SessionRecord[],
|
|
12
|
+
language: CliLanguage = cliLanguage,
|
|
13
|
+
currentThreadIds: readonly string[] = [],
|
|
14
|
+
): string {
|
|
15
|
+
if (sessions.length === 0) return cliText("No tasks.\n", "没有任务。\n", language);
|
|
16
|
+
|
|
17
|
+
const current = resolveCurrentTask(sessions, currentThreadIds);
|
|
18
|
+
const currentLocalSessionId = current.status === "found" ? current.session.localSessionId : undefined;
|
|
19
|
+
return sessions.map((session, index) => {
|
|
20
|
+
const title = session.title ?? cliText("Untitled task", "未命名任务", language);
|
|
21
|
+
const current = session.localSessionId === currentLocalSessionId
|
|
22
|
+
? ` [${cliText("Current", "当前任务", language)}]`
|
|
23
|
+
: "";
|
|
24
|
+
const nativeId = nativeTaskIdLabel(session, language);
|
|
25
|
+
return [
|
|
26
|
+
`${index + 1}.${current} [${taskStateLabel(session.status, language)}/${taskStateLabel(session.lastTurnStatus, language)}] ${title}`,
|
|
27
|
+
` ${cliText("Agent: ", "Agent:", language)}${session.agentName} · ${cliText("AgentBot task ID: ", "AgentBot 任务 ID:", language)}${session.localSessionId}`,
|
|
28
|
+
` ${nativeId ? `${nativeId} · ` : ""}${session.contextKey} · ${session.updatedAt}`,
|
|
29
|
+
].join("\n");
|
|
30
|
+
}).join("\n") + "\n";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function currentAppServerThreadIds(env: NodeJS.ProcessEnv): string[] {
|
|
34
|
+
return [...new Set([
|
|
35
|
+
env.CODEX_THREAD_ID?.trim(),
|
|
36
|
+
env.TRAECLI_THREAD_ID?.trim(),
|
|
37
|
+
].filter((value): value is string => Boolean(value)))];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveCurrentTask(
|
|
41
|
+
sessions: SessionRecord[],
|
|
42
|
+
currentThreadIds: readonly string[],
|
|
43
|
+
): CurrentTaskResolution {
|
|
44
|
+
const threadIds = [...new Set(currentThreadIds.map((value) => value.trim()).filter(Boolean))];
|
|
45
|
+
if (threadIds.length === 0) return { status: "missing-thread-id" };
|
|
46
|
+
|
|
47
|
+
const candidates = new Set(threadIds);
|
|
48
|
+
const matches = sessions.filter((session) => session.remoteSessionId && candidates.has(session.remoteSessionId));
|
|
49
|
+
if (matches.length === 0) return { status: "not-found", threadIds };
|
|
50
|
+
if (matches.length === 1) return { status: "found", session: matches[0]! };
|
|
51
|
+
|
|
52
|
+
const running = matches.filter((session) =>
|
|
53
|
+
session.status === "running" || session.lastTurnStatus === "running");
|
|
54
|
+
if (running.length === 1) return { status: "found", session: running[0]! };
|
|
55
|
+
return { status: "ambiguous", sessions: matches, threadIds };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function taskStateLabel(
|
|
59
|
+
status: string | undefined,
|
|
60
|
+
language: CliLanguage = cliLanguage,
|
|
61
|
+
): string {
|
|
62
|
+
if (!status) return "-";
|
|
63
|
+
const chinese: Record<string, string> = {
|
|
64
|
+
starting: "启动中",
|
|
65
|
+
ready: "就绪",
|
|
66
|
+
running: "运行中",
|
|
67
|
+
closed: "已关闭",
|
|
68
|
+
failed: "失败",
|
|
69
|
+
completed: "已完成",
|
|
70
|
+
cancelled: "已取消",
|
|
71
|
+
interrupted: "已中断",
|
|
72
|
+
inProgress: "进行中",
|
|
73
|
+
active: "活动中",
|
|
74
|
+
idle: "空闲",
|
|
75
|
+
not_loaded: "未加载",
|
|
76
|
+
error: "错误",
|
|
77
|
+
};
|
|
78
|
+
return language === "zh" ? chinese[status] ?? status : status;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function nativeTaskIdLabel(session: SessionRecord, language: CliLanguage): string | undefined {
|
|
82
|
+
if (session.runtimeKind === "acp" || session.acpSessionId) {
|
|
83
|
+
const sessionId = session.acpSessionId ?? session.remoteSessionId;
|
|
84
|
+
return sessionId
|
|
85
|
+
? `${cliText("ACP session ID: ", "ACP Session ID:", language)}${sessionId}`
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
return session.remoteSessionId
|
|
89
|
+
? `${cliText("App Server thread ID: ", "App Server 原生任务 ID(Thread ID):", language)}${session.remoteSessionId}`
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -65,6 +65,12 @@ import {
|
|
|
65
65
|
type ServerStartResult,
|
|
66
66
|
} from "./cli/ServerStarter.js";
|
|
67
67
|
import { taskChatRoute } from "./cli/taskChatRoute.js";
|
|
68
|
+
import {
|
|
69
|
+
currentAppServerThreadIds,
|
|
70
|
+
formatTaskList,
|
|
71
|
+
resolveCurrentTask,
|
|
72
|
+
taskStateLabel,
|
|
73
|
+
} from "./cli/taskListOutput.js";
|
|
68
74
|
import { isThreadContextKey } from "./feishu/contextKey.js";
|
|
69
75
|
import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
|
|
70
76
|
import {
|
|
@@ -724,6 +730,16 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
724
730
|
const store = new StateStore(config.storage.sqlitePath);
|
|
725
731
|
try {
|
|
726
732
|
const allSessions = store.listAllSessions();
|
|
733
|
+
if (action === "current") {
|
|
734
|
+
const unsupported = rest.filter((value) => value !== "--json");
|
|
735
|
+
if (unsupported.length > 0) throw new Error(cliText(
|
|
736
|
+
`task current does not accept arguments: ${unsupported.join(" ")}`,
|
|
737
|
+
`task current 不接受这些参数:${unsupported.join(" ")}`,
|
|
738
|
+
));
|
|
739
|
+
const session = resolveCurrentTaskFromEnvironment(allSessions);
|
|
740
|
+
await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
727
743
|
if (action === "prompt" || action === "send") {
|
|
728
744
|
const [reference, ...promptParts] = rest;
|
|
729
745
|
if (!reference) throw new Error(cliText(
|
|
@@ -809,28 +825,7 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
809
825
|
return;
|
|
810
826
|
}
|
|
811
827
|
if (action === "status") {
|
|
812
|
-
|
|
813
|
-
let live: TaskStatusControlData | undefined;
|
|
814
|
-
let response: ControlResponse | undefined;
|
|
815
|
-
try {
|
|
816
|
-
response = await sendControlRequest(endpoint, {
|
|
817
|
-
action: "task_status",
|
|
818
|
-
localSessionId: session.localSessionId,
|
|
819
|
-
});
|
|
820
|
-
} catch {
|
|
821
|
-
// The server can be stopped or still run an older control protocol
|
|
822
|
-
// while a safe restart is pending. Preserve the local read-only fallback.
|
|
823
|
-
}
|
|
824
|
-
if (response) {
|
|
825
|
-
ensureOk(response);
|
|
826
|
-
live = response.data as TaskStatusControlData;
|
|
827
|
-
}
|
|
828
|
-
const statusSession = live?.session ?? session;
|
|
829
|
-
const snapshot = live?.snapshot
|
|
830
|
-
?? (statusSession.lastTurnId ? store.getTurnSnapshot(statusSession.lastTurnId) : undefined);
|
|
831
|
-
const result = { ...statusSession, snapshot, ...(live?.remote ? { remote: live.remote } : {}) };
|
|
832
|
-
if (rest.includes("--json")) printJson(result);
|
|
833
|
-
else printTaskStatus(statusSession, snapshot, live?.remote);
|
|
828
|
+
await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
|
|
834
829
|
return;
|
|
835
830
|
}
|
|
836
831
|
const endpoint = controlEndpoint(config.storage.sqlitePath);
|
|
@@ -862,6 +857,53 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
862
857
|
}
|
|
863
858
|
}
|
|
864
859
|
|
|
860
|
+
function resolveCurrentTaskFromEnvironment(sessions: SessionRecord[]): SessionRecord {
|
|
861
|
+
const resolution = resolveCurrentTask(sessions, currentAppServerThreadIds(process.env));
|
|
862
|
+
if (resolution.status === "found") return resolution.session;
|
|
863
|
+
if (resolution.status === "missing-thread-id") throw new Error(cliText(
|
|
864
|
+
"The current task cannot be detected because neither CODEX_THREAD_ID nor TRAECLI_THREAD_ID is set. Run this command from a Codex or TraeX task.",
|
|
865
|
+
"无法检测当前任务:CODEX_THREAD_ID 和 TRAECLI_THREAD_ID 均未设置。请从 Codex 或 TraeX 任务中运行此命令。",
|
|
866
|
+
));
|
|
867
|
+
if (resolution.status === "not-found") throw new Error(cliText(
|
|
868
|
+
`No AgentBot task matches the current App Server Thread ID (${resolution.threadIds.join(", ")}). Check that the correct Profile is selected.`,
|
|
869
|
+
`没有 AgentBot 任务匹配当前 App Server Thread ID(${resolution.threadIds.join(",")})。请检查是否选择了正确的 Profile。`,
|
|
870
|
+
));
|
|
871
|
+
throw new Error(cliText(
|
|
872
|
+
"The current task is ambiguous because multiple injected Thread IDs match AgentBot tasks. Use task status <task> with an explicit AgentBot task ID.",
|
|
873
|
+
"当前任务存在歧义:多个注入的 Thread ID 匹配到了 AgentBot 任务。请使用明确的 AgentBot 任务 ID 执行 task status <任务>。",
|
|
874
|
+
));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function outputTaskStatus(
|
|
878
|
+
sqlitePath: string,
|
|
879
|
+
store: StateStore,
|
|
880
|
+
session: SessionRecord,
|
|
881
|
+
json: boolean,
|
|
882
|
+
): Promise<void> {
|
|
883
|
+
const endpoint = controlEndpoint(sqlitePath);
|
|
884
|
+
let live: TaskStatusControlData | undefined;
|
|
885
|
+
let response: ControlResponse | undefined;
|
|
886
|
+
try {
|
|
887
|
+
response = await sendControlRequest(endpoint, {
|
|
888
|
+
action: "task_status",
|
|
889
|
+
localSessionId: session.localSessionId,
|
|
890
|
+
});
|
|
891
|
+
} catch {
|
|
892
|
+
// The server can be stopped or still run an older control protocol
|
|
893
|
+
// while a safe restart is pending. Preserve the local read-only fallback.
|
|
894
|
+
}
|
|
895
|
+
if (response) {
|
|
896
|
+
ensureOk(response);
|
|
897
|
+
live = response.data as TaskStatusControlData;
|
|
898
|
+
}
|
|
899
|
+
const statusSession = live?.session ?? session;
|
|
900
|
+
const snapshot = live?.snapshot
|
|
901
|
+
?? (statusSession.lastTurnId ? store.getTurnSnapshot(statusSession.lastTurnId) : undefined);
|
|
902
|
+
const result = { ...statusSession, snapshot, ...(live?.remote ? { remote: live.remote } : {}) };
|
|
903
|
+
if (json) printJson(result);
|
|
904
|
+
else printTaskStatus(statusSession, snapshot, live?.remote);
|
|
905
|
+
}
|
|
906
|
+
|
|
865
907
|
function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
|
|
866
908
|
const context = optionValue(args, "--context");
|
|
867
909
|
const status = optionValue(args, "--status");
|
|
@@ -896,19 +938,12 @@ function resolveTask(sessions: SessionRecord[], reference: string): SessionRecor
|
|
|
896
938
|
}
|
|
897
939
|
|
|
898
940
|
function printTaskList(sessions: SessionRecord[]): void {
|
|
899
|
-
|
|
900
|
-
process.stdout.write(cliText("No tasks.\n", "没有任务。\n"));
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
for (const [index, session] of sessions.entries()) {
|
|
904
|
-
const id = session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId;
|
|
905
|
-
process.stdout.write(`${index + 1}. [${taskStateLabel(session.status)}/${taskStateLabel(session.lastTurnStatus)}] ${session.title ?? cliText("Untitled task", "未命名任务")}\n`);
|
|
906
|
-
process.stdout.write(` ${id} · ${session.contextKey} · ${session.updatedAt}\n`);
|
|
907
|
-
}
|
|
941
|
+
process.stdout.write(formatTaskList(sessions, cliLanguage, currentAppServerThreadIds(process.env)));
|
|
908
942
|
}
|
|
909
943
|
|
|
910
944
|
function printTaskStatus(session: SessionRecord, snapshot: unknown, remote?: TaskStatusControlData["remote"]): void {
|
|
911
945
|
process.stdout.write(`${cliText("Title: ", "标题:")}${session.title ?? cliText("Untitled task", "未命名任务")}\n`);
|
|
946
|
+
process.stdout.write(`${cliText("Agent: ", "Agent:")}${session.agentName}\n`);
|
|
912
947
|
process.stdout.write(`${cliText("Task ID: ", "任务 ID:")}${session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId}\n`);
|
|
913
948
|
process.stdout.write(`${cliText("Local ID: ", "本地 ID:")}${session.localSessionId}\n`);
|
|
914
949
|
process.stdout.write(`${cliText("Context: ", "上下文:")}${session.contextKey}\n`);
|
|
@@ -1201,26 +1236,6 @@ function initializationStatusLabel(status: InitializationStatus): string {
|
|
|
1201
1236
|
return cliText("already exists; unchanged", "已存在,未修改");
|
|
1202
1237
|
}
|
|
1203
1238
|
|
|
1204
|
-
function taskStateLabel(status: string | undefined): string {
|
|
1205
|
-
if (!status) return "-";
|
|
1206
|
-
const chinese: Record<string, string> = {
|
|
1207
|
-
starting: "启动中",
|
|
1208
|
-
ready: "就绪",
|
|
1209
|
-
running: "运行中",
|
|
1210
|
-
closed: "已关闭",
|
|
1211
|
-
failed: "失败",
|
|
1212
|
-
completed: "已完成",
|
|
1213
|
-
cancelled: "已取消",
|
|
1214
|
-
interrupted: "已中断",
|
|
1215
|
-
inProgress: "进行中",
|
|
1216
|
-
active: "活动中",
|
|
1217
|
-
idle: "空闲",
|
|
1218
|
-
not_loaded: "未加载",
|
|
1219
|
-
error: "错误",
|
|
1220
|
-
};
|
|
1221
|
-
return cliLanguage === "zh" ? chinese[status] ?? status : status;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
1239
|
function ensureOk(response: ControlResponse): void {
|
|
1225
1240
|
if (response.ok) return;
|
|
1226
1241
|
const message = response.message?.trim();
|
|
@@ -5,12 +5,17 @@ import readline from "node:readline";
|
|
|
5
5
|
import type { Logger } from "pino";
|
|
6
6
|
import { AppServerConnection } from "./AppServerConnection.js";
|
|
7
7
|
import type { AppServerClient, AppServerClientProvider } from "./CodexRuntime.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
agentBotEnvironment,
|
|
10
|
+
type AgentEnvironmentContext,
|
|
11
|
+
} from "../runtime/agentEnvironment.js";
|
|
12
|
+
import type { AgentProcessInfo } from "../runtime/types.js";
|
|
9
13
|
import { spawnStdioCommand } from "../utils/spawnCommand.js";
|
|
10
14
|
|
|
11
15
|
export class CodexProcessManager implements AppServerClientProvider {
|
|
12
16
|
private client?: AppServerConnection;
|
|
13
17
|
private child?: ChildProcessWithoutNullStreams;
|
|
18
|
+
private version?: string;
|
|
14
19
|
private readonly disconnectListeners = new Set<(error: Error) => void>();
|
|
15
20
|
|
|
16
21
|
constructor(
|
|
@@ -18,11 +23,17 @@ export class CodexProcessManager implements AppServerClientProvider {
|
|
|
18
23
|
private readonly args: string[],
|
|
19
24
|
private readonly env: Record<string, string>,
|
|
20
25
|
private readonly logger: Logger,
|
|
26
|
+
private readonly environmentContext: () => AgentEnvironmentContext = () => ({}),
|
|
21
27
|
) {}
|
|
22
28
|
|
|
23
29
|
async getClient(): Promise<AppServerClient> {
|
|
24
30
|
if (this.client) return this.client;
|
|
25
|
-
|
|
31
|
+
this.version = undefined;
|
|
32
|
+
const child = spawnStdioCommand(
|
|
33
|
+
this.command,
|
|
34
|
+
this.args,
|
|
35
|
+
agentBotEnvironment(process.env, this.env, this.environmentContext()),
|
|
36
|
+
);
|
|
26
37
|
this.child = child;
|
|
27
38
|
const client = new AppServerConnection(child, this.logger.child({ component: "codex-app-server" }));
|
|
28
39
|
this.client = client;
|
|
@@ -38,14 +49,24 @@ export class CodexProcessManager implements AppServerClientProvider {
|
|
|
38
49
|
const error = new Error(`App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
|
|
39
50
|
for (const listener of this.disconnectListeners) listener(error);
|
|
40
51
|
});
|
|
41
|
-
await client.request("initialize", {
|
|
52
|
+
const initializeResult = await client.request("initialize", {
|
|
42
53
|
clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
|
|
43
54
|
capabilities: { experimentalApi: true },
|
|
44
55
|
});
|
|
56
|
+
this.version = initializedAgentVersion(initializeResult);
|
|
45
57
|
client.notify("initialized", {});
|
|
46
58
|
return client;
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
getProcessInfo(): AgentProcessInfo {
|
|
62
|
+
const pid = this.child?.pid;
|
|
63
|
+
if (!pid) return {};
|
|
64
|
+
return {
|
|
65
|
+
pid,
|
|
66
|
+
...(this.version ? { version: this.version } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
getCodexHome(): string {
|
|
50
71
|
return this.env.CODEX_HOME ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
51
72
|
}
|
|
@@ -62,3 +83,17 @@ export class CodexProcessManager implements AppServerClientProvider {
|
|
|
62
83
|
return () => this.disconnectListeners.delete(listener);
|
|
63
84
|
}
|
|
64
85
|
}
|
|
86
|
+
|
|
87
|
+
function initializedAgentVersion(value: unknown): string | undefined {
|
|
88
|
+
if (!value || typeof value !== "object") return undefined;
|
|
89
|
+
const result = value as Record<string, unknown>;
|
|
90
|
+
const serverInfo = result.serverInfo && typeof result.serverInfo === "object"
|
|
91
|
+
? result.serverInfo as Record<string, unknown>
|
|
92
|
+
: undefined;
|
|
93
|
+
for (const candidate of [serverInfo?.version, result.version, result.userAgent]) {
|
|
94
|
+
if (typeof candidate !== "string") continue;
|
|
95
|
+
const match = /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b/u.exec(candidate);
|
|
96
|
+
if (match?.[1]) return match[1];
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
2
|
import type {
|
|
3
3
|
AgentRuntime,
|
|
4
|
+
AgentProcessInfo,
|
|
4
5
|
ApprovalDecision,
|
|
5
6
|
CreateRuntimeSessionInput,
|
|
6
7
|
ForkRuntimeSessionInput,
|
|
@@ -55,6 +56,7 @@ export interface AppServerClient {
|
|
|
55
56
|
|
|
56
57
|
export interface AppServerClientProvider {
|
|
57
58
|
getClient(): Promise<AppServerClient>;
|
|
59
|
+
getProcessInfo?(): AgentProcessInfo;
|
|
58
60
|
getCodexHome?(): string;
|
|
59
61
|
onDisconnect?(listener: (error: Error) => void): () => void;
|
|
60
62
|
close(): void;
|
|
@@ -100,6 +102,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
100
102
|
return this.sessions.get(localSessionId);
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
getProcessInfo(): AgentProcessInfo {
|
|
106
|
+
return this.provider.getProcessInfo?.() ?? {};
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
async createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
104
110
|
const client = await this.client();
|
|
105
111
|
const response = await client.request<ThreadResponse>("thread/start", {
|
|
@@ -139,6 +139,18 @@ export interface TaskListCardAction {
|
|
|
139
139
|
value: Record<string, string>;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
export interface HelpCardCommand {
|
|
143
|
+
text: string;
|
|
144
|
+
action?: TaskListCardAction;
|
|
145
|
+
usage?: string;
|
|
146
|
+
description: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface HelpCardSection {
|
|
150
|
+
title: string;
|
|
151
|
+
commands: HelpCardCommand[];
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
export interface TaskListCardEntry {
|
|
143
155
|
lines: string[];
|
|
144
156
|
actions?: TaskListCardAction[];
|
|
@@ -794,6 +806,19 @@ export class CardRenderer {
|
|
|
794
806
|
return sectionCard(title, elements);
|
|
795
807
|
}
|
|
796
808
|
|
|
809
|
+
renderHelpCard(
|
|
810
|
+
title: string,
|
|
811
|
+
introLines: string[],
|
|
812
|
+
sections: HelpCardSection[],
|
|
813
|
+
): Record<string, unknown> {
|
|
814
|
+
const elements: Record<string, unknown>[] = [markdown(introLines.join("\n"))];
|
|
815
|
+
for (const section of sections) {
|
|
816
|
+
elements.push({ tag: "hr" }, markdown(`**${section.title}**`));
|
|
817
|
+
elements.push(...section.commands.map(helpCommandRow));
|
|
818
|
+
}
|
|
819
|
+
return sectionCard(title, elements);
|
|
820
|
+
}
|
|
821
|
+
|
|
797
822
|
renderTaskListCard(
|
|
798
823
|
title: string,
|
|
799
824
|
sectionTitle: string,
|
|
@@ -813,10 +838,12 @@ export class CardRenderer {
|
|
|
813
838
|
if (index < entries.length - 1) elements.push({ tag: "hr" });
|
|
814
839
|
});
|
|
815
840
|
}
|
|
841
|
+
if (footerActions.length > 0) {
|
|
842
|
+
elements.push({ tag: "hr" }, taskActionRow(footerActions));
|
|
843
|
+
}
|
|
816
844
|
if (footerLines.length > 0) {
|
|
817
845
|
elements.push({ tag: "hr" }, markdown(footerLines.join("\n")));
|
|
818
846
|
}
|
|
819
|
-
if (footerActions.length > 0) elements.push(taskActionRow(footerActions));
|
|
820
847
|
return {
|
|
821
848
|
schema: "2.0",
|
|
822
849
|
config: {
|
|
@@ -1588,6 +1615,39 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
|
|
|
1588
1615
|
};
|
|
1589
1616
|
}
|
|
1590
1617
|
|
|
1618
|
+
function helpCommandRow(command: HelpCardCommand): Record<string, unknown> {
|
|
1619
|
+
const details = [
|
|
1620
|
+
command.usage ? `**${command.usage}**` : undefined,
|
|
1621
|
+
command.description,
|
|
1622
|
+
].filter((line): line is string => Boolean(line)).join(" ");
|
|
1623
|
+
return {
|
|
1624
|
+
tag: "column_set",
|
|
1625
|
+
flex_mode: "none",
|
|
1626
|
+
horizontal_spacing: "12px",
|
|
1627
|
+
vertical_align: "top",
|
|
1628
|
+
margin: "2px 0 0 0",
|
|
1629
|
+
columns: [
|
|
1630
|
+
{
|
|
1631
|
+
tag: "column",
|
|
1632
|
+
width: "auto",
|
|
1633
|
+
vertical_align: "top",
|
|
1634
|
+
elements: [
|
|
1635
|
+
command.action
|
|
1636
|
+
? taskActionElement(command.action)
|
|
1637
|
+
: markdown(`**${escapeCardHtml(command.text)}**`),
|
|
1638
|
+
],
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
tag: "column",
|
|
1642
|
+
width: "weighted",
|
|
1643
|
+
weight: 1,
|
|
1644
|
+
vertical_align: "top",
|
|
1645
|
+
elements: [markdown(details)],
|
|
1646
|
+
},
|
|
1647
|
+
],
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1591
1651
|
function turnCard(
|
|
1592
1652
|
title: string,
|
|
1593
1653
|
template: string,
|
|
@@ -5,6 +5,7 @@ import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
|
|
|
5
5
|
import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
|
|
6
6
|
|
|
7
7
|
type BotOpenIdResolver = (appId: string, appSecret: string) => Promise<string>;
|
|
8
|
+
type BotOpenIdListener = (botOpenId: string) => void;
|
|
8
9
|
|
|
9
10
|
export class FeishuConnector {
|
|
10
11
|
constructor(
|
|
@@ -12,6 +13,7 @@ export class FeishuConnector {
|
|
|
12
13
|
private readonly handler: FeishuEventHandler,
|
|
13
14
|
private readonly logger: Logger,
|
|
14
15
|
private readonly botOpenIdResolver: BotOpenIdResolver = resolveFeishuBotOpenId,
|
|
16
|
+
private readonly onBotOpenId?: BotOpenIdListener,
|
|
15
17
|
) {}
|
|
16
18
|
|
|
17
19
|
async start(): Promise<void> {
|
|
@@ -21,10 +23,17 @@ export class FeishuConnector {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const respondToAllGroupMessages = this.config.feishu.respondToAllGroupMessages !== false;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
let botOpenId: string | undefined;
|
|
27
|
+
if (!respondToAllGroupMessages || this.onBotOpenId) {
|
|
28
|
+
try {
|
|
29
|
+
botOpenId = await this.botOpenIdResolver(appId, appSecret);
|
|
30
|
+
this.onBotOpenId?.(botOpenId);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!respondToAllGroupMessages) throw error;
|
|
33
|
+
this.logger.warn({ error }, "Failed to resolve the Lark bot Open ID for the Agent environment.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
await this.startFeishuWs(appId, appSecret, respondToAllGroupMessages ? undefined : botOpenId);
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
stop(): void {
|