@keyou007/agent-bot 0.1.13-alpha.0 → 0.1.13-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -2
- package/CHANGELOG.md +9 -0
- package/README.md +10 -4
- package/README.zh.md +10 -4
- package/dist/cli/ServerStarter.js +17 -2
- package/dist/cli/ServerStarter.js.map +1 -1
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +12 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/FeishuUserOpenIdStore.js +76 -0
- package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
- package/dist/config/loadConfig.js +3 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/feishu/CardRenderer.js +82 -22
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/index.js +103 -23
- package/dist/index.js.map +1 -1
- package/dist/presentation/turnGraph.js +36 -0
- package/dist/presentation/turnGraph.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +270 -47
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +16 -7
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -2
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/state/StateStore.js +169 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +21 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +46 -3
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +65 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/replacementSupervisor.js +27 -2
- package/dist/supervision/replacementSupervisor.js.map +1 -1
- package/dist/supervisor.js +27 -2
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +11 -4
- package/docs/technical-reference.zh.md +11 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +10 -4
- package/src/cli/ServerStarter.ts +25 -2
- package/src/cli/controlProtocol.ts +6 -1
- package/src/cli/help.ts +12 -2
- package/src/cli.ts +57 -0
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/config/FeishuUserOpenIdStore.ts +81 -0
- package/src/config/loadConfig.ts +3 -0
- package/src/feishu/CardRenderer.ts +109 -22
- package/src/index.ts +161 -21
- package/src/presentation/turnGraph.ts +48 -0
- package/src/proxy/ProxySessionController.ts +323 -55
- package/src/startup/StartupNotifier.ts +23 -7
- package/src/startup/startFeishu.ts +3 -2
- package/src/state/StateStore.ts +221 -0
- package/src/state/migrations.ts +21 -0
- package/src/supervision/SafeRestartNotifier.ts +53 -4
- package/src/supervision/SafeRestartScheduler.ts +78 -7
- package/src/supervision/replacementSupervisor.ts +27 -1
- package/src/supervisor.ts +33 -2
|
@@ -17,11 +17,14 @@ import {
|
|
|
17
17
|
CardRenderer,
|
|
18
18
|
type CardSection,
|
|
19
19
|
type ExecutionSettingsTab,
|
|
20
|
+
type ResetHistoryCardEntry,
|
|
20
21
|
type TaskListCardAction,
|
|
22
|
+
type TaskListCardEntry,
|
|
21
23
|
} from "../feishu/CardRenderer.js";
|
|
22
24
|
import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
|
|
23
25
|
import type { OutboundRouter } from "../presentation/OutboundRouter.js";
|
|
24
26
|
import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
|
|
27
|
+
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
25
28
|
import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
|
|
26
29
|
import type {
|
|
27
30
|
AgentRuntime,
|
|
@@ -61,6 +64,7 @@ const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
|
61
64
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
62
65
|
const MESSAGE_CANCELLED_REACTION = "CrossMark";
|
|
63
66
|
const SESSION_PAGE_SIZE = 5;
|
|
67
|
+
const RESET_HISTORY_PAGE_SIZE = 10;
|
|
64
68
|
const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
|
|
65
69
|
|
|
66
70
|
interface AgentRemoteSession {
|
|
@@ -77,7 +81,7 @@ interface AgentRemoteSessionSummary {
|
|
|
77
81
|
interface SessionsCardOptions {
|
|
78
82
|
updateMessageId?: string;
|
|
79
83
|
forceSwitchTaskId?: string;
|
|
80
|
-
|
|
84
|
+
page?: number;
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
interface StatusCardOptions {
|
|
@@ -85,6 +89,12 @@ interface StatusCardOptions {
|
|
|
85
89
|
forceSwitchTaskId?: string;
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
interface ResetHistoryCardOptions {
|
|
93
|
+
expectedSessionId?: string;
|
|
94
|
+
updateMessageId?: string;
|
|
95
|
+
page?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
interface ModelCardOptions {
|
|
89
99
|
sessionId?: string;
|
|
90
100
|
updateMessageId?: string;
|
|
@@ -167,8 +177,9 @@ export interface ControlTaskGroupResult {
|
|
|
167
177
|
|
|
168
178
|
export interface ProxyLifecycle {
|
|
169
179
|
supervised?: boolean;
|
|
170
|
-
restart(contextKey: string, force: boolean): Promise<void>;
|
|
180
|
+
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
171
181
|
cancelSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
182
|
+
rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
export type ShellCommandExecutor = (command: string, cwd: string) => Promise<ShellCommandResult>;
|
|
@@ -181,6 +192,7 @@ export class ProxySessionController {
|
|
|
181
192
|
private readonly queuedPromptStarts = new Map<string, Promise<void>>();
|
|
182
193
|
private readonly queuedPromptCards = new Map<string, Map<string, string>>();
|
|
183
194
|
private readonly queuedPromptCardWrites = new Map<string, Promise<void>>();
|
|
195
|
+
private readonly sessionResets = new Map<string, Promise<void>>();
|
|
184
196
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
185
197
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
186
198
|
private readonly unsubscribe: Array<() => void> = [];
|
|
@@ -240,8 +252,20 @@ export class ProxySessionController {
|
|
|
240
252
|
"Failed to acknowledge the incoming Feishu message with a reaction.",
|
|
241
253
|
);
|
|
242
254
|
}
|
|
255
|
+
if (message.chatType === "p2p" && message.userId?.startsWith("ou_")) {
|
|
256
|
+
try {
|
|
257
|
+
await this.lifecycle?.rememberFeishuUserOpenId?.(message.userId);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
this.logger.warn(
|
|
260
|
+
{ error, messageId: message.messageId },
|
|
261
|
+
"Failed to persist the first private-chat Lark user Open ID.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
243
265
|
if (message.chatId && message.chatType) {
|
|
244
|
-
|
|
266
|
+
const chatContextKey = baseChatContextKey(message.contextKey);
|
|
267
|
+
this.store.recordChatContext(chatContextKey, message.chatType);
|
|
268
|
+
this.store.markChatActive(chatContextKey);
|
|
245
269
|
}
|
|
246
270
|
const replyTarget = message.replyInThread
|
|
247
271
|
? { messageId: message.messageId, replyInThread: true as const }
|
|
@@ -252,9 +276,6 @@ export class ProxySessionController {
|
|
|
252
276
|
text: message.text,
|
|
253
277
|
...(imageCount > 0 ? { imageCount } : {}),
|
|
254
278
|
});
|
|
255
|
-
if (imageCount > 0 && message.chatId && message.chatType) {
|
|
256
|
-
this.store.markChatActive(baseChatContextKey(message.contextKey));
|
|
257
|
-
}
|
|
258
279
|
let localImagePaths: string[] | undefined;
|
|
259
280
|
if (imageCount > 0) {
|
|
260
281
|
try {
|
|
@@ -284,17 +305,6 @@ export class ProxySessionController {
|
|
|
284
305
|
);
|
|
285
306
|
return;
|
|
286
307
|
}
|
|
287
|
-
if (
|
|
288
|
-
message.chatId
|
|
289
|
-
&& message.chatType
|
|
290
|
-
&& (
|
|
291
|
-
command.type === "shell"
|
|
292
|
-
|| (command.type === "prompt" && !message.text.trimStart().startsWith("/"))
|
|
293
|
-
)
|
|
294
|
-
) {
|
|
295
|
-
this.store.markChatActive(baseChatContextKey(message.contextKey));
|
|
296
|
-
}
|
|
297
|
-
|
|
298
308
|
// Operational and read-only commands must remain available even if a prompt operation is slow.
|
|
299
309
|
if (isQueueIndependentCommand(command)) {
|
|
300
310
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
@@ -369,6 +379,26 @@ export class ProxySessionController {
|
|
|
369
379
|
} else if (kind === "turn_cancel") {
|
|
370
380
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
371
381
|
await this.cancelSession(this.requireSession(contextKey, sessionId));
|
|
382
|
+
} else if (kind === "turn_reset") {
|
|
383
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
384
|
+
await this.resetCurrentSessionToTurn(
|
|
385
|
+
contextKey,
|
|
386
|
+
sessionId,
|
|
387
|
+
String(scopedAction.value.turnId ?? ""),
|
|
388
|
+
);
|
|
389
|
+
if (scopedAction.value.cardView === "reset_history") {
|
|
390
|
+
await this.openResetHistory(contextKey, {
|
|
391
|
+
expectedSessionId: sessionId,
|
|
392
|
+
updateMessageId: requiredCardMessageId(scopedAction.messageId),
|
|
393
|
+
page: resetHistoryPageValue(scopedAction.value.page),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
} else if (kind === "turn_reset_page") {
|
|
397
|
+
await this.openResetHistory(contextKey, {
|
|
398
|
+
expectedSessionId: String(scopedAction.value.sessionId ?? ""),
|
|
399
|
+
updateMessageId: requiredCardMessageId(scopedAction.messageId),
|
|
400
|
+
page: resetHistoryPageValue(scopedAction.value.page),
|
|
401
|
+
});
|
|
372
402
|
} else if (kind === "queued_prompt_cancel") {
|
|
373
403
|
await this.cancelQueuedPrompt(scopedAction);
|
|
374
404
|
} else if (kind === "safe_restart_cancel") {
|
|
@@ -383,17 +413,17 @@ export class ProxySessionController {
|
|
|
383
413
|
if (!cancelled) {
|
|
384
414
|
await this.outbound.sendText(contextKey, "该安全重启计划已失效,请查看最新状态卡片。");
|
|
385
415
|
}
|
|
386
|
-
} else if (kind === "
|
|
387
|
-
await this.refreshSessionsCardFromAction(scopedAction
|
|
416
|
+
} else if (kind === "session_page") {
|
|
417
|
+
await this.refreshSessionsCardFromAction(scopedAction);
|
|
388
418
|
} else if (kind === "session_switch") {
|
|
389
419
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
390
420
|
await this.switchSession(contextKey, sessionId);
|
|
391
421
|
if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction);
|
|
392
|
-
else await this.refreshSessionsCardFromAction(scopedAction);
|
|
422
|
+
else await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
|
|
393
423
|
} else if (kind === "session_fork") {
|
|
394
424
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
395
425
|
await this.forkSessionReference(contextKey, sessionId);
|
|
396
|
-
await this.refreshSessionsCardFromAction(scopedAction);
|
|
426
|
+
await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
|
|
397
427
|
} else if (kind === "session_fork_group") {
|
|
398
428
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
399
429
|
await this.forkSessionReferenceToFeishuGroup(contextKey, sessionId, scopedAction.userId);
|
|
@@ -401,7 +431,7 @@ export class ProxySessionController {
|
|
|
401
431
|
} else if (kind === "session_new") {
|
|
402
432
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
403
433
|
await this.createProjectSessionFromReference(contextKey, sessionId);
|
|
404
|
-
await this.refreshSessionsCardFromAction(scopedAction);
|
|
434
|
+
await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
|
|
405
435
|
} else if (kind === "session_new_group") {
|
|
406
436
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
407
437
|
await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
|
|
@@ -410,7 +440,7 @@ export class ProxySessionController {
|
|
|
410
440
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
411
441
|
await this.stopSessionReference(contextKey, sessionId);
|
|
412
442
|
if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction, sessionId);
|
|
413
|
-
else await this.refreshSessionsCardFromAction(scopedAction, sessionId);
|
|
443
|
+
else await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
|
|
414
444
|
} else if (kind === "session_status") {
|
|
415
445
|
await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
|
|
416
446
|
} else if (kind === "session_status_refresh") {
|
|
@@ -541,6 +571,7 @@ export class ProxySessionController {
|
|
|
541
571
|
this.lastSessionListings.clear();
|
|
542
572
|
this.queuedPromptCards.clear();
|
|
543
573
|
this.queuedPromptCardWrites.clear();
|
|
574
|
+
this.sessionResets.clear();
|
|
544
575
|
}
|
|
545
576
|
|
|
546
577
|
async controlStopTask(localSessionId: string): Promise<string> {
|
|
@@ -831,7 +862,10 @@ export class ProxySessionController {
|
|
|
831
862
|
return;
|
|
832
863
|
case "restart":
|
|
833
864
|
if (!this.lifecycle) throw new Error("当前运行方式不支持自动重启。");
|
|
834
|
-
await this.lifecycle.restart(contextKey, command.force === true);
|
|
865
|
+
await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
|
|
866
|
+
return;
|
|
867
|
+
case "turns":
|
|
868
|
+
await this.openResetHistory(contextKey);
|
|
835
869
|
return;
|
|
836
870
|
case "model":
|
|
837
871
|
await this.openExecutionSettings(contextKey, "model");
|
|
@@ -2063,6 +2097,24 @@ export class ProxySessionController {
|
|
|
2063
2097
|
}
|
|
2064
2098
|
|
|
2065
2099
|
private async handleRuntimeEvent(event: RuntimeEvent): Promise<void> {
|
|
2100
|
+
if ("turnId" in event) {
|
|
2101
|
+
const source = this.store.getSession(event.sessionId);
|
|
2102
|
+
if (source?.remoteSessionId && this.isCodexSession(source)) {
|
|
2103
|
+
this.store.saveTurnRuntimeOrigin(
|
|
2104
|
+
event.turnId,
|
|
2105
|
+
source.localSessionId,
|
|
2106
|
+
source.agentName,
|
|
2107
|
+
source.remoteSessionId,
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
if (event.type === "turn_started") {
|
|
2111
|
+
this.store.saveTurnParent(
|
|
2112
|
+
event.turnId,
|
|
2113
|
+
event.sessionId,
|
|
2114
|
+
source?.lastTurnStatus === "completed" ? source.lastTurnId : undefined,
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2066
2118
|
if (event.type === "session_metadata_updated") {
|
|
2067
2119
|
this.store.updateRuntimeSession(event.sessionId, { title: event.title });
|
|
2068
2120
|
this.outbound.updateSessionTitle(event.sessionId, event.title);
|
|
@@ -2271,6 +2323,111 @@ export class ProxySessionController {
|
|
|
2271
2323
|
await loaded.runtime.cancelTurn(record.localSessionId, turnId);
|
|
2272
2324
|
}
|
|
2273
2325
|
|
|
2326
|
+
private async resetCurrentSessionToTurn(
|
|
2327
|
+
contextKey: string,
|
|
2328
|
+
sessionId: string,
|
|
2329
|
+
turnId: string,
|
|
2330
|
+
): Promise<void> {
|
|
2331
|
+
if (!sessionId || !turnId) throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
|
|
2332
|
+
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
2333
|
+
const reset = previous.catch(() => undefined).then(() =>
|
|
2334
|
+
this.performCurrentSessionReset(contextKey, sessionId, turnId));
|
|
2335
|
+
this.sessionResets.set(sessionId, reset);
|
|
2336
|
+
try {
|
|
2337
|
+
await reset;
|
|
2338
|
+
} finally {
|
|
2339
|
+
if (this.sessionResets.get(sessionId) === reset) this.sessionResets.delete(sessionId);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
private async performCurrentSessionReset(
|
|
2344
|
+
contextKey: string,
|
|
2345
|
+
sessionId: string,
|
|
2346
|
+
turnId: string,
|
|
2347
|
+
): Promise<void> {
|
|
2348
|
+
const current = this.requireCurrentSession(contextKey);
|
|
2349
|
+
if (current.localSessionId !== sessionId) {
|
|
2350
|
+
throw new Error("这张思考卡片不属于当前任务。请先切换回对应任务,再点击 Reset。");
|
|
2351
|
+
}
|
|
2352
|
+
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
2353
|
+
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
2357
|
+
if (!snapshot || snapshot.sessionId !== sessionId || snapshot.status !== "completed") {
|
|
2358
|
+
throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
|
|
2359
|
+
}
|
|
2360
|
+
if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
|
|
2361
|
+
await this.outbound.sendText(contextKey, "当前任务已经处于本轮完成后的对话状态,无需 Reset。");
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
const runtime = this.runtimes.forAgent(current.agentName);
|
|
2366
|
+
if (runtime.kind !== "codex" || !runtime.forkSession) {
|
|
2367
|
+
throw new Error("当前 App Server Agent 不支持 Reset。");
|
|
2368
|
+
}
|
|
2369
|
+
const loaded = await this.loadSession(current);
|
|
2370
|
+
const activity = await this.assertSessionTurnOwnership(current, runtime);
|
|
2371
|
+
const active = activity?.active
|
|
2372
|
+
?? Boolean(loaded.session.activeTurnId || current.status === "running" || current.lastTurnStatus === "running");
|
|
2373
|
+
if (active) {
|
|
2374
|
+
throw new Error("当前任务仍在执行。请等待任务完成或先停止任务,再点击 Reset。");
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
const origin = this.store.getTurnRuntimeOrigin(turnId);
|
|
2378
|
+
if (origin && (origin.localSessionId !== sessionId || origin.agentName !== current.agentName)) {
|
|
2379
|
+
throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
|
|
2380
|
+
}
|
|
2381
|
+
const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
|
|
2382
|
+
const previousRemoteSessionId = current.remoteSessionId;
|
|
2383
|
+
const forked = await runtime.forkSession({
|
|
2384
|
+
localSessionId: current.localSessionId,
|
|
2385
|
+
remoteSessionId: sourceRemoteSessionId,
|
|
2386
|
+
lastTurnId: turnId,
|
|
2387
|
+
agentName: current.agentName,
|
|
2388
|
+
cwd: current.cwd,
|
|
2389
|
+
title: current.title,
|
|
2390
|
+
modelProvider: current.modelProvider,
|
|
2391
|
+
model: current.model,
|
|
2392
|
+
reasoningEffort: current.reasoningEffort,
|
|
2393
|
+
permissionMode: current.permissionMode ?? "auto",
|
|
2394
|
+
});
|
|
2395
|
+
this.persistRuntimeSession(current, forked, "ready");
|
|
2396
|
+
this.store.updateRuntimeSession(current.localSessionId, {
|
|
2397
|
+
lastTurnId: turnId,
|
|
2398
|
+
lastTurnStatus: "completed",
|
|
2399
|
+
});
|
|
2400
|
+
this.outbound.registerSession(
|
|
2401
|
+
current.localSessionId,
|
|
2402
|
+
contextKey,
|
|
2403
|
+
forked.title ?? current.title,
|
|
2404
|
+
current.cwd,
|
|
2405
|
+
this.agentLabel(current.agentName),
|
|
2406
|
+
);
|
|
2407
|
+
this.store.audit(contextKey, "session_reset_to_turn", {
|
|
2408
|
+
localSessionId: current.localSessionId,
|
|
2409
|
+
previousRemoteSessionId,
|
|
2410
|
+
sourceRemoteSessionId,
|
|
2411
|
+
resetTurnId: turnId,
|
|
2412
|
+
forkedRemoteSessionId: forked.remoteSessionId,
|
|
2413
|
+
});
|
|
2414
|
+
const targetSummary = truncateText(
|
|
2415
|
+
(snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(),
|
|
2416
|
+
100,
|
|
2417
|
+
) || "未记录对话内容";
|
|
2418
|
+
const targetTime = snapshot.completedAt ?? snapshot.startedAt;
|
|
2419
|
+
await this.outbound.sendText(
|
|
2420
|
+
contextKey,
|
|
2421
|
+
[
|
|
2422
|
+
"已将当前任务重置到:",
|
|
2423
|
+
targetSummary,
|
|
2424
|
+
`完成时间:${targetTime === undefined ? "未知" : formatResetTurnTime(targetTime)}`,
|
|
2425
|
+
`Turn ID:${turnId}`,
|
|
2426
|
+
"后续对话将从该轮完成后的状态继续;本地文件没有回退。",
|
|
2427
|
+
].join("\n"),
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2274
2431
|
private async resolveApproval(action: CardAction): Promise<void> {
|
|
2275
2432
|
const sessionId = String(action.value.sessionId ?? "");
|
|
2276
2433
|
const requestId = String(action.value.requestId ?? "");
|
|
@@ -2824,6 +2981,92 @@ export class ProxySessionController {
|
|
|
2824
2981
|
await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
|
|
2825
2982
|
}
|
|
2826
2983
|
|
|
2984
|
+
private async openResetHistory(
|
|
2985
|
+
contextKey: string,
|
|
2986
|
+
options: ResetHistoryCardOptions = {},
|
|
2987
|
+
): Promise<void> {
|
|
2988
|
+
const current = this.requireCurrentSession(contextKey);
|
|
2989
|
+
if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
|
|
2990
|
+
throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
|
|
2991
|
+
}
|
|
2992
|
+
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
2993
|
+
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
2994
|
+
}
|
|
2995
|
+
const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
|
|
2996
|
+
const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
|
|
2997
|
+
turnId: turn.turnId,
|
|
2998
|
+
parentTurnId: turn.parentTurnId,
|
|
2999
|
+
})));
|
|
3000
|
+
const total = allTurns.length;
|
|
3001
|
+
const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
|
|
3002
|
+
const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
|
|
3003
|
+
const offset = page * RESET_HISTORY_PAGE_SIZE;
|
|
3004
|
+
const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
|
|
3005
|
+
const entries: ResetHistoryCardEntry[] = turns.flatMap((turn, index) => {
|
|
3006
|
+
const snapshot = turnViewSnapshot(turn.snapshot);
|
|
3007
|
+
if (!snapshot) return [];
|
|
3008
|
+
const graph = graphRows[offset + index]!;
|
|
3009
|
+
const summary = truncateText(
|
|
3010
|
+
(snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(),
|
|
3011
|
+
100,
|
|
3012
|
+
);
|
|
3013
|
+
const isCurrent = current.lastTurnId === turn.turnId && current.lastTurnStatus === "completed";
|
|
3014
|
+
return [{
|
|
3015
|
+
sequence: graph.sequence,
|
|
3016
|
+
graphNodeLine: graph.nodeLine,
|
|
3017
|
+
graphConnectorLine: graph.connectorLine,
|
|
3018
|
+
lines: [
|
|
3019
|
+
`**${cardText(summary || "未记录对话内容")}**`,
|
|
3020
|
+
`${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardCode(turn.turnId)}`,
|
|
3021
|
+
],
|
|
3022
|
+
current: isCurrent,
|
|
3023
|
+
actions: isCurrent ? undefined : [{
|
|
3024
|
+
text: "Reset",
|
|
3025
|
+
value: {
|
|
3026
|
+
action: "turn_reset",
|
|
3027
|
+
cardView: "reset_history",
|
|
3028
|
+
sessionId: current.localSessionId,
|
|
3029
|
+
turnId: turn.turnId,
|
|
3030
|
+
contextKey,
|
|
3031
|
+
page: String(page),
|
|
3032
|
+
},
|
|
3033
|
+
}],
|
|
3034
|
+
}];
|
|
3035
|
+
});
|
|
3036
|
+
const pageActions: TaskListCardAction[] = [
|
|
3037
|
+
...(page > 0 ? [{
|
|
3038
|
+
text: "Previous",
|
|
3039
|
+
value: {
|
|
3040
|
+
action: "turn_reset_page",
|
|
3041
|
+
sessionId: current.localSessionId,
|
|
3042
|
+
contextKey,
|
|
3043
|
+
page: String(page - 1),
|
|
3044
|
+
},
|
|
3045
|
+
}] : []),
|
|
3046
|
+
...(page < totalPages - 1 ? [{
|
|
3047
|
+
text: "Next",
|
|
3048
|
+
value: {
|
|
3049
|
+
action: "turn_reset_page",
|
|
3050
|
+
sessionId: current.localSessionId,
|
|
3051
|
+
contextKey,
|
|
3052
|
+
page: String(page + 1),
|
|
3053
|
+
},
|
|
3054
|
+
}] : []),
|
|
3055
|
+
];
|
|
3056
|
+
const card = this.cardRenderer.renderResetHistoryCard({
|
|
3057
|
+
entries,
|
|
3058
|
+
footerLines: [
|
|
3059
|
+
`第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
|
|
3060
|
+
],
|
|
3061
|
+
pageActions,
|
|
3062
|
+
});
|
|
3063
|
+
if (options.updateMessageId) {
|
|
3064
|
+
await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
3065
|
+
} else {
|
|
3066
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
|
|
2827
3070
|
private async listSessions(
|
|
2828
3071
|
contextKey: string,
|
|
2829
3072
|
searchTerm?: string,
|
|
@@ -2831,7 +3074,8 @@ export class ProxySessionController {
|
|
|
2831
3074
|
): Promise<void> {
|
|
2832
3075
|
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
|
|
2833
3076
|
const normalizedSearch = searchTerm?.trim().toLowerCase();
|
|
2834
|
-
const
|
|
3077
|
+
const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
|
|
3078
|
+
const fetchLimit = (requestedPage + 1) * SESSION_PAGE_SIZE;
|
|
2835
3079
|
const localSessions = this.store.listSessions(contextKey).filter((session) =>
|
|
2836
3080
|
session.status !== "closed" && this.isCodexSession(session),
|
|
2837
3081
|
);
|
|
@@ -2841,9 +3085,9 @@ export class ProxySessionController {
|
|
|
2841
3085
|
await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
|
|
2842
3086
|
if (!runtime.listRemoteSessions) return;
|
|
2843
3087
|
try {
|
|
2844
|
-
const
|
|
2845
|
-
remoteSessions.push(...
|
|
2846
|
-
remoteHasMore ||= Boolean(
|
|
3088
|
+
const result = await runtime.listRemoteSessions({ searchTerm, limit: fetchLimit });
|
|
3089
|
+
remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
|
|
3090
|
+
remoteHasMore ||= Boolean(result.nextCursor);
|
|
2847
3091
|
} catch (error) {
|
|
2848
3092
|
this.logger.warn({ error, contextKey, agentName }, "Failed to list App Server sessions for an Agent.");
|
|
2849
3093
|
remoteErrors.push(`${agentName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -2861,9 +3105,13 @@ export class ProxySessionController {
|
|
|
2861
3105
|
|| Number(right.active) - Number(left.active)
|
|
2862
3106
|
|| right.updatedAt - left.updatedAt);
|
|
2863
3107
|
const activeCount = entries.filter((entry) => entry.active).length;
|
|
2864
|
-
const
|
|
2865
|
-
const
|
|
2866
|
-
|
|
3108
|
+
const lastKnownPage = Math.max(0, Math.ceil(entries.length / SESSION_PAGE_SIZE) - 1);
|
|
3109
|
+
const page = remoteHasMore ? requestedPage : Math.min(requestedPage, lastKnownPage);
|
|
3110
|
+
const offset = page * SESSION_PAGE_SIZE;
|
|
3111
|
+
const visibleEntries = entries.slice(offset, offset + SESSION_PAGE_SIZE);
|
|
3112
|
+
const hasPrevious = page > 0;
|
|
3113
|
+
const hasNext = remoteHasMore || entries.length > offset + SESSION_PAGE_SIZE;
|
|
3114
|
+
this.lastSessionListings.set(contextKey, entries.map((entry) => entry.reference));
|
|
2867
3115
|
const cardEntries = visibleEntries.map((entry, index) => {
|
|
2868
3116
|
const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
|
|
2869
3117
|
const showStop = entry.status === "外部执行中"
|
|
@@ -2876,7 +3124,7 @@ export class ProxySessionController {
|
|
|
2876
3124
|
action: showStop ? "session_stop" : "session_switch",
|
|
2877
3125
|
sessionId: entry.reference,
|
|
2878
3126
|
...(searchTerm ? { searchTerm } : {}),
|
|
2879
|
-
|
|
3127
|
+
page: String(page),
|
|
2880
3128
|
contextKey,
|
|
2881
3129
|
},
|
|
2882
3130
|
}];
|
|
@@ -2886,7 +3134,7 @@ export class ProxySessionController {
|
|
|
2886
3134
|
action: "session_new",
|
|
2887
3135
|
sessionId: entry.reference,
|
|
2888
3136
|
...(searchTerm ? { searchTerm } : {}),
|
|
2889
|
-
|
|
3137
|
+
page: String(page),
|
|
2890
3138
|
contextKey,
|
|
2891
3139
|
},
|
|
2892
3140
|
});
|
|
@@ -2896,7 +3144,7 @@ export class ProxySessionController {
|
|
|
2896
3144
|
action: "session_new_group",
|
|
2897
3145
|
sessionId: entry.reference,
|
|
2898
3146
|
...(searchTerm ? { searchTerm } : {}),
|
|
2899
|
-
|
|
3147
|
+
page: String(page),
|
|
2900
3148
|
contextKey,
|
|
2901
3149
|
},
|
|
2902
3150
|
});
|
|
@@ -2906,7 +3154,7 @@ export class ProxySessionController {
|
|
|
2906
3154
|
action: "session_fork",
|
|
2907
3155
|
sessionId: entry.reference,
|
|
2908
3156
|
...(searchTerm ? { searchTerm } : {}),
|
|
2909
|
-
|
|
3157
|
+
page: String(page),
|
|
2910
3158
|
contextKey,
|
|
2911
3159
|
},
|
|
2912
3160
|
});
|
|
@@ -2916,7 +3164,7 @@ export class ProxySessionController {
|
|
|
2916
3164
|
action: "session_fork_group",
|
|
2917
3165
|
sessionId: entry.reference,
|
|
2918
3166
|
...(searchTerm ? { searchTerm } : {}),
|
|
2919
|
-
|
|
3167
|
+
page: String(page),
|
|
2920
3168
|
contextKey,
|
|
2921
3169
|
},
|
|
2922
3170
|
});
|
|
@@ -2930,7 +3178,7 @@ export class ProxySessionController {
|
|
|
2930
3178
|
});
|
|
2931
3179
|
return {
|
|
2932
3180
|
lines: [
|
|
2933
|
-
`**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
|
|
3181
|
+
`**${offset + index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
|
|
2934
3182
|
`${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
|
|
2935
3183
|
],
|
|
2936
3184
|
actions,
|
|
@@ -2942,22 +3190,33 @@ export class ProxySessionController {
|
|
|
2942
3190
|
cardEntries,
|
|
2943
3191
|
[
|
|
2944
3192
|
...(remoteHint ? [remoteHint] : []),
|
|
3193
|
+
`第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
|
|
2945
3194
|
"点击 **New** 在对应任务的项目中创建新任务;点击 **Switch** 快速切换;点击 **Fork** 从任务最新已完成轮次创建分支;外部正在运行的任务显示 **Stop**,点击后发送 Interrupt 并变为 **Switch**。",
|
|
2946
3195
|
"点击 **NewGroup** 在对应任务的项目中创建新群和新任务;点击 **ForkGroup** 从对应任务最新已完成轮次创建分支群。",
|
|
2947
3196
|
"也可发送 **/switch [序号或任务 ID]**;不带参数切回上一个任务。外部正在执行的回合不会被接管。",
|
|
2948
3197
|
"发送 **/fork [序号或任务 ID]**,可从当前或指定任务创建分支;任务运行中时使用最近已完成轮次。",
|
|
2949
3198
|
"点击 **Status**,或发送 **/status [序号或任务 ID]**,查看当前或指定任务状态。",
|
|
2950
3199
|
],
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3200
|
+
[
|
|
3201
|
+
...(hasPrevious ? [{
|
|
3202
|
+
text: "Previous",
|
|
3203
|
+
value: {
|
|
3204
|
+
action: "session_page",
|
|
3205
|
+
...(searchTerm ? { searchTerm } : {}),
|
|
3206
|
+
page: String(page - 1),
|
|
3207
|
+
contextKey,
|
|
3208
|
+
},
|
|
3209
|
+
}] : []),
|
|
3210
|
+
...(hasNext ? [{
|
|
3211
|
+
text: "Next",
|
|
3212
|
+
value: {
|
|
3213
|
+
action: "session_page",
|
|
3214
|
+
...(searchTerm ? { searchTerm } : {}),
|
|
3215
|
+
page: String(page + 1),
|
|
3216
|
+
contextKey,
|
|
3217
|
+
},
|
|
3218
|
+
}] : []),
|
|
3219
|
+
],
|
|
2961
3220
|
);
|
|
2962
3221
|
if (options.updateMessageId) {
|
|
2963
3222
|
await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
@@ -2968,18 +3227,16 @@ export class ProxySessionController {
|
|
|
2968
3227
|
|
|
2969
3228
|
private async refreshSessionsCardFromAction(
|
|
2970
3229
|
action: CardAction,
|
|
2971
|
-
forceSwitchTaskId?: string,
|
|
2972
|
-
loadMore = false,
|
|
3230
|
+
options: { forceSwitchTaskId?: string; page?: number } = {},
|
|
2973
3231
|
): Promise<void> {
|
|
2974
3232
|
if (!action.messageId) return;
|
|
2975
3233
|
const searchTerm = typeof action.value.searchTerm === "string" && action.value.searchTerm.trim()
|
|
2976
3234
|
? action.value.searchTerm
|
|
2977
3235
|
: undefined;
|
|
2978
|
-
const currentVisibleCount = parseSessionVisibleCount(action.value.visibleCount);
|
|
2979
3236
|
await this.listSessions(action.contextKey, searchTerm, {
|
|
2980
3237
|
updateMessageId: action.messageId,
|
|
2981
|
-
forceSwitchTaskId,
|
|
2982
|
-
|
|
3238
|
+
forceSwitchTaskId: options.forceSwitchTaskId,
|
|
3239
|
+
page: options.page ?? parseSessionPage(action.value.page),
|
|
2983
3240
|
});
|
|
2984
3241
|
}
|
|
2985
3242
|
|
|
@@ -3418,6 +3675,7 @@ export class ProxySessionController {
|
|
|
3418
3675
|
"**/newgroup [title] [--dir <cwd> | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
|
|
3419
3676
|
"**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
|
|
3420
3677
|
"**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
|
|
3678
|
+
"**/turns** 浏览当前任务的历史对话轮次,并可将对话上下文恢复到所选轮次",
|
|
3421
3679
|
"**/title <新标题>** 修改当前任务的标题",
|
|
3422
3680
|
"**/sessions [关键词]** 查找本机任务",
|
|
3423
3681
|
"**/switch [序号或任务 ID]** 不填参数切回上一个任务",
|
|
@@ -3825,6 +4083,10 @@ function formatStatusTime(value: string): string {
|
|
|
3825
4083
|
}).format(date);
|
|
3826
4084
|
}
|
|
3827
4085
|
|
|
4086
|
+
function formatResetTurnTime(value?: number): string {
|
|
4087
|
+
return value === undefined ? "完成时间未知" : formatStatusTime(new Date(value).toISOString());
|
|
4088
|
+
}
|
|
4089
|
+
|
|
3828
4090
|
function cardText(value: string): string {
|
|
3829
4091
|
return value.replaceAll("&", "&").replaceAll("`", "`").replaceAll("<", "<").replaceAll(">", ">");
|
|
3830
4092
|
}
|
|
@@ -3843,9 +4105,15 @@ function executionSettingsTabValue(value: unknown): ExecutionSettingsTab {
|
|
|
3843
4105
|
throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
|
|
3844
4106
|
}
|
|
3845
4107
|
|
|
4108
|
+
function resetHistoryPageValue(value: unknown): number {
|
|
4109
|
+
const page = Number(value);
|
|
4110
|
+
if (!Number.isSafeInteger(page) || page < 0) throw new Error("历史轮次卡片页码无效,请重新发送 /turns。");
|
|
4111
|
+
return page;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
3846
4114
|
function requiredCardMessageId(value: string | undefined): string {
|
|
3847
4115
|
if (value) return value;
|
|
3848
|
-
throw new Error("
|
|
4116
|
+
throw new Error("无法更新卡片,请重新发送对应命令。");
|
|
3849
4117
|
}
|
|
3850
4118
|
|
|
3851
4119
|
function remoteSessionStatusLabel(status: RemoteSessionSummary["status"]): string {
|
|
@@ -3947,9 +4215,9 @@ function isQueueIndependentCommand(command: Command): boolean {
|
|
|
3947
4215
|
return false;
|
|
3948
4216
|
}
|
|
3949
4217
|
|
|
3950
|
-
function
|
|
4218
|
+
function parseSessionPage(value: unknown): number {
|
|
3951
4219
|
const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
|
|
3952
|
-
return Number.isSafeInteger(parsed) && parsed >=
|
|
4220
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
3953
4221
|
}
|
|
3954
4222
|
|
|
3955
4223
|
function isPromptCommand(command: Command): boolean {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
2
|
import { detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
3
3
|
import { CardRenderer } from "../feishu/CardRenderer.js";
|
|
4
|
+
import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
|
|
4
5
|
import type { FeishuOutbound } from "../feishu/types.js";
|
|
5
6
|
import type { SessionRecord, StateStore, UserContextRecord } from "../state/StateStore.js";
|
|
6
7
|
|
|
@@ -32,15 +33,30 @@ export class StartupNotifier {
|
|
|
32
33
|
private readonly metadataHydrator?: StartupTaskMetadataHydrator,
|
|
33
34
|
) {}
|
|
34
35
|
|
|
35
|
-
async notify(
|
|
36
|
+
async notify(
|
|
37
|
+
startedAt: Date,
|
|
38
|
+
restartReason: string,
|
|
39
|
+
restartGroupContextKeys: string[] = [],
|
|
40
|
+
): Promise<void> {
|
|
36
41
|
let targets: StartupNotificationTarget[];
|
|
37
42
|
try {
|
|
38
|
-
const activeGroupSince = new Date(startedAt.getTime() -
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
43
|
+
const activeGroupSince = new Date(startedAt.getTime() - 60_000);
|
|
44
|
+
const knownGroups = new Map(
|
|
45
|
+
this.store.listChatContexts("group")
|
|
46
|
+
.filter((chat) => !isThreadContextKey(chat.contextKey))
|
|
47
|
+
.map((chat) => [chat.contextKey, chat]),
|
|
48
|
+
);
|
|
49
|
+
const targetGroups = new Map(
|
|
50
|
+
this.store.listRecentlyActiveChatContexts(activeGroupSince)
|
|
51
|
+
.filter((chat) => chat.chatType === "group" && !isThreadContextKey(chat.contextKey))
|
|
52
|
+
.map((chat) => [chat.contextKey, chat]),
|
|
53
|
+
);
|
|
54
|
+
for (const contextKey of restartGroupContextKeys) {
|
|
55
|
+
const baseContextKey = baseChatContextKey(contextKey.trim());
|
|
56
|
+
const group = knownGroups.get(baseContextKey);
|
|
57
|
+
if (group) targetGroups.set(baseContextKey, group);
|
|
58
|
+
}
|
|
59
|
+
const chats = [...this.store.listChatContexts("p2p"), ...targetGroups.values()];
|
|
44
60
|
targets = chats.map((chat) => ({
|
|
45
61
|
contextKey: chat.contextKey,
|
|
46
62
|
context: this.store.getUserContext(chat.contextKey),
|