chatccc 0.2.200 → 0.2.202

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.
Files changed (92) hide show
  1. package/README.md +17 -15
  2. package/agent-prompts/claude_specific.md +45 -45
  3. package/agent-prompts/codex_specific.md +2 -2
  4. package/agent-prompts/cursor_specific.md +13 -13
  5. package/bin/cccagent.mjs +17 -17
  6. package/config.sample.json +27 -27
  7. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  8. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  9. package/im-skills/feishu-skill/skill.md +3 -3
  10. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  11. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  12. package/im-skills/wechat-file-skill/skill.md +10 -10
  13. package/im-skills/wechat-image-skill/skill.md +10 -10
  14. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  15. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  16. package/im-skills/wechat-video-skill/skill.md +10 -10
  17. package/package.json +1 -1
  18. package/scripts/postinstall-sharp-check.mjs +58 -58
  19. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  20. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  21. package/src/__tests__/builtin-chat-session.test.ts +277 -277
  22. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  23. package/src/__tests__/builtin-config.test.ts +33 -33
  24. package/src/__tests__/builtin-context.test.ts +163 -163
  25. package/src/__tests__/builtin-file-tools.test.ts +224 -224
  26. package/src/__tests__/builtin-session-select.test.ts +116 -116
  27. package/src/__tests__/builtin-sigint.test.ts +56 -56
  28. package/src/__tests__/card-plain-text.test.ts +5 -5
  29. package/src/__tests__/cardkit.test.ts +60 -60
  30. package/src/__tests__/cards.test.ts +133 -122
  31. package/src/__tests__/ccc-adapter.test.ts +114 -114
  32. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  33. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  34. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  35. package/src/__tests__/claude-adapter.test.ts +592 -592
  36. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  37. package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
  38. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  39. package/src/__tests__/config-reload.test.ts +10 -10
  40. package/src/__tests__/config-sample.test.ts +18 -18
  41. package/src/__tests__/cursor-adapter.test.ts +226 -226
  42. package/src/__tests__/feishu-api.test.ts +60 -60
  43. package/src/__tests__/feishu-avatar.test.ts +90 -90
  44. package/src/__tests__/feishu-platform.test.ts +22 -22
  45. package/src/__tests__/format-message.test.ts +47 -47
  46. package/src/__tests__/jsonl-stream.test.ts +79 -79
  47. package/src/__tests__/orchestrator.test.ts +464 -301
  48. package/src/__tests__/privacy.test.ts +198 -198
  49. package/src/__tests__/raw-stream-log.test.ts +106 -106
  50. package/src/__tests__/session.test.ts +46 -44
  51. package/src/__tests__/shared-prefix.test.ts +36 -36
  52. package/src/__tests__/sim-platform.test.ts +12 -12
  53. package/src/__tests__/stream-state.test.ts +42 -42
  54. package/src/__tests__/web-ui.test.ts +209 -209
  55. package/src/adapters/ccc-adapter.ts +121 -121
  56. package/src/adapters/claude-adapter.ts +603 -603
  57. package/src/adapters/claude-session-meta-store.ts +120 -120
  58. package/src/adapters/codex-adapter.ts +380 -380
  59. package/src/adapters/cursor-adapter.ts +673 -673
  60. package/src/adapters/jsonl-stream.ts +157 -157
  61. package/src/adapters/raw-stream-log.ts +124 -124
  62. package/src/adapters/resource-monitor.ts +140 -140
  63. package/src/agent-delegate-task-rpc.ts +153 -153
  64. package/src/agent-delegate-task.ts +81 -81
  65. package/src/agent-reload-config-rpc.ts +34 -34
  66. package/src/agent-stop-stuck.ts +129 -129
  67. package/src/builtin/cli.ts +473 -473
  68. package/src/builtin/context.ts +323 -323
  69. package/src/builtin/file-tools.ts +1072 -1072
  70. package/src/builtin/index.ts +404 -404
  71. package/src/builtin/session-select.ts +48 -48
  72. package/src/builtin/sigint.ts +50 -50
  73. package/src/cards.ts +222 -210
  74. package/src/chatgpt-subscription-rpc.ts +27 -27
  75. package/src/chatgpt-subscription.ts +299 -299
  76. package/src/chrome-devtools-guard.ts +318 -318
  77. package/src/codex-reset-actions.ts +184 -184
  78. package/src/config.ts +125 -125
  79. package/src/feishu-api.ts +118 -118
  80. package/src/feishu-platform.ts +20 -20
  81. package/src/format-message.ts +293 -293
  82. package/src/litellm-proxy.ts +374 -374
  83. package/src/orchestrator.ts +348 -329
  84. package/src/privacy.ts +118 -118
  85. package/src/runtime-reload.ts +34 -34
  86. package/src/session-chat-binding.ts +6 -6
  87. package/src/session-name.ts +8 -8
  88. package/src/session.ts +177 -169
  89. package/src/shared-prefix.ts +29 -29
  90. package/src/sim-platform.ts +20 -20
  91. package/src/turn-cards.ts +117 -117
  92. package/src/web-ui.ts +205 -205
@@ -1,16 +1,16 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
3
+ import { homedir, tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
6
6
  import type { PlatformAdapter } from "../platform-adapter.ts";
7
7
  import type { SessionInfo, ToolAdapter } from "../adapters/adapter-interface.ts";
8
8
 
9
9
  const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
10
- const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
11
- const mockGetCursorUsageSummary = vi.hoisted(() => vi.fn());
12
- const mockGetChatGptSubscriptionStatus = vi.hoisted(() => vi.fn());
13
- const mockReloadRuntimeConfig = vi.hoisted(() => vi.fn());
10
+ const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
11
+ const mockGetCursorUsageSummary = vi.hoisted(() => vi.fn());
12
+ const mockGetChatGptSubscriptionStatus = vi.hoisted(() => vi.fn());
13
+ const mockReloadRuntimeConfig = vi.hoisted(() => vi.fn());
14
14
 
15
15
  vi.mock("../im-skills.ts", () => ({
16
16
  buildImSkillsPrompt: async () => "",
@@ -66,15 +66,15 @@ vi.mock("../cursor-usage.ts", () => ({
66
66
  getCursorUsageSummary: mockGetCursorUsageSummary,
67
67
  }));
68
68
 
69
- vi.mock("../chatgpt-subscription.ts", () => ({
70
- getChatGptSubscriptionStatus: mockGetChatGptSubscriptionStatus,
71
- }));
72
-
73
- vi.mock("../runtime-reload.ts", () => ({
74
- reloadRuntimeConfig: mockReloadRuntimeConfig,
75
- }));
76
-
77
- import { handleCommand } from "../orchestrator.ts";
69
+ vi.mock("../chatgpt-subscription.ts", () => ({
70
+ getChatGptSubscriptionStatus: mockGetChatGptSubscriptionStatus,
71
+ }));
72
+
73
+ vi.mock("../runtime-reload.ts", () => ({
74
+ reloadRuntimeConfig: mockReloadRuntimeConfig,
75
+ }));
76
+
77
+ import { handleCommand } from "../orchestrator.ts";
78
78
  import {
79
79
  _clearAdapterCacheForTest,
80
80
  _resetSessionRegistryFileForTest,
@@ -82,14 +82,14 @@ import {
82
82
  _setAdapterForToolForTest,
83
83
  _setSessionRegistryFileForTest,
84
84
  _setSessionToolsFileForTest,
85
- loadSessionRegistryForBinding,
86
- recordSessionRegistry,
87
- resetState,
88
- sessionInfoMap,
89
- } from "../session.ts";
85
+ loadSessionRegistryForBinding,
86
+ recordSessionRegistry,
87
+ resetState,
88
+ sessionInfoMap,
89
+ } from "../session.ts";
90
90
  import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
91
- import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
92
- import { config } from "../config.ts";
91
+ import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
92
+ import { config } from "../config.ts";
93
93
 
94
94
  function mockPlatform(kind: "wechat" | "feishu" = "wechat"): PlatformAdapter {
95
95
  return {
@@ -136,16 +136,16 @@ describe("handleCommand WeChat processing ack", () => {
136
136
  tempDir = await mkdtemp(join(tmpdir(), "chatccc-orchestrator-"));
137
137
  _setSessionRegistryFileForTest(join(tempDir, "session-registry.json"));
138
138
  _setSessionToolsFileForTest(join(tempDir, "sessions.json"));
139
- resetState();
140
- resetBindingState();
141
- config.claude.defaultAgent = true;
142
- config.cursor.defaultAgent = false;
143
- config.codex.defaultAgent = false;
144
- mockStreamStates.clear();
145
- mockGetCodexUsageSummary.mockReset();
146
- mockGetCursorUsageSummary.mockReset();
147
- mockGetChatGptSubscriptionStatus.mockReset();
148
- mockReloadRuntimeConfig.mockReset();
139
+ resetState();
140
+ resetBindingState();
141
+ config.claude.defaultAgent = true;
142
+ config.cursor.defaultAgent = false;
143
+ config.codex.defaultAgent = false;
144
+ mockStreamStates.clear();
145
+ mockGetCodexUsageSummary.mockReset();
146
+ mockGetCursorUsageSummary.mockReset();
147
+ mockGetChatGptSubscriptionStatus.mockReset();
148
+ mockReloadRuntimeConfig.mockReset();
149
149
  mockGetCodexUsageSummary.mockResolvedValue({
150
150
  fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
151
151
  weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
@@ -177,19 +177,19 @@ describe("handleCommand WeChat processing ack", () => {
177
177
  displayMessage: "You've hit your usage limit",
178
178
  autoBucketModels: ["default"],
179
179
  });
180
- mockGetChatGptSubscriptionStatus.mockResolvedValue({
181
- ok: false,
182
- code: "chrome_cdp_disabled",
183
- reason: "Chrome CDP guard is disabled in ChatCCC config.",
184
- chromeCdp: { enabled: false, port: 15166, status: "skipped" },
185
- });
186
- mockReloadRuntimeConfig.mockResolvedValue({
187
- configPath: "C:\\Users\\me\\.chatccc\\config.json",
188
- defaultAgent: "codex",
189
- reloadedAt: "2026-07-02T05:00:00.000Z",
190
- });
191
- _setAdapterForToolForTest("claude", mockAdapter());
192
- });
180
+ mockGetChatGptSubscriptionStatus.mockResolvedValue({
181
+ ok: false,
182
+ code: "chrome_cdp_disabled",
183
+ reason: "Chrome CDP guard is disabled in ChatCCC config.",
184
+ chromeCdp: { enabled: false, port: 15166, status: "skipped" },
185
+ });
186
+ mockReloadRuntimeConfig.mockResolvedValue({
187
+ configPath: "C:\\Users\\me\\.chatccc\\config.json",
188
+ defaultAgent: "codex",
189
+ reloadedAt: "2026-07-02T05:00:00.000Z",
190
+ });
191
+ _setAdapterForToolForTest("claude", mockAdapter());
192
+ });
193
193
 
194
194
  afterEach(async () => {
195
195
  resetState();
@@ -254,12 +254,12 @@ describe("handleCommand WeChat processing ack", () => {
254
254
  _setAdapterForToolForTest("claude", {
255
255
  displayName: "Claude",
256
256
  sessionDescPrefix: "Claude Session:",
257
- createSession: vi.fn(async () => ({ sessionId: "sid-wechat" })),
258
- prompt,
259
- getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
260
- sessionId,
261
- cwd: "F:\\repo",
262
- }),
257
+ createSession: vi.fn(async () => ({ sessionId: "sid-wechat" })),
258
+ prompt,
259
+ getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
260
+ sessionId,
261
+ cwd: "F:\\repo",
262
+ }),
263
263
  closeSession: async () => {},
264
264
  });
265
265
  await recordSessionRegistry({
@@ -298,9 +298,9 @@ describe("handleCommand WeChat processing ack", () => {
298
298
  expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "会话已停止。");
299
299
  });
300
300
 
301
- it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
302
- const platform = mockPlatform("feishu");
303
- const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
301
+ it("resumes the Feishu p2p session in place instead of creating a group", async () => {
302
+ const platform = mockPlatform("feishu");
303
+ const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
304
304
  yield {
305
305
  type: "assistant" as const,
306
306
  blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
@@ -309,191 +309,354 @@ describe("handleCommand WeChat processing ack", () => {
309
309
  _setAdapterForToolForTest("claude", {
310
310
  displayName: "Claude",
311
311
  sessionDescPrefix: "Claude Session:",
312
- createSession: vi.fn(async () => ({ sessionId: "sid-feishu-new" })),
313
- prompt,
314
- getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
315
- sessionId,
316
- cwd: "F:\\repo",
317
- }),
312
+ createSession: vi.fn(async () => ({ sessionId: "sid-feishu-new" })),
313
+ prompt,
314
+ getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
315
+ sessionId,
316
+ cwd: homedir(),
317
+ }),
318
318
  closeSession: async () => {},
319
319
  });
320
- await recordSessionRegistry({
321
- chatId: "feishu-p2p",
322
- sessionId: "stale-sid",
323
- tool: "claude",
324
- chatName: "旧私聊绑定",
325
- running: false,
326
- });
327
-
328
- await handleCommand(platform, "帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
329
-
330
- expect(platform.createGroup).toHaveBeenCalledWith(expect.stringContaining("帮我看一下日志"), ["ou-user"]);
331
- expect(platform.updateChatInfo).toHaveBeenCalledWith(
332
- "feishu-group",
333
- expect.stringContaining("帮我看一下日志"),
334
- expect.stringContaining("sid-feishu-new"),
335
- );
336
- expect(prompt).toHaveBeenCalledWith(
337
- "sid-feishu-new",
338
- expect.stringContaining("帮我看一下日志"),
339
- "F:\\repo",
340
- expect.any(AbortSignal),
320
+ await recordSessionRegistry({
321
+ chatId: "feishu-p2p",
322
+ sessionId: "sid-feishu-private",
323
+ tool: "claude",
324
+ chatType: "p2p",
325
+ chatName: "飞书私聊",
326
+ running: false,
327
+ });
328
+
329
+ await handleCommand(platform, "帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
330
+
331
+ expect(platform.createGroup).not.toHaveBeenCalled();
332
+ expect(platform.updateChatInfo).not.toHaveBeenCalled();
333
+ expect(prompt).toHaveBeenCalledWith(
334
+ "sid-feishu-private",
335
+ expect.stringContaining("帮我看一下日志"),
336
+ homedir(),
337
+ expect.any(AbortSignal),
341
338
  expect.any(Object),
342
339
  );
343
-
344
- const registry = await loadSessionRegistryForBinding();
345
- expect(registry["feishu-p2p"]).toBeUndefined();
346
- expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
347
- });
348
-
349
- it("auto-creates a Feishu group for /abd private messages and sends the transformed prompt", async () => {
350
- const platform = mockPlatform("feishu");
351
- const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
340
+
341
+ const registry = await loadSessionRegistryForBinding();
342
+ expect(registry["feishu-p2p"]?.sessionId).toBe("sid-feishu-private");
343
+ });
344
+
345
+ it("creates the first Feishu p2p session in the OS user directory and sends the first prompt in place", async () => {
346
+ const platform = mockPlatform("feishu");
347
+ const createSession = vi.fn(async () => ({ sessionId: "sid-feishu-private" }));
348
+ const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
349
+ yield {
350
+ type: "assistant" as const,
351
+ blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
352
+ };
353
+ });
354
+ _setAdapterForToolForTest("claude", {
355
+ displayName: "Claude",
356
+ sessionDescPrefix: "Claude Session:",
357
+ createSession,
358
+ prompt,
359
+ getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
360
+ sessionId,
361
+ cwd: homedir(),
362
+ }),
363
+ closeSession: async () => {},
364
+ });
365
+
366
+ await handleCommand(platform, "帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
367
+
368
+ expect(createSession).toHaveBeenCalledWith(homedir());
369
+ expect(platform.createGroup).not.toHaveBeenCalled();
370
+ expect(platform.updateChatInfo).not.toHaveBeenCalled();
371
+ expect(prompt).toHaveBeenCalledWith(
372
+ "sid-feishu-private",
373
+ expect.stringContaining("帮我看一下日志"),
374
+ homedir(),
375
+ expect.any(AbortSignal),
376
+ expect.any(Object),
377
+ );
378
+
379
+ const registry = await loadSessionRegistryForBinding();
380
+ expect(registry["feishu-p2p"]?.sessionId).toBe("sid-feishu-private");
381
+ expect(registry["feishu-p2p"]?.tool).toBe("claude");
382
+ expect(registry["feishu-p2p"]?.chatType).toBe("p2p");
383
+ });
384
+
385
+ it("replaces an unmarked legacy Feishu p2p binding with a home-directory session", async () => {
386
+ const platform = mockPlatform("feishu");
387
+ const createSession = vi.fn(async () => ({ sessionId: "sid-feishu-migrated" }));
388
+ const prompt = vi.fn(async function* () {
389
+ yield {
390
+ type: "assistant" as const,
391
+ blocks: [{ type: "text" as const, text: "done" }],
392
+ };
393
+ });
394
+ _setAdapterForToolForTest("claude", {
395
+ displayName: "Claude",
396
+ sessionDescPrefix: "Claude Session:",
397
+ createSession,
398
+ prompt,
399
+ getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
400
+ sessionId,
401
+ cwd: homedir(),
402
+ }),
403
+ closeSession: async () => {},
404
+ });
405
+ await recordSessionRegistry({
406
+ chatId: "feishu-p2p",
407
+ sessionId: "sid-feishu-legacy",
408
+ tool: "claude",
409
+ chatName: "旧私聊绑定",
410
+ running: false,
411
+ });
412
+
413
+ await handleCommand(platform, "继续", "feishu-p2p", "ou-user", Date.now(), "p2p");
414
+
415
+ expect(createSession).toHaveBeenCalledWith(homedir());
416
+ expect(prompt).toHaveBeenCalledWith(
417
+ "sid-feishu-migrated",
418
+ expect.stringContaining("继续"),
419
+ homedir(),
420
+ expect.any(AbortSignal),
421
+ expect.any(Object),
422
+ );
423
+ expect(platform.createGroup).not.toHaveBeenCalled();
424
+ const registry = await loadSessionRegistryForBinding();
425
+ expect(registry["feishu-p2p"]?.sessionId).toBe("sid-feishu-migrated");
426
+ expect(registry["feishu-p2p"]?.chatType).toBe("p2p");
427
+ });
428
+
429
+ it("creates a Feishu p2p session for /abd and sends the transformed prompt without creating a group", async () => {
430
+ const platform = mockPlatform("feishu");
431
+ const createSession = vi.fn(async () => ({ sessionId: "sid-feishu-abd" }));
432
+ const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
352
433
  yield {
353
434
  type: "assistant" as const,
354
435
  blocks: [{ type: "text" as const, text: "done" }],
355
436
  };
356
437
  });
357
438
  _setAdapterForToolForTest("claude", {
358
- displayName: "Claude",
359
- sessionDescPrefix: "Claude Session:",
360
- createSession: vi.fn(async () => ({ sessionId: "sid-feishu-abd" })),
439
+ displayName: "Claude",
440
+ sessionDescPrefix: "Claude Session:",
441
+ createSession,
361
442
  prompt,
362
443
  getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
363
444
  sessionId,
364
- cwd: "F:\\repo",
445
+ cwd: homedir(),
365
446
  }),
366
447
  closeSession: async () => {},
367
448
  });
368
-
369
- await handleCommand(platform, "/abd帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
370
-
371
- expect(platform.createGroup).toHaveBeenCalledWith(expect.stringContaining("帮我看一下日志"), ["ou-user"]);
372
- expect(platform.createGroup).not.toHaveBeenCalledWith(expect.stringContaining("---"), expect.anything());
373
- const updateCall = vi.mocked(platform.updateChatInfo).mock.calls[0];
374
- expect(updateCall[1]).not.toContain("---");
375
- expect(updateCall[1]).not.toContain(ABD_APPEND_PROMPT);
376
- const userText = prompt.mock.calls[0][1];
377
- expect(userText).toContain(`[User message]\n帮我看一下日志\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
378
- expect(userText).not.toContain("/abd");
379
- });
380
-
381
- it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
382
- const platform = mockPlatform("feishu");
383
- await recordSessionRegistry({
384
- chatId: "feishu-p2p",
385
- sessionId: "stale-sid",
386
- tool: "claude",
387
- chatName: "旧私聊绑定",
388
- running: false,
389
- });
390
-
391
- await handleCommand(platform, "/model", "feishu-p2p", "ou-user", Date.now(), "p2p");
392
-
393
- expect(platform.createGroup).not.toHaveBeenCalled();
394
- expect(platform.sendRawCard).toHaveBeenCalled();
395
- const registry = await loadSessionRegistryForBinding();
396
- expect(registry["feishu-p2p"]).toBeUndefined();
397
- });
398
-
399
- it("shows Claude effort switch card in an active Feishu session", async () => {
400
- const platform = mockPlatform("feishu");
401
- vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "claude-session", description: "Claude Session: sid-claude-effort" });
402
- vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-claude-effort", tool: "claude" });
403
- await recordSessionRegistry({
404
- chatId: "feishu-chat",
405
- sessionId: "sid-claude-effort",
406
- tool: "claude",
407
- chatName: "claude-session",
408
- running: false,
409
- });
410
- sessionInfoMap.set("feishu-chat", {
411
- sessionId: "sid-claude-effort",
412
- tool: "claude",
413
- turnCount: 0,
414
- lastContextTokens: 0,
415
- startTime: Date.now(),
416
- });
417
-
418
- await handleCommand(platform, "/effort", "feishu-chat", "ou-user", Date.now(), "group");
419
-
420
- expect(platform.sendRawCard).toHaveBeenCalled();
421
- const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
422
- const raw = JSON.stringify(card);
423
- expect(raw).toContain("/effort low");
424
- expect(raw).toContain("/effort xhigh");
425
- expect(raw).toContain("/effort max");
426
- });
427
-
428
- it("switches Codex effort for the current session and reflects it in /state", async () => {
429
- const platform = mockPlatform("feishu");
430
- vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "codex-session", description: "Codex Session: sid-codex-effort" });
431
- vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-codex-effort", tool: "codex" });
432
- _setAdapterForToolForTest("codex", mockAdapter("sid-codex-effort"));
433
- await recordSessionRegistry({
434
- chatId: "codex-chat",
435
- sessionId: "sid-codex-effort",
436
- tool: "codex",
437
- chatName: "codex-session",
438
- running: false,
439
- });
440
- sessionInfoMap.set("codex-chat", {
441
- sessionId: "sid-codex-effort",
442
- tool: "codex",
443
- turnCount: 0,
444
- lastContextTokens: 0,
445
- startTime: Date.now(),
446
- });
447
-
448
- await handleCommand(platform, "/effort xhigh", "codex-chat", "ou-user", Date.now(), "group");
449
- expect(platform.sendCard).toHaveBeenCalledWith(
450
- "codex-chat",
451
- "Effort 切换",
452
- expect.stringContaining("xhigh"),
453
- "green",
454
- );
455
-
456
- vi.mocked(platform.sendCard).mockClear();
457
- vi.mocked(platform.sendRawCard).mockClear();
458
- await handleCommand(platform, "/state", "codex-chat", "ou-user", Date.now() + 1, "group");
459
-
460
- expect(platform.sendRawCard).toHaveBeenCalled();
461
- const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
462
- expect(JSON.stringify(card)).toContain("xhigh");
463
- });
464
-
465
- it("rejects /effort in Cursor sessions", async () => {
466
- const platform = mockPlatform("feishu");
467
- vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "cursor-session", description: "Cursor Session: sid-cursor-effort" });
468
- vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-cursor-effort", tool: "cursor" });
469
- _setAdapterForToolForTest("cursor", mockAdapter("sid-cursor-effort"));
470
- await recordSessionRegistry({
471
- chatId: "cursor-chat",
472
- sessionId: "sid-cursor-effort",
473
- tool: "cursor",
474
- chatName: "cursor-session",
475
- running: false,
476
- });
477
- sessionInfoMap.set("cursor-chat", {
478
- sessionId: "sid-cursor-effort",
479
- tool: "cursor",
480
- turnCount: 0,
481
- lastContextTokens: 0,
482
- startTime: Date.now(),
483
- });
484
-
485
- await handleCommand(platform, "/effort high", "cursor-chat", "ou-user", Date.now(), "group");
486
-
487
- expect(platform.sendCard).toHaveBeenCalledWith(
488
- "cursor-chat",
489
- "Effort 切换",
490
- expect.stringContaining("不支持 effort"),
491
- "red",
492
- );
493
- });
494
-
495
- it("handles /usage without creating a new Feishu group", async () => {
496
- const platform = mockPlatform("feishu");
449
+
450
+ await handleCommand(platform, "/abd帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
451
+
452
+ expect(createSession).toHaveBeenCalledWith(homedir());
453
+ expect(platform.createGroup).not.toHaveBeenCalled();
454
+ expect(platform.updateChatInfo).not.toHaveBeenCalled();
455
+ const userText = prompt.mock.calls[0][1];
456
+ expect(userText).toContain(`[User message]\n帮我看一下日志\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
457
+ expect(userText).not.toContain("/abd");
458
+ });
459
+
460
+ it("keeps the Feishu p2p binding when handling commands", async () => {
461
+ const platform = mockPlatform("feishu");
462
+ await recordSessionRegistry({
463
+ chatId: "feishu-p2p",
464
+ sessionId: "sid-feishu-private",
465
+ tool: "claude",
466
+ chatType: "p2p",
467
+ chatName: "飞书私聊",
468
+ running: false,
469
+ });
470
+
471
+ await handleCommand(platform, "/model", "feishu-p2p", "ou-user", Date.now(), "p2p");
472
+
473
+ expect(platform.createGroup).not.toHaveBeenCalled();
474
+ expect(platform.sendRawCard).toHaveBeenCalled();
475
+ const registry = await loadSessionRegistryForBinding();
476
+ expect(registry["feishu-p2p"]?.sessionId).toBe("sid-feishu-private");
477
+ });
478
+
479
+ it("keeps the Feishu p2p session bound when /new creates a separate group", async () => {
480
+ const platform = mockPlatform("feishu");
481
+ const createSession = vi.fn(async () => ({ sessionId: "sid-feishu-group" }));
482
+ _setAdapterForToolForTest("claude", {
483
+ ...mockAdapter("sid-feishu-group"),
484
+ createSession,
485
+ });
486
+ await recordSessionRegistry({
487
+ chatId: "feishu-p2p",
488
+ sessionId: "sid-feishu-private",
489
+ tool: "claude",
490
+ chatType: "p2p",
491
+ chatName: "飞书私聊",
492
+ running: false,
493
+ });
494
+
495
+ await handleCommand(platform, "/new", "feishu-p2p", "ou-user", Date.now(), "p2p");
496
+
497
+ expect(platform.createGroup).toHaveBeenCalledWith(expect.any(String), ["ou-user"]);
498
+ const registry = await loadSessionRegistryForBinding();
499
+ expect(registry["feishu-p2p"]?.sessionId).toBe("sid-feishu-private");
500
+ expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-group");
501
+ });
502
+
503
+ it("resets a Feishu p2p session in place with /newh and forces the OS user directory", async () => {
504
+ const platform = mockPlatform("feishu");
505
+ const createSession = vi.fn(async () => ({ sessionId: "sid-feishu-private-reset" }));
506
+ _setAdapterForToolForTest("claude", {
507
+ ...mockAdapter("sid-feishu-private-reset"),
508
+ createSession,
509
+ getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
510
+ sessionId,
511
+ cwd: "F:\\some-project",
512
+ }),
513
+ });
514
+ await recordSessionRegistry({
515
+ chatId: "feishu-p2p",
516
+ sessionId: "sid-feishu-private",
517
+ tool: "claude",
518
+ chatType: "p2p",
519
+ chatName: "飞书私聊",
520
+ running: false,
521
+ });
522
+
523
+ await handleCommand(platform, "/newh", "feishu-p2p", "ou-user", Date.now(), "p2p");
524
+
525
+ expect(createSession).toHaveBeenCalledWith(homedir());
526
+ expect(platform.createGroup).not.toHaveBeenCalled();
527
+ expect(platform.updateChatInfo).not.toHaveBeenCalled();
528
+ const registry = await loadSessionRegistryForBinding();
529
+ expect(registry["feishu-p2p"]?.sessionId).toBe("sid-feishu-private-reset");
530
+ expect(platform.sendCard).toHaveBeenCalledWith(
531
+ "feishu-p2p",
532
+ "Claude Code Session Reset",
533
+ expect.stringContaining(`${homedir()}\`(飞书私聊固定使用系统用户目录)`),
534
+ "green",
535
+ );
536
+ });
537
+
538
+ it("does not allow /session to replace the dedicated Feishu p2p session", async () => {
539
+ const platform = mockPlatform("feishu");
540
+ await recordSessionRegistry({
541
+ chatId: "feishu-p2p",
542
+ sessionId: "sid-feishu-private",
543
+ tool: "claude",
544
+ chatType: "p2p",
545
+ chatName: "飞书私聊",
546
+ running: false,
547
+ });
548
+
549
+ await handleCommand(platform, "/session 1", "feishu-p2p", "ou-user", Date.now(), "p2p");
550
+
551
+ expect(platform.sendCard).toHaveBeenCalledWith(
552
+ "feishu-p2p",
553
+ "/session",
554
+ expect.stringContaining("飞书私聊使用固定的专属会话"),
555
+ "yellow",
556
+ );
557
+ expect(platform.updateChatInfo).not.toHaveBeenCalled();
558
+ const registry = await loadSessionRegistryForBinding();
559
+ expect(registry["feishu-p2p"]?.sessionId).toBe("sid-feishu-private");
560
+ });
561
+
562
+ it("shows Claude effort switch card in an active Feishu session", async () => {
563
+ const platform = mockPlatform("feishu");
564
+ vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "claude-session", description: "Claude Session: sid-claude-effort" });
565
+ vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-claude-effort", tool: "claude" });
566
+ await recordSessionRegistry({
567
+ chatId: "feishu-chat",
568
+ sessionId: "sid-claude-effort",
569
+ tool: "claude",
570
+ chatName: "claude-session",
571
+ running: false,
572
+ });
573
+ sessionInfoMap.set("feishu-chat", {
574
+ sessionId: "sid-claude-effort",
575
+ tool: "claude",
576
+ turnCount: 0,
577
+ lastContextTokens: 0,
578
+ startTime: Date.now(),
579
+ });
580
+
581
+ await handleCommand(platform, "/effort", "feishu-chat", "ou-user", Date.now(), "group");
582
+
583
+ expect(platform.sendRawCard).toHaveBeenCalled();
584
+ const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
585
+ const raw = JSON.stringify(card);
586
+ expect(raw).toContain("/effort low");
587
+ expect(raw).toContain("/effort xhigh");
588
+ expect(raw).toContain("/effort max");
589
+ });
590
+
591
+ it("switches Codex effort for the current session and reflects it in /state", async () => {
592
+ const platform = mockPlatform("feishu");
593
+ vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "codex-session", description: "Codex Session: sid-codex-effort" });
594
+ vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-codex-effort", tool: "codex" });
595
+ _setAdapterForToolForTest("codex", mockAdapter("sid-codex-effort"));
596
+ await recordSessionRegistry({
597
+ chatId: "codex-chat",
598
+ sessionId: "sid-codex-effort",
599
+ tool: "codex",
600
+ chatName: "codex-session",
601
+ running: false,
602
+ });
603
+ sessionInfoMap.set("codex-chat", {
604
+ sessionId: "sid-codex-effort",
605
+ tool: "codex",
606
+ turnCount: 0,
607
+ lastContextTokens: 0,
608
+ startTime: Date.now(),
609
+ });
610
+
611
+ await handleCommand(platform, "/effort xhigh", "codex-chat", "ou-user", Date.now(), "group");
612
+ expect(platform.sendCard).toHaveBeenCalledWith(
613
+ "codex-chat",
614
+ "Effort 切换",
615
+ expect.stringContaining("xhigh"),
616
+ "green",
617
+ );
618
+
619
+ vi.mocked(platform.sendCard).mockClear();
620
+ vi.mocked(platform.sendRawCard).mockClear();
621
+ await handleCommand(platform, "/state", "codex-chat", "ou-user", Date.now() + 1, "group");
622
+
623
+ expect(platform.sendRawCard).toHaveBeenCalled();
624
+ const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
625
+ expect(JSON.stringify(card)).toContain("xhigh");
626
+ });
627
+
628
+ it("rejects /effort in Cursor sessions", async () => {
629
+ const platform = mockPlatform("feishu");
630
+ vi.mocked(platform.getChatInfo).mockResolvedValue({ name: "cursor-session", description: "Cursor Session: sid-cursor-effort" });
631
+ vi.mocked(platform.extractSessionInfo).mockReturnValue({ sessionId: "sid-cursor-effort", tool: "cursor" });
632
+ _setAdapterForToolForTest("cursor", mockAdapter("sid-cursor-effort"));
633
+ await recordSessionRegistry({
634
+ chatId: "cursor-chat",
635
+ sessionId: "sid-cursor-effort",
636
+ tool: "cursor",
637
+ chatName: "cursor-session",
638
+ running: false,
639
+ });
640
+ sessionInfoMap.set("cursor-chat", {
641
+ sessionId: "sid-cursor-effort",
642
+ tool: "cursor",
643
+ turnCount: 0,
644
+ lastContextTokens: 0,
645
+ startTime: Date.now(),
646
+ });
647
+
648
+ await handleCommand(platform, "/effort high", "cursor-chat", "ou-user", Date.now(), "group");
649
+
650
+ expect(platform.sendCard).toHaveBeenCalledWith(
651
+ "cursor-chat",
652
+ "Effort 切换",
653
+ expect.stringContaining("不支持 effort"),
654
+ "red",
655
+ );
656
+ });
657
+
658
+ it("handles /usage without creating a new Feishu group", async () => {
659
+ const platform = mockPlatform("feishu");
497
660
  const usage = {
498
661
  fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
499
662
  weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
@@ -519,38 +682,38 @@ describe("handleCommand WeChat processing ack", () => {
519
682
  expect(card.elements[0].text.content).toContain("**5h:** 已用 37%,剩余 63%,重置:");
520
683
  expect(card.elements[0].text.content).toContain("约 2小时52分钟后");
521
684
  expect(card.elements[0].text.content).toContain("[███████░░░░░░░░░░░░░]");
522
- expect(card.elements[0].text.content).toContain("**7天:** 已用 12%,剩余 88%,重置:");
685
+ expect(card.elements[0].text.content).toContain("**7天:** 已用 12%,剩余 88%,重置:");
523
686
  expect(card.elements[0].text.content).toContain("约 3天18小时17分钟后");
524
687
  expect(card.elements[0].text.content).toContain("[██░░░░░░░░░░░░░░░░░░]");
525
688
  expect(card.elements[2].actions[0].text.content).toBe("发起重置");
526
689
  expect(card.elements[2].actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
527
- expect(platform.setChatAvatar).toHaveBeenCalledWith("feishu-p2p", "codex", "idle", { codexUsage: usage });
528
- });
529
-
530
- it("shows only the 7-day window when the 5h window is absent", async () => {
531
- const platform = mockPlatform("feishu");
532
- const usage = {
533
- fiveHour: null,
534
- weekly: {
535
- usedPercent: 23,
536
- remainingPercent: 77,
537
- resetAtEpochSeconds: 1784510226,
538
- resetAfterSeconds: 500000,
539
- limitWindowSeconds: 604800,
540
- },
541
- rateLimitResetCreditsAvailable: 0,
542
- rateLimitResetCredits: [],
543
- };
544
- mockGetCodexUsageSummary.mockResolvedValue(usage);
545
-
546
- await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
547
-
548
- const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
549
- const content = card.elements[0].text.content as string;
550
- expect(content).not.toContain("**5h:**");
551
- expect(content).toContain("**7天:** 已用 23%,剩余 77%,重置:");
552
- expect(platform.setChatAvatar).toHaveBeenCalledWith("feishu-p2p", "codex", "idle", { codexUsage: usage });
553
- });
690
+ expect(platform.setChatAvatar).toHaveBeenCalledWith("feishu-p2p", "codex", "idle", { codexUsage: usage });
691
+ });
692
+
693
+ it("shows only the 7-day window when the 5h window is absent", async () => {
694
+ const platform = mockPlatform("feishu");
695
+ const usage = {
696
+ fiveHour: null,
697
+ weekly: {
698
+ usedPercent: 23,
699
+ remainingPercent: 77,
700
+ resetAtEpochSeconds: 1784510226,
701
+ resetAfterSeconds: 500000,
702
+ limitWindowSeconds: 604800,
703
+ },
704
+ rateLimitResetCreditsAvailable: 0,
705
+ rateLimitResetCredits: [],
706
+ };
707
+ mockGetCodexUsageSummary.mockResolvedValue(usage);
708
+
709
+ await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
710
+
711
+ const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
712
+ const content = card.elements[0].text.content as string;
713
+ expect(content).not.toContain("**5h:**");
714
+ expect(content).toContain("**7天:** 已用 23%,剩余 77%,重置:");
715
+ expect(platform.setChatAvatar).toHaveBeenCalledWith("feishu-p2p", "codex", "idle", { codexUsage: usage });
716
+ });
554
717
 
555
718
  it("adds ChatGPT subscription expiry to Codex /usage when CDP lookup succeeds", async () => {
556
719
  const platform = mockPlatform("feishu");
@@ -662,16 +825,16 @@ describe("handleCommand WeChat processing ack", () => {
662
825
  );
663
826
  });
664
827
 
665
- it("advertises /usage in new Codex and Cursor session ready messages", async () => {
666
- const codexPlatform = mockPlatform("feishu");
667
- _setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
828
+ it("advertises /usage in new Codex and Cursor session ready messages", async () => {
829
+ const codexPlatform = mockPlatform("feishu");
830
+ _setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
668
831
 
669
832
  await handleCommand(codexPlatform, "/new codex", "feishu-p2p", "ou-user", Date.now(), "p2p");
670
833
 
671
834
  expect(codexPlatform.sendCard).toHaveBeenCalledWith(
672
835
  "feishu-group",
673
836
  "Codex Session Ready",
674
- expect.stringContaining("发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡。"),
837
+ expect.stringContaining("发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡。"),
675
838
  "green",
676
839
  );
677
840
 
@@ -688,50 +851,50 @@ describe("handleCommand WeChat processing ack", () => {
688
851
  const claudePlatform = mockPlatform("feishu");
689
852
  await handleCommand(claudePlatform, "/new claude", "feishu-p2p-2", "ou-user", Date.now(), "p2p");
690
853
 
691
- const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
692
- ([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
693
- );
694
- expect(claudeReadyCall?.[2]).not.toContain("/usage");
695
- });
696
-
697
- it("reloads runtime config for the exact /reload command", async () => {
698
- const platform = mockPlatform("feishu");
699
-
700
- await handleCommand(platform, "/reload", "feishu-chat", "open-1", Date.now(), "group");
701
-
702
- expect(mockReloadRuntimeConfig).toHaveBeenCalledWith("chat-command");
703
- expect(platform.sendText).toHaveBeenCalledWith(
704
- "feishu-chat",
705
- expect.stringContaining("配置已重新加载"),
706
- );
707
- expect(platform.sendText).toHaveBeenCalledWith(
708
- "feishu-chat",
709
- expect.stringContaining("默认 Agent: Codex"),
710
- );
711
- });
712
-
713
- it("allows the hidden /new ccc entry without advertising it in normal help", async () => {
714
- const platform = mockPlatform("feishu");
715
- _setAdapterForToolForTest("ccc", mockAdapter("session-20260702-121530-a1b2c3"));
716
-
717
- await handleCommand(platform, "/new ccc", "feishu-p2p-ccc", "ou-user", Date.now(), "p2p");
718
-
719
- expect(platform.updateChatInfo).toHaveBeenCalledWith(
720
- "feishu-group",
721
- expect.any(String),
722
- "CCC Session: session-20260702-121530-a1b2c3",
723
- );
724
- expect(platform.sendCard).toHaveBeenCalledWith(
725
- "feishu-group",
726
- "CCC Agent Session Ready",
727
- expect.stringContaining("CCC Agent"),
728
- "green",
729
- );
730
-
731
- const helpPlatform = mockPlatform("feishu");
732
- await handleCommand(helpPlatform, "hello", "feishu-group-help", "ou-user", Date.now(), "group");
733
- const helpCard = vi.mocked(helpPlatform.sendRawCard).mock.calls.at(-1)?.[1] as string;
734
- expect(helpCard).not.toContain("/new ccc");
735
- expect(helpCard).not.toContain("CCC Agent");
736
- });
737
- });
854
+ const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
855
+ ([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
856
+ );
857
+ expect(claudeReadyCall?.[2]).not.toContain("/usage");
858
+ });
859
+
860
+ it("reloads runtime config for the exact /reload command", async () => {
861
+ const platform = mockPlatform("feishu");
862
+
863
+ await handleCommand(platform, "/reload", "feishu-chat", "open-1", Date.now(), "group");
864
+
865
+ expect(mockReloadRuntimeConfig).toHaveBeenCalledWith("chat-command");
866
+ expect(platform.sendText).toHaveBeenCalledWith(
867
+ "feishu-chat",
868
+ expect.stringContaining("配置已重新加载"),
869
+ );
870
+ expect(platform.sendText).toHaveBeenCalledWith(
871
+ "feishu-chat",
872
+ expect.stringContaining("默认 Agent: Codex"),
873
+ );
874
+ });
875
+
876
+ it("allows the hidden /new ccc entry without advertising it in normal help", async () => {
877
+ const platform = mockPlatform("feishu");
878
+ _setAdapterForToolForTest("ccc", mockAdapter("session-20260702-121530-a1b2c3"));
879
+
880
+ await handleCommand(platform, "/new ccc", "feishu-p2p-ccc", "ou-user", Date.now(), "p2p");
881
+
882
+ expect(platform.updateChatInfo).toHaveBeenCalledWith(
883
+ "feishu-group",
884
+ expect.any(String),
885
+ "CCC Session: session-20260702-121530-a1b2c3",
886
+ );
887
+ expect(platform.sendCard).toHaveBeenCalledWith(
888
+ "feishu-group",
889
+ "CCC Agent Session Ready",
890
+ expect.stringContaining("CCC Agent"),
891
+ "green",
892
+ );
893
+
894
+ const helpPlatform = mockPlatform("feishu");
895
+ await handleCommand(helpPlatform, "hello", "feishu-group-help", "ou-user", Date.now(), "group");
896
+ const helpCard = vi.mocked(helpPlatform.sendRawCard).mock.calls.at(-1)?.[1] as string;
897
+ expect(helpCard).not.toContain("/new ccc");
898
+ expect(helpCard).not.toContain("CCC Agent");
899
+ });
900
+ });