chatccc 0.2.252 → 0.2.254

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 (296) hide show
  1. package/README.md +10 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/config.sample.json +2 -1
  5. package/deepccc-agent/README.md +40 -35
  6. package/deepccc-agent/os-prompts/darwin.md +8 -8
  7. package/deepccc-agent/os-prompts/linux.md +8 -8
  8. package/deepccc-agent/os-prompts/win32.md +11 -11
  9. package/deepccc-agent/package.json +65 -65
  10. package/dist/deepccc-agent/src/cli.js +634 -0
  11. package/dist/deepccc-agent/src/config.js +81 -0
  12. package/dist/deepccc-agent/src/context.js +348 -0
  13. package/dist/deepccc-agent/src/file-log.js +34 -0
  14. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  15. package/dist/deepccc-agent/src/index.js +601 -0
  16. package/dist/deepccc-agent/src/permissions.js +170 -0
  17. package/dist/deepccc-agent/src/privacy.js +124 -0
  18. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  19. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  20. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  21. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  22. package/dist/deepccc-agent/src/progress/view.js +30 -0
  23. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  24. package/dist/deepccc-agent/src/session-search.js +276 -0
  25. package/dist/deepccc-agent/src/session-select.js +23 -0
  26. package/dist/deepccc-agent/src/sigint.js +26 -0
  27. package/dist/deepccc-agent/src/skills.js +178 -0
  28. package/dist/deepccc-agent/src/web-tools.js +246 -0
  29. package/dist/src/adapters/adapter-interface.js +19 -0
  30. package/dist/src/adapters/ccc-adapter.js +112 -0
  31. package/dist/src/adapters/claude-adapter.js +497 -0
  32. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  33. package/dist/src/adapters/codex-adapter.js +279 -0
  34. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  35. package/dist/src/adapters/cursor-adapter.js +491 -0
  36. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  37. package/dist/src/adapters/jsonl-stream.js +104 -0
  38. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  39. package/dist/src/adapters/raw-stream-log.js +106 -0
  40. package/dist/src/adapters/resource-monitor.js +113 -0
  41. package/dist/src/agent-activity.js +133 -0
  42. package/dist/src/agent-delegate-task-rpc.js +129 -0
  43. package/dist/src/agent-delegate-task.js +48 -0
  44. package/dist/src/agent-file-rpc.js +152 -0
  45. package/dist/src/agent-image-rpc.js +148 -0
  46. package/dist/src/agent-platform-routing.js +13 -0
  47. package/dist/src/agent-reload-config-rpc.js +23 -0
  48. package/dist/src/agent-rpc-body.js +87 -0
  49. package/dist/src/agent-stop-stuck.js +110 -0
  50. package/dist/src/agent-team/application/board-service.js +142 -0
  51. package/dist/src/agent-team/application/main-agent-service.js +151 -0
  52. package/dist/src/agent-team/domain/board.js +69 -0
  53. package/dist/src/agent-team/http/board-routes.js +213 -0
  54. package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
  55. package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
  56. package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
  57. package/dist/src/agent-team/repositories/board-repository.js +10 -0
  58. package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
  59. package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
  60. package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
  61. package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
  62. package/dist/src/agent-team/web/agent-team-page.js +219 -0
  63. package/dist/src/agent-tool.js +10 -0
  64. package/dist/src/card-action-routing.js +7 -0
  65. package/dist/src/card-plain-text.js +101 -0
  66. package/dist/src/cardkit.js +158 -0
  67. package/dist/src/cards.js +573 -0
  68. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  69. package/dist/src/chatgpt-subscription.js +199 -0
  70. package/dist/src/chrome-devtools-guard.js +238 -0
  71. package/dist/src/claude-sdk-installer.js +249 -0
  72. package/dist/src/codex-reset-actions.js +143 -0
  73. package/dist/src/config-utils.js +149 -0
  74. package/dist/src/config.js +808 -0
  75. package/dist/src/cursor-usage.js +77 -0
  76. package/dist/src/exit-banner.js +28 -0
  77. package/dist/src/feishu-api.js +1404 -0
  78. package/dist/src/feishu-message-ingress.js +137 -0
  79. package/dist/src/feishu-platform.js +97 -0
  80. package/dist/src/format-message.js +252 -0
  81. package/dist/src/git-command.js +155 -0
  82. package/dist/src/im-skills.js +121 -0
  83. package/dist/src/index.js +835 -0
  84. package/dist/src/litellm-proxy.js +300 -0
  85. package/dist/src/orchestrator.js +2096 -0
  86. package/dist/src/package-root.js +26 -0
  87. package/dist/src/platform-adapter.js +7 -0
  88. package/dist/src/platform-startup.js +6 -0
  89. package/dist/src/privacy.js +100 -0
  90. package/dist/src/progress/reducer.js +102 -0
  91. package/dist/src/progress/terminal-renderer.js +264 -0
  92. package/dist/src/progress/view.js +30 -0
  93. package/dist/src/response-stall.js +14 -0
  94. package/dist/src/runtime-entry.js +13 -0
  95. package/dist/src/runtime-reload.js +19 -0
  96. package/dist/src/session-chat-binding.js +183 -0
  97. package/dist/src/session-name.js +7 -0
  98. package/dist/src/session.js +2147 -0
  99. package/dist/src/shared-prefix.js +16 -0
  100. package/dist/src/shared.js +493 -0
  101. package/dist/src/sim-agent.js +105 -0
  102. package/dist/src/sim-platform.js +142 -0
  103. package/dist/src/sim-store.js +231 -0
  104. package/dist/src/simplify.js +99 -0
  105. package/dist/src/startup-lifecycle.js +209 -0
  106. package/dist/src/stream-state.js +141 -0
  107. package/dist/src/terminal-error.js +100 -0
  108. package/dist/src/trace.js +50 -0
  109. package/dist/src/turn-cards.js +92 -0
  110. package/dist/src/update-command-guard.js +114 -0
  111. package/{src/web-ui.ts → dist/src/web-ui.js} +753 -839
  112. package/dist/src/wechat-platform.js +545 -0
  113. package/package.json +76 -74
  114. package/deepccc-agent/LICENSE +0 -201
  115. package/deepccc-agent/bin/deepccc.mjs +0 -26
  116. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  117. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  118. package/deepccc-agent/package-lock.json +0 -2027
  119. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  120. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  121. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  122. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  123. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  124. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  125. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  126. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  127. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  128. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  129. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  130. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  131. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  132. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  133. package/deepccc-agent/src/cli.ts +0 -682
  134. package/deepccc-agent/src/config.ts +0 -101
  135. package/deepccc-agent/src/context.ts +0 -465
  136. package/deepccc-agent/src/file-log.ts +0 -38
  137. package/deepccc-agent/src/file-tools.ts +0 -1493
  138. package/deepccc-agent/src/index.ts +0 -710
  139. package/deepccc-agent/src/permissions.ts +0 -226
  140. package/deepccc-agent/src/privacy.ts +0 -141
  141. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  142. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  143. package/deepccc-agent/src/progress/reducer.ts +0 -113
  144. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  145. package/deepccc-agent/src/progress/view.ts +0 -77
  146. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  147. package/deepccc-agent/src/session-search.ts +0 -370
  148. package/deepccc-agent/src/session-select.ts +0 -48
  149. package/deepccc-agent/src/sigint.ts +0 -50
  150. package/deepccc-agent/src/skills.ts +0 -205
  151. package/deepccc-agent/src/web-tools.ts +0 -313
  152. package/deepccc-agent/tsconfig.build.json +0 -13
  153. package/deepccc-agent/tsconfig.json +0 -13
  154. package/deepccc-agent/vitest.config.ts +0 -7
  155. package/src/__tests__/adapter-interface.test.ts +0 -152
  156. package/src/__tests__/agent-activity.test.ts +0 -86
  157. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  158. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  159. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  160. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  161. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  162. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  163. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  164. package/src/__tests__/builtin-config.test.ts +0 -26
  165. package/src/__tests__/builtin-context.test.ts +0 -319
  166. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  167. package/src/__tests__/builtin-permissions.test.ts +0 -219
  168. package/src/__tests__/builtin-session-search.test.ts +0 -262
  169. package/src/__tests__/builtin-session-select.test.ts +0 -116
  170. package/src/__tests__/builtin-sigint.test.ts +0 -56
  171. package/src/__tests__/builtin-skills.test.ts +0 -284
  172. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  173. package/src/__tests__/card-action-routing.test.ts +0 -18
  174. package/src/__tests__/card-plain-text.test.ts +0 -45
  175. package/src/__tests__/cardkit.test.ts +0 -60
  176. package/src/__tests__/cards.test.ts +0 -607
  177. package/src/__tests__/ccc-adapter.test.ts +0 -194
  178. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  179. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  180. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  181. package/src/__tests__/claude-adapter.test.ts +0 -614
  182. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  183. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  184. package/src/__tests__/codex-adapter.test.ts +0 -331
  185. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  186. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  187. package/src/__tests__/config-reload.test.ts +0 -284
  188. package/src/__tests__/config-sample.test.ts +0 -97
  189. package/src/__tests__/config-utils.test.ts +0 -40
  190. package/src/__tests__/config.test.ts +0 -395
  191. package/src/__tests__/crash-logging.test.ts +0 -360
  192. package/src/__tests__/cursor-adapter.test.ts +0 -890
  193. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  194. package/src/__tests__/feishu-api.test.ts +0 -60
  195. package/src/__tests__/feishu-avatar.test.ts +0 -504
  196. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  197. package/src/__tests__/feishu-platform.test.ts +0 -75
  198. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  199. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  200. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  201. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  202. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  203. package/src/__tests__/format-message.test.ts +0 -316
  204. package/src/__tests__/git-command.test.ts +0 -288
  205. package/src/__tests__/im-skills.test.ts +0 -125
  206. package/src/__tests__/jsonl-stream.test.ts +0 -79
  207. package/src/__tests__/orchestrator.test.ts +0 -1268
  208. package/src/__tests__/package-files.test.ts +0 -24
  209. package/src/__tests__/platform-startup.test.ts +0 -19
  210. package/src/__tests__/privacy.test.ts +0 -198
  211. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  212. package/src/__tests__/progress-reducer.test.ts +0 -121
  213. package/src/__tests__/raw-stream-log.test.ts +0 -106
  214. package/src/__tests__/response-stall.test.ts +0 -49
  215. package/src/__tests__/restart.test.ts +0 -232
  216. package/src/__tests__/session-ccc-config.test.ts +0 -66
  217. package/src/__tests__/session.test.ts +0 -3004
  218. package/src/__tests__/shared-prefix.test.ts +0 -36
  219. package/src/__tests__/sim-agent.test.ts +0 -174
  220. package/src/__tests__/sim-platform.test.ts +0 -93
  221. package/src/__tests__/sim-store.test.ts +0 -214
  222. package/src/__tests__/simplify.test.ts +0 -283
  223. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  224. package/src/__tests__/stop-session.test.ts +0 -162
  225. package/src/__tests__/stream-state.test.ts +0 -164
  226. package/src/__tests__/terminal-error.test.ts +0 -54
  227. package/src/__tests__/terminal-renderer.test.ts +0 -247
  228. package/src/__tests__/update-command-guard.test.ts +0 -144
  229. package/src/__tests__/web-ui.test.ts +0 -438
  230. package/src/__tests__/wechat-platform.test.ts +0 -111
  231. package/src/adapters/adapter-interface.ts +0 -217
  232. package/src/adapters/ccc-adapter.ts +0 -150
  233. package/src/adapters/claude-adapter.ts +0 -673
  234. package/src/adapters/claude-session-meta-store.ts +0 -120
  235. package/src/adapters/codex-adapter.ts +0 -426
  236. package/src/adapters/codex-session-meta-store.ts +0 -131
  237. package/src/adapters/cursor-adapter.ts +0 -681
  238. package/src/adapters/cursor-session-meta-store.ts +0 -154
  239. package/src/adapters/jsonl-stream.ts +0 -157
  240. package/src/adapters/raw-stream-log.ts +0 -124
  241. package/src/adapters/resource-monitor.ts +0 -141
  242. package/src/agent-activity.ts +0 -175
  243. package/src/agent-delegate-task-rpc.ts +0 -153
  244. package/src/agent-delegate-task.ts +0 -91
  245. package/src/agent-file-rpc.ts +0 -172
  246. package/src/agent-image-rpc.ts +0 -168
  247. package/src/agent-platform-routing.ts +0 -28
  248. package/src/agent-reload-config-rpc.ts +0 -34
  249. package/src/agent-rpc-body.ts +0 -92
  250. package/src/agent-stop-stuck.ts +0 -129
  251. package/src/card-action-routing.ts +0 -14
  252. package/src/card-plain-text.ts +0 -108
  253. package/src/cardkit.ts +0 -179
  254. package/src/cards.ts +0 -684
  255. package/src/chatgpt-subscription-rpc.ts +0 -27
  256. package/src/chatgpt-subscription.ts +0 -299
  257. package/src/chrome-devtools-guard.ts +0 -318
  258. package/src/claude-sdk-installer.ts +0 -324
  259. package/src/codex-reset-actions.ts +0 -184
  260. package/src/config-utils.ts +0 -211
  261. package/src/config.ts +0 -1063
  262. package/src/cursor-usage.ts +0 -128
  263. package/src/exit-banner.ts +0 -33
  264. package/src/feishu-api.ts +0 -1616
  265. package/src/feishu-message-ingress.ts +0 -195
  266. package/src/feishu-platform.ts +0 -159
  267. package/src/format-message.ts +0 -293
  268. package/src/git-command.ts +0 -202
  269. package/src/im-skills.ts +0 -149
  270. package/src/index.ts +0 -1089
  271. package/src/litellm-proxy.ts +0 -374
  272. package/src/orchestrator.ts +0 -2543
  273. package/src/platform-adapter.ts +0 -70
  274. package/src/platform-startup.ts +0 -16
  275. package/src/privacy.ts +0 -118
  276. package/src/progress/reducer.ts +0 -113
  277. package/src/progress/terminal-renderer.ts +0 -294
  278. package/src/progress/view.ts +0 -77
  279. package/src/response-stall.ts +0 -28
  280. package/src/runtime-reload.ts +0 -34
  281. package/src/session-chat-binding.ts +0 -292
  282. package/src/session-name.ts +0 -8
  283. package/src/session.ts +0 -2659
  284. package/src/shared-prefix.ts +0 -29
  285. package/src/shared.ts +0 -552
  286. package/src/sim-agent.ts +0 -167
  287. package/src/sim-platform.ts +0 -177
  288. package/src/sim-store.ts +0 -317
  289. package/src/simplify.ts +0 -120
  290. package/src/startup-lifecycle.ts +0 -250
  291. package/src/stream-state.ts +0 -177
  292. package/src/terminal-error.ts +0 -129
  293. package/src/trace.ts +0 -51
  294. package/src/turn-cards.ts +0 -118
  295. package/src/update-command-guard.ts +0 -165
  296. 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
+ }