mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
Add DingTalk to unified IM plugin
This commit is contained in:
parent
c7b573a7aa
commit
af7e6a2296
14 changed files with 1809 additions and 578 deletions
34
README.md
34
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 中文
|
||||
|
||||
通过扫码把 IM 机器人接入 DeepSeek Harness。一个插件、一个设置入口,统一管理飞书和微信机器人。
|
||||
通过扫码把 IM 机器人接入 DeepSeek Harness。一个插件、一个设置入口,统一管理飞书、微信和钉钉机器人。
|
||||
|
||||
> GitHub 简介:通过扫码把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉等)。
|
||||
|
||||
|
|
@ -13,9 +13,10 @@
|
|||
## 当前内置渠道
|
||||
|
||||
- 飞书:扫码创建并绑定机器人,使用长连接收发消息;
|
||||
- 微信:扫码绑定微信机器人,使用腾讯 iLink 长轮询收发消息。
|
||||
- 微信:扫码绑定微信机器人,使用腾讯 iLink 长轮询收发消息;
|
||||
- 钉钉:扫码创建并授权机器人,使用钉钉 Stream 长连接收发消息。
|
||||
|
||||
钉钉等其他 IM 平台可继续按同一渠道适配器结构接入;当前可操作界面仅展示已实现的飞书和微信。
|
||||
其他 IM 平台可继续按同一渠道适配器结构接入。
|
||||
|
||||
## 安装
|
||||
|
||||
|
|
@ -23,14 +24,16 @@
|
|||
npx -y github:xmanrui/dsh-im install
|
||||
```
|
||||
|
||||
重启 `dsh web`,然后打开「设置 → 插件 → IM机器人」。安装器会用 `dsh-im` 替换 profile 中直接安装的 `dsh-feishu` 和 `dsh-weixin`,但不删除任何渠道数据;原有飞书凭据和微信扫码绑定会继续使用。
|
||||
重启 `dsh web`,然后打开「设置 → 插件 → IM机器人」。安装器会用 `dsh-im` 替换 profile 中直接安装的 `dsh-feishu`、`dsh-weixin` 和 `dsh-dingtalk`,但不删除任何渠道数据;原有渠道凭据和扫码绑定会继续使用。
|
||||
|
||||
钉钉接入时,用钉钉 App 扫描页面二维码,再在钉钉授权页点击「一键创建新机器人」。钉钉当前可能在该官方授权页显示 OpenClaw 品牌;扫码后的机器人连接、凭据和消息均由 DeepSeek Harness 管理。由于扫码结果不包含扫码人的 staff ID,首次私聊后还需要在本机页面批准该使用者,未批准消息不会进入 Harness。
|
||||
|
||||
## 设计
|
||||
|
||||
- Harness 中只注册一个「IM机器人」设置页;
|
||||
- 左侧使用飞书和微信 Logo 切换渠道,不使用启用/停用开关;
|
||||
- 两个渠道保持独立的 RPC、凭据、连接监督和会话映射;
|
||||
- 浏览器只获得二维码和脱敏状态,不获得 App Secret 或 `bot_token`。
|
||||
- 左侧使用渠道 Logo 切换微信、飞书和钉钉,不使用启用/停用开关;
|
||||
- 三个渠道保持独立的 RPC、凭据、连接监督和会话映射;
|
||||
- 浏览器只获得二维码和脱敏状态,不获得 App Secret、`bot_token`、钉钉 `client_secret` 或原始 staff ID。
|
||||
|
||||
## 本地开发
|
||||
|
||||
|
|
@ -40,13 +43,13 @@ npm run check
|
|||
node bin/dsh-im.mjs install --source .
|
||||
```
|
||||
|
||||
`npm run check` 运行单元测试、构建 Host/Client 产物,并验证发布包不包含凭据或独立飞书/微信设置页注册。
|
||||
`npm run check` 运行单元测试、构建 Host/Client 产物,并验证发布包不包含凭据或独立渠道设置页注册。
|
||||
|
||||
---
|
||||
|
||||
## English
|
||||
|
||||
Connect IM bots to DeepSeek Harness by scanning a QR code. One plugin and one settings entry provide unified management for Feishu and WeChat bots.
|
||||
Connect IM bots to DeepSeek Harness by scanning a QR code. One plugin and one settings entry provide unified management for Feishu, WeChat, and DingTalk bots.
|
||||
|
||||
> GitHub description: Connect IM bots to DeepSeek Harness by scanning a QR code (supports Feishu, WeChat, DingTalk, and more).
|
||||
|
||||
|
|
@ -58,8 +61,9 @@ Connect IM bots to DeepSeek Harness by scanning a QR code. One plugin and one se
|
|||
|
||||
- Feishu: create and bind a bot by scanning a QR code, then send and receive messages over a persistent connection.
|
||||
- WeChat: bind a WeChat bot by scanning a QR code, then send and receive messages through Tencent iLink long polling.
|
||||
- DingTalk: create and authorize a bot by scanning a QR code, then send and receive messages through DingTalk Stream.
|
||||
|
||||
Other IM platforms such as DingTalk can be added through the same channel-adapter structure. The current interface only exposes the implemented Feishu and WeChat channels.
|
||||
Other IM platforms can be added through the same channel-adapter structure.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -67,14 +71,16 @@ Other IM platforms such as DingTalk can be added through the same channel-adapte
|
|||
npx -y github:xmanrui/dsh-im install
|
||||
```
|
||||
|
||||
Restart `dsh web`, then open **Settings → Plugins → IM Bot**. The installer replaces directly installed `dsh-feishu` and `dsh-weixin` entries in the profile with `dsh-im` without deleting channel data. Existing Feishu credentials and WeChat QR bindings continue to work.
|
||||
Restart `dsh web`, then open **Settings → Plugins → IM Bot**. The installer replaces directly installed `dsh-feishu`, `dsh-weixin`, and `dsh-dingtalk` entries in the profile with `dsh-im` without deleting channel data.
|
||||
|
||||
For DingTalk, scan the QR code with the DingTalk app and choose **Create a new bot** on the authorization page. That DingTalk-hosted page may currently display OpenClaw branding; the resulting connection, credentials, and messages are managed by DeepSeek Harness. Because the scan result does not identify the scanning user, send the bot a direct message and approve that sender locally before the message can enter Harness.
|
||||
|
||||
## Design
|
||||
|
||||
- Registers a single **IM Bot** settings page in Harness.
|
||||
- Uses Feishu and WeChat logos for channel navigation without enable/disable switches.
|
||||
- Uses channel logos for WeChat, Feishu, and DingTalk navigation without enable/disable switches.
|
||||
- Keeps RPC endpoints, credentials, connection supervision, and session mappings isolated by channel.
|
||||
- Sends only QR codes and redacted status data to the browser, never App Secrets or `bot_token` values.
|
||||
- Sends only QR codes and redacted status data to the browser, never App Secrets, `bot_token`, DingTalk `client_secret`, or raw staff IDs.
|
||||
|
||||
## Local development
|
||||
|
||||
|
|
@ -84,4 +90,4 @@ npm run check
|
|||
node bin/dsh-im.mjs install --source .
|
||||
```
|
||||
|
||||
`npm run check` runs unit tests, builds the Host and Client artifacts, and verifies that the published package contains neither credentials nor standalone Feishu or WeChat settings-page registrations.
|
||||
`npm run check` runs unit tests, builds the Host and Client artifacts, and verifies that the published package contains neither credentials nor standalone channel settings-page registrations.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Third-party notices
|
||||
|
||||
This package composes [`@xmanrui/dsh-feishu`](https://github.com/xmanrui/dsh-feishu) at commit `aad650feabadd511241aa58b236d64273d5e397f` and [`@xmanrui/dsh-weixin`](https://github.com/xmanrui/dsh-weixin) at commit `76d076771b2c84fb4c5598c2344d486695eda080`, both under the MIT License.
|
||||
This package composes [`@xmanrui/dsh-feishu`](https://github.com/xmanrui/dsh-feishu) at commit `aad650feabadd511241aa58b236d64273d5e397f`, [`@xmanrui/dsh-weixin`](https://github.com/xmanrui/dsh-weixin) at commit `76d076771b2c84fb4c5598c2344d486695eda080`, and [`@xmanrui/dsh-dingtalk`](https://github.com/xmanrui/dsh-dingtalk) at commit `67f7211b8011f0623b47ce9e01004bbab8e6614f`, all under the MIT License.
|
||||
|
||||
The channel client components are adapted from those projects and preserve their security model: secrets remain in the DeepSeek Harness Host credential provider and never enter the browser-facing RPC response.
|
||||
|
||||
The WeChat mark uses path data published by Simple Icons under the CC0 1.0 Universal license. The Feishu mark is an original inline vector used for channel identification. Product names and logos remain trademarks of their respective owners.
|
||||
The WeChat mark uses path data published by Simple Icons under the CC0 1.0 Universal license. The Feishu and DingTalk marks are inline vectors used for channel identification. Product names and logos remain trademarks of their respective owners.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import { spawnSync } from 'node:child_process';
|
|||
|
||||
const PACKAGE_NAME = '@xmanrui/dsh-im';
|
||||
const DEFAULT_SOURCE = 'github:xmanrui/dsh-im';
|
||||
const LEGACY_PACKAGES = ['@xmanrui/dsh-feishu', '@xmanrui/dsh-weixin'];
|
||||
const LEGACY_PACKAGES = [
|
||||
'@xmanrui/dsh-feishu',
|
||||
'@xmanrui/dsh-weixin',
|
||||
'@xmanrui/dsh-dingtalk',
|
||||
];
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage:
|
||||
|
|
@ -80,7 +84,7 @@ try {
|
|||
}
|
||||
console.log('\nIM 机器人插件已安装。请重启 dsh web,然后打开「设置 → 插件 → IM机器人」。');
|
||||
if (legacy.size > 0) {
|
||||
console.log('已用 dsh-im 替换独立飞书/微信插件;原有凭据和扫码绑定保持不变。');
|
||||
console.log('已用 dsh-im 替换独立飞书/微信/钉钉插件;原有凭据和扫码绑定保持不变。');
|
||||
}
|
||||
} else if (command === 'uninstall') {
|
||||
if (args.length > 0) throw new Error(`无法识别的参数:${args.join(' ')}`);
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 42 KiB |
2223
lib/client.js
2223
lib/client.js
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
|||
// plugin-src/host/index.mjs
|
||||
import { apply as applyDingtalk } from "@xmanrui/dsh-dingtalk";
|
||||
import { apply as applyFeishu } from "@xmanrui/dsh-feishu";
|
||||
import { apply as applyWeixin } from "@xmanrui/dsh-weixin";
|
||||
var name = "dsh-im-host";
|
||||
|
|
@ -6,12 +7,14 @@ var inject = ["connection", "credentials", "webServer"];
|
|||
function createImHostPlugin(internals = {}) {
|
||||
const startFeishu = internals.applyFeishu ?? applyFeishu;
|
||||
const startWeixin = internals.applyWeixin ?? applyWeixin;
|
||||
const startDingtalk = internals.applyDingtalk ?? applyDingtalk;
|
||||
return Object.freeze({
|
||||
name,
|
||||
inject,
|
||||
async apply(ctx, config = {}) {
|
||||
await startFeishu(ctx, config.feishu ?? {});
|
||||
await startWeixin(ctx, config.weixin ?? {});
|
||||
await startDingtalk(ctx, config.dingtalk ?? {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xmanrui/dsh-dingtalk": "https://github.com/xmanrui/dsh-dingtalk/archive/67f7211b8011f0623b47ce9e01004bbab8e6614f.tar.gz",
|
||||
"@xmanrui/dsh-feishu": "https://github.com/xmanrui/dsh-feishu/archive/aad650feabadd511241aa58b236d64273d5e397f.tar.gz",
|
||||
"@xmanrui/dsh-weixin": "https://github.com/xmanrui/dsh-weixin/archive/76d076771b2c84fb4c5598c2344d486695eda080.tar.gz"
|
||||
},
|
||||
|
|
@ -547,6 +548,22 @@
|
|||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmanrui/dsh-dingtalk": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://github.com/xmanrui/dsh-dingtalk/archive/67f7211b8011f0623b47ce9e01004bbab8e6614f.tar.gz",
|
||||
"integrity": "sha512-aOA3vvEL7mKt/NIlMRVJTWB+V5E+J3gWgqE0Q19in8KTrEwYqUk1TYe08B/OdikEghwaQnNfri2bqqpgsCzCqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dingtalk-stream": "2.1.4",
|
||||
"qrcode": "1.5.4"
|
||||
},
|
||||
"bin": {
|
||||
"dsh-dingtalk": "bin/dsh-dingtalk.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmanrui/dsh-feishu": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://github.com/xmanrui/dsh-feishu/archive/aad650feabadd511241aa58b236d64273d5e397f.tar.gz",
|
||||
|
|
@ -753,6 +770,17 @@
|
|||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dingtalk-stream": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dingtalk-stream/-/dingtalk-stream-2.1.4.tgz",
|
||||
"integrity": "sha512-rgQbXLGWfASuB9onFcqXTnRSj4ZotimhBOnzrB4kS19AaU9lshXiuofs1GAYcKh5uzPWCAuEs3tMtiadTQWP4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"debug": "^4.3.4",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
"node": ">=22.19"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xmanrui/dsh-dingtalk": "https://github.com/xmanrui/dsh-dingtalk/archive/67f7211b8011f0623b47ce9e01004bbab8e6614f.tar.gz",
|
||||
"@xmanrui/dsh-feishu": "https://github.com/xmanrui/dsh-feishu/archive/aad650feabadd511241aa58b236d64273d5e397f.tar.gz",
|
||||
"@xmanrui/dsh-weixin": "https://github.com/xmanrui/dsh-weixin/archive/76d076771b2c84fb4c5598c2344d486695eda080.tar.gz"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { DINGTALK_RPC_CHANNEL } from '@xmanrui/dsh-dingtalk/client-api';
|
||||
import { DingtalkSettingsTab } from '@xmanrui/dsh-dingtalk/client-source';
|
||||
import { FeishuSettingsTab } from './channels/feishu/index.js';
|
||||
import { FEISHU_RPC_CHANNEL } from './channels/feishu/api.js';
|
||||
import { installFeishuStyles } from './channels/feishu/styles.js';
|
||||
|
|
@ -16,6 +18,7 @@ export const inject = ['slots', 'connection'];
|
|||
const CHANNELS = Object.freeze([
|
||||
{ id: 'weixin', label: '微信' },
|
||||
{ id: 'feishu', label: '飞书' },
|
||||
{ id: 'dingtalk', label: '钉钉' },
|
||||
]);
|
||||
|
||||
function WeixinLogo() {
|
||||
|
|
@ -37,17 +40,29 @@ function FeishuLogo() {
|
|||
));
|
||||
}
|
||||
|
||||
function ChannelLogo({ channel }) {
|
||||
return channel === 'weixin' ? h(WeixinLogo) : h(FeishuLogo);
|
||||
function DingtalkLogo() {
|
||||
return h('span', { className: 'dim-logo dim-logoDingtalk', 'aria-hidden': 'true' },
|
||||
h('svg', { viewBox: '0 0 48 48', focusable: 'false' },
|
||||
h('path', {
|
||||
fill: 'currentColor',
|
||||
d: 'M37.05 22.783c-6.758-5.216-14.378-12.128-22.73-19.538-.655-.585-1.242-.354-1.536.42-1.88 4.973-.058 9.386 2.889 11.932s7.368 4.912 10.058 6.155c.105.049.013.203-.093.163-4.953-2.182-8.397-3.765-13.07-7.368-.497-.388-1.01-.242-1.07.521-.384 4.748 2.657 8.483 6.058 9.745 2.1.781 4.398 1.212 6.53 1.474.109.015.084.178-.027.178-2.747.01-6.058-.654-8.935-1.751-.606-.233-.818.25-.722.633.491 2.008 2.974 5.076 6.926 5.73a12 12 0 0 0 2.228.115c.164 0 .208.089.154.217q-2.685 4.6-2.803 4.797c-.091.152-.036.275.156.275h3.543c.164 0 .264.106.18.246l-4.958 8.196c-.191.328.035.565.395.301s15.212-11.133 15.636-11.448c.195-.142.148-.327-.124-.327h-3.18c-.206 0-.252-.14-.111-.28.14-.141 3.602-3.594 4.837-4.888 1.283-1.35 1.938-3.825-.231-5.498',
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
export function IMSettingsTab({ feishuRpcCall, weixinRpcCall }) {
|
||||
function ChannelLogo({ channel }) {
|
||||
if (channel === 'weixin') return h(WeixinLogo);
|
||||
if (channel === 'feishu') return h(FeishuLogo);
|
||||
return h(DingtalkLogo);
|
||||
}
|
||||
|
||||
export function IMSettingsTab({ dingtalkRpcCall, feishuRpcCall, weixinRpcCall }) {
|
||||
const [selected, setSelected] = React.useState('weixin');
|
||||
const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
|
||||
return h('section', { className: 'dim-page', 'aria-label': 'IM机器人设置' },
|
||||
h('header', { className: 'dim-title' },
|
||||
h('p', null, '通过扫码把机器人接入 DeepSeek Harness'),
|
||||
h('div', { className: 'dim-channelCount' }, '2 个渠道'),
|
||||
h('div', { className: 'dim-channelCount' }, `${CHANNELS.length} 个渠道`),
|
||||
),
|
||||
h('div', { className: 'dim-layout' },
|
||||
h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM 渠道' },
|
||||
|
|
@ -73,7 +88,9 @@ export function IMSettingsTab({ feishuRpcCall, weixinRpcCall }) {
|
|||
'aria-labelledby': `dim-tab-${active.id}`,
|
||||
}, active.id === 'weixin'
|
||||
? h(WeixinSettingsTab, { rpcCall: weixinRpcCall })
|
||||
: h(FeishuSettingsTab, { rpcCall: feishuRpcCall })),
|
||||
: active.id === 'feishu'
|
||||
? h(FeishuSettingsTab, { rpcCall: feishuRpcCall })
|
||||
: h(DingtalkSettingsTab, { rpcCall: dingtalkRpcCall })),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -90,12 +107,14 @@ export function apply(ctx) {
|
|||
ctx.connection.rpc.call(FEISHU_RPC_CHANNEL, endpoint, payload, signal);
|
||||
const weixinRpcCall = (endpoint, payload, signal) =>
|
||||
ctx.connection.rpc.call(WEIXIN_RPC_CHANNEL, endpoint, payload, signal);
|
||||
const dingtalkRpcCall = (endpoint, payload, signal) =>
|
||||
ctx.connection.rpc.call(DINGTALK_RPC_CHANNEL, endpoint, payload, signal);
|
||||
|
||||
ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
|
||||
name: 'settings.plugins.tab',
|
||||
id: 'im',
|
||||
order: 20,
|
||||
label: 'IM机器人',
|
||||
inject: () => ({ feishuRpcCall, weixinRpcCall }),
|
||||
inject: () => ({ dingtalkRpcCall, feishuRpcCall, weixinRpcCall }),
|
||||
}, IMSettingsTab));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,13 +28,16 @@ const CSS = String.raw`
|
|||
.dim-logoWeixin svg { width: 19px; height: 19px; }
|
||||
.dim-logoFeishu { background: white; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); }
|
||||
.dim-logoFeishu svg { width: 22px; height: 22px; }
|
||||
.dim-logoDingtalk { color: white; background: #1677ff; }
|
||||
.dim-logoDingtalk svg { width: 24px; height: 24px; }
|
||||
.dim-channelCopy { min-width: 0; display: block; }
|
||||
.dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-line-divider, #eef0f3); }
|
||||
.dim-panel { min-width: 0; }
|
||||
.dim-panel .bxf-page, .dim-panel .dxw-page { width: 100%; max-width: none; padding: 0 0 24px; }
|
||||
.dim-panel .bxf-heading, .dim-panel .dxw-heading { justify-content: flex-end; }
|
||||
.dim-panel .bxf-headingTools, .dim-panel .dxw-tools { width: 100%; justify-content: flex-end; }
|
||||
.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page { width: 100%; max-width: none; padding: 0 0 24px; }
|
||||
.dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; }
|
||||
.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; justify-content: flex-end; }
|
||||
.dim-panel .ddt-headingCopy { display: none; }
|
||||
@media (max-width: 840px) {
|
||||
.dim-title { align-items: flex-start; }
|
||||
.dim-layout { grid-template-columns: minmax(0, 1fr); gap: 18px; }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { apply as applyDingtalk } from '@xmanrui/dsh-dingtalk';
|
||||
import { apply as applyFeishu } from '@xmanrui/dsh-feishu';
|
||||
import { apply as applyWeixin } from '@xmanrui/dsh-weixin';
|
||||
|
||||
|
|
@ -7,12 +8,14 @@ export const inject = ['connection', 'credentials', 'webServer'];
|
|||
export function createImHostPlugin(internals = {}) {
|
||||
const startFeishu = internals.applyFeishu ?? applyFeishu;
|
||||
const startWeixin = internals.applyWeixin ?? applyWeixin;
|
||||
const startDingtalk = internals.applyDingtalk ?? applyDingtalk;
|
||||
return Object.freeze({
|
||||
name,
|
||||
inject,
|
||||
async apply(ctx, config = {}) {
|
||||
await startFeishu(ctx, config.feishu ?? {});
|
||||
await startWeixin(ctx, config.weixin ?? {});
|
||||
await startDingtalk(ctx, config.dingtalk ?? {});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,15 +33,18 @@ if ((client.match(/ctx\.slots\.inject\("settings\.plugins\.tab"/g) ?? []).length
|
|||
if (/role:\s*["']switch|type:\s*["']checkbox/.test(client)) {
|
||||
throw new Error('client bundle contains a channel enable switch');
|
||||
}
|
||||
if (!host.includes('@xmanrui/dsh-feishu') || !host.includes('@xmanrui/dsh-weixin')) {
|
||||
throw new Error('host bundle does not compose both channel providers');
|
||||
if (!host.includes('@xmanrui/dsh-feishu')
|
||||
|| !host.includes('@xmanrui/dsh-weixin')
|
||||
|| !host.includes('@xmanrui/dsh-dingtalk')) {
|
||||
throw new Error('host bundle does not compose all three channel providers');
|
||||
}
|
||||
if (!patch.includes("name: '@xmanrui/dsh-im'") || /dsh-(?:feishu|weixin)/.test(patch)) {
|
||||
if (!patch.includes("name: '@xmanrui/dsh-im'") || /dsh-(?:feishu|weixin|dingtalk)/.test(patch)) {
|
||||
throw new Error('bundle patch must activate only dsh-im');
|
||||
}
|
||||
for (const name of ['@xmanrui/dsh-feishu', '@xmanrui/dsh-weixin']) {
|
||||
for (const name of ['@xmanrui/dsh-feishu', '@xmanrui/dsh-weixin', '@xmanrui/dsh-dingtalk']) {
|
||||
const spec = manifest.dependencies?.[name];
|
||||
if (typeof spec !== 'string' || !spec.startsWith('https://github.com/')) {
|
||||
if (typeof spec !== 'string'
|
||||
|| !/^https:\/\/github\.com\/[^/]+\/[^/]+\/archive\/[0-9a-f]{40}\.tar\.gz$/.test(spec)) {
|
||||
throw new Error(`${name} must use a pinned public HTTPS archive`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,19 +6,22 @@ import { renderToStaticMarkup } from 'react-dom/server';
|
|||
|
||||
import { IMSettingsTab } from '../plugin-src/client/index.js';
|
||||
|
||||
test('IM settings renders two logo channel tabs without enable switches', () => {
|
||||
test('IM settings renders three compact logo channel tabs without enable switches', () => {
|
||||
const markup = renderToStaticMarkup(React.createElement(IMSettingsTab, {
|
||||
feishuRpcCall: async () => ({ ok: true, value: {} }),
|
||||
weixinRpcCall: async () => ({ ok: true, value: {} }),
|
||||
dingtalkRpcCall: async () => ({ ok: true, value: {} }),
|
||||
}));
|
||||
|
||||
assert.match(markup, /IM机器人/);
|
||||
assert.match(markup, /通过扫码把机器人接入 DeepSeek Harness/);
|
||||
assert.match(markup, />微信</);
|
||||
assert.match(markup, />飞书</);
|
||||
assert.match(markup, />钉钉</);
|
||||
assert.match(markup, /dim-logoWeixin/);
|
||||
assert.match(markup, /dim-logoFeishu/);
|
||||
assert.equal((markup.match(/role="tab"/g) ?? []).length, 2);
|
||||
assert.match(markup, /dim-logoDingtalk/);
|
||||
assert.equal((markup.match(/role="tab"/g) ?? []).length, 3);
|
||||
assert.equal((markup.match(/aria-selected="true"/g) ?? []).length, 1);
|
||||
assert.doesNotMatch(markup, /role="switch"|type="checkbox"/);
|
||||
assert.doesNotMatch(markup, /dim-chevron|扫码绑定<\/small>|扫码接入<\/small>/);
|
||||
|
|
|
|||
|
|
@ -3,14 +3,19 @@ import test from 'node:test';
|
|||
|
||||
import { createImHostPlugin, inject, name } from '../plugin-src/host/index.mjs';
|
||||
|
||||
test('Host composes Feishu and Weixin inside one plugin context', async () => {
|
||||
test('Host composes Feishu, Weixin, and DingTalk inside one plugin context', async () => {
|
||||
const calls = [];
|
||||
const plugin = createImHostPlugin({
|
||||
applyFeishu: async (ctx, config) => calls.push(['feishu', ctx, config]),
|
||||
applyWeixin: async (ctx, config) => calls.push(['weixin', ctx, config]),
|
||||
applyDingtalk: async (ctx, config) => calls.push(['dingtalk', ctx, config]),
|
||||
});
|
||||
const ctx = { marker: 'shared-context' };
|
||||
const config = { feishu: { domain: 'feishu' }, weixin: { timeout: 30 } };
|
||||
const config = {
|
||||
feishu: { domain: 'feishu' },
|
||||
weixin: { timeout: 30 },
|
||||
dingtalk: { replyTimeoutMs: 60_000 },
|
||||
};
|
||||
|
||||
await plugin.apply(ctx, config);
|
||||
|
||||
|
|
@ -19,6 +24,7 @@ test('Host composes Feishu and Weixin inside one plugin context', async () => {
|
|||
assert.deepEqual(calls, [
|
||||
['feishu', ctx, config.feishu],
|
||||
['weixin', ctx, config.weixin],
|
||||
['dingtalk', ctx, config.dingtalk],
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -27,8 +33,21 @@ test('Host does not start Weixin when Feishu activation fails', async () => {
|
|||
const plugin = createImHostPlugin({
|
||||
applyFeishu: async () => { throw new Error('feishu unavailable'); },
|
||||
applyWeixin: async () => { weixinStarted = true; },
|
||||
applyDingtalk: async () => { throw new Error('DingTalk must not start'); },
|
||||
});
|
||||
|
||||
await assert.rejects(() => plugin.apply({}, {}), /feishu unavailable/);
|
||||
assert.equal(weixinStarted, false);
|
||||
});
|
||||
|
||||
test('Host does not start DingTalk when Weixin activation fails', async () => {
|
||||
let dingtalkStarted = false;
|
||||
const plugin = createImHostPlugin({
|
||||
applyFeishu: async () => {},
|
||||
applyWeixin: async () => { throw new Error('weixin unavailable'); },
|
||||
applyDingtalk: async () => { dingtalkStarted = true; },
|
||||
});
|
||||
|
||||
await assert.rejects(() => plugin.apply({}, {}), /weixin unavailable/);
|
||||
assert.equal(dingtalkStarted, false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue