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/telegram.js
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import { Bot, InputFile } from 'grammy';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import fsPromises from 'node:fs/promises';
|
|
4
|
+
import https from 'node:https';
|
|
5
|
+
import { Agent as HttpsAgent } from 'node:https';
|
|
6
|
+
import { ProxyAgent } from 'proxy-agent';
|
|
7
|
+
import { storeChatMetadata, storeMessageDirect, updateChatName } from './db.js';
|
|
8
|
+
import { notifyNewImMessage } from './message-notifier.js';
|
|
9
|
+
import { broadcastNewMessage } from './web.js';
|
|
10
|
+
import { logger } from './logger.js';
|
|
11
|
+
import { saveDownloadedFile, MAX_FILE_SIZE, FileTooLargeError, } from './im-downloader.js';
|
|
12
|
+
import { detectImageMimeType } from './image-detector.js';
|
|
13
|
+
// ─── Shared Helpers (pure functions, no instance state) ────────
|
|
14
|
+
function escapeHtml(text) {
|
|
15
|
+
return text
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Convert Markdown to Telegram-compatible HTML.
|
|
22
|
+
* Handles: code blocks, inline code, bold, italic, strikethrough, links, headings.
|
|
23
|
+
*/
|
|
24
|
+
function markdownToTelegramHtml(md) {
|
|
25
|
+
// Step 1: Extract code blocks to protect them from further processing
|
|
26
|
+
const codeBlocks = [];
|
|
27
|
+
let text = md.replace(/```[\s\S]*?```/g, (match) => {
|
|
28
|
+
const code = match.replace(/^```\w*\n?/, '').replace(/\n?```$/, '');
|
|
29
|
+
codeBlocks.push(`<pre><code>${escapeHtml(code)}</code></pre>`);
|
|
30
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
31
|
+
});
|
|
32
|
+
// Step 2: Extract inline code
|
|
33
|
+
const inlineCodes = [];
|
|
34
|
+
text = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
35
|
+
inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
|
|
36
|
+
return `\x00IC${inlineCodes.length - 1}\x00`;
|
|
37
|
+
});
|
|
38
|
+
// Step 3: Escape HTML in remaining text
|
|
39
|
+
text = escapeHtml(text);
|
|
40
|
+
// Step 4: Convert Markdown formatting
|
|
41
|
+
// Links: [text](url)
|
|
42
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
43
|
+
// Bold: **text** or __text__
|
|
44
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
45
|
+
text = text.replace(/__(.+?)__/g, '<b>$1</b>');
|
|
46
|
+
// Strikethrough: ~~text~~ (before italic to avoid conflicts)
|
|
47
|
+
text = text.replace(/~~(.+?)~~/g, '<s>$1</s>');
|
|
48
|
+
// Italic: *text* (not preceded/followed by word chars to avoid false matches)
|
|
49
|
+
text = text.replace(/(?<!\w)\*(?!\s)(.+?)(?<!\s)\*(?!\w)/g, '<i>$1</i>');
|
|
50
|
+
// Headings: # text → bold
|
|
51
|
+
text = text.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>');
|
|
52
|
+
// Step 5: Restore code blocks and inline code
|
|
53
|
+
text = text.replace(/\x00CB(\d+)\x00/g, (_, i) => codeBlocks[Number(i)]);
|
|
54
|
+
text = text.replace(/\x00IC(\d+)\x00/g, (_, i) => inlineCodes[Number(i)]);
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Split markdown text into chunks at safe boundaries (paragraphs, lines, words).
|
|
59
|
+
*/
|
|
60
|
+
function splitMarkdownChunks(text, limit) {
|
|
61
|
+
if (text.length <= limit)
|
|
62
|
+
return [text];
|
|
63
|
+
const chunks = [];
|
|
64
|
+
let remaining = text;
|
|
65
|
+
while (remaining.length > 0) {
|
|
66
|
+
if (remaining.length <= limit) {
|
|
67
|
+
chunks.push(remaining);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
// Try to split at paragraph boundary
|
|
71
|
+
let splitIdx = remaining.lastIndexOf('\n\n', limit);
|
|
72
|
+
if (splitIdx < limit * 0.3) {
|
|
73
|
+
// Try single newline
|
|
74
|
+
splitIdx = remaining.lastIndexOf('\n', limit);
|
|
75
|
+
}
|
|
76
|
+
if (splitIdx < limit * 0.3) {
|
|
77
|
+
// Try space
|
|
78
|
+
splitIdx = remaining.lastIndexOf(' ', limit);
|
|
79
|
+
}
|
|
80
|
+
if (splitIdx < limit * 0.3) {
|
|
81
|
+
// Hard split
|
|
82
|
+
splitIdx = limit;
|
|
83
|
+
}
|
|
84
|
+
chunks.push(remaining.slice(0, splitIdx));
|
|
85
|
+
remaining = remaining.slice(splitIdx).trimStart();
|
|
86
|
+
}
|
|
87
|
+
return chunks;
|
|
88
|
+
}
|
|
89
|
+
// ─── Factory Function ──────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Create an independent Telegram connection instance.
|
|
92
|
+
* Each instance manages its own bot and deduplication state.
|
|
93
|
+
*/
|
|
94
|
+
export function createTelegramConnection(config) {
|
|
95
|
+
// LRU deduplication cache
|
|
96
|
+
const MSG_DEDUP_MAX = 1000;
|
|
97
|
+
const MSG_DEDUP_TTL = 30 * 60 * 1000; // 30min
|
|
98
|
+
const POLLING_RESTART_DELAY_MS = 5000;
|
|
99
|
+
const msgCache = new Map();
|
|
100
|
+
let bot = null;
|
|
101
|
+
let pollingPromise = null;
|
|
102
|
+
let reconnectTimer = null;
|
|
103
|
+
let stopping = false;
|
|
104
|
+
let readyFired = false;
|
|
105
|
+
const telegramApiAgent = config.proxyUrl && config.proxyUrl.trim()
|
|
106
|
+
? new ProxyAgent({
|
|
107
|
+
getProxyForUrl: () => config.proxyUrl.trim(),
|
|
108
|
+
})
|
|
109
|
+
: new HttpsAgent({ keepAlive: true, family: 4 });
|
|
110
|
+
function isDuplicate(msgId) {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
// Map preserves insertion order; stop at first non-expired entry
|
|
113
|
+
for (const [id, ts] of msgCache.entries()) {
|
|
114
|
+
if (now - ts > MSG_DEDUP_TTL) {
|
|
115
|
+
msgCache.delete(id);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (msgCache.size >= MSG_DEDUP_MAX) {
|
|
122
|
+
const firstKey = msgCache.keys().next().value;
|
|
123
|
+
if (firstKey)
|
|
124
|
+
msgCache.delete(firstKey);
|
|
125
|
+
}
|
|
126
|
+
return msgCache.has(msgId);
|
|
127
|
+
}
|
|
128
|
+
function markSeen(msgId) {
|
|
129
|
+
// delete + set to refresh insertion order (move to end)
|
|
130
|
+
msgCache.delete(msgId);
|
|
131
|
+
msgCache.set(msgId, Date.now());
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 通过 Telegram Bot API 下载文件到工作区磁盘。
|
|
135
|
+
* 返回工作区相对路径,失败返回 null。
|
|
136
|
+
*/
|
|
137
|
+
async function downloadTelegramFile(fileId, originalFilename, groupFolder, fileSizeHint) {
|
|
138
|
+
// Telegram Bot API 免费 tier 上限 20 MB,提前预检
|
|
139
|
+
if (fileSizeHint !== undefined && fileSizeHint > MAX_FILE_SIZE) {
|
|
140
|
+
logger.warn({ fileId, fileSizeHint }, 'Telegram file exceeds MAX_FILE_SIZE, skipping');
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (!bot)
|
|
145
|
+
return null;
|
|
146
|
+
const file = await bot.api.getFile(fileId);
|
|
147
|
+
const filePath = file.file_path;
|
|
148
|
+
if (!filePath) {
|
|
149
|
+
logger.warn({ fileId }, 'Telegram getFile returned no file_path');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const url = `https://api.telegram.org/file/bot${config.botToken}/${filePath}`;
|
|
153
|
+
const buffer = await new Promise((resolve, reject) => {
|
|
154
|
+
https
|
|
155
|
+
.get(url, { agent: telegramApiAgent }, (res) => {
|
|
156
|
+
const chunks = [];
|
|
157
|
+
let total = 0;
|
|
158
|
+
res.on('data', (chunk) => {
|
|
159
|
+
total += chunk.length;
|
|
160
|
+
if (total > MAX_FILE_SIZE) {
|
|
161
|
+
res.destroy(new Error('File exceeds MAX_FILE_SIZE during download'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
chunks.push(chunk);
|
|
165
|
+
});
|
|
166
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
167
|
+
res.on('error', reject);
|
|
168
|
+
})
|
|
169
|
+
.on('error', reject);
|
|
170
|
+
});
|
|
171
|
+
// 使用 file_path 中的最后一段作为文件名(若无则用 originalFilename)
|
|
172
|
+
const pathBasename = filePath.split('/').pop() || '';
|
|
173
|
+
const effectiveName = originalFilename || pathBasename || `file_${fileId}`;
|
|
174
|
+
try {
|
|
175
|
+
return await saveDownloadedFile(groupFolder, 'telegram', effectiveName, buffer);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (err instanceof FileTooLargeError) {
|
|
179
|
+
logger.warn({ fileId, effectiveName }, 'Telegram file too large after download');
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
logger.warn({ err, fileId }, 'Failed to download Telegram file');
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 下载 Telegram 图片并返回 base64 字符串,用于 Vision 通道。
|
|
192
|
+
* 失败返回 null。
|
|
193
|
+
*/
|
|
194
|
+
async function downloadTelegramPhotoAsBase64(fileId, fileSizeHint) {
|
|
195
|
+
if (fileSizeHint !== undefined && fileSizeHint > MAX_FILE_SIZE) {
|
|
196
|
+
logger.warn({ fileId, fileSizeHint }, 'Telegram photo exceeds MAX_FILE_SIZE, skipping');
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
if (!bot)
|
|
201
|
+
return null;
|
|
202
|
+
const file = await bot.api.getFile(fileId);
|
|
203
|
+
const filePath = file.file_path;
|
|
204
|
+
if (!filePath) {
|
|
205
|
+
logger.warn({ fileId }, 'Telegram getFile returned no file_path (photo)');
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const url = `https://api.telegram.org/file/bot${config.botToken}/${filePath}`;
|
|
209
|
+
const buffer = await new Promise((resolve, reject) => {
|
|
210
|
+
https
|
|
211
|
+
.get(url, { agent: telegramApiAgent }, (res) => {
|
|
212
|
+
const chunks = [];
|
|
213
|
+
let total = 0;
|
|
214
|
+
res.on('data', (chunk) => {
|
|
215
|
+
total += chunk.length;
|
|
216
|
+
if (total > MAX_FILE_SIZE) {
|
|
217
|
+
res.destroy(new Error('Photo exceeds MAX_FILE_SIZE during download'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
chunks.push(chunk);
|
|
221
|
+
});
|
|
222
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
223
|
+
res.on('error', reject);
|
|
224
|
+
})
|
|
225
|
+
.on('error', reject);
|
|
226
|
+
});
|
|
227
|
+
if (buffer.length === 0) {
|
|
228
|
+
logger.warn({ fileId }, 'Empty response from Telegram photo download');
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const mimeType = detectImageMimeType(buffer);
|
|
232
|
+
return {
|
|
233
|
+
base64: buffer.toString('base64'),
|
|
234
|
+
mimeType,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
logger.warn({ err, fileId }, 'Failed to download Telegram photo as base64');
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Rate-limit rejection messages: one per chat per 5 minutes
|
|
243
|
+
const rejectTimestamps = new Map();
|
|
244
|
+
const REJECT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
245
|
+
function isExpectedStopError(err) {
|
|
246
|
+
const msg = err instanceof Error ? err.message : String(err ?? '');
|
|
247
|
+
return msg.includes('Aborted delay') || msg.includes('AbortError');
|
|
248
|
+
}
|
|
249
|
+
/** Return true if this message was sent before the current connection window. */
|
|
250
|
+
function isStaleMessage(msgDate, ignoreMessagesBefore) {
|
|
251
|
+
if (!ignoreMessagesBefore)
|
|
252
|
+
return false;
|
|
253
|
+
const msgTimeMs = msgDate * 1000;
|
|
254
|
+
if (msgTimeMs < ignoreMessagesBefore) {
|
|
255
|
+
logger.info({ msgTime: msgTimeMs, threshold: ignoreMessagesBefore }, 'Skipping stale Telegram message from before reconnection');
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const connection = {
|
|
261
|
+
async connect(opts) {
|
|
262
|
+
if (!config.botToken) {
|
|
263
|
+
logger.info('Telegram bot token not configured, skipping');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
bot = new Bot(config.botToken, {
|
|
267
|
+
client: {
|
|
268
|
+
timeoutSeconds: 30,
|
|
269
|
+
baseFetchConfig: {
|
|
270
|
+
agent: telegramApiAgent,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
stopping = false;
|
|
275
|
+
readyFired = false;
|
|
276
|
+
if (reconnectTimer) {
|
|
277
|
+
clearTimeout(reconnectTimer);
|
|
278
|
+
reconnectTimer = null;
|
|
279
|
+
}
|
|
280
|
+
bot.on('message:text', async (ctx) => {
|
|
281
|
+
try {
|
|
282
|
+
// Construct deduplication key
|
|
283
|
+
const msgId = String(ctx.message.message_id) + ':' + String(ctx.chat.id);
|
|
284
|
+
if (isDuplicate(msgId)) {
|
|
285
|
+
logger.debug({ msgId }, 'Duplicate Telegram message, skipping');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
markSeen(msgId);
|
|
289
|
+
if (isStaleMessage(ctx.message.date, opts.ignoreMessagesBefore))
|
|
290
|
+
return;
|
|
291
|
+
const chatId = String(ctx.chat.id);
|
|
292
|
+
const jid = `telegram:${chatId}`;
|
|
293
|
+
const chatName = ctx.chat.title ||
|
|
294
|
+
[ctx.chat.first_name, ctx.chat.last_name]
|
|
295
|
+
.filter(Boolean)
|
|
296
|
+
.join(' ') ||
|
|
297
|
+
`Telegram ${chatId}`;
|
|
298
|
+
const senderName = [ctx.from?.first_name, ctx.from?.last_name]
|
|
299
|
+
.filter(Boolean)
|
|
300
|
+
.join(' ') || 'Unknown';
|
|
301
|
+
const text = ctx.message.text;
|
|
302
|
+
// ── /pair <code> command ──
|
|
303
|
+
const pairMatch = text.match(/^\/pair\s+(\S+)/i);
|
|
304
|
+
if (pairMatch && opts.onPairAttempt) {
|
|
305
|
+
const code = pairMatch[1];
|
|
306
|
+
try {
|
|
307
|
+
const success = await opts.onPairAttempt(jid, chatName, code);
|
|
308
|
+
if (success) {
|
|
309
|
+
await ctx.reply('Pairing successful! This chat is now connected.');
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
await ctx.reply('Invalid or expired pairing code. Please generate a new code from the web settings page.');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
logger.error({ err, jid }, 'Error during pair attempt');
|
|
317
|
+
await ctx.reply('Pairing failed due to an internal error. Please try again.');
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// ── /start command ──
|
|
322
|
+
if (text.trim() === '/start') {
|
|
323
|
+
if (opts.isChatAuthorized(jid)) {
|
|
324
|
+
await ctx.reply('This chat is already connected. You can send messages normally.');
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
await ctx.reply('Welcome! To connect this chat, please:\n' +
|
|
328
|
+
'1. Go to the web settings page\n' +
|
|
329
|
+
'2. Generate a pairing code\n' +
|
|
330
|
+
'3. Send /pair <code> here');
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// ── Authorization check ──
|
|
335
|
+
if (!opts.isChatAuthorized(jid)) {
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
const lastReject = rejectTimestamps.get(jid) ?? 0;
|
|
338
|
+
if (now - lastReject >= REJECT_COOLDOWN_MS) {
|
|
339
|
+
rejectTimestamps.set(jid, now);
|
|
340
|
+
await ctx.reply('This chat is not yet paired. Please send /pair <code> to connect.\n' +
|
|
341
|
+
'You can generate a pairing code from the web settings page.');
|
|
342
|
+
}
|
|
343
|
+
logger.debug({ jid, chatName }, 'Unauthorized Telegram chat, message ignored');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// ── Authorized chat: normal flow ──
|
|
347
|
+
// 自动注册(确保 metadata 和名称同步)
|
|
348
|
+
storeChatMetadata(jid, new Date().toISOString());
|
|
349
|
+
updateChatName(jid, chatName);
|
|
350
|
+
opts.onNewChat(jid, chatName);
|
|
351
|
+
// ── 斜杠指令:拦截已知 /xxx 命令,不进入消息流 ──
|
|
352
|
+
// Telegram 群聊中会追加 @BotUsername,需要去掉
|
|
353
|
+
const tgSlashMatch = text
|
|
354
|
+
.trim()
|
|
355
|
+
.match(/^\/(\S+?)(?:@\S+)?(?:\s+(.*))?$/i);
|
|
356
|
+
if (tgSlashMatch && opts.onCommand) {
|
|
357
|
+
const cmdBody = (tgSlashMatch[1] + (tgSlashMatch[2] ? ' ' + tgSlashMatch[2] : '')).trim();
|
|
358
|
+
logger.info({ jid, cmd: tgSlashMatch[1], cmdBody }, 'Telegram slash command detected');
|
|
359
|
+
try {
|
|
360
|
+
const reply = await opts.onCommand(jid, cmdBody);
|
|
361
|
+
if (reply) {
|
|
362
|
+
await ctx.reply(reply);
|
|
363
|
+
return; // 已知命令,拦截
|
|
364
|
+
}
|
|
365
|
+
// reply 为 null 表示未知命令,继续作为普通消息处理
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
logger.error({ jid, cmd: tgSlashMatch[1], err }, 'Telegram slash command failed');
|
|
369
|
+
try {
|
|
370
|
+
await ctx.reply('⚠️ 命令执行失败,请稍后重试');
|
|
371
|
+
}
|
|
372
|
+
catch (sendErr) {
|
|
373
|
+
logger.error({ jid, sendErr }, 'Failed to send slash command error feedback');
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Reaction 确认
|
|
379
|
+
try {
|
|
380
|
+
await ctx.react('👀');
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
logger.debug({ err, msgId }, 'Failed to add Telegram reaction');
|
|
384
|
+
}
|
|
385
|
+
// 解析绑定路由
|
|
386
|
+
const agentRouting = opts.resolveEffectiveChatJid?.(jid);
|
|
387
|
+
const targetJid = agentRouting?.effectiveJid ?? jid;
|
|
388
|
+
// 存储消息
|
|
389
|
+
const id = crypto.randomUUID();
|
|
390
|
+
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
391
|
+
const senderId = ctx.from?.id ? `tg:${ctx.from.id}` : 'tg:unknown';
|
|
392
|
+
storeChatMetadata(targetJid, timestamp);
|
|
393
|
+
storeMessageDirect(id, targetJid, senderId, senderName, text, timestamp, false, { sourceJid: jid });
|
|
394
|
+
// 广播到 Web 客户端
|
|
395
|
+
broadcastNewMessage(targetJid, {
|
|
396
|
+
id,
|
|
397
|
+
chat_jid: targetJid,
|
|
398
|
+
source_jid: jid,
|
|
399
|
+
sender: senderId,
|
|
400
|
+
sender_name: senderName,
|
|
401
|
+
content: text,
|
|
402
|
+
timestamp,
|
|
403
|
+
is_from_me: false,
|
|
404
|
+
}, agentRouting?.agentId ?? undefined);
|
|
405
|
+
notifyNewImMessage();
|
|
406
|
+
// 触发 agent 处理
|
|
407
|
+
if (agentRouting?.agentId) {
|
|
408
|
+
opts.onAgentMessage?.(jid, agentRouting.agentId);
|
|
409
|
+
logger.info({
|
|
410
|
+
jid,
|
|
411
|
+
effectiveJid: targetJid,
|
|
412
|
+
agentId: agentRouting.agentId,
|
|
413
|
+
sender: senderName,
|
|
414
|
+
msgId,
|
|
415
|
+
}, 'Telegram message routed to conversation agent');
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
logger.info({ jid, sender: senderName, msgId, routed: !!agentRouting }, 'Telegram message stored');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
logger.error({ err }, 'Error handling Telegram message');
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
// ── message:photo 处理器(Vision 通道,与飞书独立图片逻辑一致)──
|
|
426
|
+
bot.on('message:photo', async (ctx) => {
|
|
427
|
+
try {
|
|
428
|
+
const msgId = String(ctx.message.message_id) + ':' + String(ctx.chat.id);
|
|
429
|
+
if (isDuplicate(msgId))
|
|
430
|
+
return;
|
|
431
|
+
markSeen(msgId);
|
|
432
|
+
if (isStaleMessage(ctx.message.date, opts.ignoreMessagesBefore))
|
|
433
|
+
return;
|
|
434
|
+
const chatId = String(ctx.chat.id);
|
|
435
|
+
const jid = `telegram:${chatId}`;
|
|
436
|
+
const chatName = ctx.chat.title ||
|
|
437
|
+
[ctx.chat.first_name, ctx.chat.last_name]
|
|
438
|
+
.filter(Boolean)
|
|
439
|
+
.join(' ') ||
|
|
440
|
+
`Telegram ${chatId}`;
|
|
441
|
+
const senderName = [ctx.from?.first_name, ctx.from?.last_name]
|
|
442
|
+
.filter(Boolean)
|
|
443
|
+
.join(' ') || 'Unknown';
|
|
444
|
+
if (!opts.isChatAuthorized(jid)) {
|
|
445
|
+
logger.debug({ jid }, 'Unauthorized Telegram chat (photo), ignoring');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
storeChatMetadata(jid, new Date().toISOString());
|
|
449
|
+
updateChatName(jid, chatName);
|
|
450
|
+
opts.onNewChat(jid, chatName);
|
|
451
|
+
// 取最高分辨率,下载为 base64 供 Vision
|
|
452
|
+
const photo = ctx.message.photo.at(-1);
|
|
453
|
+
if (!photo)
|
|
454
|
+
return;
|
|
455
|
+
const imageData = await downloadTelegramPhotoAsBase64(photo.file_id, photo.file_size);
|
|
456
|
+
let attachmentsJson;
|
|
457
|
+
let imgMarker = '[图片]';
|
|
458
|
+
if (imageData) {
|
|
459
|
+
attachmentsJson = JSON.stringify([
|
|
460
|
+
{
|
|
461
|
+
type: 'image',
|
|
462
|
+
data: imageData.base64,
|
|
463
|
+
mimeType: imageData.mimeType,
|
|
464
|
+
},
|
|
465
|
+
]);
|
|
466
|
+
// 存盘:与飞书图片处理逻辑对齐,agent 可通过路径直接操作文件
|
|
467
|
+
const groupFolder = opts.resolveGroupFolder?.(jid);
|
|
468
|
+
if (groupFolder) {
|
|
469
|
+
const extMap = {
|
|
470
|
+
'image/jpeg': '.jpg',
|
|
471
|
+
'image/png': '.png',
|
|
472
|
+
'image/gif': '.gif',
|
|
473
|
+
'image/webp': '.webp',
|
|
474
|
+
'image/bmp': '.bmp',
|
|
475
|
+
'image/tiff': '.tiff',
|
|
476
|
+
};
|
|
477
|
+
const ext = extMap[imageData.mimeType] ?? '.jpg';
|
|
478
|
+
const fileName = `telegram_img_${photo.file_id.slice(-8)}${ext}`;
|
|
479
|
+
try {
|
|
480
|
+
const relPath = await saveDownloadedFile(groupFolder, 'telegram', fileName, Buffer.from(imageData.base64, 'base64'));
|
|
481
|
+
if (relPath)
|
|
482
|
+
imgMarker = `[图片: ${relPath}]`;
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
logger.warn({ err, fileId: photo.file_id }, 'Failed to save Telegram photo to disk');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const caption = ctx.message.caption;
|
|
490
|
+
const text = caption ? `${imgMarker}\n${caption}` : imgMarker;
|
|
491
|
+
try {
|
|
492
|
+
await ctx.react('👀');
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
logger.debug({ err, msgId }, 'Failed to add Telegram reaction');
|
|
496
|
+
}
|
|
497
|
+
// 解析绑定路由
|
|
498
|
+
const agentRouting = opts.resolveEffectiveChatJid?.(jid);
|
|
499
|
+
const targetJid = agentRouting?.effectiveJid ?? jid;
|
|
500
|
+
const id = crypto.randomUUID();
|
|
501
|
+
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
502
|
+
const senderId = ctx.from?.id ? `tg:${ctx.from.id}` : 'tg:unknown';
|
|
503
|
+
storeChatMetadata(targetJid, timestamp);
|
|
504
|
+
storeMessageDirect(id, targetJid, senderId, senderName, text, timestamp, false, { attachments: attachmentsJson, sourceJid: jid });
|
|
505
|
+
broadcastNewMessage(targetJid, {
|
|
506
|
+
id,
|
|
507
|
+
chat_jid: targetJid,
|
|
508
|
+
source_jid: jid,
|
|
509
|
+
sender: senderId,
|
|
510
|
+
sender_name: senderName,
|
|
511
|
+
content: text,
|
|
512
|
+
timestamp,
|
|
513
|
+
attachments: attachmentsJson,
|
|
514
|
+
is_from_me: false,
|
|
515
|
+
}, agentRouting?.agentId ?? undefined);
|
|
516
|
+
notifyNewImMessage();
|
|
517
|
+
if (agentRouting?.agentId) {
|
|
518
|
+
opts.onAgentMessage?.(jid, agentRouting.agentId);
|
|
519
|
+
}
|
|
520
|
+
logger.info({ jid, sender: senderName, msgId, routed: !!agentRouting }, 'Telegram photo stored');
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
logger.error({ err }, 'Error handling Telegram photo');
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
// ── message:document 处理器 ──
|
|
527
|
+
bot.on('message:document', async (ctx) => {
|
|
528
|
+
try {
|
|
529
|
+
const msgId = String(ctx.message.message_id) + ':' + String(ctx.chat.id);
|
|
530
|
+
if (isDuplicate(msgId))
|
|
531
|
+
return;
|
|
532
|
+
markSeen(msgId);
|
|
533
|
+
if (isStaleMessage(ctx.message.date, opts.ignoreMessagesBefore))
|
|
534
|
+
return;
|
|
535
|
+
const chatId = String(ctx.chat.id);
|
|
536
|
+
const jid = `telegram:${chatId}`;
|
|
537
|
+
const chatName = ctx.chat.title ||
|
|
538
|
+
[ctx.chat.first_name, ctx.chat.last_name]
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.join(' ') ||
|
|
541
|
+
`Telegram ${chatId}`;
|
|
542
|
+
const senderName = [ctx.from?.first_name, ctx.from?.last_name]
|
|
543
|
+
.filter(Boolean)
|
|
544
|
+
.join(' ') || 'Unknown';
|
|
545
|
+
if (!opts.isChatAuthorized(jid)) {
|
|
546
|
+
logger.debug({ jid }, 'Unauthorized Telegram chat (document), ignoring');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
storeChatMetadata(jid, new Date().toISOString());
|
|
550
|
+
updateChatName(jid, chatName);
|
|
551
|
+
opts.onNewChat(jid, chatName);
|
|
552
|
+
const doc = ctx.message.document;
|
|
553
|
+
const originalFilename = doc.file_name || 'file';
|
|
554
|
+
// file_size 超过上限时跳过下载
|
|
555
|
+
if (doc.file_size !== undefined && doc.file_size > MAX_FILE_SIZE) {
|
|
556
|
+
const earlyRouting = opts.resolveEffectiveChatJid?.(jid);
|
|
557
|
+
const earlyTargetJid = earlyRouting?.effectiveJid ?? jid;
|
|
558
|
+
const text = `[文件过大,未下载: ${originalFilename}]`;
|
|
559
|
+
const id = crypto.randomUUID();
|
|
560
|
+
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
561
|
+
const senderId = ctx.from?.id ? `tg:${ctx.from.id}` : 'tg:unknown';
|
|
562
|
+
storeMessageDirect(id, earlyTargetJid, senderId, senderName, text, timestamp, false, { sourceJid: jid });
|
|
563
|
+
broadcastNewMessage(earlyTargetJid, {
|
|
564
|
+
id,
|
|
565
|
+
chat_jid: earlyTargetJid,
|
|
566
|
+
source_jid: jid,
|
|
567
|
+
sender: senderId,
|
|
568
|
+
sender_name: senderName,
|
|
569
|
+
content: text,
|
|
570
|
+
timestamp,
|
|
571
|
+
is_from_me: false,
|
|
572
|
+
}, earlyRouting?.agentId ?? undefined);
|
|
573
|
+
notifyNewImMessage();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const groupFolder = opts.resolveGroupFolder?.(jid);
|
|
577
|
+
let fileText;
|
|
578
|
+
if (!groupFolder) {
|
|
579
|
+
fileText = `[文件下载失败: 无法确定工作目录]`;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
const relPath = await downloadTelegramFile(doc.file_id, originalFilename, groupFolder, doc.file_size);
|
|
583
|
+
fileText = relPath
|
|
584
|
+
? `[文件: ${relPath}]`
|
|
585
|
+
: `[文件下载失败: ${originalFilename}]`;
|
|
586
|
+
}
|
|
587
|
+
const caption = ctx.message.caption;
|
|
588
|
+
const text = caption ? `${fileText}\n${caption}` : fileText;
|
|
589
|
+
try {
|
|
590
|
+
await ctx.react('👀');
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
logger.debug({ err, msgId }, 'Failed to add Telegram reaction');
|
|
594
|
+
}
|
|
595
|
+
// 解析绑定路由
|
|
596
|
+
const agentRouting = opts.resolveEffectiveChatJid?.(jid);
|
|
597
|
+
const targetJid = agentRouting?.effectiveJid ?? jid;
|
|
598
|
+
const id = crypto.randomUUID();
|
|
599
|
+
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
600
|
+
const senderId = ctx.from?.id ? `tg:${ctx.from.id}` : 'tg:unknown';
|
|
601
|
+
storeChatMetadata(targetJid, timestamp);
|
|
602
|
+
storeMessageDirect(id, targetJid, senderId, senderName, text, timestamp, false, { sourceJid: jid });
|
|
603
|
+
broadcastNewMessage(targetJid, {
|
|
604
|
+
id,
|
|
605
|
+
chat_jid: targetJid,
|
|
606
|
+
source_jid: jid,
|
|
607
|
+
sender: senderId,
|
|
608
|
+
sender_name: senderName,
|
|
609
|
+
content: text,
|
|
610
|
+
timestamp,
|
|
611
|
+
is_from_me: false,
|
|
612
|
+
}, agentRouting?.agentId ?? undefined);
|
|
613
|
+
notifyNewImMessage();
|
|
614
|
+
if (agentRouting?.agentId) {
|
|
615
|
+
opts.onAgentMessage?.(jid, agentRouting.agentId);
|
|
616
|
+
}
|
|
617
|
+
logger.info({ jid, sender: senderName, msgId, routed: !!agentRouting }, 'Telegram document stored');
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
logger.error({ err }, 'Error handling Telegram document');
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
// ── my_chat_member: Bot 加入/离开群聊检测 ──
|
|
624
|
+
bot.on('my_chat_member', async (ctx) => {
|
|
625
|
+
try {
|
|
626
|
+
const update = ctx.myChatMember;
|
|
627
|
+
const chatType = update.chat.type;
|
|
628
|
+
// 仅处理群聊;私聊走 /start + /pair 流程
|
|
629
|
+
if (chatType !== 'group' && chatType !== 'supergroup')
|
|
630
|
+
return;
|
|
631
|
+
const chatId = String(update.chat.id);
|
|
632
|
+
const jid = `telegram:${chatId}`;
|
|
633
|
+
const chatName = update.chat.title || `Telegram ${chatId}`;
|
|
634
|
+
const newStatus = update.new_chat_member.status;
|
|
635
|
+
const oldStatus = update.old_chat_member.status;
|
|
636
|
+
if ((oldStatus === 'left' || oldStatus === 'kicked') &&
|
|
637
|
+
(newStatus === 'member' || newStatus === 'administrator')) {
|
|
638
|
+
logger.info({ jid, chatName, newStatus }, 'Telegram bot added to group');
|
|
639
|
+
opts.onBotAddedToGroup?.(jid, chatName);
|
|
640
|
+
}
|
|
641
|
+
if ((oldStatus === 'member' || oldStatus === 'administrator') &&
|
|
642
|
+
(newStatus === 'left' || newStatus === 'kicked')) {
|
|
643
|
+
logger.info({ jid, chatName, newStatus }, 'Telegram bot removed from group');
|
|
644
|
+
opts.onBotRemovedFromGroup?.(jid);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
logger.error({ err }, 'Error handling Telegram my_chat_member update');
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
const startPolling = () => {
|
|
652
|
+
if (!bot || stopping)
|
|
653
|
+
return;
|
|
654
|
+
pollingPromise = bot
|
|
655
|
+
.start({
|
|
656
|
+
allowed_updates: ['message', 'edited_message', 'my_chat_member'],
|
|
657
|
+
onStart: () => {
|
|
658
|
+
logger.info('Telegram bot started');
|
|
659
|
+
if (!readyFired) {
|
|
660
|
+
readyFired = true;
|
|
661
|
+
opts.onReady?.();
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
})
|
|
665
|
+
.catch((err) => {
|
|
666
|
+
// bot.stop() during hot-reload will abort long polling; this is expected.
|
|
667
|
+
if (stopping && isExpectedStopError(err))
|
|
668
|
+
return;
|
|
669
|
+
logger.error({ err }, 'Telegram bot polling crashed');
|
|
670
|
+
if (stopping || !bot)
|
|
671
|
+
return;
|
|
672
|
+
reconnectTimer = setTimeout(() => {
|
|
673
|
+
reconnectTimer = null;
|
|
674
|
+
if (!stopping && bot) {
|
|
675
|
+
logger.info('Restarting Telegram bot polling');
|
|
676
|
+
startPolling();
|
|
677
|
+
}
|
|
678
|
+
}, POLLING_RESTART_DELAY_MS);
|
|
679
|
+
});
|
|
680
|
+
};
|
|
681
|
+
startPolling();
|
|
682
|
+
},
|
|
683
|
+
async disconnect() {
|
|
684
|
+
stopping = true;
|
|
685
|
+
if (reconnectTimer) {
|
|
686
|
+
clearTimeout(reconnectTimer);
|
|
687
|
+
reconnectTimer = null;
|
|
688
|
+
}
|
|
689
|
+
if (bot) {
|
|
690
|
+
try {
|
|
691
|
+
bot.stop();
|
|
692
|
+
logger.info('Telegram bot stopped');
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
logger.error({ err }, 'Error stopping Telegram bot');
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
try {
|
|
699
|
+
await pollingPromise;
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
if (!isExpectedStopError(err)) {
|
|
703
|
+
logger.debug({ err }, 'Telegram polling promise rejected on disconnect');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
pollingPromise = null;
|
|
707
|
+
bot = null;
|
|
708
|
+
telegramApiAgent.destroy();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
async sendMessage(chatId, text, localImagePaths) {
|
|
713
|
+
if (!bot) {
|
|
714
|
+
logger.warn({ chatId }, 'Telegram bot not initialized, skip sending message');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const chatIdNum = Number(chatId);
|
|
718
|
+
if (isNaN(chatIdNum)) {
|
|
719
|
+
logger.error({ chatId }, 'Invalid Telegram chat ID');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
// Split original markdown into chunks (leave room for HTML tag overhead)
|
|
724
|
+
const mdChunks = splitMarkdownChunks(text, 3800);
|
|
725
|
+
for (const mdChunk of mdChunks) {
|
|
726
|
+
const html = markdownToTelegramHtml(mdChunk);
|
|
727
|
+
try {
|
|
728
|
+
await bot.api.sendMessage(chatIdNum, html, { parse_mode: 'HTML' });
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
// HTML parse failed (e.g. unclosed tags), fallback to plain text
|
|
732
|
+
logger.debug({ err, chatId }, 'HTML parse failed, fallback to plain');
|
|
733
|
+
await bot.api.sendMessage(chatIdNum, mdChunk);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
for (const localImagePath of localImagePaths || []) {
|
|
737
|
+
try {
|
|
738
|
+
await bot.api.sendPhoto(chatIdNum, new InputFile(localImagePath));
|
|
739
|
+
}
|
|
740
|
+
catch (imageErr) {
|
|
741
|
+
logger.warn({ chatId, localImagePath, err: imageErr }, 'Failed to send Telegram image attachment');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
logger.info({ chatId }, 'Telegram message sent');
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
logger.error({ err, chatId }, 'Failed to send Telegram message');
|
|
748
|
+
throw err;
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
async sendImage(chatId, imageBuffer, mimeType, caption, fileName) {
|
|
752
|
+
if (!bot) {
|
|
753
|
+
logger.warn({ chatId }, 'Telegram bot not initialized, skip sending image');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const chatIdNum = Number(chatId);
|
|
757
|
+
if (isNaN(chatIdNum)) {
|
|
758
|
+
logger.error({ chatId }, 'Invalid Telegram chat ID for image');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
// Determine file extension from MIME type
|
|
763
|
+
const extMap = {
|
|
764
|
+
'image/png': '.png',
|
|
765
|
+
'image/jpeg': '.jpg',
|
|
766
|
+
'image/gif': '.gif',
|
|
767
|
+
'image/webp': '.webp',
|
|
768
|
+
'image/bmp': '.bmp',
|
|
769
|
+
'image/tiff': '.tiff',
|
|
770
|
+
};
|
|
771
|
+
const ext = extMap[mimeType] || '.png';
|
|
772
|
+
const effectiveFileName = fileName || `image${ext}`;
|
|
773
|
+
const inputFile = new InputFile(imageBuffer, effectiveFileName);
|
|
774
|
+
// Telegram caption limit is 1024 characters; truncate to avoid API errors
|
|
775
|
+
const CAPTION_MAX = 1024;
|
|
776
|
+
const safeCaption = caption && caption.length > CAPTION_MAX
|
|
777
|
+
? caption.slice(0, CAPTION_MAX - 3) + '...'
|
|
778
|
+
: caption || undefined;
|
|
779
|
+
// GIF → sendAnimation (preserves animation); JPEG/PNG/WebP → sendPhoto; others → sendDocument
|
|
780
|
+
const isGif = mimeType === 'image/gif';
|
|
781
|
+
const isPhoto = ['image/png', 'image/jpeg', 'image/webp'].includes(mimeType);
|
|
782
|
+
if (isGif) {
|
|
783
|
+
await bot.api.sendAnimation(chatIdNum, inputFile, {
|
|
784
|
+
caption: safeCaption,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
else if (isPhoto) {
|
|
788
|
+
await bot.api.sendPhoto(chatIdNum, inputFile, {
|
|
789
|
+
caption: safeCaption,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
await bot.api.sendDocument(chatIdNum, inputFile, {
|
|
794
|
+
caption: safeCaption,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
logger.info({
|
|
798
|
+
chatId,
|
|
799
|
+
mimeType,
|
|
800
|
+
size: imageBuffer.length,
|
|
801
|
+
fileName: effectiveFileName,
|
|
802
|
+
}, 'Telegram image sent');
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
logger.error({ err, chatId, mimeType }, 'Failed to send Telegram image');
|
|
806
|
+
throw err;
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
async sendFile(chatId, filePath, fileName) {
|
|
810
|
+
if (!bot) {
|
|
811
|
+
logger.warn({ chatId }, 'Telegram bot not initialized, skip sending file');
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const chatIdNum = Number(chatId);
|
|
815
|
+
if (isNaN(chatIdNum)) {
|
|
816
|
+
logger.error({ chatId }, 'Invalid Telegram chat ID for file');
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
// Check file size (30MB limit, same as MCP tool)
|
|
821
|
+
const stat = await fsPromises.stat(filePath);
|
|
822
|
+
const MAX_SEND_FILE_SIZE = 30 * 1024 * 1024;
|
|
823
|
+
if (stat.size > MAX_SEND_FILE_SIZE) {
|
|
824
|
+
throw new Error(`文件大小超过 30MB 限制 (${(stat.size / 1024 / 1024).toFixed(2)}MB)`);
|
|
825
|
+
}
|
|
826
|
+
await bot.api.sendDocument(chatIdNum, new InputFile(filePath, fileName));
|
|
827
|
+
logger.info({ chatId, filePath, fileName, size: stat.size }, 'Telegram file sent');
|
|
828
|
+
}
|
|
829
|
+
catch (err) {
|
|
830
|
+
logger.error({ err, chatId, filePath, fileName }, 'Failed to send Telegram file');
|
|
831
|
+
throw err;
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
async sendChatAction(chatId, action) {
|
|
835
|
+
if (!bot)
|
|
836
|
+
return;
|
|
837
|
+
const chatIdNum = Number(chatId);
|
|
838
|
+
if (isNaN(chatIdNum))
|
|
839
|
+
return;
|
|
840
|
+
try {
|
|
841
|
+
await bot.api.sendChatAction(chatIdNum, action);
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
logger.debug({ err, chatId }, 'Failed to send Telegram chat action');
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
isConnected() {
|
|
848
|
+
return bot !== null;
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
return connection;
|
|
852
|
+
}
|
|
853
|
+
// ─── Backward-compatible global singleton ──────────────────────
|
|
854
|
+
// @deprecated — 旧的顶层导出函数,内部使用一个默认全局实例。
|
|
855
|
+
// 后续由 imManager 替代。
|
|
856
|
+
let _defaultInstance = null;
|
|
857
|
+
/**
|
|
858
|
+
* @deprecated Use createTelegramConnection() factory instead. Will be replaced by imManager.
|
|
859
|
+
*/
|
|
860
|
+
export async function connectTelegram(opts) {
|
|
861
|
+
const { getTelegramProviderConfig } = await import('./runtime-config.js');
|
|
862
|
+
const config = getTelegramProviderConfig();
|
|
863
|
+
if (!config.botToken) {
|
|
864
|
+
logger.info('Telegram bot token not configured, skipping');
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
_defaultInstance = createTelegramConnection({
|
|
868
|
+
botToken: config.botToken,
|
|
869
|
+
proxyUrl: config.proxyUrl,
|
|
870
|
+
});
|
|
871
|
+
return _defaultInstance.connect(opts);
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* @deprecated Use TelegramConnection.sendMessage() instead.
|
|
875
|
+
*/
|
|
876
|
+
export async function sendTelegramMessage(chatId, text, localImagePaths) {
|
|
877
|
+
if (!_defaultInstance) {
|
|
878
|
+
logger.warn({ chatId }, 'Telegram bot not initialized, skip sending message');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
return _defaultInstance.sendMessage(chatId, text, localImagePaths);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* @deprecated Use TelegramConnection.disconnect() instead.
|
|
885
|
+
*/
|
|
886
|
+
export async function disconnectTelegram() {
|
|
887
|
+
if (_defaultInstance) {
|
|
888
|
+
await _defaultInstance.disconnect();
|
|
889
|
+
_defaultInstance = null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* @deprecated Use TelegramConnection.isConnected() instead.
|
|
894
|
+
*/
|
|
895
|
+
export function isTelegramConnected() {
|
|
896
|
+
return _defaultInstance?.isConnected() ?? false;
|
|
897
|
+
}
|