@keyou007/agent-bot 0.1.22 → 0.1.24-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +26 -2
- package/README.zh.md +26 -2
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +4 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/taskGroupOptions.js +27 -0
- package/dist/cli/taskGroupOptions.js.map +1 -1
- package/dist/cli.js +21 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +19 -3
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +92 -9
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +27 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +31 -2
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +6 -3
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +17 -7
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +106 -87
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/proxy/ConversationExport.js +41 -0
- package/dist/proxy/ConversationExport.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +118 -7
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +65 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +12 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/updates/AutomaticUpdatePreparer.js +66 -0
- package/dist/updates/AutomaticUpdatePreparer.js.map +1 -0
- package/dist/updates/DailyUpdateMonitor.js +261 -0
- package/dist/updates/DailyUpdateMonitor.js.map +1 -0
- package/dist/updates/PublishedRelease.js +43 -0
- package/dist/updates/PublishedRelease.js.map +1 -0
- package/dist/updates/types.js +2 -0
- package/dist/updates/types.js.map +1 -0
- package/docs/technical-reference.md +4 -0
- package/docs/technical-reference.zh.md +4 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +11 -0
- package/src/cli/controlProtocol.ts +9 -0
- package/src/cli/help.ts +4 -0
- package/src/cli/taskGroupOptions.ts +33 -3
- package/src/cli.ts +21 -0
- package/src/codex/CodexProcessManager.ts +21 -4
- package/src/codex/CodexRuntime.ts +104 -12
- package/src/commands/CommandRouter.ts +24 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +4 -1
- package/src/feishu/CardRenderer.ts +34 -2
- package/src/feishu/FeishuMessageClient.ts +6 -2
- package/src/feishu/MergedForwardMessage.ts +29 -7
- package/src/feishu/types.ts +1 -1
- package/src/index.ts +49 -0
- package/src/local-files/TurnPreviewPage.ts +105 -88
- package/src/proxy/ConversationExport.ts +43 -0
- package/src/proxy/ProxySessionController.ts +166 -8
- package/src/runtime/types.ts +6 -0
- package/src/state/StateStore.ts +69 -0
- package/src/state/migrations.ts +12 -0
- package/src/updates/AutomaticUpdatePreparer.ts +76 -0
- package/src/updates/DailyUpdateMonitor.ts +252 -0
- package/src/updates/PublishedRelease.ts +44 -0
- package/src/updates/types.ts +19 -0
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
AgentProcessInfo,
|
|
6
6
|
ApprovalDecision,
|
|
7
7
|
CreateRuntimeSessionInput,
|
|
8
|
+
ConversationTurn,
|
|
8
9
|
ForkRuntimeSessionInput,
|
|
9
10
|
ModelOption,
|
|
10
11
|
ModelProviderOption,
|
|
@@ -383,6 +384,46 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
383
384
|
};
|
|
384
385
|
}
|
|
385
386
|
|
|
387
|
+
async *readConversation(remoteSessionId: string, throughTurnId?: string): AsyncIterable<ConversationTurn> {
|
|
388
|
+
const client = await this.client();
|
|
389
|
+
const seenCursors = new Set<string>();
|
|
390
|
+
let cursor: string | undefined;
|
|
391
|
+
const boundary = throughTurnId
|
|
392
|
+
?? (await this.listRecentThreadTurnsThroughCompleted(client, remoteSessionId))
|
|
393
|
+
.find((turn) => turn.status === "completed")?.id;
|
|
394
|
+
if (!boundary) return;
|
|
395
|
+
do {
|
|
396
|
+
const page = await this.readTurnPage(client, remoteSessionId, {
|
|
397
|
+
cursor, limit: 50, itemsView: "summary", sortDirection: "asc",
|
|
398
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
399
|
+
for (const turn of page.data) {
|
|
400
|
+
if (turn.status === "inProgress" || (turn.id === boundary && turn.status !== "completed")) {
|
|
401
|
+
throw new Error("上下文历史发生变化,请重试。");
|
|
402
|
+
}
|
|
403
|
+
if (turn.status !== "completed") continue;
|
|
404
|
+
const messages: ConversationTurn["messages"] = [];
|
|
405
|
+
for (const item of turn.items ?? []) {
|
|
406
|
+
if (item.type === "userMessage") {
|
|
407
|
+
const text = (item.content ?? []).filter((block) => block.type === "text")
|
|
408
|
+
.map((block) => block.text ?? "").join("\n");
|
|
409
|
+
if (text.trim()) messages.push({ role: "user", text });
|
|
410
|
+
} else if (item.type === "agentMessage" && item.phase !== "commentary" && item.text?.trim()) {
|
|
411
|
+
messages.push({ role: "assistant", text: item.text });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
yield { turnId: turn.id, messages };
|
|
415
|
+
if (turn.id === boundary) return;
|
|
416
|
+
}
|
|
417
|
+
const nextCursor = page.nextCursor ?? undefined;
|
|
418
|
+
if (nextCursor && (nextCursor === cursor || seenCursors.has(nextCursor))) {
|
|
419
|
+
throw new Error("App Server repeated a conversation history cursor.");
|
|
420
|
+
}
|
|
421
|
+
if (nextCursor) seenCursors.add(nextCursor);
|
|
422
|
+
cursor = nextCursor;
|
|
423
|
+
} while (cursor);
|
|
424
|
+
throw new Error("找不到上下文截止轮次,未创建克隆任务。");
|
|
425
|
+
}
|
|
426
|
+
|
|
386
427
|
async readRemoteForkSource(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
387
428
|
const client = await this.client();
|
|
388
429
|
const metadata = await client.request<ThreadReadResponse>(
|
|
@@ -454,14 +495,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
454
495
|
private async readTurnPage(
|
|
455
496
|
client: AppServerClient,
|
|
456
497
|
remoteSessionId: string,
|
|
457
|
-
input: { cursor?: string; limit: number; itemsView: "summary" | "full" },
|
|
498
|
+
input: { cursor?: string; limit: number; itemsView: "summary" | "full"; sortDirection?: "asc" | "desc" },
|
|
458
499
|
timeoutMs = SYNC_REQUEST_TIMEOUT_MS,
|
|
459
500
|
): Promise<ThreadTurnsListResponse> {
|
|
460
501
|
try {
|
|
461
502
|
return await client.request<ThreadTurnsListResponse>("thread/turns/list", {
|
|
462
503
|
threadId: remoteSessionId,
|
|
463
504
|
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
464
|
-
limit: input.limit, sortDirection: "desc", itemsView: input.itemsView,
|
|
505
|
+
limit: input.limit, sortDirection: input.sortDirection ?? "desc", itemsView: input.itemsView,
|
|
465
506
|
}, timeoutMs);
|
|
466
507
|
} catch (error) {
|
|
467
508
|
if (error instanceof AppServerRequestError && error.method === "thread/turns/list"
|
|
@@ -674,17 +715,38 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
674
715
|
await this.detachThreadForSettings(client, session);
|
|
675
716
|
detached = true;
|
|
676
717
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
718
|
+
let response: ThreadResponse;
|
|
719
|
+
if (empty) {
|
|
720
|
+
response = await client.request<ThreadResponse>("thread/start", {
|
|
721
|
+
...params, threadSource: "user", allowProviderModelFallback: false,
|
|
722
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
723
|
+
replacementId = response.thread.id;
|
|
724
|
+
} else {
|
|
725
|
+
response = await client.request<ThreadResponse>("thread/resume", {
|
|
726
|
+
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
727
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
728
|
+
if (!providerSettingsMatch(response, settings)) {
|
|
729
|
+
this.logger.warn(
|
|
730
|
+
{
|
|
731
|
+
sessionId,
|
|
732
|
+
threadId: session.remoteSessionId,
|
|
733
|
+
requestedProvider: settings.modelProvider,
|
|
734
|
+
actualProvider: response.modelProvider,
|
|
735
|
+
requestedModel: settings.model,
|
|
736
|
+
actualModel: response.model,
|
|
737
|
+
},
|
|
738
|
+
"App Server did not apply Provider settings while resuming; forking the latest completed Turn.",
|
|
739
|
+
);
|
|
740
|
+
await this.detachThreadForSettings(client, session);
|
|
741
|
+
response = await this.forkThreadForSettings(client, session, settings);
|
|
742
|
+
replacementId = response.thread.id;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
685
745
|
assertProviderSettingsApplied(response, settings);
|
|
686
|
-
if (!empty && response.thread.id !== session.remoteSessionId)
|
|
687
|
-
|
|
746
|
+
if (!empty && !replacementId && response.thread.id !== session.remoteSessionId) {
|
|
747
|
+
throw new Error("App Server 返回了不同的任务 ID,未保存设置。");
|
|
748
|
+
}
|
|
749
|
+
if (session.title && (empty || replacementId)) await client.request("thread/name/set", {
|
|
688
750
|
threadId: response.thread.id, name: session.title,
|
|
689
751
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
690
752
|
const candidate = { ...session, ...settings, remoteSessionId: response.thread.id,
|
|
@@ -734,6 +796,29 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
734
796
|
}
|
|
735
797
|
}
|
|
736
798
|
|
|
799
|
+
private async forkThreadForSettings(
|
|
800
|
+
client: AppServerClient,
|
|
801
|
+
session: CodexSession,
|
|
802
|
+
settings: RuntimeExecutionSettings,
|
|
803
|
+
): Promise<ThreadResponse> {
|
|
804
|
+
const turns = await this.listRecentThreadTurnsThroughCompleted(client, session.remoteSessionId);
|
|
805
|
+
const lastTurnId = turns.find((turn) => turn.status === "completed")?.id;
|
|
806
|
+
if (!lastTurnId) {
|
|
807
|
+
throw new Error("无法为 Provider 切换找到已完成的 Turn。请先完成一轮任务后重试。");
|
|
808
|
+
}
|
|
809
|
+
return client.request<ThreadResponse>("thread/fork", {
|
|
810
|
+
threadId: session.remoteSessionId,
|
|
811
|
+
lastTurnId,
|
|
812
|
+
cwd: session.cwd,
|
|
813
|
+
model: settings.model,
|
|
814
|
+
modelProvider: settings.modelProvider,
|
|
815
|
+
threadSource: "user",
|
|
816
|
+
excludeTurns: true,
|
|
817
|
+
...threadLifecycleParams(session.cwd),
|
|
818
|
+
...permissionParams(settings.permissionMode),
|
|
819
|
+
}, FORK_REQUEST_TIMEOUT_MS);
|
|
820
|
+
}
|
|
821
|
+
|
|
737
822
|
private async discardUnusedThread(client: AppServerClient, threadId: string): Promise<void> {
|
|
738
823
|
try {
|
|
739
824
|
await client.request("thread/unsubscribe", { threadId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
@@ -1688,6 +1773,13 @@ function remoteThreadStatus(value: string | undefined): RemoteSessionSummary["st
|
|
|
1688
1773
|
return "not_loaded";
|
|
1689
1774
|
}
|
|
1690
1775
|
|
|
1776
|
+
function providerSettingsMatch(
|
|
1777
|
+
response: Pick<ThreadResponse, "modelProvider" | "model">,
|
|
1778
|
+
settings: RuntimeExecutionSettings,
|
|
1779
|
+
): boolean {
|
|
1780
|
+
return response.modelProvider === settings.modelProvider && response.model === settings.model;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1691
1783
|
function codexSourceLabel(source: unknown): string {
|
|
1692
1784
|
if (typeof source === "string") return source;
|
|
1693
1785
|
if (isRecord(source)) {
|
|
@@ -3,6 +3,8 @@ import type { Command } from "./commandTypes.js";
|
|
|
3
3
|
export const COMMAND_NAMES = [
|
|
4
4
|
"agent",
|
|
5
5
|
"archive",
|
|
6
|
+
"clone",
|
|
7
|
+
"clonegroup",
|
|
6
8
|
"dir",
|
|
7
9
|
"dismiss",
|
|
8
10
|
"file",
|
|
@@ -34,6 +36,7 @@ type CommandName = (typeof COMMAND_NAMES)[number];
|
|
|
34
36
|
const COMMAND_INITIALISMS: Partial<Record<CommandName, string>> = {
|
|
35
37
|
dir: "di",
|
|
36
38
|
forkgroup: "fg",
|
|
39
|
+
clonegroup: "cg",
|
|
37
40
|
newgroup: "ng",
|
|
38
41
|
nosteer: "ns",
|
|
39
42
|
};
|
|
@@ -67,6 +70,9 @@ export class CommandRouter {
|
|
|
67
70
|
return parseNewCommand(args);
|
|
68
71
|
case "newgroup":
|
|
69
72
|
return parseNewGroupCommand(args);
|
|
73
|
+
case "clone":
|
|
74
|
+
case "clonegroup":
|
|
75
|
+
return parseCloneCommand(command, args);
|
|
70
76
|
case "forkgroup":
|
|
71
77
|
return parseForkGroupCommand(args);
|
|
72
78
|
case "fork":
|
|
@@ -242,6 +248,24 @@ function parseNewTaskOptions(
|
|
|
242
248
|
};
|
|
243
249
|
}
|
|
244
250
|
|
|
251
|
+
function parseCloneCommand(type: "clone" | "clonegroup", args: string[]): Command {
|
|
252
|
+
const title: string[] = [];
|
|
253
|
+
let agentName: string | undefined;
|
|
254
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
255
|
+
const argument = args[index]!;
|
|
256
|
+
if (argument === "--agent") {
|
|
257
|
+
if (agentName !== undefined) throw new Error(`/${type} 只能指定一次 --agent。`);
|
|
258
|
+
const name = args[++index];
|
|
259
|
+
if (!name?.trim() || name.startsWith("--")) throw new Error("请在 --agent 后指定 Agent 名称。");
|
|
260
|
+
agentName = name.trim();
|
|
261
|
+
} else {
|
|
262
|
+
if (argument.startsWith("--")) throw new Error(`/${type} 不支持参数:${argument}。`);
|
|
263
|
+
title.push(argument);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { type, title: title.join(" ").trim() || undefined, agentName };
|
|
267
|
+
}
|
|
268
|
+
|
|
245
269
|
function parseForkCommand(args: string[]): Extract<Command, { type: "fork" }> {
|
|
246
270
|
rejectOptions("/fork", args);
|
|
247
271
|
if (args.length > 1) throw new Error("/fork 只接受一个可选的任务序号或任务 ID。");
|
|
@@ -4,6 +4,7 @@ export type Command =
|
|
|
4
4
|
| { type: "newgroup"; title?: string; cwd?: string; projectless?: boolean }
|
|
5
5
|
| { type: "dir"; directory?: string }
|
|
6
6
|
| { type: "file"; filePath: string }
|
|
7
|
+
| { type: "clone" | "clonegroup"; title?: string; agentName?: string }
|
|
7
8
|
| { type: "forkgroup"; title?: string }
|
|
8
9
|
| { type: "fork"; sessionId?: string }
|
|
9
10
|
| { type: "title"; title: string }
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -26,6 +26,10 @@ const DEFAULT_CONFIG = `feishu:
|
|
|
26
26
|
console:
|
|
27
27
|
enabled: true
|
|
28
28
|
|
|
29
|
+
updates:
|
|
30
|
+
# Daily stable update check, private 60-second cancellation window, then safe update.
|
|
31
|
+
enabled: true
|
|
32
|
+
|
|
29
33
|
fileViewer:
|
|
30
34
|
enabled: true
|
|
31
35
|
# Use 0.0.0.0 to accept LAN connections and automatically prefer
|
package/src/config/schema.ts
CHANGED
|
@@ -57,6 +57,7 @@ export const agentConfigSchema = z.object({
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
export const appConfigSchema = z.object({
|
|
60
|
+
updates: z.object({ enabled: z.boolean().default(true) }).default({ enabled: true }),
|
|
60
61
|
console: z
|
|
61
62
|
.object({
|
|
62
63
|
enabled: z.boolean().default(true),
|
|
@@ -107,4 +108,6 @@ export type AgentConfig = Omit<ParsedAgentConfig, "defaults"> & {
|
|
|
107
108
|
defaults?: AgentExecutionDefaults;
|
|
108
109
|
};
|
|
109
110
|
export type GroupNameFormatConfig = z.infer<typeof groupNameFormatSchema>;
|
|
110
|
-
export type AppConfig = z.infer<typeof appConfigSchema
|
|
111
|
+
export type AppConfig = Omit<z.infer<typeof appConfigSchema>, "updates"> & {
|
|
112
|
+
updates?: { enabled: boolean };
|
|
113
|
+
};
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from "../runtime/types.js";
|
|
13
13
|
import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
14
14
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
15
|
+
import type { UpdateNotice } from "../updates/types.js";
|
|
15
16
|
import { localCardImage } from "./LocalCardImage.js";
|
|
16
17
|
|
|
17
18
|
export interface StartupStatusView {
|
|
@@ -953,6 +954,33 @@ export class CardRenderer {
|
|
|
953
954
|
};
|
|
954
955
|
}
|
|
955
956
|
|
|
957
|
+
renderUpdateNotice(view: UpdateNotice, now = Date.now()): Record<string, unknown> {
|
|
958
|
+
const cancellable = view.status === "announcing" || view.status === "countdown";
|
|
959
|
+
const status = cancellable
|
|
960
|
+
? `**自动更新倒计时:${Math.max(0, Math.ceil(((view.deadline ?? now + 60_000) - now) / 1_000))} 秒**`
|
|
961
|
+
: view.status === "cancelled" ? "已取消本版本自动更新,不再提醒。"
|
|
962
|
+
: view.status === "preparing" ? "正在下载并校验更新;任务结束后自动安装并重启。"
|
|
963
|
+
: view.status === "scheduled" ? "更新已就绪,等待任务和结果投递完成后自动重启。"
|
|
964
|
+
: view.status === "completed" ? "更新已完成。"
|
|
965
|
+
: "自动更新未完成,当前版本继续运行。";
|
|
966
|
+
const elements = [
|
|
967
|
+
markdown(`**Agent Bot** ${inlineCode(view.currentVersion)} → ${inlineCode(view.version)}`),
|
|
968
|
+
markdown(`**本版更新**\n${truncateText(view.notes, 4_000)}`),
|
|
969
|
+
{ tag: "hr" },
|
|
970
|
+
markdown(status),
|
|
971
|
+
];
|
|
972
|
+
if (view.error) elements.push(markdown(inlineCode(truncateText(view.error, 600))));
|
|
973
|
+
if (cancellable) {
|
|
974
|
+
elements.push(markdown("> 1 分钟内未取消将自动更新;不会中断正在执行的任务。"));
|
|
975
|
+
elements.push(taskActionRow([{
|
|
976
|
+
text: "取消本次更新",
|
|
977
|
+
value: { action: "agentbot_update_cancel", version: view.version, token: view.token },
|
|
978
|
+
}]));
|
|
979
|
+
}
|
|
980
|
+
return sectionCard("Agent Bot 更新提醒", elements,
|
|
981
|
+
view.status === "completed" ? "green" : view.status === "cancelled" ? "grey" : "blue");
|
|
982
|
+
}
|
|
983
|
+
|
|
956
984
|
renderSafeRestartStatus(view: SafeRestartStatusView): Record<string, unknown> {
|
|
957
985
|
const countdown = view.phase === "countdown"
|
|
958
986
|
? `${Math.max(0, Math.ceil((view.remainingMs ?? 0) / 1_000))}s`
|
|
@@ -2370,7 +2398,7 @@ function truncateToolResult(result: string): string {
|
|
|
2370
2398
|
return `${result.slice(0, headLength)}${marker}${result.slice(-tailLength)}`;
|
|
2371
2399
|
}
|
|
2372
2400
|
|
|
2373
|
-
function displayFilePath(filePath: string, projectCwd?: string): string {
|
|
2401
|
+
export function displayFilePath(filePath: string, projectCwd?: string): string {
|
|
2374
2402
|
if (!projectCwd) return filePath;
|
|
2375
2403
|
const pathApi = usesWindowsPaths(projectCwd, filePath) ? path.win32 : path;
|
|
2376
2404
|
const normalizedCwd = pathApi.resolve(projectCwd);
|
|
@@ -2393,8 +2421,12 @@ function usesWindowsPaths(...values: string[]): boolean {
|
|
|
2393
2421
|
return values.some((value) => /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value));
|
|
2394
2422
|
}
|
|
2395
2423
|
|
|
2424
|
+
export function toolStatusIcon(status: ToolState["status"]): string {
|
|
2425
|
+
return status === "failed" ? "❌" : status === "running" ? "⏳" : "✅";
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2396
2428
|
function toolPanelTitle(tool: ToolState): string {
|
|
2397
|
-
const icon = tool.status
|
|
2429
|
+
const icon = toolStatusIcon(tool.status);
|
|
2398
2430
|
const command = stripAnsi(tool.command ?? tool.title).trim();
|
|
2399
2431
|
const meaningfulCommand = tool.kind === "web_search"
|
|
2400
2432
|
? tool.title
|
|
@@ -259,8 +259,11 @@ export class FeishuMessageClient implements FeishuOutbound {
|
|
|
259
259
|
private async readMessageItems(messageId: string, operation: string): Promise<MergedForwardMessageItem[]> {
|
|
260
260
|
try {
|
|
261
261
|
const token = await this.getTenantAccessToken();
|
|
262
|
+
const url = new URL(`https://open.feishu.cn/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`);
|
|
263
|
+
// The default compatibility format can replace Card 2.0 content with an upgrade notice.
|
|
264
|
+
url.searchParams.set("card_msg_content_type", "user_card_content");
|
|
262
265
|
const response = await fetch(
|
|
263
|
-
|
|
266
|
+
url,
|
|
264
267
|
{ headers: { Authorization: `Bearer ${token}` } },
|
|
265
268
|
);
|
|
266
269
|
const payload = (await response.json()) as GetMessageResponse;
|
|
@@ -303,8 +306,9 @@ export class FeishuMessageClient implements FeishuOutbound {
|
|
|
303
306
|
async sendInteractiveCard(
|
|
304
307
|
contextKey: string,
|
|
305
308
|
card: Record<string, unknown>,
|
|
309
|
+
idempotencyKey?: string,
|
|
306
310
|
): Promise<string | undefined> {
|
|
307
|
-
return this.sendMessage(contextKey, "interactive", await this.prepareInteractiveCard(card));
|
|
311
|
+
return this.sendMessage(contextKey, "interactive", await this.prepareInteractiveCard(card), idempotencyKey);
|
|
308
312
|
}
|
|
309
313
|
|
|
310
314
|
async replyText(
|
|
@@ -187,7 +187,7 @@ function renderMessageContent(
|
|
|
187
187
|
case "sticker":
|
|
188
188
|
return "[表情包]";
|
|
189
189
|
case "interactive":
|
|
190
|
-
return renderInteractiveCard(content) || "[消息卡片]";
|
|
190
|
+
return renderInteractiveCard(content, imageLabel) || "[消息卡片]";
|
|
191
191
|
case "share_chat":
|
|
192
192
|
return "[群名片]";
|
|
193
193
|
case "share_user":
|
|
@@ -235,27 +235,49 @@ function messageTypeLabel(messageType: string): string {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
function renderInteractiveCard(
|
|
238
|
+
function renderInteractiveCard(
|
|
239
|
+
content: Record<string, unknown>,
|
|
240
|
+
imageLabel: (imageKey: string) => string,
|
|
241
|
+
): string {
|
|
239
242
|
const values: string[] = [];
|
|
240
|
-
collectCardText(content, undefined, values);
|
|
243
|
+
collectCardText(content, undefined, values, imageLabel);
|
|
241
244
|
return [...new Set(values.map((value) => normalizeFeishuPostText(value)).filter(Boolean))].join("\n");
|
|
242
245
|
}
|
|
243
246
|
|
|
244
|
-
function collectCardText(
|
|
247
|
+
function collectCardText(
|
|
248
|
+
value: unknown,
|
|
249
|
+
key: string | undefined,
|
|
250
|
+
output: string[],
|
|
251
|
+
imageLabel: (imageKey: string) => string,
|
|
252
|
+
): void {
|
|
245
253
|
if (typeof value === "string") {
|
|
246
|
-
if (
|
|
254
|
+
if (
|
|
255
|
+
(key === "content" || key === "text" || key === "title" || key === "subtitle")
|
|
256
|
+
&& !isUnsupportedCardClientPlaceholder(value)
|
|
257
|
+
) {
|
|
258
|
+
output.push(value);
|
|
259
|
+
}
|
|
247
260
|
return;
|
|
248
261
|
}
|
|
249
262
|
if (Array.isArray(value)) {
|
|
250
|
-
for (const item of value) collectCardText(item, key, output);
|
|
263
|
+
for (const item of value) collectCardText(item, key, output, imageLabel);
|
|
251
264
|
return;
|
|
252
265
|
}
|
|
253
266
|
if (!isRecord(value)) return;
|
|
267
|
+
const imageKey = textValue(value.image_key ?? value.img_key);
|
|
268
|
+
if (imageKey) {
|
|
269
|
+
output.push(imageLabel(imageKey));
|
|
270
|
+
}
|
|
254
271
|
for (const [childKey, childValue] of Object.entries(value)) {
|
|
255
|
-
collectCardText(childValue, childKey, output);
|
|
272
|
+
collectCardText(childValue, childKey, output, imageLabel);
|
|
256
273
|
}
|
|
257
274
|
}
|
|
258
275
|
|
|
276
|
+
function isUnsupportedCardClientPlaceholder(value: string): boolean {
|
|
277
|
+
return value.trim() === "请升级至最新版本客户端,以查看内容"
|
|
278
|
+
|| /^please upgrade to the latest .*client.* view/iu.test(value.trim());
|
|
279
|
+
}
|
|
280
|
+
|
|
259
281
|
function renderPost(
|
|
260
282
|
content: Record<string, unknown>,
|
|
261
283
|
mentions: MergedForwardMessageItem["mentions"],
|
package/src/feishu/types.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface FeishuOutbound {
|
|
|
72
72
|
sendText(contextKey: string, text: string): Promise<string | undefined>;
|
|
73
73
|
sendFile?(contextKey: string, filePath: string): Promise<string | undefined>;
|
|
74
74
|
sendMarkdown(contextKey: string, markdown: string, idempotencyKey?: string): Promise<string | undefined>;
|
|
75
|
-
sendInteractiveCard(contextKey: string, card: Record<string, unknown
|
|
75
|
+
sendInteractiveCard(contextKey: string, card: Record<string, unknown>, idempotencyKey?: string): Promise<string | undefined>;
|
|
76
76
|
replyText?(
|
|
77
77
|
contextKey: string,
|
|
78
78
|
target: MessageReplyTarget,
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,9 @@ import { ProxySessionController } from "./proxy/ProxySessionController.js";
|
|
|
29
29
|
import type { AgentEnvironmentContext } from "./runtime/agentEnvironment.js";
|
|
30
30
|
import { createAgentRuntimeRegistry } from "./runtime/createAgentRuntimeRegistry.js";
|
|
31
31
|
import { StateStore } from "./state/StateStore.js";
|
|
32
|
+
import { DailyUpdateMonitor } from "./updates/DailyUpdateMonitor.js";
|
|
33
|
+
import { readLatestStableVersion, readReleaseNotes } from "./updates/PublishedRelease.js";
|
|
34
|
+
import { checkAutomaticUpdateSupport, prepareAutomaticUpdate } from "./updates/AutomaticUpdatePreparer.js";
|
|
32
35
|
import { StartupNotifier } from "./startup/StartupNotifier.js";
|
|
33
36
|
import { SessionMetadataHydrator } from "./startup/SessionMetadataHydrator.js";
|
|
34
37
|
import { startFeishu } from "./startup/startFeishu.js";
|
|
@@ -106,6 +109,7 @@ let feishuConnector: FeishuConnector | undefined;
|
|
|
106
109
|
let consoleConnector: ConsoleConnector | undefined;
|
|
107
110
|
let startupNotifier: StartupNotifier | undefined;
|
|
108
111
|
let safeRestartNotifier: SafeRestartNotifier | undefined;
|
|
112
|
+
let dailyUpdateMonitor: DailyUpdateMonitor | undefined;
|
|
109
113
|
let controlServer: LocalControlServer | undefined;
|
|
110
114
|
let serverReady = false;
|
|
111
115
|
|
|
@@ -157,14 +161,48 @@ const safeRestart = new SafeRestartScheduler({
|
|
|
157
161
|
onStatus: (status) => safeRestartNotifier?.update(status),
|
|
158
162
|
onStatusError: (error) => logger.warn({ error }, "Failed to publish safe restart status."),
|
|
159
163
|
});
|
|
164
|
+
if (feishuOutbound && config.updates?.enabled !== false) {
|
|
165
|
+
dailyUpdateMonitor = new DailyUpdateMonitor({
|
|
166
|
+
store, outbound: feishuOutbound, logger, currentVersion: agentBotVersion,
|
|
167
|
+
userOpenId: () => config.feishu.userOpenId,
|
|
168
|
+
readLatest: readLatestStableVersion,
|
|
169
|
+
readNotes: readReleaseNotes,
|
|
170
|
+
checkSupport: () => checkAutomaticUpdateSupport({ home: agentBotHome(), configPath: activeConfigPath }),
|
|
171
|
+
hasPendingUpdate: () => shuttingDown || restartRequested || !!pendingSelfUpdatePlanPath || safeRestart.scheduled,
|
|
172
|
+
pendingVersion: () => pendingSelfUpdatePlanPath ? readPendingSelfUpdate(agentBotHome())?.plan.toVersion : undefined,
|
|
173
|
+
applyUpdate: async (version) => {
|
|
174
|
+
const prepared = await prepareAutomaticUpdate(version, { home: agentBotHome(), configPath: activeConfigPath });
|
|
175
|
+
try {
|
|
176
|
+
if (shuttingDown || restartRequested || pendingSelfUpdatePlanPath || safeRestart.scheduled) {
|
|
177
|
+
throw new Error("服务正在停止或已有更新/重启等待执行,本次自动更新已停止。");
|
|
178
|
+
}
|
|
179
|
+
const target = privateRestartNotificationTarget(config.feishu.userOpenId);
|
|
180
|
+
if (prepared.status === "prepared") {
|
|
181
|
+
assertSelfUpdatePlanPath(prepared.planPath, agentBotHome());
|
|
182
|
+
pendingSelfUpdatePlanPath = prepared.planPath;
|
|
183
|
+
}
|
|
184
|
+
safeRestart.schedule(`Agent Bot 自动更新到 ${version}`, target);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (prepared.status === "prepared") releaseSelfUpdatePlan(prepared.planPath);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
160
192
|
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
161
193
|
supervised,
|
|
162
194
|
restart: requestRestart,
|
|
195
|
+
cancelAutomaticUpdate: async (action) => {
|
|
196
|
+
if (!dailyUpdateMonitor) throw new Error("自动更新功能未启用。");
|
|
197
|
+
await dailyUpdateMonitor.cancel(action);
|
|
198
|
+
},
|
|
163
199
|
cancelSafeRestart: async (scheduleId) => {
|
|
164
200
|
const cancelled = await safeRestart.cancelScheduled(scheduleId);
|
|
165
201
|
if (cancelled && pendingSelfUpdatePlanPath) {
|
|
202
|
+
const version = readPendingSelfUpdate(agentBotHome())?.plan.toVersion;
|
|
166
203
|
releaseSelfUpdatePlan(pendingSelfUpdatePlanPath);
|
|
167
204
|
pendingSelfUpdatePlanPath = undefined;
|
|
205
|
+
if (version) await dailyUpdateMonitor?.cancelPreparedUpdate(version);
|
|
168
206
|
}
|
|
169
207
|
return cancelled;
|
|
170
208
|
},
|
|
@@ -229,6 +267,7 @@ await controller.recoverInterruptedTasks().catch((error: unknown) => {
|
|
|
229
267
|
});
|
|
230
268
|
consoleConnector?.start();
|
|
231
269
|
recoverPendingSelfUpdate();
|
|
270
|
+
await dailyUpdateMonitor?.start();
|
|
232
271
|
|
|
233
272
|
function recoverPendingSelfUpdate(): void {
|
|
234
273
|
const pending = readPendingSelfUpdate(agentBotHome());
|
|
@@ -511,6 +550,15 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
511
550
|
request.agentName,
|
|
512
551
|
),
|
|
513
552
|
};
|
|
553
|
+
case "task_clone":
|
|
554
|
+
case "task_clone_group":
|
|
555
|
+
return {
|
|
556
|
+
ok: true,
|
|
557
|
+
data: await controller.controlCloneTask(
|
|
558
|
+
request.localSessionId, request.title, request.agentName,
|
|
559
|
+
request.action === "task_clone_group", config.feishu.userOpenId,
|
|
560
|
+
),
|
|
561
|
+
};
|
|
514
562
|
case "task_fork_group":
|
|
515
563
|
return {
|
|
516
564
|
ok: true,
|
|
@@ -530,6 +578,7 @@ async function shutdown(
|
|
|
530
578
|
): Promise<void> {
|
|
531
579
|
if (shuttingDown) return;
|
|
532
580
|
shuttingDown = true;
|
|
581
|
+
await dailyUpdateMonitor?.stop();
|
|
533
582
|
if (pendingSelfUpdatePlanPath) {
|
|
534
583
|
releaseSelfUpdatePlan(pendingSelfUpdatePlanPath);
|
|
535
584
|
pendingSelfUpdatePlanPath = undefined;
|