chatccc 0.2.201 → 0.2.203

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 (95) hide show
  1. package/README.md +23 -21
  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__/update-command-guard.test.ts +144 -0
  55. package/src/__tests__/web-ui.test.ts +209 -209
  56. package/src/adapters/ccc-adapter.ts +121 -121
  57. package/src/adapters/claude-adapter.ts +603 -603
  58. package/src/adapters/claude-session-meta-store.ts +120 -120
  59. package/src/adapters/codex-adapter.ts +380 -380
  60. package/src/adapters/cursor-adapter.ts +673 -673
  61. package/src/adapters/jsonl-stream.ts +157 -157
  62. package/src/adapters/raw-stream-log.ts +124 -124
  63. package/src/adapters/resource-monitor.ts +140 -140
  64. package/src/agent-delegate-task-rpc.ts +153 -153
  65. package/src/agent-delegate-task.ts +81 -81
  66. package/src/agent-reload-config-rpc.ts +34 -34
  67. package/src/agent-stop-stuck.ts +129 -129
  68. package/src/builtin/cli.ts +473 -473
  69. package/src/builtin/context.ts +323 -323
  70. package/src/builtin/file-tools.ts +1072 -1072
  71. package/src/builtin/index.ts +404 -404
  72. package/src/builtin/session-select.ts +48 -48
  73. package/src/builtin/sigint.ts +50 -50
  74. package/src/cards.ts +222 -210
  75. package/src/chatgpt-subscription-rpc.ts +27 -27
  76. package/src/chatgpt-subscription.ts +299 -299
  77. package/src/chrome-devtools-guard.ts +318 -318
  78. package/src/codex-reset-actions.ts +184 -184
  79. package/src/config.ts +125 -125
  80. package/src/feishu-api.ts +118 -118
  81. package/src/feishu-platform.ts +20 -20
  82. package/src/format-message.ts +293 -293
  83. package/src/index.ts +48 -9
  84. package/src/litellm-proxy.ts +374 -374
  85. package/src/orchestrator.ts +391 -346
  86. package/src/privacy.ts +118 -118
  87. package/src/runtime-reload.ts +34 -34
  88. package/src/session-chat-binding.ts +6 -6
  89. package/src/session-name.ts +8 -8
  90. package/src/session.ts +177 -169
  91. package/src/shared-prefix.ts +29 -29
  92. package/src/sim-platform.ts +20 -20
  93. package/src/turn-cards.ts +117 -117
  94. package/src/update-command-guard.ts +165 -0
  95. package/src/web-ui.ts +205 -205
@@ -22,24 +22,24 @@ import {
22
22
  PROJECT_ROOT,
23
23
  anthropicConfigDisplay,
24
24
  config,
25
- fileLog,
26
- getAllEffortsForTool,
27
- getAllModelsForTool,
28
- getDefaultEffortForTool,
29
- getDefaultCwd,
25
+ fileLog,
26
+ getAllEffortsForTool,
27
+ getAllModelsForTool,
28
+ getDefaultEffortForTool,
29
+ getDefaultCwd,
30
30
  setDefaultCwd,
31
31
  getRecentDirs,
32
32
  addRecentDir,
33
33
  resolveDefaultAgentTool,
34
- sessionPrefixForTool,
35
- toolDisplayName,
36
- ts,
37
- type AgentTool,
38
- } from "./config.ts";
34
+ sessionPrefixForTool,
35
+ toolDisplayName,
36
+ ts,
37
+ type AgentTool,
38
+ } from "./config.ts";
39
39
  import {
40
- buildHelpCard,
41
- buildEffortCard,
42
- buildModelCard,
40
+ buildHelpCard,
41
+ buildEffortCard,
42
+ buildModelCard,
43
43
  buildStatusCard,
44
44
  buildCdContent,
45
45
  buildCdCard,
@@ -54,21 +54,21 @@ import {
54
54
  runGitCommand,
55
55
  } from "./git-command.ts";
56
56
  import {
57
- clearSessionModelOverride,
58
- clearSessionEffortOverride,
57
+ clearSessionModelOverride,
58
+ clearSessionEffortOverride,
59
59
  getSessionStatus,
60
60
  getAllSessionsStatus,
61
61
  initClaudeSession,
62
62
  lastMsgTimestamps,
63
63
  resumeAndPrompt,
64
64
  sessionInfoMap,
65
- setSessionModelOverride,
66
- setSessionEffortOverride,
65
+ setSessionModelOverride,
66
+ setSessionEffortOverride,
67
67
  switchChatBinding,
68
68
  recordSessionRegistry,
69
69
  getAdapterForTool,
70
- getEffectiveModelForTool,
71
- getEffectiveEffortForTool,
70
+ getEffectiveModelForTool,
71
+ getEffectiveEffortForTool,
72
72
  stopSession,
73
73
  loadSessionRegistryForBinding,
74
74
  removeSessionRegistryRecord,
@@ -87,11 +87,11 @@ import {
87
87
  import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
88
88
  import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
89
89
  import { getChatGptSubscriptionStatus, type ChatGptSubscriptionResult } from "./chatgpt-subscription.ts";
90
- import { delegateAgentTask } from "./agent-delegate-task.ts";
91
- import { applySharedPrefix } from "./shared-prefix.ts";
92
- import { cwdDisplayName, sessionChatName } from "./session-name.ts";
93
- import { reloadRuntimeConfig } from "./runtime-reload.ts";
94
- export { type PlatformAdapter } from "./platform-adapter.ts";
90
+ import { applySharedPrefix } from "./shared-prefix.ts";
91
+ import { cwdDisplayName, sessionChatName } from "./session-name.ts";
92
+ import { reloadRuntimeConfig } from "./runtime-reload.ts";
93
+ import { acquireUpdateCommandGuard } from "./update-command-guard.ts";
94
+ export { type PlatformAdapter } from "./platform-adapter.ts";
95
95
  import type { ChatAvatarUsageHints, PlatformAdapter } from "./platform-adapter.ts";
96
96
  import type { CodexUsageSummary } from "./feishu-api.ts";
97
97
 
@@ -135,7 +135,7 @@ function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription:
135
135
  return `(约 ${parts.join("")}后)`;
136
136
  };
137
137
 
138
- const formatResetTime = (balance: NonNullable<CodexUsageSummary["fiveHour"]>) => {
138
+ const formatResetTime = (balance: NonNullable<CodexUsageSummary["fiveHour"]>) => {
139
139
  if (balance.resetAtEpochSeconds === null) return "暂无数据";
140
140
  const date = new Date(balance.resetAtEpochSeconds * 1000);
141
141
  const pad = (value: number) => String(value).padStart(2, "0");
@@ -263,8 +263,8 @@ function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription:
263
263
  formatChatGptSubscriptionFailure(),
264
264
  formatResetCredits(),
265
265
  "",
266
- usage.fiveHour ? formatWindow("5h", usage.fiveHour) : "",
267
- usage.weekly ? formatWindow("7天", usage.weekly) : "",
266
+ usage.fiveHour ? formatWindow("5h", usage.fiveHour) : "",
267
+ usage.weekly ? formatWindow("7天", usage.weekly) : "",
268
268
  ].filter((line, index, arr) => line !== "" || (index > 0 && arr[index - 1] !== "")).join("\n");
269
269
  }
270
270
 
@@ -325,7 +325,7 @@ function formatCursorUsageSummary(usage: CursorUsageSummary): string {
325
325
  }
326
326
 
327
327
  function usageHelpLine(tool: string): string {
328
- if (tool === "codex") return "\n发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡。";
328
+ if (tool === "codex") return "\n发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡。";
329
329
  if (tool === "cursor") return "\n发送 **/usage** 查看 Cursor 用量。";
330
330
  return "";
331
331
  }
@@ -410,42 +410,10 @@ function shouldSendWechatProcessingAck(
410
410
  return platform.kind === "wechat" && chatType === "p2p" && !isCommandText;
411
411
  }
412
412
 
413
- function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
414
- return chatType === "p2p" && platform.kind !== "wechat";
415
- }
416
-
417
- async function cleanupNonWechatP2pBinding(
418
- platform: PlatformAdapter,
419
- chatId: string,
420
- chatType: string,
421
- tid: string,
422
- ): Promise<void> {
423
- if (!isNonWechatP2p(platform, chatType)) return;
424
-
425
- try {
426
- const registry = await loadSessionRegistryForBinding();
427
- const record = registry[chatId];
428
- if (!record?.sessionId) return;
429
-
430
- unbindChatFromSession(record.sessionId, chatId);
431
- displayCards.delete(chatId);
432
- cancelQueuedMessage(record.sessionId);
433
- sessionInfoMap.delete(chatId);
434
- await removeSessionRegistryRecord(chatId);
435
- logTrace(tid, "BRANCH", {
436
- reason: "cleanup_non_wechat_p2p_binding",
437
- chatId,
438
- oldSessionId: record.sessionId,
439
- });
440
- console.log(
441
- `[${ts()}] [P2P] Removed non-WeChat p2p binding: chat=${chatId} session=${record.sessionId}`,
442
- );
443
- } catch (err) {
444
- console.log(
445
- `[${ts()}] [INFO] Cannot cleanup p2p registry for ${chatId}: ${(err as Error).message}`,
446
- );
447
- }
448
- }
413
+ /** 飞书私聊是固定会话容器;显式 /new 才创建独立群聊。 */
414
+ function isFeishuP2p(platform: PlatformAdapter, chatType: string): boolean {
415
+ return chatType === "p2p" && platform.kind === "feishu";
416
+ }
449
417
 
450
418
  /** 检测当前进程是否从 npm 全局安装启动 */
451
419
  function isRunningFromGlobalNpm(): boolean {
@@ -530,46 +498,46 @@ function syncUpdateAndRestart(): void {
530
498
  // handleCommand — 平台无关的命令分发
531
499
  // ---------------------------------------------------------------------------
532
500
 
533
- export async function handleCommand(
534
- platform: PlatformAdapter,
535
- text: string,
536
- chatId: string,
537
- openId: string,
538
- msgTimestamp: number,
539
- chatType = "group",
540
- traceId?: string,
541
- ): Promise<void> {
501
+ export async function handleCommand(
502
+ platform: PlatformAdapter,
503
+ text: string,
504
+ chatId: string,
505
+ openId: string,
506
+ msgTimestamp: number,
507
+ chatType = "group",
508
+ traceId?: string,
509
+ commandId?: string,
510
+ ): Promise<void> {
542
511
  const tid = traceId ?? makeTraceId();
543
512
  const sharedPrefix = applySharedPrefix(text);
544
513
  const promptText = sharedPrefix.text;
545
514
  text = sharedPrefix.body;
546
515
  const textLower = text.toLowerCase();
547
- const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
548
- recordChatPlatform(chatId, platform);
549
- await cleanupNonWechatP2pBinding(platform, chatId, chatType, tid);
550
-
551
- if (isCommandText && textLower === "/reload") {
552
- logTrace(tid, "BRANCH", { cmd: "/reload" });
553
- try {
554
- const result = await reloadRuntimeConfig("chat-command");
555
- await platform.sendText(
556
- chatId,
557
- [
558
- "配置已重新加载。",
559
- `默认 Agent: ${toolDisplayName(result.defaultAgent)}`,
560
- `配置文件: ${result.configPath}`,
561
- "后续新会话会使用最新配置;正在生成的会话不会被中断。",
562
- ].join("\n"),
563
- ).catch(() => {});
564
- logTrace(tid, "DONE", { outcome: "reload", defaultAgent: result.defaultAgent });
565
- } catch (err) {
566
- await platform.sendText(chatId, `配置重载失败:${(err as Error).message}`).catch(() => {});
567
- logTrace(tid, "DONE", { outcome: "reload_fail", error: (err as Error).message });
568
- }
569
- return;
570
- }
571
-
572
- if (isCommandText && textLower === "/restart") {
516
+ const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
517
+ recordChatPlatform(chatId, platform);
518
+
519
+ if (isCommandText && textLower === "/reload") {
520
+ logTrace(tid, "BRANCH", { cmd: "/reload" });
521
+ try {
522
+ const result = await reloadRuntimeConfig("chat-command");
523
+ await platform.sendText(
524
+ chatId,
525
+ [
526
+ "配置已重新加载。",
527
+ `默认 Agent: ${toolDisplayName(result.defaultAgent)}`,
528
+ `配置文件: ${result.configPath}`,
529
+ "后续新会话会使用最新配置;正在生成的会话不会被中断。",
530
+ ].join("\n"),
531
+ ).catch(() => {});
532
+ logTrace(tid, "DONE", { outcome: "reload", defaultAgent: result.defaultAgent });
533
+ } catch (err) {
534
+ await platform.sendText(chatId, `配置重载失败:${(err as Error).message}`).catch(() => {});
535
+ logTrace(tid, "DONE", { outcome: "reload_fail", error: (err as Error).message });
536
+ }
537
+ return;
538
+ }
539
+
540
+ if (isCommandText && textLower === "/restart") {
573
541
  logTrace(tid, "BRANCH", { cmd: "/restart" });
574
542
  await platform.sendText(chatId, "重启中...请几秒后发消息唤醒我").catch(() => {});
575
543
  logTrace(tid, "DONE", { outcome: "restart" });
@@ -604,16 +572,40 @@ export async function handleCommand(
604
572
  return;
605
573
  }
606
574
 
607
- if (isCommandText && textLower === "/update") {
608
- logTrace(tid, "BRANCH", { cmd: "/update" });
609
- const isGlobal = isRunningFromGlobalNpm();
610
- appendStartupTrace("update: command received", { isGlobal, chatId });
575
+ if (isCommandText && textLower === "/update") {
576
+ logTrace(tid, "BRANCH", { cmd: "/update" });
577
+ const isGlobal = isRunningFromGlobalNpm();
578
+ appendStartupTrace("update: command received", { isGlobal, chatId });
611
579
  if (!isGlobal) {
612
580
  await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /update 更新。请通过 npm install -g chatccc 安装后使用。").catch(() => {});
613
- logTrace(tid, "DONE", { outcome: "update_not_global" });
614
- return;
615
- }
616
- await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => {});
581
+ logTrace(tid, "DONE", { outcome: "update_not_global" });
582
+ return;
583
+ }
584
+
585
+ // `/update` 会主动重启进程,内存 processedMessages 随之丢失。必须在发送
586
+ // “正在更新”以及执行 npm 命令之前同步落盘,才能挡住新进程收到的飞书重投。
587
+ // 该护栏只位于此分支,不改变普通消息和 `/restart` 的现有去重行为。
588
+ const updateGuard = acquireUpdateCommandGuard({ commandId });
589
+ appendStartupTrace("update: command guard checked", {
590
+ allowed: updateGuard.allowed,
591
+ reason: updateGuard.reason,
592
+ hasCommandId: Boolean(commandId),
593
+ });
594
+ if (!updateGuard.allowed) {
595
+ if (updateGuard.reason === "duplicate_id") {
596
+ // 同一条飞书消息的重投静默丢弃,避免用户再次看到重复提示。
597
+ logTrace(tid, "DONE", { outcome: "update_duplicate_id" });
598
+ return;
599
+ }
600
+ await platform.sendText(
601
+ chatId,
602
+ "无法写入更新保护状态。为避免连续更新和重启,本次 /update 未执行。",
603
+ ).catch(() => {});
604
+ logTrace(tid, "DONE", { outcome: "update_guard_write_failed" });
605
+ return;
606
+ }
607
+
608
+ await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => {});
617
609
  logTrace(tid, "DONE", { outcome: "update" });
618
610
  appendStartupTrace("update: sync update begin", { fromPid: process.pid });
619
611
  syncUpdateAndRestart();
@@ -763,7 +755,7 @@ export async function handleCommand(
763
755
  const toolArg = text.slice(5).trim().toLowerCase();
764
756
  const tool = toolArg || resolveDefaultAgentTool();
765
757
  logTrace(tid, "BRANCH", { cmd: "/new", tool });
766
- const validTools = ["claude", "cursor", "codex", "ccc"];
758
+ const validTools = ["claude", "cursor", "codex", "ccc"];
767
759
  if (!validTools.includes(tool)) {
768
760
  logTrace(tid, "DONE", { outcome: "new_invalid_tool", tool });
769
761
  await platform.sendCard(
@@ -815,9 +807,9 @@ export async function handleCommand(
815
807
  const cwd = sessionCwd;
816
808
  const initialName = sessionChatName("新会话", cwd);
817
809
 
818
- // 微信私聊:不创建群,直接绑定 session 到当前私聊
819
- // 飞书私聊:也要建群
820
- if (chatType === "p2p" && platform.kind === "wechat") {
810
+ // /new 的平台语义保持不同:微信在当前私聊新建 session;飞书显式
811
+ // /new 始终创建独立群聊。飞书私聊自己的常驻 session 不在这里切换。
812
+ if (chatType === "p2p" && platform.kind === "wechat") {
821
813
  // 先解绑旧 session(如果存在),避免旧 session 的 display loop
822
814
  // 继续往同一个 chat 推送内容(/newh 走 switchChatBinding 已有此逻辑,
823
815
  // 但 /new p2p 之前遗漏了解绑)。
@@ -837,11 +829,12 @@ export async function handleCommand(
837
829
  tool,
838
830
  });
839
831
  await setDefaultCwd(cwd, chatId);
840
- await recordSessionRegistry({
841
- chatId,
842
- sessionId,
843
- tool,
844
- chatName: initialName,
832
+ await recordSessionRegistry({
833
+ chatId,
834
+ sessionId,
835
+ tool,
836
+ chatType,
837
+ chatName: initialName,
845
838
  turnCount: 0,
846
839
  startTime: Date.now(),
847
840
  running: false,
@@ -923,11 +916,12 @@ export async function handleCommand(
923
916
  // 让新群的默认工作目录继承当前会话的 cwd
924
917
  await setDefaultCwd(cwd, newChatId);
925
918
  bindChatToSession(sessionId, newChatId);
926
- await recordSessionRegistry({
927
- chatId: newChatId,
928
- sessionId,
929
- tool,
930
- chatName: initialName,
919
+ await recordSessionRegistry({
920
+ chatId: newChatId,
921
+ sessionId,
922
+ tool,
923
+ chatType: "group",
924
+ chatName: initialName,
931
925
  turnCount: 0,
932
926
  startTime: Date.now(),
933
927
  running: false,
@@ -962,7 +956,8 @@ export async function handleCommand(
962
956
  return;
963
957
  }
964
958
 
965
- // 检测会话上下文:群聊从 description 获取,私聊从 session-registry 获取
959
+ // 检测会话上下文:群聊从 description 获取,飞书/微信私聊都从
960
+ // session-registry 获取。私聊 chatId 是稳定容器,进程重启后仍恢复绑定。
966
961
  let sessionId: string | null = null;
967
962
  let descriptionTool: string | null = null;
968
963
  let toolLabel: string | null = null;
@@ -988,17 +983,30 @@ export async function handleCommand(
988
983
  `[${ts()}] [INFO] Cannot get chat info for ${chatId}: ${(err as Error).message}`,
989
984
  );
990
985
  }
991
- } else if (platform.kind === "wechat") {
992
- // 微信私聊:从 session-registry.json 获取绑定的 session
993
- // 飞书私聊不再作为持久会话入口,避免历史 registry 残留误路由到旧 session。
994
- try {
995
- const registry = await loadSessionRegistryForBinding();
996
- const record = registry[chatId];
997
- if (record && record.sessionId && record.tool) {
998
- sessionId = record.sessionId;
986
+ } else if (platform.kind === "wechat" || platform.kind === "feishu") {
987
+ // 私聊没有可写的群描述,因此会话绑定只持久化在 session-registry.json。
988
+ try {
989
+ const registry = await loadSessionRegistryForBinding();
990
+ const record = registry[chatId];
991
+ // 旧版飞书曾把私聊视为建群入口,并会清理私聊 registry。没有 p2p
992
+ // 标记的残留记录不能证明它是在固定用户目录创建的,因此只迁移一次:
993
+ // 先解除旧绑定;若本条是普通消息,随后在用户目录创建新的私聊 session。
994
+ if (platform.kind === "feishu" && record?.sessionId && record.chatType !== "p2p") {
995
+ unbindChatFromSession(record.sessionId, chatId);
996
+ displayCards.delete(chatId);
997
+ cancelQueuedMessage(record.sessionId);
998
+ sessionInfoMap.delete(chatId);
999
+ await removeSessionRegistryRecord(chatId);
1000
+ logTrace(tid, "BRANCH", {
1001
+ reason: "migrate_legacy_feishu_p2p_binding",
1002
+ chatId,
1003
+ oldSessionId: record.sessionId,
1004
+ });
1005
+ } else if (record && record.sessionId && record.tool) {
1006
+ sessionId = record.sessionId;
999
1007
  descriptionTool = record.tool;
1000
1008
  toolLabel = toolDisplayName(descriptionTool);
1001
- // 确保 sessionInfoMap 中有该私聊的信息
1009
+ // 确保内存状态在冷启动后恢复;bindChatToSession 是幂等的。
1002
1010
  if (!sessionInfoMap.has(chatId)) {
1003
1011
  sessionInfoMap.set(chatId, {
1004
1012
  sessionId,
@@ -1008,8 +1016,8 @@ export async function handleCommand(
1008
1016
  tool: descriptionTool,
1009
1017
  });
1010
1018
  }
1011
- bindChatToSession(sessionId, chatId);
1012
- }
1019
+ bindChatToSession(sessionId, chatId);
1020
+ }
1013
1021
  } catch (err) {
1014
1022
  console.log(
1015
1023
  `[${ts()}] [INFO] Cannot load registry for p2p ${chatId}: ${(err as Error).message}`,
@@ -1060,11 +1068,11 @@ export async function handleCommand(
1060
1068
  }
1061
1069
  }
1062
1070
 
1063
- // 微信 P2P:首条非指令消息 → 更新 registry 中的会话名
1064
- if (
1065
- chatType === "p2p" &&
1066
- platform.kind === "wechat" &&
1067
- !isCommandText
1071
+ // P2P:首条非指令消息只更新 registry 中的展示名,不修改私聊信息。
1072
+ if (
1073
+ chatType === "p2p" &&
1074
+ (platform.kind === "wechat" || platform.kind === "feishu") &&
1075
+ !isCommandText
1068
1076
  ) {
1069
1077
  try {
1070
1078
  const reg = await loadSessionRegistryForBinding();
@@ -1093,12 +1101,12 @@ export async function handleCommand(
1093
1101
  () => {},
1094
1102
  );
1095
1103
  console.log(
1096
- `[${ts()}] [RENAME] WeChat P2P → "${newName2}"`,
1104
+ `[${ts()}] [RENAME] ${platform.kind} P2P → "${newName2}"`,
1097
1105
  );
1098
1106
  }
1099
1107
  } catch (err) {
1100
1108
  console.error(
1101
- `[${ts()}] [RENAME] WeChat P2P failed: ${(err as Error).message}`,
1109
+ `[${ts()}] [RENAME] ${platform.kind} P2P failed: ${(err as Error).message}`,
1102
1110
  );
1103
1111
  }
1104
1112
  }
@@ -1211,10 +1219,11 @@ export async function handleCommand(
1211
1219
  const allSessions = await getAllSessionsStatus();
1212
1220
  const now = Date.now();
1213
1221
  const cardData = allSessions.map((s) => ({
1214
- sessionId: s.sessionId,
1215
- chatName: s.chatName,
1216
- chatId: s.chatId,
1217
- active: s.active,
1222
+ sessionId: s.sessionId,
1223
+ chatName: s.chatName,
1224
+ chatId: s.chatId,
1225
+ chatType: s.chatType,
1226
+ active: s.active,
1218
1227
  turnCount: s.turnCount,
1219
1228
  elapsedSeconds: s.active
1220
1229
  ? Math.floor((now - s.startTime) / 1000)
@@ -1222,7 +1231,10 @@ export async function handleCommand(
1222
1231
  model: s.model,
1223
1232
  tool: s.tool,
1224
1233
  }));
1225
- const card = buildSessionsCard(cardData, { defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()) });
1234
+ const card = buildSessionsCard(cardData, {
1235
+ defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()),
1236
+ fixedPrivateSession: isFeishuP2p(platform, chatType),
1237
+ });
1226
1238
  const ok = await platform.sendRawCard(chatId, card);
1227
1239
  console.log(
1228
1240
  `[${ts()}] [SESSIONS] card sent, ok=${ok}, count=${cardData.length}`,
@@ -1231,16 +1243,22 @@ export async function handleCommand(
1231
1243
  return;
1232
1244
  }
1233
1245
 
1234
- if (isCommandText && textLower === "/newh") {
1235
- logTrace(tid, "BRANCH", { cmd: "/newh" });
1236
- const adapter = getAdapterForTool(descriptionTool, sessionId);
1237
- let cwd: string;
1238
- try {
1239
- const info = await adapter.getSessionInfo(sessionId);
1240
- cwd = info?.cwd ?? (await getDefaultCwd(chatId));
1241
- } catch {
1242
- cwd = await getDefaultCwd(chatId);
1243
- }
1246
+ if (isCommandText && textLower === "/newh") {
1247
+ logTrace(tid, "BRANCH", { cmd: "/newh" });
1248
+ let cwd: string;
1249
+ if (isFeishuP2p(platform, chatType)) {
1250
+ // 飞书私聊不支持切换工作目录。即使 /cd 已为后续 /new 群聊
1251
+ // 保存了其他默认目录,/newh 仍必须在运行 ChatCCC 的用户目录重建。
1252
+ cwd = homedir();
1253
+ } else {
1254
+ const adapter = getAdapterForTool(descriptionTool, sessionId);
1255
+ try {
1256
+ const info = await adapter.getSessionInfo(sessionId);
1257
+ cwd = info?.cwd ?? (await getDefaultCwd(chatId));
1258
+ } catch {
1259
+ cwd = await getDefaultCwd(chatId);
1260
+ }
1261
+ }
1244
1262
 
1245
1263
  // 第一步:创建新 session(此时尚未碰任何内存绑定,失败可直接返回,
1246
1264
  // 旧 session 状态完全保留)。
@@ -1299,13 +1317,13 @@ export async function handleCommand(
1299
1317
  .setChatAvatar(chatId, descriptionTool, "new")
1300
1318
  .catch(() => {});
1301
1319
 
1302
- await platform.sendCard(
1303
- chatId,
1304
- `${toolLabel} Session Reset`,
1305
- `会话已重置为新的 **${toolLabel}** 会话。\n\n` +
1306
- `**Session ID:** ${newSessionId}\n` +
1307
- `**工作目录:** \`${cwd}\`(沿用当前会话目录)\n\n` +
1308
- `直接在这里发消息即可继续对话。\n` +
1320
+ await platform.sendCard(
1321
+ chatId,
1322
+ `${toolLabel} Session Reset`,
1323
+ `会话已重置为新的 **${toolLabel}** 会话。\n\n` +
1324
+ `**Session ID:** ${newSessionId}\n` +
1325
+ `**工作目录:** \`${cwd}\`${isFeishuP2p(platform, chatType) ? "(飞书私聊固定使用系统用户目录)" : "(沿用当前会话目录)"}\n\n` +
1326
+ `直接在这里发消息即可继续对话。\n` +
1309
1327
  `发送 **/cd** 可切换新建会话的默认目录。\n` +
1310
1328
  `发送 **/model** 查看或切换当前会话的模型。`,
1311
1329
  "green",
@@ -1356,9 +1374,22 @@ export async function handleCommand(
1356
1374
  }
1357
1375
 
1358
1376
  // /session <number>:切换到 /sessions 列表中的指定会话
1359
- const sessionMatch = isCommandText ? textLower.match(/^\/session\s+(\d+)$/) : null;
1360
- if (sessionMatch) {
1361
- const index = parseInt(sessionMatch[1], 10) - 1;
1377
+ const sessionMatch = isCommandText ? textLower.match(/^\/session\s+(\d+)$/) : null;
1378
+ if (sessionMatch) {
1379
+ // 飞书私聊有自己唯一的常驻 session;历史群聊 session 只能回到对应群聊
1380
+ // 继续,禁止通过 /session 把它们重新绑定进私聊。
1381
+ if (isFeishuP2p(platform, chatType)) {
1382
+ await platform.sendCard(
1383
+ chatId,
1384
+ "/session",
1385
+ "飞书私聊使用固定的专属会话,不能切换到历史群聊会话。请回到对应群聊继续,或发送 /new 新建群聊。",
1386
+ "yellow",
1387
+ );
1388
+ logTrace(tid, "DONE", { outcome: "session_switch_disabled_feishu_p2p" });
1389
+ return;
1390
+ }
1391
+
1392
+ const index = parseInt(sessionMatch[1], 10) - 1;
1362
1393
  logTrace(tid, "BRANCH", { cmd: "/session", index: index + 1 });
1363
1394
  const allSessions = await getAllSessionsStatus();
1364
1395
  const claudeOrdered = allSessions.filter(
@@ -1555,96 +1586,96 @@ export async function handleCommand(
1555
1586
  }
1556
1587
 
1557
1588
  // /git <args>:在「当前会话工作目录」执行 git 命令
1558
- if (isCommandText && textLower === "/effort clear") {
1559
- logTrace(tid, "BRANCH", { cmd: "/effort clear", sessionId, tool: descriptionTool });
1560
- const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
1561
- const toolLabel = toolDisplayName(descriptionTool);
1562
- if (efforts.length === 0) {
1563
- const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
1564
- await (platform.kind === "wechat"
1565
- ? platform.sendText(chatId, msg)
1566
- : platform.sendCard(chatId, "Effort 切换", msg, "red")
1567
- ).catch(() => {});
1568
- logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
1569
- return;
1570
- }
1571
-
1572
- clearSessionEffortOverride(sessionId);
1573
- const defaultEffort = getDefaultEffortForTool(descriptionTool as AgentTool);
1574
- const msg = `已清除当前 ${toolLabel} 会话的 effort 覆盖,恢复使用: \`${defaultEffort || "(未指定)"}\``;
1575
- await (platform.kind === "wechat"
1576
- ? platform.sendText(chatId, msg)
1577
- : platform.sendCard(chatId, "Effort 切换", msg, "green")
1578
- ).catch(() => {});
1579
- logTrace(tid, "DONE", { outcome: "effort_cleared", sessionId, tool: descriptionTool });
1580
- return;
1581
- }
1582
-
1583
- if (isCommandText && textLower.startsWith("/effort ")) {
1584
- const effortArg = text.slice(8).trim();
1585
- if (!effortArg) return;
1586
- logTrace(tid, "BRANCH", { cmd: "/effort", arg: effortArg, sessionId, tool: descriptionTool });
1587
-
1588
- const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
1589
- const toolLabel = toolDisplayName(descriptionTool);
1590
- if (efforts.length === 0) {
1591
- const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
1592
- await (platform.kind === "wechat"
1593
- ? platform.sendText(chatId, msg)
1594
- : platform.sendCard(chatId, "Effort 切换", msg, "red")
1595
- ).catch(() => {});
1596
- logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
1597
- return;
1598
- }
1599
-
1600
- const target = findModelMatch(effortArg, efforts);
1601
- if (!target) {
1602
- const msg = `未找到匹配 "${effortArg}" 的 effort。当前 ${toolLabel} 可选 effort:\n${efforts.map(e => ` \`${e}\``).join("\n")}`;
1603
- await (platform.kind === "wechat"
1604
- ? platform.sendText(chatId, msg)
1605
- : platform.sendCard(chatId, "Effort 切换", msg, "red")
1606
- ).catch(() => {});
1607
- logTrace(tid, "DONE", { outcome: "effort_not_found", arg: effortArg, tool: descriptionTool });
1608
- return;
1609
- }
1610
-
1611
- setSessionEffortOverride(sessionId, target);
1612
- const msg = `已切换当前 ${toolLabel} 会话 effort 为: \`${target}\``;
1613
- await (platform.kind === "wechat"
1614
- ? platform.sendText(chatId, msg)
1615
- : platform.sendCard(chatId, "Effort 切换", msg, "green")
1616
- ).catch(() => {});
1617
- logTrace(tid, "DONE", { outcome: "effort_switched", arg: effortArg, target, sessionId, tool: descriptionTool });
1618
- return;
1619
- }
1620
-
1621
- if (isCommandText && textLower === "/effort") {
1622
- logTrace(tid, "BRANCH", { cmd: "/effort", sessionId, tool: descriptionTool });
1623
- const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
1624
- const currentEffort = getEffectiveEffortForTool(descriptionTool, sessionId);
1625
- const toolLabel = toolDisplayName(descriptionTool);
1626
-
1627
- if (efforts.length === 0) {
1628
- const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
1629
- await (platform.kind === "wechat"
1630
- ? platform.sendText(chatId, msg)
1631
- : platform.sendCard(chatId, "Effort 切换", msg, "red")
1632
- ).catch(() => {});
1633
- } else if (platform.kind === "wechat") {
1634
- const lines = [currentEffort ? `当前 effort (${toolLabel}): ${currentEffort}` : `当前 effort (${toolLabel}): 未指定`];
1635
- lines.push("", "可切换 effort:");
1636
- for (const e of efforts) lines.push(` ${e}`);
1637
- lines.push("", "输入 /effort <effort> 切换 effort");
1638
- await platform.sendText(chatId, lines.join("\n")).catch(() => {});
1639
- } else {
1640
- const card = buildEffortCard(currentEffort, efforts, descriptionTool);
1641
- await platform.sendRawCard(chatId, card);
1642
- }
1643
- logTrace(tid, "DONE", { outcome: "effort_query", tool: descriptionTool });
1644
- return;
1645
- }
1646
-
1647
- if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
1589
+ if (isCommandText && textLower === "/effort clear") {
1590
+ logTrace(tid, "BRANCH", { cmd: "/effort clear", sessionId, tool: descriptionTool });
1591
+ const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
1592
+ const toolLabel = toolDisplayName(descriptionTool);
1593
+ if (efforts.length === 0) {
1594
+ const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
1595
+ await (platform.kind === "wechat"
1596
+ ? platform.sendText(chatId, msg)
1597
+ : platform.sendCard(chatId, "Effort 切换", msg, "red")
1598
+ ).catch(() => {});
1599
+ logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
1600
+ return;
1601
+ }
1602
+
1603
+ clearSessionEffortOverride(sessionId);
1604
+ const defaultEffort = getDefaultEffortForTool(descriptionTool as AgentTool);
1605
+ const msg = `已清除当前 ${toolLabel} 会话的 effort 覆盖,恢复使用: \`${defaultEffort || "(未指定)"}\``;
1606
+ await (platform.kind === "wechat"
1607
+ ? platform.sendText(chatId, msg)
1608
+ : platform.sendCard(chatId, "Effort 切换", msg, "green")
1609
+ ).catch(() => {});
1610
+ logTrace(tid, "DONE", { outcome: "effort_cleared", sessionId, tool: descriptionTool });
1611
+ return;
1612
+ }
1613
+
1614
+ if (isCommandText && textLower.startsWith("/effort ")) {
1615
+ const effortArg = text.slice(8).trim();
1616
+ if (!effortArg) return;
1617
+ logTrace(tid, "BRANCH", { cmd: "/effort", arg: effortArg, sessionId, tool: descriptionTool });
1618
+
1619
+ const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
1620
+ const toolLabel = toolDisplayName(descriptionTool);
1621
+ if (efforts.length === 0) {
1622
+ const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
1623
+ await (platform.kind === "wechat"
1624
+ ? platform.sendText(chatId, msg)
1625
+ : platform.sendCard(chatId, "Effort 切换", msg, "red")
1626
+ ).catch(() => {});
1627
+ logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
1628
+ return;
1629
+ }
1630
+
1631
+ const target = findModelMatch(effortArg, efforts);
1632
+ if (!target) {
1633
+ const msg = `未找到匹配 "${effortArg}" 的 effort。当前 ${toolLabel} 可选 effort:\n${efforts.map(e => ` \`${e}\``).join("\n")}`;
1634
+ await (platform.kind === "wechat"
1635
+ ? platform.sendText(chatId, msg)
1636
+ : platform.sendCard(chatId, "Effort 切换", msg, "red")
1637
+ ).catch(() => {});
1638
+ logTrace(tid, "DONE", { outcome: "effort_not_found", arg: effortArg, tool: descriptionTool });
1639
+ return;
1640
+ }
1641
+
1642
+ setSessionEffortOverride(sessionId, target);
1643
+ const msg = `已切换当前 ${toolLabel} 会话 effort 为: \`${target}\``;
1644
+ await (platform.kind === "wechat"
1645
+ ? platform.sendText(chatId, msg)
1646
+ : platform.sendCard(chatId, "Effort 切换", msg, "green")
1647
+ ).catch(() => {});
1648
+ logTrace(tid, "DONE", { outcome: "effort_switched", arg: effortArg, target, sessionId, tool: descriptionTool });
1649
+ return;
1650
+ }
1651
+
1652
+ if (isCommandText && textLower === "/effort") {
1653
+ logTrace(tid, "BRANCH", { cmd: "/effort", sessionId, tool: descriptionTool });
1654
+ const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
1655
+ const currentEffort = getEffectiveEffortForTool(descriptionTool, sessionId);
1656
+ const toolLabel = toolDisplayName(descriptionTool);
1657
+
1658
+ if (efforts.length === 0) {
1659
+ const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
1660
+ await (platform.kind === "wechat"
1661
+ ? platform.sendText(chatId, msg)
1662
+ : platform.sendCard(chatId, "Effort 切换", msg, "red")
1663
+ ).catch(() => {});
1664
+ } else if (platform.kind === "wechat") {
1665
+ const lines = [currentEffort ? `当前 effort (${toolLabel}): ${currentEffort}` : `当前 effort (${toolLabel}): 未指定`];
1666
+ lines.push("", "可切换 effort:");
1667
+ for (const e of efforts) lines.push(` ${e}`);
1668
+ lines.push("", "输入 /effort <effort> 切换 effort");
1669
+ await platform.sendText(chatId, lines.join("\n")).catch(() => {});
1670
+ } else {
1671
+ const card = buildEffortCard(currentEffort, efforts, descriptionTool);
1672
+ await platform.sendRawCard(chatId, card);
1673
+ }
1674
+ logTrace(tid, "DONE", { outcome: "effort_query", tool: descriptionTool });
1675
+ return;
1676
+ }
1677
+
1678
+ if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
1648
1679
  const args = text === "/git" ? "" : text.slice(5).trim();
1649
1680
  logTrace(tid, "BRANCH", { cmd: "/git", args: args || "(none)" });
1650
1681
  if (!args) {
@@ -1802,53 +1833,54 @@ export async function handleCommand(
1802
1833
  }
1803
1834
 
1804
1835
  // 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
1805
- if (isCommandText && textLower === "/effort") {
1806
- const defaultTool = resolveDefaultAgentTool();
1807
- const efforts = getAllEffortsForTool(defaultTool);
1808
- const currentEffort = getDefaultEffortForTool(defaultTool);
1809
- const toolLabel = toolDisplayName(defaultTool);
1810
-
1811
- if (efforts.length === 0) {
1812
- const msg = `当前默认 agent (${toolLabel}) 不支持 effort 切换。`;
1813
- await (platform.kind === "wechat"
1814
- ? platform.sendText(chatId, msg)
1815
- : platform.sendCard(chatId, "Effort 切换", msg, "red")
1816
- ).catch(() => {});
1817
- } else if (platform.kind === "wechat") {
1818
- const lines = [currentEffort ? `当前默认 effort (${toolLabel}): ${currentEffort}` : `当前默认 effort (${toolLabel}): 未指定`];
1819
- lines.push("", "可切换 effort:");
1820
- for (const e of efforts) lines.push(` ${e}`);
1821
- lines.push("", "在会话中输入 /effort <effort> 切换 effort");
1822
- await platform.sendText(chatId, lines.join("\n")).catch(() => {});
1823
- } else {
1824
- const card = buildEffortCard(currentEffort, efforts, defaultTool);
1825
- await platform.sendRawCard(chatId, card);
1826
- }
1827
- logTrace(tid, "DONE", { outcome: "effort_query", defaultTool });
1828
- return;
1829
- }
1830
-
1831
- if (isCommandText && textLower.startsWith("/effort ")) {
1832
- const defaultTool = resolveDefaultAgentTool();
1833
- const toolLabel = toolDisplayName(defaultTool);
1834
- const msg = `当前没有绑定会话。请先进入 Claude/Codex 会话,再输入 /effort <effort> 切换当前会话的 effort。当前默认 agent: ${toolLabel}`;
1835
- await (platform.kind === "wechat"
1836
- ? platform.sendText(chatId, msg)
1837
- : platform.sendCard(chatId, "Effort 切换", msg, "yellow")
1838
- ).catch(() => {});
1839
- logTrace(tid, "DONE", { outcome: "effort_no_session", defaultTool });
1840
- return;
1841
- }
1842
-
1843
- if (isCommandText && textLower === "/sessions") {
1836
+ if (isCommandText && textLower === "/effort") {
1837
+ const defaultTool = resolveDefaultAgentTool();
1838
+ const efforts = getAllEffortsForTool(defaultTool);
1839
+ const currentEffort = getDefaultEffortForTool(defaultTool);
1840
+ const toolLabel = toolDisplayName(defaultTool);
1841
+
1842
+ if (efforts.length === 0) {
1843
+ const msg = `当前默认 agent (${toolLabel}) 不支持 effort 切换。`;
1844
+ await (platform.kind === "wechat"
1845
+ ? platform.sendText(chatId, msg)
1846
+ : platform.sendCard(chatId, "Effort 切换", msg, "red")
1847
+ ).catch(() => {});
1848
+ } else if (platform.kind === "wechat") {
1849
+ const lines = [currentEffort ? `当前默认 effort (${toolLabel}): ${currentEffort}` : `当前默认 effort (${toolLabel}): 未指定`];
1850
+ lines.push("", "可切换 effort:");
1851
+ for (const e of efforts) lines.push(` ${e}`);
1852
+ lines.push("", "在会话中输入 /effort <effort> 切换 effort");
1853
+ await platform.sendText(chatId, lines.join("\n")).catch(() => {});
1854
+ } else {
1855
+ const card = buildEffortCard(currentEffort, efforts, defaultTool);
1856
+ await platform.sendRawCard(chatId, card);
1857
+ }
1858
+ logTrace(tid, "DONE", { outcome: "effort_query", defaultTool });
1859
+ return;
1860
+ }
1861
+
1862
+ if (isCommandText && textLower.startsWith("/effort ")) {
1863
+ const defaultTool = resolveDefaultAgentTool();
1864
+ const toolLabel = toolDisplayName(defaultTool);
1865
+ const msg = `当前没有绑定会话。请先进入 Claude/Codex 会话,再输入 /effort <effort> 切换当前会话的 effort。当前默认 agent: ${toolLabel}`;
1866
+ await (platform.kind === "wechat"
1867
+ ? platform.sendText(chatId, msg)
1868
+ : platform.sendCard(chatId, "Effort 切换", msg, "yellow")
1869
+ ).catch(() => {});
1870
+ logTrace(tid, "DONE", { outcome: "effort_no_session", defaultTool });
1871
+ return;
1872
+ }
1873
+
1874
+ if (isCommandText && textLower === "/sessions") {
1844
1875
  logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
1845
1876
  const allSessions = await getAllSessionsStatus();
1846
1877
  const now = Date.now();
1847
1878
  const cardData = allSessions.map((s) => ({
1848
- sessionId: s.sessionId,
1849
- chatName: s.chatName,
1850
- chatId: s.chatId,
1851
- active: s.active,
1879
+ sessionId: s.sessionId,
1880
+ chatName: s.chatName,
1881
+ chatId: s.chatId,
1882
+ chatType: s.chatType,
1883
+ active: s.active,
1852
1884
  turnCount: s.turnCount,
1853
1885
  elapsedSeconds: s.active
1854
1886
  ? Math.floor((now - s.startTime) / 1000)
@@ -1856,7 +1888,10 @@ export async function handleCommand(
1856
1888
  model: s.model,
1857
1889
  tool: s.tool,
1858
1890
  }));
1859
- const card = buildSessionsCard(cardData, { defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()) });
1891
+ const card = buildSessionsCard(cardData, {
1892
+ defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()),
1893
+ fixedPrivateSession: isFeishuP2p(platform, chatType),
1894
+ });
1860
1895
  const ok = await platform.sendRawCard(chatId, card);
1861
1896
  console.log(
1862
1897
  `[${ts()}] [SESSIONS] card sent, ok=${ok}, count=${cardData.length}`,
@@ -1865,53 +1900,63 @@ export async function handleCommand(
1865
1900
  return;
1866
1901
  }
1867
1902
 
1868
- // 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
1869
- if (isNonWechatP2p(platform, chatType) && !isCommandText) {
1870
- const tool = resolveDefaultAgentTool();
1871
- const toolLabel = toolDisplayName(tool);
1872
- logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
1873
-
1874
- if (!openId) {
1875
- logTrace(tid, "DONE", { outcome: "auto_new_p2p_no_openid" });
1876
- await platform.sendCard(
1877
- chatId,
1878
- "Error",
1879
- "Cannot identify sender.",
1880
- "red",
1881
- );
1882
- return;
1883
- }
1884
-
1885
- try {
1886
- const cwd = await getDefaultCwd(chatId);
1887
- const result = await delegateAgentTask({
1888
- platform,
1889
- tool,
1890
- cwd,
1891
- promptText,
1892
- openIds: [openId],
1893
- chatNamePrefix: text.slice(0, 10) || "新会话",
1894
- msgTimestamp,
1895
- traceId: tid,
1896
- });
1897
- logTrace(tid, "DONE", {
1898
- outcome: "auto_new_p2p_prompt_done",
1899
- newChatId: result.chatId,
1900
- sessionId: result.sessionId,
1901
- tool,
1902
- });
1903
- } catch (err) {
1904
- console.error(`[${ts()}] [AUTO-P2P] FAIL: ${(err as Error).message}`);
1905
- logTrace(tid, "DONE", {
1906
- outcome: "auto_new_p2p_delegate_fail",
1907
- error: (err as Error).message,
1908
- });
1909
- await platform.sendCard(
1910
- chatId,
1911
- "Error",
1912
- `Failed to create ${toolLabel} delegated task:\n${(err as Error).message}`,
1913
- "red",
1914
- );
1903
+ // 飞书私聊普通消息:首次使用时在当前私聊创建并持久化一个专属 session,
1904
+ // 随后的消息会在上面的 registry 路由中继续该 session。只有显式 /new 才建群。
1905
+ if (isFeishuP2p(platform, chatType) && !isCommandText) {
1906
+ const tool = resolveDefaultAgentTool();
1907
+ const toolLabel = toolDisplayName(tool);
1908
+ // 私聊 cwd 故意不读取 /cd 的 chatId 默认值:/cd 只为之后显式
1909
+ // /new 创建的群聊服务,飞书私聊始终从 ChatCCC 运行账号的用户目录启动。
1910
+ const cwd = homedir();
1911
+ logTrace(tid, "BRANCH", { cmd: "auto_new_feishu_p2p", tool, cwd });
1912
+
1913
+ try {
1914
+ const init = await initClaudeSession(tool, cwd);
1915
+ const sessionId = init.sessionId;
1916
+ const chatName = sessionChatName(text.slice(0, 10) || "私聊会话", cwd);
1917
+ const switchResult = await switchChatBinding({
1918
+ chatId,
1919
+ chatType,
1920
+ oldSessionId: null,
1921
+ newSessionId: sessionId,
1922
+ tool,
1923
+ chatName,
1924
+ newDescription: `${sessionPrefixForTool(tool)} ${sessionId}`,
1925
+ updateChatInfoFn: (cid, name, desc) =>
1926
+ platform.updateChatInfo(cid, name, desc),
1927
+ });
1928
+ if (!switchResult.ok) {
1929
+ throw switchResult.error ?? new Error("Failed to bind Feishu private session");
1930
+ }
1931
+
1932
+ await resumeAndPrompt(
1933
+ sessionId,
1934
+ promptText,
1935
+ platform,
1936
+ chatId,
1937
+ msgTimestamp,
1938
+ tool,
1939
+ tid,
1940
+ );
1941
+ logTrace(tid, "DONE", {
1942
+ outcome: "auto_new_feishu_p2p_prompt_done",
1943
+ chatId,
1944
+ sessionId,
1945
+ tool,
1946
+ cwd,
1947
+ });
1948
+ } catch (err) {
1949
+ console.error(`[${ts()}] [AUTO-P2P] FAIL: ${(err as Error).message}`);
1950
+ logTrace(tid, "DONE", {
1951
+ outcome: "auto_new_feishu_p2p_fail",
1952
+ error: (err as Error).message,
1953
+ });
1954
+ await platform.sendCard(
1955
+ chatId,
1956
+ "Error",
1957
+ `Failed to create ${toolLabel} private session:\n${(err as Error).message}`,
1958
+ "red",
1959
+ );
1915
1960
  }
1916
1961
  return;
1917
1962
  }