@keyou007/agent-bot 0.1.13-alpha.0 → 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/.env.example +2 -2
- package/CHANGELOG.md +15 -0
- package/README.md +107 -98
- package/README.zh.md +81 -72
- 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/ServerStarter.js +17 -2
- package/dist/cli/ServerStarter.js.map +1 -1
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +16 -4
- 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 +88 -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/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/FeishuUserOpenIdStore.js +76 -0
- package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
- package/dist/config/loadConfig.js +3 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/feishu/CardRenderer.js +123 -22
- 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 +125 -26
- package/dist/index.js.map +1 -1
- package/dist/presentation/turnGraph.js +36 -0
- package/dist/presentation/turnGraph.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +425 -104
- 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/startup/StartupNotifier.js +16 -7
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -2
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/state/StateStore.js +172 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +21 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +46 -3
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +65 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/replacementSupervisor.js +27 -2
- package/dist/supervision/replacementSupervisor.js.map +1 -1
- package/dist/supervisor.js +27 -2
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +20 -7
- package/docs/technical-reference.zh.md +20 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +63 -26
- package/src/acp/AcpProcessManager.ts +16 -3
- package/src/acp/AcpSessionManager.ts +14 -0
- package/src/cli/ServerStarter.ts +25 -2
- package/src/cli/controlProtocol.ts +6 -1
- package/src/cli/help.ts +16 -4
- package/src/cli/serverStatus.ts +28 -0
- package/src/cli/taskListOutput.ts +91 -0
- package/src/cli.ts +123 -51
- package/src/codex/CodexProcessManager.ts +38 -3
- package/src/codex/CodexRuntime.ts +6 -0
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/config/FeishuUserOpenIdStore.ts +81 -0
- package/src/config/loadConfig.ts +3 -0
- package/src/feishu/CardRenderer.ts +169 -22
- package/src/feishu/FeishuConnector.ts +13 -4
- package/src/index.ts +195 -23
- package/src/presentation/turnGraph.ts +48 -0
- package/src/proxy/ProxySessionController.ts +507 -112
- 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/startup/StartupNotifier.ts +23 -7
- package/src/startup/startFeishu.ts +3 -2
- package/src/state/StateStore.ts +224 -0
- package/src/state/migrations.ts +21 -0
- package/src/supervision/SafeRestartNotifier.ts +53 -4
- package/src/supervision/SafeRestartScheduler.ts +78 -7
- package/src/supervision/replacementSupervisor.ts +27 -1
- package/src/supervisor.ts +33 -2
|
@@ -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,13 @@ 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";
|
|
74
|
+
import { isThreadContextKey } from "./feishu/contextKey.js";
|
|
68
75
|
import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
|
|
69
76
|
import {
|
|
70
77
|
parseTaskForkGroupOptions,
|
|
@@ -136,6 +143,7 @@ interface InitCommandResult extends InitializationResult {
|
|
|
136
143
|
feishu: {
|
|
137
144
|
status: FeishuInitializationStatus;
|
|
138
145
|
appId?: string;
|
|
146
|
+
userOpenIdStatus?: "configured" | "pending";
|
|
139
147
|
configuration?: EnsureFeishuAppConfigurationResult;
|
|
140
148
|
};
|
|
141
149
|
server: InitializationServerResult;
|
|
@@ -448,6 +456,9 @@ async function initializeFeishu(
|
|
|
448
456
|
return {
|
|
449
457
|
status: "skipped",
|
|
450
458
|
...(existing.appId ? { appId: existing.appId } : {}),
|
|
459
|
+
...(existing.appId && existing.appSecret
|
|
460
|
+
? { userOpenIdStatus: existing.userOpenId ? "configured" as const : "pending" as const }
|
|
461
|
+
: {}),
|
|
451
462
|
};
|
|
452
463
|
}
|
|
453
464
|
if (!options.json) {
|
|
@@ -513,6 +524,7 @@ async function initializeFeishu(
|
|
|
513
524
|
return {
|
|
514
525
|
status,
|
|
515
526
|
appId: credentials.appId,
|
|
527
|
+
userOpenIdStatus: credentials.userOpenId ? "configured" : "pending",
|
|
516
528
|
configuration,
|
|
517
529
|
};
|
|
518
530
|
} finally {
|
|
@@ -659,12 +671,14 @@ async function serverCommand(input: string[]): Promise<void> {
|
|
|
659
671
|
}
|
|
660
672
|
if (action === "restart") {
|
|
661
673
|
const immediate = rest.includes("--immediate") || rest.includes("--force");
|
|
674
|
+
const notificationSessionId = resolveRestartNotificationSessionId(config.storage.sqlitePath, rest);
|
|
662
675
|
const reason = optionValue(rest, "--reason")
|
|
663
676
|
?? (immediate ? "通过 Agent Bot CLI 立即重启" : "通过 Agent Bot CLI 安全重启");
|
|
664
677
|
ensureOk(await sendControlRequest(endpoint, {
|
|
665
678
|
action: "server_restart",
|
|
666
679
|
mode: immediate ? "immediate" : "safe",
|
|
667
680
|
reason,
|
|
681
|
+
notificationSessionId,
|
|
668
682
|
}));
|
|
669
683
|
process.stdout.write(immediate
|
|
670
684
|
? cliText("Immediate restart requested.\n", "已请求立即重启。\n")
|
|
@@ -674,12 +688,58 @@ async function serverCommand(input: string[]): Promise<void> {
|
|
|
674
688
|
throw new Error(cliText(`Unknown server command: ${action}`, `未知的 server 命令:${action}`));
|
|
675
689
|
}
|
|
676
690
|
|
|
691
|
+
function resolveRestartNotificationSessionId(sqlitePath: string, args: string[]): string | undefined {
|
|
692
|
+
const store = new StateStore(sqlitePath);
|
|
693
|
+
try {
|
|
694
|
+
const sessions = store.listAllSessions();
|
|
695
|
+
if (args.includes("--task")) {
|
|
696
|
+
const reference = optionValue(args, "--task")?.trim();
|
|
697
|
+
if (!reference || reference.startsWith("--")) throw new Error(cliText(
|
|
698
|
+
"server restart --task requires a task number or task ID.",
|
|
699
|
+
"server restart --task 需要任务序号或任务 ID。",
|
|
700
|
+
));
|
|
701
|
+
const session = resolveTask(sessions, reference);
|
|
702
|
+
if (
|
|
703
|
+
isThreadContextKey(session.contextKey)
|
|
704
|
+
&& !store.findLatestMessageIdForSession(session.localSessionId, session.contextKey)
|
|
705
|
+
&& !store.findLatestMessageIdForContext(session.contextKey)
|
|
706
|
+
) {
|
|
707
|
+
throw new Error(cliText(
|
|
708
|
+
"The task topic has no message anchor for restart notifications. Send /restart in that topic instead.",
|
|
709
|
+
"该任务话题没有可用于重启通知的消息锚点,请直接在该话题中发送 /restart。",
|
|
710
|
+
));
|
|
711
|
+
}
|
|
712
|
+
return session.localSessionId;
|
|
713
|
+
}
|
|
714
|
+
const runningContexts = new Set(
|
|
715
|
+
sessions.filter((session) => session.status === "running").map((session) => session.contextKey),
|
|
716
|
+
);
|
|
717
|
+
if (runningContexts.size > 1) throw new Error(cliText(
|
|
718
|
+
"Multiple conversations are running. Pass --task <task> so restart notifications return to the requester.",
|
|
719
|
+
"当前有多个会话正在运行。请传入 --task <任务>,确保重启通知返回发起会话。",
|
|
720
|
+
));
|
|
721
|
+
return undefined;
|
|
722
|
+
} finally {
|
|
723
|
+
store.close();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
677
727
|
async function taskCommand(input: string[]): Promise<void> {
|
|
678
728
|
const [action = "list", ...rest] = input;
|
|
679
729
|
const config = loadConfig();
|
|
680
730
|
const store = new StateStore(config.storage.sqlitePath);
|
|
681
731
|
try {
|
|
682
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
|
+
}
|
|
683
743
|
if (action === "prompt" || action === "send") {
|
|
684
744
|
const [reference, ...promptParts] = rest;
|
|
685
745
|
if (!reference) throw new Error(cliText(
|
|
@@ -765,28 +825,7 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
765
825
|
return;
|
|
766
826
|
}
|
|
767
827
|
if (action === "status") {
|
|
768
|
-
|
|
769
|
-
let live: TaskStatusControlData | undefined;
|
|
770
|
-
let response: ControlResponse | undefined;
|
|
771
|
-
try {
|
|
772
|
-
response = await sendControlRequest(endpoint, {
|
|
773
|
-
action: "task_status",
|
|
774
|
-
localSessionId: session.localSessionId,
|
|
775
|
-
});
|
|
776
|
-
} catch {
|
|
777
|
-
// The server can be stopped or still run an older control protocol
|
|
778
|
-
// while a safe restart is pending. Preserve the local read-only fallback.
|
|
779
|
-
}
|
|
780
|
-
if (response) {
|
|
781
|
-
ensureOk(response);
|
|
782
|
-
live = response.data as TaskStatusControlData;
|
|
783
|
-
}
|
|
784
|
-
const statusSession = live?.session ?? session;
|
|
785
|
-
const snapshot = live?.snapshot
|
|
786
|
-
?? (statusSession.lastTurnId ? store.getTurnSnapshot(statusSession.lastTurnId) : undefined);
|
|
787
|
-
const result = { ...statusSession, snapshot, ...(live?.remote ? { remote: live.remote } : {}) };
|
|
788
|
-
if (rest.includes("--json")) printJson(result);
|
|
789
|
-
else printTaskStatus(statusSession, snapshot, live?.remote);
|
|
828
|
+
await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
|
|
790
829
|
return;
|
|
791
830
|
}
|
|
792
831
|
const endpoint = controlEndpoint(config.storage.sqlitePath);
|
|
@@ -818,6 +857,53 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
818
857
|
}
|
|
819
858
|
}
|
|
820
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
|
+
|
|
821
907
|
function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
|
|
822
908
|
const context = optionValue(args, "--context");
|
|
823
909
|
const status = optionValue(args, "--status");
|
|
@@ -852,19 +938,12 @@ function resolveTask(sessions: SessionRecord[], reference: string): SessionRecor
|
|
|
852
938
|
}
|
|
853
939
|
|
|
854
940
|
function printTaskList(sessions: SessionRecord[]): void {
|
|
855
|
-
|
|
856
|
-
process.stdout.write(cliText("No tasks.\n", "没有任务。\n"));
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
for (const [index, session] of sessions.entries()) {
|
|
860
|
-
const id = session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId;
|
|
861
|
-
process.stdout.write(`${index + 1}. [${taskStateLabel(session.status)}/${taskStateLabel(session.lastTurnStatus)}] ${session.title ?? cliText("Untitled task", "未命名任务")}\n`);
|
|
862
|
-
process.stdout.write(` ${id} · ${session.contextKey} · ${session.updatedAt}\n`);
|
|
863
|
-
}
|
|
941
|
+
process.stdout.write(formatTaskList(sessions, cliLanguage, currentAppServerThreadIds(process.env)));
|
|
864
942
|
}
|
|
865
943
|
|
|
866
944
|
function printTaskStatus(session: SessionRecord, snapshot: unknown, remote?: TaskStatusControlData["remote"]): void {
|
|
867
945
|
process.stdout.write(`${cliText("Title: ", "标题:")}${session.title ?? cliText("Untitled task", "未命名任务")}\n`);
|
|
946
|
+
process.stdout.write(`${cliText("Agent: ", "Agent:")}${session.agentName}\n`);
|
|
868
947
|
process.stdout.write(`${cliText("Task ID: ", "任务 ID:")}${session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId}\n`);
|
|
869
948
|
process.stdout.write(`${cliText("Local ID: ", "本地 ID:")}${session.localSessionId}\n`);
|
|
870
949
|
process.stdout.write(`${cliText("Context: ", "上下文:")}${session.contextKey}\n`);
|
|
@@ -945,6 +1024,12 @@ function printInitializationResult(result: Omit<InitCommandResult, "server">): v
|
|
|
945
1024
|
"飞书应用:已跳过;请使用 Console 模式或稍后重新运行 init。\n",
|
|
946
1025
|
));
|
|
947
1026
|
}
|
|
1027
|
+
if (result.feishu.userOpenIdStatus === "pending") {
|
|
1028
|
+
process.stdout.write(cliText(
|
|
1029
|
+
"Lark user: pending; send the bot a direct message to save FEISHU_USER_OPEN_ID\n",
|
|
1030
|
+
"飞书用户:等待补全;请私聊机器人以保存 FEISHU_USER_OPEN_ID\n",
|
|
1031
|
+
));
|
|
1032
|
+
}
|
|
948
1033
|
if (result.feishu.configuration?.status === "updated") {
|
|
949
1034
|
const added = result.feishu.configuration.added;
|
|
950
1035
|
process.stdout.write(cliText(
|
|
@@ -984,6 +1069,13 @@ function printInitializationServerResult(result: InitCommandResult["server"]): v
|
|
|
984
1069
|
));
|
|
985
1070
|
return;
|
|
986
1071
|
}
|
|
1072
|
+
if (result.status === "restart-scheduled") {
|
|
1073
|
+
process.stdout.write(cliText(
|
|
1074
|
+
"Agent Bot server: safe restart scheduled to load the current version.\n",
|
|
1075
|
+
"Agent Bot 服务:已安排安全重启,以加载当前版本。\n",
|
|
1076
|
+
));
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
987
1079
|
printServerStartResult(result);
|
|
988
1080
|
}
|
|
989
1081
|
|
|
@@ -1144,26 +1236,6 @@ function initializationStatusLabel(status: InitializationStatus): string {
|
|
|
1144
1236
|
return cliText("already exists; unchanged", "已存在,未修改");
|
|
1145
1237
|
}
|
|
1146
1238
|
|
|
1147
|
-
function taskStateLabel(status: string | undefined): string {
|
|
1148
|
-
if (!status) return "-";
|
|
1149
|
-
const chinese: Record<string, string> = {
|
|
1150
|
-
starting: "启动中",
|
|
1151
|
-
ready: "就绪",
|
|
1152
|
-
running: "运行中",
|
|
1153
|
-
closed: "已关闭",
|
|
1154
|
-
failed: "失败",
|
|
1155
|
-
completed: "已完成",
|
|
1156
|
-
cancelled: "已取消",
|
|
1157
|
-
interrupted: "已中断",
|
|
1158
|
-
inProgress: "进行中",
|
|
1159
|
-
active: "活动中",
|
|
1160
|
-
idle: "空闲",
|
|
1161
|
-
not_loaded: "未加载",
|
|
1162
|
-
error: "错误",
|
|
1163
|
-
};
|
|
1164
|
-
return cliLanguage === "zh" ? chinese[status] ?? status : status;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
1239
|
function ensureOk(response: ControlResponse): void {
|
|
1168
1240
|
if (response.ok) return;
|
|
1169
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", {
|
|
@@ -20,6 +20,7 @@ const COMMAND_NAMES = [
|
|
|
20
20
|
"switch",
|
|
21
21
|
"thinking",
|
|
22
22
|
"title",
|
|
23
|
+
"turns",
|
|
23
24
|
] as const;
|
|
24
25
|
|
|
25
26
|
type CommandName = (typeof COMMAND_NAMES)[number];
|
|
@@ -93,6 +94,9 @@ export class CommandRouter {
|
|
|
93
94
|
if (args.length === 0) return { type: "restart" };
|
|
94
95
|
if (args.length === 1 && args[0] === "--force") return { type: "restart", force: true };
|
|
95
96
|
throw new Error("/restart 只接受一个可选的 --force 参数。");
|
|
97
|
+
case "turns":
|
|
98
|
+
if (args.length > 0) throw new Error("/turns 不接受参数,请在历史轮次卡片中选择 turn。");
|
|
99
|
+
return { type: "turns" };
|
|
96
100
|
case "model":
|
|
97
101
|
return settingsCommand("/model", args, { type: "model" });
|
|
98
102
|
case "provider":
|
|
@@ -15,6 +15,7 @@ export type Command =
|
|
|
15
15
|
| { type: "goal"; action: "set" | "edit"; objective: string }
|
|
16
16
|
| { type: "goal"; action: "pause" | "resume" | "clear" }
|
|
17
17
|
| { type: "restart"; force?: boolean }
|
|
18
|
+
| { type: "turns" }
|
|
18
19
|
| { type: "model" }
|
|
19
20
|
| { type: "provider" }
|
|
20
21
|
| { type: "thinking" }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse as parseDotEnv } from "dotenv";
|
|
4
|
+
|
|
5
|
+
const USER_OPEN_ID_KEY = "FEISHU_USER_OPEN_ID";
|
|
6
|
+
|
|
7
|
+
export interface FeishuUserOpenIdPersistenceResult {
|
|
8
|
+
status: "stored" | "existing";
|
|
9
|
+
userOpenId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function persistFeishuUserOpenIdIfMissing(
|
|
13
|
+
envPath: string,
|
|
14
|
+
userOpenId: string,
|
|
15
|
+
): FeishuUserOpenIdPersistenceResult {
|
|
16
|
+
if (!/^ou_[A-Za-z0-9_-]+$/u.test(userOpenId)) {
|
|
17
|
+
throw new Error("Cannot persist an invalid Lark user Open ID.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const original = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
21
|
+
const existing = parseDotEnv(original)[USER_OPEN_ID_KEY]?.trim();
|
|
22
|
+
if (existing) return { status: "existing", userOpenId: existing };
|
|
23
|
+
|
|
24
|
+
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
25
|
+
writeFileAtomically(envPath, upsertDotEnvValue(original, USER_OPEN_ID_KEY, userOpenId));
|
|
26
|
+
return { status: "stored", userOpenId };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function upsertDotEnvValue(contents: string, key: string, value: string): string {
|
|
30
|
+
const newline = contents.includes("\r\n") ? "\r\n" : "\n";
|
|
31
|
+
const lines = contents.length > 0 ? contents.split(/\r?\n/u) : [];
|
|
32
|
+
const matcher = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=`);
|
|
33
|
+
let replaced = false;
|
|
34
|
+
const updated = lines.map((line) => {
|
|
35
|
+
if (!matcher.test(line)) return line;
|
|
36
|
+
replaced = true;
|
|
37
|
+
return `${key}=${value}`;
|
|
38
|
+
});
|
|
39
|
+
while (updated.length > 0 && updated.at(-1) === "") updated.pop();
|
|
40
|
+
if (!replaced) updated.push(`${key}=${value}`);
|
|
41
|
+
return `${updated.join(newline)}${newline}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeFileAtomically(filePath: string, contents: string): void {
|
|
45
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
46
|
+
let temporaryFile: number | undefined;
|
|
47
|
+
try {
|
|
48
|
+
const mode = fs.statSync(filePath, { throwIfNoEntry: false })?.mode ?? 0o600;
|
|
49
|
+
temporaryFile = fs.openSync(temporaryPath, "wx", mode);
|
|
50
|
+
fs.writeFileSync(temporaryFile, contents, "utf8");
|
|
51
|
+
fs.fsyncSync(temporaryFile);
|
|
52
|
+
fs.closeSync(temporaryFile);
|
|
53
|
+
temporaryFile = undefined;
|
|
54
|
+
fs.renameSync(temporaryPath, filePath);
|
|
55
|
+
restrictFilePermissions(filePath);
|
|
56
|
+
syncDirectory(path.dirname(filePath));
|
|
57
|
+
} finally {
|
|
58
|
+
if (temporaryFile !== undefined) fs.closeSync(temporaryFile);
|
|
59
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function restrictFilePermissions(filePath: string): void {
|
|
64
|
+
try {
|
|
65
|
+
fs.chmodSync(filePath, 0o600);
|
|
66
|
+
} catch {
|
|
67
|
+
// Windows and some mounted filesystems do not implement POSIX modes.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function syncDirectory(directoryPath: string): void {
|
|
72
|
+
let directory: number | undefined;
|
|
73
|
+
try {
|
|
74
|
+
directory = fs.openSync(directoryPath, "r");
|
|
75
|
+
fs.fsyncSync(directory);
|
|
76
|
+
} catch {
|
|
77
|
+
// Directory fsync is not supported on every Windows or mounted filesystem.
|
|
78
|
+
} finally {
|
|
79
|
+
if (directory !== undefined) fs.closeSync(directory);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -58,6 +58,9 @@ logging:
|
|
|
58
58
|
export function loadConfig(
|
|
59
59
|
configPath?: string,
|
|
60
60
|
): AppConfig {
|
|
61
|
+
if (process.env.FEISHU_USER_OPEN_ID !== undefined && !process.env.FEISHU_USER_OPEN_ID.trim()) {
|
|
62
|
+
delete process.env.FEISHU_USER_OPEN_ID;
|
|
63
|
+
}
|
|
61
64
|
loadDotEnv({
|
|
62
65
|
path: defaultDotEnvPath(),
|
|
63
66
|
quiet: true,
|