chatccc 0.2.74 → 0.2.75
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/package.json +1 -1
- package/src/__tests__/orchestrator.test.ts +5 -4
- package/src/cards.ts +41 -0
- package/src/index.ts +9 -1
- package/src/orchestrator.ts +42 -13
- package/src/session-chat-binding.ts +52 -0
- package/src/session.ts +14 -0
package/package.json
CHANGED
|
@@ -142,13 +142,14 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
142
142
|
|
|
143
143
|
await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
144
144
|
|
|
145
|
+
// 不再发"生成中"卡片,改为入队文本通知
|
|
145
146
|
expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
146
|
-
expect(platform.
|
|
147
|
+
expect(platform.sendText).toHaveBeenCalledWith(
|
|
147
148
|
"wx-chat",
|
|
148
|
-
"
|
|
149
|
-
"该会话正在生成回复中,请等待完成后再发送新消息。也可以发送 /stop 结束,已完成的步骤不会丢失。",
|
|
150
|
-
"yellow",
|
|
149
|
+
"当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。",
|
|
151
150
|
);
|
|
151
|
+
// sendCard 不再被调用(WeChat 用 sendText)
|
|
152
|
+
expect(platform.sendCard).not.toHaveBeenCalled();
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
package/src/cards.ts
CHANGED
|
@@ -346,6 +346,47 @@ export function buildSessionsCard(sessions: Array<{
|
|
|
346
346
|
});
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// 队列缓存卡片(消息已进入缓存队列,带取消按钮)
|
|
350
|
+
export function buildQueuedCard(text: string): string {
|
|
351
|
+
const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
|
|
352
|
+
return JSON.stringify({
|
|
353
|
+
config: { wide_screen_mode: true },
|
|
354
|
+
header: { template: "blue", title: { content: "消息已进入缓存队列", tag: "plain_text" } },
|
|
355
|
+
elements: [
|
|
356
|
+
{ tag: "div", text: { tag: "lark_md", content: `当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。\n\n> ${preview}` } },
|
|
357
|
+
{ tag: "hr" },
|
|
358
|
+
{
|
|
359
|
+
tag: "action",
|
|
360
|
+
actions: [{
|
|
361
|
+
tag: "button",
|
|
362
|
+
text: { tag: "plain_text", content: "取消缓存(/cancel)" },
|
|
363
|
+
type: "danger",
|
|
364
|
+
value: { action: "cancel" },
|
|
365
|
+
}],
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 队列满卡片(提示 /stop 或 /cancel)
|
|
372
|
+
export function buildQueueFullCard(): string {
|
|
373
|
+
return JSON.stringify({
|
|
374
|
+
config: { wide_screen_mode: true },
|
|
375
|
+
header: { template: "yellow", title: { content: "消息队列已满", tag: "plain_text" } },
|
|
376
|
+
elements: [
|
|
377
|
+
{ tag: "div", text: { tag: "lark_md", content: "当前缓存队列中已有消息等待处理,请等待或发送指令:\n- **/stop** — 停止当前生成\n- **/cancel** — 取消队列中的消息" } },
|
|
378
|
+
{ tag: "hr" },
|
|
379
|
+
{
|
|
380
|
+
tag: "action",
|
|
381
|
+
actions: [
|
|
382
|
+
{ tag: "button", text: { tag: "plain_text", content: "取消缓存(/cancel)" }, type: "danger", value: { action: "cancel" } },
|
|
383
|
+
{ tag: "button", text: { tag: "plain_text", content: "停止生成(/stop)" }, type: "default", value: { action: "stop" } },
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
349
390
|
// 状态卡片(带关闭按钮)
|
|
350
391
|
export function buildStatusCard(statusText: string, template = "blue"): string {
|
|
351
392
|
return JSON.stringify({
|
package/src/index.ts
CHANGED
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
} from "./session.ts";
|
|
96
96
|
import {
|
|
97
97
|
rebuildSessionChatsFromRegistry,
|
|
98
|
+
setQueueConsumer,
|
|
98
99
|
} from "./session-chat-binding.ts";
|
|
99
100
|
import { fixStaleStreamStates } from "./stream-state.ts";
|
|
100
101
|
import { handleCommand, type PlatformAdapter } from "./orchestrator.ts";
|
|
@@ -156,6 +157,13 @@ const feishuPlatform = createFeishuAdapter();
|
|
|
156
157
|
const wechatPlatform = createWechatAdapter();
|
|
157
158
|
setSessionPlatform(feishuPlatform);
|
|
158
159
|
|
|
160
|
+
// 注册队列消费回调:session 生成完成后自动处理缓存消息
|
|
161
|
+
setQueueConsumer((platform, msg) => {
|
|
162
|
+
handleCommand(platform, msg.text, msg.chatId, msg.openId, msg.msgTimestamp, msg.chatType, msg.traceId).catch(err =>
|
|
163
|
+
console.error(`[${ts()}] Queue consume failed: ${(err as Error).message}`)
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
159
167
|
// ---------------------------------------------------------------------------
|
|
160
168
|
// Event types
|
|
161
169
|
// ---------------------------------------------------------------------------
|
|
@@ -276,7 +284,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
276
284
|
}
|
|
277
285
|
if (!cmd) return null;
|
|
278
286
|
|
|
279
|
-
const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", newh: "/newh" };
|
|
287
|
+
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", newh: "/newh" };
|
|
280
288
|
let text = CMD_MAP[cmd] ?? "";
|
|
281
289
|
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
282
290
|
const path = (action.value as Record<string, string>).path;
|
package/src/orchestrator.ts
CHANGED
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
buildCdContent,
|
|
33
33
|
buildCdCard,
|
|
34
34
|
buildSessionsCard,
|
|
35
|
+
buildQueuedCard,
|
|
36
|
+
buildQueueFullCard,
|
|
35
37
|
} from "./cards.ts";
|
|
36
38
|
import {
|
|
37
39
|
formatGitResult,
|
|
@@ -60,6 +62,8 @@ import {
|
|
|
60
62
|
isSessionRunning,
|
|
61
63
|
displayCards,
|
|
62
64
|
recordLastActiveChat,
|
|
65
|
+
enqueueMessage,
|
|
66
|
+
cancelQueuedMessage,
|
|
63
67
|
} from "./session-chat-binding.ts";
|
|
64
68
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
65
69
|
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
@@ -306,6 +310,7 @@ export async function handleCommand(
|
|
|
306
310
|
if (oldRecord?.sessionId && oldRecord.sessionId !== sessionId) {
|
|
307
311
|
unbindChatFromSession(oldRecord.sessionId, chatId);
|
|
308
312
|
displayCards.delete(chatId);
|
|
313
|
+
cancelQueuedMessage(oldRecord.sessionId);
|
|
309
314
|
}
|
|
310
315
|
bindChatToSession(sessionId, chatId);
|
|
311
316
|
sessionInfoMap.set(chatId, {
|
|
@@ -590,6 +595,19 @@ export async function handleCommand(
|
|
|
590
595
|
return;
|
|
591
596
|
}
|
|
592
597
|
|
|
598
|
+
if (textLower === "/cancel") {
|
|
599
|
+
logTrace(tid, "BRANCH", { cmd: "/cancel" });
|
|
600
|
+
if (cancelQueuedMessage(sessionId)) {
|
|
601
|
+
console.log(`[${ts()}] [CANCEL] Queue cancelled for session=${sessionId}`);
|
|
602
|
+
await platform.sendText(chatId, "已取消缓存队列中的消息。").catch(() => {});
|
|
603
|
+
logTrace(tid, "DONE", { outcome: "cancelled" });
|
|
604
|
+
} else {
|
|
605
|
+
await platform.sendText(chatId, "当前缓存队列中没有消息。").catch(() => {});
|
|
606
|
+
logTrace(tid, "DONE", { outcome: "cancel_no_queue" });
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
593
611
|
if (textLower === "/status") {
|
|
594
612
|
logTrace(tid, "BRANCH", { cmd: "/status" });
|
|
595
613
|
const status = await getSessionStatus(chatId);
|
|
@@ -980,21 +998,32 @@ export async function handleCommand(
|
|
|
980
998
|
return;
|
|
981
999
|
}
|
|
982
1000
|
|
|
983
|
-
// 并发检查:同一 session 只能有一个活跃 prompt
|
|
1001
|
+
// 并发检查:同一 session 只能有一个活跃 prompt,多余消息进入队列
|
|
984
1002
|
if (isSessionRunning(sessionId)) {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
sessionId,
|
|
1003
|
+
const queued = enqueueMessage(sessionId, {
|
|
1004
|
+
text, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
988
1005
|
});
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
"
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1006
|
+
if (queued) {
|
|
1007
|
+
logTrace(tid, "QUEUED", { sessionId });
|
|
1008
|
+
console.log(
|
|
1009
|
+
`[${ts()}] [QUEUED] Session ${sessionId} is busy, message from chat ${chatId} enqueued`,
|
|
1010
|
+
);
|
|
1011
|
+
if (platform.kind === "wechat") {
|
|
1012
|
+
await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
|
|
1013
|
+
} else {
|
|
1014
|
+
await platform.sendRawCard(chatId, buildQueuedCard(text)).catch(() => {});
|
|
1015
|
+
}
|
|
1016
|
+
} else {
|
|
1017
|
+
logTrace(tid, "QUEUE_FULL", { sessionId });
|
|
1018
|
+
console.log(
|
|
1019
|
+
`[${ts()}] [QUEUE_FULL] Session ${sessionId} queue full, rejecting message from chat ${chatId}`,
|
|
1020
|
+
);
|
|
1021
|
+
if (platform.kind === "wechat") {
|
|
1022
|
+
await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
|
|
1023
|
+
} else {
|
|
1024
|
+
await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
998
1027
|
return;
|
|
999
1028
|
}
|
|
1000
1029
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// 由 session.ts 在初始化时调用 rebuildSessionChatsFromRegistry 重建
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
+
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
9
|
+
|
|
8
10
|
const sessionChatsMap = new Map<string, Set<string>>();
|
|
9
11
|
|
|
10
12
|
/** 从 registry 数据重建映射(由 session.ts 调用,避免循环依赖) */
|
|
@@ -123,10 +125,60 @@ export const displayCards = new Map<string, DisplayCardState>();
|
|
|
123
125
|
/** displayLoops: sessionId → 展示循环的 stop 函数 */
|
|
124
126
|
export const displayLoops = new Map<string, () => void>();
|
|
125
127
|
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// queuedMessages: sessionId → 缓存消息(生成中排队,队列最大长度 1)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export interface QueuedMessage {
|
|
133
|
+
text: string;
|
|
134
|
+
chatId: string;
|
|
135
|
+
openId: string;
|
|
136
|
+
msgTimestamp: number;
|
|
137
|
+
chatType: string;
|
|
138
|
+
traceId?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const queuedMessages = new Map<string, QueuedMessage>();
|
|
142
|
+
|
|
143
|
+
export function enqueueMessage(sessionId: string, msg: QueuedMessage): boolean {
|
|
144
|
+
if (queuedMessages.has(sessionId)) return false;
|
|
145
|
+
queuedMessages.set(sessionId, msg);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function dequeueMessage(sessionId: string): QueuedMessage | undefined {
|
|
150
|
+
const msg = queuedMessages.get(sessionId);
|
|
151
|
+
queuedMessages.delete(sessionId);
|
|
152
|
+
return msg;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function cancelQueuedMessage(sessionId: string): boolean {
|
|
156
|
+
return queuedMessages.delete(sessionId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function hasQueuedMessage(sessionId: string): boolean {
|
|
160
|
+
return queuedMessages.has(sessionId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// 队列消费回调(由 index.ts 注入,避免 session.ts → orchestrator.ts 循环依赖)
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
let onConsumeQueuedMessage: ((platform: PlatformAdapter, msg: QueuedMessage) => void) | null = null;
|
|
168
|
+
|
|
169
|
+
export function setQueueConsumer(fn: (platform: PlatformAdapter, msg: QueuedMessage) => void): void {
|
|
170
|
+
onConsumeQueuedMessage = fn;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function consumeQueuedMessage(platform: PlatformAdapter, msg: QueuedMessage): void {
|
|
174
|
+
onConsumeQueuedMessage?.(platform, msg);
|
|
175
|
+
}
|
|
176
|
+
|
|
126
177
|
export function resetBindingState(): void {
|
|
127
178
|
sessionChatsMap.clear();
|
|
128
179
|
lastActiveChatMap.clear();
|
|
129
180
|
activePrompts.clear();
|
|
181
|
+
queuedMessages.clear();
|
|
130
182
|
displayCards.clear();
|
|
131
183
|
for (const stop of displayLoops.values()) stop();
|
|
132
184
|
displayLoops.clear();
|
package/src/session.ts
CHANGED
|
@@ -50,6 +50,9 @@ import {
|
|
|
50
50
|
recordLastActiveChat,
|
|
51
51
|
getLastActiveChat,
|
|
52
52
|
pickDisplayChat,
|
|
53
|
+
dequeueMessage,
|
|
54
|
+
consumeQueuedMessage,
|
|
55
|
+
cancelQueuedMessage,
|
|
53
56
|
} from "./session-chat-binding.ts";
|
|
54
57
|
|
|
55
58
|
// ---------------------------------------------------------------------------
|
|
@@ -575,6 +578,7 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
|
|
|
575
578
|
if (oldSessionId) {
|
|
576
579
|
unbindChatFromSession(oldSessionId, chatId);
|
|
577
580
|
displayCards.delete(chatId);
|
|
581
|
+
cancelQueuedMessage(oldSessionId);
|
|
578
582
|
}
|
|
579
583
|
bindChatToSession(newSessionId, chatId);
|
|
580
584
|
recordLastActiveChat(newSessionId, chatId);
|
|
@@ -854,6 +858,15 @@ export async function runAgentSession(
|
|
|
854
858
|
const wasStopped = prompt?.stopped ?? false;
|
|
855
859
|
activePrompts.delete(sessionId);
|
|
856
860
|
|
|
861
|
+
// 消费队列中的缓存消息(异步,不阻塞后续清理)
|
|
862
|
+
const queued = dequeueMessage(sessionId);
|
|
863
|
+
if (queued) {
|
|
864
|
+
console.log(`[${ts()}] [QUEUE] Consuming queued message for session ${sessionId}: "${queued.text.slice(0, 50)}"`);
|
|
865
|
+
setImmediate(() => {
|
|
866
|
+
consumeQueuedMessage(platform, queued);
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
857
870
|
// 写最终状态
|
|
858
871
|
const finalStatus = wasStopped ? "stopped" : "done";
|
|
859
872
|
const finalReply = pickFinalReply(state).trim();
|
|
@@ -1184,6 +1197,7 @@ export function stopSession(sessionId: string): boolean {
|
|
|
1184
1197
|
const prompt = activePrompts.get(sessionId);
|
|
1185
1198
|
if (!prompt) return false;
|
|
1186
1199
|
prompt.stopped = true;
|
|
1200
|
+
cancelQueuedMessage(sessionId);
|
|
1187
1201
|
prompt.controller.abort();
|
|
1188
1202
|
console.log(`[${ts()}] [STOP] Session ${sessionId} aborted`);
|
|
1189
1203
|
return true;
|