From a57fa82154496b109721129febe1549d9d2671df Mon Sep 17 00:00:00 2001 From: "CatEqdvbcxw-GPT-5.6-sol" Date: Sun, 20 Sep 2026 09:56:36 +0800 Subject: [PATCH 001/365] feat(plugins): migrate initial Train C1 package slices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Train C1 needs package-owned business implementations and connector adapters before Core can perform a no-double-run cutover. Freeze the eleven-item inventory, add the public SDK session surface, migrate three business plugins and five provider adapters, and keep unresolved Host binding/checkpoint activation fail-closed outside the catalog. Verification: connector package typecheck and 30/30 tests pass; SDK 313/313 passed before checkpoint; fresh-consumer covered the three cataloged business packages; packed runtime entrypoint guard 3/3 passes; inventory remains intentionally RED at the first uncataloged connector. [砚砚/gpt-5.6-sol🐾] --- .github/workflows/contract-ci.yml | 57 + catalog/catalog.json | 126 ++ ...19-train-c1-plugins-aggregate-migration.md | 140 ++ migration/f202-train-c1-inventory.json | 390 ++++ package.json | 3 +- packages/connector-dingtalk/README.md | 32 + packages/connector-dingtalk/assets/icon.svg | 4 + .../connector-dingtalk/npm-shrinkwrap.json | 953 +++++++++ packages/connector-dingtalk/package.json | 49 + packages/connector-dingtalk/plugin.yaml | 43 + .../src/DingTalkAdapter.test.ts | 137 ++ .../connector-dingtalk/src/DingTalkAdapter.ts | 911 +++++++++ packages/connector-dingtalk/src/index.ts | 7 + .../connector-dingtalk/src/manifest.test.ts | 37 + packages/connector-dingtalk/src/types.ts | 18 + .../connector-dingtalk/tsconfig.build.json | 16 + .../connector-dingtalk/tsconfig.test.json | 5 + packages/connector-telegram/README.md | 26 + packages/connector-telegram/assets/icon.svg | 4 + .../connector-telegram/npm-shrinkwrap.json | 697 +++++++ packages/connector-telegram/package.json | 39 + packages/connector-telegram/plugin.yaml | 39 + .../src/TelegramAdapter.test.ts | 115 ++ .../connector-telegram/src/TelegramAdapter.ts | 752 +++++++ packages/connector-telegram/src/index.ts | 5 + .../connector-telegram/src/manifest.test.ts | 28 + .../src/telegram-html-formatter.ts | 44 + packages/connector-telegram/src/token.ts | 7 + packages/connector-telegram/src/types.ts | 64 + .../connector-telegram/tsconfig.build.json | 16 + .../connector-telegram/tsconfig.test.json | 5 + packages/connector-wecom-agent/README.md | 34 + .../connector-wecom-agent/assets/icon.svg | 6 + .../connector-wecom-agent/npm-shrinkwrap.json | 200 ++ packages/connector-wecom-agent/package.json | 36 + packages/connector-wecom-agent/plugin.yaml | 64 + .../src/WeComAgentAdapter.test.ts | 112 ++ .../src/WeComAgentAdapter.ts | 689 +++++++ packages/connector-wecom-agent/src/index.ts | 12 + .../src/manifest.test.ts | 23 + packages/connector-wecom-agent/src/types.ts | 16 + .../connector-wecom-agent/tsconfig.build.json | 16 + .../connector-wecom-agent/tsconfig.test.json | 5 + packages/connector-wecom-bot/README.md | 32 + packages/connector-wecom-bot/assets/icon.svg | 5 + .../connector-wecom-bot/npm-shrinkwrap.json | 174 ++ packages/connector-wecom-bot/package.json | 36 + packages/connector-wecom-bot/plugin.yaml | 43 + .../src/WeComBotAdapter.test.ts | 116 ++ .../src/WeComBotAdapter.ts | 932 +++++++++ packages/connector-wecom-bot/src/index.ts | 7 + .../connector-wecom-bot/src/manifest.test.ts | 22 + packages/connector-wecom-bot/src/types.ts | 18 + .../connector-wecom-bot/tsconfig.build.json | 16 + .../connector-wecom-bot/tsconfig.test.json | 5 + packages/connector-xiaoyi/README.md | 35 + packages/connector-xiaoyi/assets/icon.svg | 4 + packages/connector-xiaoyi/npm-shrinkwrap.json | 210 ++ packages/connector-xiaoyi/package.json | 40 + packages/connector-xiaoyi/plugin.yaml | 47 + .../src/XiaoyiAdapter.test.ts | 110 + .../connector-xiaoyi/src/XiaoyiAdapter.ts | 313 +++ packages/connector-xiaoyi/src/index.ts | 9 + .../connector-xiaoyi/src/manifest.test.ts | 23 + packages/connector-xiaoyi/src/types.ts | 8 + .../src/xiaoyi-protocol.test.ts | 39 + .../connector-xiaoyi/src/xiaoyi-protocol.ts | 151 ++ packages/connector-xiaoyi/src/xiaoyi-ws.ts | 145 ++ packages/connector-xiaoyi/tsconfig.build.json | 16 + packages/connector-xiaoyi/tsconfig.test.json | 5 + packages/plugin-sdk/package.json | 2 +- .../plugin-sdk/src/host-bound-session.test.ts | 303 +++ packages/plugin-sdk/src/host-bound-session.ts | 522 +++++ packages/plugin-sdk/src/index.ts | 20 + .../plugin-sdk/src/messaging-client.test.ts | 177 ++ packages/plugin-sdk/src/messaging-client.ts | 141 ++ packages/video-generation/README.md | 64 + packages/video-generation/assets/icon.svg | 6 + packages/video-generation/npm-shrinkwrap.json | 1777 +++++++++++++++++ packages/video-generation/package.json | 60 + packages/video-generation/plugin.yaml | 97 + .../video-generation/protocols/jimeng.yaml | 99 + .../video-generation/protocols/kling.yaml | 69 + .../video-generation/protocols/zhipu.yaml | 45 + .../video-generation/src/callback-client.ts | 69 + packages/video-generation/src/index.ts | 15 + .../video-generation/src/manifest.test.ts | 43 + .../video-generation/src/mcp-entrypoint.ts | 65 + .../src/protocol-engine.test.ts | 177 ++ .../src/protocol-engine/auth/apikey.ts | 22 + .../src/protocol-engine/auth/credentials.ts | 10 + .../protocol-engine/auth/hmac-sha256-v4.ts | 87 + .../src/protocol-engine/auth/index.ts | 17 + .../src/protocol-engine/auth/jwt-hs256.ts | 35 + .../src/protocol-engine/engine.ts | 415 ++++ .../src/protocol-engine/index.ts | 24 + .../src/protocol-engine/loader.ts | 34 + .../src/protocol-engine/template-utils.ts | 70 + .../src/protocol-engine/types.ts | 126 ++ .../video-generation/src/protocol-tools.ts | 307 +++ packages/video-generation/src/tool-result.ts | 12 + packages/video-generation/tsconfig.build.json | 4 + packages/video-generation/tsconfig.json | 19 + packages/video-generation/tsconfig.test.json | 4 + packages/wechat-visible-reader/README.md | 42 + .../wechat-visible-reader/assets/icon.svg | 6 + .../limbs/wechat-visible-reader.yml | 51 + .../native/WeChatConversationNavigator.swift | 350 ++++ .../native/WeChatLayoutGuard.swift | 153 ++ .../native/WeChatNavigationFixtures.swift | 128 ++ .../native/WeChatNavigationModels.swift | 186 ++ .../native/WeChatReaderCore.swift | 222 ++ .../native/WeChatReaderModels.swift | 181 ++ .../native/WeChatVisibleReader.swift | 175 ++ .../wechat-visible-reader/npm-shrinkwrap.json | 602 ++++++ packages/wechat-visible-reader/package.json | 52 + packages/wechat-visible-reader/plugin.yaml | 25 + .../src/WeChatVisibleReaderArmStore.ts | 69 + .../src/WeChatVisibleReaderMetrics.ts | 50 + .../wechat-visible-reader/src/handlers.ts | 110 + packages/wechat-visible-reader/src/index.ts | 18 + .../src/manifest.test.ts | 38 + .../src/native-runner.ts | 253 +++ packages/wechat-visible-reader/src/types.ts | 251 +++ .../src/wechat-visible-reader.test.ts | 230 +++ .../wechat-visible-reader/tsconfig.build.json | 16 + .../wechat-visible-reader/tsconfig.test.json | 5 + packages/weixin-mp/README.md | 41 + packages/weixin-mp/assets/icon.svg | 5 + packages/weixin-mp/limbs/weixin-mp.yml | 292 +++ packages/weixin-mp/npm-shrinkwrap.json | 587 ++++++ packages/weixin-mp/package.json | 54 + packages/weixin-mp/plugin.yaml | 41 + packages/weixin-mp/skills/weixin-mp/SKILL.md | 99 + packages/weixin-mp/src/handlers.ts | 377 ++++ packages/weixin-mp/src/index.ts | 23 + packages/weixin-mp/src/manifest.test.ts | 41 + packages/weixin-mp/src/markdown-to-wx-html.ts | 172 ++ packages/weixin-mp/src/safe-fetch.ts | 358 ++++ packages/weixin-mp/src/security.test.ts | 108 + packages/weixin-mp/tsconfig.build.json | 16 + packages/weixin-mp/tsconfig.test.json | 8 + pnpm-lock.yaml | 469 ++++- scripts/catalog-check.mjs | 91 +- scripts/catalog-runtime-entrypoints.mjs | 13 + scripts/catalog-runtime-entrypoints.test.mjs | 40 + scripts/fresh-consumer-install.test.mjs | 168 +- scripts/train-c1-inventory.test.mjs | 155 ++ 148 files changed, 19846 insertions(+), 5 deletions(-) create mode 100644 docs/plans/2026-09-19-train-c1-plugins-aggregate-migration.md create mode 100644 migration/f202-train-c1-inventory.json create mode 100644 packages/connector-dingtalk/README.md create mode 100644 packages/connector-dingtalk/assets/icon.svg create mode 100644 packages/connector-dingtalk/npm-shrinkwrap.json create mode 100644 packages/connector-dingtalk/package.json create mode 100644 packages/connector-dingtalk/plugin.yaml create mode 100644 packages/connector-dingtalk/src/DingTalkAdapter.test.ts create mode 100644 packages/connector-dingtalk/src/DingTalkAdapter.ts create mode 100644 packages/connector-dingtalk/src/index.ts create mode 100644 packages/connector-dingtalk/src/manifest.test.ts create mode 100644 packages/connector-dingtalk/src/types.ts create mode 100644 packages/connector-dingtalk/tsconfig.build.json create mode 100644 packages/connector-dingtalk/tsconfig.test.json create mode 100644 packages/connector-telegram/README.md create mode 100644 packages/connector-telegram/assets/icon.svg create mode 100644 packages/connector-telegram/npm-shrinkwrap.json create mode 100644 packages/connector-telegram/package.json create mode 100644 packages/connector-telegram/plugin.yaml create mode 100644 packages/connector-telegram/src/TelegramAdapter.test.ts create mode 100644 packages/connector-telegram/src/TelegramAdapter.ts create mode 100644 packages/connector-telegram/src/index.ts create mode 100644 packages/connector-telegram/src/manifest.test.ts create mode 100644 packages/connector-telegram/src/telegram-html-formatter.ts create mode 100644 packages/connector-telegram/src/token.ts create mode 100644 packages/connector-telegram/src/types.ts create mode 100644 packages/connector-telegram/tsconfig.build.json create mode 100644 packages/connector-telegram/tsconfig.test.json create mode 100644 packages/connector-wecom-agent/README.md create mode 100644 packages/connector-wecom-agent/assets/icon.svg create mode 100644 packages/connector-wecom-agent/npm-shrinkwrap.json create mode 100644 packages/connector-wecom-agent/package.json create mode 100644 packages/connector-wecom-agent/plugin.yaml create mode 100644 packages/connector-wecom-agent/src/WeComAgentAdapter.test.ts create mode 100644 packages/connector-wecom-agent/src/WeComAgentAdapter.ts create mode 100644 packages/connector-wecom-agent/src/index.ts create mode 100644 packages/connector-wecom-agent/src/manifest.test.ts create mode 100644 packages/connector-wecom-agent/src/types.ts create mode 100644 packages/connector-wecom-agent/tsconfig.build.json create mode 100644 packages/connector-wecom-agent/tsconfig.test.json create mode 100644 packages/connector-wecom-bot/README.md create mode 100644 packages/connector-wecom-bot/assets/icon.svg create mode 100644 packages/connector-wecom-bot/npm-shrinkwrap.json create mode 100644 packages/connector-wecom-bot/package.json create mode 100644 packages/connector-wecom-bot/plugin.yaml create mode 100644 packages/connector-wecom-bot/src/WeComBotAdapter.test.ts create mode 100644 packages/connector-wecom-bot/src/WeComBotAdapter.ts create mode 100644 packages/connector-wecom-bot/src/index.ts create mode 100644 packages/connector-wecom-bot/src/manifest.test.ts create mode 100644 packages/connector-wecom-bot/src/types.ts create mode 100644 packages/connector-wecom-bot/tsconfig.build.json create mode 100644 packages/connector-wecom-bot/tsconfig.test.json create mode 100644 packages/connector-xiaoyi/README.md create mode 100644 packages/connector-xiaoyi/assets/icon.svg create mode 100644 packages/connector-xiaoyi/npm-shrinkwrap.json create mode 100644 packages/connector-xiaoyi/package.json create mode 100644 packages/connector-xiaoyi/plugin.yaml create mode 100644 packages/connector-xiaoyi/src/XiaoyiAdapter.test.ts create mode 100644 packages/connector-xiaoyi/src/XiaoyiAdapter.ts create mode 100644 packages/connector-xiaoyi/src/index.ts create mode 100644 packages/connector-xiaoyi/src/manifest.test.ts create mode 100644 packages/connector-xiaoyi/src/types.ts create mode 100644 packages/connector-xiaoyi/src/xiaoyi-protocol.test.ts create mode 100644 packages/connector-xiaoyi/src/xiaoyi-protocol.ts create mode 100644 packages/connector-xiaoyi/src/xiaoyi-ws.ts create mode 100644 packages/connector-xiaoyi/tsconfig.build.json create mode 100644 packages/connector-xiaoyi/tsconfig.test.json create mode 100644 packages/plugin-sdk/src/host-bound-session.test.ts create mode 100644 packages/plugin-sdk/src/host-bound-session.ts create mode 100644 packages/plugin-sdk/src/messaging-client.test.ts create mode 100644 packages/plugin-sdk/src/messaging-client.ts create mode 100644 packages/video-generation/README.md create mode 100644 packages/video-generation/assets/icon.svg create mode 100644 packages/video-generation/npm-shrinkwrap.json create mode 100644 packages/video-generation/package.json create mode 100644 packages/video-generation/plugin.yaml create mode 100644 packages/video-generation/protocols/jimeng.yaml create mode 100644 packages/video-generation/protocols/kling.yaml create mode 100644 packages/video-generation/protocols/zhipu.yaml create mode 100644 packages/video-generation/src/callback-client.ts create mode 100644 packages/video-generation/src/index.ts create mode 100644 packages/video-generation/src/manifest.test.ts create mode 100644 packages/video-generation/src/mcp-entrypoint.ts create mode 100644 packages/video-generation/src/protocol-engine.test.ts create mode 100644 packages/video-generation/src/protocol-engine/auth/apikey.ts create mode 100644 packages/video-generation/src/protocol-engine/auth/credentials.ts create mode 100644 packages/video-generation/src/protocol-engine/auth/hmac-sha256-v4.ts create mode 100644 packages/video-generation/src/protocol-engine/auth/index.ts create mode 100644 packages/video-generation/src/protocol-engine/auth/jwt-hs256.ts create mode 100644 packages/video-generation/src/protocol-engine/engine.ts create mode 100644 packages/video-generation/src/protocol-engine/index.ts create mode 100644 packages/video-generation/src/protocol-engine/loader.ts create mode 100644 packages/video-generation/src/protocol-engine/template-utils.ts create mode 100644 packages/video-generation/src/protocol-engine/types.ts create mode 100644 packages/video-generation/src/protocol-tools.ts create mode 100644 packages/video-generation/src/tool-result.ts create mode 100644 packages/video-generation/tsconfig.build.json create mode 100644 packages/video-generation/tsconfig.json create mode 100644 packages/video-generation/tsconfig.test.json create mode 100644 packages/wechat-visible-reader/README.md create mode 100644 packages/wechat-visible-reader/assets/icon.svg create mode 100644 packages/wechat-visible-reader/limbs/wechat-visible-reader.yml create mode 100644 packages/wechat-visible-reader/native/WeChatConversationNavigator.swift create mode 100644 packages/wechat-visible-reader/native/WeChatLayoutGuard.swift create mode 100644 packages/wechat-visible-reader/native/WeChatNavigationFixtures.swift create mode 100644 packages/wechat-visible-reader/native/WeChatNavigationModels.swift create mode 100644 packages/wechat-visible-reader/native/WeChatReaderCore.swift create mode 100644 packages/wechat-visible-reader/native/WeChatReaderModels.swift create mode 100644 packages/wechat-visible-reader/native/WeChatVisibleReader.swift create mode 100644 packages/wechat-visible-reader/npm-shrinkwrap.json create mode 100644 packages/wechat-visible-reader/package.json create mode 100644 packages/wechat-visible-reader/plugin.yaml create mode 100644 packages/wechat-visible-reader/src/WeChatVisibleReaderArmStore.ts create mode 100644 packages/wechat-visible-reader/src/WeChatVisibleReaderMetrics.ts create mode 100644 packages/wechat-visible-reader/src/handlers.ts create mode 100644 packages/wechat-visible-reader/src/index.ts create mode 100644 packages/wechat-visible-reader/src/manifest.test.ts create mode 100644 packages/wechat-visible-reader/src/native-runner.ts create mode 100644 packages/wechat-visible-reader/src/types.ts create mode 100644 packages/wechat-visible-reader/src/wechat-visible-reader.test.ts create mode 100644 packages/wechat-visible-reader/tsconfig.build.json create mode 100644 packages/wechat-visible-reader/tsconfig.test.json create mode 100644 packages/weixin-mp/README.md create mode 100644 packages/weixin-mp/assets/icon.svg create mode 100644 packages/weixin-mp/limbs/weixin-mp.yml create mode 100644 packages/weixin-mp/npm-shrinkwrap.json create mode 100644 packages/weixin-mp/package.json create mode 100644 packages/weixin-mp/plugin.yaml create mode 100644 packages/weixin-mp/skills/weixin-mp/SKILL.md create mode 100644 packages/weixin-mp/src/handlers.ts create mode 100644 packages/weixin-mp/src/index.ts create mode 100644 packages/weixin-mp/src/manifest.test.ts create mode 100644 packages/weixin-mp/src/markdown-to-wx-html.ts create mode 100644 packages/weixin-mp/src/safe-fetch.ts create mode 100644 packages/weixin-mp/src/security.test.ts create mode 100644 packages/weixin-mp/tsconfig.build.json create mode 100644 packages/weixin-mp/tsconfig.test.json create mode 100644 scripts/catalog-runtime-entrypoints.mjs create mode 100644 scripts/catalog-runtime-entrypoints.test.mjs create mode 100644 scripts/train-c1-inventory.test.mjs diff --git a/.github/workflows/contract-ci.yml b/.github/workflows/contract-ci.yml index 0d1ebcc9..2c2f46a2 100644 --- a/.github/workflows/contract-ci.yml +++ b/.github/workflows/contract-ci.yml @@ -15,6 +15,9 @@ on: - 'packages/companion/**' - 'packages/feishu-meeting-intake/**' - 'packages/video-analysis/**' + - 'packages/video-generation/**' + - 'packages/wechat-visible-reader/**' + - 'packages/weixin-mp/**' - 'packages/genoffice-docx/**' - 'catalog/**' - 'scripts/fresh-consumer-install.test.mjs' @@ -111,6 +114,42 @@ jobs: - name: Video analysis build run: pnpm --filter @clowder-ai/video-analysis build + - name: Video generation typecheck + run: pnpm --filter @clowder-ai/video-generation typecheck + + - name: Video generation tests + run: pnpm --filter @clowder-ai/video-generation test + + - name: Video generation lint + run: pnpm --filter @clowder-ai/video-generation lint + + - name: Video generation build + run: pnpm --filter @clowder-ai/video-generation build + + - name: Weixin MP typecheck + run: pnpm --filter @clowder-ai/weixin-mp typecheck + + - name: Weixin MP tests + run: pnpm --filter @clowder-ai/weixin-mp test + + - name: Weixin MP lint + run: pnpm --filter @clowder-ai/weixin-mp lint + + - name: Weixin MP build + run: pnpm --filter @clowder-ai/weixin-mp build + + - name: WeChat visible reader typecheck + run: pnpm --filter @clowder-ai/wechat-visible-reader typecheck + + - name: WeChat visible reader tests + run: pnpm --filter @clowder-ai/wechat-visible-reader test + + - name: WeChat visible reader lint + run: pnpm --filter @clowder-ai/wechat-visible-reader lint + + - name: WeChat visible reader build + run: pnpm --filter @clowder-ai/wechat-visible-reader build + - name: GenOffice typecheck and tests run: | pnpm --filter @clowder-ai/genoffice-docx typecheck @@ -311,6 +350,12 @@ jobs: - name: Build video analysis package run: pnpm --filter @clowder-ai/video-analysis build + - name: Build video generation package + run: pnpm --filter @clowder-ai/video-generation build + + - name: Build Weixin MP package + run: pnpm --filter @clowder-ai/weixin-mp build + - name: Build and verify frozen GenOffice renderer run: | pnpm --filter @clowder-ai/genoffice-docx build:renderer @@ -340,6 +385,18 @@ jobs: package-directory: packages/video-analysis npm-token: ${{ secrets.NPM_TOKEN }} + - name: Publish video generation prerelease + uses: ./.github/actions/publish-prerelease + with: + package-directory: packages/video-generation + npm-token: ${{ secrets.NPM_TOKEN }} + + - name: Publish Weixin MP prerelease + uses: ./.github/actions/publish-prerelease + with: + package-directory: packages/weixin-mp + npm-token: ${{ secrets.NPM_TOKEN }} + - name: Publish GenOffice prerelease uses: ./.github/actions/publish-prerelease with: diff --git a/catalog/catalog.json b/catalog/catalog.json index bde2f702..f561617c 100644 --- a/catalog/catalog.json +++ b/catalog/catalog.json @@ -121,6 +121,48 @@ } ] }, + { + "pluginId": "dev.clowder.video-generation", + "name": "Video Generation", + "description": { + "default": "Submit and track image or video generation jobs through a configured provider.", + "translations": { + "zh-CN": "通过已配置的提供商提交并跟踪图片或视频生成任务。" + } + }, + "icon": { + "type": "svg", + "src": "assets/icon.svg" + }, + "publisher": { + "id": "clowder-ai", + "name": "Clowder AI" + }, + "keywords": [ + "generation", + "image", + "video" + ], + "versions": [ + { + "version": "0.1.0-alpha.0", + "contractVersion": "0.1.0", + "manifestPath": "plugin.yaml", + "artifact": { + "kind": "npm", + "packageName": "@clowder-ai/video-generation", + "version": "0.1.0-alpha.0", + "tarballUrl": "https://registry.npmjs.org/@clowder-ai/video-generation/-/video-generation-0.1.0-alpha.0.tgz", + "integrity": "sha512-jr5W7aAKHK49iIHAo7T8uh5f2wwoLWj82jRsRLXMMQlkdEG8Gu58Vu+BwMEZiTgP9YoVFHxtrs0Jd/ZEMkTyXg==", + "shasum": "4b7696e59e7d79414cd5309768ca18cdd5825a9c", + "provenance": { + "repository": "https://github.com/zts212653/clowder-ai-plugins", + "sourceDirectory": "packages/video-generation" + } + } + } + ] + }, { "pluginId": "official.companion", "name": "猫猫球", @@ -163,6 +205,90 @@ } } ] + }, + { + "pluginId": "official.wechat-visible-reader", + "name": "WeChat Visible Reader", + "description": { + "default": "Read visible Mac WeChat messages under explicit, short-lived local authorization.", + "translations": { + "zh-CN": "在明确、短时的本机授权下读取 Mac 微信可见消息。" + } + }, + "icon": { + "type": "svg", + "src": "assets/icon.svg" + }, + "publisher": { + "id": "clowder-ai", + "name": "Clowder AI" + }, + "keywords": [ + "macos", + "reader", + "wechat" + ], + "versions": [ + { + "version": "0.1.0-alpha.0", + "contractVersion": "0.1.0", + "manifestPath": "plugin.yaml", + "artifact": { + "kind": "npm", + "packageName": "@clowder-ai/wechat-visible-reader", + "version": "0.1.0-alpha.0", + "tarballUrl": "https://registry.npmjs.org/@clowder-ai/wechat-visible-reader/-/wechat-visible-reader-0.1.0-alpha.0.tgz", + "integrity": "sha512-rBpA0hYtiBYjchQUO7PoDw3nYOmRf/X+X9r3jwvaSLXoB7f8arAA4PUOW9zp6PZVH/3Ms1pV3jqu8oHg11sXpA==", + "shasum": "3423c791e30597def78ea9565a2a17c364760ac0", + "provenance": { + "repository": "https://github.com/zts212653/clowder-ai-plugins", + "sourceDirectory": "packages/wechat-visible-reader" + } + } + } + ] + }, + { + "pluginId": "official.weixin-mp", + "name": "WeChat Official Account", + "description": { + "default": "Prepare, upload, publish, and inspect WeChat Official Account content.", + "translations": { + "zh-CN": "准备、上传、发布并查看微信公众号内容。" + } + }, + "icon": { + "type": "svg", + "src": "assets/icon.svg" + }, + "publisher": { + "id": "clowder-ai", + "name": "Clowder AI" + }, + "keywords": [ + "publishing", + "wechat", + "weixin" + ], + "versions": [ + { + "version": "0.1.0-alpha.0", + "contractVersion": "0.1.0", + "manifestPath": "plugin.yaml", + "artifact": { + "kind": "npm", + "packageName": "@clowder-ai/weixin-mp", + "version": "0.1.0-alpha.0", + "tarballUrl": "https://registry.npmjs.org/@clowder-ai/weixin-mp/-/weixin-mp-0.1.0-alpha.0.tgz", + "integrity": "sha512-JZCty6kFXgJuowblan9nJQ4KVkENPZ0QVz/kg7NzaT283mvG7WiXnAbHTgqYYm2sTBhtWM5vDn8sMy5gNgXYjQ==", + "shasum": "c80999d55337418361be8786323aebb990d7b949", + "provenance": { + "repository": "https://github.com/zts212653/clowder-ai-plugins", + "sourceDirectory": "packages/weixin-mp" + } + } + } + ] } ] } diff --git a/docs/plans/2026-09-19-train-c1-plugins-aggregate-migration.md b/docs/plans/2026-09-19-train-c1-plugins-aggregate-migration.md new file mode 100644 index 00000000..9f3c3696 --- /dev/null +++ b/docs/plans/2026-09-19-train-c1-plugins-aggregate-migration.md @@ -0,0 +1,140 @@ +--- +title: Train C1 Plugins aggregate migration plan +feature_ids: [P-1, F202] +topics: [plugin-migration, im-connectors, business-plugins, catalog, release] +doc_kind: plan +created: 2026-09-19 +--- + +# Train C1 Plugins aggregate migration plan + +## Outcome + +One Plugins PR turns every remaining in-scope Core IM connector and repository-local business plugin into +package-owned, cataloged artifacts. Core then consumes those exact reviewed artifacts, maps existing +configuration/bindings/data, proves no double-run, switches the default path, and deletes the old business +implementations in its separate deletion-dominant C1 PR. + +The machine-readable frozen inventory is +[`migration/f202-train-c1-inventory.json`](../../migration/f202-train-c1-inventory.json). It is pinned to +Plugins `123112c`, Core `9ab0eaf287381efcb209781463f38cc5f23870ea`, and accepted Core issue #1478. + +## Frozen boundary + +### Migrated in C1 + +- IM connectors: DingTalk, Feishu, Telegram, WeCom Agent, WeCom Bot, Weixin, and XiaoYi. +- Repository-local business plugins: GitHub operations, video generation, WeChat visible reader, and Weixin + Official Account. +- Existing external artifacts are census baselines rather than duplicate implementations: + `@clowder-ai/video-analysis`, `@clowder-ai/personal-chrome-companion`, and + `@clowder-ai/feishu-meeting-intake`. + +### Not migrated in C1 + +- ASR, TTS, audio capture, embedding, and LLM post-processing managed services belong to Train C2. +- C1 does not add public UI slots, generic public hooks, or another Host authority layer. +- Artifact verification, grants, secret/config authority, inventory, connector/thread bindings, delivery + retry/dead-letter policy, and lifecycle supervision remain Host-owned. + +This supersedes the pre-#1478 sentence in the Train B plan that placed managed services in undifferentiated +Train C. The accepted split is C1 connectors/business plugins and C2 public extension seams/managed services. + +## Code-derived consumer census + +The inventory records the source roots, config/secrets, binding and data truth, current consumers, dedicated +journeys, target package/catalog identity, runtime needs, and rollback for each item. The census is derived +from Core source at the pinned commit rather than from UI labels or an old planning list. + +The shared execution shape is: + +```text +package-owned provider/business implementation + ↕ public plugin contract + SDK +Host-owned install/grants/config/secrets/lifecycle/bindings/delivery + ↕ +existing Core Agent, Console, messaging, schedule and webhook consumers +``` + +The public 13-row wire already contains `messaging.send` and `host.messaging.deliver`, but that is not yet a +complete connector lifecycle. `ConnectorBindingAddress` can carry an already-issued opaque handle; an +external package currently has no public Host route to resolve or create that handle from +`(connectorId, externalChatId)`, nor can it recover the corresponding external-chat coordinate for outbound +delivery after restart. Core C1 therefore owns one business-blind activation prerequisite: Host-issued +connector-binding bootstrap/lookup and restart-safe outbound coordinates. The current external-runtime +supervisor also projects only four `CLOWDER_*` protocol metadata variables; the connector contribution has +no environment binding and the frozen wire has no config/secret read row. Core C1 must therefore project +only manifest-declared configuration and secrets into the verified package process (or provide an +equivalently typed Host-owned read path). These are parts of activating the existing connector contribution, +not a C2 mention parser, UI slot, or package-owned authority seam. + +Restart-safe provider cursors are a third verified prerequisite. Although `plugin.state.get` and +`plugin.state.set` are reserved capability names, they are absent from the frozen wire registry and Core has +no handler, durable store, or external-runtime composition path for them. Packages must not substitute local +files, ambient Redis, messaging subscription cursors, seven-day settlement receipts, or inventory snapshots. +The stable design assumptions are Host-derived own-instance namespaces, manifest-declared keys, TTL=0, +compare-and-swap plus operation-id idempotency, and settlement-coupled checkpoint commits so crash replay +converges without skipping or duplicating a delivered message. Concrete SDK signatures remain blocked until +the public contract delta is approved. + +The SDK does not yet expose a connector-facing session that combines handshake, Host requests, and +plugin-originating messaging requests. C1 adds the part that can be implemented against the frozen rows and +keeps provider runtime wiring blocked on the exact Host prerequisite above. Existing `connector`, `webhook`, +and `schedule` contribution declarations remain the static activation contract; packages must not invent an +ambient `.env` fallback, configured handle, or synthesized binding state to conceal the missing Host path. + +Connector ingress always sends to a Host-authenticated `connector_binding` handle. Packages neither mint nor +resolve that handle and never infer wake authority from message text. In particular, `@` inside a plain +`thread_handle` message remains opaque, non-waking content; only Core may derive admission/wake behavior from +an authenticated connector binding. Contract/SDK fixtures lock both the connector-binding positive path and +the ordinary-text negative path. + +## Test-first implementation sequence + +1. Freeze this inventory and make `scripts/train-c1-inventory.test.mjs` RED because the eleven target + packages and catalog entries do not exist. +2. Add SDK RED tests for a standalone connector session: ready handshake, outbound `messaging.send`, inbound + `host.messaging.deliver`, grants change, ping, drain, and deterministic rejection after drain. Reuse the + frozen wire rows. Synthetic binding handles are valid only for protocol-unit tests; they are not evidence + that a provider runtime can bootstrap a real binding. +3. Hold provider runtime wiring at the Host boundary until Core supplies business-blind binding + bootstrap/lookup, declared config/secret projection, and durable checkpoint paths. The checkpoint path is + an unapproved public wire/schema delta, so restart/resume stays RED and no concrete SDK signature is + frozen until approval; never hide any gap in package config or package-local persistence. +4. Migrate provider-neutral connector fixtures first, then each provider adapter. Every package gets + `plugin.yaml`, README, icon, locked production dependencies, provider protocol tests, restart/drain tests, + and an isolated fake-provider journey. +5. Migrate the four business plugins with their actual runtime surfaces: MCP for video generation, limb/skill + for Weixin MP, limb plus the macOS native helper for visible reader, and schedule/event contributions for + GitHub operations. Package code imports no Core private path. +6. Add all eleven packages to the deterministic catalog. Extend exact-pack validation and fresh-consumer + installation to every artifact; catalog metadata must equal packed manifest truth. +7. Run the aggregate quality gate and obtain independent cross-individual review on the exact HEAD. Leave + the PR unmerged and unpublished until maintainer authority acts. + +## Preservation matrix and acceptance + +Every migrated entry must prove: + +- configuration values and secrets keep the same user meaning, sensitivity, required/conditional rules, and + setup operation; +- binding identity and durable cursors remain Host-owned and survive package restart/rollback; +- inbound and outbound journeys cannot run in both package and Core implementations simultaneously; +- provider retry, formatting, upload/media, deduplication, credential scrubbing, and platform-specific + behavior remain covered by the listed dedicated journeys; +- disable/drain/restart/resume/rollback settle honestly without losing acknowledged messages; +- package install works in a blank consumer using only public exports and isolated fixtures. + +Terminal evidence for the aggregate PR is `pnpm build`, `pnpm typecheck`, `pnpm lint`, `pnpm test`, +`pnpm conformance`, `pnpm catalog:check`, `pnpm test:fresh-consumer`, plus exact version/SHA-1/SHA-512 and +archive-member evidence for all new packages under Node 24.18.0, npm 11.16.0, zlib 1.3.1-e00f703. + +## Release and rollback + +The Plugins PR is additive. Its merge does not activate, cut over, or delete a Core implementation. Release +publishes one reviewed generation of contract/SDK (only if changed), all eleven packages, and the catalog. +Core pins the exact coordinates and performs per-item migration under a global no-double-run gate. + +Rollback is therefore two-stage and recoverable: Core first drains/disables package runtimes and restores +the preserved Core routes against unchanged Host-owned config/bindings/checkpoints; only then may a bad +catalog generation be superseded. Published npm bytes are never rewritten or manually unpublished. diff --git a/migration/f202-train-c1-inventory.json b/migration/f202-train-c1-inventory.json new file mode 100644 index 00000000..eb539d03 --- /dev/null +++ b/migration/f202-train-c1-inventory.json @@ -0,0 +1,390 @@ +{ + "schemaVersion": 1, + "subject": "subject:f202:train-c1-plugins-aggregate-migration", + "scopeAuthority": { + "operatorMessage": "0001789821850712-001832-35062316", + "acceptedCoreIssue": "zts212653/clowder-ai#1478", + "pluginsBaseCommit": "123112c", + "coreSourceCommit": "9ab0eaf287381efcb209781463f38cc5f23870ea" + }, + "hostKernelRetained": [ + "artifact verification and local inventory", + "grants, config, and secret authority", + "connector/thread binding store", + "message delivery, retry, and dead-letter policy", + "lifecycle supervision and no-double-run cutover", + "generic webhook and schedule activation", + "Console and Agent projections" + ], + "trustBoundary": { + "connectorIngressTarget": "connector_binding", + "bindingAuthority": "Host", + "wakeAuthority": "Host", + "connectorBindingBootstrap": { + "owner": "Core Host", + "status": "required-prerequisite", + "missingAtCoreSourceCommit": "No production caller issues or restores connector_binding handles for an external package runtime", + "requiredBehavior": [ + "resolve or create an opaque binding handle for an authenticated plugin instance and connector/external-chat coordinate", + "provide restart-safe outbound external-chat coordinates without exposing Host wake policy to the package" + ] + }, + "runtimeConfigurationProjection": { + "owner": "Core Host", + "status": "required-prerequisite", + "missingAtCoreSourceCommit": "External stdio supervisor injects only four CLOWDER protocol metadata values and exposes no connector config/secret read path", + "requiredBehavior": [ + "project only manifest-declared configuration and secrets into the verified package runtime", + "preserve secret redaction and deny ambient Host environment access" + ] + }, + "durableCheckpointAuthority": { + "owner": "Core Host", + "status": "required-prerequisite", + "missingAtCoreSourceCommit": "plugin.state.get/set are reserved capability names but are absent from the frozen wire registry and have no Core handler, store, or external-runtime composition path", + "stableAssumptions": [ + "namespace derives from authenticated plugin instance, contribution, and a manifest-declared key", + "checkpoint values persist with TTL=0 and survive restart or rollback of the same installed instance", + "writes use compare-and-swap plus operation-id idempotency", + "delivery checkpoints advance only after the corresponding messaging.send settlement so replay converges without skipped or duplicate messages", + "checkpoint values contain operational cursors, sequences, resume tokens, or deduplication watermarks only and never messages or credentials" + ], + "signatureStatus": "unapproved-public-contract-delta" + }, + "ordinaryPluginText": "opaque non-waking content, including text containing @", + "forbiddenPackageBehavior": [ + "minting or resolving connector binding handles", + "parsing mentions to derive wake authority", + "supplying a raw thread identity as authenticated connector ingress", + "using package config or environment values as a substitute for Host-issued connector binding state", + "reading repository or ambient Host environment as a substitute for manifest-declared runtime configuration", + "persisting cursors, sequences, resume tokens, or deduplication watermarks in package-local files or ambient stores", + "reusing messaging subscription cursors, settlement receipts, or inventory snapshots as connector checkpoint state" + ] + }, + "entries": [ + { + "id": "dingtalk", + "category": "im-connector", + "disposition": "migrate", + "sourceRoots": [ + "packages/api/src/infrastructure/connectors/im-connectors/dingtalk" + ], + "configKeys": ["DINGTALK_APP_KEY"], + "secretKeys": ["DINGTALK_APP_SECRET"], + "bindingData": ["Host-owned connector/thread binding keyed by connector instance"], + "data": ["Provider access token cache is runtime-ephemeral and must not become package inventory truth"], + "currentConsumers": [ + "packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts", + "packages/api/src/infrastructure/connectors/ConnectorCommandLayer.ts", + "packages/web/src/components/HubListModal.tsx" + ], + "dedicatedJourneys": ["packages/api/test/dingtalk-adapter.test.js"], + "targetPackage": "@clowder-ai/connector-dingtalk", + "catalogPluginId": "official.connector.dingtalk", + "runtimeRequirements": ["standalone stdio", "messaging.send", "host.messaging.deliver"], + "rollback": "Disable the installed package and re-enable the preserved Core adapter before deleting old code." + }, + { + "id": "feishu", + "category": "im-connector", + "disposition": "migrate", + "sourceRoots": [ + "packages/api/src/infrastructure/connectors/im-connectors/feishu" + ], + "configKeys": ["FEISHU_APP_ID", "FEISHU_CONNECTION_MODE", "FEISHU_GROUP_BOT_MENTIONS_JSON"], + "secretKeys": ["FEISHU_APP_SECRET", "FEISHU_VERIFICATION_TOKEN"], + "bindingData": ["Host-owned connector/thread binding", "QR operation state and connection mode"], + "data": ["Tenant token cache is runtime-ephemeral", "Group-bot mention aliases remain configuration"], + "currentConsumers": [ + "packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts", + "packages/api/src/infrastructure/connectors/FeishuQrBindClient.ts", + "packages/api/src/routes/connector-hub.ts", + "packages/web/src/components/HubListModal.tsx" + ], + "dedicatedJourneys": [ + "packages/api/test/feishu-adapter.test.js", + "packages/api/test/feishu-adapter-upload.test.js", + "packages/api/test/feishu-action-handler.test.js", + "packages/api/test/connector-phase-b4-integration.test.js" + ], + "targetPackage": "@clowder-ai/connector-feishu", + "catalogPluginId": "official.connector.feishu", + "runtimeRequirements": ["standalone stdio", "messaging.send", "host.messaging.deliver", "existing webhook contribution activation", "QR setup operation"], + "rollback": "Keep binding/config records Host-owned; switch the connector instance back to the preserved Core adapter without rewriting thread identities." + }, + { + "id": "telegram", + "category": "im-connector", + "disposition": "migrate", + "sourceRoots": [ + "packages/api/src/infrastructure/connectors/im-connectors/telegram" + ], + "configKeys": [], + "secretKeys": ["TELEGRAM_BOT_TOKEN"], + "bindingData": ["Host-owned connector/thread binding keyed by Telegram chat identity"], + "data": ["Polling cursor is runtime checkpoint data and must resume without duplicate delivery"], + "currentConsumers": [ + "packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts", + "packages/api/src/routes/connector-hub.ts", + "packages/web/src/components/HubListModal.tsx" + ], + "dedicatedJourneys": [ + "packages/api/test/telegram-adapter.test.js", + "packages/api/test/telegram-html-formatter.test.js" + ], + "targetPackage": "@clowder-ai/connector-telegram", + "catalogPluginId": "official.connector.telegram", + "runtimeRequirements": ["standalone stdio", "messaging.send", "host.messaging.deliver", "restart-safe polling cursor"], + "rollback": "Stop the package before starting the Core poller; reuse the Host-owned binding and last committed cursor." + }, + { + "id": "wecom-agent", + "category": "im-connector", + "disposition": "migrate", + "sourceRoots": [ + "packages/api/src/infrastructure/connectors/im-connectors/wecom-agent" + ], + "configKeys": ["WECOM_CORP_ID", "WECOM_AGENT_ID"], + "secretKeys": ["WECOM_AGENT_SECRET", "WECOM_TOKEN", "WECOM_ENCODING_AES_KEY"], + "bindingData": ["Host-owned connector/thread binding keyed by WeCom user or room identity"], + "data": ["Access token cache is runtime-ephemeral", "Webhook replay/dedup facts remain Host-owned"], + "currentConsumers": [ + "packages/api/src/infrastructure/connectors/im-connector-loader.ts", + "packages/api/src/infrastructure/connectors/ConnectorCommandLayer.ts", + "packages/web/src/components/HubListModal.tsx" + ], + "dedicatedJourneys": ["packages/api/test/wecom-agent-adapter.test.js"], + "targetPackage": "@clowder-ai/connector-wecom-agent", + "catalogPluginId": "official.connector.wecom-agent", + "runtimeRequirements": ["standalone stdio", "messaging.send", "host.messaging.deliver", "existing webhook contribution activation"], + "rollback": "Route the callback to the preserved Core adapter only after the package runtime drains; never run both webhook consumers." + }, + { + "id": "wecom-bot", + "category": "im-connector", + "disposition": "migrate", + "sourceRoots": [ + "packages/api/src/infrastructure/connectors/im-connectors/wecom-bot" + ], + "configKeys": ["WECOM_BOT_ID"], + "secretKeys": ["WECOM_BOT_SECRET"], + "bindingData": ["Host-owned connector/thread binding", "Validate/connect operation state"], + "data": ["WebSocket resume state is runtime checkpoint data"], + "currentConsumers": [ + "packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts", + "packages/api/src/routes/connector-hub.ts", + "packages/web/src/components/HubListModal.tsx" + ], + "dedicatedJourneys": ["packages/api/test/wecom-bot-adapter.test.js"], + "targetPackage": "@clowder-ai/connector-wecom-bot", + "catalogPluginId": "official.connector.wecom-bot", + "runtimeRequirements": ["standalone stdio", "messaging.send", "host.messaging.deliver", "restart-safe WebSocket resume"], + "rollback": "Disconnect and drain the package WebSocket before reconnecting the preserved Core adapter." + }, + { + "id": "weixin", + "category": "im-connector", + "disposition": "migrate", + "sourceRoots": [ + "packages/api/src/infrastructure/connectors/im-connectors/weixin" + ], + "configKeys": [], + "secretKeys": ["WEIXIN_BOT_TOKEN"], + "bindingData": ["Host-owned connector/thread binding", "QR login operation state"], + "data": ["CDN media is fetched transiently and must not be retained by package inventory"], + "currentConsumers": [ + "packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts", + "packages/api/src/routes/connector-hub.ts", + "packages/web/src/components/HubListModal.tsx" + ], + "dedicatedJourneys": [ + "packages/api/test/weixin-adapter.test.js", + "packages/api/test/weixin-cdn.test.js", + "packages/api/test/connector-action-endpoint.test.js" + ], + "targetPackage": "@clowder-ai/connector-weixin", + "catalogPluginId": "official.connector.weixin", + "runtimeRequirements": ["standalone stdio", "messaging.send", "host.messaging.deliver", "QR setup operation"], + "rollback": "Drain the package connection and restore the Core adapter against the same Host-owned credential and bindings." + }, + { + "id": "xiaoyi", + "category": "im-connector", + "disposition": "migrate", + "sourceRoots": [ + "packages/api/src/infrastructure/connectors/im-connectors/xiaoyi" + ], + "configKeys": ["XIAOYI_AK", "XIAOYI_AGENT_ID"], + "secretKeys": ["XIAOYI_SK"], + "bindingData": ["Host-owned connector/thread binding keyed by XiaoYi conversation identity"], + "data": ["WebSocket sequence/resume state is runtime checkpoint data"], + "currentConsumers": [ + "packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts", + "packages/api/src/infrastructure/connectors/im-connector-loader.ts", + "packages/web/src/components/HubListModal.tsx" + ], + "dedicatedJourneys": [ + "packages/api/test/xiaoyi-adapter.test.js", + "packages/api/test/xiaoyi-protocol.test.js" + ], + "targetPackage": "@clowder-ai/connector-xiaoyi", + "catalogPluginId": "official.connector.xiaoyi", + "runtimeRequirements": ["standalone stdio", "messaging.send", "host.messaging.deliver", "restart-safe WebSocket resume"], + "rollback": "Drain the package session before reconnecting the preserved Core WebSocket adapter with the last Host checkpoint." + }, + { + "id": "github", + "category": "business-plugin", + "disposition": "migrate", + "sourceRoots": ["packages/api/src/plugins/github"], + "configKeys": ["GITHUB_SETUP_NOISE_BOT_LOGINS"], + "secretKeys": ["GITHUB_TOKEN", "GITHUB_MCP_PAT"], + "bindingData": ["Repository tracking registrations and authenticated GitHub identity stay Host-owned"], + "data": ["Schedule cursors, deduplication keys, and delivery leases stay Host-owned"], + "currentConsumers": [ + "packages/api/src/domains/plugin/PluginRegistry.ts", + "packages/api/src/domains/plugin/ScheduleFactoryRegistry.ts", + "packages/api/src/domains/plugin/github-schedule-factories.ts", + "packages/api/src/infrastructure/scheduler/index.ts" + ], + "dedicatedJourneys": [ + "packages/api/test/repo-scan-admission-gate-self-heal.test.js", + "packages/api/test/github-schedule-factories.test.js", + "packages/api/test/pr-review-event-wait-coverage.test.js", + "packages/api/test/gate-keeping-guard-register-issue-tracking.test.js" + ], + "targetPackage": "@clowder-ai/github-operations", + "catalogPluginId": "official.github-operations", + "runtimeRequirements": ["schedule contributions", "events.publish", "Host-owned tracking state"], + "rollback": "Disable package schedules before restoring Core factories against unchanged Host schedule state." + }, + { + "id": "video-gen", + "category": "business-plugin", + "disposition": "migrate", + "sourceRoots": ["packages/api/src/plugins/video-gen"], + "configKeys": ["VIDEO_GEN_PROVIDER", "VIDEO_GEN_AUTH_TYPE", "VIDEO_GEN_BASE_URL", "VIDEO_GEN_MODEL"], + "secretKeys": ["VIDEO_GEN_API_KEY", "VIDEO_GEN_ACCESS_KEY", "VIDEO_GEN_SECRET_KEY"], + "bindingData": ["No provider-specific Host binding; installed plugin identity is Host-owned"], + "data": ["Generated media references are returned to the caller; no package-owned durable inventory data"], + "currentConsumers": [ + "packages/api/src/domains/plugin/PluginRegistry.ts", + "packages/mcp-server/src/protocol-server.ts" + ], + "dedicatedJourneys": [ + "packages/api/test/mcp-config-adapters.test.js", + "packages/mcp-server/test/protocol-engine.test.js" + ], + "targetPackage": "@clowder-ai/video-generation", + "catalogPluginId": "dev.clowder.video-generation", + "runtimeRequirements": ["MCP contribution", "provider protocol validation", "credential scrubbing"], + "rollback": "Disable the installed MCP contribution and restore the Core protocol-server registration without changing configuration keys." + }, + { + "id": "wechat-visible-reader", + "category": "business-plugin", + "disposition": "migrate", + "sourceRoots": ["packages/api/src/plugins/wechat-visible-reader"], + "configKeys": [], + "secretKeys": [], + "bindingData": ["Ten-minute local authorization lease and selected thread stay Host-owned"], + "data": ["Screenshots are process-memory-only and must never be persisted", "Metrics contain no captured message text"], + "currentConsumers": [ + "packages/api/src/index.ts", + "packages/api/src/plugins/wechat-visible-reader/routes.ts", + "packages/web/src/components/settings/WeChatVisibleReaderArmControl.tsx" + ], + "dedicatedJourneys": [ + "packages/api/test/wechat-visible-reader-arm-routes.test.js", + "packages/api/test/wechat-visible-reader-native-runner.test.js", + "packages/api/test/wechat-conversation-recent-live-contract.test.js" + ], + "targetPackage": "@clowder-ai/wechat-visible-reader", + "catalogPluginId": "official.wechat-visible-reader", + "runtimeRequirements": ["limb contribution", "bounded local authorization", "macOS native executable"], + "rollback": "Revoke the package lease, stop its native helper, and restore the Core limb without carrying screenshots or transient OCR data." + }, + { + "id": "weixin-mp", + "category": "business-plugin", + "disposition": "migrate", + "sourceRoots": ["packages/api/src/plugins/weixin-mp"], + "configKeys": ["WEIXIN_MP_APP_ID"], + "secretKeys": ["WEIXIN_MP_APP_SECRET"], + "bindingData": ["No conversation binding; installed plugin identity and grants stay Host-owned"], + "data": ["Draft/article/media identifiers are provider-owned results and are not copied into plugin inventory"], + "currentConsumers": [ + "packages/api/src/index.ts", + "packages/api/src/plugins/weixin-mp/handlers.ts" + ], + "dedicatedJourneys": [ + "packages/api/test/weixin-mp-limb-node.test.js", + "packages/api/test/weixin-mp-path-security.test.js", + "packages/api/test/weixin-mp-security.test.js" + ], + "targetPackage": "@clowder-ai/weixin-mp", + "catalogPluginId": "official.weixin-mp", + "runtimeRequirements": ["limb contribution", "skill contribution", "health check"], + "rollback": "Disable the package limb and restore the Core limb against unchanged configuration and secret authority." + }, + { + "id": "video-analysis", + "category": "business-plugin", + "disposition": "already-migrated", + "sourceRoots": ["packages/api/src/plugins/video-analysis"], + "targetPackage": "@clowder-ai/video-analysis", + "catalogPluginId": "dev.clowder.video-analysis", + "acceptedVersion": "0.1.0-alpha.1", + "acceptedShasum": "e4e79a7426e9ad3c3e477f03af9cf2e2daabc488" + }, + { + "id": "cloud-cat-personal-host", + "category": "business-plugin", + "disposition": "already-externalized", + "sourceRoots": ["packages/api/src/plugins/cloud-cat-personal-host"], + "targetPackage": "@clowder-ai/personal-chrome-companion", + "catalogPluginId": null, + "note": "C1 must census Core consumers and deletion safety; do not duplicate the existing package implementation." + }, + { + "id": "feishu-meeting-intake", + "category": "business-plugin", + "disposition": "already-externalized", + "sourceRoots": [ + "packages/api/src/domains/plugin/official-plugin-meeting-intake.ts", + "packages/api/src/domains/signal-intake" + ], + "targetPackage": "@clowder-ai/feishu-meeting-intake", + "catalogPluginId": null, + "note": "Existing standalone package remains a dependency/cutover census item; C1 must not fork its implementation." + } + ], + "c2Exclusions": [ + { + "id": "audio-capture", + "sourceRoots": ["scripts/services/audio-capture-*"], + "reason": "Managed service and public audio seam belong to Train C2." + }, + { + "id": "asr", + "sourceRoots": ["scripts/services/whisper-*", "scripts/services/qwen_asr_backend.py"], + "reason": "TTS/ASR are explicitly deferred to Train C2." + }, + { + "id": "tts", + "sourceRoots": ["scripts/services/tts-*"], + "reason": "TTS/ASR are explicitly deferred to Train C2." + }, + { + "id": "embedding-model", + "sourceRoots": ["scripts/services/embed-*", "scripts/services/dedicated_model_worker.py"], + "reason": "Managed model service lifecycle belongs to Train C2." + }, + { + "id": "llm-postprocess", + "sourceRoots": ["scripts/services/llm-postprocess-*"], + "reason": "Managed model service lifecycle belongs to Train C2." + } + ] +} diff --git a/package.json b/package.json index 672111d5..f16e93bd 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,11 @@ "build": "pnpm -r build", "test": "pnpm -r --workspace-concurrency=1 test", "test:fresh-consumer": "node --test scripts/fresh-consumer-install.test.mjs", + "test:train-c1-inventory": "node --test scripts/train-c1-inventory.test.mjs", "typecheck": "pnpm -r typecheck", "lint": "pnpm -r lint", "conformance": "pnpm --filter @clowder-ai/plugin-contract conformance", - "catalog:check": "pnpm --filter @clowder-ai/plugin-contract build && pnpm --filter @clowder-ai/video-analysis build && pnpm --filter @clowder-ai/plugin-sdk build && pnpm --filter @clowder-ai/companion build && node --test scripts/catalog-static-package.test.mjs && node scripts/catalog-check.mjs" + "catalog:check": "pnpm --filter @clowder-ai/plugin-contract build && pnpm --filter @clowder-ai/video-analysis build && pnpm --filter @clowder-ai/video-generation build && pnpm --filter @clowder-ai/wechat-visible-reader build && pnpm --filter @clowder-ai/weixin-mp build && pnpm --filter @clowder-ai/plugin-sdk build && pnpm --filter @clowder-ai/companion build && node --test scripts/catalog-static-package.test.mjs && node scripts/catalog-check.mjs" }, "devDependencies": { "yaml": "2.9.0" diff --git a/packages/connector-dingtalk/README.md b/packages/connector-dingtalk/README.md new file mode 100644 index 00000000..2231e912 --- /dev/null +++ b/packages/connector-dingtalk/README.md @@ -0,0 +1,32 @@ +# DingTalk Connector + +`@clowder-ai/connector-dingtalk` packages DingTalk Stream ingress and OpenAPI/AI Card egress for a Clowder AI +Host. It parses direct and group messages, preserves provider message IDs and media download codes, sends +bounded text/markdown/media replies, and shuts down the Stream client during Host drain. + +## Authority boundary + +The Host owns installation, grants, configuration and secrets, connector-to-thread bindings, admission, +wake routing, retry/dead-letter policy, and lifecycle supervision. The package never parses `@` text to +invent wake authority and never mints a connector-binding handle. It requires the Host to supply a verified +opaque `connector_binding` handle for inbound delivery and the corresponding external DingTalk coordinate +for outbound delivery and restart recovery. + +## Configuration + +- `appKey` is the DingTalk enterprise application App Key. +- `appSecret` is a secret and must be projected only from the manifest-declared Host configuration. + +The package does not read a repository `.env` file and does not persist credentials. DingTalk access tokens +remain runtime-ephemeral. Group conversation identifiers may be checkpointed only through a Host-owned +store injected into the adapter. + +## Safety and recovery + +Provider HTTP errors fail closed. Media upload and download responses are bounded by the provider contract; +error text must not include credentials. Disabling or draining the plugin closes the Stream connection before +the preserved Core adapter can be restored, preventing double consumption. + +The standalone stdio activation remains gated on the C1 Host prerequisite for declared config/secret +projection and connector-binding bootstrap/recovery. Adapter exports are usable for isolated provider +conformance tests without claiming that synthetic handles constitute production activation. diff --git a/packages/connector-dingtalk/assets/icon.svg b/packages/connector-dingtalk/assets/icon.svg new file mode 100644 index 00000000..8d844205 --- /dev/null +++ b/packages/connector-dingtalk/assets/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/connector-dingtalk/npm-shrinkwrap.json b/packages/connector-dingtalk/npm-shrinkwrap.json new file mode 100644 index 00000000..3e1576c0 --- /dev/null +++ b/packages/connector-dingtalk/npm-shrinkwrap.json @@ -0,0 +1,953 @@ +{ + "name": "@clowder-ai/connector-dingtalk", + "version": "0.1.0-alpha.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@clowder-ai/connector-dingtalk", + "version": "0.1.0-alpha.0", + "license": "MIT", + "dependencies": { + "dingtalk-stream": "2.1.6-beta.1" + }, + "devDependencies": { + "@types/node": "^20.17.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dingtalk-stream": { + "version": "2.1.6-beta.1", + "resolved": "https://registry.npmjs.org/dingtalk-stream/-/dingtalk-stream-2.1.6-beta.1.tgz", + "integrity": "sha512-uYcBnf0Z4rfHHyN1ae4YnAFA6hUW2DmGVb0OZ53r/A272kuHnZynylE5pEJIJHkNIer6R9PCqpnsfsk9IuvglQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.4.0", + "debug": "^4.3.4", + "ws": "^8.13.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/packages/connector-dingtalk/package.json b/packages/connector-dingtalk/package.json new file mode 100644 index 00000000..8548b610 --- /dev/null +++ b/packages/connector-dingtalk/package.json @@ -0,0 +1,49 @@ +{ + "name": "@clowder-ai/connector-dingtalk", + "version": "0.1.0-alpha.0", + "description": "DingTalk enterprise messaging connector for Clowder AI hosts", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./manifest": "./plugin.yaml" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "plugin.yaml", + "README.md", + "assets", + "npm-shrinkwrap.json" + ], + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "build": "pnpm clean && tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.build.json --noEmit && tsc -p tsconfig.test.json", + "test": "node --import tsx --test src/**/*.test.ts", + "lint": "tsc -p tsconfig.build.json --noEmit && tsc -p tsconfig.test.json", + "clean": "rm -rf dist", + "prepack": "pnpm build" + }, + "dependencies": { + "dingtalk-stream": "2.1.6-beta.1" + }, + "devDependencies": { + "@types/node": "^20.17.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "yaml": "2.9.0" + }, + "keywords": ["clowder-ai", "connector", "dingtalk", "messaging"], + "repository": { + "type": "git", + "url": "https://github.com/zts212653/clowder-ai-plugins.git", + "directory": "packages/connector-dingtalk" + } +} diff --git a/packages/connector-dingtalk/plugin.yaml b/packages/connector-dingtalk/plugin.yaml new file mode 100644 index 00000000..981bae91 --- /dev/null +++ b/packages/connector-dingtalk/plugin.yaml @@ -0,0 +1,43 @@ +pluginId: official.connector.dingtalk +version: 0.1.0-alpha.0 +contractVersion: 0.1.0 +name: DingTalk Connector +description: + default: Receive and send DingTalk enterprise messages through a Host-owned connector binding. + translations: + zh-CN: 通过宿主所有的连接器绑定接收和发送钉钉企业消息。 +icon: + type: svg + src: assets/icon.svg +configuration: + - key: appKey + label: App Key + kind: string + required: true + - key: appSecret + label: App Secret + kind: secret + required: true +contributions: + - type: identity + id: dingtalk-bot + displayName: DingTalk bot + - type: connector + id: dingtalk + identityRef: dingtalk-bot + inboundMethod: dingtalk.inbound + outboundMethod: dingtalk.outbound +features: + - id: dingtalk-messaging + name: DingTalk messaging + resources: [] + contributions: + - type: identity + id: dingtalk-bot + - type: connector + id: dingtalk + capabilities: + - messaging.send +runtime: + transport: stdio + entrypoint: dist/stdio-entrypoint.js diff --git a/packages/connector-dingtalk/src/DingTalkAdapter.test.ts b/packages/connector-dingtalk/src/DingTalkAdapter.test.ts new file mode 100644 index 00000000..33d5ff16 --- /dev/null +++ b/packages/connector-dingtalk/src/DingTalkAdapter.test.ts @@ -0,0 +1,137 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { DingTalkAdapter } from './DingTalkAdapter.js'; +import type { ConnectorLogger } from './types.js'; + +const noop = () => undefined; +const logger: ConnectorLogger = { info: noop, warn: noop, error: noop, debug: noop }; + +function adapter() { + return new DingTalkAdapter(logger, { appKey: 'app-key', appSecret: 'app-secret' }); +} + +test('parses direct and group text while preserving provider routing facts', () => { + const subject = adapter(); + assert.deepEqual(subject.parseEvent({ + msgtype: 'text', + conversationType: '1', + conversationId: 'conversation-1', + msgId: 'message-1', + senderStaffId: 'staff-1', + text: { content: ' hello ' }, + }), { + chatId: 'staff-1', + conversationId: 'conversation-1', + text: 'hello', + messageId: 'message-1', + senderId: 'staff-1', + chatType: 'p2p', + senderNick: undefined, + conversationTitle: undefined, + }); + const group = subject.parseEvent({ + msgtype: 'text', + conversationType: '2', + conversationId: 'conversation-group', + openConversationId: 'open-group-1', + conversationTitle: 'Project', + msgId: 'message-2', + senderStaffId: 'staff-2', + senderNick: 'Alice', + text: { content: 'group hello' }, + }); + assert.equal(group?.chatId, 'open-group-1'); + assert.equal(group?.chatType, 'group'); + assert.equal(subject.resolveSenderName('staff-2'), 'Alice'); + assert.equal(subject.resolveConversationTitle('open-group-1'), 'Project'); +}); + +test('parses rich text and media without inventing unsupported content', () => { + const subject = adapter(); + const rich = subject.parseEvent({ + msgtype: 'richText', + conversationType: '1', + conversationId: 'conversation-1', + msgId: 'message-1', + senderStaffId: 'staff-1', + richText: [{ text: 'look ' }, { type: 'picture', downloadCode: 'download-1' }], + }); + assert.equal(rich?.text, 'look '); + assert.deepEqual(rich?.attachments, [{ type: 'image', downloadCode: 'download-1' }]); + assert.equal(subject.parseEvent({ msgtype: 'interactive', conversationType: '1' }), null); + assert.equal(subject.parseEvent({ msgtype: 'text', conversationType: '3', text: { content: 'x' } }), null); +}); + +test('group replies mention only the authenticated sender metadata supplied by Host', async () => { + const subject = adapter(); + const calls: Array<{ chatId: string; content: string; msgType: string; chatType?: 'p2p' | 'group' }> = []; + subject._injectSendMessage(async input => { + calls.push(input); + }); + + await subject.sendReply('open-group-1', 'answer', { + chatType: 'group', + replyToSender: { id: 'staff-2', name: 'Alice' }, + }); + await subject.sendReply('staff-1', 'direct answer', { + chatType: 'p2p', + replyToSender: { id: 'staff-1', name: 'Bob' }, + }); + + assert.match(calls[0]?.content ?? '', /@Alice answer/); + assert.equal(calls[0]?.chatType, 'group'); + assert.doesNotMatch(calls[1]?.content ?? '', /@Bob/); + assert.equal(calls[1]?.chatType, 'p2p'); +}); + +test('AI Card failure falls back to one markdown send', async () => { + const subject = adapter(); + subject.parseEvent({ + msgtype: 'text', + conversationType: '1', + conversationId: 'conversation-1', + msgId: 'message-1', + senderStaffId: 'staff-1', + text: { content: 'seed' }, + }); + subject._injectCreateCard(async () => { + throw new Error('provider unavailable'); + }); + const sends: string[] = []; + subject._injectSendMessage(async input => { + sends.push(input.msgType); + }); + + await subject.sendFormattedReply('staff-1', { + header: 'Cat', + body: 'Answer', + origin: 'direct', + }); + assert.deepEqual(sends, ['markdown']); +}); + +test('group routing is learned only from authenticated provider metadata in the live session', async () => { + const subject = adapter(); + subject.parseEvent({ + msgtype: 'text', + conversationType: '2', + conversationId: 'conversation-group', + openConversationId: 'open-group-current', + msgId: 'message-1', + senderStaffId: 'staff-1', + text: { content: 'hello' }, + }); + const calls: Array<{ chatType?: 'p2p' | 'group' }> = []; + subject._injectSendMessage(async input => { + calls.push(input); + }); + await subject.sendReply('open-group-current', 'current'); + assert.equal(calls[0]?.chatType, 'group'); +}); + +test('start and stop use the injected DingTalk stream SDK only through explicit lifecycle calls', async () => { + const subject = adapter(); + await subject.stopStream(); + assert.equal(subject.connectorId, 'dingtalk'); +}); diff --git a/packages/connector-dingtalk/src/DingTalkAdapter.ts b/packages/connector-dingtalk/src/DingTalkAdapter.ts new file mode 100644 index 00000000..f1104882 --- /dev/null +++ b/packages/connector-dingtalk/src/DingTalkAdapter.ts @@ -0,0 +1,911 @@ +/** + * DingTalk (钉钉) Enterprise Bot Adapter + * Inbound: Parse Stream events → extract DM messages + * Outbound: Send reply via DingTalk OpenAPI + AI Card streaming + * + * Uses dingtalk-stream for Stream mode (no public URL needed). + * AI Card for rich/streaming replies (create → streaming update → finish). + * + * F132 DingTalk + WeCom Chat Gateway — Phase A + */ + +import { basename } from 'node:path'; +import type { ConnectorLogger, MessageEnvelope, RichBlock } from './types.js'; + +// ── Types ── + +export interface DingTalkAttachment { + type: 'image' | 'file' | 'audio'; + /** DingTalk media download code (for images/files/audio) */ + downloadCode?: string; + fileName?: string; + duration?: number; +} + +export interface DingTalkInboundMessage { + /** DM: staffId | Group: openConversationId */ + chatId: string; + /** DingTalk conversationId — used for AI Card delivery routing */ + conversationId: string; + text: string; + messageId: string; + senderId: string; + chatType: 'p2p' | 'group'; + /** Group: sender's display name from webhook payload */ + senderNick?: string; + /** Group: chat title from webhook payload */ + conversationTitle?: string; + attachments?: DingTalkAttachment[]; +} + +export interface DingTalkAdapterOptions { + appKey: string; + appSecret: string; + /** Robot code (used for sending messages), defaults to appKey */ + robotCode?: string; +} + +/** AI Card streaming state machine */ +type CardState = 'PROCESSING' | 'INPUTING' | 'FINISHED'; + +interface ActiveCard { + outTrackId: string; + state: CardState; + lastUpdateAt: number; + lastContentLength: number; +} + +// ── AI Card Throttle Config ── + +const AI_CARD_THROTTLE_MS = 300; +const AI_CARD_TEMPLATE_ID = 'StandardCard'; + +// ── Adapter ── + +export class DingTalkAdapter { + readonly connectorId = 'dingtalk'; + private readonly log: ConnectorLogger; + private readonly appKey: string; + private readonly appSecret: string; + private readonly robotCode: string; + + // Stream client (dingtalk-stream SDK) + private streamClient: unknown = null; + private stopFn: (() => Promise) | null = null; + + // Active AI Card sessions (keyed by outTrackId) + private readonly activeCards = new Map(); + + // staffId → conversationId mapping (populated from inbound parseEvent) + // AI Card delivery needs the real conversationId, but the public layer + // passes externalChatId (= staffId) for outbound routing. + private readonly staffToConversation = new Map(); + private readonly groupConversationIds = new Set(); + private readonly senderNickCache = new Map(); + private readonly conversationTitleCache = new Map(); + + // DI injection points (for testing + runtime override) + private sendMessageFn: + | ((params: { chatId: string; content: string; msgType: string; chatType?: 'p2p' | 'group' }) => Promise) + | null = null; + private createCardFn: + | ((params: { outTrackId: string; cardData: Record }) => Promise) + | null = null; + private streamingCardFn: + | ((params: { outTrackId: string; content: string; state: CardState }) => Promise) + | null = null; + private accessTokenFn: (() => Promise) | null = null; + private downloadMediaFn: ((downloadCode: string) => Promise) | null = null; + private uploadMediaFn: ((params: { filePath: string; type: string }) => Promise) | null = null; + + constructor(log: ConnectorLogger, options: DingTalkAdapterOptions) { + this.log = log; + this.appKey = options.appKey; + this.appSecret = options.appSecret; + this.robotCode = options.robotCode ?? options.appKey; + } + + // ── Inbound: Parse Stream Event ── + + /** + * Parse a DingTalk Stream bot message callback into an inbound message. + * Supports text, richText, picture, audio, file message types. + * Returns null for group or unsupported events. + * + * AC-A1: DM text + richText parsing + */ + parseEvent(eventBody: unknown): DingTalkInboundMessage | null { + if (!eventBody || typeof eventBody !== 'object') return null; + + const body = eventBody as Record; + + const msgType = body.msgtype as string | undefined; + if (!msgType) return null; + + const conversationType = body.conversationType as string | undefined; + if (conversationType !== '1' && conversationType !== '2') return null; + + const isGroup = conversationType === '2'; + const chatType: 'p2p' | 'group' = isGroup ? 'group' : 'p2p'; + const conversationId = (body.conversationId as string) ?? ''; + const openConversationId = (body.openConversationId as string) ?? ''; + const conversationTitle = (body.conversationTitle as string) ?? undefined; + const messageId = (body.msgId as string) ?? ''; + const senderStaffId = (body.senderStaffId as string) ?? (body.senderId as string) ?? 'unknown'; + const senderNick = (body.senderNick as string) ?? undefined; + const chatId = isGroup ? openConversationId : senderStaffId; + + const base = { + chatId, + conversationId, + messageId, + senderId: senderStaffId, + chatType, + senderNick, + conversationTitle, + }; + + if (conversationId) this.staffToConversation.set(senderStaffId, conversationId); + if (isGroup && openConversationId) { + this.groupConversationIds.add(openConversationId); + if (senderNick) this.senderNickCache.set(senderStaffId, senderNick); + if (conversationTitle) this.conversationTitleCache.set(openConversationId, conversationTitle); + } + + switch (msgType) { + case 'text': { + const textObj = body.text as Record | undefined; + const text = textObj?.content as string | undefined; + if (!text) return null; + return { ...base, text: text.trim() }; + } + case 'richText': { + // DingTalk Stream FAQ confirms: body.richText is a flat array + const richContent = Array.isArray(body.richText) ? (body.richText as unknown[]) : null; + if (!richContent) return null; + const textParts: string[] = []; + const attachments: DingTalkAttachment[] = []; + for (const node of richContent) { + const n = node as Record; + if (n.text) textParts.push(String(n.text)); + if (n.type === 'picture' && n.downloadCode) { + attachments.push({ type: 'image', downloadCode: String(n.downloadCode) }); + } + } + const text = textParts.join('') || '[富文本]'; + return { ...base, text, ...(attachments.length > 0 ? { attachments } : {}) }; + } + case 'picture': { + const downloadCode = (body.content as Record)?.downloadCode as string | undefined; + return { + ...base, + text: '[图片]', + attachments: downloadCode ? [{ type: 'image' as const, downloadCode }] : undefined, + }; + } + case 'audio': { + const audioBody = body.content as Record | undefined; + const downloadCode = audioBody?.downloadCode as string | undefined; + const duration = audioBody?.duration as number | undefined; + return { + ...base, + text: '[语音]', + attachments: downloadCode + ? [{ type: 'audio' as const, downloadCode, ...(duration != null ? { duration } : {}) }] + : undefined, + }; + } + case 'file': { + const fileBody = body.content as Record | undefined; + const downloadCode = fileBody?.downloadCode as string | undefined; + const fileName = fileBody?.fileName as string | undefined; + return { + ...base, + text: fileName ? `[文件] ${fileName}` : '[文件]', + attachments: downloadCode + ? [{ type: 'file' as const, downloadCode, ...(fileName ? { fileName } : {}) }] + : undefined, + }; + } + default: + return null; + } + } + + // ── Name Resolution (AC-A2.5) ── + + /** + * Resolve sender display name from cached inbound event data. + * DingTalk group webhooks include senderNick, so no API call needed. + */ + resolveSenderName(staffId: string): string | undefined { + return this.senderNickCache.get(staffId); + } + + /** + * Resolve group conversation title from cached inbound event data. + * DingTalk group webhooks include conversationTitle, so no API call needed. + */ + resolveConversationTitle(openConversationId: string): string | undefined { + return this.conversationTitleCache.get(openConversationId); + } + + // ── Outbound: Send Messages ── + + /** + * Prepend @sender mention for group chat replies (AC-A2.4). + * Only used for group chats — DM replies must NOT @mention (Feishu AC-C2 pattern). + */ + private prependAtSender(content: string, sender: { id: string; name?: string }): string { + const name = sender.name ?? this.senderNickCache.get(sender.id) ?? '用户'; + return `@${name} ${content}`; + } + + /** + * Send a plain text reply via DingTalk Robot API. + * AC-A2: Basic text + markdown sending + */ + async sendReply(externalChatId: string, content: string, metadata?: Record): Promise { + const metaChatType = (metadata as { chatType?: 'p2p' | 'group' } | undefined)?.chatType; + const isGroup = metaChatType === 'group' || this.groupConversationIds.has(externalChatId); + const sender = (metadata as { replyToSender?: { id: string; name?: string } } | undefined)?.replyToSender; + const text = isGroup && sender ? this.prependAtSender(content, sender) : content; + await this.sendDingTalkMessage(externalChatId, 'text', { content: text }, isGroup ? 'group' : undefined); + } + + /** + * Send a markdown reply. + */ + async sendMarkdown( + externalChatId: string, + title: string, + text: string, + chatTypeOverride?: 'p2p' | 'group', + ): Promise { + await this.sendDingTalkMessage(externalChatId, 'markdown', { title, text }, chatTypeOverride); + } + + /** + * Send a rich block message (convert blocks to markdown text). + */ + async sendRichMessage( + externalChatId: string, + textContent: string, + _blocks: RichBlock[], + catDisplayName: string, + metadata?: Record, + ): Promise { + const chatTypeOverride = (metadata as { chatType?: 'p2p' | 'group' } | undefined)?.chatType; + const title = `🐱 ${catDisplayName}`; + await this.sendMarkdown(externalChatId, title, textContent, chatTypeOverride); + } + + /** + * Send a formatted reply as AI Card. + * AC-A3: AI Card with cat name header + body + deep link + */ + async sendFormattedReply( + externalChatId: string, + envelope: MessageEnvelope, + metadata?: Record, + ): Promise { + const metaChatType = (metadata as { chatType?: 'p2p' | 'group' } | undefined)?.chatType; + const chatTypeOverride = metaChatType === 'group' ? 'group' : undefined; + const isCallback = envelope.origin === 'callback'; + const headerTitle = isCallback ? `📨 ${envelope.header} · 传话` : envelope.header; + + // Build markdown body for AI Card + let body = ''; + if (envelope.subtitle) { + body += `**${envelope.subtitle}**\n\n`; + } + body += envelope.body; + if (envelope.footer) { + body += `\n\n---\n${envelope.footer}`; + } + + // Try AI Card first, fall back to markdown + try { + await this.sendAICard(externalChatId, headerTitle, body, chatTypeOverride); + } catch (err) { + this.log.warn({ err }, '[DingTalkAdapter] AI Card sendFormattedReply failed, falling back to markdown'); + await this.sendMarkdown(externalChatId, headerTitle, body, chatTypeOverride); + } + } + + // ── Streaming: AI Card ── + + /** + * Send a placeholder AI Card and return its outTrackId. + * AC-A4: AI Card streaming (create phase) + */ + async sendPlaceholder(externalChatId: string, text: string): Promise { + const outTrackId = `cc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + try { + await this.createAICardInstance(externalChatId, outTrackId, text); + + this.activeCards.set(outTrackId, { + outTrackId, + state: 'PROCESSING', + lastUpdateAt: 0, + lastContentLength: 0, + }); + + return outTrackId; + } catch (err) { + this.log.warn({ err }, '[DingTalkAdapter] sendPlaceholder failed'); + return ''; + } + } + + /** + * Edit an AI Card via streaming update. + * AC-A4: AI Card streaming (update phase, 300ms throttle) + */ + async editMessage(_externalChatId: string, platformMessageId: string, text: string): Promise { + const card = this.activeCards.get(platformMessageId); + if (!card) { + this.log.warn({ platformMessageId }, '[DingTalkAdapter] editMessage: no active card found'); + return; + } + + // 300ms throttle (AC-A4) + const now = Date.now(); + if (now - card.lastUpdateAt < AI_CARD_THROTTLE_MS) return; + + try { + // Transition to INPUTING if still PROCESSING + const newState: CardState = card.state === 'PROCESSING' ? 'INPUTING' : card.state; + + await this.updateAICardStreaming(platformMessageId, text, newState); + + card.state = newState; + card.lastUpdateAt = now; + card.lastContentLength = text.length; + } catch (err) { + this.log.warn({ err, platformMessageId }, '[DingTalkAdapter] editMessage streaming update failed'); + } + } + + /** + * Delete/finish an AI Card (transition to FINISHED state). + * StreamingOutboundHook calls this for cleanup. + */ + async deleteMessage(platformMessageId: string): Promise { + const card = this.activeCards.get(platformMessageId); + if (!card) return; + + try { + await this.updateAICardStreaming(platformMessageId, '', 'FINISHED'); + } catch (err) { + this.log.warn({ err, platformMessageId }, '[DingTalkAdapter] deleteMessage (finish card) failed'); + } finally { + this.activeCards.delete(platformMessageId); + } + } + + // ── Media ── + + /** + * Send a media message (image, file, audio). + * AC-A5: Media upload + send + */ + async sendMedia( + externalChatId: string, + payload: { + type: 'image' | 'file' | 'audio'; + url?: string; + absPath?: string; + fileName?: string; + duration?: number; + [key: string]: unknown; + }, + ): Promise { + const url = typeof payload.url === 'string' && payload.url.length > 0 ? payload.url : undefined; + const absPath = typeof payload.absPath === 'string' && payload.absPath.length > 0 ? payload.absPath : undefined; + + // Path 1: Image with URL — direct photoURL fast path (no upload needed) + if (payload.type === 'image' && url && !absPath) { + await this.sendDingTalkImageMessage(externalChatId, url); + return; + } + + // Path 2: Has absPath → native media sending via upload + if (absPath) { + try { + const mediaId = await this.uploadToDingTalk(absPath, payload.type); + if (mediaId) { + await this.sendDingTalkMediaMessage(externalChatId, payload.type, mediaId, { + fileName: payload.fileName ?? basename(absPath), + duration: payload.duration, + }); + return; + } + } catch (err) { + this.log.warn( + { err, type: payload.type, absPath }, + '[DingTalkAdapter] sendMedia: upload failed, falling through', + ); + } + } + + // Path 3: Fallback — text link + const mediaReference = + url ?? + (typeof payload.fileName === 'string' && payload.fileName.length > 0 + ? payload.fileName + : absPath + ? basename(absPath) + : undefined); + + if (mediaReference) { + const label = payload.type === 'image' ? '🖼️' : payload.type === 'audio' ? '🔊' : '📎'; + await this.sendReply(externalChatId, `${label} ${mediaReference}`); + return; + } + this.log.warn({ type: payload.type }, '[DingTalkAdapter] sendMedia: no URL available, skipping'); + } + + /** + * Download media file from an inbound message using its downloadCode. + * Returns a temporary download URL. + * AC-A5: Inbound media download via POST /v1.0/robot/messageFiles/download + */ + async downloadMedia(downloadCode: string): Promise { + if (this.downloadMediaFn) return this.downloadMediaFn(downloadCode); + + const accessToken = await this.getAccessToken(); + const url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'; + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-acs-dingtalk-access-token': accessToken, + }, + body: JSON.stringify({ downloadCode, robotCode: this.robotCode }), + }); + + if (!res.ok) { + const body = await res.text().catch(() => '(unreadable)'); + throw new Error(`DingTalk media download error ${res.status}: ${body}`); + } + + const data = (await res.json()) as { downloadUrl?: string }; + if (!data.downloadUrl) throw new Error('DingTalk media download: missing downloadUrl in response'); + return data.downloadUrl; + } + + // ── Stream Connection ── + + /** + * Start the DingTalk Stream connection (long-lived, like Telegram polling). + * Calls onMessage for each inbound bot message. + * AC-A7: Stream connection + reconnect + dedup + */ + async startStream(onMessage: (msg: DingTalkInboundMessage) => Promise): Promise { + try { + const { DWClient, EventAck, TOPIC_ROBOT } = await import('dingtalk-stream'); + + const client = new DWClient({ + clientId: this.appKey, + clientSecret: this.appSecret, + debug: false, + }); + + client.registerCallbackListener(TOPIC_ROBOT, async (res: unknown) => { + const downstream = res as { headers?: { messageId?: string }; data?: string }; + const messageId = downstream.headers?.messageId ?? ''; + try { + const data = downstream.data ? JSON.parse(downstream.data) : null; + if (!data) return; + + const parsed = this.parseEvent(data); + if (parsed) { + await onMessage(parsed); + } + } catch (err) { + this.log.error({ err }, '[DingTalkAdapter] Stream message handler error'); + } finally { + // Always ACK to prevent 60s retry (guard: SDK throws if messageId is empty) + if (messageId) client.socketCallBackResponse(messageId, EventAck.SUCCESS); + } + }); + + await client.connect(); + this.streamClient = client; + this.stopFn = async () => { + try { + client.disconnect(); + } catch { + // ignore disconnect errors + } + }; + + this.log.info('[DingTalkAdapter] Stream connection established'); + } catch (err) { + this.log.error({ err }, '[DingTalkAdapter] Failed to start Stream connection'); + throw err; + } + } + + /** + * Stop the Stream connection. + */ + async stopStream(): Promise { + if (this.stopFn) { + await this.stopFn(); + this.stopFn = null; + this.streamClient = null; + this.log.info('[DingTalkAdapter] Stream connection stopped'); + } + } + + // ── Private: DingTalk OpenAPI Calls ── + + private async postRobotMessage( + chatId: string, + msgKey: string, + msgParam: Record, + diMsgType: string, + errorLabel: string, + chatTypeOverride?: 'p2p' | 'group', + ): Promise { + const isGroup = chatTypeOverride === 'group' || this.groupConversationIds.has(chatId); + + if (this.sendMessageFn) { + return this.sendMessageFn({ + chatId, + content: JSON.stringify(msgParam), + msgType: diMsgType, + chatType: isGroup ? 'group' : 'p2p', + }); + } + + const accessToken = await this.getAccessToken(); + const url = isGroup + ? 'https://api.dingtalk.com/v1.0/robot/orgGroupSend' + : 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend'; + + const payload = isGroup + ? { robotCode: this.robotCode, openConversationId: chatId, msgKey, msgParam: JSON.stringify(msgParam) } + : { robotCode: this.robotCode, userIds: [chatId], msgKey, msgParam: JSON.stringify(msgParam) }; + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-acs-dingtalk-access-token': accessToken, + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const body = await res.text().catch(() => '(unreadable)'); + throw new Error(`DingTalk ${errorLabel} error ${res.status}: ${body}`); + } + + return res.json(); + } + + private async sendDingTalkMessage( + chatId: string, + msgType: string, + msgContent: Record, + chatTypeOverride?: 'p2p' | 'group', + ): Promise { + const msgKey = msgType === 'text' ? 'sampleText' : 'sampleMarkdown'; + return this.postRobotMessage(chatId, msgKey, msgContent, msgType, 'send', chatTypeOverride); + } + + private async sendDingTalkImageMessage( + chatId: string, + photoURL: string, + chatTypeOverride?: 'p2p' | 'group', + ): Promise { + return this.postRobotMessage(chatId, 'sampleImageMsg', { photoURL }, 'image', 'image send', chatTypeOverride); + } + + private async uploadToDingTalk(absPath: string, type: string): Promise { + if (this.uploadMediaFn) { + return this.uploadMediaFn({ filePath: absPath, type }); + } + + const accessToken = await this.getAccessToken(); + const uploadUrl = 'https://api.dingtalk.com/v1.0/robot/messageFiles/upload'; + + const { readFile } = await import('node:fs/promises'); + const fileBuffer = await readFile(absPath); + const fileName = basename(absPath); + + const formData = new FormData(); + formData.append('robotCode', this.robotCode); + formData.append('mediaType', type === 'image' ? 'image' : 'file'); + formData.append('file', new Blob([fileBuffer]), fileName); + + const res = await fetch(uploadUrl, { + method: 'POST', + headers: { 'x-acs-dingtalk-access-token': accessToken }, + body: formData, + }); + + if (!res.ok) { + const body = await res.text().catch(() => '(unreadable)'); + this.log.warn({ status: res.status, body }, '[DingTalkAdapter] uploadToDingTalk failed'); + return null; + } + + const data = (await res.json()) as { mediaId?: string }; + return data.mediaId ?? null; + } + + private async sendDingTalkMediaMessage( + chatId: string, + type: 'image' | 'file' | 'audio', + mediaId: string, + meta: { fileName?: string; duration?: number }, + chatTypeOverride?: 'p2p' | 'group', + ): Promise { + const msgKeyMap: Record Record }> = { + audio: { + msgKey: 'sampleAudio', + buildParam: () => ({ mediaId, duration: String(meta.duration ?? 0) }), + }, + file: { + msgKey: 'sampleFile', + buildParam: () => ({ + mediaId, + fileName: meta.fileName ?? 'file', + fileType: (meta.fileName ?? 'file').split('.').pop() ?? '', + }), + }, + image: { + msgKey: 'sampleImageMsg', + buildParam: () => ({ photoURL: mediaId }), + }, + }; + + const entry = msgKeyMap[type]; + if (!entry) throw new Error(`Unsupported media type for DingTalk: ${type}`); + + return this.postRobotMessage( + chatId, + entry.msgKey, + entry.buildParam(), + entry.msgKey, + 'media send', + chatTypeOverride, + ); + } + + /** + * Create an AI Card instance and deliver it. + * POST /v1.0/card/instances/createAndDeliver + */ + private async createAICardInstance( + chatId: string, + outTrackId: string, + headerText: string, + chatTypeOverride?: 'p2p' | 'group', + ): Promise { + const isGroup = chatTypeOverride === 'group' || this.groupConversationIds.has(chatId); + + if (!isGroup) { + const conversationId = this.staffToConversation.get(chatId); + if (!conversationId) { + throw new Error(`No conversationId mapped for staffId=${chatId}; AI Card requires a prior inbound message`); + } + + if (this.createCardFn) { + await this.createCardFn({ outTrackId, cardData: { headerText, conversationId, chatType: 'p2p' } }); + return; + } + + const accessToken = await this.getAccessToken(); + const url = 'https://api.dingtalk.com/v1.0/card/instances/createAndDeliver'; + + const cardData = { + outTrackId, + cardTemplateId: AI_CARD_TEMPLATE_ID, + cardData: { + cardParamMap: { + title: headerText, + content: '...', + status: 'PROCESSING', + }, + }, + imRobotOpenDeliverModel: { + spaceType: 'IM_ROBOT', + robotCode: this.robotCode, + extension: JSON.stringify({ + conversationType: '1', + conversationId, + }), + }, + }; + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-acs-dingtalk-access-token': accessToken, + }, + body: JSON.stringify(cardData), + }); + + if (!res.ok) { + const body = await res.text().catch(() => '(unreadable)'); + throw new Error(`DingTalk AI Card create error ${res.status}: ${body}`); + } + return; + } + + if (this.createCardFn) { + await this.createCardFn({ + outTrackId, + cardData: { headerText, chatType: 'group', openConversationId: chatId }, + }); + return; + } + + const accessToken = await this.getAccessToken(); + const url = 'https://api.dingtalk.com/v1.0/card/instances/createAndDeliver'; + + const cardData = { + outTrackId, + cardTemplateId: AI_CARD_TEMPLATE_ID, + cardData: { + cardParamMap: { + title: headerText, + content: '...', + status: 'PROCESSING', + }, + }, + imGroupOpenSpaceModel: { + supportForward: true, + }, + imGroupOpenDeliverModel: { + robotCode: this.robotCode, + openConversationId: chatId, + }, + }; + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-acs-dingtalk-access-token': accessToken, + }, + body: JSON.stringify(cardData), + }); + + if (!res.ok) { + const body = await res.text().catch(() => '(unreadable)'); + throw new Error(`DingTalk AI Card group create error ${res.status}: ${body}`); + } + } + + /** + * Update an AI Card streaming content. + * PUT /v1.0/card/streaming + * State machine: PROCESSING → INPUTING → FINISHED + */ + private async updateAICardStreaming(outTrackId: string, content: string, state: CardState): Promise { + if (this.streamingCardFn) { + await this.streamingCardFn({ outTrackId, content, state }); + return; + } + + const accessToken = await this.getAccessToken(); + const url = 'https://api.dingtalk.com/v1.0/card/streaming'; + + const payload = { + outTrackId, + key: 'content', + content, + isFull: true, + isFinalize: state === 'FINISHED', + guid: `${outTrackId}-${Date.now()}`, + }; + + const res = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'x-acs-dingtalk-access-token': accessToken, + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const body = await res.text().catch(() => '(unreadable)'); + throw new Error(`DingTalk AI Card streaming error ${res.status}: ${body}`); + } + } + + /** + * Send an AI Card with title and markdown body (non-streaming, single shot). + */ + private async sendAICard( + staffId: string, + title: string, + body: string, + chatTypeOverride?: 'p2p' | 'group', + ): Promise { + const outTrackId = `cc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + await this.createAICardInstance(staffId, outTrackId, title, chatTypeOverride); + await this.updateAICardStreaming(outTrackId, body, 'FINISHED'); + } + + /** + * Get DingTalk access token (cached, 2h TTL). + */ + private cachedToken: { token: string; expiresAt: number } | null = null; + + private async getAccessToken(): Promise { + if (this.accessTokenFn) return this.accessTokenFn(); + + const now = Date.now(); + if (this.cachedToken && this.cachedToken.expiresAt > now + 60_000) { + return this.cachedToken.token; + } + + const url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'; + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ appKey: this.appKey, appSecret: this.appSecret }), + }); + + if (!res.ok) { + const body = await res.text().catch(() => '(unreadable)'); + throw new Error(`DingTalk accessToken error ${res.status}: ${body}`); + } + + const data = (await res.json()) as { accessToken?: string; expireIn?: number }; + const token = data.accessToken; + if (!token) throw new Error('DingTalk accessToken response missing token'); + + // expireIn is in seconds, cache with 5-minute safety margin + const expiresAt = now + (data.expireIn ?? 7200) * 1000; + this.cachedToken = { token, expiresAt }; + return token; + } + + // ── Test Helpers ── + + /** @internal */ + _injectSendMessage( + fn: (params: { chatId: string; content: string; msgType: string; chatType?: 'p2p' | 'group' }) => Promise, + ): void { + this.sendMessageFn = fn; + } + + /** @internal */ + _injectCreateCard(fn: (params: { outTrackId: string; cardData: Record }) => Promise): void { + this.createCardFn = fn; + } + + /** @internal */ + _injectStreamingCard( + fn: (params: { outTrackId: string; content: string; state: CardState }) => Promise, + ): void { + this.streamingCardFn = fn; + } + + /** @internal */ + _injectAccessToken(fn: () => Promise): void { + this.accessTokenFn = fn; + } + + /** @internal */ + _injectDownloadMedia(fn: (downloadCode: string) => Promise): void { + this.downloadMediaFn = fn; + } + + /** @internal */ + _injectUploadMedia(fn: (params: { filePath: string; type: string }) => Promise): void { + this.uploadMediaFn = fn; + } +} diff --git a/packages/connector-dingtalk/src/index.ts b/packages/connector-dingtalk/src/index.ts new file mode 100644 index 00000000..0af63181 --- /dev/null +++ b/packages/connector-dingtalk/src/index.ts @@ -0,0 +1,7 @@ +export { DingTalkAdapter } from './DingTalkAdapter.js'; +export type { + DingTalkAdapterOptions, + DingTalkAttachment, + DingTalkInboundMessage, +} from './DingTalkAdapter.js'; +export type { ConnectorLogger, MessageEnvelope, RichBlock } from './types.js'; diff --git a/packages/connector-dingtalk/src/manifest.test.ts b/packages/connector-dingtalk/src/manifest.test.ts new file mode 100644 index 00000000..cc9a888c --- /dev/null +++ b/packages/connector-dingtalk/src/manifest.test.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import { parse } from 'yaml'; + +test('manifest preserves DingTalk config sensitivity and Host authority', async () => { + const manifest = parse(await readFile(new URL('../plugin.yaml', import.meta.url), 'utf8')) as { + pluginId: string; + version: string; + contractVersion: string; + description: { default: string; translations: Record }; + configuration: Array<{ key: string; kind: string; required: boolean }>; + contributions: Array>; + features: Array<{ capabilities: string[] }>; + }; + const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')) as { + version: string; + files: string[]; + }; + + assert.equal(manifest.pluginId, 'official.connector.dingtalk'); + assert.equal(manifest.version, packageJson.version); + assert.equal(manifest.contractVersion, '0.1.0'); + assert.deepEqual(manifest.configuration, [ + { key: 'appKey', label: 'App Key', kind: 'string', required: true }, + { key: 'appSecret', label: 'App Secret', kind: 'secret', required: true }, + ]); + assert.deepEqual(manifest.contributions.map(entry => entry.type), ['identity', 'connector']); + assert.deepEqual(manifest.features[0]?.capabilities, ['messaging.send']); + for (const value of [manifest.description.default, ...Object.values(manifest.description.translations)]) { + assert.ok([...value].length <= 100); + } + for (const member of ['README.md', 'plugin.yaml', 'assets', 'npm-shrinkwrap.json']) { + assert.ok(packageJson.files.includes(member)); + } +}); diff --git a/packages/connector-dingtalk/src/types.ts b/packages/connector-dingtalk/src/types.ts new file mode 100644 index 00000000..2722b602 --- /dev/null +++ b/packages/connector-dingtalk/src/types.ts @@ -0,0 +1,18 @@ +export type ConnectorLogMethod = (...args: readonly unknown[]) => void; + +export interface ConnectorLogger { + info: ConnectorLogMethod; + warn: ConnectorLogMethod; + error: ConnectorLogMethod; + debug?: ConnectorLogMethod; +} + +export type RichBlock = Readonly>; + +export interface MessageEnvelope { + header: string; + body: string; + origin: 'direct' | 'callback'; + subtitle?: string; + footer?: string; +} diff --git a/packages/connector-dingtalk/tsconfig.build.json b/packages/connector-dingtalk/tsconfig.build.json new file mode 100644 index 00000000..375fb91c --- /dev/null +++ b/packages/connector-dingtalk/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/connector-dingtalk/tsconfig.test.json b/packages/connector-dingtalk/tsconfig.test.json new file mode 100644 index 00000000..36fbdf62 --- /dev/null +++ b/packages/connector-dingtalk/tsconfig.test.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { "noEmit": true, "rootDir": "." }, + "include": ["src/**/*.ts"] +} diff --git a/packages/connector-telegram/README.md b/packages/connector-telegram/README.md new file mode 100644 index 00000000..a796eef7 --- /dev/null +++ b/packages/connector-telegram/README.md @@ -0,0 +1,26 @@ +# Telegram Connector + +`@clowder-ai/connector-telegram` packages Telegram Bot long-poll ingress and Bot API egress for a Clowder AI +Host. It accepts private human messages, preserves Telegram message and file IDs, renders safe Telegram HTML, +splits messages without cutting surrogate pairs or HTML entities, and drains polling before shutdown. + +## Authority boundary + +The Host owns the bot token, connector/thread bindings, admission and wake routing, durable polling cursor, +delivery retry/dead-letter state, grants, and lifecycle. The package never derives wake authority from `@` +text and never writes an undeclared local cursor. Provider updates are sent only through a Host-issued opaque +`connector_binding` address. + +## Configuration + +`botToken` is a required secret issued by BotFather. The Host must project only this manifest-declared value; +the package does not read repository `.env` files or log the token. + +## Safety and recovery + +Only private, non-bot messages enter the connector. Long polling has bounded conflict backoff and closes the +Bot API session on drain. A Host-owned cursor is committed only after accepted delivery so restart and +rollback do not double-run the package and preserved Core poller. + +The standalone stdio activation remains gated on C1 Host config/secret projection, connector-binding +bootstrap/recovery, and durable checkpoint support. Adapter exports remain testable with isolated fixtures. diff --git a/packages/connector-telegram/assets/icon.svg b/packages/connector-telegram/assets/icon.svg new file mode 100644 index 00000000..1a5a22ac --- /dev/null +++ b/packages/connector-telegram/assets/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/connector-telegram/npm-shrinkwrap.json b/packages/connector-telegram/npm-shrinkwrap.json new file mode 100644 index 00000000..79914c5c --- /dev/null +++ b/packages/connector-telegram/npm-shrinkwrap.json @@ -0,0 +1,697 @@ +{ + "name": "@clowder-ai/connector-telegram", + "version": "0.1.0-alpha.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@clowder-ai/connector-telegram", + "version": "0.1.0-alpha.0", + "license": "MIT", + "dependencies": { + "grammy": "1.41.1" + }, + "devDependencies": { + "@types/node": "^20.17.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grammyjs/types": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.25.0.tgz", + "integrity": "sha512-iN9i5p+8ZOu9OMxWNcguojQfz4K/PDyMPOnL7PPCON+SoA/F8OKMH3uR7CVUkYfdNe0GCz8QOzAWrnqusQYFOg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/grammy": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.41.1.tgz", + "integrity": "sha512-wcHAQ1e7svL3fJMpDchcQVcWUmywhuepOOjHUHmMmWAwUJEIyK5ea5sbSjZd+Gy1aMpZeP8VYJa+4tP+j1YptQ==", + "license": "MIT", + "dependencies": { + "@grammyjs/types": "3.25.0", + "abort-controller": "^3.0.0", + "debug": "^4.4.3", + "node-fetch": "^2.7.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/packages/connector-telegram/package.json b/packages/connector-telegram/package.json new file mode 100644 index 00000000..35fd93cb --- /dev/null +++ b/packages/connector-telegram/package.json @@ -0,0 +1,39 @@ +{ + "name": "@clowder-ai/connector-telegram", + "version": "0.1.0-alpha.0", + "description": "Telegram Bot messaging connector for Clowder AI hosts", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./manifest": "./plugin.yaml" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist", "plugin.yaml", "README.md", "assets", "npm-shrinkwrap.json"], + "engines": { "node": ">=20.0.0" }, + "scripts": { + "build": "pnpm clean && tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.build.json --noEmit && tsc -p tsconfig.test.json", + "test": "node --import tsx --test src/**/*.test.ts", + "lint": "tsc -p tsconfig.build.json --noEmit && tsc -p tsconfig.test.json", + "clean": "rm -rf dist", + "prepack": "pnpm build" + }, + "dependencies": { "grammy": "1.41.1" }, + "devDependencies": { + "@types/node": "^20.17.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "yaml": "2.9.0" + }, + "keywords": ["clowder-ai", "connector", "telegram", "messaging"], + "repository": { + "type": "git", + "url": "https://github.com/zts212653/clowder-ai-plugins.git", + "directory": "packages/connector-telegram" + } +} diff --git a/packages/connector-telegram/plugin.yaml b/packages/connector-telegram/plugin.yaml new file mode 100644 index 00000000..288d6249 --- /dev/null +++ b/packages/connector-telegram/plugin.yaml @@ -0,0 +1,39 @@ +pluginId: official.connector.telegram +version: 0.1.0-alpha.0 +contractVersion: 0.1.0 +name: Telegram Connector +description: + default: Receive and send Telegram Bot messages through a Host-owned connector binding. + translations: + zh-CN: 通过宿主所有的连接器绑定接收和发送 Telegram Bot 消息。 +icon: + type: svg + src: assets/icon.svg +configuration: + - key: botToken + label: Bot Token + kind: secret + required: true +contributions: + - type: identity + id: telegram-bot + displayName: Telegram bot + - type: connector + id: telegram + identityRef: telegram-bot + inboundMethod: telegram.inbound + outboundMethod: telegram.outbound +features: + - id: telegram-messaging + name: Telegram messaging + resources: [] + contributions: + - type: identity + id: telegram-bot + - type: connector + id: telegram + capabilities: + - messaging.send +runtime: + transport: stdio + entrypoint: dist/stdio-entrypoint.js diff --git a/packages/connector-telegram/src/TelegramAdapter.test.ts b/packages/connector-telegram/src/TelegramAdapter.test.ts new file mode 100644 index 00000000..f89a75bd --- /dev/null +++ b/packages/connector-telegram/src/TelegramAdapter.test.ts @@ -0,0 +1,115 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { TelegramAdapter } from './TelegramAdapter.js'; +import { formatTelegramHtml } from './telegram-html-formatter.js'; +import { normalizeTelegramBotToken } from './token.js'; +import type { ConnectorLogger, RichBlock } from './types.js'; + +function recordingLogger() { + const entries = { info: [] as unknown[][], warn: [] as unknown[][], error: [] as unknown[][] }; + const logger: ConnectorLogger = { + info: (...args) => entries.info.push([...args]), + warn: (...args) => entries.warn.push([...args]), + error: (...args) => entries.error.push([...args]), + }; + return { entries, logger }; +} + +async function flushPollingLoop(): Promise { + await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise(resolve => setTimeout(resolve, 0)); +} + +test('token validation rejects malformed or blank secrets before adapter creation', () => { + assert.equal(normalizeTelegramBotToken(' 123456:abcdefghij_ABC-123 '), '123456:abcdefghij_ABC-123'); + assert.equal(normalizeTelegramBotToken('not-a-token'), null); + assert.equal(normalizeTelegramBotToken(''), null); +}); + +test('inbound parsing accepts only private human messages and preserves file identifiers', () => { + const { logger } = recordingLogger(); + const subject = new TelegramAdapter('123456:abcdefghij_ABC-123', logger); + const direct = subject.parseUpdate({ + message: { + message_id: 456, + from: { id: 789, is_bot: false }, + chat: { id: 123, type: 'private' }, + text: 'hello', + }, + }); + assert.deepEqual(direct, { chatId: '123', text: 'hello', messageId: '456', senderId: '789' }); + const photo = subject.parseUpdate({ + message: { + message_id: 457, + from: { id: 789, is_bot: false }, + chat: { id: 123, type: 'private' }, + caption: 'photo', + photo: [{ file_id: 'small' }, { file_id: 'largest' }], + }, + }); + assert.deepEqual(photo?.attachments, [{ type: 'image', telegramFileId: 'largest' }]); + assert.equal(subject.parseUpdate({ message: { from: { id: 1 }, chat: { id: 2, type: 'group' }, text: 'x' } }), null); + assert.equal(subject.parseUpdate({ message: { from: { id: 1, is_bot: true }, chat: { id: 2, type: 'private' }, text: 'x' } }), null); +}); + +test('outbound text is split without truncation or surrogate-pair corruption', async () => { + const { logger } = recordingLogger(); + const subject = new TelegramAdapter('123456:abcdefghij_ABC-123', logger); + const sent: string[] = []; + subject._injectSendMessage(async (_chatId, text) => { + sent.push(text); + }); + const value = `${'A'.repeat(4095)}😀tail`; + await subject.sendReply('123', value); + assert.ok(sent.length >= 2); + assert.ok(sent.every(segment => segment.length <= 4096)); + assert.equal(sent.join(''), value); + assert.ok(sent.every(segment => !segment.includes('\ufffd'))); +}); + +test('rich formatter escapes untrusted HTML and keeps structured content', () => { + const blocks: RichBlock[] = [{ + id: 'card-1', + v: 1, + kind: 'card', + title: '', + fields: [{ label: 'k&', value: '' }], + }]; + const html = formatTelegramHtml(blocks, 'Cat ', 'hello & goodbye'); + assert.doesNotMatch(html, /'); + assert.doesNotMatch(tag, /