cli-claw-kit 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +245 -0
- package/config/default-groups.json +1 -0
- package/config/global-agents-md.template.md +37 -0
- package/config/mount-allowlist.json +11 -0
- package/container/Dockerfile +160 -0
- package/container/agent-runner/dist/.tsbuildinfo +1 -0
- package/container/agent-runner/dist/agent-definitions.js +22 -0
- package/container/agent-runner/dist/channel-prefixes.js +16 -0
- package/container/agent-runner/dist/codex-config.js +29 -0
- package/container/agent-runner/dist/image-detector.js +96 -0
- package/container/agent-runner/dist/index.js +2587 -0
- package/container/agent-runner/dist/mcp-tools.js +1076 -0
- package/container/agent-runner/dist/stream-event.types.js +5 -0
- package/container/agent-runner/dist/stream-processor.js +867 -0
- package/container/agent-runner/dist/types.js +6 -0
- package/container/agent-runner/dist/utils.js +115 -0
- package/container/agent-runner/package.json +36 -0
- package/container/agent-runner/prompts/security-rules.md +31 -0
- package/container/agent-runner/src/agent-definitions.ts +27 -0
- package/container/agent-runner/src/channel-prefixes.ts +16 -0
- package/container/agent-runner/src/codex-config.ts +40 -0
- package/container/agent-runner/src/image-detector.ts +116 -0
- package/container/agent-runner/src/index.ts +3107 -0
- package/container/agent-runner/src/mcp-tools.ts +1295 -0
- package/container/agent-runner/src/stream-event.types.ts +10 -0
- package/container/agent-runner/src/stream-processor.ts +932 -0
- package/container/agent-runner/src/types.ts +75 -0
- package/container/agent-runner/src/utils.ts +114 -0
- package/container/agent-runner/tsconfig.json +17 -0
- package/container/build.sh +28 -0
- package/container/entrypoint.sh +64 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/install-skill/SKILL.md +64 -0
- package/container/skills/post-test-cleanup/SKILL.md +121 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/agent-output-parser.js +459 -0
- package/dist/app-root.js +52 -0
- package/dist/assistant-meta-footer.js +1 -0
- package/dist/auth.js +91 -0
- package/dist/billing.js +694 -0
- package/dist/channel-prefixes.js +16 -0
- package/dist/cli.js +86 -0
- package/dist/commands.js +79 -0
- package/dist/config.js +120 -0
- package/dist/container-runner.js +981 -0
- package/dist/daily-summary.js +210 -0
- package/dist/db.js +3683 -0
- package/dist/dingtalk.js +1347 -0
- package/dist/feishu-markdown-style.js +97 -0
- package/dist/feishu-streaming-card.js +1875 -0
- package/dist/feishu.js +1628 -0
- package/dist/file-manager.js +270 -0
- package/dist/group-queue.js +1070 -0
- package/dist/group-runtime.js +35 -0
- package/dist/host-workspace-cwd.js +85 -0
- package/dist/im-channel.js +384 -0
- package/dist/im-command-utils.js +142 -0
- package/dist/im-downloader.js +45 -0
- package/dist/im-manager.js +527 -0
- package/dist/im-utils.js +53 -0
- package/dist/image-detector.js +96 -0
- package/dist/index.js +5828 -0
- package/dist/logger.js +22 -0
- package/dist/mcp-utils.js +66 -0
- package/dist/message-attachments.js +69 -0
- package/dist/message-notifier.js +36 -0
- package/dist/middleware/auth.js +85 -0
- package/dist/mount-security.js +315 -0
- package/dist/permissions.js +67 -0
- package/dist/project-memory.js +6 -0
- package/dist/provider-pool.js +189 -0
- package/dist/qq.js +826 -0
- package/dist/reset-admin.js +42 -0
- package/dist/routes/admin.js +543 -0
- package/dist/routes/agent-definitions.js +241 -0
- package/dist/routes/agents.js +533 -0
- package/dist/routes/auth.js +675 -0
- package/dist/routes/billing.js +490 -0
- package/dist/routes/browse.js +210 -0
- package/dist/routes/bug-report.js +387 -0
- package/dist/routes/config.js +1868 -0
- package/dist/routes/files.js +671 -0
- package/dist/routes/groups.js +1367 -0
- package/dist/routes/mcp-servers.js +320 -0
- package/dist/routes/memory.js +523 -0
- package/dist/routes/monitor.js +307 -0
- package/dist/routes/skills.js +777 -0
- package/dist/routes/tasks.js +509 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/workspace-config.js +458 -0
- package/dist/runtime-build.js +112 -0
- package/dist/runtime-command-handler.js +189 -0
- package/dist/runtime-command-registry.js +1 -0
- package/dist/runtime-config.js +1777 -0
- package/dist/runtime-identity.js +52 -0
- package/dist/schemas.js +590 -0
- package/dist/script-runner.js +64 -0
- package/dist/sdk-query.js +82 -0
- package/dist/skill-utils.js +145 -0
- package/dist/sqlite-compat.js +19 -0
- package/dist/stream-event.types.js +5 -0
- package/dist/streaming-runtime-meta.js +29 -0
- package/dist/task-scheduler.js +695 -0
- package/dist/task-utils.js +13 -0
- package/dist/telegram-pairing.js +59 -0
- package/dist/telegram.js +897 -0
- package/dist/terminal-manager.js +307 -0
- package/dist/tool-step-display.js +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +85 -0
- package/dist/web-context.js +161 -0
- package/dist/web.js +1377 -0
- package/dist/wechat-crypto.js +182 -0
- package/dist/wechat.js +589 -0
- package/dist/workspace-runtime-reset.js +35 -0
- package/package.json +107 -0
- package/shared/assistant-meta-footer.ts +127 -0
- package/shared/channel-prefixes.ts +16 -0
- package/shared/dist/assistant-meta-footer.d.ts +29 -0
- package/shared/dist/assistant-meta-footer.js +85 -0
- package/shared/dist/channel-prefixes.d.ts +4 -0
- package/shared/dist/channel-prefixes.js +16 -0
- package/shared/dist/image-detector.d.ts +20 -0
- package/shared/dist/image-detector.js +96 -0
- package/shared/dist/runtime-command-registry.d.ts +38 -0
- package/shared/dist/runtime-command-registry.js +185 -0
- package/shared/dist/stream-event.d.ts +65 -0
- package/shared/dist/stream-event.js +8 -0
- package/shared/dist/tool-step-display.d.ts +4 -0
- package/shared/dist/tool-step-display.js +11 -0
- package/shared/image-detector.ts +116 -0
- package/shared/runtime-command-registry.ts +252 -0
- package/shared/stream-event.ts +67 -0
- package/shared/tool-step-display.ts +21 -0
- package/shared/tsconfig.json +24 -0
- package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
- package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
- package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
- package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
- package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
- package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
- package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
- package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
- package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
- package/web/dist/assets/band-CquvqAHh.js +1 -0
- package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
- package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
- package/web/dist/assets/channel-BOVj73LR.js +1 -0
- package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
- package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
- package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
- package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
- package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
- package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
- package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
- package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
- package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
- package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
- package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
- package/web/dist/assets/clone-BmaCesfa.js +1 -0
- package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
- package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
- package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
- package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
- package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
- package/web/dist/assets/error-CGD5mp5f.js +1 -0
- package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
- package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
- package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
- package/web/dist/assets/graph-CeAEckur.js +1 -0
- package/web/dist/assets/index-CPnL1_qC.js +768 -0
- package/web/dist/assets/index-DVevCbcO.css +10 -0
- package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
- package/web/dist/assets/init-Dmth1JHB.js +1 -0
- package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
- package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
- package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
- package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
- package/web/dist/assets/linear-DiaJloY5.js +1 -0
- package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
- package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
- package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
- package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
- package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
- package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
- package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
- package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
- package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
- package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
- package/web/dist/assets/square-0CqMX1Q3.js +11 -0
- package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
- package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
- package/web/dist/assets/step-D51IIHGA.js +1 -0
- package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
- package/web/dist/assets/time-O8zIGux3.js +1 -0
- package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
- package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
- package/web/dist/assets/utils-KGAn0XTg.js +11 -0
- package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
- package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
- package/web/dist/assets/zap-_hKJYy7J.js +6 -0
- package/web/dist/favicon.svg +332 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin.woff2 +0 -0
- package/web/dist/icons/README.md +20 -0
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-128.png +0 -0
- package/web/dist/icons/icon-144.png +0 -0
- package/web/dist/icons/icon-152.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-192.svg +332 -0
- package/web/dist/icons/icon-384.png +0 -0
- package/web/dist/icons/icon-48.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-512.svg +332 -0
- package/web/dist/icons/icon-72.png +0 -0
- package/web/dist/icons/icon-96.png +0 -0
- package/web/dist/icons/loading-logo.svg +332 -0
- package/web/dist/icons/logo-1024.png +0 -0
- package/web/dist/icons/logo-icon.svg +332 -0
- package/web/dist/icons/logo-text.svg +332 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +1 -0
- package/web/dist/registerSW.js +1 -0
- package/web/dist/sw.js +1 -0
- package/web/dist/workbox-08d6266a.js +1 -0
package/dist/wechat.js
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat iLink Bot API Connection Factory
|
|
3
|
+
*
|
|
4
|
+
* Implements WeChat Bot connection using iLink Bot API protocol:
|
|
5
|
+
* - Long-polling message reception (getupdates)
|
|
6
|
+
* - Message sending with context_token (sendmessage)
|
|
7
|
+
* - Typing indicator (getconfig + sendtyping)
|
|
8
|
+
* - CDN image download + AES decryption
|
|
9
|
+
* - Message deduplication (LRU 1000 / 30min TTL)
|
|
10
|
+
*
|
|
11
|
+
* Base URL: https://ilinkai.weixin.qq.com
|
|
12
|
+
* CDN URL: https://novac2c.cdn.weixin.qq.com/c2c
|
|
13
|
+
*/
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
import { storeChatMetadata, storeMessageDirect, updateChatName } from './db.js';
|
|
16
|
+
import { notifyNewImMessage } from './message-notifier.js';
|
|
17
|
+
import { broadcastNewMessage } from './web.js';
|
|
18
|
+
import { logger } from './logger.js';
|
|
19
|
+
import { saveDownloadedFile, MAX_FILE_SIZE } from './im-downloader.js';
|
|
20
|
+
import { detectImageMimeType } from './image-detector.js';
|
|
21
|
+
import { downloadAndDecryptMedia } from './wechat-crypto.js';
|
|
22
|
+
import { markdownToPlainText, splitTextChunks } from './im-utils.js';
|
|
23
|
+
// ─── Constants ──────────────────────────────────────────────────
|
|
24
|
+
const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
25
|
+
const DEFAULT_CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';
|
|
26
|
+
const MSG_DEDUP_MAX = 1000;
|
|
27
|
+
const MSG_DEDUP_TTL = 30 * 60 * 1000; // 30min
|
|
28
|
+
const MSG_SPLIT_LIMIT = 2000; // WeChat has stricter text limits than other channels
|
|
29
|
+
const LONGPOLL_EXTRA_TIMEOUT_MS = 5000;
|
|
30
|
+
const DEFAULT_LONGPOLL_TIMEOUT_MS = 35000;
|
|
31
|
+
const RECONNECT_MIN_DELAY_MS = 3000;
|
|
32
|
+
const RECONNECT_MAX_DELAY_MS = 60000;
|
|
33
|
+
const IMAGE_MAX_BASE64_SIZE = 5 * 1024 * 1024; // 5 MB for inline base64
|
|
34
|
+
const CHANNEL_VERSION = '0.1.0';
|
|
35
|
+
// iLink message types
|
|
36
|
+
// const MESSAGE_TYPE_USER = 1;
|
|
37
|
+
const MESSAGE_TYPE_BOT = 2;
|
|
38
|
+
// iLink message item types
|
|
39
|
+
const MESSAGE_ITEM_TYPE_TEXT = 1;
|
|
40
|
+
const MESSAGE_ITEM_TYPE_IMAGE = 2;
|
|
41
|
+
// const MESSAGE_ITEM_TYPE_VOICE = 3;
|
|
42
|
+
const MESSAGE_ITEM_TYPE_FILE = 4;
|
|
43
|
+
// const MESSAGE_ITEM_TYPE_VIDEO = 5;
|
|
44
|
+
// iLink message state
|
|
45
|
+
// const MESSAGE_STATE_NEW = 0;
|
|
46
|
+
// const MESSAGE_STATE_GENERATING = 1;
|
|
47
|
+
const MESSAGE_STATE_FINISH = 2;
|
|
48
|
+
// errcode for session expired
|
|
49
|
+
const ERRCODE_SESSION_EXPIRED = -14;
|
|
50
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Generate random X-WECHAT-UIN header value.
|
|
53
|
+
* A random uint32 converted to string, then base64-encoded.
|
|
54
|
+
*/
|
|
55
|
+
function randomWechatUin() {
|
|
56
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
57
|
+
return Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extract text content from message item_list.
|
|
61
|
+
* Includes voice-to-text transcription and fallback labels for non-text items.
|
|
62
|
+
*/
|
|
63
|
+
function extractTextContent(items) {
|
|
64
|
+
const parts = [];
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
if (item.type === MESSAGE_ITEM_TYPE_TEXT && item.text_item?.text) {
|
|
67
|
+
parts.push(item.text_item.text);
|
|
68
|
+
}
|
|
69
|
+
else if (item.type === MESSAGE_ITEM_TYPE_IMAGE) {
|
|
70
|
+
// Image placeholder — actual image is handled separately via CDN download
|
|
71
|
+
// Only add placeholder if no CDN media to download
|
|
72
|
+
if (!item.image_item?.media?.encrypt_query_param) {
|
|
73
|
+
parts.push('(image)');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (item.type === 3 /* VOICE */) {
|
|
77
|
+
// Voice: prefer speech-to-text transcription
|
|
78
|
+
if (item.voice_item?.text) {
|
|
79
|
+
parts.push(item.voice_item.text);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
parts.push('(voice)');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (item.type === MESSAGE_ITEM_TYPE_FILE) {
|
|
86
|
+
parts.push(`(file: ${item.file_item?.file_name ?? 'unknown'})`);
|
|
87
|
+
}
|
|
88
|
+
else if (item.type === 5 /* VIDEO */) {
|
|
89
|
+
parts.push('(video)');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return parts.join('\n').trim();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generate a unique dedup key from a WeixinMessage.
|
|
96
|
+
*/
|
|
97
|
+
function dedupKey(msg) {
|
|
98
|
+
if (msg.message_id !== undefined)
|
|
99
|
+
return `mid:${msg.message_id}`;
|
|
100
|
+
if (msg.seq !== undefined)
|
|
101
|
+
return `seq:${msg.seq}`;
|
|
102
|
+
// Fallback: combination of sender + timestamp + client_id
|
|
103
|
+
return `fallback:${msg.from_user_id}:${msg.create_time_ms}:${msg.client_id}`;
|
|
104
|
+
}
|
|
105
|
+
// ─── Factory Function ───────────────────────────────────────────
|
|
106
|
+
export function createWeChatConnection(config) {
|
|
107
|
+
const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
108
|
+
const cdnBaseUrl = config.cdnBaseUrl || DEFAULT_CDN_BASE_URL;
|
|
109
|
+
// Generate UIN once per connection instance (no need to regenerate per request)
|
|
110
|
+
const wechatUin = randomWechatUin();
|
|
111
|
+
// Polling state
|
|
112
|
+
let currentGetUpdatesBuf = config.getUpdatesBuf || '';
|
|
113
|
+
let longpollTimeoutMs = DEFAULT_LONGPOLL_TIMEOUT_MS;
|
|
114
|
+
let stopping = false;
|
|
115
|
+
let connected = false;
|
|
116
|
+
let cancelSleep = null;
|
|
117
|
+
// context_token cache: from_user_id -> latest context_token
|
|
118
|
+
const contextTokenCache = new Map();
|
|
119
|
+
// Known JIDs — skip redundant storeChatMetadata/onNewChat for repeat messages
|
|
120
|
+
const knownJids = new Set();
|
|
121
|
+
// Message deduplication: key -> timestamp
|
|
122
|
+
const msgCache = new Map();
|
|
123
|
+
// ─── Deduplication ────────────────────────────────────────
|
|
124
|
+
function isDuplicate(key) {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
// Evict expired entries — Map preserves insertion order, so oldest entries
|
|
127
|
+
// come first. Stop at the first non-expired entry for O(expired) instead of O(n).
|
|
128
|
+
for (const [id, ts] of msgCache.entries()) {
|
|
129
|
+
if (now - ts > MSG_DEDUP_TTL) {
|
|
130
|
+
msgCache.delete(id);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Evict oldest if at capacity
|
|
137
|
+
if (msgCache.size >= MSG_DEDUP_MAX) {
|
|
138
|
+
const firstKey = msgCache.keys().next().value;
|
|
139
|
+
if (firstKey)
|
|
140
|
+
msgCache.delete(firstKey);
|
|
141
|
+
}
|
|
142
|
+
return msgCache.has(key);
|
|
143
|
+
}
|
|
144
|
+
function markSeen(key) {
|
|
145
|
+
// delete + set to refresh insertion order (move to end)
|
|
146
|
+
msgCache.delete(key);
|
|
147
|
+
msgCache.set(key, Date.now());
|
|
148
|
+
}
|
|
149
|
+
// ─── HTTP Helpers ─────────────────────────────────────────
|
|
150
|
+
function buildHeaders() {
|
|
151
|
+
return {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
AuthorizationType: 'ilink_bot_token',
|
|
154
|
+
Authorization: `Bearer ${config.botToken}`,
|
|
155
|
+
'X-WECHAT-UIN': wechatUin,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function baseInfo() {
|
|
159
|
+
return { channel_version: CHANNEL_VERSION };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Make an HTTPS POST request to the iLink API using fetch.
|
|
163
|
+
*/
|
|
164
|
+
async function apiPost(endpoint, body, timeoutMs) {
|
|
165
|
+
const bodyStr = JSON.stringify(body);
|
|
166
|
+
const url = new URL(endpoint, baseUrl);
|
|
167
|
+
const headers = buildHeaders();
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
const timer = timeoutMs
|
|
170
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
171
|
+
: undefined;
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(url.toString(), {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
...headers,
|
|
177
|
+
'Content-Length': String(Buffer.byteLength(bodyStr, 'utf-8')),
|
|
178
|
+
},
|
|
179
|
+
body: bodyStr,
|
|
180
|
+
signal: controller.signal,
|
|
181
|
+
});
|
|
182
|
+
const text = await res.text();
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(text);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
throw new Error(`WeChat API ${endpoint} invalid JSON: ${text.slice(0, 200)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
192
|
+
throw new Error(`WeChat API ${endpoint} timed out`);
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
if (timer)
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ─── API Methods ──────────────────────────────────────────
|
|
202
|
+
async function getUpdates() {
|
|
203
|
+
const httpTimeout = longpollTimeoutMs + LONGPOLL_EXTRA_TIMEOUT_MS;
|
|
204
|
+
return apiPost('ilink/bot/getupdates', {
|
|
205
|
+
get_updates_buf: currentGetUpdatesBuf,
|
|
206
|
+
base_info: baseInfo(),
|
|
207
|
+
}, httpTimeout);
|
|
208
|
+
}
|
|
209
|
+
async function sendMessageApi(toUserId, contextToken, text) {
|
|
210
|
+
const clientId = String(crypto.randomBytes(4).readUInt32BE(0));
|
|
211
|
+
const resp = await apiPost('ilink/bot/sendmessage', {
|
|
212
|
+
msg: {
|
|
213
|
+
to_user_id: toUserId,
|
|
214
|
+
context_token: contextToken,
|
|
215
|
+
item_list: [
|
|
216
|
+
{
|
|
217
|
+
type: MESSAGE_ITEM_TYPE_TEXT,
|
|
218
|
+
text_item: { text },
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
message_type: MESSAGE_TYPE_BOT,
|
|
222
|
+
message_state: MESSAGE_STATE_FINISH,
|
|
223
|
+
client_id: clientId,
|
|
224
|
+
},
|
|
225
|
+
base_info: baseInfo(),
|
|
226
|
+
});
|
|
227
|
+
if (resp.ret !== undefined && resp.ret !== 0) {
|
|
228
|
+
throw new Error(`sendMessage failed: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? ''}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function getTypingTicket(ilinkUserId, contextToken) {
|
|
232
|
+
try {
|
|
233
|
+
const res = await apiPost('ilink/bot/getconfig', {
|
|
234
|
+
ilink_user_id: ilinkUserId,
|
|
235
|
+
context_token: contextToken,
|
|
236
|
+
base_info: baseInfo(),
|
|
237
|
+
});
|
|
238
|
+
return res.typing_ticket || null;
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
logger.debug({ err }, 'WeChat getconfig failed');
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function sendTypingApi(ilinkUserId, typingTicket, status) {
|
|
246
|
+
try {
|
|
247
|
+
await apiPost('ilink/bot/sendtyping', {
|
|
248
|
+
ilink_user_id: ilinkUserId,
|
|
249
|
+
typing_ticket: typingTicket,
|
|
250
|
+
status,
|
|
251
|
+
base_info: baseInfo(),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
logger.debug({ err, status }, 'WeChat sendtyping failed');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// ─── Image Handling ───────────────────────────────────────
|
|
259
|
+
async function processImageItem(item, msgIdentifier, groupFolder) {
|
|
260
|
+
const imageItem = item.image_item;
|
|
261
|
+
if (!imageItem)
|
|
262
|
+
return {};
|
|
263
|
+
const media = imageItem.media;
|
|
264
|
+
if (!media?.encrypt_query_param || !media?.aes_key) {
|
|
265
|
+
logger.debug('WeChat image missing media or aes_key, skipping');
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const buffer = await downloadAndDecryptMedia(media.encrypt_query_param, media.aes_key, cdnBaseUrl);
|
|
270
|
+
if (!buffer || buffer.length === 0) {
|
|
271
|
+
logger.warn('WeChat image download returned empty buffer');
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
274
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
275
|
+
logger.warn({ size: buffer.length }, 'WeChat image exceeds max file size, skipping');
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
const mimeType = detectImageMimeType(buffer);
|
|
279
|
+
const extMap = {
|
|
280
|
+
'image/jpeg': '.jpg',
|
|
281
|
+
'image/png': '.png',
|
|
282
|
+
'image/gif': '.gif',
|
|
283
|
+
'image/webp': '.webp',
|
|
284
|
+
};
|
|
285
|
+
const ext = extMap[mimeType] ?? '.jpg';
|
|
286
|
+
const fileName = `wechat_img_${msgIdentifier}${ext}`;
|
|
287
|
+
// Save to workspace
|
|
288
|
+
let textPrefix;
|
|
289
|
+
if (groupFolder) {
|
|
290
|
+
try {
|
|
291
|
+
const relPath = await saveDownloadedFile(groupFolder, 'wechat', fileName, buffer);
|
|
292
|
+
if (relPath)
|
|
293
|
+
textPrefix = `[图片: ${relPath}]`;
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
logger.warn({ err }, 'Failed to save WeChat image to disk');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Inline base64 for small images
|
|
300
|
+
let attachmentEntry;
|
|
301
|
+
if (buffer.length <= IMAGE_MAX_BASE64_SIZE) {
|
|
302
|
+
attachmentEntry = {
|
|
303
|
+
type: 'image',
|
|
304
|
+
data: buffer.toString('base64'),
|
|
305
|
+
mimeType,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return { attachmentEntry, textPrefix };
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
logger.warn({ err }, 'WeChat image download/decrypt failed, skipping');
|
|
312
|
+
return {};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ─── Message Processing ───────────────────────────────────
|
|
316
|
+
async function processMessage(msg, opts) {
|
|
317
|
+
try {
|
|
318
|
+
// Skip bot's own messages
|
|
319
|
+
if (msg.message_type === MESSAGE_TYPE_BOT)
|
|
320
|
+
return;
|
|
321
|
+
const fromUserId = msg.from_user_id;
|
|
322
|
+
if (!fromUserId)
|
|
323
|
+
return;
|
|
324
|
+
// Dedup
|
|
325
|
+
const key = dedupKey(msg);
|
|
326
|
+
if (isDuplicate(key))
|
|
327
|
+
return;
|
|
328
|
+
markSeen(key);
|
|
329
|
+
// Skip stale messages — if no timestamp available, skip as well (can't verify freshness)
|
|
330
|
+
if (opts.ignoreMessagesBefore) {
|
|
331
|
+
if (!msg.create_time_ms ||
|
|
332
|
+
msg.create_time_ms < opts.ignoreMessagesBefore)
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Cache context_token for replies
|
|
336
|
+
if (msg.context_token) {
|
|
337
|
+
contextTokenCache.set(fromUserId, msg.context_token);
|
|
338
|
+
}
|
|
339
|
+
const jid = `wechat:${fromUserId}`;
|
|
340
|
+
const senderName = fromUserId.split('@')[0] || 'WeChat用户';
|
|
341
|
+
const chatName = senderName;
|
|
342
|
+
// Extract text content
|
|
343
|
+
let content = msg.item_list ? extractTextContent(msg.item_list) : '';
|
|
344
|
+
// ── Auto-register chat (WeChat is 1:1 bound, no pairing needed) ──
|
|
345
|
+
const nowIso = new Date().toISOString();
|
|
346
|
+
if (!knownJids.has(jid)) {
|
|
347
|
+
knownJids.add(jid);
|
|
348
|
+
storeChatMetadata(jid, nowIso);
|
|
349
|
+
updateChatName(jid, chatName);
|
|
350
|
+
opts.onNewChat(jid, chatName);
|
|
351
|
+
}
|
|
352
|
+
// Handle slash commands
|
|
353
|
+
const slashMatch = content.match(/^\/(\S+)(?:\s+(.*))?$/i);
|
|
354
|
+
if (slashMatch && opts.onCommand) {
|
|
355
|
+
const cmdBody = (slashMatch[1] + (slashMatch[2] ? ' ' + slashMatch[2] : '')).trim();
|
|
356
|
+
try {
|
|
357
|
+
const reply = await opts.onCommand(jid, cmdBody);
|
|
358
|
+
if (reply) {
|
|
359
|
+
const ct = contextTokenCache.get(fromUserId);
|
|
360
|
+
if (ct) {
|
|
361
|
+
await sendMessageApi(fromUserId, ct, markdownToPlainText(reply));
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
logger.error({ jid, err }, 'WeChat slash command failed');
|
|
368
|
+
const ct = contextTokenCache.get(fromUserId);
|
|
369
|
+
if (ct) {
|
|
370
|
+
await sendMessageApi(fromUserId, ct, '命令执行失败,请稍后重试');
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Handle image attachments
|
|
376
|
+
let attachmentsJson;
|
|
377
|
+
const groupFolder = opts.resolveGroupFolder?.(jid);
|
|
378
|
+
if (msg.item_list) {
|
|
379
|
+
const imageAttachments = [];
|
|
380
|
+
const textPrefixes = [];
|
|
381
|
+
// Download images in parallel (independent CDN requests)
|
|
382
|
+
const msgId = msg.message_id !== undefined
|
|
383
|
+
? String(msg.message_id)
|
|
384
|
+
: String(msg.seq ?? Date.now());
|
|
385
|
+
const imageItems = msg.item_list.filter((item) => item.type === MESSAGE_ITEM_TYPE_IMAGE);
|
|
386
|
+
if (imageItems.length > 0) {
|
|
387
|
+
const results = await Promise.allSettled(imageItems.map((item) => processImageItem(item, msgId.slice(-8), groupFolder)));
|
|
388
|
+
for (const r of results) {
|
|
389
|
+
if (r.status === 'fulfilled') {
|
|
390
|
+
if (r.value.attachmentEntry) {
|
|
391
|
+
imageAttachments.push(r.value.attachmentEntry);
|
|
392
|
+
}
|
|
393
|
+
if (r.value.textPrefix) {
|
|
394
|
+
textPrefixes.push(r.value.textPrefix);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Handle file items — note the path in content
|
|
400
|
+
for (const item of msg.item_list) {
|
|
401
|
+
if (item.type === MESSAGE_ITEM_TYPE_FILE && item.file_item) {
|
|
402
|
+
const fileName = item.file_item.file_name || 'unknown_file';
|
|
403
|
+
content = `[文件: ${fileName}]\n${content}`.trim();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (imageAttachments.length > 0) {
|
|
407
|
+
attachmentsJson = JSON.stringify(imageAttachments);
|
|
408
|
+
if (textPrefixes.length > 0) {
|
|
409
|
+
content = `${textPrefixes.join('\n')}\n${content}`.trim();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (!content && imageAttachments.length > 0) {
|
|
413
|
+
content = '[图片]';
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!content)
|
|
417
|
+
return; // No usable content
|
|
418
|
+
// Route and store message
|
|
419
|
+
const agentRouting = opts.resolveEffectiveChatJid?.(jid);
|
|
420
|
+
const targetJid = agentRouting?.effectiveJid ?? jid;
|
|
421
|
+
const id = crypto.randomUUID();
|
|
422
|
+
const timestamp = msg.create_time_ms
|
|
423
|
+
? new Date(msg.create_time_ms).toISOString()
|
|
424
|
+
: nowIso;
|
|
425
|
+
const senderId = `wechat:${fromUserId}`;
|
|
426
|
+
if (targetJid !== jid)
|
|
427
|
+
storeChatMetadata(targetJid, timestamp);
|
|
428
|
+
storeMessageDirect(id, targetJid, senderId, senderName, content, timestamp, false, {
|
|
429
|
+
attachments: attachmentsJson,
|
|
430
|
+
sourceJid: jid,
|
|
431
|
+
});
|
|
432
|
+
broadcastNewMessage(targetJid, {
|
|
433
|
+
id,
|
|
434
|
+
chat_jid: targetJid,
|
|
435
|
+
source_jid: jid,
|
|
436
|
+
sender: senderId,
|
|
437
|
+
sender_name: senderName,
|
|
438
|
+
content,
|
|
439
|
+
timestamp,
|
|
440
|
+
attachments: attachmentsJson,
|
|
441
|
+
is_from_me: false,
|
|
442
|
+
}, agentRouting?.agentId ?? undefined);
|
|
443
|
+
notifyNewImMessage();
|
|
444
|
+
if (agentRouting?.agentId) {
|
|
445
|
+
opts.onAgentMessage?.(jid, agentRouting.agentId);
|
|
446
|
+
logger.info({ jid, effectiveJid: targetJid, agentId: agentRouting.agentId }, 'WeChat message routed to agent');
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
logger.info({ jid, sender: senderName, msgId: msg.message_id ?? msg.seq }, 'WeChat message stored');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
logger.error({ err, msgId: msg.message_id }, 'Error handling WeChat message');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// ─── Long-Polling Loop ────────────────────────────────────
|
|
457
|
+
async function pollLoop(opts) {
|
|
458
|
+
let reconnectDelay = RECONNECT_MIN_DELAY_MS;
|
|
459
|
+
while (!stopping) {
|
|
460
|
+
try {
|
|
461
|
+
const response = await getUpdates();
|
|
462
|
+
// Update longpoll timeout from server
|
|
463
|
+
if (response.longpolling_timeout_ms) {
|
|
464
|
+
longpollTimeoutMs = response.longpolling_timeout_ms;
|
|
465
|
+
}
|
|
466
|
+
// Check for session expiry
|
|
467
|
+
if (response.ret === ERRCODE_SESSION_EXPIRED) {
|
|
468
|
+
logger.warn('WeChat session expired (errcode -14), stopping poll loop');
|
|
469
|
+
connected = false;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
// ret is absent (undefined) when the request succeeds — treat as 0
|
|
473
|
+
if (response.ret !== undefined && response.ret !== 0) {
|
|
474
|
+
logger.warn(`WeChat getUpdates error: ret=${response.ret}, response=${JSON.stringify(response).slice(0, 500)}`);
|
|
475
|
+
// Back off on errors
|
|
476
|
+
await sleep(reconnectDelay);
|
|
477
|
+
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_DELAY_MS);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
// Reset backoff on success
|
|
481
|
+
reconnectDelay = RECONNECT_MIN_DELAY_MS;
|
|
482
|
+
// Update cursor
|
|
483
|
+
if (response.get_updates_buf) {
|
|
484
|
+
currentGetUpdatesBuf = response.get_updates_buf;
|
|
485
|
+
}
|
|
486
|
+
// Process messages
|
|
487
|
+
if (response.msgs && response.msgs.length > 0) {
|
|
488
|
+
for (const msg of response.msgs) {
|
|
489
|
+
await processMessage(msg, opts);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
if (stopping)
|
|
495
|
+
break;
|
|
496
|
+
// Long-poll timeout is expected when no new messages arrive — just retry immediately.
|
|
497
|
+
const isTimeout = err instanceof Error && err.message.includes('timed out');
|
|
498
|
+
if (isTimeout) {
|
|
499
|
+
logger.debug({ err }, 'WeChat poll timeout (normal, retrying)');
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
logger.error({ err }, 'WeChat poll loop error');
|
|
503
|
+
await sleep(reconnectDelay);
|
|
504
|
+
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_DELAY_MS);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function sleep(ms) {
|
|
509
|
+
return new Promise((resolve) => {
|
|
510
|
+
const timer = setTimeout(resolve, ms);
|
|
511
|
+
cancelSleep = () => {
|
|
512
|
+
clearTimeout(timer);
|
|
513
|
+
resolve();
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
// ─── Connection Interface ─────────────────────────────────
|
|
518
|
+
const connection = {
|
|
519
|
+
async connect(opts) {
|
|
520
|
+
if (!config.botToken || !config.ilinkBotId) {
|
|
521
|
+
logger.info('WeChat botToken/ilinkBotId not configured, skipping');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
stopping = false;
|
|
525
|
+
connected = true;
|
|
526
|
+
msgCache.clear();
|
|
527
|
+
contextTokenCache.clear();
|
|
528
|
+
knownJids.clear();
|
|
529
|
+
logger.info({ baseUrl, ilinkBotId: config.ilinkBotId }, 'WeChat iLink bot connecting');
|
|
530
|
+
// Fire onReady immediately since there's no handshake
|
|
531
|
+
opts.onReady?.();
|
|
532
|
+
// Start poll loop in background (non-blocking)
|
|
533
|
+
pollLoop(opts).catch((err) => {
|
|
534
|
+
logger.error({ err }, 'WeChat poll loop exited with error');
|
|
535
|
+
connected = false;
|
|
536
|
+
});
|
|
537
|
+
},
|
|
538
|
+
async disconnect() {
|
|
539
|
+
stopping = true;
|
|
540
|
+
connected = false;
|
|
541
|
+
// Abort any pending sleep
|
|
542
|
+
cancelSleep?.();
|
|
543
|
+
cancelSleep = null;
|
|
544
|
+
msgCache.clear();
|
|
545
|
+
contextTokenCache.clear();
|
|
546
|
+
knownJids.clear();
|
|
547
|
+
logger.info('WeChat iLink bot disconnected');
|
|
548
|
+
},
|
|
549
|
+
async sendMessage(chatId, text, _localImagePaths) {
|
|
550
|
+
// chatId is the raw WeChat user ID (prefix already stripped by IM manager)
|
|
551
|
+
const userId = chatId;
|
|
552
|
+
const contextToken = contextTokenCache.get(userId);
|
|
553
|
+
if (!contextToken) {
|
|
554
|
+
logger.warn({ chatId }, 'No context_token available for WeChat user, cannot send message');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const plainText = markdownToPlainText(text);
|
|
559
|
+
const chunks = splitTextChunks(plainText, MSG_SPLIT_LIMIT);
|
|
560
|
+
for (const chunk of chunks) {
|
|
561
|
+
await sendMessageApi(userId, contextToken, chunk);
|
|
562
|
+
}
|
|
563
|
+
logger.info({ chatId }, 'WeChat message sent');
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
logger.error({ err, chatId }, 'Failed to send WeChat message');
|
|
567
|
+
throw err;
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
async sendTyping(chatId, isTyping) {
|
|
571
|
+
// chatId is the raw WeChat user ID (prefix already stripped by IM manager)
|
|
572
|
+
const userId = chatId;
|
|
573
|
+
const contextToken = contextTokenCache.get(userId);
|
|
574
|
+
if (!contextToken)
|
|
575
|
+
return;
|
|
576
|
+
const ticket = await getTypingTicket(userId, contextToken);
|
|
577
|
+
if (!ticket)
|
|
578
|
+
return;
|
|
579
|
+
await sendTypingApi(userId, ticket, isTyping ? 1 : 2);
|
|
580
|
+
},
|
|
581
|
+
isConnected() {
|
|
582
|
+
return connected && !stopping;
|
|
583
|
+
},
|
|
584
|
+
getUpdatesBuf() {
|
|
585
|
+
return currentGetUpdatesBuf;
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
return connection;
|
|
589
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DATA_DIR } from './config.js';
|
|
4
|
+
import { deleteSession, getJidsByFolder, listAgentsByJid } from './db.js';
|
|
5
|
+
export function clearSessionJsonlFiles(folder, agentId) {
|
|
6
|
+
const claudeDir = agentId
|
|
7
|
+
? path.join(DATA_DIR, 'sessions', folder, 'agents', agentId, '.claude')
|
|
8
|
+
: path.join(DATA_DIR, 'sessions', folder, '.claude');
|
|
9
|
+
if (!fs.existsSync(claudeDir))
|
|
10
|
+
return;
|
|
11
|
+
const keep = new Set(['settings.json']);
|
|
12
|
+
const entries = fs.readdirSync(claudeDir);
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (keep.has(entry))
|
|
15
|
+
continue;
|
|
16
|
+
const fullPath = path.join(claudeDir, entry);
|
|
17
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function resetWorkspaceRuntimeState(deps, jid, group) {
|
|
21
|
+
const siblingJids = getJidsByFolder(group.folder);
|
|
22
|
+
const agents = jid.startsWith('web:') ? listAgentsByJid(jid) : [];
|
|
23
|
+
const stopTargets = new Set(siblingJids);
|
|
24
|
+
for (const agent of agents) {
|
|
25
|
+
stopTargets.add(`${jid}#agent:${agent.id}`);
|
|
26
|
+
}
|
|
27
|
+
await Promise.all([...stopTargets].map((targetJid) => deps.queue.stopGroup(targetJid, { force: true })));
|
|
28
|
+
clearSessionJsonlFiles(group.folder);
|
|
29
|
+
deleteSession(group.folder);
|
|
30
|
+
delete deps.getSessions()[group.folder];
|
|
31
|
+
for (const agent of agents) {
|
|
32
|
+
clearSessionJsonlFiles(group.folder, agent.id);
|
|
33
|
+
deleteSession(group.folder, agent.id);
|
|
34
|
+
}
|
|
35
|
+
}
|