@keyou007/agent-bot 0.1.13-alpha.1 → 0.1.13-alpha.3
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 +11 -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/InitializationWelcome.js +116 -0
- package/dist/cli/InitializationWelcome.js.map +1 -0
- 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 +103 -56
- 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 +164 -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/dist/supervision/SafeRestartNotifier.js +1 -9
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/docs/technical-reference.md +12 -4
- package/docs/technical-reference.zh.md +12 -4
- 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/InitializationWelcome.ts +160 -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 +140 -55
- package/src/codex/CodexProcessManager.ts +38 -3
- package/src/codex/CodexRuntime.ts +6 -0
- package/src/feishu/CardRenderer.ts +204 -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
- package/src/supervision/SafeRestartNotifier.ts +1 -11
package/src/cli.ts
CHANGED
|
@@ -56,6 +56,13 @@ import {
|
|
|
56
56
|
import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatus.js";
|
|
57
57
|
import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
|
|
58
58
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
59
|
+
import {
|
|
60
|
+
readInitializationReceipt,
|
|
61
|
+
resolveInitializationWelcomeKind,
|
|
62
|
+
sendInitializationWelcome,
|
|
63
|
+
writeInitializationReceipt,
|
|
64
|
+
type InitializationWelcomeResult,
|
|
65
|
+
} from "./cli/InitializationWelcome.js";
|
|
59
66
|
import { applyExplicitProfile, parseGlobalOptions } from "./cli/profile.js";
|
|
60
67
|
import { printVerificationQrAndLink } from "./cli/VerificationOutput.js";
|
|
61
68
|
import {
|
|
@@ -65,6 +72,12 @@ import {
|
|
|
65
72
|
type ServerStartResult,
|
|
66
73
|
} from "./cli/ServerStarter.js";
|
|
67
74
|
import { taskChatRoute } from "./cli/taskChatRoute.js";
|
|
75
|
+
import {
|
|
76
|
+
currentAppServerThreadIds,
|
|
77
|
+
formatTaskList,
|
|
78
|
+
resolveCurrentTask,
|
|
79
|
+
taskStateLabel,
|
|
80
|
+
} from "./cli/taskListOutput.js";
|
|
68
81
|
import { isThreadContextKey } from "./feishu/contextKey.js";
|
|
69
82
|
import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
|
|
70
83
|
import {
|
|
@@ -141,6 +154,7 @@ interface InitCommandResult extends InitializationResult {
|
|
|
141
154
|
configuration?: EnsureFeishuAppConfigurationResult;
|
|
142
155
|
};
|
|
143
156
|
server: InitializationServerResult;
|
|
157
|
+
welcome: InitializationWelcomeResult;
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
interface InitializedAgent extends SupportedAgentInspection {
|
|
@@ -156,11 +170,13 @@ async function initCommand(
|
|
|
156
170
|
explicitProfile: boolean,
|
|
157
171
|
): Promise<void> {
|
|
158
172
|
const options = parseInitCommandOptions(input, explicitProfile);
|
|
173
|
+
const version = readPackageVersion();
|
|
174
|
+
const previousInitialization = readInitializationReceipt(agentBotHome());
|
|
159
175
|
let initializationLock = options.reset
|
|
160
176
|
? acquireInitializationLock(agentBotHome())
|
|
161
177
|
: undefined;
|
|
162
178
|
let paths: InitializationResult;
|
|
163
|
-
let initialized: Omit<InitCommandResult, "server">;
|
|
179
|
+
let initialized: Omit<InitCommandResult, "server" | "welcome">;
|
|
164
180
|
try {
|
|
165
181
|
if (options.reset) await assertResetProfileServerStopped(configPath);
|
|
166
182
|
paths = initializeAgentBot({ configPath, reset: options.reset });
|
|
@@ -183,9 +199,46 @@ async function initCommand(
|
|
|
183
199
|
process.stdout.write(cliText("\nStarting Agent Bot server...\n", "\n正在启动 Agent Bot 服务...\n"));
|
|
184
200
|
}
|
|
185
201
|
const server = await startInitializedServer({ skipFeishu: options.skipFeishu, configPath });
|
|
186
|
-
const
|
|
202
|
+
const welcomeKind = resolveInitializationWelcomeKind({
|
|
203
|
+
firstInitialization: paths.config.status === "created",
|
|
204
|
+
previousVersion: previousInitialization?.version,
|
|
205
|
+
currentVersion: version,
|
|
206
|
+
});
|
|
207
|
+
let welcome: InitializationWelcomeResult;
|
|
208
|
+
if (options.skipFeishu) {
|
|
209
|
+
welcome = { status: "skipped", kind: welcomeKind, reason: "feishu-skipped" };
|
|
210
|
+
} else {
|
|
211
|
+
try {
|
|
212
|
+
welcome = await sendInitializationWelcome({
|
|
213
|
+
configPath,
|
|
214
|
+
language: cliLanguage,
|
|
215
|
+
version,
|
|
216
|
+
previousVersion: previousInitialization?.version,
|
|
217
|
+
kind: welcomeKind,
|
|
218
|
+
activationPending: server.status === "restart-scheduled",
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
welcome = {
|
|
222
|
+
status: "failed",
|
|
223
|
+
kind: welcomeKind,
|
|
224
|
+
message: error instanceof Error ? error.message : String(error),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
writeInitializationReceipt(paths.home.path, version);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
process.stderr.write(`${cliText(
|
|
232
|
+
"Warning: Could not save initialization version metadata: ",
|
|
233
|
+
"警告:无法保存初始化版本信息:",
|
|
234
|
+
)}${error instanceof Error ? error.message : String(error)}\n`);
|
|
235
|
+
}
|
|
236
|
+
const result: InitCommandResult = { ...initialized, server, welcome };
|
|
187
237
|
if (options.json) printJson(result);
|
|
188
|
-
else
|
|
238
|
+
else {
|
|
239
|
+
printInitializationServerResult(server);
|
|
240
|
+
printInitializationWelcomeResult(welcome);
|
|
241
|
+
}
|
|
189
242
|
}
|
|
190
243
|
|
|
191
244
|
async function initializeSupportedAgents(json: boolean): Promise<InitializedAgent[]> {
|
|
@@ -724,6 +777,16 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
724
777
|
const store = new StateStore(config.storage.sqlitePath);
|
|
725
778
|
try {
|
|
726
779
|
const allSessions = store.listAllSessions();
|
|
780
|
+
if (action === "current") {
|
|
781
|
+
const unsupported = rest.filter((value) => value !== "--json");
|
|
782
|
+
if (unsupported.length > 0) throw new Error(cliText(
|
|
783
|
+
`task current does not accept arguments: ${unsupported.join(" ")}`,
|
|
784
|
+
`task current 不接受这些参数:${unsupported.join(" ")}`,
|
|
785
|
+
));
|
|
786
|
+
const session = resolveCurrentTaskFromEnvironment(allSessions);
|
|
787
|
+
await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
727
790
|
if (action === "prompt" || action === "send") {
|
|
728
791
|
const [reference, ...promptParts] = rest;
|
|
729
792
|
if (!reference) throw new Error(cliText(
|
|
@@ -809,28 +872,7 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
809
872
|
return;
|
|
810
873
|
}
|
|
811
874
|
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);
|
|
875
|
+
await outputTaskStatus(config.storage.sqlitePath, store, session, rest.includes("--json"));
|
|
834
876
|
return;
|
|
835
877
|
}
|
|
836
878
|
const endpoint = controlEndpoint(config.storage.sqlitePath);
|
|
@@ -862,6 +904,53 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
862
904
|
}
|
|
863
905
|
}
|
|
864
906
|
|
|
907
|
+
function resolveCurrentTaskFromEnvironment(sessions: SessionRecord[]): SessionRecord {
|
|
908
|
+
const resolution = resolveCurrentTask(sessions, currentAppServerThreadIds(process.env));
|
|
909
|
+
if (resolution.status === "found") return resolution.session;
|
|
910
|
+
if (resolution.status === "missing-thread-id") throw new Error(cliText(
|
|
911
|
+
"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.",
|
|
912
|
+
"无法检测当前任务:CODEX_THREAD_ID 和 TRAECLI_THREAD_ID 均未设置。请从 Codex 或 TraeX 任务中运行此命令。",
|
|
913
|
+
));
|
|
914
|
+
if (resolution.status === "not-found") throw new Error(cliText(
|
|
915
|
+
`No AgentBot task matches the current App Server Thread ID (${resolution.threadIds.join(", ")}). Check that the correct Profile is selected.`,
|
|
916
|
+
`没有 AgentBot 任务匹配当前 App Server Thread ID(${resolution.threadIds.join(",")})。请检查是否选择了正确的 Profile。`,
|
|
917
|
+
));
|
|
918
|
+
throw new Error(cliText(
|
|
919
|
+
"The current task is ambiguous because multiple injected Thread IDs match AgentBot tasks. Use task status <task> with an explicit AgentBot task ID.",
|
|
920
|
+
"当前任务存在歧义:多个注入的 Thread ID 匹配到了 AgentBot 任务。请使用明确的 AgentBot 任务 ID 执行 task status <任务>。",
|
|
921
|
+
));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function outputTaskStatus(
|
|
925
|
+
sqlitePath: string,
|
|
926
|
+
store: StateStore,
|
|
927
|
+
session: SessionRecord,
|
|
928
|
+
json: boolean,
|
|
929
|
+
): Promise<void> {
|
|
930
|
+
const endpoint = controlEndpoint(sqlitePath);
|
|
931
|
+
let live: TaskStatusControlData | undefined;
|
|
932
|
+
let response: ControlResponse | undefined;
|
|
933
|
+
try {
|
|
934
|
+
response = await sendControlRequest(endpoint, {
|
|
935
|
+
action: "task_status",
|
|
936
|
+
localSessionId: session.localSessionId,
|
|
937
|
+
});
|
|
938
|
+
} catch {
|
|
939
|
+
// The server can be stopped or still run an older control protocol
|
|
940
|
+
// while a safe restart is pending. Preserve the local read-only fallback.
|
|
941
|
+
}
|
|
942
|
+
if (response) {
|
|
943
|
+
ensureOk(response);
|
|
944
|
+
live = response.data as TaskStatusControlData;
|
|
945
|
+
}
|
|
946
|
+
const statusSession = live?.session ?? session;
|
|
947
|
+
const snapshot = live?.snapshot
|
|
948
|
+
?? (statusSession.lastTurnId ? store.getTurnSnapshot(statusSession.lastTurnId) : undefined);
|
|
949
|
+
const result = { ...statusSession, snapshot, ...(live?.remote ? { remote: live.remote } : {}) };
|
|
950
|
+
if (json) printJson(result);
|
|
951
|
+
else printTaskStatus(statusSession, snapshot, live?.remote);
|
|
952
|
+
}
|
|
953
|
+
|
|
865
954
|
function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
|
|
866
955
|
const context = optionValue(args, "--context");
|
|
867
956
|
const status = optionValue(args, "--status");
|
|
@@ -896,19 +985,12 @@ function resolveTask(sessions: SessionRecord[], reference: string): SessionRecor
|
|
|
896
985
|
}
|
|
897
986
|
|
|
898
987
|
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
|
-
}
|
|
988
|
+
process.stdout.write(formatTaskList(sessions, cliLanguage, currentAppServerThreadIds(process.env)));
|
|
908
989
|
}
|
|
909
990
|
|
|
910
991
|
function printTaskStatus(session: SessionRecord, snapshot: unknown, remote?: TaskStatusControlData["remote"]): void {
|
|
911
992
|
process.stdout.write(`${cliText("Title: ", "标题:")}${session.title ?? cliText("Untitled task", "未命名任务")}\n`);
|
|
993
|
+
process.stdout.write(`${cliText("Agent: ", "Agent:")}${session.agentName}\n`);
|
|
912
994
|
process.stdout.write(`${cliText("Task ID: ", "任务 ID:")}${session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId}\n`);
|
|
913
995
|
process.stdout.write(`${cliText("Local ID: ", "本地 ID:")}${session.localSessionId}\n`);
|
|
914
996
|
process.stdout.write(`${cliText("Context: ", "上下文:")}${session.contextKey}\n`);
|
|
@@ -967,7 +1049,7 @@ function printInitializationPaths(result: InitializationResult): void {
|
|
|
967
1049
|
process.stdout.write(`${cliText("Log directory: ", "日志目录:")}${result.logs.path} (${initializationStatusLabel(result.logs.status)})\n`);
|
|
968
1050
|
}
|
|
969
1051
|
|
|
970
|
-
function printInitializationResult(result: Omit<InitCommandResult, "server">): void {
|
|
1052
|
+
function printInitializationResult(result: Omit<InitCommandResult, "server" | "welcome">): void {
|
|
971
1053
|
process.stdout.write(cliText(
|
|
972
1054
|
"\nAgent Bot initialization completed.\n",
|
|
973
1055
|
"\nAgent Bot 初始化完成。\n",
|
|
@@ -1044,6 +1126,29 @@ function printInitializationServerResult(result: InitCommandResult["server"]): v
|
|
|
1044
1126
|
printServerStartResult(result);
|
|
1045
1127
|
}
|
|
1046
1128
|
|
|
1129
|
+
function printInitializationWelcomeResult(result: InitializationWelcomeResult): void {
|
|
1130
|
+
if (result.status === "sent") {
|
|
1131
|
+
process.stdout.write(cliText(
|
|
1132
|
+
"Welcome card: sent to your Lark private chat.\n",
|
|
1133
|
+
"欢迎卡片:已发送到你的飞书私聊。\n",
|
|
1134
|
+
));
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (result.status === "failed") {
|
|
1138
|
+
process.stderr.write(cliText(
|
|
1139
|
+
`Warning: The private welcome card could not be sent: ${result.message}\n`,
|
|
1140
|
+
`警告:无法发送私聊欢迎卡片:${result.message}\n`,
|
|
1141
|
+
));
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (result.reason === "missing-user-open-id") {
|
|
1145
|
+
process.stderr.write(cliText(
|
|
1146
|
+
"Warning: The private welcome card was skipped because FEISHU_USER_OPEN_ID is not available. Send the bot a private message, then run agentbot init again.\n",
|
|
1147
|
+
"警告:尚未获得 FEISHU_USER_OPEN_ID,无法发送私聊欢迎卡片。请先私聊机器人,再次运行 agentbot init。\n",
|
|
1148
|
+
));
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1047
1152
|
function printServerStartResult(result: ServerStartResult): void {
|
|
1048
1153
|
process.stdout.write(
|
|
1049
1154
|
result.status === "already-running"
|
|
@@ -1201,26 +1306,6 @@ function initializationStatusLabel(status: InitializationStatus): string {
|
|
|
1201
1306
|
return cliText("already exists; unchanged", "已存在,未修改");
|
|
1202
1307
|
}
|
|
1203
1308
|
|
|
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
1309
|
function ensureOk(response: ControlResponse): void {
|
|
1225
1310
|
if (response.ok) return;
|
|
1226
1311
|
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", {
|
|
@@ -35,6 +35,27 @@ export interface StartupStatusView {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export type InitializationWelcomeKind = "first" | "upgrade" | "refresh";
|
|
39
|
+
|
|
40
|
+
export interface InitializationWelcomeFeature {
|
|
41
|
+
icon: string;
|
|
42
|
+
title: string;
|
|
43
|
+
description: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface InitializationWelcomeView {
|
|
47
|
+
kind: InitializationWelcomeKind;
|
|
48
|
+
language: "en" | "zh";
|
|
49
|
+
version: string;
|
|
50
|
+
previousVersion?: string;
|
|
51
|
+
activationPending?: boolean;
|
|
52
|
+
defaultAgentName: string;
|
|
53
|
+
defaultAgentTitle: string;
|
|
54
|
+
availableAgents: string[];
|
|
55
|
+
logoPath: string;
|
|
56
|
+
features: InitializationWelcomeFeature[];
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
export interface SafeRestartStatusView {
|
|
39
60
|
scheduleId: number;
|
|
40
61
|
reason: string;
|
|
@@ -139,6 +160,18 @@ export interface TaskListCardAction {
|
|
|
139
160
|
value: Record<string, string>;
|
|
140
161
|
}
|
|
141
162
|
|
|
163
|
+
export interface HelpCardCommand {
|
|
164
|
+
text: string;
|
|
165
|
+
action?: TaskListCardAction;
|
|
166
|
+
usage?: string;
|
|
167
|
+
description: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface HelpCardSection {
|
|
171
|
+
title: string;
|
|
172
|
+
commands: HelpCardCommand[];
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
export interface TaskListCardEntry {
|
|
143
176
|
lines: string[];
|
|
144
177
|
actions?: TaskListCardAction[];
|
|
@@ -667,6 +700,128 @@ export class CardRenderer {
|
|
|
667
700
|
return sectionCard("Agent Bot 已启动", [markdown(lines.join("\n"))], "green");
|
|
668
701
|
}
|
|
669
702
|
|
|
703
|
+
renderInitializationWelcome(view: InitializationWelcomeView): Record<string, unknown> {
|
|
704
|
+
const chinese = view.language === "zh";
|
|
705
|
+
const title = view.kind === "first"
|
|
706
|
+
? (chinese ? "欢迎使用 Agent Bot" : "Welcome to Agent Bot")
|
|
707
|
+
: view.kind === "upgrade"
|
|
708
|
+
? (chinese ? "Agent Bot 已更新" : "Agent Bot is updated")
|
|
709
|
+
: (chinese ? "Agent Bot 已准备就绪" : "Agent Bot is ready");
|
|
710
|
+
const subtitle = view.activationPending
|
|
711
|
+
? (chinese ? "配置已完成,安全重启后生效" : "Configured and pending a safe restart")
|
|
712
|
+
: view.kind === "first"
|
|
713
|
+
? (chinese ? "本地 Agent 已接入飞书" : "Your local Agents are now available in Lark")
|
|
714
|
+
: view.kind === "upgrade"
|
|
715
|
+
? (chinese ? `新版本 ${view.version} 已生效` : `Version ${view.version} is now active`)
|
|
716
|
+
: (chinese ? "初始化配置已刷新" : "Initialization settings were refreshed");
|
|
717
|
+
const activationNote = view.activationPending
|
|
718
|
+
? (chinese ? "当前任务完成并安全重启后生效。" : "It will take effect after active work finishes and the safe restart completes.")
|
|
719
|
+
: undefined;
|
|
720
|
+
const intro = view.kind === "first"
|
|
721
|
+
? (chinese
|
|
722
|
+
? "**初始化完成**\n从现在起,你可以直接在飞书里把任务交给本机 Agent,并随时查看进度、切换任务或创建分支。"
|
|
723
|
+
: "**Setup complete**\nYou can now work with local Agents from Lark, follow progress, switch tasks, and branch conversations.")
|
|
724
|
+
: view.kind === "upgrade"
|
|
725
|
+
? (chinese
|
|
726
|
+
? `**升级完成**\n${view.previousVersion ? `${inlineCode(view.previousVersion)} → ` : ""}${inlineCode(view.version)} 已准备好。${activationNote ?? "下面是本版值得关注的能力。"}`
|
|
727
|
+
: `**Upgrade complete**\n${view.previousVersion ? `${inlineCode(view.previousVersion)} → ` : ""}${inlineCode(view.version)} is ready. ${activationNote ?? "Here is what is new."}`)
|
|
728
|
+
: (chinese
|
|
729
|
+
? `**配置刷新完成**\n${inlineCode(view.version)} 已重新检查配置、Agent 和飞书连接。${activationNote ?? ""}`
|
|
730
|
+
: `**Configuration refreshed**\n${inlineCode(view.version)} rechecked your configuration, Agents, and Lark connection. ${activationNote ?? ""}`);
|
|
731
|
+
const logo = {
|
|
732
|
+
...localCardImage(view.logoPath, "Agent Bot logo"),
|
|
733
|
+
preview: false,
|
|
734
|
+
};
|
|
735
|
+
const featureColumns = view.features.slice(0, 4).map((feature) => ({
|
|
736
|
+
tag: "column",
|
|
737
|
+
width: "weighted",
|
|
738
|
+
weight: 1,
|
|
739
|
+
vertical_align: "top",
|
|
740
|
+
elements: [markdown([
|
|
741
|
+
`${feature.icon} **${feature.title}**`,
|
|
742
|
+
`<font color='grey'>${feature.description}</font>`,
|
|
743
|
+
].join("\n"))],
|
|
744
|
+
}));
|
|
745
|
+
const featureRows = [0, 2].flatMap((start) => {
|
|
746
|
+
const columns = featureColumns.slice(start, start + 2);
|
|
747
|
+
return columns.length > 0
|
|
748
|
+
? [{
|
|
749
|
+
tag: "column_set",
|
|
750
|
+
flex_mode: "none",
|
|
751
|
+
horizontal_spacing: "16px",
|
|
752
|
+
vertical_align: "top",
|
|
753
|
+
columns,
|
|
754
|
+
}]
|
|
755
|
+
: [];
|
|
756
|
+
});
|
|
757
|
+
const availableAgents = view.availableAgents.length > 0
|
|
758
|
+
? view.availableAgents.map((agent) => inlineCode(agent)).join(" · ")
|
|
759
|
+
: inlineCode(view.defaultAgentName);
|
|
760
|
+
return {
|
|
761
|
+
schema: "2.0",
|
|
762
|
+
config: {
|
|
763
|
+
update_multi: true,
|
|
764
|
+
width_mode: "fill",
|
|
765
|
+
},
|
|
766
|
+
header: {
|
|
767
|
+
template: view.kind === "first" ? "turquoise" : view.kind === "upgrade" ? "blue" : "green",
|
|
768
|
+
title: { tag: "plain_text", content: title },
|
|
769
|
+
subtitle: { tag: "plain_text", content: subtitle },
|
|
770
|
+
padding: "12px 12px 12px 12px",
|
|
771
|
+
},
|
|
772
|
+
body: {
|
|
773
|
+
direction: "vertical",
|
|
774
|
+
vertical_spacing: "12px",
|
|
775
|
+
padding: "12px 12px 12px 12px",
|
|
776
|
+
elements: [
|
|
777
|
+
{
|
|
778
|
+
tag: "column_set",
|
|
779
|
+
flex_mode: "none",
|
|
780
|
+
horizontal_spacing: "16px",
|
|
781
|
+
vertical_align: "center",
|
|
782
|
+
columns: [
|
|
783
|
+
{
|
|
784
|
+
tag: "column",
|
|
785
|
+
width: "weighted",
|
|
786
|
+
weight: 1,
|
|
787
|
+
vertical_align: "center",
|
|
788
|
+
elements: [logo],
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
tag: "column",
|
|
792
|
+
width: "weighted",
|
|
793
|
+
weight: 3,
|
|
794
|
+
vertical_align: "center",
|
|
795
|
+
elements: [markdown(intro)],
|
|
796
|
+
},
|
|
797
|
+
],
|
|
798
|
+
},
|
|
799
|
+
{ tag: "hr" },
|
|
800
|
+
markdown(chinese
|
|
801
|
+
? `**${view.kind === "upgrade" ? "本版亮点" : "你可以这样使用"}**`
|
|
802
|
+
: `**${view.kind === "upgrade" ? "What's new" : "What you can do"}**`),
|
|
803
|
+
...featureRows,
|
|
804
|
+
{ tag: "hr" },
|
|
805
|
+
markdown(chinese
|
|
806
|
+
? "> 直接发送消息即可开始;发送 `/new` 创建新任务;发送 `/help` 查看全部命令。"
|
|
807
|
+
: "> Send a message to begin, `/new` for a new task, or `/help` for all commands."),
|
|
808
|
+
{
|
|
809
|
+
...markdown(chinese
|
|
810
|
+
? `**版本** ${inlineCode(view.version)} · **默认 Agent** ${inlineCode(view.defaultAgentTitle)} · **可用 Agent** ${availableAgents}`
|
|
811
|
+
: `**Version** ${inlineCode(view.version)} · **Default Agent** ${inlineCode(view.defaultAgentTitle)} · **Available** ${availableAgents}`),
|
|
812
|
+
text_size: "notation",
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
...markdown(chinese
|
|
816
|
+
? "📋 [查看更新日志](https://github.com/keyou/agent-bot/blob/master/CHANGELOG.md)"
|
|
817
|
+
: "📋 [View Changelog](https://github.com/keyou/agent-bot/blob/master/CHANGELOG.md)"),
|
|
818
|
+
text_size: "notation",
|
|
819
|
+
},
|
|
820
|
+
],
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
670
825
|
renderTurn(state: TurnViewState): Record<string, unknown> {
|
|
671
826
|
const elements = renderTurnElements(state, "hidden");
|
|
672
827
|
if (isTurnStoppable(state.status)) {
|
|
@@ -794,6 +949,19 @@ export class CardRenderer {
|
|
|
794
949
|
return sectionCard(title, elements);
|
|
795
950
|
}
|
|
796
951
|
|
|
952
|
+
renderHelpCard(
|
|
953
|
+
title: string,
|
|
954
|
+
introLines: string[],
|
|
955
|
+
sections: HelpCardSection[],
|
|
956
|
+
): Record<string, unknown> {
|
|
957
|
+
const elements: Record<string, unknown>[] = [markdown(introLines.join("\n"))];
|
|
958
|
+
for (const section of sections) {
|
|
959
|
+
elements.push({ tag: "hr" }, markdown(`**${section.title}**`));
|
|
960
|
+
elements.push(...section.commands.map(helpCommandRow));
|
|
961
|
+
}
|
|
962
|
+
return sectionCard(title, elements);
|
|
963
|
+
}
|
|
964
|
+
|
|
797
965
|
renderTaskListCard(
|
|
798
966
|
title: string,
|
|
799
967
|
sectionTitle: string,
|
|
@@ -813,10 +981,12 @@ export class CardRenderer {
|
|
|
813
981
|
if (index < entries.length - 1) elements.push({ tag: "hr" });
|
|
814
982
|
});
|
|
815
983
|
}
|
|
984
|
+
if (footerActions.length > 0) {
|
|
985
|
+
elements.push({ tag: "hr" }, taskActionRow(footerActions));
|
|
986
|
+
}
|
|
816
987
|
if (footerLines.length > 0) {
|
|
817
988
|
elements.push({ tag: "hr" }, markdown(footerLines.join("\n")));
|
|
818
989
|
}
|
|
819
|
-
if (footerActions.length > 0) elements.push(taskActionRow(footerActions));
|
|
820
990
|
return {
|
|
821
991
|
schema: "2.0",
|
|
822
992
|
config: {
|
|
@@ -1588,6 +1758,39 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
|
|
|
1588
1758
|
};
|
|
1589
1759
|
}
|
|
1590
1760
|
|
|
1761
|
+
function helpCommandRow(command: HelpCardCommand): Record<string, unknown> {
|
|
1762
|
+
const details = [
|
|
1763
|
+
command.usage ? `**${command.usage}**` : undefined,
|
|
1764
|
+
command.description,
|
|
1765
|
+
].filter((line): line is string => Boolean(line)).join(" ");
|
|
1766
|
+
return {
|
|
1767
|
+
tag: "column_set",
|
|
1768
|
+
flex_mode: "none",
|
|
1769
|
+
horizontal_spacing: "12px",
|
|
1770
|
+
vertical_align: "top",
|
|
1771
|
+
margin: "2px 0 0 0",
|
|
1772
|
+
columns: [
|
|
1773
|
+
{
|
|
1774
|
+
tag: "column",
|
|
1775
|
+
width: "auto",
|
|
1776
|
+
vertical_align: "top",
|
|
1777
|
+
elements: [
|
|
1778
|
+
command.action
|
|
1779
|
+
? taskActionElement(command.action)
|
|
1780
|
+
: markdown(`**${escapeCardHtml(command.text)}**`),
|
|
1781
|
+
],
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
tag: "column",
|
|
1785
|
+
width: "weighted",
|
|
1786
|
+
weight: 1,
|
|
1787
|
+
vertical_align: "top",
|
|
1788
|
+
elements: [markdown(details)],
|
|
1789
|
+
},
|
|
1790
|
+
],
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1591
1794
|
function turnCard(
|
|
1592
1795
|
title: string,
|
|
1593
1796
|
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 {
|