chatccc 0.2.171 → 0.2.172

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.171",
3
+ "version": "0.2.172",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -169,6 +169,26 @@ describe("handleCommand WeChat processing ack", () => {
169
169
  expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
170
170
  });
171
171
 
172
+ it("does not send the stopped success text until the running prompt really exits", async () => {
173
+ const platform = mockPlatform();
174
+ await recordSessionRegistry({
175
+ chatId: "wx-chat",
176
+ sessionId: "sid-wechat",
177
+ tool: "claude",
178
+ chatName: "busy-session",
179
+ running: true,
180
+ });
181
+ activePrompts.set("sid-wechat", {
182
+ controller: new AbortController(),
183
+ stopped: false,
184
+ startTime: Date.now(),
185
+ });
186
+
187
+ await handleCommand(platform, "/stop", "wx-chat", "wx-user", Date.now(), "p2p");
188
+
189
+ expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "会话已停止。");
190
+ });
191
+
172
192
  it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
173
193
  const platform = mockPlatform("feishu");
174
194
  const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
@@ -177,7 +197,7 @@ describe("handleCommand WeChat processing ack", () => {
177
197
  blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
178
198
  };
179
199
  });
180
- _setAdapterForToolForTest("claude", {
200
+ _setAdapterForToolForTest("claude", {
181
201
  displayName: "Claude",
182
202
  sessionDescPrefix: "Claude Session:",
183
203
  createSession: vi.fn(async () => ({ sessionId: "sid-feishu-new" })),
@@ -88,6 +88,7 @@ import {
88
88
  recordChatPlatform,
89
89
  _getPlatformForChatForTest,
90
90
  runAgentSession,
91
+ stopSession,
91
92
  startUnifiedDisplayLoop,
92
93
  stopUnifiedDisplayLoop,
93
94
  _setProcessAliveForTest,
@@ -396,6 +397,66 @@ describe("runAgentSession process monitor", () => {
396
397
  expect.stringContaining("进程异常结束"),
397
398
  );
398
399
  });
400
+
401
+ it("sends the stopped notice only after the prompt generator exits", async () => {
402
+ const platform = mockPlatform("feishu");
403
+ setSessionPlatform(platform);
404
+ bindChatToSession("sid-stop-notice", "chat-stop-notice");
405
+ recordLastActiveChat("sid-stop-notice", "chat-stop-notice");
406
+
407
+ const closeSession = vi.fn();
408
+ let releasePrompt: (() => void) | undefined;
409
+ const adapter: ToolAdapter = {
410
+ displayName: "Claude Code",
411
+ sessionDescPrefix: "Claude Code Session:",
412
+ createSession: async () => ({ sessionId: "sid-stop-notice" }),
413
+ getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
414
+ closeSession: async () => {},
415
+ prompt: async function* (
416
+ _sid: string,
417
+ _text: string,
418
+ _cwd: string,
419
+ signal?: AbortSignal,
420
+ options?: ToolPromptOptions,
421
+ ) {
422
+ const waitForStop = new Promise<void>((resolve) => {
423
+ releasePrompt = resolve;
424
+ signal?.addEventListener("abort", () => resolve(), { once: true });
425
+ });
426
+ options?.onSessionCreated?.(() => {
427
+ closeSession();
428
+ releasePrompt?.();
429
+ });
430
+ yield { type: "assistant", blocks: [{ type: "text", text: "partial answer" }] };
431
+ await waitForStop;
432
+ },
433
+ };
434
+ _setAdapterForToolForTest("claude", adapter);
435
+
436
+ const runPromise = runAgentSession(
437
+ "sid-stop-notice",
438
+ "prompt",
439
+ platform,
440
+ "chat-stop-notice",
441
+ Date.now(),
442
+ "claude",
443
+ );
444
+
445
+ await vi.waitFor(() => {
446
+ expect(activePrompts.get("sid-stop-notice")?.closeSession).toBeTypeOf("function");
447
+ });
448
+ expect(platform.sendText).not.toHaveBeenCalledWith("chat-stop-notice", "会话已停止。");
449
+
450
+ const ok = stopSession("sid-stop-notice");
451
+ expect(ok).toBe(true);
452
+ expect(closeSession).toHaveBeenCalledTimes(1);
453
+
454
+ await runPromise;
455
+
456
+ expect(platform.sendText).toHaveBeenCalledWith("chat-stop-notice", "会话已停止。");
457
+ expect(activePrompts.has("sid-stop-notice")).toBe(false);
458
+ });
459
+
399
460
  it("re-injects IM skill capabilities for each resumed Claude prompt", async () => {
400
461
  const platform = mockPlatform("feishu");
401
462
  setSessionPlatform(platform);
@@ -41,9 +41,13 @@ vi.mock("../stream-state.ts", () => ({
41
41
  import { stopSession } from "../session.ts";
42
42
  import { activePrompts } from "../session-chat-binding.ts";
43
43
 
44
- function seedRunningSession(sid: string, accumulated = "partial output"): AbortController {
44
+ function seedRunningSession(
45
+ sid: string,
46
+ accumulated = "partial output",
47
+ closeSession?: () => void,
48
+ ): AbortController {
45
49
  const controller = new AbortController();
46
- activePrompts.set(sid, { controller, stopped: false, startTime: Date.now() });
50
+ activePrompts.set(sid, { controller, stopped: false, startTime: Date.now(), closeSession });
47
51
  stateStore.set(sid, {
48
52
  sessionId: sid,
49
53
  status: "running",
@@ -123,4 +127,14 @@ describe("stopSession 行为护栏", () => {
123
127
  stopSession("sid-C");
124
128
  expect(activePrompts.get("sid-C")?.stopped).toBe(true);
125
129
  });
130
+
131
+ it("调用 adapter 提供的 closeSession 以主动关闭底层 SDK session", () => {
132
+ const closeSession = vi.fn();
133
+ seedRunningSession("sid-D", "partial output", closeSession);
134
+
135
+ const ok = stopSession("sid-D");
136
+
137
+ expect(ok).toBe(true);
138
+ expect(closeSession).toHaveBeenCalledTimes(1);
139
+ });
126
140
  });
@@ -778,8 +778,7 @@ export async function handleCommand(
778
778
  logTrace(tid, "BRANCH", { cmd: "/stop" });
779
779
  if (stopSession(sessionId)) {
780
780
  console.log(`[${ts()}] [STOP] User sent /stop, session=${sessionId}`);
781
- await platform.sendText(chatId, "会话已停止。").catch(() => {});
782
- logTrace(tid, "DONE", { outcome: "stopped" });
781
+ logTrace(tid, "DONE", { outcome: "stop_requested" });
783
782
  } else {
784
783
  await platform
785
784
  .sendText(chatId, "当前没有正在进行的会话。")
package/src/session.ts CHANGED
@@ -1247,7 +1247,10 @@ export async function runAgentSession(
1247
1247
  });
1248
1248
  }
1249
1249
  const active1 = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
1250
- if (active1) platform.setChatAvatar(active1, tool, "idle").catch(() => {});
1250
+ if (active1) {
1251
+ await platform.sendText(active1, "会话已停止。").catch(() => {});
1252
+ platform.setChatAvatar(active1, tool, "idle").catch(() => {});
1253
+ }
1251
1254
  console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
1252
1255
  if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
1253
1256
  } else if (wasAbnormalExit) {
@@ -1623,6 +1626,11 @@ export function stopSession(sessionId: string): boolean {
1623
1626
  prompt.stopped = true;
1624
1627
  clearPromptProcessMonitor(sessionId);
1625
1628
  cancelQueuedMessage(sessionId);
1629
+ try {
1630
+ prompt.closeSession?.();
1631
+ } catch (err) {
1632
+ console.warn(`[${ts()}] [STOP] closeSession failed for ${sessionId}: ${(err as Error).message}`);
1633
+ }
1626
1634
  prompt.controller.abort();
1627
1635
 
1628
1636
  // 强制杀死 CLI 子进程。controller.abort() 只在 for-await 收到下一条