@keyou007/agent-bot 0.1.14-alpha.2 → 0.1.14-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +7 -4
- package/README.zh.md +7 -4
- package/dist/cli/FeishuAppConfiguration.js +4 -0
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +2 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/CommandRouter.js +9 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +94 -15
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +16 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +42 -6
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +7 -0
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +21 -7
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +102 -2
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +35 -0
- package/dist/state/StateStore.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +2 -0
- package/src/cli/FeishuAppConfiguration.ts +4 -0
- package/src/cli/controlProtocol.ts +8 -0
- package/src/cli/help.ts +2 -0
- package/src/cli.ts +28 -0
- package/src/commands/CommandRouter.ts +8 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +114 -12
- package/src/feishu/FeishuMessageClient.ts +33 -0
- package/src/feishu/FeishuTurnPresenter.ts +43 -6
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +2 -0
- package/src/presentation/OutboundRouter.ts +7 -0
- package/src/presentation/TurnStateReducer.ts +21 -7
- package/src/proxy/ProxySessionController.ts +131 -3
- package/src/state/StateStore.ts +37 -0
|
@@ -42,12 +42,14 @@ export interface TurnPresentationStore {
|
|
|
42
42
|
export interface FeishuTurnPresenterOptions {
|
|
43
43
|
normalIntervalMs?: number;
|
|
44
44
|
criticalGapMs?: number;
|
|
45
|
+
elapsedUpdateIntervalMs?: number;
|
|
45
46
|
finalChunkLength?: number;
|
|
46
47
|
onError?: (error: unknown) => void;
|
|
47
48
|
finalRetryBackoffMs?: number[];
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const MAX_FINAL_TABLES_PER_CARD = 5;
|
|
52
|
+
const DEFAULT_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
|
|
51
53
|
|
|
52
54
|
interface TurnEntry {
|
|
53
55
|
contextKey: string;
|
|
@@ -58,6 +60,7 @@ interface TurnEntry {
|
|
|
58
60
|
reactionId?: string;
|
|
59
61
|
reactionEmoji?: string;
|
|
60
62
|
scheduler?: CardUpdateScheduler<TurnViewState>;
|
|
63
|
+
elapsedTimer?: ReturnType<typeof setInterval>;
|
|
61
64
|
finalizing?: Promise<void>;
|
|
62
65
|
lastFinalizedStatus?: TurnViewState["status"];
|
|
63
66
|
}
|
|
@@ -148,6 +151,7 @@ export class FeishuTurnPresenter {
|
|
|
148
151
|
await entry.initializing;
|
|
149
152
|
return state.turnId;
|
|
150
153
|
} catch (error) {
|
|
154
|
+
this.stopElapsedUpdates(entry);
|
|
151
155
|
if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
|
|
152
156
|
this.entries.delete(state.turnId);
|
|
153
157
|
throw error;
|
|
@@ -158,7 +162,15 @@ export class FeishuTurnPresenter {
|
|
|
158
162
|
const entry = this.pendingEntries.get(sessionId);
|
|
159
163
|
if (!entry) return;
|
|
160
164
|
this.pendingEntries.delete(sessionId);
|
|
161
|
-
|
|
165
|
+
this.stopElapsedUpdates(entry);
|
|
166
|
+
const completedAt = Date.now();
|
|
167
|
+
entry.state = {
|
|
168
|
+
...entry.state,
|
|
169
|
+
status: "failed",
|
|
170
|
+
error: message,
|
|
171
|
+
completedAt,
|
|
172
|
+
durationMs: Math.max(0, completedAt - entry.state.startedAt),
|
|
173
|
+
};
|
|
162
174
|
this.store.saveTurnSnapshot(entry.state.turnId, sessionId, entry.state, entry.contextKey);
|
|
163
175
|
await entry.initializing;
|
|
164
176
|
try {
|
|
@@ -178,24 +190,28 @@ export class FeishuTurnPresenter {
|
|
|
178
190
|
): Promise<void> {
|
|
179
191
|
const snapshot = this.store.getTurnSnapshot(turnId);
|
|
180
192
|
if (!isTurnViewState(snapshot) || snapshot.sessionId !== sessionId || isTerminalViewStatus(snapshot.status)) return;
|
|
193
|
+
const completedAt = Date.now();
|
|
181
194
|
const state: TurnViewState = {
|
|
182
195
|
...snapshot,
|
|
183
196
|
status: "cancelled",
|
|
184
197
|
progressText: message,
|
|
185
198
|
activeTool: undefined,
|
|
186
199
|
approval: undefined,
|
|
187
|
-
completedAt
|
|
200
|
+
completedAt,
|
|
201
|
+
durationMs: Math.max(0, completedAt - snapshot.startedAt),
|
|
188
202
|
};
|
|
189
203
|
this.store.saveTurnSnapshot(turnId, sessionId, state, contextKey);
|
|
190
|
-
const delivery = this.store.getTurnDelivery(turnId);
|
|
191
|
-
if (delivery?.progressMessageId) {
|
|
192
|
-
await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderer.renderTurn(state));
|
|
193
|
-
}
|
|
194
204
|
const entry = this.entries.get(turnId);
|
|
195
205
|
if (entry) {
|
|
206
|
+
entry.state = state;
|
|
207
|
+
this.stopElapsedUpdates(entry);
|
|
196
208
|
this.entries.delete(turnId);
|
|
197
209
|
if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
|
|
198
210
|
}
|
|
211
|
+
const delivery = this.store.getTurnDelivery(turnId);
|
|
212
|
+
if (delivery?.progressMessageId) {
|
|
213
|
+
await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderer.renderTurn(state));
|
|
214
|
+
}
|
|
199
215
|
}
|
|
200
216
|
|
|
201
217
|
async appendSteerMessage(
|
|
@@ -287,6 +303,7 @@ export class FeishuTurnPresenter {
|
|
|
287
303
|
if (!entry.scheduler) return;
|
|
288
304
|
|
|
289
305
|
if (isTerminalEvent(event)) {
|
|
306
|
+
this.stopElapsedUpdates(entry);
|
|
290
307
|
if (entry.finalizing) await entry.finalizing;
|
|
291
308
|
const terminalState = entry.state;
|
|
292
309
|
if (entry.lastFinalizedStatus === terminalState.status) return;
|
|
@@ -434,10 +451,30 @@ export class FeishuTurnPresenter {
|
|
|
434
451
|
onError: this.options.onError,
|
|
435
452
|
});
|
|
436
453
|
entry.scheduler.seed(sentState);
|
|
454
|
+
this.startElapsedUpdates(entry);
|
|
437
455
|
if (entry.state !== sentState) entry.scheduler.update(entry.state, "critical");
|
|
438
456
|
await this.syncThinkingCardReaction(entry);
|
|
439
457
|
}
|
|
440
458
|
|
|
459
|
+
private startElapsedUpdates(entry: TurnEntry): void {
|
|
460
|
+
if (entry.elapsedTimer || isTerminalViewStatus(entry.state.status)) return;
|
|
461
|
+
const intervalMs = Math.max(1, this.options.elapsedUpdateIntervalMs ?? DEFAULT_ELAPSED_UPDATE_INTERVAL_MS);
|
|
462
|
+
entry.elapsedTimer = setInterval(() => {
|
|
463
|
+
if (isTerminalViewStatus(entry.state.status)) {
|
|
464
|
+
this.stopElapsedUpdates(entry);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!entry.historySnapshot) entry.scheduler?.update(entry.state);
|
|
468
|
+
}, intervalMs);
|
|
469
|
+
entry.elapsedTimer.unref?.();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private stopElapsedUpdates(entry: TurnEntry): void {
|
|
473
|
+
if (!entry.elapsedTimer) return;
|
|
474
|
+
clearInterval(entry.elapsedTimer);
|
|
475
|
+
entry.elapsedTimer = undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
441
478
|
private async finalize(entry: TurnEntry, state: TurnViewState): Promise<void> {
|
|
442
479
|
let terminalCardError: unknown;
|
|
443
480
|
if (!entry.historySnapshot) {
|
package/src/feishu/types.ts
CHANGED
|
@@ -50,6 +50,7 @@ export interface ChatUpdatedEvent {
|
|
|
50
50
|
|
|
51
51
|
export interface FeishuOutbound {
|
|
52
52
|
createGroup?(input: CreateGroupInput): Promise<CreatedGroup>;
|
|
53
|
+
deleteGroup?(chatId: string): Promise<void>;
|
|
53
54
|
addReaction?(messageId: string, emojiType: string): Promise<string | undefined>;
|
|
54
55
|
deleteReaction?(messageId: string, reactionId: string): Promise<void>;
|
|
55
56
|
downloadImage?(messageId: string, imageKey: string): Promise<string>;
|
package/src/index.ts
CHANGED
|
@@ -403,6 +403,8 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
403
403
|
return { ok: true, message: await controller.controlStopTask(request.localSessionId) };
|
|
404
404
|
case "task_archive":
|
|
405
405
|
return { ok: true, data: await controller.controlArchiveTask(request.localSessionId) };
|
|
406
|
+
case "task_dismiss":
|
|
407
|
+
return { ok: true, data: await controller.controlDismissTask(request.localSessionId) };
|
|
406
408
|
case "task_title":
|
|
407
409
|
return { ok: true, message: await controller.controlSetTaskTitle(request.localSessionId, request.title) };
|
|
408
410
|
case "task_prompt":
|
|
@@ -172,6 +172,13 @@ export class OutboundRouter {
|
|
|
172
172
|
return createGroup.call(outbound, input);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
async deleteGroup(contextKey: string, chatId: string): Promise<void> {
|
|
176
|
+
const outbound = this.route(contextKey).outbound;
|
|
177
|
+
const deleteGroup = outbound.deleteGroup;
|
|
178
|
+
if (!deleteGroup) throw new Error("当前消息通道不支持解散飞书群。");
|
|
179
|
+
await deleteGroup.call(outbound, chatId);
|
|
180
|
+
}
|
|
181
|
+
|
|
175
182
|
withReplyTarget<T>(
|
|
176
183
|
contextKey: string,
|
|
177
184
|
target: MessageReplyTarget | undefined,
|
|
@@ -102,31 +102,45 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
|
|
|
102
102
|
return { ...state, status: "waiting_for_approval", approval: event.request };
|
|
103
103
|
case "approval_resolved":
|
|
104
104
|
return { ...state, status: state.activeTool ? "tool_running" : "running", approval: undefined };
|
|
105
|
-
case "turn_completed":
|
|
105
|
+
case "turn_completed": {
|
|
106
|
+
const durationMs = Math.max(0, event.durationMs ?? Date.now() - state.startedAt);
|
|
106
107
|
return {
|
|
107
108
|
...state,
|
|
108
109
|
status: "completed",
|
|
109
110
|
activeTool: undefined,
|
|
110
111
|
approval: undefined,
|
|
111
|
-
completedAt: state.startedAt +
|
|
112
|
-
durationMs
|
|
112
|
+
completedAt: state.startedAt + durationMs,
|
|
113
|
+
durationMs,
|
|
113
114
|
finalResponse: appendGeneratedImageMarkdown(
|
|
114
115
|
event.finalResponse,
|
|
115
116
|
state.completedTools.flatMap((tool) =>
|
|
116
117
|
tool.kind === "image_generation" && tool.imagePath ? [tool.imagePath] : []),
|
|
117
118
|
),
|
|
118
119
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
}
|
|
121
|
+
case "turn_cancelled": {
|
|
122
|
+
const completedAt = Date.now();
|
|
123
|
+
return {
|
|
124
|
+
...state,
|
|
125
|
+
status: "cancelled",
|
|
126
|
+
activeTool: undefined,
|
|
127
|
+
approval: undefined,
|
|
128
|
+
completedAt,
|
|
129
|
+
durationMs: Math.max(0, completedAt - state.startedAt),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
case "turn_failed": {
|
|
133
|
+
const completedAt = Date.now();
|
|
122
134
|
return {
|
|
123
135
|
...state,
|
|
124
136
|
status: "failed",
|
|
125
137
|
activeTool: undefined,
|
|
126
138
|
approval: undefined,
|
|
127
|
-
completedAt
|
|
139
|
+
completedAt,
|
|
140
|
+
durationMs: Math.max(0, completedAt - state.startedAt),
|
|
128
141
|
error: bound(event.message),
|
|
129
142
|
};
|
|
143
|
+
}
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
146
|
|
|
@@ -146,6 +146,7 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
146
146
|
usage: "[序号或任务 ID]",
|
|
147
147
|
description: "归档当前或指定任务",
|
|
148
148
|
},
|
|
149
|
+
{ command: "/dismiss", description: "归档当前任务并解散当前群" },
|
|
149
150
|
{
|
|
150
151
|
command: "/switch",
|
|
151
152
|
usage: "[序号或任务 ID]",
|
|
@@ -498,7 +499,7 @@ export class ProxySessionController {
|
|
|
498
499
|
if (isQueueIndependentCommand(command)) {
|
|
499
500
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
500
501
|
try {
|
|
501
|
-
if (command.type !== "forkgroup") await this.ensureThreadFork(message);
|
|
502
|
+
if (command.type !== "forkgroup" && command.type !== "dismiss") await this.ensureThreadFork(message);
|
|
502
503
|
await this.execute(
|
|
503
504
|
message.contextKey,
|
|
504
505
|
command,
|
|
@@ -667,6 +668,20 @@ export class ProxySessionController {
|
|
|
667
668
|
source: "sessions_card",
|
|
668
669
|
});
|
|
669
670
|
await this.refreshSessionsCardFromAction(scopedAction);
|
|
671
|
+
} else if (kind === "group_dismiss_keep") {
|
|
672
|
+
this.assertDismissGroupRequester(scopedAction);
|
|
673
|
+
await this.outbound.updateInteractiveCard(
|
|
674
|
+
contextKey,
|
|
675
|
+
requiredCardMessageId(scopedAction.messageId),
|
|
676
|
+
this.cardRenderer.renderDismissGroupKept(),
|
|
677
|
+
);
|
|
678
|
+
} else if (kind === "group_dismiss_confirm") {
|
|
679
|
+
this.assertDismissGroupRequester(scopedAction);
|
|
680
|
+
await this.dismissGroupAndArchiveTask(
|
|
681
|
+
contextKey,
|
|
682
|
+
String(scopedAction.value.sessionId ?? ""),
|
|
683
|
+
"command",
|
|
684
|
+
);
|
|
670
685
|
} else if (kind === "session_stop") {
|
|
671
686
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
672
687
|
await this.stopSessionReference(contextKey, sessionId);
|
|
@@ -827,6 +842,21 @@ export class ProxySessionController {
|
|
|
827
842
|
});
|
|
828
843
|
}
|
|
829
844
|
|
|
845
|
+
async controlDismissTask(localSessionId: string): Promise<{
|
|
846
|
+
localSessionId: string;
|
|
847
|
+
remoteSessionId: string;
|
|
848
|
+
title: string;
|
|
849
|
+
chatId: string;
|
|
850
|
+
}> {
|
|
851
|
+
const record = this.store.getSession(localSessionId);
|
|
852
|
+
if (!record) throw new Error(`Task not found: ${localSessionId}`);
|
|
853
|
+
return this.dismissGroupAndArchiveTask(
|
|
854
|
+
this.controlSessionContextKey(record),
|
|
855
|
+
localSessionId,
|
|
856
|
+
"cli",
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
830
860
|
async controlGetTaskStatus(localSessionId: string): Promise<{
|
|
831
861
|
session: SessionRecord;
|
|
832
862
|
snapshot?: TurnViewState;
|
|
@@ -1493,6 +1523,9 @@ export class ProxySessionController {
|
|
|
1493
1523
|
});
|
|
1494
1524
|
return;
|
|
1495
1525
|
}
|
|
1526
|
+
case "dismiss":
|
|
1527
|
+
await this.openDismissGroupCard(contextKey, userId);
|
|
1528
|
+
return;
|
|
1496
1529
|
case "status":
|
|
1497
1530
|
await this.status(contextKey, command.sessionId);
|
|
1498
1531
|
return;
|
|
@@ -4780,7 +4813,7 @@ export class ProxySessionController {
|
|
|
4780
4813
|
private async archiveSessionReference(
|
|
4781
4814
|
contextKey: string,
|
|
4782
4815
|
reference: string,
|
|
4783
|
-
options: { announce: boolean; source: "cli" | "command" | "sessions_card" },
|
|
4816
|
+
options: { announce: boolean; source: "cli" | "command" | "dismiss" | "sessions_card" },
|
|
4784
4817
|
): Promise<{ localSessionId: string; remoteSessionId: string; title: string }> {
|
|
4785
4818
|
const taskId = this.resolveSessionReference(contextKey, reference);
|
|
4786
4819
|
const direct = this.store.getSession(taskId);
|
|
@@ -4838,6 +4871,101 @@ export class ProxySessionController {
|
|
|
4838
4871
|
};
|
|
4839
4872
|
}
|
|
4840
4873
|
|
|
4874
|
+
private async openDismissGroupCard(contextKey: string, userId?: string): Promise<void> {
|
|
4875
|
+
const groupContextKey = this.dismissibleGroupContextKey(contextKey);
|
|
4876
|
+
if (!userId) throw new Error("无法识别当前用户,不能发起解散群操作。");
|
|
4877
|
+
const session = this.requireCurrentSession(groupContextKey);
|
|
4878
|
+
await this.outbound.sendInteractiveCard(groupContextKey, this.cardRenderer.renderDismissGroupConfirmation({
|
|
4879
|
+
contextKey: groupContextKey,
|
|
4880
|
+
sessionId: session.localSessionId,
|
|
4881
|
+
taskTitle: session.title ?? session.remoteSessionId ?? session.localSessionId,
|
|
4882
|
+
requestedBy: userId,
|
|
4883
|
+
}));
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4886
|
+
private assertDismissGroupRequester(action: CardAction): void {
|
|
4887
|
+
const requestedBy = String(action.value.requestedBy ?? "");
|
|
4888
|
+
if (!requestedBy || !action.userId || action.userId !== requestedBy) {
|
|
4889
|
+
throw new Error("只有发起解散操作的用户可以点击这张卡片。");
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
private async dismissGroupAndArchiveTask(
|
|
4894
|
+
contextKey: string,
|
|
4895
|
+
localSessionId: string,
|
|
4896
|
+
source: "cli" | "command",
|
|
4897
|
+
): Promise<{ localSessionId: string; remoteSessionId: string; title: string; chatId: string }> {
|
|
4898
|
+
const groupContextKey = this.dismissibleGroupContextKey(contextKey);
|
|
4899
|
+
const session = this.store.getSessionForContext(localSessionId, groupContextKey);
|
|
4900
|
+
if (!session) throw new Error("解散群卡片已失效,请重新发送 /dismiss。");
|
|
4901
|
+
|
|
4902
|
+
const currentSessionId = this.store.getUserContext(groupContextKey)?.currentSessionId;
|
|
4903
|
+
if (session.status !== "closed" && currentSessionId !== localSessionId) {
|
|
4904
|
+
throw new Error("当前任务已经切换,解散群卡片已失效,请重新发送 /dismiss。");
|
|
4905
|
+
}
|
|
4906
|
+
this.assertNoOtherActiveGroupTasks(groupContextKey, localSessionId);
|
|
4907
|
+
|
|
4908
|
+
const archived = session.status === "closed"
|
|
4909
|
+
? {
|
|
4910
|
+
localSessionId: session.localSessionId,
|
|
4911
|
+
remoteSessionId: session.remoteSessionId ?? session.acpSessionId ?? "",
|
|
4912
|
+
title: session.title ?? session.remoteSessionId ?? session.localSessionId,
|
|
4913
|
+
}
|
|
4914
|
+
: await this.archiveSessionReference(groupContextKey, localSessionId, {
|
|
4915
|
+
announce: false,
|
|
4916
|
+
source: "dismiss",
|
|
4917
|
+
});
|
|
4918
|
+
const chatId = groupContextKey.slice("chat_id:".length);
|
|
4919
|
+
try {
|
|
4920
|
+
await this.outbound.deleteGroup(groupContextKey, chatId);
|
|
4921
|
+
} catch (error) {
|
|
4922
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4923
|
+
throw new Error(`当前任务已归档,但解散群失败:${message}`);
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
const detachedSessionIds = this.store.removeChatContext(groupContextKey);
|
|
4927
|
+
for (const detachedSessionId of detachedSessionIds) {
|
|
4928
|
+
const linked = this.store.getSession(detachedSessionId);
|
|
4929
|
+
const routedContextKey = this.outbound.getSessionContextKey(detachedSessionId) ?? linked?.contextKey;
|
|
4930
|
+
if (routedContextKey && baseChatContextKey(routedContextKey) === groupContextKey) {
|
|
4931
|
+
this.outbound.unregisterSession(detachedSessionId);
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
this.store.audit(groupContextKey, "group_dismissed", {
|
|
4935
|
+
chatId,
|
|
4936
|
+
localSessionId,
|
|
4937
|
+
remoteSessionId: archived.remoteSessionId,
|
|
4938
|
+
source,
|
|
4939
|
+
});
|
|
4940
|
+
return { ...archived, chatId };
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4943
|
+
private assertNoOtherActiveGroupTasks(contextKey: string, dismissedSessionId: string): void {
|
|
4944
|
+
const active = this.store.listSessionsForChat(contextKey).find((session) => (
|
|
4945
|
+
session.localSessionId !== dismissedSessionId
|
|
4946
|
+
&& session.status !== "closed"
|
|
4947
|
+
&& (
|
|
4948
|
+
session.status === "running"
|
|
4949
|
+
|| Boolean(this.runtimes.forAgent(session.agentName).getSession(session.localSessionId)?.activeTurnId)
|
|
4950
|
+
|| this.store.countQueuedPrompts(session.localSessionId) > 0
|
|
4951
|
+
)
|
|
4952
|
+
));
|
|
4953
|
+
if (active) {
|
|
4954
|
+
throw new Error(`群内任务仍在执行或有排队消息,暂时不能解散:${active.title ?? active.localSessionId}`);
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4958
|
+
private dismissibleGroupContextKey(contextKey: string): string {
|
|
4959
|
+
if (isThreadContextKey(contextKey)) {
|
|
4960
|
+
throw new Error("/dismiss 不能在话题中使用,请回到群聊正文后重试。");
|
|
4961
|
+
}
|
|
4962
|
+
const groupContextKey = baseChatContextKey(contextKey);
|
|
4963
|
+
if (!groupContextKey.startsWith("chat_id:") || this.store.getChatContext(groupContextKey)?.chatType !== "group") {
|
|
4964
|
+
throw new Error("/dismiss 仅适用于普通群聊。");
|
|
4965
|
+
}
|
|
4966
|
+
return groupContextKey;
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4841
4969
|
private resolveSessionReference(contextKey: string, reference?: string): string {
|
|
4842
4970
|
if (reference === undefined) {
|
|
4843
4971
|
const previous = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!).previousSessionId;
|
|
@@ -5763,7 +5891,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
|
|
|
5763
5891
|
}
|
|
5764
5892
|
|
|
5765
5893
|
function isQueueIndependentCommand(command: Command): boolean {
|
|
5766
|
-
if (["archive", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type)) return true;
|
|
5894
|
+
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type)) return true;
|
|
5767
5895
|
if (command.type === "agent") return command.agent === undefined;
|
|
5768
5896
|
if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
|
|
5769
5897
|
return false;
|
package/src/state/StateStore.ts
CHANGED
|
@@ -344,6 +344,31 @@ export class StateStore {
|
|
|
344
344
|
return rows.map(mapChatContext);
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
removeChatContext(contextKey: string): string[] {
|
|
348
|
+
const threadPrefix = `${contextKey}:thread_id:`;
|
|
349
|
+
const linked = this.db.prepare(`
|
|
350
|
+
SELECT DISTINCT local_session_id
|
|
351
|
+
FROM context_sessions
|
|
352
|
+
WHERE context_key = ? OR instr(context_key, ?) = 1
|
|
353
|
+
`).all(contextKey, threadPrefix) as Array<{ local_session_id: string }>;
|
|
354
|
+
const remove = this.db.transaction(() => {
|
|
355
|
+
this.db.prepare(`
|
|
356
|
+
DELETE FROM context_sessions
|
|
357
|
+
WHERE context_key = ? OR instr(context_key, ?) = 1
|
|
358
|
+
`).run(contextKey, threadPrefix);
|
|
359
|
+
this.db.prepare(`
|
|
360
|
+
DELETE FROM user_contexts
|
|
361
|
+
WHERE context_key = ? OR instr(context_key, ?) = 1
|
|
362
|
+
`).run(contextKey, threadPrefix);
|
|
363
|
+
this.db.prepare(`
|
|
364
|
+
DELETE FROM chat_contexts
|
|
365
|
+
WHERE context_key = ? OR instr(context_key, ?) = 1
|
|
366
|
+
`).run(contextKey, threadPrefix);
|
|
367
|
+
});
|
|
368
|
+
remove();
|
|
369
|
+
return linked.map((row) => row.local_session_id);
|
|
370
|
+
}
|
|
371
|
+
|
|
347
372
|
enqueuePrompt(input: {
|
|
348
373
|
promptId: string;
|
|
349
374
|
localSessionId: string;
|
|
@@ -1233,6 +1258,18 @@ export class StateStore {
|
|
|
1233
1258
|
return rows.map((row) => ({ ...mapSession(row), contextKey }));
|
|
1234
1259
|
}
|
|
1235
1260
|
|
|
1261
|
+
listSessionsForChat(contextKey: string): SessionRecord[] {
|
|
1262
|
+
const threadPrefix = `${contextKey}:thread_id:`;
|
|
1263
|
+
const rows = this.db.prepare(`
|
|
1264
|
+
SELECT DISTINCT sessions.*
|
|
1265
|
+
FROM context_sessions
|
|
1266
|
+
JOIN sessions ON sessions.local_session_id = context_sessions.local_session_id
|
|
1267
|
+
WHERE context_sessions.context_key = ? OR instr(context_sessions.context_key, ?) = 1
|
|
1268
|
+
ORDER BY sessions.updated_at DESC
|
|
1269
|
+
`).all(contextKey, threadPrefix) as SessionRow[];
|
|
1270
|
+
return rows.map(mapSession);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1236
1273
|
listAllSessions(): SessionRecord[] {
|
|
1237
1274
|
const rows = this.db
|
|
1238
1275
|
.prepare("SELECT * FROM sessions ORDER BY updated_at DESC, created_at DESC")
|