@xmanrui/dsh-im 4.15.0 → 4.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -1
- package/README.md +9 -5
- package/lib/client.js +1731 -612
- package/lib/index.js +303 -293
- package/package.json +10 -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/dingtalk/dingtalk-api.mjs +41 -48
- package/src/channels/dingtalk/dingtalk-bridge.mjs +5 -1
- 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}`);
|
|
@@ -337,10 +337,13 @@ function cardMarkdown(text, target) {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
function cardData(text, flowStatus, target) {
|
|
340
|
+
const markdown = cardMarkdown(text, target);
|
|
341
|
+
// The shared template uses msgContent for processing, finished, and
|
|
342
|
+
// failed cards. Switching its order to staticMsgContent hides the reply.
|
|
340
343
|
return {
|
|
341
344
|
cardParamMap: {
|
|
342
345
|
flowStatus,
|
|
343
|
-
msgContent:
|
|
346
|
+
msgContent: markdown,
|
|
344
347
|
staticMsgContent: '',
|
|
345
348
|
sys_full_json_obj: JSON.stringify({ order: ['msgContent'] }),
|
|
346
349
|
config: JSON.stringify({ autoLayout: true }),
|
|
@@ -922,13 +925,17 @@ export function createDingtalkApi({
|
|
|
922
925
|
|
|
923
926
|
let delivered = false;
|
|
924
927
|
try {
|
|
925
|
-
|
|
928
|
+
// Deliver the thinking copy in the same request that first shows the
|
|
929
|
+
// card. Creating an empty instance and filling it after deliver (the
|
|
930
|
+
// previous three-request sequence) leaves a blank bubble visible in
|
|
931
|
+
// DingTalk for the time between deliver and the follow-up PUT, and
|
|
932
|
+
// leaves a permanently blank card if that PUT ever fails.
|
|
933
|
+
await cardRequest('v1.0/card/instances/createAndDeliver', {
|
|
926
934
|
body: {
|
|
935
|
+
...cardDeliverBody(cardInstanceId, normalizedTarget, appKey),
|
|
927
936
|
cardTemplateId: DINGTALK_AI_CARD_TEMPLATE_ID,
|
|
928
937
|
outTrackId: cardInstanceId,
|
|
929
|
-
cardData:
|
|
930
|
-
cardParamMap: { config: JSON.stringify({ autoLayout: true }) },
|
|
931
|
-
},
|
|
938
|
+
cardData: cardData(content, '2', normalizedTarget),
|
|
932
939
|
callbackType: 'STREAM',
|
|
933
940
|
cardAtUserIds: normalizedTarget.atUserIds
|
|
934
941
|
? Object.keys(normalizedTarget.atUserIds)
|
|
@@ -938,37 +945,31 @@ export function createDingtalkApi({
|
|
|
938
945
|
},
|
|
939
946
|
headers,
|
|
940
947
|
signal,
|
|
941
|
-
action: 'AI Card
|
|
942
|
-
});
|
|
943
|
-
await cardRequest('v1.0/card/instances/deliver', {
|
|
944
|
-
body: cardDeliverBody(cardInstanceId, normalizedTarget, appKey),
|
|
945
|
-
headers,
|
|
946
|
-
signal,
|
|
947
|
-
action: 'AI Card 投放',
|
|
948
|
+
action: 'AI Card 创建并投放',
|
|
948
949
|
});
|
|
949
950
|
delivered = true;
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
951
|
+
try {
|
|
952
|
+
await cardRequest('v1.0/card/streaming', {
|
|
953
|
+
method: 'PUT',
|
|
954
|
+
body: {
|
|
955
|
+
outTrackId: cardInstanceId,
|
|
956
|
+
guid: randomUUID(),
|
|
957
|
+
key: 'msgContent',
|
|
958
|
+
content: cardMarkdown(content, normalizedTarget).replace(/\n+$/, ''),
|
|
959
|
+
isFull: true,
|
|
960
|
+
isFinalize: false,
|
|
961
|
+
isError: false,
|
|
962
|
+
},
|
|
963
|
+
headers,
|
|
964
|
+
signal,
|
|
965
|
+
action: 'AI Card 启动',
|
|
966
|
+
});
|
|
967
|
+
} catch (error) {
|
|
968
|
+
if (signal?.aborted) throw error;
|
|
969
|
+
// The card is already visible with the thinking copy from
|
|
970
|
+
// createAndDeliver above. Keep the instance so finishAiCard can
|
|
971
|
+
// still replace it in place instead of sending a second message.
|
|
972
|
+
}
|
|
972
973
|
} catch (error) {
|
|
973
974
|
if (delivered) {
|
|
974
975
|
const cleanupSignal = AbortSignal.timeout(5_000);
|
|
@@ -1018,6 +1019,8 @@ export function createDingtalkApi({
|
|
|
1018
1019
|
const token = await accessToken({ clientId, clientSecret, signal });
|
|
1019
1020
|
const headers = { 'x-acs-dingtalk-access-token': token };
|
|
1020
1021
|
const normalizedContent = cardMarkdown(content, target);
|
|
1022
|
+
// Close the streaming widget before persisting the template's
|
|
1023
|
+
// finished state and full answer in msgContent.
|
|
1021
1024
|
await cardRequest('v1.0/card/streaming', {
|
|
1022
1025
|
method: 'PUT',
|
|
1023
1026
|
body: {
|
|
@@ -1033,8 +1036,7 @@ export function createDingtalkApi({
|
|
|
1033
1036
|
signal,
|
|
1034
1037
|
action: 'AI Card 完成',
|
|
1035
1038
|
});
|
|
1036
|
-
|
|
1037
|
-
const completionRequest = {
|
|
1039
|
+
await cardRequest('v1.0/card/instances', {
|
|
1038
1040
|
method: 'PUT',
|
|
1039
1041
|
body: {
|
|
1040
1042
|
outTrackId: instanceId,
|
|
@@ -1043,18 +1045,9 @@ export function createDingtalkApi({
|
|
|
1043
1045
|
},
|
|
1044
1046
|
headers,
|
|
1045
1047
|
signal,
|
|
1046
|
-
action: 'AI Card
|
|
1047
|
-
};
|
|
1048
|
-
|
|
1049
|
-
await cardRequest('v1.0/card/instances', completionRequest);
|
|
1050
|
-
} catch {
|
|
1051
|
-
try {
|
|
1052
|
-
await cardRequest('v1.0/card/instances', completionRequest);
|
|
1053
|
-
} catch {
|
|
1054
|
-
completed = false;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
return { delivered: true, completed };
|
|
1048
|
+
action: 'AI Card 完成状态',
|
|
1049
|
+
});
|
|
1050
|
+
return { delivered: true, completed: true };
|
|
1058
1051
|
},
|
|
1059
1052
|
|
|
1060
1053
|
failAiCard: failCard,
|
|
@@ -1336,6 +1336,8 @@ export class DingtalkHarnessBridge {
|
|
|
1336
1336
|
providerMessageIds: cardStream.providerMessageIds,
|
|
1337
1337
|
});
|
|
1338
1338
|
} else {
|
|
1339
|
+
// Failed streams close the card with a notice pointing to a
|
|
1340
|
+
// follow-up message. Deliver the answer through that fallback.
|
|
1339
1341
|
textReceipt = createDeliveryReceipt({
|
|
1340
1342
|
deliveryId: messageId,
|
|
1341
1343
|
presentation: 'dingtalk-text',
|
|
@@ -1411,7 +1413,9 @@ export class DingtalkHarnessBridge {
|
|
|
1411
1413
|
? `${errorText}\n\n${batchFailureMessage}`
|
|
1412
1414
|
: errorText;
|
|
1413
1415
|
const streamed = cardStarted && await cardStream.finish(visibleError);
|
|
1414
|
-
if (!streamed)
|
|
1416
|
+
if (!streamed) {
|
|
1417
|
+
await this.#send(sessionWebhook, visibleError, this.#atUsersFor(message));
|
|
1418
|
+
}
|
|
1415
1419
|
} catch {
|
|
1416
1420
|
this.#logger.error?.('[dsh-dingtalk] failed to send the safe error reply');
|
|
1417
1421
|
}
|