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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.74",
3
+ "version": "0.2.75",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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.sendCard).toHaveBeenCalledWith(
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;
@@ -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
- logTrace(tid, "BLOCKED", {
986
- outcome: "session_busy",
987
- sessionId,
1003
+ const queued = enqueueMessage(sessionId, {
1004
+ text, chatId, openId, msgTimestamp, chatType, traceId: tid,
988
1005
  });
989
- console.log(
990
- `[${ts()}] [BLOCKED] Session ${sessionId} is already generating, rejecting message from chat ${chatId}`,
991
- );
992
- await platform.sendCard(
993
- chatId,
994
- "生成中",
995
- "该会话正在生成回复中,请等待完成后再发送新消息。也可以发送 /stop 结束,已完成的步骤不会丢失。",
996
- "yellow",
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;