@xmanrui/dsh-im 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/bin/dsh-im.mjs +99 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +6211 -0
- package/lib/index.js +13339 -0
- package/package.json +73 -0
- package/plugin-src/client/build.mjs +38 -0
- package/plugin-src/client/channel-logos.js +107 -0
- package/plugin-src/client/channels/dingtalk/api.js +203 -0
- package/plugin-src/client/channels/dingtalk/index.js +709 -0
- package/plugin-src/client/channels/dingtalk/styles.js +140 -0
- package/plugin-src/client/channels/discord/api.js +11 -0
- package/plugin-src/client/channels/discord/index.js +25 -0
- package/plugin-src/client/channels/discord/styles.js +19 -0
- package/plugin-src/client/channels/feishu/api.js +325 -0
- package/plugin-src/client/channels/feishu/index.js +986 -0
- package/plugin-src/client/channels/feishu/styles.js +505 -0
- package/plugin-src/client/channels/qq/api.js +119 -0
- package/plugin-src/client/channels/qq/index.js +402 -0
- package/plugin-src/client/channels/qq/styles.js +19 -0
- package/plugin-src/client/channels/shared/token-api.js +88 -0
- package/plugin-src/client/channels/shared/token-channel.js +277 -0
- package/plugin-src/client/channels/telegram/api.js +11 -0
- package/plugin-src/client/channels/telegram/index.js +25 -0
- package/plugin-src/client/channels/telegram/styles.js +19 -0
- package/plugin-src/client/channels/wecom/api.js +119 -0
- package/plugin-src/client/channels/wecom/index.js +402 -0
- package/plugin-src/client/channels/wecom/styles.js +19 -0
- package/plugin-src/client/channels/weixin/api.js +156 -0
- package/plugin-src/client/channels/weixin/index.js +577 -0
- package/plugin-src/client/channels/weixin/styles.js +120 -0
- package/plugin-src/client/credential-binding.js +117 -0
- package/plugin-src/client/index.js +192 -0
- package/plugin-src/client/lifecycle.js +86 -0
- package/plugin-src/client/styles.js +192 -0
- package/plugin-src/host/build.mjs +24 -0
- package/plugin-src/host/channels/dingtalk/connection-supervisor.mjs +130 -0
- package/plugin-src/host/channels/dingtalk/index.mjs +39 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +122 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +237 -0
- package/plugin-src/host/channels/discord/index.mjs +30 -0
- package/plugin-src/host/channels/discord/production.mjs +17 -0
- package/plugin-src/host/channels/discord/rpc.mjs +21 -0
- package/plugin-src/host/channels/feishu/connection-supervisor.mjs +167 -0
- package/plugin-src/host/channels/feishu/controller.mjs +172 -0
- package/plugin-src/host/channels/feishu/credential-store.mjs +84 -0
- package/plugin-src/host/channels/feishu/index.mjs +73 -0
- package/plugin-src/host/channels/feishu/production.mjs +138 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +454 -0
- package/plugin-src/host/channels/qq/connection-supervisor.mjs +124 -0
- package/plugin-src/host/channels/qq/index.mjs +30 -0
- package/plugin-src/host/channels/qq/production.mjs +109 -0
- package/plugin-src/host/channels/qq/rpc.mjs +146 -0
- package/plugin-src/host/channels/shared/connection-supervisor.mjs +133 -0
- package/plugin-src/host/channels/shared/production.mjs +104 -0
- package/plugin-src/host/channels/shared/rpc.mjs +119 -0
- package/plugin-src/host/channels/telegram/index.mjs +30 -0
- package/plugin-src/host/channels/telegram/production.mjs +17 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +21 -0
- package/plugin-src/host/channels/wecom/connection-supervisor.mjs +124 -0
- package/plugin-src/host/channels/wecom/index.mjs +36 -0
- package/plugin-src/host/channels/wecom/production.mjs +113 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +151 -0
- package/plugin-src/host/channels/weixin/connection-supervisor.mjs +127 -0
- package/plugin-src/host/channels/weixin/index.mjs +39 -0
- package/plugin-src/host/channels/weixin/production.mjs +119 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +178 -0
- package/plugin-src/host/index.mjs +44 -0
- package/plugin-src/host/rpc-authority.mjs +11 -0
- package/scripts/verify-package.mjs +97 -0
- package/src/channels/dingtalk/config-store.mjs +282 -0
- package/src/channels/dingtalk/device-auth.mjs +237 -0
- package/src/channels/dingtalk/dingtalk-api.mjs +579 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +281 -0
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +233 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +752 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +358 -0
- package/src/channels/dingtalk/harness-client.mjs +299 -0
- package/src/channels/dingtalk/state-store.mjs +212 -0
- package/src/channels/discord/config-store.mjs +24 -0
- package/src/channels/discord/discord-api.mjs +153 -0
- package/src/channels/discord/discord-bridge.mjs +15 -0
- package/src/channels/discord/discord-controller.mjs +16 -0
- package/src/channels/discord/discord-runtime.mjs +460 -0
- package/src/channels/discord/harness-client.mjs +3 -0
- package/src/channels/discord/state-store.mjs +3 -0
- package/src/channels/feishu/bridge.mjs +216 -0
- package/src/channels/feishu/config.mjs +71 -0
- package/src/channels/feishu/feishu-app.mjs +51 -0
- package/src/channels/feishu/feishu-channel.mjs +153 -0
- package/src/channels/feishu/feishu-runtime.mjs +222 -0
- package/src/channels/feishu/harness-client.mjs +268 -0
- package/src/channels/feishu/message-utils.mjs +51 -0
- package/src/channels/feishu/multi-bot-controller.mjs +686 -0
- package/src/channels/feishu/plugin-config-store.mjs +204 -0
- package/src/channels/feishu/plugin-controller.mjs +248 -0
- package/src/channels/feishu/registration-manager.mjs +345 -0
- package/src/channels/feishu/state-store.mjs +71 -0
- package/src/channels/qq/config-store.mjs +171 -0
- package/src/channels/qq/harness-client.mjs +3 -0
- package/src/channels/qq/qq-bridge.mjs +177 -0
- package/src/channels/qq/qq-controller.mjs +452 -0
- package/src/channels/qq/qq-runtime.mjs +208 -0
- package/src/channels/qq/qr-auth.mjs +24 -0
- package/src/channels/qq/state-store.mjs +96 -0
- package/src/channels/shared/conversation-state-store.mjs +107 -0
- package/src/channels/shared/editable-message-stream.mjs +81 -0
- package/src/channels/shared/text-harness-bridge.mjs +180 -0
- package/src/channels/shared/token-bot-controller.mjs +298 -0
- package/src/channels/shared/token-config-store.mjs +180 -0
- package/src/channels/telegram/config-store.mjs +24 -0
- package/src/channels/telegram/harness-client.mjs +3 -0
- package/src/channels/telegram/state-store.mjs +3 -0
- package/src/channels/telegram/telegram-api.mjs +120 -0
- package/src/channels/telegram/telegram-bridge.mjs +15 -0
- package/src/channels/telegram/telegram-controller.mjs +16 -0
- package/src/channels/telegram/telegram-runtime.mjs +278 -0
- package/src/channels/wecom/config-store.mjs +170 -0
- package/src/channels/wecom/harness-client.mjs +3 -0
- package/src/channels/wecom/qr-auth.mjs +95 -0
- package/src/channels/wecom/state-store.mjs +90 -0
- package/src/channels/wecom/wecom-bridge.mjs +243 -0
- package/src/channels/wecom/wecom-controller.mjs +474 -0
- package/src/channels/wecom/wecom-runtime.mjs +209 -0
- package/src/channels/weixin/config-store.mjs +182 -0
- package/src/channels/weixin/harness-client.mjs +259 -0
- package/src/channels/weixin/state-store.mjs +112 -0
- package/src/channels/weixin/weixin-api.mjs +319 -0
- package/src/channels/weixin/weixin-bridge.mjs +173 -0
- package/src/channels/weixin/weixin-controller.mjs +545 -0
- package/src/channels/weixin/weixin-runtime.mjs +204 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { unlink } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { WeixinConfigStore } from '../../../../src/channels/weixin/config-store.mjs';
|
|
6
|
+
import { HarnessClient } from '../../../../src/channels/weixin/harness-client.mjs';
|
|
7
|
+
import { WeixinStateStore } from '../../../../src/channels/weixin/state-store.mjs';
|
|
8
|
+
import { createWeixinApi } from '../../../../src/channels/weixin/weixin-api.mjs';
|
|
9
|
+
import { WeixinController } from '../../../../src/channels/weixin/weixin-controller.mjs';
|
|
10
|
+
import { WeixinRuntime } from '../../../../src/channels/weixin/weixin-runtime.mjs';
|
|
11
|
+
import { createConnectionSupervisor } from './connection-supervisor.mjs';
|
|
12
|
+
|
|
13
|
+
function harnessOrigin(webServer, configured) {
|
|
14
|
+
if (configured !== undefined) return new URL(configured);
|
|
15
|
+
const port = webServer?.port;
|
|
16
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
17
|
+
throw new Error('dsh-weixin requires an initialized DSH webServer port');
|
|
18
|
+
}
|
|
19
|
+
return new URL(`http://127.0.0.1:${port}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pluginPaths(config) {
|
|
23
|
+
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
|
|
24
|
+
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-weixin'));
|
|
25
|
+
return {
|
|
26
|
+
root,
|
|
27
|
+
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
28
|
+
accounts: resolve(config.accountsDir ?? join(root, 'accounts')),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
33
|
+
if (!ctx?.credentials) throw new TypeError('dsh-weixin requires ctx.credentials');
|
|
34
|
+
if (!ctx?.webServer) throw new TypeError('dsh-weixin requires ctx.webServer');
|
|
35
|
+
|
|
36
|
+
const ConfigStore = internals.ConfigStore ?? WeixinConfigStore;
|
|
37
|
+
const StateStore = internals.StateStore ?? WeixinStateStore;
|
|
38
|
+
const Harness = internals.HarnessClient ?? HarnessClient;
|
|
39
|
+
const Controller = internals.Controller ?? WeixinController;
|
|
40
|
+
const Runtime = internals.Runtime ?? WeixinRuntime;
|
|
41
|
+
const api = internals.api ?? createWeixinApi();
|
|
42
|
+
const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor;
|
|
43
|
+
const logger = typeof ctx.logger === 'function'
|
|
44
|
+
? ctx.logger('dsh-weixin')
|
|
45
|
+
: (ctx.logger ?? console);
|
|
46
|
+
const paths = pluginPaths(config);
|
|
47
|
+
const configStore = await new ConfigStore(paths.config).load();
|
|
48
|
+
const stateStores = new Map();
|
|
49
|
+
|
|
50
|
+
const statePath = (botId) => resolve(paths.accounts, botId, 'state.json');
|
|
51
|
+
const stateFor = async (botId) => {
|
|
52
|
+
let state = stateStores.get(botId);
|
|
53
|
+
if (!state) {
|
|
54
|
+
state = await new StateStore(statePath(botId)).load();
|
|
55
|
+
stateStores.set(botId, state);
|
|
56
|
+
}
|
|
57
|
+
return state;
|
|
58
|
+
};
|
|
59
|
+
const harness = new Harness({
|
|
60
|
+
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
61
|
+
workspace: resolve(config.workspace ?? process.cwd()),
|
|
62
|
+
agentPreset: config.agentPreset ?? 'standard',
|
|
63
|
+
autostart: false,
|
|
64
|
+
dshBin: config.dshBin ?? 'dsh',
|
|
65
|
+
});
|
|
66
|
+
const controller = new Controller({
|
|
67
|
+
api,
|
|
68
|
+
credentials: ctx.credentials,
|
|
69
|
+
configStore,
|
|
70
|
+
logger,
|
|
71
|
+
createRuntime: async ({ botId, config: accountConfig, token }) => {
|
|
72
|
+
const state = await stateFor(botId);
|
|
73
|
+
return new Runtime({
|
|
74
|
+
api,
|
|
75
|
+
config: accountConfig,
|
|
76
|
+
token,
|
|
77
|
+
harness,
|
|
78
|
+
state,
|
|
79
|
+
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
80
|
+
maxMessageChars: config.maxMessageChars ?? 4_000,
|
|
81
|
+
logger: {
|
|
82
|
+
error: (...args) => logger.error?.(`[${botId}]`, ...args),
|
|
83
|
+
warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
|
|
84
|
+
info: (...args) => logger.info?.(`[${botId}]`, ...args),
|
|
85
|
+
debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
deleteState: async ({ botId }) => {
|
|
90
|
+
const state = stateStores.get(botId);
|
|
91
|
+
stateStores.delete(botId);
|
|
92
|
+
if (state && typeof state.remove === 'function') {
|
|
93
|
+
await state.remove();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await unlink(statePath(botId));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const supervisor = createSupervisor({
|
|
104
|
+
controller,
|
|
105
|
+
harness,
|
|
106
|
+
logger,
|
|
107
|
+
retryDelaysMs: config.retryDelaysMs,
|
|
108
|
+
healthyIntervalMs: config.healthyIntervalMs,
|
|
109
|
+
}).start();
|
|
110
|
+
return {
|
|
111
|
+
controller,
|
|
112
|
+
ready: supervisor.ready,
|
|
113
|
+
async close() {
|
|
114
|
+
await supervisor.close();
|
|
115
|
+
await controller.close();
|
|
116
|
+
harness.stopManagedProcess();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import QRCode from 'qrcode';
|
|
2
|
+
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
3
|
+
|
|
4
|
+
export const WEIXIN_RPC_CHANNEL = '/weixin';
|
|
5
|
+
export const WEIXIN_ENDPOINTS = Object.freeze({
|
|
6
|
+
status: 'connection.status',
|
|
7
|
+
beginProvisioning: 'provision.begin',
|
|
8
|
+
pollProvisioning: 'provision.poll',
|
|
9
|
+
submitVerification: 'provision.verify',
|
|
10
|
+
cancelProvisioning: 'provision.cancel',
|
|
11
|
+
reconnectBot: 'bot.reconnect',
|
|
12
|
+
deleteBot: 'bot.delete',
|
|
13
|
+
});
|
|
14
|
+
export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
|
|
15
|
+
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function exactKeys(value, allowed) {
|
|
21
|
+
return isRecord(value) && Object.keys(value).every((key) => allowed.includes(key));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validId(value) {
|
|
25
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function payloadFailure(endpoint, payload) {
|
|
29
|
+
if (!isRecord(payload)) return 'Payload must be an object.';
|
|
30
|
+
if (endpoint === WEIXIN_ENDPOINTS.status) {
|
|
31
|
+
return exactKeys(payload, []) ? null : 'connection.status does not accept fields.';
|
|
32
|
+
}
|
|
33
|
+
if (endpoint === WEIXIN_ENDPOINTS.beginProvisioning) {
|
|
34
|
+
return exactKeys(payload, ['locale']) && (payload.locale === undefined || payload.locale === 'zh-CN')
|
|
35
|
+
? null
|
|
36
|
+
: 'provision.begin received unsupported fields.';
|
|
37
|
+
}
|
|
38
|
+
if ([WEIXIN_ENDPOINTS.pollProvisioning, WEIXIN_ENDPOINTS.cancelProvisioning].includes(endpoint)) {
|
|
39
|
+
return exactKeys(payload, ['attemptId']) && validId(payload.attemptId)
|
|
40
|
+
? null
|
|
41
|
+
: `${endpoint} requires an attemptId.`;
|
|
42
|
+
}
|
|
43
|
+
if (endpoint === WEIXIN_ENDPOINTS.submitVerification) {
|
|
44
|
+
return exactKeys(payload, ['attemptId', 'verifyCode'])
|
|
45
|
+
&& validId(payload.attemptId)
|
|
46
|
+
&& typeof payload.verifyCode === 'string'
|
|
47
|
+
&& /^\d{4,8}$/.test(payload.verifyCode)
|
|
48
|
+
? null
|
|
49
|
+
: 'provision.verify requires an attemptId and a 4-to-8-digit code.';
|
|
50
|
+
}
|
|
51
|
+
if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) {
|
|
52
|
+
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
53
|
+
? null
|
|
54
|
+
: 'bot.reconnect requires a botId.';
|
|
55
|
+
}
|
|
56
|
+
if (endpoint === WEIXIN_ENDPOINTS.deleteBot) {
|
|
57
|
+
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
58
|
+
? null
|
|
59
|
+
: 'bot.delete requires a botId and confirm=true.';
|
|
60
|
+
}
|
|
61
|
+
return 'Unknown Weixin endpoint.';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function badRequest(message) {
|
|
65
|
+
return { ok: false, error: { code: 'bad-request', message } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cancelled() {
|
|
69
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function internalFailure() {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: { code: 'weixin-operation-failed', message: '微信操作失败,请稍后重试。' },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function qrDataUrl(value) {
|
|
80
|
+
return QRCode.toDataURL(value, {
|
|
81
|
+
type: 'image/png',
|
|
82
|
+
errorCorrectionLevel: 'M',
|
|
83
|
+
margin: 2,
|
|
84
|
+
width: 320,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function withEncodedQr(value, encodeQr) {
|
|
89
|
+
if (!value || !value.verificationUrl) return value;
|
|
90
|
+
return {
|
|
91
|
+
...value,
|
|
92
|
+
qrCodeDataUrl: await encodeQr(value.verificationUrl),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function publicStatus(status, encodeQr) {
|
|
97
|
+
const safe = structuredClone(status);
|
|
98
|
+
if (safe.provisioning) safe.provisioning = await withEncodedQr(safe.provisioning, encodeQr);
|
|
99
|
+
return safe;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertController(controller) {
|
|
103
|
+
if (!controller
|
|
104
|
+
|| typeof controller.status !== 'function'
|
|
105
|
+
|| typeof controller.startProvisioning !== 'function'
|
|
106
|
+
|| typeof controller.registrationStatus !== 'function'
|
|
107
|
+
|| typeof controller.submitVerification !== 'function'
|
|
108
|
+
|| typeof controller.cancelProvisioning !== 'function'
|
|
109
|
+
|| typeof controller.reconnectBot !== 'function'
|
|
110
|
+
|| typeof controller.deleteBot !== 'function') {
|
|
111
|
+
throw new TypeError('A complete Weixin controller is required');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
|
116
|
+
assertController(controller);
|
|
117
|
+
const qrCache = new Map();
|
|
118
|
+
const cachedEncode = (url) => {
|
|
119
|
+
let encoded = qrCache.get(url);
|
|
120
|
+
if (!encoded) {
|
|
121
|
+
if (qrCache.size >= 16) qrCache.delete(qrCache.keys().next().value);
|
|
122
|
+
encoded = Promise.resolve().then(() => encodeQr(url));
|
|
123
|
+
qrCache.set(url, encoded);
|
|
124
|
+
}
|
|
125
|
+
return encoded;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return async (endpoint, payload, signal) => {
|
|
129
|
+
if (signal?.aborted) return cancelled();
|
|
130
|
+
if (!WEIXIN_RPC_ENDPOINTS.includes(endpoint)) return badRequest('Unknown Weixin endpoint.');
|
|
131
|
+
const invalid = payloadFailure(endpoint, payload);
|
|
132
|
+
if (invalid) return badRequest(invalid);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
let value;
|
|
136
|
+
if (endpoint === WEIXIN_ENDPOINTS.status) {
|
|
137
|
+
value = await publicStatus(await controller.status(), cachedEncode);
|
|
138
|
+
} else if (endpoint === WEIXIN_ENDPOINTS.beginProvisioning) {
|
|
139
|
+
const started = await controller.startProvisioning();
|
|
140
|
+
if (signal?.aborted) {
|
|
141
|
+
await controller.cancelProvisioning(started.attemptId);
|
|
142
|
+
return cancelled();
|
|
143
|
+
}
|
|
144
|
+
value = await withEncodedQr(started, cachedEncode);
|
|
145
|
+
} else if (endpoint === WEIXIN_ENDPOINTS.pollProvisioning) {
|
|
146
|
+
const current = await controller.registrationStatus(payload.attemptId);
|
|
147
|
+
if (!current) return badRequest('The provisioning attempt no longer exists.');
|
|
148
|
+
value = await withEncodedQr(current, cachedEncode);
|
|
149
|
+
} else if (endpoint === WEIXIN_ENDPOINTS.submitVerification) {
|
|
150
|
+
value = await withEncodedQr(
|
|
151
|
+
await controller.submitVerification(payload.attemptId, payload.verifyCode),
|
|
152
|
+
cachedEncode,
|
|
153
|
+
);
|
|
154
|
+
} else if (endpoint === WEIXIN_ENDPOINTS.cancelProvisioning) {
|
|
155
|
+
value = await controller.cancelProvisioning(payload.attemptId);
|
|
156
|
+
if (!value) return badRequest('The provisioning attempt no longer exists.');
|
|
157
|
+
} else if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) {
|
|
158
|
+
value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
|
|
159
|
+
} else {
|
|
160
|
+
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
|
161
|
+
}
|
|
162
|
+
return signal?.aborted ? cancelled() : { ok: true, value };
|
|
163
|
+
} catch {
|
|
164
|
+
return signal?.aborted ? cancelled() : internalFailure();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function installWeixinRpc(ctx, controller, options, authority) {
|
|
170
|
+
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
|
171
|
+
throw new TypeError('DSH Host Connection RPC is required');
|
|
172
|
+
}
|
|
173
|
+
return ctx.connection.rpc.handle(
|
|
174
|
+
WEIXIN_RPC_CHANNEL,
|
|
175
|
+
createWeixinRpcHandler(controller, options),
|
|
176
|
+
{ authority: resolveRpcAuthority(authority) },
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { apply as applyDingtalk } from './channels/dingtalk/index.mjs';
|
|
2
|
+
import { apply as applyDiscord } from './channels/discord/index.mjs';
|
|
3
|
+
import { apply as applyFeishu } from './channels/feishu/index.mjs';
|
|
4
|
+
import { apply as applyQq } from './channels/qq/index.mjs';
|
|
5
|
+
import { apply as applyTelegram } from './channels/telegram/index.mjs';
|
|
6
|
+
import { apply as applyWecom } from './channels/wecom/index.mjs';
|
|
7
|
+
import { apply as applyWeixin } from './channels/weixin/index.mjs';
|
|
8
|
+
|
|
9
|
+
export const name = 'dsh-im-host';
|
|
10
|
+
export const inject = ['connection', 'credentials', 'webServer'];
|
|
11
|
+
|
|
12
|
+
function channelConfig(config, name) {
|
|
13
|
+
const channel = config[name] ?? {};
|
|
14
|
+
return config.rpcAuthority === undefined
|
|
15
|
+
? channel
|
|
16
|
+
: { ...channel, rpcAuthority: config.rpcAuthority };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createImHostPlugin(internals = {}) {
|
|
20
|
+
const startFeishu = internals.applyFeishu ?? applyFeishu;
|
|
21
|
+
const startWeixin = internals.applyWeixin ?? applyWeixin;
|
|
22
|
+
const startDingtalk = internals.applyDingtalk ?? applyDingtalk;
|
|
23
|
+
const startWecom = internals.applyWecom ?? applyWecom;
|
|
24
|
+
const startQq = internals.applyQq ?? applyQq;
|
|
25
|
+
const startTelegram = internals.applyTelegram ?? applyTelegram;
|
|
26
|
+
const startDiscord = internals.applyDiscord ?? applyDiscord;
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
name,
|
|
29
|
+
inject,
|
|
30
|
+
async apply(ctx, config = {}) {
|
|
31
|
+
await startFeishu(ctx, channelConfig(config, 'feishu'));
|
|
32
|
+
await startWeixin(ctx, channelConfig(config, 'weixin'));
|
|
33
|
+
await startDingtalk(ctx, channelConfig(config, 'dingtalk'));
|
|
34
|
+
await startWecom(ctx, channelConfig(config, 'wecom'));
|
|
35
|
+
await startQq(ctx, channelConfig(config, 'qq'));
|
|
36
|
+
await startTelegram(ctx, channelConfig(config, 'telegram'));
|
|
37
|
+
await startDiscord(ctx, channelConfig(config, 'discord'));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function apply(ctx, config = {}) {
|
|
43
|
+
return createImHostPlugin().apply(ctx, config);
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const RPC_AUTHORITIES = new Set(['loopback', 'trusted-host']);
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the browser authority accepted by an IM management RPC channel.
|
|
5
|
+
* The default keeps credential and bot-management operations on loopback.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveRpcAuthority(value) {
|
|
8
|
+
if (value === undefined) return 'loopback';
|
|
9
|
+
if (RPC_AUTHORITIES.has(value)) return value;
|
|
10
|
+
throw new TypeError('dsh-im rpcAuthority must be "loopback" or "trusted-host"');
|
|
11
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { access, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const root = resolve(import.meta.dirname, '..');
|
|
5
|
+
const required = [
|
|
6
|
+
'lib/index.js',
|
|
7
|
+
'lib/client.js',
|
|
8
|
+
'bin/dsh-im.mjs',
|
|
9
|
+
'cordis.patch.yml',
|
|
10
|
+
'README.md',
|
|
11
|
+
'THIRD_PARTY_NOTICES.md',
|
|
12
|
+
'plugin-src/client/channels/dingtalk/index.js',
|
|
13
|
+
'plugin-src/host/channels/feishu/index.mjs',
|
|
14
|
+
'plugin-src/host/channels/weixin/index.mjs',
|
|
15
|
+
'plugin-src/host/channels/dingtalk/index.mjs',
|
|
16
|
+
'plugin-src/host/channels/qq/index.mjs',
|
|
17
|
+
'plugin-src/host/channels/wecom/index.mjs',
|
|
18
|
+
'plugin-src/host/channels/telegram/index.mjs',
|
|
19
|
+
'plugin-src/host/channels/discord/index.mjs',
|
|
20
|
+
'src/channels/feishu/feishu-runtime.mjs',
|
|
21
|
+
'src/channels/weixin/weixin-runtime.mjs',
|
|
22
|
+
'src/channels/dingtalk/dingtalk-runtime.mjs',
|
|
23
|
+
'src/channels/qq/qq-runtime.mjs',
|
|
24
|
+
'src/channels/wecom/wecom-runtime.mjs',
|
|
25
|
+
'src/channels/telegram/telegram-runtime.mjs',
|
|
26
|
+
'src/channels/discord/discord-runtime.mjs',
|
|
27
|
+
];
|
|
28
|
+
await Promise.all(required.map((path) => access(resolve(root, path))));
|
|
29
|
+
|
|
30
|
+
const [client, host, patch, manifestText, lockText, hostSource, clientSource, executable] = await Promise.all([
|
|
31
|
+
readFile(resolve(root, 'lib/client.js'), 'utf8'),
|
|
32
|
+
readFile(resolve(root, 'lib/index.js'), 'utf8'),
|
|
33
|
+
readFile(resolve(root, 'cordis.patch.yml'), 'utf8'),
|
|
34
|
+
readFile(resolve(root, 'package.json'), 'utf8'),
|
|
35
|
+
readFile(resolve(root, 'package-lock.json'), 'utf8'),
|
|
36
|
+
readFile(resolve(root, 'plugin-src/host/index.mjs'), 'utf8'),
|
|
37
|
+
readFile(resolve(root, 'plugin-src/client/index.js'), 'utf8'),
|
|
38
|
+
stat(resolve(root, 'bin/dsh-im.mjs')),
|
|
39
|
+
]);
|
|
40
|
+
const manifest = JSON.parse(manifestText);
|
|
41
|
+
|
|
42
|
+
if (!client.includes('id: "@xmanrui/dsh-im"')) {
|
|
43
|
+
throw new Error('client bundle does not register the dsh-im loader id');
|
|
44
|
+
}
|
|
45
|
+
if (!client.includes('id: "im"') || !client.includes('label: "IM\\u673A\\u5668\\u4EBA"')) {
|
|
46
|
+
throw new Error('client bundle does not register the IM机器人 settings tab');
|
|
47
|
+
}
|
|
48
|
+
if ((client.match(/ctx\.slots\.inject\("settings\.plugins\.tab"/g) ?? []).length !== 1) {
|
|
49
|
+
throw new Error('client bundle must register exactly one settings tab');
|
|
50
|
+
}
|
|
51
|
+
if (/role:\s*["']switch|type:\s*["']checkbox/.test(client)) {
|
|
52
|
+
throw new Error('client bundle contains a channel enable switch');
|
|
53
|
+
}
|
|
54
|
+
if (!client.includes('container-type: inline-size')
|
|
55
|
+
|| !client.includes('@container (max-width: 680px)')) {
|
|
56
|
+
throw new Error('client bundle does not contain the narrow-panel DingTalk QR layout');
|
|
57
|
+
}
|
|
58
|
+
for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/telegram', '/discord']) {
|
|
59
|
+
if (!host.includes(marker)) {
|
|
60
|
+
throw new Error(`host bundle does not contain the internal ${marker} RPC provider`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (/@xmanrui\/dsh-(?:feishu|weixin|dingtalk)/.test(host)) {
|
|
64
|
+
throw new Error('host bundle still imports an external channel plugin');
|
|
65
|
+
}
|
|
66
|
+
if (/@xmanrui\/dsh-(?:feishu|weixin|dingtalk)/.test(
|
|
67
|
+
manifestText + lockText + hostSource + clientSource,
|
|
68
|
+
)) {
|
|
69
|
+
throw new Error('source or package metadata still depends on an external channel plugin');
|
|
70
|
+
}
|
|
71
|
+
if (!patch.includes("name: '@xmanrui/dsh-im'") || /dsh-(?:feishu|weixin|dingtalk)/.test(patch)) {
|
|
72
|
+
throw new Error('bundle patch must activate only dsh-im');
|
|
73
|
+
}
|
|
74
|
+
for (const name of ['@xmanrui/dsh-feishu', '@xmanrui/dsh-weixin', '@xmanrui/dsh-dingtalk']) {
|
|
75
|
+
if (manifest.dependencies?.[name]) {
|
|
76
|
+
throw new Error(`${name} must not remain an external dependency`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const directDependencies = {
|
|
80
|
+
'@larksuiteoapi/node-sdk': '1.73.0',
|
|
81
|
+
'dingtalk-stream': '2.1.4',
|
|
82
|
+
'@tencent-connect/qqbot-connector': '1.2.0',
|
|
83
|
+
'@tencent-connect/qqbot-nodejs': '1.0.4',
|
|
84
|
+
'@wecom/aibot-node-sdk': '1.0.7',
|
|
85
|
+
qrcode: '1.5.4',
|
|
86
|
+
};
|
|
87
|
+
for (const [name, version] of Object.entries(directDependencies)) {
|
|
88
|
+
if (manifest.dependencies?.[name] !== version) {
|
|
89
|
+
throw new Error(`${name} must be a pinned direct dependency at ${version}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if ((executable.mode & 0o111) === 0) throw new Error('dsh-im CLI is not executable');
|
|
93
|
+
if (/private-bot-token|must-be-rolled-back|DEEPSEEK_API_KEY=/.test(client + host)) {
|
|
94
|
+
throw new Error('built artifacts contain a test or environment secret marker');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log('Verified dsh-im package artifacts.');
|