@xmanrui/dsh-im 4.15.0 → 4.16.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/README.en.md +2 -1
- package/README.md +9 -5
- package/lib/client.js +1729 -614
- package/lib/index.js +303 -293
- package/package.json +6 -2
- package/plugin-src/client/access-policy-settings.js +6 -0
- package/plugin-src/client/channels/feishu/index.js +15 -3
- package/plugin-src/client/channels/wecom-app/api.js +124 -0
- package/plugin-src/client/channels/wecom-app/index.js +612 -0
- package/plugin-src/client/channels/wecom-app/styles.js +25 -0
- package/plugin-src/client/delivery-settings.js +7 -0
- package/plugin-src/client/i18n.js +63 -0
- package/plugin-src/client/index.js +18 -0
- package/plugin-src/client/model-setting.js +135 -69
- package/plugin-src/client/styles.js +22 -7
- package/plugin-src/host/channels/dingtalk/index.mjs +8 -15
- package/plugin-src/host/channels/discord/index.mjs +8 -10
- package/plugin-src/host/channels/feishu/index.mjs +8 -15
- package/plugin-src/host/channels/office/index.mjs +9 -5
- package/plugin-src/host/channels/qq/index.mjs +8 -15
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/channels/shared/model-setting-rpc.mjs +1 -2
- package/plugin-src/host/channels/shared/startup-error.mjs +53 -0
- package/plugin-src/host/channels/shared/startup.mjs +47 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/channels/slack/index.mjs +8 -10
- package/plugin-src/host/channels/telegram/index.mjs +8 -10
- package/plugin-src/host/channels/wecom/index.mjs +8 -15
- package/plugin-src/host/channels/wecom/rpc.mjs +6 -1
- package/plugin-src/host/channels/wecom-app/index.mjs +33 -0
- package/plugin-src/host/channels/wecom-app/production.mjs +217 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +225 -0
- package/plugin-src/host/channels/weixin/index.mjs +8 -15
- package/plugin-src/host/channels/whatsapp/index.mjs +8 -15
- package/plugin-src/host/delivery-adapter.mjs +4 -0
- package/plugin-src/host/delivery-suggestions.mjs +4 -0
- package/plugin-src/host/index.mjs +7 -2
- package/scripts/verify-model-setting.mjs +54 -0
- package/src/channels/feishu/bridge.mjs +295 -9
- package/src/channels/feishu/feishu-cards.mjs +38 -0
- package/src/channels/feishu/feishu-channel.mjs +88 -20
- package/src/channels/shared/bot-workspace-store.mjs +11 -3
- package/src/channels/shared/command-catalog.mjs +102 -0
- package/src/channels/shared/i18n-en/feishu.mjs +12 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +9 -0
- package/src/channels/shared/i18n-en/wecom-app.mjs +33 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/model-setting.mjs +43 -8
- package/src/channels/shared/text-harness-bridge.mjs +2 -26
- package/src/channels/telegram/telegram-api.mjs +18 -6
- package/src/channels/telegram/telegram-runtime.mjs +34 -32
- package/src/channels/wecom/send-error.mjs +31 -0
- package/src/channels/wecom/wecom-bridge.mjs +59 -17
- package/src/channels/wecom-app/callback-server.mjs +471 -0
- package/src/channels/wecom-app/config-store.mjs +240 -0
- package/src/channels/wecom-app/harness-client.mjs +11 -0
- package/src/channels/wecom-app/state-store.mjs +107 -0
- package/src/channels/wecom-app/wecom-app-api.mjs +432 -0
- package/src/channels/wecom-app/wecom-app-bridge.mjs +1059 -0
- package/src/channels/wecom-app/wecom-app-controller.mjs +440 -0
- package/src/channels/wecom-app/wecom-app-runtime.mjs +221 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
|
2
|
+
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
|
3
|
+
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
4
|
+
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
5
|
+
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
|
6
|
+
import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
|
|
7
|
+
import {
|
|
8
|
+
connectionTestTargetUnavailable,
|
|
9
|
+
publicConnectionTestResult,
|
|
10
|
+
} from '../../../../src/channels/shared/connection-test.mjs';
|
|
11
|
+
|
|
12
|
+
export const WECOM_APP_RPC_CHANNEL = '/wecom-app';
|
|
13
|
+
export const WECOM_APP_ENDPOINTS = Object.freeze({
|
|
14
|
+
status: 'connection.status',
|
|
15
|
+
bindApp: 'bot.bind',
|
|
16
|
+
updateSettings: 'bot.settings.update',
|
|
17
|
+
resetCallbackSecret: 'bot.callback-secret.reset',
|
|
18
|
+
reconnectBot: 'bot.reconnect',
|
|
19
|
+
deleteBot: 'bot.delete',
|
|
20
|
+
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
|
21
|
+
setModel: SET_MODEL_ENDPOINT,
|
|
22
|
+
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
23
|
+
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
24
|
+
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
25
|
+
});
|
|
26
|
+
export const WECOM_APP_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_APP_ENDPOINTS));
|
|
27
|
+
|
|
28
|
+
const FORBIDDEN_PUBLIC_KEYS = new Set([
|
|
29
|
+
'secret', 'secretRef', 'token', 'encodingAESKey', 'callbackTokenRef', 'callbackKeyRef',
|
|
30
|
+
'callbackSecret', 'corpSecret', 'bot_info',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function exactKeys(value, allowed) {
|
|
38
|
+
return isRecord(value) && Object.keys(value).every((key) => allowed.includes(key));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validId(value) {
|
|
42
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validCredential(value, maxLength) {
|
|
46
|
+
return typeof value === 'string' && value.trim().length > 0 && value.length <= maxLength;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function payloadFailure(endpoint, payload) {
|
|
50
|
+
if (!isRecord(payload)) return 'Payload must be an object.';
|
|
51
|
+
if (endpoint === WECOM_APP_ENDPOINTS.status) return exactKeys(payload, []) ? null : 'connection.status does not accept fields.';
|
|
52
|
+
if (endpoint === WECOM_APP_ENDPOINTS.bindApp) {
|
|
53
|
+
const allowed = ['corpId', 'agentId', 'secret', 'token', 'encodingAESKey', 'apiBaseUrl', 'callbackBaseUrl', 'streamEnabled'];
|
|
54
|
+
if (!exactKeys(payload, allowed)) return 'bot.bind received unsupported fields.';
|
|
55
|
+
if (!validCredential(payload.corpId, 128) || !validCredential(payload.agentId, 32)
|
|
56
|
+
|| !validCredential(payload.secret, 256) || !validCredential(payload.token, 128)
|
|
57
|
+
|| !validCredential(payload.encodingAESKey, 128)) {
|
|
58
|
+
return 'bot.bind requires corpId, agentId, secret, token, and encodingAESKey.';
|
|
59
|
+
}
|
|
60
|
+
if (payload.apiBaseUrl !== undefined && payload.apiBaseUrl !== null && typeof payload.apiBaseUrl !== 'string') {
|
|
61
|
+
return 'bot.bind received an invalid apiBaseUrl.';
|
|
62
|
+
}
|
|
63
|
+
if (payload.callbackBaseUrl !== undefined && payload.callbackBaseUrl !== null && typeof payload.callbackBaseUrl !== 'string') {
|
|
64
|
+
return 'bot.bind received an invalid callbackBaseUrl.';
|
|
65
|
+
}
|
|
66
|
+
if (payload.streamEnabled !== undefined && payload.streamEnabled !== true && payload.streamEnabled !== false) {
|
|
67
|
+
return 'bot.bind received an invalid streamEnabled flag.';
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (endpoint === WECOM_APP_ENDPOINTS.updateSettings) {
|
|
72
|
+
if (!exactKeys(payload, ['botId', 'apiBaseUrl', 'callbackBaseUrl', 'streamEnabled']) || !validId(payload.botId)) {
|
|
73
|
+
return 'bot.settings.update requires a botId and settings fields.';
|
|
74
|
+
}
|
|
75
|
+
if (payload.apiBaseUrl !== undefined && payload.apiBaseUrl !== null && typeof payload.apiBaseUrl !== 'string') {
|
|
76
|
+
return 'bot.settings.update received an invalid apiBaseUrl.';
|
|
77
|
+
}
|
|
78
|
+
if (payload.callbackBaseUrl !== undefined && payload.callbackBaseUrl !== null && typeof payload.callbackBaseUrl !== 'string') {
|
|
79
|
+
return 'bot.settings.update received an invalid callbackBaseUrl.';
|
|
80
|
+
}
|
|
81
|
+
if (payload.streamEnabled !== undefined && payload.streamEnabled !== true && payload.streamEnabled !== false) {
|
|
82
|
+
return 'bot.settings.update received an invalid streamEnabled flag.';
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (endpoint === WECOM_APP_ENDPOINTS.resetCallbackSecret) {
|
|
87
|
+
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
88
|
+
? null : 'bot.callback-secret.reset requires a botId.';
|
|
89
|
+
}
|
|
90
|
+
if (endpoint === WECOM_APP_ENDPOINTS.reconnectBot) {
|
|
91
|
+
return exactKeys(payload, ['botId', 'sendTest'])
|
|
92
|
+
&& validId(payload.botId)
|
|
93
|
+
&& (payload.sendTest === undefined || payload.sendTest === true)
|
|
94
|
+
? null : 'bot.reconnect requires a botId and optional sendTest=true.';
|
|
95
|
+
}
|
|
96
|
+
if (endpoint === WECOM_APP_ENDPOINTS.deleteBot) {
|
|
97
|
+
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
98
|
+
? null : 'bot.delete requires a botId and confirm=true.';
|
|
99
|
+
}
|
|
100
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setWorkspace) {
|
|
101
|
+
return validWorkspacePayload(payload) ? null : '请输入工作区绝对路径。';
|
|
102
|
+
}
|
|
103
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setModel) {
|
|
104
|
+
return validModelPayload(payload) ? null : '请选择有效模型。';
|
|
105
|
+
}
|
|
106
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setAgentPreset) {
|
|
107
|
+
return validAgentPresetPayload(payload) ? null : '请选择 Agent Preset。';
|
|
108
|
+
}
|
|
109
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setContextEnhancement) {
|
|
110
|
+
return validContextEnhancementPayload(payload) ? null : '请提交有效的上下文增强设置。';
|
|
111
|
+
}
|
|
112
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setAccessPolicy) {
|
|
113
|
+
return validAccessPolicyPayload(payload) ? null : '请提交有效的访问设置。';
|
|
114
|
+
}
|
|
115
|
+
return 'Unknown Enterprise WeChat app endpoint.';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sanitizePublic(value) {
|
|
119
|
+
if (Array.isArray(value)) return value.map(sanitizePublic);
|
|
120
|
+
if (!isRecord(value)) return value;
|
|
121
|
+
const safe = {};
|
|
122
|
+
for (const [key, child] of Object.entries(value)) {
|
|
123
|
+
if (!FORBIDDEN_PUBLIC_KEYS.has(key)) safe[key] = sanitizePublic(child);
|
|
124
|
+
}
|
|
125
|
+
return safe;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function publicStatus(status) {
|
|
129
|
+
return sanitizePublic(structuredClone(status));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createWecomAppRpcHandler(controller) {
|
|
133
|
+
for (const method of ['status', 'bindApp', 'updateAppSettings', 'resetCallbackSecret', 'reconnectBot', 'deleteBot']) {
|
|
134
|
+
if (typeof controller?.[method] !== 'function') {
|
|
135
|
+
throw new TypeError(`A complete Enterprise WeChat app controller is required (${method})`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return async (endpoint, payload, signal) => {
|
|
139
|
+
if (signal?.aborted) return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
|
|
140
|
+
if (!WECOM_APP_RPC_ENDPOINTS.includes(endpoint)) {
|
|
141
|
+
return { ok: false, error: { code: 'bad-request', message: 'Unknown Enterprise WeChat app endpoint.', details: {} } };
|
|
142
|
+
}
|
|
143
|
+
const invalid = payloadFailure(endpoint, payload);
|
|
144
|
+
if (invalid) return { ok: false, error: { code: 'bad-request', message: invalid } };
|
|
145
|
+
try {
|
|
146
|
+
let value;
|
|
147
|
+
if (endpoint === WECOM_APP_ENDPOINTS.status) value = await publicStatus(await controller.status());
|
|
148
|
+
else if (endpoint === WECOM_APP_ENDPOINTS.bindApp) {
|
|
149
|
+
value = await publicStatus(await controller.bindApp(payload));
|
|
150
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.updateSettings) {
|
|
151
|
+
value = await publicStatus(await controller.updateAppSettings(payload.botId, {
|
|
152
|
+
apiBaseUrl: payload.apiBaseUrl,
|
|
153
|
+
callbackBaseUrl: payload.callbackBaseUrl,
|
|
154
|
+
streamEnabled: payload.streamEnabled,
|
|
155
|
+
}));
|
|
156
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.resetCallbackSecret) {
|
|
157
|
+
value = await publicStatus(await controller.resetCallbackSecret(payload.botId));
|
|
158
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.reconnectBot) {
|
|
159
|
+
const snapshot = await controller.reconnectBot(payload.botId);
|
|
160
|
+
if (signal?.aborted) {
|
|
161
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
|
|
162
|
+
}
|
|
163
|
+
let testMessage;
|
|
164
|
+
if (payload.sendTest === true) {
|
|
165
|
+
const connected = snapshot?.bots?.some(
|
|
166
|
+
(bot) => bot?.botId === payload.botId && bot?.connected === true,
|
|
167
|
+
);
|
|
168
|
+
if (!connected || typeof controller.sendConnectionTest !== 'function') {
|
|
169
|
+
testMessage = publicConnectionTestResult(connectionTestTargetUnavailable('企业微信应用'));
|
|
170
|
+
} else {
|
|
171
|
+
try {
|
|
172
|
+
await controller.sendConnectionTest(payload.botId);
|
|
173
|
+
testMessage = publicConnectionTestResult();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
testMessage = publicConnectionTestResult(error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
value = await publicStatus({ ...snapshot, ...(testMessage ? { testMessage } : {}) });
|
|
180
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setWorkspace) {
|
|
181
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
182
|
+
value = await publicStatus(await controller.updateWorkspace(payload.botId, payload.workspace));
|
|
183
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setModel) {
|
|
184
|
+
if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
|
|
185
|
+
value = await publicStatus(await controller.updateModel(payload.botId, payload.model));
|
|
186
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setContextEnhancement) {
|
|
187
|
+
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
|
|
188
|
+
value = await controller.updateContextEnhancement(
|
|
189
|
+
payload.botId, payload.config, (status) => publicStatus(status),
|
|
190
|
+
);
|
|
191
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setAccessPolicy) {
|
|
192
|
+
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
193
|
+
value = await controller.updateAccessPolicy(
|
|
194
|
+
payload.botId, payload.policy, (status) => publicStatus(status),
|
|
195
|
+
);
|
|
196
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setAgentPreset) {
|
|
197
|
+
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent Preset update is unavailable');
|
|
198
|
+
value = await publicStatus(await controller.updateAgentPreset(payload.botId, payload.agentPreset));
|
|
199
|
+
} else {
|
|
200
|
+
value = await publicStatus(await controller.deleteBot(payload.botId));
|
|
201
|
+
}
|
|
202
|
+
return signal?.aborted
|
|
203
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } }
|
|
204
|
+
: { ok: true, value };
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const workspaceError = publicWorkspaceError(error);
|
|
207
|
+
return signal?.aborted
|
|
208
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } }
|
|
209
|
+
: { ok: false, error: workspaceError
|
|
210
|
+
? { ...workspaceError, details: {} }
|
|
211
|
+
: { code: 'wecom-app-operation-failed', message: '企业微信应用操作失败,请稍后重试。', details: {} } };
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function installWecomAppRpc(ctx, controller, options, authority) {
|
|
217
|
+
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
|
218
|
+
throw new TypeError('DSH Host Connection RPC is required');
|
|
219
|
+
}
|
|
220
|
+
return ctx.connection.rpc.handle(
|
|
221
|
+
WECOM_APP_RPC_CHANNEL,
|
|
222
|
+
createWecomAppRpcHandler(controller, options),
|
|
223
|
+
{ authority: resolveRpcAuthority(authority) },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installWeixinRpc } from './rpc.mjs';
|
|
2
|
+
import { createWeixinRpcHandler, installWeixinRpc, WEIXIN_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-weixin-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
@@ -9,20 +10,12 @@ export async function apply(ctx, config = {}) {
|
|
|
9
10
|
return installWeixinRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
config.rpcOptions,
|
|
19
|
-
config.rpcAuthority,
|
|
20
|
-
);
|
|
21
|
-
ctx.effect(() => async () => {
|
|
22
|
-
await unregisterDelivery?.();
|
|
23
|
-
await production.close();
|
|
24
|
-
}, 'dsh-weixin: close account connections');
|
|
25
|
-
return disposeRpc;
|
|
13
|
+
return installProductionChannel(ctx, config, {
|
|
14
|
+
channel: 'weixin',
|
|
15
|
+
rpcChannel: WEIXIN_RPC_CHANNEL,
|
|
16
|
+
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
17
|
+
createHandler: controller => createWeixinRpcHandler(controller, config.rpcOptions),
|
|
18
|
+
});
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
export function createWeixinHostPlugin(config) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installWhatsappRpc } from './rpc.mjs';
|
|
2
|
+
import { createWhatsappRpcHandler, installWhatsappRpc, WHATSAPP_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-whatsapp-host';
|
|
5
6
|
export const inject = ['connection', 'typertGateway'];
|
|
@@ -8,20 +9,12 @@ export async function apply(ctx, config = {}) {
|
|
|
8
9
|
if (config?.controller) {
|
|
9
10
|
return installWhatsappRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
config.rpcOptions,
|
|
18
|
-
config.rpcAuthority,
|
|
19
|
-
);
|
|
20
|
-
ctx.effect(() => async () => {
|
|
21
|
-
await unregisterDelivery?.();
|
|
22
|
-
await production.close();
|
|
23
|
-
}, 'dsh-im: close WhatsApp Web connections');
|
|
24
|
-
return disposeRpc;
|
|
12
|
+
return installProductionChannel(ctx, config, {
|
|
13
|
+
channel: 'whatsapp',
|
|
14
|
+
rpcChannel: WHATSAPP_RPC_CHANNEL,
|
|
15
|
+
createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
|
|
16
|
+
createHandler: controller => createWhatsappRpcHandler(controller, config.rpcOptions),
|
|
17
|
+
});
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
export function createWhatsappHostPlugin(config) {
|
|
@@ -9,6 +9,7 @@ const CHANNELS = new Set([
|
|
|
9
9
|
'feishu',
|
|
10
10
|
'dingtalk',
|
|
11
11
|
'wecom',
|
|
12
|
+
'wecom-app',
|
|
12
13
|
'qq',
|
|
13
14
|
'slack',
|
|
14
15
|
'telegram',
|
|
@@ -82,6 +83,9 @@ function normalizeRoute(channel, kind, route) {
|
|
|
82
83
|
case 'wecom':
|
|
83
84
|
oneOf(kind, ['user', 'group']);
|
|
84
85
|
return routeWithStrings(route, ['chatId']);
|
|
86
|
+
case 'wecom-app':
|
|
87
|
+
oneOf(kind, ['user']);
|
|
88
|
+
return routeWithStrings(route, ['chatId']);
|
|
85
89
|
case 'qq':
|
|
86
90
|
oneOf(kind, ['user', 'group']);
|
|
87
91
|
return routeWithStrings(route, kind === 'user' ? ['userOpenId'] : ['groupOpenId']);
|
|
@@ -3,6 +3,7 @@ const CHANNELS = new Set([
|
|
|
3
3
|
'feishu',
|
|
4
4
|
'dingtalk',
|
|
5
5
|
'wecom',
|
|
6
|
+
'wecom-app',
|
|
6
7
|
'qq',
|
|
7
8
|
'slack',
|
|
8
9
|
'telegram',
|
|
@@ -97,6 +98,8 @@ export function deliverySuggestionFromConversationKey(channel, key) {
|
|
|
97
98
|
['direct', 'user', 'chatId'],
|
|
98
99
|
['group', 'group', 'chatId'],
|
|
99
100
|
]);
|
|
101
|
+
case 'wecom-app':
|
|
102
|
+
return simpleSuggestion(key, [['p2p', 'user', 'chatId']]);
|
|
100
103
|
case 'qq':
|
|
101
104
|
return simpleSuggestion(key, [
|
|
102
105
|
['c2c', 'user', 'userOpenId'],
|
|
@@ -124,6 +127,7 @@ export function privateDeliverySuggestionFromConversationKey(channel, key) {
|
|
|
124
127
|
feishu: 'p2p',
|
|
125
128
|
dingtalk: 'p2p',
|
|
126
129
|
wecom: 'direct',
|
|
130
|
+
'wecom-app': 'p2p',
|
|
127
131
|
qq: 'c2c',
|
|
128
132
|
slack: 'direct',
|
|
129
133
|
telegram: 'direct',
|
|
@@ -6,6 +6,7 @@ import { apply as applyQq } from './channels/qq/index.mjs';
|
|
|
6
6
|
import { apply as applySlack } from './channels/slack/index.mjs';
|
|
7
7
|
import { apply as applyTelegram } from './channels/telegram/index.mjs';
|
|
8
8
|
import { apply as applyWecom } from './channels/wecom/index.mjs';
|
|
9
|
+
import { apply as applyWecomApp } from './channels/wecom-app/index.mjs';
|
|
9
10
|
import { apply as applyWeixin } from './channels/weixin/index.mjs';
|
|
10
11
|
import { apply as applyWhatsapp } from './channels/whatsapp/index.mjs';
|
|
11
12
|
import { installOutboundArtifactTool } from '../../src/channels/shared/semantic/artifact.mjs';
|
|
@@ -45,6 +46,7 @@ export function createImHostPlugin(internals = {}) {
|
|
|
45
46
|
const startWeixin = internals.applyWeixin ?? applyWeixin;
|
|
46
47
|
const startDingtalk = internals.applyDingtalk ?? applyDingtalk;
|
|
47
48
|
const startWecom = internals.applyWecom ?? applyWecom;
|
|
49
|
+
const startWecomApp = internals.applyWecomApp ?? applyWecomApp;
|
|
48
50
|
const startQq = internals.applyQq ?? applyQq;
|
|
49
51
|
const startSlack = internals.applySlack ?? applySlack;
|
|
50
52
|
const startTelegram = internals.applyTelegram ?? applyTelegram;
|
|
@@ -56,6 +58,7 @@ export function createImHostPlugin(internals = {}) {
|
|
|
56
58
|
['weixin', startWeixin],
|
|
57
59
|
['dingtalk', startDingtalk],
|
|
58
60
|
['wecom', startWecom],
|
|
61
|
+
['wecomApp', startWecomApp],
|
|
59
62
|
['qq', startQq],
|
|
60
63
|
['slack', startSlack],
|
|
61
64
|
['telegram', startTelegram],
|
|
@@ -145,14 +148,16 @@ export function createImHostPlugin(internals = {}) {
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
const failures = [];
|
|
148
|
-
|
|
151
|
+
// Each channel mounts its management RPC before awaiting initialization.
|
|
152
|
+
// Start them together so a slow channel cannot leave later routes absent.
|
|
153
|
+
await Promise.all(channels.map(async ([channel, start]) => {
|
|
149
154
|
try {
|
|
150
155
|
await start(ctx, channelConfig(config, channel, deliveryService));
|
|
151
156
|
} catch (error) {
|
|
152
157
|
failures.push(error);
|
|
153
158
|
logger.error?.(`[dsh-im] failed to activate ${channel}; continuing with the remaining channels`, error);
|
|
154
159
|
}
|
|
155
|
-
}
|
|
160
|
+
}));
|
|
156
161
|
if (failures.length === channels.length) {
|
|
157
162
|
throw new AggregateError(failures, 'dsh-im failed to activate every channel');
|
|
158
163
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Exercise the actual settings card in Chromium without a running Host or IM account.
|
|
2
|
+
// Usage: node scripts/verify-model-setting.mjs [output-directory]
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { build } from 'esbuild';
|
|
11
|
+
|
|
12
|
+
const browser = process.env.CHROME_PATH ?? [
|
|
13
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
14
|
+
'/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome',
|
|
15
|
+
].find(existsSync);
|
|
16
|
+
if (!browser) throw new Error('Set CHROME_PATH to a Chromium/Chrome executable.');
|
|
17
|
+
const output = process.argv[2] ? resolve(process.argv[2]) : await mkdtemp(join(tmpdir(), 'dsh-im-model-ui-'));
|
|
18
|
+
await mkdir(output, { recursive: true });
|
|
19
|
+
const built = await build({
|
|
20
|
+
entryPoints: [resolve(import.meta.dirname, '../test/browser/model-setting.fixture.js')],
|
|
21
|
+
bundle: true, write: false, format: 'iife', platform: 'browser', target: 'chrome100',
|
|
22
|
+
define: { 'process.env.NODE_ENV': '"development"' },
|
|
23
|
+
});
|
|
24
|
+
const htmlPath = join(output, 'preview.html');
|
|
25
|
+
await writeFile(htmlPath, `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>DSH-IM Model Settings</title>
|
|
26
|
+
<style>
|
|
27
|
+
body{margin:0;padding:24px;background:#fff;color:#1f2329;font:14px/20px -apple-system,BlinkMacSystemFont,sans-serif}#app{max-width:760px;margin:auto}#result{max-width:760px;margin:18px auto;font:11px/17px monospace;white-space:pre-wrap;color:#687380}
|
|
28
|
+
@media(max-width:600px){body{padding:12px}}
|
|
29
|
+
@media(prefers-color-scheme:dark){:root{color-scheme:dark;--dsw-alias-bg-layer-1:#202125;--dsw-alias-bg-layer-3:#282a30;--dsw-specific-menu:#282a30;--dsw-alias-bg-module-platform:#25272c;--dsw-alias-label-primary:#e9ebef;--dsw-alias-label-secondary:#b1b5bf;--dsw-alias-label-tertiary:#9a9faa;--dsw-alias-border-l1:#34363c;--dsw-alias-border-l2:#41434c;--dsw-alias-border-l3:#7b9ff9;--dsw-alias-interactive-bg-hover:#353842}body{background:#202125;color:#e9ebef}}
|
|
30
|
+
</style><body><div id="app"></div><pre id="result">Running browser checks…</pre><script>${built.outputFiles[0].text.replace(/<\/script/giu, '<\\/script')}</script></body></html>`);
|
|
31
|
+
for (const [name, size, extra, query] of [
|
|
32
|
+
['desktop', '1100,1100', [], ''],
|
|
33
|
+
['mobile', '390,1250', [], '?mobile'],
|
|
34
|
+
['dark-en', '1100,1100', ['--force-dark-mode'], '?en'],
|
|
35
|
+
]) {
|
|
36
|
+
const profile = await mkdtemp(join(tmpdir(), 'dsh-im-model-browser-'));
|
|
37
|
+
const { stdout, stderr } = await promisify(execFile)(browser, [
|
|
38
|
+
'--headless', '--disable-gpu', '--no-sandbox', '--hide-scrollbars',
|
|
39
|
+
`--user-data-dir=${profile}`, '--no-first-run', '--no-default-browser-check',
|
|
40
|
+
`--window-size=${size}`, '--force-device-scale-factor=1', '--virtual-time-budget=8000',
|
|
41
|
+
...extra, `--screenshot=${join(output, `${name}.png`)}`, '--dump-dom', pathToFileURL(htmlPath).href + query,
|
|
42
|
+
], { maxBuffer: 8 * 1024 * 1024, timeout: 15_000, killSignal: 'SIGKILL' }).catch((error) => {
|
|
43
|
+
// Some desktop Chromium builds finish dump-dom and the screenshot but
|
|
44
|
+
// hang during shutdown. Only accept a fully returned fixture result.
|
|
45
|
+
if (error.killed && /<body\b[^>]*\bdata-result="(?:passed|failed)"/u.test(error.stdout ?? '')) return error;
|
|
46
|
+
throw error;
|
|
47
|
+
}).finally(() => rm(profile, { recursive: true, force: true }));
|
|
48
|
+
await writeFile(join(output, `${name}.html`), stdout);
|
|
49
|
+
await writeFile(join(output, `${name}.log`), stderr);
|
|
50
|
+
const result = stdout.match(/<pre id="result">([\s\S]*?)<\/pre>/u)?.[1] ?? 'No browser result';
|
|
51
|
+
if (!stdout.includes('data-result="passed"')) throw new Error(`${name}: ${result}`);
|
|
52
|
+
console.log(`${name}: ${result}`);
|
|
53
|
+
}
|
|
54
|
+
console.log(`Previews: ${output}`);
|