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