chatccc 0.2.252 → 0.2.253

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 (280) hide show
  1. package/README.md +1 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/deepccc-agent/os-prompts/darwin.md +8 -8
  5. package/deepccc-agent/os-prompts/linux.md +8 -8
  6. package/deepccc-agent/os-prompts/win32.md +11 -11
  7. package/deepccc-agent/package.json +65 -65
  8. package/dist/deepccc-agent/src/cli.js +634 -0
  9. package/dist/deepccc-agent/src/config.js +76 -0
  10. package/dist/deepccc-agent/src/context.js +348 -0
  11. package/dist/deepccc-agent/src/file-log.js +34 -0
  12. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  13. package/dist/deepccc-agent/src/index.js +571 -0
  14. package/dist/deepccc-agent/src/permissions.js +170 -0
  15. package/dist/deepccc-agent/src/privacy.js +124 -0
  16. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  17. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  18. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  19. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  20. package/dist/deepccc-agent/src/progress/view.js +30 -0
  21. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  22. package/dist/deepccc-agent/src/session-search.js +276 -0
  23. package/dist/deepccc-agent/src/session-select.js +23 -0
  24. package/dist/deepccc-agent/src/sigint.js +26 -0
  25. package/dist/deepccc-agent/src/skills.js +178 -0
  26. package/dist/deepccc-agent/src/web-tools.js +246 -0
  27. package/dist/src/adapters/adapter-interface.js +19 -0
  28. package/dist/src/adapters/ccc-adapter.js +112 -0
  29. package/dist/src/adapters/claude-adapter.js +497 -0
  30. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  31. package/dist/src/adapters/codex-adapter.js +279 -0
  32. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  33. package/dist/src/adapters/cursor-adapter.js +491 -0
  34. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  35. package/dist/src/adapters/jsonl-stream.js +104 -0
  36. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  37. package/dist/src/adapters/raw-stream-log.js +106 -0
  38. package/dist/src/adapters/resource-monitor.js +113 -0
  39. package/dist/src/agent-activity.js +133 -0
  40. package/dist/src/agent-delegate-task-rpc.js +129 -0
  41. package/dist/src/agent-delegate-task.js +48 -0
  42. package/dist/src/agent-file-rpc.js +152 -0
  43. package/dist/src/agent-image-rpc.js +148 -0
  44. package/dist/src/agent-platform-routing.js +13 -0
  45. package/dist/src/agent-reload-config-rpc.js +23 -0
  46. package/dist/src/agent-rpc-body.js +87 -0
  47. package/dist/src/agent-stop-stuck.js +110 -0
  48. package/dist/src/card-action-routing.js +7 -0
  49. package/dist/src/card-plain-text.js +101 -0
  50. package/dist/src/cardkit.js +158 -0
  51. package/dist/src/cards.js +573 -0
  52. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  53. package/dist/src/chatgpt-subscription.js +199 -0
  54. package/dist/src/chrome-devtools-guard.js +238 -0
  55. package/dist/src/claude-sdk-installer.js +249 -0
  56. package/dist/src/codex-reset-actions.js +143 -0
  57. package/dist/src/config-utils.js +149 -0
  58. package/dist/src/config.js +804 -0
  59. package/dist/src/cursor-usage.js +77 -0
  60. package/dist/src/exit-banner.js +28 -0
  61. package/dist/src/feishu-api.js +1404 -0
  62. package/dist/src/feishu-message-ingress.js +137 -0
  63. package/dist/src/feishu-platform.js +97 -0
  64. package/dist/src/format-message.js +252 -0
  65. package/dist/src/git-command.js +155 -0
  66. package/dist/src/im-skills.js +121 -0
  67. package/dist/src/index.js +833 -0
  68. package/dist/src/litellm-proxy.js +300 -0
  69. package/dist/src/orchestrator.js +2078 -0
  70. package/dist/src/package-root.js +26 -0
  71. package/dist/src/platform-adapter.js +7 -0
  72. package/dist/src/platform-startup.js +6 -0
  73. package/dist/src/privacy.js +100 -0
  74. package/dist/src/progress/reducer.js +102 -0
  75. package/dist/src/progress/terminal-renderer.js +264 -0
  76. package/dist/src/progress/view.js +30 -0
  77. package/dist/src/response-stall.js +14 -0
  78. package/dist/src/runtime-entry.js +13 -0
  79. package/dist/src/runtime-reload.js +19 -0
  80. package/dist/src/session-chat-binding.js +183 -0
  81. package/dist/src/session-name.js +7 -0
  82. package/dist/src/session.js +2144 -0
  83. package/dist/src/shared-prefix.js +16 -0
  84. package/dist/src/shared.js +493 -0
  85. package/dist/src/sim-agent.js +105 -0
  86. package/dist/src/sim-platform.js +142 -0
  87. package/dist/src/sim-store.js +231 -0
  88. package/dist/src/simplify.js +99 -0
  89. package/dist/src/startup-lifecycle.js +209 -0
  90. package/dist/src/stream-state.js +141 -0
  91. package/dist/src/terminal-error.js +100 -0
  92. package/dist/src/trace.js +50 -0
  93. package/dist/src/turn-cards.js +92 -0
  94. package/dist/src/update-command-guard.js +114 -0
  95. package/{src/web-ui.ts → dist/src/web-ui.js} +749 -823
  96. package/dist/src/wechat-platform.js +545 -0
  97. package/package.json +76 -74
  98. package/deepccc-agent/LICENSE +0 -201
  99. package/deepccc-agent/bin/deepccc.mjs +0 -26
  100. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  101. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  102. package/deepccc-agent/package-lock.json +0 -2027
  103. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  104. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  105. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  106. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  107. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  108. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  109. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  110. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  111. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  112. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  113. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  114. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  115. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  116. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  117. package/deepccc-agent/src/cli.ts +0 -682
  118. package/deepccc-agent/src/config.ts +0 -101
  119. package/deepccc-agent/src/context.ts +0 -465
  120. package/deepccc-agent/src/file-log.ts +0 -38
  121. package/deepccc-agent/src/file-tools.ts +0 -1493
  122. package/deepccc-agent/src/index.ts +0 -710
  123. package/deepccc-agent/src/permissions.ts +0 -226
  124. package/deepccc-agent/src/privacy.ts +0 -141
  125. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  126. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  127. package/deepccc-agent/src/progress/reducer.ts +0 -113
  128. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  129. package/deepccc-agent/src/progress/view.ts +0 -77
  130. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  131. package/deepccc-agent/src/session-search.ts +0 -370
  132. package/deepccc-agent/src/session-select.ts +0 -48
  133. package/deepccc-agent/src/sigint.ts +0 -50
  134. package/deepccc-agent/src/skills.ts +0 -205
  135. package/deepccc-agent/src/web-tools.ts +0 -313
  136. package/deepccc-agent/tsconfig.build.json +0 -13
  137. package/deepccc-agent/tsconfig.json +0 -13
  138. package/deepccc-agent/vitest.config.ts +0 -7
  139. package/src/__tests__/adapter-interface.test.ts +0 -152
  140. package/src/__tests__/agent-activity.test.ts +0 -86
  141. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  142. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  143. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  144. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  145. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  146. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  147. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  148. package/src/__tests__/builtin-config.test.ts +0 -26
  149. package/src/__tests__/builtin-context.test.ts +0 -319
  150. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  151. package/src/__tests__/builtin-permissions.test.ts +0 -219
  152. package/src/__tests__/builtin-session-search.test.ts +0 -262
  153. package/src/__tests__/builtin-session-select.test.ts +0 -116
  154. package/src/__tests__/builtin-sigint.test.ts +0 -56
  155. package/src/__tests__/builtin-skills.test.ts +0 -284
  156. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  157. package/src/__tests__/card-action-routing.test.ts +0 -18
  158. package/src/__tests__/card-plain-text.test.ts +0 -45
  159. package/src/__tests__/cardkit.test.ts +0 -60
  160. package/src/__tests__/cards.test.ts +0 -607
  161. package/src/__tests__/ccc-adapter.test.ts +0 -194
  162. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  163. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  164. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  165. package/src/__tests__/claude-adapter.test.ts +0 -614
  166. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  167. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  168. package/src/__tests__/codex-adapter.test.ts +0 -331
  169. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  170. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  171. package/src/__tests__/config-reload.test.ts +0 -284
  172. package/src/__tests__/config-sample.test.ts +0 -97
  173. package/src/__tests__/config-utils.test.ts +0 -40
  174. package/src/__tests__/config.test.ts +0 -395
  175. package/src/__tests__/crash-logging.test.ts +0 -360
  176. package/src/__tests__/cursor-adapter.test.ts +0 -890
  177. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  178. package/src/__tests__/feishu-api.test.ts +0 -60
  179. package/src/__tests__/feishu-avatar.test.ts +0 -504
  180. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  181. package/src/__tests__/feishu-platform.test.ts +0 -75
  182. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  183. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  184. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  185. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  186. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  187. package/src/__tests__/format-message.test.ts +0 -316
  188. package/src/__tests__/git-command.test.ts +0 -288
  189. package/src/__tests__/im-skills.test.ts +0 -125
  190. package/src/__tests__/jsonl-stream.test.ts +0 -79
  191. package/src/__tests__/orchestrator.test.ts +0 -1268
  192. package/src/__tests__/package-files.test.ts +0 -24
  193. package/src/__tests__/platform-startup.test.ts +0 -19
  194. package/src/__tests__/privacy.test.ts +0 -198
  195. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  196. package/src/__tests__/progress-reducer.test.ts +0 -121
  197. package/src/__tests__/raw-stream-log.test.ts +0 -106
  198. package/src/__tests__/response-stall.test.ts +0 -49
  199. package/src/__tests__/restart.test.ts +0 -232
  200. package/src/__tests__/session-ccc-config.test.ts +0 -66
  201. package/src/__tests__/session.test.ts +0 -3004
  202. package/src/__tests__/shared-prefix.test.ts +0 -36
  203. package/src/__tests__/sim-agent.test.ts +0 -174
  204. package/src/__tests__/sim-platform.test.ts +0 -93
  205. package/src/__tests__/sim-store.test.ts +0 -214
  206. package/src/__tests__/simplify.test.ts +0 -283
  207. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  208. package/src/__tests__/stop-session.test.ts +0 -162
  209. package/src/__tests__/stream-state.test.ts +0 -164
  210. package/src/__tests__/terminal-error.test.ts +0 -54
  211. package/src/__tests__/terminal-renderer.test.ts +0 -247
  212. package/src/__tests__/update-command-guard.test.ts +0 -144
  213. package/src/__tests__/web-ui.test.ts +0 -438
  214. package/src/__tests__/wechat-platform.test.ts +0 -111
  215. package/src/adapters/adapter-interface.ts +0 -217
  216. package/src/adapters/ccc-adapter.ts +0 -150
  217. package/src/adapters/claude-adapter.ts +0 -673
  218. package/src/adapters/claude-session-meta-store.ts +0 -120
  219. package/src/adapters/codex-adapter.ts +0 -426
  220. package/src/adapters/codex-session-meta-store.ts +0 -131
  221. package/src/adapters/cursor-adapter.ts +0 -681
  222. package/src/adapters/cursor-session-meta-store.ts +0 -154
  223. package/src/adapters/jsonl-stream.ts +0 -157
  224. package/src/adapters/raw-stream-log.ts +0 -124
  225. package/src/adapters/resource-monitor.ts +0 -141
  226. package/src/agent-activity.ts +0 -175
  227. package/src/agent-delegate-task-rpc.ts +0 -153
  228. package/src/agent-delegate-task.ts +0 -91
  229. package/src/agent-file-rpc.ts +0 -172
  230. package/src/agent-image-rpc.ts +0 -168
  231. package/src/agent-platform-routing.ts +0 -28
  232. package/src/agent-reload-config-rpc.ts +0 -34
  233. package/src/agent-rpc-body.ts +0 -92
  234. package/src/agent-stop-stuck.ts +0 -129
  235. package/src/card-action-routing.ts +0 -14
  236. package/src/card-plain-text.ts +0 -108
  237. package/src/cardkit.ts +0 -179
  238. package/src/cards.ts +0 -684
  239. package/src/chatgpt-subscription-rpc.ts +0 -27
  240. package/src/chatgpt-subscription.ts +0 -299
  241. package/src/chrome-devtools-guard.ts +0 -318
  242. package/src/claude-sdk-installer.ts +0 -324
  243. package/src/codex-reset-actions.ts +0 -184
  244. package/src/config-utils.ts +0 -211
  245. package/src/config.ts +0 -1063
  246. package/src/cursor-usage.ts +0 -128
  247. package/src/exit-banner.ts +0 -33
  248. package/src/feishu-api.ts +0 -1616
  249. package/src/feishu-message-ingress.ts +0 -195
  250. package/src/feishu-platform.ts +0 -159
  251. package/src/format-message.ts +0 -293
  252. package/src/git-command.ts +0 -202
  253. package/src/im-skills.ts +0 -149
  254. package/src/index.ts +0 -1089
  255. package/src/litellm-proxy.ts +0 -374
  256. package/src/orchestrator.ts +0 -2543
  257. package/src/platform-adapter.ts +0 -70
  258. package/src/platform-startup.ts +0 -16
  259. package/src/privacy.ts +0 -118
  260. package/src/progress/reducer.ts +0 -113
  261. package/src/progress/terminal-renderer.ts +0 -294
  262. package/src/progress/view.ts +0 -77
  263. package/src/response-stall.ts +0 -28
  264. package/src/runtime-reload.ts +0 -34
  265. package/src/session-chat-binding.ts +0 -292
  266. package/src/session-name.ts +0 -8
  267. package/src/session.ts +0 -2659
  268. package/src/shared-prefix.ts +0 -29
  269. package/src/shared.ts +0 -552
  270. package/src/sim-agent.ts +0 -167
  271. package/src/sim-platform.ts +0 -177
  272. package/src/sim-store.ts +0 -317
  273. package/src/simplify.ts +0 -120
  274. package/src/startup-lifecycle.ts +0 -250
  275. package/src/stream-state.ts +0 -177
  276. package/src/terminal-error.ts +0 -129
  277. package/src/trace.ts +0 -51
  278. package/src/turn-cards.ts +0 -118
  279. package/src/update-command-guard.ts +0 -165
  280. package/src/wechat-platform.ts +0 -680
@@ -0,0 +1,545 @@
1
+ /**
2
+ * wechat-platform.ts — WeChat iLink 平台适配器
3
+ *
4
+ * 基于 @openilink/openilink-sdk-node,提供:
5
+ * - QR 登录(每次启动强制显示新 QR)
6
+ * - 长轮询消息接收
7
+ * - PlatformAdapter 实现(纯文本、不支持卡片/群管理)
8
+ * - 自动重连
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { createRequire } from "node:module";
12
+ import { dirname, extname, join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { Client as OpenIlinkWire, extractText, } from "@openilink/openilink-sdk-node";
15
+ import { setupFileLogging } from "./shared.js";
16
+ import { appendChatLog } from "./config.js";
17
+ import { cardJsonToPlainText } from "./card-plain-text.js";
18
+ import { applyPrivacy } from "./privacy.js";
19
+ const USER_DATA_DIR = join(homedir(), ".chatccc");
20
+ const ILINK_AUTH_PATH = join(USER_DATA_DIR, "state", "ilink-auth.json");
21
+ const ILINK_LOG_DIR = join(USER_DATA_DIR, "logs");
22
+ const requireFromWechat = createRequire(import.meta.url);
23
+ const terminalQr = requireFromWechat("qrcode-terminal");
24
+ const { logPath: WECHAT_LOG_PATH } = setupFileLogging(ILINK_LOG_DIR, "wechat");
25
+ let ilinkWire = null;
26
+ /** 获取当前 iLink wire 实例(供外部脚本/测试使用) */
27
+ export function getIlinkWire() {
28
+ return ilinkWire;
29
+ }
30
+ /** chatId → 最新 context_token */
31
+ const contextTokenMap = new Map();
32
+ /** chatId → 用户未回复时已连发消息数 */
33
+ const consecutiveSendCount = new Map();
34
+ /** chatId → 上一次 sendText 成功的时间戳 */
35
+ const lastSendTimeMap = new Map();
36
+ const textCardMap = new Map();
37
+ let textCardSeq = 0;
38
+ let platformLog = () => { };
39
+ const TEXT_CARD_UPDATE_INTERVAL_MS = 30_000;
40
+ /** 微信连发间隔阈值:第5条起限制 ≥ 此值(默认10秒),测试可覆写 */
41
+ let wxMinSendIntervalMs = 10_000;
42
+ export function _setWxMinSendIntervalMsForTest(ms) {
43
+ wxMinSendIntervalMs = ms;
44
+ }
45
+ function isTerminalCardText(text) {
46
+ return text.startsWith("# 完成") || text.startsWith("# 已停止");
47
+ }
48
+ function isFinalReplyText(text) {
49
+ return text.includes("━━━ 回答结束 ━━━");
50
+ }
51
+ export function compressGeneratingText(text) {
52
+ const lines = text.split("\n");
53
+ if (lines.length <= 10)
54
+ return text;
55
+ return [...lines.slice(0, 5), "...", ...lines.slice(-5)].join("\n");
56
+ }
57
+ async function sendWechatTextRaw(wire, chatId, text) {
58
+ const contextToken = contextTokenMap.get(chatId);
59
+ if (contextToken) {
60
+ await wire.sendText(chatId, text, contextToken);
61
+ }
62
+ else {
63
+ await wire.push(chatId, text);
64
+ }
65
+ }
66
+ export function _resetWechatClawStateForTest() {
67
+ consecutiveSendCount.clear();
68
+ contextTokenMap.clear();
69
+ lastSendTimeMap.clear();
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Snapshot 持久化
73
+ // ---------------------------------------------------------------------------
74
+ function ensureParentDir(filePath) {
75
+ mkdirSync(dirname(filePath), { recursive: true });
76
+ }
77
+ function readSnapshot() {
78
+ if (!existsSync(ILINK_AUTH_PATH))
79
+ return {};
80
+ try {
81
+ return JSON.parse(readFileSync(ILINK_AUTH_PATH, "utf8"));
82
+ }
83
+ catch {
84
+ return {};
85
+ }
86
+ }
87
+ function writeSnapshot(snapshot) {
88
+ ensureParentDir(ILINK_AUTH_PATH);
89
+ writeFileSync(ILINK_AUTH_PATH, `${JSON.stringify(snapshot, null, 2)}\n`);
90
+ }
91
+ function updateSnapshot(patch) {
92
+ const next = { ...readSnapshot(), ...patch, lastSeenAt: new Date().toISOString() };
93
+ writeSnapshot(next);
94
+ return next;
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // QR 终端输出
98
+ // ---------------------------------------------------------------------------
99
+ function printScanMaterial(content) {
100
+ platformLog("\n========== 微信 iLink 扫码内容 ==========");
101
+ platformLog(content);
102
+ platformLog("========== 终端二维码 ==========");
103
+ terminalQr.generate(content, { small: true }, (renderedQr) => {
104
+ // 逐行用裸 console.log 输出,避免 platformLog 前缀导致首行错位
105
+ for (const line of renderedQr.split("\n")) {
106
+ console.log(` ${line}`);
107
+ }
108
+ });
109
+ platformLog("========== 请使用微信扫描上方二维码登录 ==========\n");
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // WeChat PlatformAdapter 工厂
113
+ // ---------------------------------------------------------------------------
114
+ export function createWechatAdapter(options = {}) {
115
+ const getWire = options.getWire ?? (() => ilinkWire);
116
+ const log = options.log ?? ((msg) => platformLog(msg));
117
+ return {
118
+ kind: "wechat",
119
+ // ---- 基础消息 ----
120
+ async sendText(chatId, text) {
121
+ const wire = getWire();
122
+ if (!wire)
123
+ return false;
124
+ text = applyPrivacy(text);
125
+ // 微信 claw 连发限制:统计用户未回复时已连发条数
126
+ const count = (consecutiveSendCount.get(chatId) ?? 0) + 1;
127
+ consecutiveSendCount.set(chatId, count);
128
+ const isFinal = isFinalReplyText(text);
129
+ // 第9条且非最终消息:附加 claw 限制提示,此后禁止继续发送生成消息
130
+ if (count === 9 && !isFinal) {
131
+ text = text + "\n---由于微信claw机制限制,不再发送过程,稍后把最终结果发送给你---";
132
+ }
133
+ // 超过9条后不允许发送非最终消息(生成消息),最终结果消息仍然允许
134
+ if (count > 9 && !isFinal) {
135
+ log(`[WECHAT] sendText skipped (claw limit): chatId=${chatId} count=${count}`);
136
+ return false;
137
+ }
138
+ // 第5条起限制发送间隔 ≥ 10 秒(前4条保持原有 3s 显示循环节奏)
139
+ if (count > 4) {
140
+ const lastSentAt = lastSendTimeMap.get(chatId) ?? 0;
141
+ const elapsed = Date.now() - lastSentAt;
142
+ if (elapsed < wxMinSendIntervalMs) {
143
+ await new Promise((r) => setTimeout(r, wxMinSendIntervalMs - elapsed));
144
+ }
145
+ }
146
+ try {
147
+ await sendWechatTextRaw(wire, chatId, text);
148
+ lastSendTimeMap.set(chatId, Date.now());
149
+ const preview = text.length > 60 ? text.slice(0, 60) + "..." : text;
150
+ log(`[WECHAT] sendText OK: chatId=${chatId} len=${text.length} count=${count} text="${preview}"`);
151
+ return true;
152
+ }
153
+ catch (err) {
154
+ log(`[WECHAT] sendText failed: ${err.message}`);
155
+ return false;
156
+ }
157
+ },
158
+ async sendCard(chatId, title, content, _template) {
159
+ // WeChat has no card renderer here; send the same message as plain text.
160
+ const text = [title.trim(), content.trim()].filter(Boolean).join("\n\n");
161
+ return this.sendText(chatId, text);
162
+ },
163
+ async sendRawCard(chatId, cardJson) {
164
+ const text = cardJsonToPlainText(cardJson);
165
+ if (!text) {
166
+ log(`[WECHAT] sendRawCard text fallback failed: empty card text`);
167
+ return false;
168
+ }
169
+ log(`[WECHAT] sendRawCard degraded to text`);
170
+ return this.sendText(chatId, text);
171
+ },
172
+ // ---- 群聊管理 ----
173
+ async createGroup(_name, _userIds) {
174
+ throw new Error("微信不支持创建群聊");
175
+ },
176
+ async updateChatInfo(_chatId, _name, _description) {
177
+ // 微信不支持修改群信息,静默成功
178
+ },
179
+ async getChatInfo(_chatId) {
180
+ return { name: "微信会话", description: "" };
181
+ },
182
+ async disbandChat(_chatId) {
183
+ // 微信不支持解散群聊
184
+ },
185
+ async setChatAvatar(_chatId, _tool, _status) {
186
+ // 微信不支持设置头像
187
+ },
188
+ extractSessionInfo(_description) {
189
+ // 微信没有群描述机制,session 绑定走 session-registry
190
+ return null;
191
+ },
192
+ // ---- 进度展示(微信无卡片,降级为文本) ----
193
+ async cardCreate(cardJson) {
194
+ const text = cardJsonToPlainText(cardJson);
195
+ if (!text) {
196
+ log(`[WECHAT] cardCreate text fallback failed: empty card text`);
197
+ return null;
198
+ }
199
+ const cardId = `wechat-text-card-${Date.now()}-${++textCardSeq}`;
200
+ textCardMap.set(cardId, {
201
+ text,
202
+ lastSentText: "",
203
+ lastSentAt: 0,
204
+ });
205
+ log(`[WECHAT] cardCreate degraded to text card: ${cardId}`);
206
+ return cardId;
207
+ },
208
+ async cardSend(chatId, cardId) {
209
+ const entry = textCardMap.get(cardId);
210
+ if (!entry) {
211
+ log(`[WECHAT] cardSend text fallback failed: missing card ${cardId}`);
212
+ return "";
213
+ }
214
+ entry.chatId = chatId;
215
+ const compressed = compressGeneratingText(entry.text);
216
+ const ok = await this.sendText(chatId, compressed);
217
+ if (!ok)
218
+ return "";
219
+ entry.lastSentText = entry.text;
220
+ entry.lastSentAt = Date.now();
221
+ log(`[WECHAT] cardSend degraded to text: ${cardId}`);
222
+ return `wechat-text-message-${cardId}`;
223
+ },
224
+ async cardUpdate(cardId, cardJson, _sequence) {
225
+ const entry = textCardMap.get(cardId);
226
+ if (!entry || !entry.chatId) {
227
+ log(`[WECHAT] cardUpdate text fallback skipped: missing sent card ${cardId}`);
228
+ return;
229
+ }
230
+ const text = cardJsonToPlainText(cardJson);
231
+ if (!text || text === entry.lastSentText)
232
+ return;
233
+ const now = Date.now();
234
+ const terminal = isTerminalCardText(text);
235
+ if (!terminal && now - entry.lastSentAt < TEXT_CARD_UPDATE_INTERVAL_MS) {
236
+ entry.text = text;
237
+ return;
238
+ }
239
+ // 非终端卡片(生成中):只发送新增部分(delta),不重发已发送的内容
240
+ let sendText;
241
+ if (!terminal && entry.lastSentText && text.startsWith(entry.lastSentText)) {
242
+ sendText = text.slice(entry.lastSentText.length).trim();
243
+ if (!sendText) {
244
+ entry.text = text;
245
+ return;
246
+ }
247
+ }
248
+ else {
249
+ sendText = text;
250
+ }
251
+ entry.text = text;
252
+ const compressed = compressGeneratingText(sendText);
253
+ const ok = await this.sendText(entry.chatId, compressed);
254
+ if (!ok)
255
+ return;
256
+ entry.lastSentText = text;
257
+ entry.lastSentAt = now;
258
+ if (terminal)
259
+ textCardMap.delete(cardId);
260
+ log(`[WECHAT] cardUpdate degraded to text: ${cardId}`);
261
+ },
262
+ };
263
+ }
264
+ /**
265
+ * 启动微信 iLink 平台。
266
+ *
267
+ * 若 reuseTokenOnStart 为 true 且上次保存的 token 仍然有效,则跳过 QR 直接复用;
268
+ * 否则显示 QR 码等待扫码登录。
269
+ * 接收到的消息统一交给 handler 处理(即 orchestrator.handleCommand)。
270
+ *
271
+ * 返回永不 resolve(除非主动停止或会话过期),由调用方 supervisor 管理。
272
+ */
273
+ export async function startWechatPlatform(handler, signal, reuseTokenOnStart) {
274
+ platformLog = (msg) => {
275
+ console.log(`[WX:${new Date().toISOString().slice(0, 19).replace("T", " ")}] ${msg}`);
276
+ };
277
+ platformLog(`日志文件: ${WECHAT_LOG_PATH}`);
278
+ const saved = readSnapshot();
279
+ platformLog(`上次登录: ${saved.lastSeenAt ?? "无记录"}`);
280
+ // 尝试复用已保存的 token
281
+ if (reuseTokenOnStart && saved.token && saved.baseUrl) {
282
+ platformLog("检测到已保存的 token,尝试复用...");
283
+ ilinkWire = new OpenIlinkWire(saved.token, {
284
+ base_url: saved.baseUrl,
285
+ });
286
+ try {
287
+ const probeResp = await ilinkWire.getUpdates(saved.pollCursor ?? "", 5000);
288
+ if (probeResp.ret === 0 || probeResp.errcode === 0 || probeResp.msgs !== undefined) {
289
+ platformLog("token 有效,跳过扫码。");
290
+ updateSnapshot({ lastSeenAt: new Date().toISOString() });
291
+ const latest = readSnapshot();
292
+ // 恢复 context token 到内存
293
+ if (latest.contextToken && latest.lastChatId) {
294
+ contextTokenMap.set(latest.lastChatId, latest.contextToken);
295
+ }
296
+ await ilinkWire.monitor(async (message) => {
297
+ try {
298
+ await handleWechatMessage(message, handler);
299
+ }
300
+ catch (error) {
301
+ platformLog(`消息处理失败: ${error.stack ?? error.message}`);
302
+ }
303
+ }, {
304
+ initial_buf: probeResp.sync_buf ?? probeResp.get_updates_buf ?? latest.pollCursor ?? "",
305
+ on_buf_update: (pollCursor) => updateSnapshot({ pollCursor }),
306
+ on_response: (response) => {
307
+ const pollCursor = response.sync_buf ?? response.get_updates_buf;
308
+ if (pollCursor) {
309
+ updateSnapshot({ pollCursor });
310
+ }
311
+ },
312
+ on_error: (error) => {
313
+ platformLog(`iLink 监听错误: ${error.stack ?? error.message}`);
314
+ },
315
+ on_session_expired: () => {
316
+ platformLog("微信 iLink 会话已过期,需要重新扫码登录。");
317
+ signal.stopped = true;
318
+ },
319
+ should_continue: () => !signal.stopped,
320
+ });
321
+ return;
322
+ }
323
+ platformLog(`token 验证失败 (ret=${probeResp.ret} errcode=${probeResp.errcode}),回退到扫码登录。`);
324
+ }
325
+ catch (err) {
326
+ platformLog(`token 探测失败: ${err.message},回退到扫码登录。`);
327
+ }
328
+ }
329
+ // 请求新 QR(5 分钟内未扫码则放弃本轮,由 supervisor 稍后重试)
330
+ const QR_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
331
+ platformLog("启动微信 iLink QR 登录(5 分钟内未扫码将自动重试)...");
332
+ ilinkWire = new OpenIlinkWire("", {
333
+ base_url: saved.baseUrl,
334
+ });
335
+ let qrExpiredCount = 0;
336
+ const loginResult = await Promise.race([
337
+ ilinkWire.loginWithQr({
338
+ on_qrcode: (content) => {
339
+ printScanMaterial(content);
340
+ },
341
+ on_scanned: () => {
342
+ platformLog("QR 已扫描,请在微信中确认登录。");
343
+ },
344
+ on_expired: (attempt, maxAttempts) => {
345
+ qrExpiredCount++;
346
+ platformLog(`QR 已过期,正在刷新 (${attempt}/${maxAttempts})。`);
347
+ },
348
+ }),
349
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`QR 登录超时(${QR_LOGIN_TIMEOUT_MS / 60000} 分钟内未扫码,已刷新 ${qrExpiredCount} 次)`)), QR_LOGIN_TIMEOUT_MS)),
350
+ ]);
351
+ if (!loginResult.connected) {
352
+ throw new Error(`微信 iLink QR 登录失败: ${loginResult.message}`);
353
+ }
354
+ updateSnapshot({
355
+ token: loginResult.bot_token ?? ilinkWire.token,
356
+ baseUrl: loginResult.base_url ?? ilinkWire.baseUrl,
357
+ botId: loginResult.bot_id,
358
+ userId: loginResult.user_id,
359
+ pollCursor: "",
360
+ });
361
+ platformLog(`微信 iLink 登录成功。BotID=${loginResult.bot_id ?? ""} UserID=${loginResult.user_id ?? ""}`);
362
+ // 监听消息
363
+ const latest = readSnapshot();
364
+ // 恢复 context token 到内存
365
+ if (latest.contextToken && latest.lastChatId) {
366
+ contextTokenMap.set(latest.lastChatId, latest.contextToken);
367
+ }
368
+ await ilinkWire.monitor(async (message) => {
369
+ try {
370
+ await handleWechatMessage(message, handler);
371
+ }
372
+ catch (error) {
373
+ platformLog(`消息处理失败: ${error.stack ?? error.message}`);
374
+ }
375
+ }, {
376
+ initial_buf: latest.pollCursor ?? "",
377
+ on_buf_update: (pollCursor) => updateSnapshot({ pollCursor }),
378
+ on_response: (response) => {
379
+ const pollCursor = response.sync_buf ?? response.get_updates_buf;
380
+ if (pollCursor) {
381
+ updateSnapshot({ pollCursor });
382
+ }
383
+ },
384
+ on_error: (error) => {
385
+ platformLog(`iLink 监听错误: ${error.stack ?? error.message}`);
386
+ },
387
+ on_session_expired: () => {
388
+ platformLog("微信 iLink 会话已过期,需要重新扫码登录。");
389
+ signal.stopped = true;
390
+ },
391
+ should_continue: () => !signal.stopped,
392
+ });
393
+ }
394
+ const WECHAT_IMAGE_DOWNLOAD_DIR = join(homedir(), ".chatccc", "images", "downloads");
395
+ function extFromMimeOrName(mime, fileName) {
396
+ if (mime) {
397
+ const map = {
398
+ "image/png": ".png",
399
+ "image/jpeg": ".jpg",
400
+ "image/webp": ".webp",
401
+ "image/gif": ".gif",
402
+ "image/bmp": ".bmp",
403
+ "image/svg+xml": ".svg",
404
+ };
405
+ const key = mime.split(";")[0].trim().toLowerCase();
406
+ if (map[key])
407
+ return map[key];
408
+ }
409
+ if (fileName) {
410
+ const ext = extname(fileName).toLowerCase();
411
+ if (ext)
412
+ return ext;
413
+ }
414
+ return ".png";
415
+ }
416
+ async function downloadWechatImage(imageItem, msgId) {
417
+ const wire = ilinkWire;
418
+ if (!wire)
419
+ throw new Error("iLink wire not available");
420
+ if (!imageItem.media)
421
+ throw new Error("image item has no media");
422
+ const data = await wire.downloadMedia(imageItem.media);
423
+ const mime = imageItem.mime_type;
424
+ const ext = extFromMimeOrName(mime);
425
+ const key = imageItem.media.aes_key?.slice(0, 16) ?? (msgId?.toString() ?? Date.now().toString());
426
+ await mkdirSync(WECHAT_IMAGE_DOWNLOAD_DIR, { recursive: true });
427
+ const localPath = join(WECHAT_IMAGE_DOWNLOAD_DIR, `wx_${key}${ext}`);
428
+ writeFileSync(localPath, data);
429
+ platformLog(`图片已下载: ${localPath}`);
430
+ return localPath;
431
+ }
432
+ async function downloadWechatFile(fileItem, msgId) {
433
+ const wire = ilinkWire;
434
+ if (!wire)
435
+ throw new Error("iLink wire not available");
436
+ if (!fileItem.media)
437
+ throw new Error("file item has no media");
438
+ const data = await wire.downloadMedia(fileItem.media);
439
+ const fileName = fileItem.file_name || "file";
440
+ const ext = extname(fileName).toLowerCase() || extFromMimeOrName(undefined);
441
+ const key = fileItem.media.aes_key?.slice(0, 16) ?? (msgId?.toString() ?? Date.now().toString());
442
+ await mkdirSync(WECHAT_IMAGE_DOWNLOAD_DIR, { recursive: true });
443
+ const localPath = join(WECHAT_IMAGE_DOWNLOAD_DIR, `wx_${key}_${fileName}`);
444
+ writeFileSync(localPath, data);
445
+ platformLog(`文件已下载: ${localPath}`);
446
+ return localPath;
447
+ }
448
+ async function downloadWechatVideo(videoItem, msgId) {
449
+ const wire = ilinkWire;
450
+ if (!wire)
451
+ throw new Error("iLink wire not available");
452
+ if (!videoItem.media)
453
+ throw new Error("video item has no media");
454
+ const data = await wire.downloadMedia(videoItem.media);
455
+ const ext = extFromMimeOrName(undefined);
456
+ const key = videoItem.media.aes_key?.slice(0, 16) ?? (msgId?.toString() ?? Date.now().toString());
457
+ await mkdirSync(WECHAT_IMAGE_DOWNLOAD_DIR, { recursive: true });
458
+ const localPath = join(WECHAT_IMAGE_DOWNLOAD_DIR, `wx_${key}${ext}`);
459
+ writeFileSync(localPath, data);
460
+ platformLog(`视频已下载: ${localPath}`);
461
+ return localPath;
462
+ }
463
+ async function handleWechatMessage(message, handler) {
464
+ const chatId = String(message.from_user_id ?? "");
465
+ if (!chatId) {
466
+ platformLog("跳过无 from_user_id 的消息");
467
+ return;
468
+ }
469
+ // 保存 lastChatId 供下次启动时发送通知
470
+ const current = readSnapshot();
471
+ if (current.lastChatId !== chatId) {
472
+ updateSnapshot({ lastChatId: chatId });
473
+ }
474
+ // 保存 context_token 到 snapshot(供重启后启动通知使用)和内存
475
+ if (message.context_token) {
476
+ contextTokenMap.set(chatId, message.context_token);
477
+ updateSnapshot({ contextToken: message.context_token, lastChatId: chatId });
478
+ }
479
+ const text = extractText(message).trim();
480
+ const msgTimestamp = message.create_time_ms ?? Date.now();
481
+ // 检测并下载图片/文件/视频
482
+ const imagePaths = [];
483
+ const filePaths = [];
484
+ const videoPaths = [];
485
+ const items = message.item_list;
486
+ if (items) {
487
+ for (const item of items) {
488
+ if (item.image_item?.media) {
489
+ try {
490
+ const localPath = await downloadWechatImage(item.image_item, message.message_id);
491
+ imagePaths.push(localPath);
492
+ }
493
+ catch (err) {
494
+ platformLog(`图片下载失败: ${err.message}`);
495
+ }
496
+ }
497
+ if (item.file_item?.media) {
498
+ try {
499
+ const localPath = await downloadWechatFile(item.file_item, message.message_id);
500
+ filePaths.push(localPath);
501
+ }
502
+ catch (err) {
503
+ platformLog(`文件下载失败: ${err.message}`);
504
+ }
505
+ }
506
+ if (item.video_item?.media) {
507
+ try {
508
+ const localPath = await downloadWechatVideo(item.video_item, message.message_id);
509
+ videoPaths.push(localPath);
510
+ }
511
+ catch (err) {
512
+ platformLog(`视频下载失败: ${err.message}`);
513
+ }
514
+ }
515
+ }
516
+ }
517
+ // 构建消息文本:文本内容 + 图片/文件/视频路径
518
+ let fullText = text;
519
+ if (imagePaths.length > 0) {
520
+ const imageLines = imagePaths.map((p) => `[图片] ${p}`).join("\n");
521
+ fullText = fullText ? `${fullText}\n${imageLines}` : imageLines;
522
+ }
523
+ if (filePaths.length > 0) {
524
+ const fileLines = filePaths.map((p) => `[文件] ${p}`).join("\n");
525
+ fullText = fullText ? `${fullText}\n${fileLines}` : fileLines;
526
+ }
527
+ if (videoPaths.length > 0) {
528
+ const videoLines = videoPaths.map((p) => `[视频] ${p}`).join("\n");
529
+ fullText = fullText ? `${fullText}\n${videoLines}` : videoLines;
530
+ }
531
+ // 纯图片且无文字时跳过(避免空消息触发会话)
532
+ if (!fullText.trim()) {
533
+ platformLog(`跳过纯媒体消息(无文本): chatId=${chatId}`);
534
+ return;
535
+ }
536
+ platformLog(`收到消息: chatId=${chatId} text="${text.slice(0, 80)}" images=${imagePaths.length} files=${filePaths.length} videos=${videoPaths.length}`);
537
+ appendChatLog(chatId, chatId, fullText);
538
+ // 用户回复,重置 claw 连发计数
539
+ consecutiveSendCount.set(chatId, 0);
540
+ // WeChat 中所有会话都视为 p2p,/new 复用 p2p 路径(等同飞书 /newh 效果)
541
+ // 不 await:避免长 prompt 阻塞后续消息处理(如 /cd、/stop 等命令)
542
+ handler(fullText, chatId, chatId, msgTimestamp, "p2p").catch((err) => {
543
+ platformLog(`消息处理失败: ${err.stack ?? err.message}`);
544
+ });
545
+ }