@keyou007/agent-bot 0.1.14-alpha.1 → 0.1.14-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -1
- package/README.md +42 -12
- package/README.zh.md +42 -12
- package/config.example.yaml +1 -1
- package/dist/cli/AutostartManager.js +431 -0
- package/dist/cli/AutostartManager.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +17 -13
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/Initializer.js +35 -0
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js +552 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -0
- package/dist/cli/SelfUpdater.js +431 -0
- package/dist/cli/SelfUpdater.js.map +1 -0
- package/dist/cli/autostartOutput.js +63 -0
- package/dist/cli/autostartOutput.js.map +1 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +92 -18
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/taskGroupOptions.js +14 -11
- package/dist/cli/taskGroupOptions.js.map +1 -1
- package/dist/cli/taskListOutput.js +3 -0
- package/dist/cli/taskListOutput.js.map +1 -1
- package/dist/cli/taskTarget.js +99 -0
- package/dist/cli/taskTarget.js.map +1 -0
- package/dist/cli.js +643 -58
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +8 -2
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +7 -1
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +172 -23
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +25 -1
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/index.js +134 -2
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +436 -12
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +18 -0
- package/dist/state/StateStore.js.map +1 -1
- package/docs/technical-reference.md +17 -7
- package/docs/technical-reference.zh.md +17 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/skills/agent-bot/SKILL.md +107 -188
- package/src/cli/AutostartManager.ts +579 -0
- package/src/cli/FeishuAppConfiguration.ts +22 -7
- package/src/cli/Initializer.ts +57 -0
- package/src/cli/SelfUpdateRunner.ts +817 -0
- package/src/cli/SelfUpdater.ts +688 -0
- package/src/cli/autostartOutput.ts +75 -0
- package/src/cli/controlProtocol.ts +111 -1
- package/src/cli/help.ts +92 -18
- package/src/cli/taskGroupOptions.ts +27 -17
- package/src/cli/taskListOutput.ts +10 -0
- package/src/cli/taskTarget.ts +168 -0
- package/src/cli.ts +781 -65
- package/src/codex/CodexRuntime.ts +8 -2
- package/src/commands/CommandRouter.ts +6 -1
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +199 -23
- package/src/feishu/FeishuMarkdown.ts +27 -1
- package/src/index.ts +172 -2
- package/src/proxy/ProxySessionController.ts +532 -15
- package/src/runtime/types.ts +1 -0
- package/src/state/StateStore.ts +19 -0
|
@@ -141,6 +141,11 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
141
141
|
requiresArgument: true,
|
|
142
142
|
},
|
|
143
143
|
{ command: "/sessions", usage: "[关键词]", description: "查找本机任务" },
|
|
144
|
+
{
|
|
145
|
+
command: "/archive",
|
|
146
|
+
usage: "[序号或任务 ID]",
|
|
147
|
+
description: "归档当前或指定任务",
|
|
148
|
+
},
|
|
144
149
|
{
|
|
145
150
|
command: "/switch",
|
|
146
151
|
usage: "[序号或任务 ID]",
|
|
@@ -655,6 +660,13 @@ export class ProxySessionController {
|
|
|
655
660
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
656
661
|
await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
|
|
657
662
|
await this.refreshSessionsCardFromAction(scopedAction);
|
|
663
|
+
} else if (kind === "session_archive") {
|
|
664
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
665
|
+
await this.archiveSessionReference(contextKey, sessionId, {
|
|
666
|
+
announce: true,
|
|
667
|
+
source: "sessions_card",
|
|
668
|
+
});
|
|
669
|
+
await this.refreshSessionsCardFromAction(scopedAction);
|
|
658
670
|
} else if (kind === "session_stop") {
|
|
659
671
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
660
672
|
await this.stopSessionReference(contextKey, sessionId);
|
|
@@ -803,6 +815,18 @@ export class ProxySessionController {
|
|
|
803
815
|
return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
|
|
804
816
|
}
|
|
805
817
|
|
|
818
|
+
async controlArchiveTask(localSessionId: string): Promise<{
|
|
819
|
+
localSessionId: string;
|
|
820
|
+
remoteSessionId: string;
|
|
821
|
+
title: string;
|
|
822
|
+
}> {
|
|
823
|
+
const record = this.requireControlSession(localSessionId);
|
|
824
|
+
return this.archiveSessionReference(this.controlSessionContextKey(record), localSessionId, {
|
|
825
|
+
announce: false,
|
|
826
|
+
source: "cli",
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
806
830
|
async controlGetTaskStatus(localSessionId: string): Promise<{
|
|
807
831
|
session: SessionRecord;
|
|
808
832
|
snapshot?: TurnViewState;
|
|
@@ -984,6 +1008,387 @@ export class ProxySessionController {
|
|
|
984
1008
|
};
|
|
985
1009
|
}
|
|
986
1010
|
|
|
1011
|
+
async controlCreateTask(
|
|
1012
|
+
localSessionId: string,
|
|
1013
|
+
requestedTitle?: string,
|
|
1014
|
+
requestedProjectCwd?: string,
|
|
1015
|
+
forceProjectless = false,
|
|
1016
|
+
requestedAgentName?: string,
|
|
1017
|
+
): Promise<SessionRecord> {
|
|
1018
|
+
const source = this.requireControlSession(localSessionId);
|
|
1019
|
+
const contextKey = this.controlSessionContextKey(source);
|
|
1020
|
+
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
|
|
1021
|
+
const agentName = requestedAgentName?.trim() || context.defaultAgent;
|
|
1022
|
+
const agent = this.ensureAgent(agentName);
|
|
1023
|
+
if (forceProjectless && agent.kind !== "app-server") {
|
|
1024
|
+
throw new Error("task new --nodir is only available for App Server agents.");
|
|
1025
|
+
}
|
|
1026
|
+
const replyTarget = this.controlSessionReplyTarget(source);
|
|
1027
|
+
const cwd = forceProjectless
|
|
1028
|
+
? undefined
|
|
1029
|
+
: requestedProjectCwd === undefined
|
|
1030
|
+
? detectProjectlessWorkspace(source.cwd)
|
|
1031
|
+
? agent.kind === "app-server" ? undefined : createProjectlessWorkspace().cwd
|
|
1032
|
+
: source.cwd
|
|
1033
|
+
: resolveUserPath(requestedProjectCwd, source.cwd);
|
|
1034
|
+
const settings: SessionExecutionSettings = source.agentName === agentName
|
|
1035
|
+
? {
|
|
1036
|
+
modelProvider: source.modelProvider,
|
|
1037
|
+
model: source.model,
|
|
1038
|
+
reasoningEffort: source.reasoningEffort,
|
|
1039
|
+
permissionMode: source.permissionMode,
|
|
1040
|
+
}
|
|
1041
|
+
: {};
|
|
1042
|
+
return this.outbound.withReplyTarget(contextKey, replyTarget, () =>
|
|
1043
|
+
this.createSession(
|
|
1044
|
+
contextKey,
|
|
1045
|
+
agentName,
|
|
1046
|
+
cwd,
|
|
1047
|
+
true,
|
|
1048
|
+
false,
|
|
1049
|
+
undefined,
|
|
1050
|
+
undefined,
|
|
1051
|
+
requestedTitle,
|
|
1052
|
+
settings,
|
|
1053
|
+
));
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async controlForkTask(localSessionId: string): Promise<{
|
|
1057
|
+
sourceLocalSessionId: string;
|
|
1058
|
+
sourceTurnId: string;
|
|
1059
|
+
task: SessionRecord;
|
|
1060
|
+
}> {
|
|
1061
|
+
const source = this.requireControlSession(localSessionId);
|
|
1062
|
+
const contextKey = this.controlSessionContextKey(source);
|
|
1063
|
+
const replyTarget = this.controlSessionReplyTarget(source);
|
|
1064
|
+
const plan = await this.prepareForkSession(contextKey, source.localSessionId);
|
|
1065
|
+
const forked = await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
|
|
1066
|
+
const result = await this.forkSessionIntoContext(contextKey, plan);
|
|
1067
|
+
await this.outbound.sendText(
|
|
1068
|
+
contextKey,
|
|
1069
|
+
`已从指定任务${plan.forkedFromHistoricalTurn ? "最近已完成轮次" : ""}创建分支并切换到新任务:${
|
|
1070
|
+
result.session.title
|
|
1071
|
+
? `${result.session.title}(${result.session.remoteSessionId})`
|
|
1072
|
+
: result.session.remoteSessionId
|
|
1073
|
+
}`,
|
|
1074
|
+
);
|
|
1075
|
+
return result;
|
|
1076
|
+
});
|
|
1077
|
+
return {
|
|
1078
|
+
sourceLocalSessionId: source.localSessionId,
|
|
1079
|
+
sourceTurnId: plan.lastTurnId,
|
|
1080
|
+
task: this.store.getSession(forked.record.localSessionId) ?? forked.record,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async controlSwitchTask(
|
|
1085
|
+
localSessionId: string,
|
|
1086
|
+
targetLocalSessionId?: string,
|
|
1087
|
+
previous = false,
|
|
1088
|
+
): Promise<SessionRecord> {
|
|
1089
|
+
const anchor = this.requireControlSession(localSessionId);
|
|
1090
|
+
const contextKey = this.controlSessionContextKey(anchor);
|
|
1091
|
+
const replyTarget = this.controlSessionReplyTarget(anchor);
|
|
1092
|
+
const target = previous ? undefined : targetLocalSessionId ?? anchor.localSessionId;
|
|
1093
|
+
await this.outbound.withReplyTarget(contextKey, replyTarget, () => this.switchSession(contextKey, target));
|
|
1094
|
+
return this.requireCurrentSession(contextKey);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
async controlQueueTaskPrompt(localSessionId: string, text: string): Promise<{ promptId: string; queued: number }> {
|
|
1098
|
+
const promptText = text.trim();
|
|
1099
|
+
if (!promptText) throw new Error("The queued Prompt cannot be empty.");
|
|
1100
|
+
const record = this.requireControlSession(localSessionId);
|
|
1101
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
1102
|
+
const replyTarget = this.controlSessionReplyTarget(record);
|
|
1103
|
+
const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, promptText, { replyTarget });
|
|
1104
|
+
this.store.audit(contextKey, "queued_prompt_added", {
|
|
1105
|
+
promptId: queued.promptId,
|
|
1106
|
+
localSessionId: record.localSessionId,
|
|
1107
|
+
source: "cli",
|
|
1108
|
+
});
|
|
1109
|
+
const queuedCount = this.store.countQueuedPrompts(record.localSessionId);
|
|
1110
|
+
await this.scheduleNextQueuedPrompt(record.localSessionId);
|
|
1111
|
+
return { promptId: queued.promptId, queued: queuedCount };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
controlTaskAgent(localSessionId: string, agentName?: string): {
|
|
1115
|
+
current: string;
|
|
1116
|
+
agents: Array<{ name: string; title: string }>;
|
|
1117
|
+
} {
|
|
1118
|
+
const record = this.requireControlSession(localSessionId);
|
|
1119
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
1120
|
+
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
|
|
1121
|
+
if (agentName) {
|
|
1122
|
+
this.ensureAgent(agentName);
|
|
1123
|
+
this.store.setDefaultAgent(contextKey, agentName);
|
|
1124
|
+
}
|
|
1125
|
+
return {
|
|
1126
|
+
current: agentName ?? context.defaultAgent,
|
|
1127
|
+
agents: Object.entries(this.config.agents).map(([name, agent]) => ({ name, title: agent.title })),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async controlTaskSettings(
|
|
1132
|
+
localSessionId: string,
|
|
1133
|
+
setting?: "provider" | "model" | "thinking" | "permissions",
|
|
1134
|
+
value?: string,
|
|
1135
|
+
): Promise<{
|
|
1136
|
+
session: SessionRecord;
|
|
1137
|
+
providers: Awaited<ReturnType<NonNullable<AgentRuntime["listModelProviders"]>>>;
|
|
1138
|
+
models: Awaited<ReturnType<AgentRuntime["listModels"]>>;
|
|
1139
|
+
reasoningOptions: Awaited<ReturnType<AgentRuntime["listModels"]>>[number]["supportedReasoningEfforts"];
|
|
1140
|
+
permissionModes: PermissionMode[];
|
|
1141
|
+
}> {
|
|
1142
|
+
const record = this.requireControlSession(localSessionId);
|
|
1143
|
+
const loaded = await this.loadSession(record);
|
|
1144
|
+
if (setting && !value?.trim()) throw new Error(`task ${setting} requires a value.`);
|
|
1145
|
+
const nextValue = value?.trim();
|
|
1146
|
+
if (setting === "provider" && nextValue) {
|
|
1147
|
+
await this.assertModelProvider(loaded, nextValue);
|
|
1148
|
+
const models = await loaded.runtime.listModels();
|
|
1149
|
+
const model = models.find((candidate) => candidate.id === loaded.session.model)
|
|
1150
|
+
?? models.find((candidate) => candidate.isDefault);
|
|
1151
|
+
if (!model) throw new Error("The current runtime has no model available for a Provider change.");
|
|
1152
|
+
const effort = model.supportedReasoningEfforts.some(
|
|
1153
|
+
(candidate) => candidate.value === loaded.session.reasoningEffort,
|
|
1154
|
+
)
|
|
1155
|
+
? loaded.session.reasoningEffort
|
|
1156
|
+
: model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
|
|
1157
|
+
if (!effort) throw new Error(`Model ${model.id} has no reasoning effort available for a Provider change.`);
|
|
1158
|
+
if (!loaded.runtime.setExecutionSettings) throw new Error("The current runtime does not support Provider settings.");
|
|
1159
|
+
const updated = await loaded.runtime.setExecutionSettings(record.localSessionId, {
|
|
1160
|
+
modelProvider: nextValue,
|
|
1161
|
+
model: model.id,
|
|
1162
|
+
reasoningEffort: effort,
|
|
1163
|
+
permissionMode: loaded.session.permissionMode,
|
|
1164
|
+
});
|
|
1165
|
+
this.store.updateRuntimeSession(record.localSessionId, {
|
|
1166
|
+
modelProvider: updated.modelProvider ?? nextValue,
|
|
1167
|
+
model: updated.model ?? model.id,
|
|
1168
|
+
reasoningEffort: updated.reasoningEffort ?? effort,
|
|
1169
|
+
permissionMode: updated.permissionMode,
|
|
1170
|
+
});
|
|
1171
|
+
} else if (setting === "model" && nextValue) {
|
|
1172
|
+
const models = await loaded.runtime.listModels();
|
|
1173
|
+
const selected = models.find((candidate) => candidate.id === nextValue);
|
|
1174
|
+
if (!selected) throw new Error(`Unknown model: ${nextValue}`);
|
|
1175
|
+
const currentEffort = loaded.session.reasoningEffort;
|
|
1176
|
+
const nextEffort = currentEffort && selected.supportedReasoningEfforts.some(
|
|
1177
|
+
(candidate) => candidate.value === currentEffort,
|
|
1178
|
+
)
|
|
1179
|
+
? currentEffort
|
|
1180
|
+
: selected.defaultReasoningEffort;
|
|
1181
|
+
await loaded.runtime.setModel(record.localSessionId, nextValue);
|
|
1182
|
+
if (nextEffort && nextEffort !== currentEffort) {
|
|
1183
|
+
await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
|
|
1184
|
+
}
|
|
1185
|
+
this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
|
|
1186
|
+
} else if (setting === "thinking" && nextValue) {
|
|
1187
|
+
const models = await loaded.runtime.listModels();
|
|
1188
|
+
const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
|
|
1189
|
+
?? models.find((candidate) => candidate.isDefault);
|
|
1190
|
+
if (!currentModel) throw new Error("The current runtime has no model with configurable reasoning effort.");
|
|
1191
|
+
if (!currentModel.supportedReasoningEfforts.some((candidate) => candidate.value === nextValue)) {
|
|
1192
|
+
const supported = currentModel.supportedReasoningEfforts.map((candidate) => candidate.value).join(", ") || "none";
|
|
1193
|
+
throw new Error(`Unsupported reasoning effort: ${nextValue}. Supported values: ${supported}.`);
|
|
1194
|
+
}
|
|
1195
|
+
await loaded.runtime.setReasoningEffort(record.localSessionId, nextValue);
|
|
1196
|
+
this.store.updateRuntimeSession(record.localSessionId, { reasoningEffort: nextValue });
|
|
1197
|
+
} else if (setting === "permissions" && nextValue) {
|
|
1198
|
+
if (nextValue !== "auto" && nextValue !== "confirm") {
|
|
1199
|
+
throw new Error("task permissions accepts auto or confirm.");
|
|
1200
|
+
}
|
|
1201
|
+
await loaded.runtime.setPermissionMode(record.localSessionId, nextValue);
|
|
1202
|
+
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
|
|
1203
|
+
}
|
|
1204
|
+
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1205
|
+
const models = await loaded.runtime.listModels();
|
|
1206
|
+
const currentModel = models.find((candidate) => candidate.id === session.model)
|
|
1207
|
+
?? models.find((candidate) => candidate.isDefault);
|
|
1208
|
+
const providers = loaded.runtime.listModelProviders
|
|
1209
|
+
? await loaded.runtime.listModelProviders()
|
|
1210
|
+
: [];
|
|
1211
|
+
return {
|
|
1212
|
+
session,
|
|
1213
|
+
providers,
|
|
1214
|
+
models,
|
|
1215
|
+
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
1216
|
+
permissionModes: ["auto", "confirm"],
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async controlTaskGoal(
|
|
1221
|
+
localSessionId: string,
|
|
1222
|
+
action: "show" | "set" | "edit" | "pause" | "resume" | "clear",
|
|
1223
|
+
objective?: string,
|
|
1224
|
+
): Promise<{ goal?: RuntimeGoal; cleared?: boolean }> {
|
|
1225
|
+
const record = this.requireControlSession(localSessionId);
|
|
1226
|
+
const loaded = await this.loadSession(record);
|
|
1227
|
+
if (loaded.runtime.kind !== "codex" || !loaded.runtime.getGoal || !loaded.runtime.setGoal || !loaded.runtime.clearGoal) {
|
|
1228
|
+
throw new Error("The current Agent does not support Goal mode.");
|
|
1229
|
+
}
|
|
1230
|
+
if (action === "show") return { goal: await loaded.runtime.getGoal(record.localSessionId) };
|
|
1231
|
+
if (action === "clear") return { cleared: await loaded.runtime.clearGoal(record.localSessionId) };
|
|
1232
|
+
const current = await loaded.runtime.getGoal(record.localSessionId);
|
|
1233
|
+
if (action === "pause" || action === "resume") {
|
|
1234
|
+
if (!current) throw new Error("The task has no Goal.");
|
|
1235
|
+
return { goal: await loaded.runtime.setGoal(record.localSessionId, { status: action === "pause" ? "paused" : "active" }) };
|
|
1236
|
+
}
|
|
1237
|
+
const nextObjective = objective?.trim();
|
|
1238
|
+
if (!nextObjective) throw new Error(`task goal ${action} requires an objective.`);
|
|
1239
|
+
validateGoalObjective(nextObjective);
|
|
1240
|
+
if (action === "set" && current && current.status !== "complete") {
|
|
1241
|
+
throw new Error("The task already has an unfinished Goal. Use task goal <task> edit <objective>, or clear it first.");
|
|
1242
|
+
}
|
|
1243
|
+
if (action === "edit" && !current) throw new Error("The task has no Goal to edit.");
|
|
1244
|
+
return {
|
|
1245
|
+
goal: await loaded.runtime.setGoal(record.localSessionId, {
|
|
1246
|
+
objective: nextObjective,
|
|
1247
|
+
status: action === "edit" ? current!.status : "active",
|
|
1248
|
+
...(action === "edit" ? { tokenBudget: current!.tokenBudget } : {}),
|
|
1249
|
+
}),
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
controlListTaskTurns(localSessionId: string): {
|
|
1254
|
+
session: SessionRecord;
|
|
1255
|
+
turns: Array<{
|
|
1256
|
+
sequence: number;
|
|
1257
|
+
turnId: string;
|
|
1258
|
+
parentTurnId?: string;
|
|
1259
|
+
prompt?: string;
|
|
1260
|
+
startedAt?: number;
|
|
1261
|
+
completedAt?: number;
|
|
1262
|
+
current: boolean;
|
|
1263
|
+
}>;
|
|
1264
|
+
} {
|
|
1265
|
+
const session = this.requireControlSession(localSessionId);
|
|
1266
|
+
if (!session.remoteSessionId || !this.isCodexSession(session)) {
|
|
1267
|
+
throw new Error("The task is not an App Server task that can be reset.");
|
|
1268
|
+
}
|
|
1269
|
+
const contextKey = this.controlSessionContextKey(session);
|
|
1270
|
+
const rows = this.store.listCompletedTurnGraph(session.localSessionId, contextKey);
|
|
1271
|
+
const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
|
|
1272
|
+
return {
|
|
1273
|
+
session,
|
|
1274
|
+
turns: rows.flatMap((turn, index) => {
|
|
1275
|
+
const snapshot = turnViewSnapshot(turn.snapshot);
|
|
1276
|
+
if (!snapshot) return [];
|
|
1277
|
+
return [{
|
|
1278
|
+
sequence: graph[index]!.sequence,
|
|
1279
|
+
turnId: turn.turnId,
|
|
1280
|
+
parentTurnId: turn.parentTurnId,
|
|
1281
|
+
prompt: snapshot.prompt ?? snapshot.taskTitle,
|
|
1282
|
+
startedAt: snapshot.startedAt,
|
|
1283
|
+
completedAt: snapshot.completedAt,
|
|
1284
|
+
current: session.lastTurnId === turn.turnId && session.lastTurnStatus === "completed",
|
|
1285
|
+
}];
|
|
1286
|
+
}),
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async controlResetTaskToTurn(localSessionId: string, turnId: string): Promise<SessionRecord> {
|
|
1291
|
+
const record = this.requireControlSession(localSessionId);
|
|
1292
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
1293
|
+
const previous = this.sessionResets.get(record.localSessionId) ?? Promise.resolve();
|
|
1294
|
+
const reset = previous.catch(() => undefined).then(() =>
|
|
1295
|
+
this.performSessionReset(contextKey, record, turnId, false));
|
|
1296
|
+
this.sessionResets.set(record.localSessionId, reset);
|
|
1297
|
+
try {
|
|
1298
|
+
await reset;
|
|
1299
|
+
} finally {
|
|
1300
|
+
if (this.sessionResets.get(record.localSessionId) === reset) this.sessionResets.delete(record.localSessionId);
|
|
1301
|
+
}
|
|
1302
|
+
return this.store.getSession(record.localSessionId) ?? record;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
controlTaskMute(localSessionId: string, enabled?: boolean): { contextKey: string; enabled: boolean } {
|
|
1306
|
+
const record = this.requireControlSession(localSessionId);
|
|
1307
|
+
const contextKey = baseChatContextKey(this.controlSessionContextKey(record));
|
|
1308
|
+
const chat = this.store.getChatContext(contextKey);
|
|
1309
|
+
if (chat?.chatType !== "group") throw new Error("task mute is only available for group chats.");
|
|
1310
|
+
const next = enabled ?? Boolean(chat.requiresMention);
|
|
1311
|
+
if (enabled !== undefined) this.store.setChatRequiresMention(contextKey, enabled);
|
|
1312
|
+
return { contextKey, enabled: next };
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async controlRunTaskShell(localSessionId: string, command: string): Promise<ShellCommandResult & {
|
|
1316
|
+
cwd: string;
|
|
1317
|
+
command: string;
|
|
1318
|
+
}> {
|
|
1319
|
+
const record = this.requireControlSession(localSessionId);
|
|
1320
|
+
const normalized = command.trim();
|
|
1321
|
+
if (!normalized) throw new Error("task shell requires a command.");
|
|
1322
|
+
return { ...await this.shellCommandExecutor(normalized, record.cwd), cwd: record.cwd, command: normalized };
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async controlListTaskDirectory(
|
|
1326
|
+
localSessionId: string,
|
|
1327
|
+
requestedDirectory?: string,
|
|
1328
|
+
requestedPage = 0,
|
|
1329
|
+
): Promise<{
|
|
1330
|
+
directory: string;
|
|
1331
|
+
parentDirectory?: string;
|
|
1332
|
+
page: number;
|
|
1333
|
+
totalPages: number;
|
|
1334
|
+
totalEntries: number;
|
|
1335
|
+
entries: Array<{
|
|
1336
|
+
name: string;
|
|
1337
|
+
path: string;
|
|
1338
|
+
kind: "directory" | "image" | "binary" | "file" | "drive";
|
|
1339
|
+
}>;
|
|
1340
|
+
}> {
|
|
1341
|
+
const record = this.requireControlSession(localSessionId);
|
|
1342
|
+
if (requestedDirectory === WINDOWS_DRIVES_DIRECTORY) {
|
|
1343
|
+
const drives = await this.windowsDriveLister();
|
|
1344
|
+
const totalPages = Math.max(1, Math.ceil(drives.length / DIRECTORY_PAGE_SIZE));
|
|
1345
|
+
const page = Math.min(Math.max(0, Math.trunc(requestedPage)), totalPages - 1);
|
|
1346
|
+
return {
|
|
1347
|
+
directory: WINDOWS_DRIVES_DIRECTORY,
|
|
1348
|
+
page,
|
|
1349
|
+
totalPages,
|
|
1350
|
+
totalEntries: drives.length,
|
|
1351
|
+
entries: drives.slice(page * DIRECTORY_PAGE_SIZE, (page + 1) * DIRECTORY_PAGE_SIZE).map((drive) => ({
|
|
1352
|
+
name: windowsDriveDisplayName(drive),
|
|
1353
|
+
path: drive.root,
|
|
1354
|
+
kind: "drive" as const,
|
|
1355
|
+
})),
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
const directory = requestedDirectory === undefined
|
|
1359
|
+
? path.resolve(record.cwd)
|
|
1360
|
+
: resolveUserPath(requestedDirectory, record.cwd);
|
|
1361
|
+
await this.assertBrowsableDirectory(directory);
|
|
1362
|
+
const entries = (await fs.readdir(directory, { withFileTypes: true }))
|
|
1363
|
+
.map((entry) => ({
|
|
1364
|
+
name: entry.name,
|
|
1365
|
+
path: path.join(directory, entry.name),
|
|
1366
|
+
kind: directoryBrowserEntryKind(entry),
|
|
1367
|
+
}))
|
|
1368
|
+
.sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory")
|
|
1369
|
+
|| left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
|
|
1370
|
+
const totalPages = Math.max(1, Math.ceil(entries.length / DIRECTORY_PAGE_SIZE));
|
|
1371
|
+
const page = Math.min(Math.max(0, Math.trunc(requestedPage)), totalPages - 1);
|
|
1372
|
+
const parentDirectory = isWindowsDriveRoot(directory) ? WINDOWS_DRIVES_DIRECTORY : path.dirname(directory);
|
|
1373
|
+
return {
|
|
1374
|
+
directory,
|
|
1375
|
+
...(parentDirectory !== directory ? { parentDirectory } : {}),
|
|
1376
|
+
page,
|
|
1377
|
+
totalPages,
|
|
1378
|
+
totalEntries: entries.length,
|
|
1379
|
+
entries: entries.slice(page * DIRECTORY_PAGE_SIZE, (page + 1) * DIRECTORY_PAGE_SIZE),
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
async controlSendTaskFile(localSessionId: string, requestedFilePath: string): Promise<string | undefined> {
|
|
1384
|
+
const record = this.requireControlSession(localSessionId);
|
|
1385
|
+
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
1386
|
+
await this.assertSendableFile(filePath);
|
|
1387
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
1388
|
+
const replyTarget = this.controlSessionReplyTarget(record);
|
|
1389
|
+
return this.outbound.withReplyTarget(contextKey, replyTarget, () => this.outbound.sendFile(contextKey, filePath));
|
|
1390
|
+
}
|
|
1391
|
+
|
|
987
1392
|
private cardActionContextKey(action: CardAction): string {
|
|
988
1393
|
const explicit = typeof action.value.contextKey === "string" ? action.value.contextKey : undefined;
|
|
989
1394
|
if (explicit && baseChatContextKey(explicit) === baseChatContextKey(action.contextKey)) return explicit;
|
|
@@ -1079,6 +1484,15 @@ export class ProxySessionController {
|
|
|
1079
1484
|
if (command.agent) await this.setDefaultAgent(contextKey, command.agent);
|
|
1080
1485
|
else await this.openAgentSettings(contextKey);
|
|
1081
1486
|
return;
|
|
1487
|
+
case "archive": {
|
|
1488
|
+
const sessionId = command.sessionId ?? context.currentSessionId;
|
|
1489
|
+
if (!sessionId) throw new Error("当前会话没有可归档的任务。请使用 /sessions 查找任务。");
|
|
1490
|
+
await this.archiveSessionReference(contextKey, sessionId, {
|
|
1491
|
+
announce: true,
|
|
1492
|
+
source: "command",
|
|
1493
|
+
});
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1082
1496
|
case "status":
|
|
1083
1497
|
await this.status(contextKey, command.sessionId);
|
|
1084
1498
|
return;
|
|
@@ -3107,16 +3521,25 @@ export class ProxySessionController {
|
|
|
3107
3521
|
if (current.localSessionId !== sessionId) {
|
|
3108
3522
|
throw new Error("这张思考卡片不属于当前任务。请先切换回对应任务,再点击 Reset。");
|
|
3109
3523
|
}
|
|
3524
|
+
await this.performSessionReset(contextKey, current, turnId, true);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
private async performSessionReset(
|
|
3528
|
+
contextKey: string,
|
|
3529
|
+
current: SessionRecord,
|
|
3530
|
+
turnId: string,
|
|
3531
|
+
announce: boolean,
|
|
3532
|
+
): Promise<void> {
|
|
3110
3533
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
3111
3534
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
3112
3535
|
}
|
|
3113
3536
|
|
|
3114
3537
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
3115
|
-
if (!snapshot || snapshot.sessionId !==
|
|
3538
|
+
if (!snapshot || snapshot.sessionId !== current.localSessionId || snapshot.status !== "completed") {
|
|
3116
3539
|
throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
|
|
3117
3540
|
}
|
|
3118
3541
|
if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
|
|
3119
|
-
await this.outbound.sendText(contextKey, "当前任务已经处于本轮完成后的对话状态,无需 Reset。");
|
|
3542
|
+
if (announce) await this.outbound.sendText(contextKey, "当前任务已经处于本轮完成后的对话状态,无需 Reset。");
|
|
3120
3543
|
return;
|
|
3121
3544
|
}
|
|
3122
3545
|
|
|
@@ -3133,7 +3556,7 @@ export class ProxySessionController {
|
|
|
3133
3556
|
}
|
|
3134
3557
|
|
|
3135
3558
|
const origin = this.store.getTurnRuntimeOrigin(turnId);
|
|
3136
|
-
if (origin && (origin.localSessionId !==
|
|
3559
|
+
if (origin && (origin.localSessionId !== current.localSessionId || origin.agentName !== current.agentName)) {
|
|
3137
3560
|
throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
|
|
3138
3561
|
}
|
|
3139
3562
|
const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
|
|
@@ -3174,16 +3597,18 @@ export class ProxySessionController {
|
|
|
3174
3597
|
100,
|
|
3175
3598
|
) || "未记录对话内容";
|
|
3176
3599
|
const targetTime = snapshot.completedAt ?? snapshot.startedAt;
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3600
|
+
if (announce) {
|
|
3601
|
+
await this.outbound.sendText(
|
|
3602
|
+
contextKey,
|
|
3603
|
+
[
|
|
3604
|
+
"已将当前任务重置到:",
|
|
3605
|
+
targetSummary,
|
|
3606
|
+
`完成时间:${targetTime === undefined ? "未知" : formatResetTurnTime(targetTime)}`,
|
|
3607
|
+
`Turn ID:${turnId}`,
|
|
3608
|
+
"后续对话将从该轮完成后的状态继续;本地文件没有回退。",
|
|
3609
|
+
].join("\n"),
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3187
3612
|
}
|
|
3188
3613
|
|
|
3189
3614
|
private async resolveApproval(action: CardAction): Promise<void> {
|
|
@@ -4112,6 +4537,16 @@ export class ProxySessionController {
|
|
|
4112
4537
|
contextKey,
|
|
4113
4538
|
},
|
|
4114
4539
|
});
|
|
4540
|
+
actions.push({
|
|
4541
|
+
text: "Archive",
|
|
4542
|
+
value: {
|
|
4543
|
+
action: "session_archive",
|
|
4544
|
+
sessionId: entry.reference,
|
|
4545
|
+
...(searchTerm ? { searchTerm } : {}),
|
|
4546
|
+
page: String(page),
|
|
4547
|
+
contextKey,
|
|
4548
|
+
},
|
|
4549
|
+
});
|
|
4115
4550
|
const title = truncateText(entry.title.replace(/\s+/gu, " ").trim() || "未命名任务", 56);
|
|
4116
4551
|
return {
|
|
4117
4552
|
project,
|
|
@@ -4153,7 +4588,7 @@ export class ProxySessionController {
|
|
|
4153
4588
|
`第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
|
|
4154
4589
|
"",
|
|
4155
4590
|
"> 项目行:**New** 新建任务,**NewGroup** 新建群。",
|
|
4156
|
-
"> 任务详情:**Switch** 切换,**Stop** 停止,**Fork** / **ForkGroup** 创建分支,**Status**
|
|
4591
|
+
"> 任务详情:**Switch** 切换,**Stop** 停止,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态,**Archive** 归档。",
|
|
4157
4592
|
],
|
|
4158
4593
|
[
|
|
4159
4594
|
...(hasPrevious ? [{
|
|
@@ -4342,6 +4777,67 @@ export class ProxySessionController {
|
|
|
4342
4777
|
await this.outbound.sendText(contextKey, `已向 Agent 发送 Interrupt 请求:${turnId}`);
|
|
4343
4778
|
}
|
|
4344
4779
|
|
|
4780
|
+
private async archiveSessionReference(
|
|
4781
|
+
contextKey: string,
|
|
4782
|
+
reference: string,
|
|
4783
|
+
options: { announce: boolean; source: "cli" | "command" | "sessions_card" },
|
|
4784
|
+
): Promise<{ localSessionId: string; remoteSessionId: string; title: string }> {
|
|
4785
|
+
const taskId = this.resolveSessionReference(contextKey, reference);
|
|
4786
|
+
const direct = this.store.getSession(taskId);
|
|
4787
|
+
if (direct?.status === "closed") throw new Error(`任务已经归档:${direct.title ?? taskId}`);
|
|
4788
|
+
const local = direct ?? this.findStoredSessionByReference(taskId);
|
|
4789
|
+
if (local && (!local.remoteSessionId || !this.isCodexSession(local))) {
|
|
4790
|
+
throw new Error("当前 Agent 不支持归档任务。");
|
|
4791
|
+
}
|
|
4792
|
+
if (local && (
|
|
4793
|
+
local.status === "running"
|
|
4794
|
+
|| Boolean(this.runtimes.forAgent(local.agentName).getSession(local.localSessionId)?.activeTurnId)
|
|
4795
|
+
)) {
|
|
4796
|
+
throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
|
|
4797
|
+
}
|
|
4798
|
+
if (local && this.store.countQueuedPrompts(local.localSessionId) > 0) {
|
|
4799
|
+
throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
const remoteReference = local
|
|
4803
|
+
? remoteSessionReference(local.agentName, local.remoteSessionId!)
|
|
4804
|
+
: taskId;
|
|
4805
|
+
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference);
|
|
4806
|
+
if (isRemoteSessionActive(remote)) {
|
|
4807
|
+
throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
|
|
4808
|
+
}
|
|
4809
|
+
if (!runtime.archiveRemoteSession) {
|
|
4810
|
+
throw new Error(`Agent ${agentName} 不支持归档任务。`);
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
const wasCurrent = Boolean(local && this.store.getUserContext(contextKey)?.currentSessionId === local.localSessionId);
|
|
4814
|
+
await runtime.archiveRemoteSession(remote.id);
|
|
4815
|
+
const title = local?.title ?? remote.title ?? remote.preview ?? remote.id;
|
|
4816
|
+
if (local) {
|
|
4817
|
+
this.store.archiveSession(local.localSessionId);
|
|
4818
|
+
this.outbound.unregisterSession(local.localSessionId);
|
|
4819
|
+
}
|
|
4820
|
+
this.store.audit(contextKey, "session_archived", {
|
|
4821
|
+
...(local ? { localSessionId: local.localSessionId } : {}),
|
|
4822
|
+
remoteSessionId: remote.id,
|
|
4823
|
+
agentName,
|
|
4824
|
+
source: options.source,
|
|
4825
|
+
});
|
|
4826
|
+
if (options.announce) {
|
|
4827
|
+
await this.outbound.sendText(
|
|
4828
|
+
contextKey,
|
|
4829
|
+
wasCurrent
|
|
4830
|
+
? `已归档任务:${title}\n当前会话已没有绑定任务,直接发送消息即可创建新任务。`
|
|
4831
|
+
: `已归档任务:${title}`,
|
|
4832
|
+
);
|
|
4833
|
+
}
|
|
4834
|
+
return {
|
|
4835
|
+
localSessionId: local?.localSessionId ?? "",
|
|
4836
|
+
remoteSessionId: remote.id,
|
|
4837
|
+
title,
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4345
4841
|
private resolveSessionReference(contextKey: string, reference?: string): string {
|
|
4346
4842
|
if (reference === undefined) {
|
|
4347
4843
|
const previous = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!).previousSessionId;
|
|
@@ -4687,6 +5183,27 @@ export class ProxySessionController {
|
|
|
4687
5183
|
);
|
|
4688
5184
|
}
|
|
4689
5185
|
|
|
5186
|
+
private requireControlSession(localSessionId: string): SessionRecord {
|
|
5187
|
+
const record = this.store.getSession(localSessionId);
|
|
5188
|
+
if (!record || record.status === "closed") throw new Error(`Task not found: ${localSessionId}`);
|
|
5189
|
+
return record;
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
private controlSessionContextKey(record: SessionRecord): string {
|
|
5193
|
+
return this.outbound.getSessionContextKey(record.localSessionId)
|
|
5194
|
+
?? (record.lastTurnId ? this.store.getTurnContextKey(record.lastTurnId) : undefined)
|
|
5195
|
+
?? record.contextKey;
|
|
5196
|
+
}
|
|
5197
|
+
|
|
5198
|
+
private controlSessionReplyTarget(record: SessionRecord): MessageReplyTarget | undefined {
|
|
5199
|
+
const routed = this.outbound.getSessionReplyTarget(record.localSessionId);
|
|
5200
|
+
if (routed) return routed;
|
|
5201
|
+
const snapshot = record.lastTurnId
|
|
5202
|
+
? turnViewSnapshot(this.store.getTurnSnapshot(record.lastTurnId))
|
|
5203
|
+
: undefined;
|
|
5204
|
+
return snapshot?.replyTarget;
|
|
5205
|
+
}
|
|
5206
|
+
|
|
4690
5207
|
private ensureAgent(agentName: string) {
|
|
4691
5208
|
const agent = this.config.agents[agentName];
|
|
4692
5209
|
if (!agent) throw new Error(`未知 agent:${agentName}`);
|
|
@@ -5246,7 +5763,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
|
|
|
5246
5763
|
}
|
|
5247
5764
|
|
|
5248
5765
|
function isQueueIndependentCommand(command: Command): boolean {
|
|
5249
|
-
if (["stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type)) return true;
|
|
5766
|
+
if (["archive", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type)) return true;
|
|
5250
5767
|
if (command.type === "agent") return command.agent === undefined;
|
|
5251
5768
|
if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
|
|
5252
5769
|
return false;
|
package/src/runtime/types.ts
CHANGED
|
@@ -221,6 +221,7 @@ export interface AgentRuntime {
|
|
|
221
221
|
steerTurn(sessionId: string, turnId: string, prompt: RuntimePrompt): Promise<void>;
|
|
222
222
|
cancelTurn(sessionId: string, turnId: string): Promise<void>;
|
|
223
223
|
interruptRemoteTurn?(remoteSessionId: string, turnId: string): Promise<void>;
|
|
224
|
+
archiveRemoteSession?(remoteSessionId: string): Promise<void>;
|
|
224
225
|
closeSession(sessionId: string): Promise<void>;
|
|
225
226
|
setTitle?(sessionId: string, title: string): Promise<void>;
|
|
226
227
|
getGoal?(sessionId: string): Promise<RuntimeGoal | undefined>;
|
package/src/state/StateStore.ts
CHANGED
|
@@ -743,6 +743,25 @@ export class StateStore {
|
|
|
743
743
|
);
|
|
744
744
|
}
|
|
745
745
|
|
|
746
|
+
archiveSession(localSessionId: string): void {
|
|
747
|
+
const now = new Date().toISOString();
|
|
748
|
+
const archive = this.db.transaction(() => {
|
|
749
|
+
this.db.prepare(`
|
|
750
|
+
UPDATE sessions
|
|
751
|
+
SET status = 'closed', updated_at = ?
|
|
752
|
+
WHERE local_session_id = ?
|
|
753
|
+
`).run(now, localSessionId);
|
|
754
|
+
this.db.prepare(`
|
|
755
|
+
UPDATE user_contexts
|
|
756
|
+
SET current_session_id = CASE WHEN current_session_id = ? THEN NULL ELSE current_session_id END,
|
|
757
|
+
previous_session_id = CASE WHEN previous_session_id = ? THEN NULL ELSE previous_session_id END,
|
|
758
|
+
updated_at = ?
|
|
759
|
+
WHERE current_session_id = ? OR previous_session_id = ?
|
|
760
|
+
`).run(localSessionId, localSessionId, now, localSessionId, localSessionId);
|
|
761
|
+
});
|
|
762
|
+
archive();
|
|
763
|
+
}
|
|
764
|
+
|
|
746
765
|
updateRuntimeSession(
|
|
747
766
|
localSessionId: string,
|
|
748
767
|
patch: Partial<
|