@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
|
@@ -59,6 +59,7 @@ import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
|
|
|
59
59
|
import type {
|
|
60
60
|
AgentRuntime,
|
|
61
61
|
ApprovalDecision,
|
|
62
|
+
ConversationTurn,
|
|
62
63
|
ModelOption,
|
|
63
64
|
PermissionMode,
|
|
64
65
|
RemoteCompletedTurnSummary,
|
|
@@ -81,6 +82,7 @@ import {
|
|
|
81
82
|
import {
|
|
82
83
|
StateStore,
|
|
83
84
|
type CardActionBinding,
|
|
85
|
+
type ForkHistorySourceRecord,
|
|
84
86
|
type MessageReactionRecord,
|
|
85
87
|
type MessageReactionStatus,
|
|
86
88
|
type QueuedPromptRecord,
|
|
@@ -92,6 +94,7 @@ import { createId } from "../utils/id.js";
|
|
|
92
94
|
import { formatStorageSize } from "../utils/formatStorageSize.js";
|
|
93
95
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
94
96
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
97
|
+
import { conversationImportPrompt, exportConversation } from "./ConversationExport.js";
|
|
95
98
|
import {
|
|
96
99
|
executeShellCommand,
|
|
97
100
|
type ShellCommandOptions,
|
|
@@ -260,6 +263,8 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
260
263
|
usage: "[序号或任务 ID]",
|
|
261
264
|
description: "从当前或指定任务最近完成轮次创建分支",
|
|
262
265
|
},
|
|
266
|
+
{ command: "/clone", usage: "[title] [--agent <name>]", description: "导出用户 Prompt 和最终回答,交给新 Agent 任务读取" },
|
|
267
|
+
{ command: "/clonegroup", usage: "[title] [--agent <name>]", description: "将对话上下文交给新群中的 Agent 任务" },
|
|
263
268
|
{ command: "/turns", usage: "[Turn ID 或序号]", description: "浏览历史轮次,并可 Reset 对话上下文;带参数查看运行详情" },
|
|
264
269
|
{
|
|
265
270
|
command: "/title",
|
|
@@ -477,6 +482,14 @@ export interface ControlTaskGroupResult {
|
|
|
477
482
|
task: SessionRecord;
|
|
478
483
|
}
|
|
479
484
|
|
|
485
|
+
export interface ControlTaskCloneResult {
|
|
486
|
+
sourceLocalSessionId: string;
|
|
487
|
+
contextFile: string;
|
|
488
|
+
turnCount: number;
|
|
489
|
+
task: SessionRecord;
|
|
490
|
+
group?: CreatedFeishuGroupContext;
|
|
491
|
+
}
|
|
492
|
+
|
|
480
493
|
export interface ControlTaskReleaseResult {
|
|
481
494
|
agentName: string;
|
|
482
495
|
status: "waiting" | "released";
|
|
@@ -487,6 +500,7 @@ export interface ProxyLifecycle {
|
|
|
487
500
|
supervised?: boolean;
|
|
488
501
|
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
489
502
|
cancelSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
503
|
+
cancelAutomaticUpdate?(action: CardAction): Promise<void>;
|
|
490
504
|
rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
|
|
491
505
|
persistAgentExecutionDefaults?(
|
|
492
506
|
agentName: string,
|
|
@@ -975,7 +989,10 @@ export class ProxySessionController {
|
|
|
975
989
|
await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
|
|
976
990
|
try {
|
|
977
991
|
const kind = String(scopedAction.value.action ?? "");
|
|
978
|
-
if (kind === "
|
|
992
|
+
if (kind === "agentbot_update_cancel") {
|
|
993
|
+
if (!this.lifecycle?.cancelAutomaticUpdate) throw new Error("自动更新功能未启用。");
|
|
994
|
+
await this.lifecycle.cancelAutomaticUpdate(scopedAction);
|
|
995
|
+
} else if (kind === "help_command") {
|
|
979
996
|
await this.executeHelpCommandAction(scopedAction, contextKey, replyTarget);
|
|
980
997
|
} else if (kind === "turn_details") {
|
|
981
998
|
await this.outbound.showDetails(contextKey, String(scopedAction.value.turnId ?? ""));
|
|
@@ -1508,6 +1525,22 @@ export class ProxySessionController {
|
|
|
1508
1525
|
};
|
|
1509
1526
|
}
|
|
1510
1527
|
|
|
1528
|
+
async controlCloneTask(
|
|
1529
|
+
localSessionId: string,
|
|
1530
|
+
requestedTitle?: string,
|
|
1531
|
+
requestedAgentName?: string,
|
|
1532
|
+
newGroup = false,
|
|
1533
|
+
userOpenId?: string,
|
|
1534
|
+
): Promise<ControlTaskCloneResult> {
|
|
1535
|
+
const source = this.requireControlSession(localSessionId);
|
|
1536
|
+
const contextKey = this.controlSessionContextKey(source);
|
|
1537
|
+
const replyTarget = this.controlSessionReplyTarget(source);
|
|
1538
|
+
return this.outbound.withReplyTarget(contextKey, replyTarget, () => this.cloneSession(
|
|
1539
|
+
contextKey, { ...source, contextKey }, requestedTitle, requestedAgentName,
|
|
1540
|
+
newGroup, userOpenId, replyTarget,
|
|
1541
|
+
));
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1511
1544
|
async controlForkTaskGroup(
|
|
1512
1545
|
localSessionId: string,
|
|
1513
1546
|
requestedTitle: string | undefined,
|
|
@@ -1708,6 +1741,7 @@ export class ProxySessionController {
|
|
|
1708
1741
|
await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
|
|
1709
1742
|
}
|
|
1710
1743
|
this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
|
|
1744
|
+
this.outbound.updateSessionModel(record.localSessionId, nextValue);
|
|
1711
1745
|
} else if (setting === "thinking" && nextValue) {
|
|
1712
1746
|
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
1713
1747
|
const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
|
|
@@ -2010,6 +2044,22 @@ export class ProxySessionController {
|
|
|
2010
2044
|
command.projectless === true,
|
|
2011
2045
|
);
|
|
2012
2046
|
return;
|
|
2047
|
+
case "clone":
|
|
2048
|
+
case "clonegroup": {
|
|
2049
|
+
let source = this.currentSession(contextKey);
|
|
2050
|
+
let throughTurnId: string | undefined;
|
|
2051
|
+
if (incomingMessage?.threadContext && (!source
|
|
2052
|
+
|| !this.store.findLatestCompletedTurnId(source.localSessionId, contextKey))) {
|
|
2053
|
+
const resolved = this.resolveThreadForkAnchor(incomingMessage, false);
|
|
2054
|
+
source = resolved.source;
|
|
2055
|
+
throughTurnId = resolved.anchor.turnId;
|
|
2056
|
+
}
|
|
2057
|
+
await this.cloneSession(
|
|
2058
|
+
contextKey, source ?? this.requireCurrentSession(contextKey), command.title,
|
|
2059
|
+
command.agentName, command.type === "clonegroup", userId, replyTarget, throughTurnId,
|
|
2060
|
+
);
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2013
2063
|
case "forkgroup":
|
|
2014
2064
|
await this.forkCurrentSessionToFeishuGroup(contextKey, command.title, userId, incomingMessage);
|
|
2015
2065
|
return;
|
|
@@ -3013,7 +3063,7 @@ export class ProxySessionController {
|
|
|
3013
3063
|
}
|
|
3014
3064
|
}
|
|
3015
3065
|
|
|
3016
|
-
private resolveThreadForkAnchor(message: IncomingMessage): ResolvedThreadForkAnchor {
|
|
3066
|
+
private resolveThreadForkAnchor(message: IncomingMessage, includeSnapshot = true): ResolvedThreadForkAnchor {
|
|
3017
3067
|
if (!message.threadContext || !message.threadId || !message.chatId) {
|
|
3018
3068
|
throw new Error("当前消息不属于可识别的飞书话题。");
|
|
3019
3069
|
}
|
|
@@ -3034,8 +3084,9 @@ export class ProxySessionController {
|
|
|
3034
3084
|
throw new Error("话题来源任务不属于当前会话,已拒绝创建分支。");
|
|
3035
3085
|
}
|
|
3036
3086
|
|
|
3037
|
-
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(anchor.turnId));
|
|
3038
|
-
|
|
3087
|
+
const snapshot = includeSnapshot ? turnViewSnapshot(this.store.getTurnSnapshot(anchor.turnId)) : undefined;
|
|
3088
|
+
const status = includeSnapshot ? snapshot?.status : this.store.getTurnPromptSummary(anchor.turnId)?.status;
|
|
3089
|
+
if (isTurnStillRunning(status)
|
|
3039
3090
|
|| (source.lastTurnId === anchor.turnId && source.lastTurnStatus === "running")) {
|
|
3040
3091
|
throw new Error("话题对应的轮次仍在执行,App Server 暂时不能从这一轮 fork。请等待该轮完成后再在话题中发送消息。");
|
|
3041
3092
|
}
|
|
@@ -3734,6 +3785,10 @@ export class ProxySessionController {
|
|
|
3734
3785
|
while (true) {
|
|
3735
3786
|
let pendingTurnId: string | undefined;
|
|
3736
3787
|
try {
|
|
3788
|
+
this.outbound.updateSessionModel(
|
|
3789
|
+
loaded.record.localSessionId,
|
|
3790
|
+
loaded.runtime.getSession(loaded.record.localSessionId)?.model ?? loaded.session.model,
|
|
3791
|
+
);
|
|
3737
3792
|
pendingTurnId = await this.outbound.startPendingTurn(
|
|
3738
3793
|
loaded.record.localSessionId,
|
|
3739
3794
|
loaded.record.contextKey,
|
|
@@ -3828,6 +3883,7 @@ export class ProxySessionController {
|
|
|
3828
3883
|
replyTarget?: MessageReplyTarget,
|
|
3829
3884
|
requestedTitle?: string,
|
|
3830
3885
|
executionSettings: SessionExecutionSettings = {},
|
|
3886
|
+
activate = true,
|
|
3831
3887
|
): Promise<SessionRecord> {
|
|
3832
3888
|
const agent = this.ensureAgent(agentName);
|
|
3833
3889
|
const resolvedExecutionSettings = mergeExecutionSettings(agent.defaults ?? {}, executionSettings);
|
|
@@ -3847,7 +3903,7 @@ export class ProxySessionController {
|
|
|
3847
3903
|
permissionMode: resolvedExecutionSettings.permissionMode,
|
|
3848
3904
|
});
|
|
3849
3905
|
}
|
|
3850
|
-
this.store.setCurrentSession(contextKey, localSessionId);
|
|
3906
|
+
if (activate) this.store.setCurrentSession(contextKey, localSessionId);
|
|
3851
3907
|
this.outbound.registerSession(
|
|
3852
3908
|
localSessionId,
|
|
3853
3909
|
contextKey,
|
|
@@ -4000,6 +4056,105 @@ export class ProxySessionController {
|
|
|
4000
4056
|
return { group, task };
|
|
4001
4057
|
}
|
|
4002
4058
|
|
|
4059
|
+
private async cloneSession(
|
|
4060
|
+
sourceContextKey: string,
|
|
4061
|
+
source: SessionRecord,
|
|
4062
|
+
requestedTitle: string | undefined,
|
|
4063
|
+
requestedAgentName: string | undefined,
|
|
4064
|
+
newGroup: boolean,
|
|
4065
|
+
userId?: string,
|
|
4066
|
+
replyTarget?: MessageReplyTarget,
|
|
4067
|
+
throughTurnId?: string,
|
|
4068
|
+
): Promise<ControlTaskCloneResult> {
|
|
4069
|
+
const context = this.store.getOrCreateUserContext(sourceContextKey, this.config.defaults.agent!);
|
|
4070
|
+
const agentName = requestedAgentName?.trim() || context.defaultAgent;
|
|
4071
|
+
this.ensureAgent(agentName);
|
|
4072
|
+
if (newGroup && !userId?.startsWith("ou_")) {
|
|
4073
|
+
throw new Error("/clonegroup 只能由具有 open_id 的飞书用户触发。");
|
|
4074
|
+
}
|
|
4075
|
+
await this.outbound.sendText(sourceContextKey, `正在导出对话并创建 ${this.agentLabel(agentName)} 克隆任务,请稍后。`);
|
|
4076
|
+
const exported = await exportConversation(
|
|
4077
|
+
path.join(path.dirname(this.config.storage.sqlitePath), "context-transfers"),
|
|
4078
|
+
this.readCloneConversation(source, throughTurnId),
|
|
4079
|
+
);
|
|
4080
|
+
const title = normalizeTaskTitle(requestedTitle) ?? `${source.title ?? "任务"}(克隆)`;
|
|
4081
|
+
const cwd = detectProjectlessWorkspace(source.cwd) ? undefined : source.cwd;
|
|
4082
|
+
const group = newGroup ? await this.createFeishuGroupContext(
|
|
4083
|
+
sourceContextKey, agentName, title, userId, cwd, "/clonegroup",
|
|
4084
|
+
) : undefined;
|
|
4085
|
+
const contextKey = group?.contextKey ?? sourceContextKey;
|
|
4086
|
+
let task: SessionRecord;
|
|
4087
|
+
try {
|
|
4088
|
+
task = await this.createSession(
|
|
4089
|
+
contextKey, agentName, cwd, false, false, undefined, undefined, title,
|
|
4090
|
+
{ permissionMode: source.permissionMode ?? "auto" }, false,
|
|
4091
|
+
);
|
|
4092
|
+
} catch (error) {
|
|
4093
|
+
await this.outbound.sendText(contextKey,
|
|
4094
|
+
`${group ? "群已创建,但" : ""}克隆任务创建失败;上下文文件已保留:${exported.filePath}`);
|
|
4095
|
+
throw error;
|
|
4096
|
+
}
|
|
4097
|
+
this.store.audit(contextKey, "session_cloned", {
|
|
4098
|
+
sourceLocalSessionId: source.localSessionId,
|
|
4099
|
+
clonedLocalSessionId: task.localSessionId,
|
|
4100
|
+
contextFile: exported.filePath,
|
|
4101
|
+
turnCount: exported.turnCount,
|
|
4102
|
+
});
|
|
4103
|
+
try {
|
|
4104
|
+
await this.startTurn(
|
|
4105
|
+
await this.loadSession(task), conversationImportPrompt(exported.filePath),
|
|
4106
|
+
group ? undefined : replyTarget, undefined,
|
|
4107
|
+
{ displayPrompt: `读取历史上下文(${exported.turnCount} 轮对话)` },
|
|
4108
|
+
);
|
|
4109
|
+
} catch (error) {
|
|
4110
|
+
await this.outbound.sendText(contextKey,
|
|
4111
|
+
`克隆任务已创建,但上下文读取启动失败。任务 ID:${task.localSessionId};上下文文件:${exported.filePath}`);
|
|
4112
|
+
throw error;
|
|
4113
|
+
}
|
|
4114
|
+
this.store.setCurrentSession(contextKey, task.localSessionId);
|
|
4115
|
+
await this.outbound.sendText(sourceContextKey, group
|
|
4116
|
+
? `已创建克隆群:${group.name},正在读取 ${exported.turnCount} 轮历史对话。`
|
|
4117
|
+
: `已切换到 ${this.agentLabel(agentName)} 克隆任务:${title},正在读取 ${exported.turnCount} 轮历史对话。`);
|
|
4118
|
+
return {
|
|
4119
|
+
sourceLocalSessionId: source.localSessionId,
|
|
4120
|
+
contextFile: exported.filePath,
|
|
4121
|
+
turnCount: exported.turnCount,
|
|
4122
|
+
task: this.store.getSession(task.localSessionId) ?? task,
|
|
4123
|
+
...(group ? { group } : {}),
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
private async *readCloneConversation(source: SessionRecord, throughTurnId?: string): AsyncIterable<ConversationTurn> {
|
|
4128
|
+
const runtime = this.runtimes.forAgent(source.agentName);
|
|
4129
|
+
if (!source.remoteSessionId || !runtime.readConversation) {
|
|
4130
|
+
yield* this.store.readConversation(source.localSessionId, throughTurnId);
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
let localSessionId: string | undefined = source.localSessionId;
|
|
4134
|
+
let remoteSessionId = source.remoteSessionId;
|
|
4135
|
+
const visited = new Set<string>();
|
|
4136
|
+
while (!visited.has(remoteSessionId)) {
|
|
4137
|
+
visited.add(remoteSessionId);
|
|
4138
|
+
const fork: ForkHistorySourceRecord | undefined = localSessionId
|
|
4139
|
+
? this.store.getForkHistorySource(localSessionId) : undefined;
|
|
4140
|
+
if (fork && runtime.readRemoteForkSource) {
|
|
4141
|
+
const remote = await runtime.readRemoteForkSource(remoteSessionId);
|
|
4142
|
+
const completed = remote.lastCompletedTurnId
|
|
4143
|
+
?? (remote.lastTurnStatus === "completed" ? remote.lastTurnId : undefined);
|
|
4144
|
+
if (!completed) {
|
|
4145
|
+
if (!fork.sourceRemoteSessionId) throw new Error("找不到分支的原始任务,无法导出继承的上下文。");
|
|
4146
|
+
throughTurnId ??= fork.sourceTurnId;
|
|
4147
|
+
localSessionId = fork.sourceLocalSessionId;
|
|
4148
|
+
remoteSessionId = fork.sourceRemoteSessionId;
|
|
4149
|
+
continue;
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
yield* runtime.readConversation(remoteSessionId, throughTurnId);
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
throw new Error("分支来源包含循环,无法导出上下文。");
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4003
4158
|
private async forkCurrentSessionToFeishuGroup(
|
|
4004
4159
|
sourceContextKey: string,
|
|
4005
4160
|
requestedTitle: string | undefined,
|
|
@@ -4098,7 +4253,7 @@ export class ProxySessionController {
|
|
|
4098
4253
|
taskTitle: string,
|
|
4099
4254
|
userId: string | undefined,
|
|
4100
4255
|
boundProjectCwd: string | undefined,
|
|
4101
|
-
commandName: "/newgroup" | "/forkgroup",
|
|
4256
|
+
commandName: "/newgroup" | "/forkgroup" | "/clonegroup",
|
|
4102
4257
|
): Promise<CreatedFeishuGroupContext> {
|
|
4103
4258
|
if (!userId?.startsWith("ou_")) {
|
|
4104
4259
|
throw new Error(`${commandName} 只能由具有 open_id 的飞书用户消息触发。`);
|
|
@@ -5187,7 +5342,9 @@ export class ProxySessionController {
|
|
|
5187
5342
|
throw error;
|
|
5188
5343
|
}
|
|
5189
5344
|
};
|
|
5190
|
-
|
|
5345
|
+
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5346
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model);
|
|
5347
|
+
return session;
|
|
5191
5348
|
}
|
|
5192
5349
|
|
|
5193
5350
|
private async assertProviderModelSettings(
|
|
@@ -5249,6 +5406,7 @@ export class ProxySessionController {
|
|
|
5249
5406
|
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
|
|
5250
5407
|
}
|
|
5251
5408
|
this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
|
|
5409
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, model);
|
|
5252
5410
|
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
5253
5411
|
const effortMessage = nextEffort && nextEffort !== currentEffort
|
|
5254
5412
|
? `,思考强度已自动调整为 ${nextEffort}`
|
|
@@ -8318,7 +8476,7 @@ function latestRemoteTimestamp(...values: Array<number | undefined>): number | u
|
|
|
8318
8476
|
return timestamps.length > 0 ? Math.max(...timestamps) : undefined;
|
|
8319
8477
|
}
|
|
8320
8478
|
|
|
8321
|
-
function isTurnStillRunning(status?:
|
|
8479
|
+
function isTurnStillRunning(status?: string): boolean {
|
|
8322
8480
|
return status === "starting"
|
|
8323
8481
|
|| status === "running"
|
|
8324
8482
|
|| status === "tool_running"
|
package/src/runtime/types.ts
CHANGED
|
@@ -124,6 +124,11 @@ export interface RemoteCompletedTurnSummary {
|
|
|
124
124
|
completedAt?: number;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
export interface ConversationTurn {
|
|
128
|
+
turnId: string;
|
|
129
|
+
messages: Array<{ role: "user" | "assistant"; text: string }>;
|
|
130
|
+
}
|
|
131
|
+
|
|
127
132
|
export interface RemoteTurnPage {
|
|
128
133
|
turns: RemoteCompletedTurnSummary[];
|
|
129
134
|
nextCursor?: string;
|
|
@@ -256,6 +261,7 @@ export interface AgentRuntime {
|
|
|
256
261
|
readRemoteSessionMetrics?(remoteSessionId: string): Promise<RemoteSessionMetrics>;
|
|
257
262
|
readRemoteForkSource?(remoteSessionId: string): Promise<RemoteSessionSummary>;
|
|
258
263
|
listRemoteTurnSummaries?(remoteSessionId: string, input: { cursor?: string; limit: number }): Promise<RemoteTurnPage>;
|
|
264
|
+
readConversation?(remoteSessionId: string, throughTurnId?: string): AsyncIterable<ConversationTurn>;
|
|
259
265
|
inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
|
|
260
266
|
synchronizeSession(sessionId: string): Promise<RuntimeSession>;
|
|
261
267
|
startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string>;
|
package/src/state/StateStore.ts
CHANGED
|
@@ -2,6 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
import { migrations } from "./migrations.js";
|
|
5
|
+
import type { UpdateCheckSchedule, UpdateNotice } from "../updates/types.js";
|
|
6
|
+
import type { ConversationTurn } from "../runtime/types.js";
|
|
5
7
|
|
|
6
8
|
export type SessionStatus = "starting" | "ready" | "running" | "closed" | "failed";
|
|
7
9
|
|
|
@@ -287,6 +289,36 @@ export class StateStore {
|
|
|
287
289
|
`);
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
getUpdateCheckSchedule(): UpdateCheckSchedule | undefined {
|
|
293
|
+
const row = this.db.prepare("SELECT value_json FROM update_check_schedule WHERE id = 1")
|
|
294
|
+
.get() as { value_json: string } | undefined;
|
|
295
|
+
return row ? JSON.parse(row.value_json) as UpdateCheckSchedule : undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
saveUpdateCheckSchedule(schedule: UpdateCheckSchedule): void {
|
|
299
|
+
this.db.prepare(`INSERT INTO update_check_schedule (id, value_json) VALUES (1, ?)
|
|
300
|
+
ON CONFLICT(id) DO UPDATE SET value_json = excluded.value_json`).run(JSON.stringify(schedule));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
getUpdateNotice(version: string): UpdateNotice | undefined {
|
|
304
|
+
const row = this.db.prepare("SELECT value_json FROM update_notices WHERE version = ?")
|
|
305
|
+
.get(version) as { value_json: string } | undefined;
|
|
306
|
+
return row ? JSON.parse(row.value_json) as UpdateNotice : undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
saveUpdateNotice(notice: UpdateNotice): void {
|
|
310
|
+
this.db.prepare(`INSERT INTO update_notices (version, status, value_json) VALUES (?, ?, ?)
|
|
311
|
+
ON CONFLICT(version) DO UPDATE SET status = excluded.status, value_json = excluded.value_json`)
|
|
312
|
+
.run(notice.version, notice.status, JSON.stringify(notice));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
listPendingUpdateNotices(): UpdateNotice[] {
|
|
316
|
+
const rows = this.db.prepare(`SELECT value_json FROM update_notices
|
|
317
|
+
WHERE status IN ('announcing', 'countdown', 'preparing', 'scheduled')`)
|
|
318
|
+
.all() as Array<{ value_json: string }>;
|
|
319
|
+
return rows.map((row) => JSON.parse(row.value_json) as UpdateNotice);
|
|
320
|
+
}
|
|
321
|
+
|
|
290
322
|
saveGoalCardDelivery(localSessionId: string, contextKey: string, messageId: string): void {
|
|
291
323
|
const updatedAt = new Date().toISOString();
|
|
292
324
|
this.db.prepare(`
|
|
@@ -1047,6 +1079,43 @@ export class StateStore {
|
|
|
1047
1079
|
return row ? JSON.parse(row.snapshot_json) : undefined;
|
|
1048
1080
|
}
|
|
1049
1081
|
|
|
1082
|
+
*readConversation(localSessionId: string, throughTurnId?: string): Iterable<ConversationTurn> {
|
|
1083
|
+
const index = this.listTaskTurnGraphIndex(localSessionId);
|
|
1084
|
+
const byId = new Map(index.map((turn) => [turn.turnId, turn]));
|
|
1085
|
+
const current = this.getSession(localSessionId)?.lastTurnId;
|
|
1086
|
+
const parent = current ? this.getTurnParent(current, localSessionId) : undefined;
|
|
1087
|
+
const head = throughTurnId ?? (current && byId.has(current) ? current : undefined)
|
|
1088
|
+
?? (parent && byId.has(parent) ? parent : undefined)
|
|
1089
|
+
?? this.findLatestCompletedTurnId(localSessionId) ?? index[0]?.turnId;
|
|
1090
|
+
const branch: string[] = [];
|
|
1091
|
+
const seen = new Set<string>();
|
|
1092
|
+
let turnId: string | undefined = head;
|
|
1093
|
+
while (turnId && !seen.has(turnId)) {
|
|
1094
|
+
seen.add(turnId);
|
|
1095
|
+
const turn = byId.get(turnId);
|
|
1096
|
+
if (!turn) throw new Error("上下文历史不完整,无法导出当前分支。");
|
|
1097
|
+
branch.push(turnId);
|
|
1098
|
+
turnId = turn.parentTurnId;
|
|
1099
|
+
}
|
|
1100
|
+
if (turnId) throw new Error("上下文历史包含循环,无法导出当前分支。");
|
|
1101
|
+
const read = this.db.prepare(`SELECT
|
|
1102
|
+
json_extract(snapshot_json, '$.prompt') AS prompt,
|
|
1103
|
+
json_extract(snapshot_json, '$.finalResponse') AS answer,
|
|
1104
|
+
(SELECT json_group_array(json_extract(value, '$.text'))
|
|
1105
|
+
FROM json_each(snapshot_json, '$.activities')
|
|
1106
|
+
WHERE json_extract(value, '$.kind') = 'user') AS followups
|
|
1107
|
+
FROM turn_snapshots WHERE turn_id = ?`);
|
|
1108
|
+
for (const id of branch.reverse()) {
|
|
1109
|
+
const row = read.get(id) as { prompt: string | null; answer: string | null; followups: string };
|
|
1110
|
+
const messages: ConversationTurn["messages"] = [];
|
|
1111
|
+
for (const text of [row.prompt, ...(JSON.parse(row.followups) as string[])]) {
|
|
1112
|
+
if (text?.trim()) messages.push({ role: "user", text });
|
|
1113
|
+
}
|
|
1114
|
+
if (row.answer?.trim()) messages.push({ role: "assistant", text: row.answer });
|
|
1115
|
+
yield { turnId: id, messages };
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1050
1119
|
getTurnPromptSummary(turnId: string): TurnPromptSummary | undefined {
|
|
1051
1120
|
const row = this.db.prepare(`SELECT
|
|
1052
1121
|
json_extract(snapshot_json, '$.status') AS status,
|
package/src/state/migrations.ts
CHANGED
|
@@ -195,4 +195,16 @@ export const migrations = [
|
|
|
195
195
|
CREATE INDEX IF NOT EXISTS idx_card_action_bindings_created
|
|
196
196
|
ON card_action_bindings(created_at);
|
|
197
197
|
`,
|
|
198
|
+
`
|
|
199
|
+
CREATE TABLE IF NOT EXISTS update_check_schedule (
|
|
200
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
201
|
+
value_json TEXT NOT NULL
|
|
202
|
+
);
|
|
203
|
+
CREATE TABLE IF NOT EXISTS update_notices (
|
|
204
|
+
version TEXT PRIMARY KEY,
|
|
205
|
+
status TEXT NOT NULL,
|
|
206
|
+
value_json TEXT NOT NULL
|
|
207
|
+
);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_update_notices_status ON update_notices(status);
|
|
209
|
+
`,
|
|
198
210
|
] as const;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFile, type ExecFileOptionsWithStringEncoding } from "node:child_process";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { loadConfig } from "../config/loadConfig.js";
|
|
6
|
+
import { controlEndpoint } from "../cli/controlProtocol.js";
|
|
7
|
+
import { finalizeSelfUpdatePlan, prepareSelfUpdate, releaseSelfUpdatePlan, requireNpmSelfUpdateInstallation } from "../cli/SelfUpdater.js";
|
|
8
|
+
import { stableVersionSchema } from "./PublishedRelease.js";
|
|
9
|
+
|
|
10
|
+
const resultSchema = z.discriminatedUnion("status", [
|
|
11
|
+
z.object({ status: z.literal("current") }),
|
|
12
|
+
z.object({ status: z.literal("prepared"), planPath: z.string().min(1), targetVersion: stableVersionSchema }),
|
|
13
|
+
]);
|
|
14
|
+
const runFile = promisify(execFile);
|
|
15
|
+
type RunPreparation = (file: string, args: string[], options: ExecFileOptionsWithStringEncoding) => Promise<{ stdout: string }>;
|
|
16
|
+
interface UpdateProfile { home: string; configPath: string }
|
|
17
|
+
|
|
18
|
+
export async function checkAutomaticUpdateSupport(profile: UpdateProfile, run: RunPreparation = runFile): Promise<void> {
|
|
19
|
+
z.object({ status: z.literal("supported") }).parse(await runPreparer("--check", profile, run));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function prepareAutomaticUpdate(
|
|
23
|
+
version: string,
|
|
24
|
+
profile: UpdateProfile,
|
|
25
|
+
run: RunPreparation = runFile,
|
|
26
|
+
): Promise<z.infer<typeof resultSchema>> {
|
|
27
|
+
stableVersionSchema.parse(version);
|
|
28
|
+
const result = resultSchema.parse(await runPreparer(version, profile, run));
|
|
29
|
+
if (result.status === "prepared" && result.targetVersion !== version) {
|
|
30
|
+
throw new Error("The prepared update does not match the announced version.");
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runPreparer(argument: string, profile: UpdateProfile, run: RunPreparation): Promise<unknown> {
|
|
36
|
+
const environment: NodeJS.ProcessEnv = { ...process.env, AGENT_BOT_HOME: profile.home, AGENT_BOT_CONFIG: profile.configPath };
|
|
37
|
+
delete environment.AGENT_BOT;
|
|
38
|
+
// npm preparation is synchronous and can take minutes; keep it off the live worker's event loop.
|
|
39
|
+
const { stdout } = await run(process.execPath, [fileURLToPath(import.meta.url), argument], {
|
|
40
|
+
env: environment, windowsHide: true, encoding: "utf8", maxBuffer: 1_000_000,
|
|
41
|
+
});
|
|
42
|
+
return JSON.parse(String(stdout)) as unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main(): Promise<void> {
|
|
46
|
+
if (process.argv[2] === "--check") {
|
|
47
|
+
requireNpmSelfUpdateInstallation();
|
|
48
|
+
process.stdout.write(JSON.stringify({ status: "supported" }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const version = stableVersionSchema.parse(process.argv[2]);
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
const prepared = await prepareSelfUpdate({ version, channel: "latest" });
|
|
54
|
+
if (prepared.status === "prepared") {
|
|
55
|
+
try {
|
|
56
|
+
finalizeSelfUpdatePlan(prepared.planPath, {
|
|
57
|
+
controlEndpoint: controlEndpoint(config.storage.sqlitePath),
|
|
58
|
+
databasePath: config.storage.sqlitePath,
|
|
59
|
+
restartService: true,
|
|
60
|
+
workingDirectory: process.cwd(),
|
|
61
|
+
reason: `Agent Bot 自动更新到 ${version}`,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
releaseSelfUpdatePlan(prepared.planPath);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write(JSON.stringify(prepared));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
72
|
+
void main().catch((error: unknown) => {
|
|
73
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
});
|
|
76
|
+
}
|