@xmanrui/dsh-im 4.35.1 → 4.36.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/PROACTIVE_DELIVERY.en.md +50 -0
- package/PROACTIVE_DELIVERY.md +22 -0
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/lib/client.js +30 -8
- package/lib/index.js +309 -309
- package/package.json +3 -3
- package/plugin-src/client/connection-error.js +2 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +4 -2
- package/plugin-src/host/channels/matrix/production.mjs +1 -1
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/shared/production.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +1 -1
- package/plugin-src/host/channels/wecom-app/production.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -1
- package/plugin-src/host/delivery-adapter.mjs +60 -12
- package/plugin-src/host/delivery-service.mjs +127 -1
- package/plugin-src/host/delivery-suggestions.mjs +2 -1
- package/plugin-src/host/harness-connection.mjs +3 -7
- package/plugin-src/host/index.mjs +5 -2
- package/plugin-src/host/modern-harness-api.mjs +139 -4
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +14 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +10 -0
- package/src/channels/discord/discord-runtime.mjs +13 -0
- package/src/channels/feishu/bridge.mjs +30 -13
- package/src/channels/feishu/external-consumer.mjs +118 -0
- package/src/channels/feishu/feishu-runtime.mjs +72 -3
- package/src/channels/feishu/history-reader.mjs +92 -0
- package/src/channels/feishu/multi-bot-controller.mjs +130 -2
- package/src/channels/feishu/nearby-history.mjs +72 -0
- package/src/channels/feishu/plugin-config-store.mjs +1 -0
- package/src/channels/matrix/matrix-controller.mjs +9 -0
- package/src/channels/matrix/matrix-runtime.mjs +14 -0
- package/src/channels/qq/qq-bridge.mjs +6 -0
- package/src/channels/qq/qq-controller.mjs +14 -0
- package/src/channels/qq/qq-runtime.mjs +10 -0
- package/src/channels/shared/connection-error.mjs +1 -0
- package/src/channels/shared/diagnostic-details.mjs +7 -1
- package/src/channels/shared/harness-approval.mjs +84 -7
- package/src/channels/shared/harness-client.mjs +51 -40
- package/src/channels/shared/i18n-en/diagnostics.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +3 -0
- package/src/channels/shared/message-failure.mjs +1 -1
- package/src/channels/shared/text-harness-bridge.mjs +6 -0
- package/src/channels/shared/token-bot-controller.mjs +16 -0
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +7 -0
- package/src/channels/slack/slack-controller.mjs +14 -0
- package/src/channels/slack/slack-runtime.mjs +12 -0
- package/src/channels/telegram/telegram-runtime.mjs +10 -0
- package/src/channels/wecom/wecom-bridge.mjs +6 -0
- package/src/channels/wecom/wecom-controller.mjs +14 -0
- package/src/channels/wecom/wecom-runtime.mjs +10 -0
- package/src/channels/wecom-app/wecom-app-bridge.mjs +6 -0
- package/src/channels/wecom-app/wecom-app-controller.mjs +14 -0
- package/src/channels/wecom-app/wecom-app-runtime.mjs +10 -0
- package/src/channels/weixin/weixin-bridge.mjs +6 -0
- package/src/channels/weixin/weixin-controller.mjs +14 -0
- package/src/channels/weixin/weixin-runtime.mjs +10 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +14 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.36.1",
|
|
4
4
|
"icon": "./assets/logo-plugin-message-link-gradient.webp",
|
|
5
5
|
"description": "把微信、飞书、企微、钉钉、QQ、Telegram、WhatsApp、Slack、Discord、Matrix接入DSH。 Connect Telegram, WhatsApp, Slack, Discord, Matrix, WeChat, Feishu, WeCom, DingTalk, and QQ to DSH.",
|
|
6
6
|
"keywords": [
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
"platform": "web"
|
|
88
88
|
},
|
|
89
89
|
"compatibility": {
|
|
90
|
-
"dsh": "0.
|
|
90
|
+
"dsh": "0.2.0-rc.2",
|
|
91
91
|
"dshReleases": {
|
|
92
|
-
"0.
|
|
92
|
+
"0.2.0-rc.2": "compatible"
|
|
93
93
|
},
|
|
94
94
|
"profiles": [
|
|
95
95
|
"web"
|
|
@@ -30,7 +30,7 @@ export function formatConnectionDiagnostic(value) {
|
|
|
30
30
|
return [
|
|
31
31
|
localizeText(error.message), details.hint ? localizeText(details.hint) : null,
|
|
32
32
|
localizeText('错误码') + ': ' + error.code,
|
|
33
|
-
...['channel', 'operation', 'stage', 'reason', 'reasons', 'host', 'durationMs', 'timeoutMs', 'nodeVersion', 'dshVersion', 'platform', 'dependencies', 'proxyConfigured', 'truncated', 'httpStatus', 'providerCode', 'resource', 'file', 'field', 'issue', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
|
|
33
|
+
...['channel', 'operation', 'stage', 'method', 'transport', 'reason', 'reasons', 'host', 'durationMs', 'timeoutMs', 'nodeVersion', 'dshVersion', 'platform', 'dependencies', 'proxyConfigured', 'truncated', 'httpStatus', 'providerCode', 'resource', 'file', 'field', 'issue', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
|
|
34
34
|
.filter(field => details[field] !== undefined)
|
|
35
35
|
.map(field => `${field}: ${typeof details[field] === 'object' ? JSON.stringify(details[field]) : details[field]}`),
|
|
36
36
|
].filter(Boolean).join('\n');
|
|
@@ -51,6 +51,7 @@ export function ConnectionError({ error: value, warning = false, showMessage = t
|
|
|
51
51
|
};
|
|
52
52
|
const fields = [
|
|
53
53
|
['错误码', error.code], ['失败阶段', localizeText(STAGE_LABELS[details.stage] ?? details.stage ?? '')],
|
|
54
|
+
['RPC 方法', details.method], ['调用方式', details.transport === 'host-api' ? localizeText('宿主 API(进程内)') : details.transport === 'http' ? 'HTTP' : undefined],
|
|
54
55
|
['底层原因', details.reason === 'unknown' ? localizeText('暂未识别') : details.reason], ['多个原因', details.reasons?.join(', ')], ['服务域名', details.host], ['请求耗时(毫秒)', details.durationMs], ['超时阈值(毫秒)', details.timeoutMs], ['Node 版本', details.nodeVersion], ['DSH 版本', details.dshVersion], ['依赖版本', details.dependencies ? Object.entries(details.dependencies).map(([name, version]) => `${name}: ${version}`).join(', ') : undefined], ['代理已配置', details.proxyConfigured], ['原因链已截断', details.truncated], ['运行平台', details.platform], ['HTTP 状态', details.httpStatus], ['服务返回码', details.providerCode],
|
|
55
56
|
['配置文件', details.file], ['配置字段', details.field],
|
|
56
57
|
['校验原因', details.issue ? localizeText(CONFIG_ISSUE_LABELS[details.issue]) : undefined],
|
|
@@ -45,7 +45,7 @@ function pluginPaths(config) {
|
|
|
45
45
|
|
|
46
46
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
47
47
|
if (!ctx?.credentials) throw new TypeError('dsh-dingtalk requires ctx.credentials');
|
|
48
|
-
const connection = harnessConnection(ctx, config);
|
|
48
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
49
49
|
|
|
50
50
|
const ConfigStore = internals.ConfigStore ?? DingtalkConfigStore;
|
|
51
51
|
const DeviceAuth = internals.DeviceAuth ?? DingtalkDeviceAuth;
|
|
@@ -101,7 +101,7 @@ function pluginPaths(config) {
|
|
|
101
101
|
*/
|
|
102
102
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
103
103
|
if (!ctx?.credentials) throw new TypeError('dsh-feishu requires ctx.credentials');
|
|
104
|
-
const connection = harnessConnection(ctx, config);
|
|
104
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
105
105
|
|
|
106
106
|
const lark = internals.lark ?? Lark;
|
|
107
107
|
const Controller = internals.Controller ?? MultiBotDshFeishuController;
|
|
@@ -226,7 +226,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
226
226
|
verifyApp,
|
|
227
227
|
credentials: ctx.credentials,
|
|
228
228
|
configStore: observedConfigStore,
|
|
229
|
-
createRuntime: async ({ botId, config: botConfig, appSecret, repair }) => {
|
|
229
|
+
createRuntime: async ({ botId, config: botConfig, appSecret, repair, acceptExternal }) => {
|
|
230
230
|
const state = await stateFor(botConfig);
|
|
231
231
|
const id = botId ?? botConfig.id ?? botConfig.appId;
|
|
232
232
|
await workspaces.ensure(id, {
|
|
@@ -248,6 +248,8 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
248
248
|
}
|
|
249
249
|
return new Runtime({
|
|
250
250
|
lark,
|
|
251
|
+
consumerMode: botConfig.consumerMode,
|
|
252
|
+
acceptExternal,
|
|
251
253
|
botId: id,
|
|
252
254
|
repair,
|
|
253
255
|
appId: botConfig.appId,
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
|
|
35
35
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
36
36
|
if (!ctx?.credentials) throw new TypeError('dsh-im matrix requires ctx.credentials');
|
|
37
|
-
const connection = harnessConnection(ctx, config);
|
|
37
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
38
38
|
|
|
39
39
|
const ResolvedConfigStore = internals.ConfigStore ?? MatrixConfigStore;
|
|
40
40
|
const ResolvedStateStore = internals.StateStore ?? ConversationStateStore;
|
|
@@ -45,7 +45,7 @@ function pluginPaths(config) {
|
|
|
45
45
|
|
|
46
46
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
47
47
|
if (!ctx?.credentials) throw new TypeError('dsh-im QQ requires ctx.credentials');
|
|
48
|
-
const connection = harnessConnection(ctx, config);
|
|
48
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
49
49
|
|
|
50
50
|
const ConfigStore = internals.ConfigStore ?? QqConfigStore;
|
|
51
51
|
const StateStore = internals.StateStore ?? QqStateStore;
|
|
@@ -44,7 +44,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
44
44
|
channel, ConfigStore, StateStore, HarnessClient, Controller, Runtime, runtimeOptions,
|
|
45
45
|
} = definitions;
|
|
46
46
|
if (!ctx?.credentials) throw new TypeError(`dsh-im ${channel} requires ctx.credentials`);
|
|
47
|
-
const connection = harnessConnection(ctx, config);
|
|
47
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: ['telegram', 'discord'].includes(channel) });
|
|
48
48
|
|
|
49
49
|
const ResolvedConfigStore = internals.ConfigStore ?? ConfigStore;
|
|
50
50
|
const ResolvedStateStore = internals.StateStore ?? StateStore;
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
|
|
34
34
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
35
35
|
if (!ctx?.credentials) throw new TypeError('dsh-im slack requires ctx.credentials');
|
|
36
|
-
const connection = harnessConnection(ctx, config);
|
|
36
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
37
37
|
|
|
38
38
|
const ResolvedConfigStore = internals.ConfigStore ?? SlackConfigStore;
|
|
39
39
|
const ResolvedStateStore = internals.StateStore ?? SlackStateStore;
|
|
@@ -44,7 +44,7 @@ function pluginPaths(config) {
|
|
|
44
44
|
|
|
45
45
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
46
46
|
if (!ctx?.credentials) throw new TypeError('dsh-im Enterprise WeChat requires ctx.credentials');
|
|
47
|
-
const connection = harnessConnection(ctx, config);
|
|
47
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
48
48
|
|
|
49
49
|
const ConfigStore = internals.ConfigStore ?? WecomConfigStore;
|
|
50
50
|
const StateStore = internals.StateStore ?? WecomStateStore;
|
|
@@ -49,7 +49,7 @@ function normalizeOrigin(value) {
|
|
|
49
49
|
|
|
50
50
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
51
51
|
if (!ctx?.credentials) throw new TypeError('dsh-im Enterprise WeChat app requires ctx.credentials');
|
|
52
|
-
const connection = harnessConnection(ctx, config);
|
|
52
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
53
53
|
|
|
54
54
|
const ConfigStore = internals.ConfigStore ?? WecomAppConfigStore;
|
|
55
55
|
const StateStore = internals.StateStore ?? WecomAppStateStore;
|
|
@@ -77,7 +77,7 @@ function diagnosticWorkspaces(store) {
|
|
|
77
77
|
|
|
78
78
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
79
79
|
if (!ctx?.credentials) throw new TypeError('dsh-weixin requires ctx.credentials');
|
|
80
|
-
const connection = harnessConnection(ctx, config);
|
|
80
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
81
81
|
|
|
82
82
|
const ConfigStore = internals.ConfigStore ?? WeixinConfigStore;
|
|
83
83
|
const StateStore = internals.StateStore ?? WeixinStateStore;
|
|
@@ -54,7 +54,7 @@ function pluginPaths(config) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
57
|
-
const connection = harnessConnection(ctx, config);
|
|
57
|
+
const connection = harnessConnection(ctx, config, { competitiveApprovals: true });
|
|
58
58
|
const logger = typeof ctx.logger === 'function'
|
|
59
59
|
? ctx.logger('dsh-im:whatsapp') : (ctx.logger ?? console);
|
|
60
60
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
@@ -231,8 +231,35 @@ export function createDeliveryAdapter({ channel, workspaces, coreController, sta
|
|
|
231
231
|
if (typeof stateFor !== 'function') {
|
|
232
232
|
throw new TypeError('delivery adapter requires a bot state getter');
|
|
233
233
|
}
|
|
234
|
+
const syncTarget = async (botId, targetId, sessionId) => {
|
|
235
|
+
const configured = sessionSyncConfigs(workspaces, botId).find((entry) => entry.targetId === targetId);
|
|
236
|
+
const target = workspaces.deliveryTargetFor(botId, targetId);
|
|
237
|
+
if (!configured || !target || !privateConversationKeyMatchesTarget(channel, configured.conversationKey, target)) {
|
|
238
|
+
throw sessionSyncUnavailable('Session sync target is no longer enabled');
|
|
239
|
+
}
|
|
240
|
+
const sessions = stateSessions(await stateFor(botId));
|
|
241
|
+
if (sessions?.[configured.conversationKey] !== sessionId) {
|
|
242
|
+
throw sessionSyncUnavailable('Session sync target is no longer bound to this Session');
|
|
243
|
+
}
|
|
244
|
+
return { target, conversationKey: configured.conversationKey };
|
|
245
|
+
};
|
|
234
246
|
return Object.freeze({
|
|
235
247
|
channel,
|
|
248
|
+
consumeInbound: (botId, options) => {
|
|
249
|
+
if (typeof coreController.consumeInbound !== 'function')
|
|
250
|
+
throw Object.assign(new Error('capability-unavailable'), { code: 'capability-unavailable' });
|
|
251
|
+
return coreController.consumeInbound(botId, options);
|
|
252
|
+
},
|
|
253
|
+
historyChecked: (botId, route, query, options) => {
|
|
254
|
+
if (typeof coreController.historyChecked !== 'function')
|
|
255
|
+
throw Object.assign(new Error('capability-unavailable'), { code: 'capability-unavailable' });
|
|
256
|
+
return coreController.historyChecked(botId, route, query, options);
|
|
257
|
+
},
|
|
258
|
+
replyChecked: (botId, route, text, options) => {
|
|
259
|
+
if (typeof coreController.replyChecked !== 'function')
|
|
260
|
+
throw Object.assign(new Error('capability-unavailable'), { code: 'capability-unavailable' });
|
|
261
|
+
return coreController.replyChecked(botId, route, text, options);
|
|
262
|
+
},
|
|
236
263
|
describeAccount: (botId) => {
|
|
237
264
|
if (typeof coreController.describeDeliveryAccount !== 'function') {
|
|
238
265
|
const error = new Error('Verified account capability unavailable');
|
|
@@ -339,20 +366,41 @@ export function createDeliveryAdapter({ channel, workspaces, coreController, sta
|
|
|
339
366
|
}
|
|
340
367
|
return matches;
|
|
341
368
|
},
|
|
369
|
+
async describeSessionSyncApprovalTarget(botId, targetId, sessionId) {
|
|
370
|
+
const { conversationKey } = await syncTarget(botId, targetId, sessionId);
|
|
371
|
+
return { conversationKey };
|
|
372
|
+
},
|
|
373
|
+
async presentSessionSyncApproval(botId, targetId, sessionId, interaction, options = {}) {
|
|
374
|
+
const original = await syncTarget(botId, targetId, sessionId);
|
|
375
|
+
if (typeof coreController.presentSessionSyncApproval !== 'function') {
|
|
376
|
+
throw sessionSyncUnavailable('This channel cannot present synced approvals');
|
|
377
|
+
}
|
|
378
|
+
const validate = async () => {
|
|
379
|
+
const current = await syncTarget(botId, targetId, sessionId);
|
|
380
|
+
if (current.conversationKey !== original.conversationKey || options.signal?.aborted) {
|
|
381
|
+
throw sessionSyncUnavailable('The approval target changed or the approval ended');
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const guarded = {
|
|
385
|
+
...interaction,
|
|
386
|
+
respond: async (result, responseOptions) => {
|
|
387
|
+
try { await validate(); }
|
|
388
|
+
catch (error) {
|
|
389
|
+
await interaction.withdraw?.();
|
|
390
|
+
throw Object.assign(new Error('Approval route is no longer available', { cause: error }), {
|
|
391
|
+
code: 'interaction-not-pending',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return interaction.respond(result, responseOptions);
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
return coreController.presentSessionSyncApproval(botId, original.target, guarded, {
|
|
398
|
+
...options, key: original.conversationKey, validate,
|
|
399
|
+
});
|
|
400
|
+
},
|
|
342
401
|
async sendSessionSyncText(botId, targetId, sessionId, text, options = {}) {
|
|
343
402
|
if (typeof text !== 'string' || !text.trim()) throw new TypeError('text is required');
|
|
344
|
-
const
|
|
345
|
-
.find((target) => target.targetId === targetId);
|
|
346
|
-
const target = workspaces.deliveryTargetFor(botId, targetId);
|
|
347
|
-
if (!configured || !target || !privateConversationKeyMatchesTarget(
|
|
348
|
-
channel,
|
|
349
|
-
configured.conversationKey,
|
|
350
|
-
target,
|
|
351
|
-
)) throw sessionSyncUnavailable('Session sync target is no longer enabled');
|
|
352
|
-
const sessions = stateSessions(await stateFor(botId));
|
|
353
|
-
if (sessions?.[configured.conversationKey] !== sessionId) {
|
|
354
|
-
throw sessionSyncUnavailable('Session sync target is no longer bound to this Session');
|
|
355
|
-
}
|
|
403
|
+
const { target } = await syncTarget(botId, targetId, sessionId);
|
|
356
404
|
if (typeof coreController.sendProactiveText !== 'function') {
|
|
357
405
|
throw new TypeError('delivery controller cannot send proactive text');
|
|
358
406
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { normalizeDeliveryTarget } from './delivery-adapter.mjs';
|
|
3
|
+
import { COMPETITIVE_APPROVAL_CHANNELS } from '../../src/channels/shared/harness-approval.mjs';
|
|
4
|
+
import { t } from '../../src/channels/shared/i18n.mjs';
|
|
3
5
|
|
|
4
6
|
const BOT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
5
7
|
const TARGET_ID_PATTERN = /^[A-Za-z0-9._:@-]{1,128}$/;
|
|
@@ -33,6 +35,17 @@ const DELIVERY_ERROR_CODES = new Set([
|
|
|
33
35
|
'send-result-unknown',
|
|
34
36
|
'session-sync-unavailable',
|
|
35
37
|
'cancelled',
|
|
38
|
+
'provider-unavailable',
|
|
39
|
+
'consumer-unavailable',
|
|
40
|
+
'consumer-conflict',
|
|
41
|
+
'ingress-not-accepted',
|
|
42
|
+
'invalid-inbound',
|
|
43
|
+
'stale-route',
|
|
44
|
+
'reply-result-unknown',
|
|
45
|
+
'history-permission-denied',
|
|
46
|
+
'history-unavailable',
|
|
47
|
+
'thread-unavailable',
|
|
48
|
+
'untrusted-source',
|
|
36
49
|
]);
|
|
37
50
|
|
|
38
51
|
const SESSION_SYNC_METHODS = Object.freeze([
|
|
@@ -132,11 +145,13 @@ export class DeliveryService {
|
|
|
132
145
|
|
|
133
146
|
registerAdapter(value) {
|
|
134
147
|
const adapter = validateAdapter(value);
|
|
135
|
-
|
|
148
|
+
this.#adapters.get(adapter.channel)?.controller.abort(deliveryError('provider-unavailable'));
|
|
149
|
+
const registration = Object.freeze({ adapter, controller: new AbortController() });
|
|
136
150
|
this.#adapters.set(adapter.channel, registration);
|
|
137
151
|
return () => {
|
|
138
152
|
if (this.#adapters.get(adapter.channel) !== registration) return false;
|
|
139
153
|
this.#adapters.delete(adapter.channel);
|
|
154
|
+
registration.controller.abort(deliveryError('provider-unavailable'));
|
|
140
155
|
return true;
|
|
141
156
|
};
|
|
142
157
|
}
|
|
@@ -273,6 +288,59 @@ export class DeliveryService {
|
|
|
273
288
|
return targets;
|
|
274
289
|
}
|
|
275
290
|
|
|
291
|
+
/** Offer one existing bound private chat the same approval the Host is awaiting. */
|
|
292
|
+
async presentSessionSyncApproval(sessionId, interaction, { signal, completion } = {}) {
|
|
293
|
+
const targets = (await this.listSessionSyncTargets(sessionId))
|
|
294
|
+
.filter((target) => COMPETITIVE_APPROVAL_CHANNELS.includes(target.channel));
|
|
295
|
+
if (signal?.aborted || targets.length === 0) return false;
|
|
296
|
+
let outcome = null;
|
|
297
|
+
let notified = false;
|
|
298
|
+
const notify = async (finished = false) => {
|
|
299
|
+
if (!finished && (signal?.aborted || outcome !== null)) return;
|
|
300
|
+
const text = finished
|
|
301
|
+
? t('未找到可处理端,本次操作未获批准。')
|
|
302
|
+
: t('有操作在等待审批:{tool},请到 Web 处理。', { tool: interaction.payload.toolName });
|
|
303
|
+
const results = await Promise.allSettled(targets.map((target) => this.sendSessionSyncText(
|
|
304
|
+
target.botId, target.targetId, sessionId, text, finished ? {} : { signal },
|
|
305
|
+
)));
|
|
306
|
+
notified ||= results.some((result) => result.status === 'fulfilled');
|
|
307
|
+
};
|
|
308
|
+
Promise.resolve(completion).then(async (value) => {
|
|
309
|
+
outcome = value;
|
|
310
|
+
if (value === 'unavailable' && notified) await notify(true);
|
|
311
|
+
}).catch((error) => console.warn('[dsh-im] approval status notification failed:', error?.code ?? error?.name));
|
|
312
|
+
const candidates = new Map();
|
|
313
|
+
let unresolvedTarget = false;
|
|
314
|
+
for (const target of targets) {
|
|
315
|
+
try {
|
|
316
|
+
const adapter = await this.#adapterFor(target.botId);
|
|
317
|
+
if (typeof adapter.describeSessionSyncApprovalTarget !== 'function') {
|
|
318
|
+
unresolvedTarget = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const info = await adapter.describeSessionSyncApprovalTarget(target.botId, target.targetId, sessionId);
|
|
322
|
+
candidates.set(JSON.stringify([target.channel, target.botId, info.conversationKey]), { adapter, target });
|
|
323
|
+
} catch (error) {
|
|
324
|
+
unresolvedTarget = true;
|
|
325
|
+
console.warn('[dsh-im] approval target unavailable:', error?.code ?? error?.name);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (signal?.aborted) return false;
|
|
329
|
+
if (!unresolvedTarget && candidates.size === 1) {
|
|
330
|
+
const { adapter, target } = candidates.values().next().value;
|
|
331
|
+
try {
|
|
332
|
+
if (await adapter.presentSessionSyncApproval(target.botId, target.targetId, sessionId, interaction, {
|
|
333
|
+
signal, completion,
|
|
334
|
+
})) return true;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (!signal?.aborted) console.warn('[dsh-im] synced approval unavailable:', error?.code ?? error?.name);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (outcome === 'unavailable') await notify(true);
|
|
340
|
+
else await notify();
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
276
344
|
async sendSessionSyncText(botId, targetId, sessionId, text, { signal } = {}) {
|
|
277
345
|
const id = botIdOf(botId);
|
|
278
346
|
const targetKey = targetIdOf(targetId);
|
|
@@ -315,6 +383,64 @@ export class DeliveryService {
|
|
|
315
383
|
} catch (error) { throw publicOperationError(error); }
|
|
316
384
|
}
|
|
317
385
|
|
|
386
|
+
async consumeInbound(botId, options = {}) {
|
|
387
|
+
const id = botIdOf(botId);
|
|
388
|
+
cancellation(options.signal);
|
|
389
|
+
if (!/^[a-f0-9]{64}$/.test(options.expectedFingerprint ?? '') || typeof options.onEvent !== 'function')
|
|
390
|
+
throw deliveryError('bad-request');
|
|
391
|
+
const registration = await this.#checkedRegistrationFor(id);
|
|
392
|
+
if (typeof registration.adapter.consumeInbound !== 'function' || typeof options.onEvent !== 'function')
|
|
393
|
+
throw deliveryError('capability-unavailable');
|
|
394
|
+
this.#assertRegistered(registration);
|
|
395
|
+
const dispose = await registration.adapter.consumeInbound(id, {
|
|
396
|
+
...options,
|
|
397
|
+
signal: options.signal ? AbortSignal.any([options.signal, registration.controller.signal]) : registration.controller.signal,
|
|
398
|
+
onEvent: async (evidence, context) => {
|
|
399
|
+
this.#assertRegistered(registration);
|
|
400
|
+
const result = await options.onEvent(evidence, context);
|
|
401
|
+
this.#assertRegistered(registration);
|
|
402
|
+
return result;
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
try { this.#assertRegistered(registration); } catch (error) { dispose(); throw error; }
|
|
406
|
+
return dispose;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async historyChecked(botId, route, query, options = {}) {
|
|
410
|
+
const id = botIdOf(botId);
|
|
411
|
+
cancellation(options.signal);
|
|
412
|
+
if (!/^[a-f0-9]{64}$/.test(options.expectedFingerprint ?? '')) throw deliveryError('bad-request');
|
|
413
|
+
const registration = await this.#checkedRegistrationFor(id);
|
|
414
|
+
if (typeof registration.adapter.historyChecked !== 'function') throw deliveryError('capability-unavailable');
|
|
415
|
+
this.#assertRegistered(registration);
|
|
416
|
+
const signal = options.signal ? AbortSignal.any([options.signal, registration.controller.signal]) : registration.controller.signal;
|
|
417
|
+
try {
|
|
418
|
+
const result = await registration.adapter.historyChecked(id, structuredClone(route), structuredClone(query), { ...options, signal });
|
|
419
|
+
this.#assertRegistered(registration);
|
|
420
|
+
cancellation(options.signal);
|
|
421
|
+
return result;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
this.#assertRegistered(registration);
|
|
424
|
+
if (signal.aborted || error?.name === 'AbortError') throw deliveryError('cancelled');
|
|
425
|
+
throw publicOperationError(error, 'history-unavailable');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async replyChecked(botId, route, text, options = {}) {
|
|
430
|
+
const id = botIdOf(botId);
|
|
431
|
+
cancellation(options.signal);
|
|
432
|
+
if (!/^[a-f0-9]{64}$/.test(options.expectedFingerprint ?? '')
|
|
433
|
+
|| typeof text !== 'string' || !text.trim() || text.length > 4000)
|
|
434
|
+
throw deliveryError('bad-request');
|
|
435
|
+
const registration = await this.#checkedRegistrationFor(id);
|
|
436
|
+
if (typeof registration.adapter.replyChecked !== 'function') throw deliveryError('capability-unavailable');
|
|
437
|
+
this.#assertRegistered(registration);
|
|
438
|
+
try { return await registration.adapter.replyChecked(id, structuredClone(route), text, {
|
|
439
|
+
...options, signal: options.signal ? AbortSignal.any([options.signal, registration.controller.signal]) : registration.controller.signal,
|
|
440
|
+
}); }
|
|
441
|
+
catch (error) { throw publicOperationError(error); }
|
|
442
|
+
}
|
|
443
|
+
|
|
318
444
|
async sendChecked(botId, targetId, text, { expectedFingerprint, expectedTargetDigest, signal, format = 'plain', receipt = false } = {}) {
|
|
319
445
|
const id = botIdOf(botId);
|
|
320
446
|
const key = targetIdOf(targetId);
|
|
@@ -124,7 +124,7 @@ export function deliverySuggestionFromConversationKey(channel, key) {
|
|
|
124
124
|
case 'whatsapp':
|
|
125
125
|
return whatsappSuggestion(key);
|
|
126
126
|
case 'matrix':
|
|
127
|
-
return matrixSuggestion(key);
|
|
127
|
+
return matrixSuggestion(key.replace(/^(direct|group):/, ''));
|
|
128
128
|
default:
|
|
129
129
|
return null;
|
|
130
130
|
}
|
|
@@ -132,6 +132,7 @@ export function deliverySuggestionFromConversationKey(channel, key) {
|
|
|
132
132
|
|
|
133
133
|
/** Convert only a persisted private-chat key into its stable delivery route. */
|
|
134
134
|
export function privateDeliverySuggestionFromConversationKey(channel, key) {
|
|
135
|
+
if (channel === 'matrix' && typeof key === 'string') key = key.replace(/^direct:dm:/, 'dm:');
|
|
135
136
|
const separator = typeof key === 'string' ? key.indexOf(':') : -1;
|
|
136
137
|
const prefix = separator > 0 ? key.slice(0, separator) : '';
|
|
137
138
|
const privatePrefix = {
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { modernHarnessApi } from './modern-harness-api.mjs';
|
|
2
2
|
|
|
3
|
-
function optionalService(ctx, name) {
|
|
4
|
-
if (ctx && Object.hasOwn(ctx, name)) return ctx[name];
|
|
5
|
-
return typeof ctx?.get === 'function' ? ctx.get(name) : undefined;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
3
|
/** Connect to this Host directly unless an external Harness URL was configured. */
|
|
9
|
-
export function harnessConnection(ctx, config = {}) {
|
|
4
|
+
export function harnessConnection(ctx, config = {}, { competitiveApprovals = false } = {}) {
|
|
10
5
|
if (config.harnessBaseUrl !== undefined) {
|
|
11
6
|
return { baseUrl: new URL(config.harnessBaseUrl) };
|
|
12
7
|
}
|
|
13
|
-
const apiProxy =
|
|
8
|
+
const apiProxy = modernHarnessApi(ctx, { deliveryService: config.deliveryService });
|
|
14
9
|
return {
|
|
15
10
|
apiProxy,
|
|
11
|
+
competitiveApprovals,
|
|
16
12
|
// Cordis child contexts share one root; different Hosts must not share
|
|
17
13
|
// ownership of pending questions and approvals.
|
|
18
14
|
interactionScope: ctx.root ?? ctx,
|
|
@@ -93,6 +93,10 @@ export function createImHostPlugin(internals = {}) {
|
|
|
93
93
|
ctx.provide('dshIm', Object.freeze({
|
|
94
94
|
contractVersion: 1,
|
|
95
95
|
receiptVersion: 1,
|
|
96
|
+
inboundVersion: 1,
|
|
97
|
+
consumeInbound: (botId, options) => deliveryService.consumeInbound(botId, options),
|
|
98
|
+
historyChecked: (botId, route, query, options) => deliveryService.historyChecked(botId, route, query, options),
|
|
99
|
+
replyChecked: (botId, route, text, options) => deliveryService.replyChecked(botId, route, text, options),
|
|
96
100
|
describeBot: (botId) => deliveryService.describeBot(botId),
|
|
97
101
|
sendChecked: (botId, targetId, text, options) => deliveryService.sendChecked(botId, targetId, text, options),
|
|
98
102
|
send: (botId, targetId, text, options) => (
|
|
@@ -106,9 +110,8 @@ export function createImHostPlugin(internals = {}) {
|
|
|
106
110
|
await activateChannels(readyCtx, config, deliveryService);
|
|
107
111
|
};
|
|
108
112
|
if (typeof ctx?.inject === 'function') {
|
|
109
|
-
const modern = typeof ctx?.typertGateway?.stream === 'function';
|
|
110
113
|
await ctx.inject(
|
|
111
|
-
|
|
114
|
+
['sessionController', 'workspaceController'],
|
|
112
115
|
activate,
|
|
113
116
|
);
|
|
114
117
|
ctx.inject(['webServer'], (httpCtx) => {
|