@xmanrui/dsh-im 4.38.0 → 4.38.2
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 +18 -2
- package/PROACTIVE_DELIVERY.md +18 -2
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/lib/client.js +49 -5
- package/lib/index.js +320 -315
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
- package/plugin-src/client/connection-error.js +12 -2
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +1 -1
- 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 +9 -1
- package/plugin-src/host/delivery-service.mjs +4 -3
- package/plugin-src/host/modern-harness-api.mjs +2 -1
- package/plugin-src/host/session-sync-coordinator.mjs +9 -5
- package/src/channels/dingtalk/dingtalk-api.mjs +41 -24
- package/src/channels/dingtalk/dingtalk-runtime.mjs +3 -2
- package/src/channels/discord/discord-runtime.mjs +13 -2
- package/src/channels/feishu/bridge.mjs +95 -24
- package/src/channels/matrix/matrix-runtime.mjs +16 -6
- package/src/channels/qq/markdown-reply.mjs +7 -340
- package/src/channels/qq/qq-runtime.mjs +11 -3
- package/src/channels/shared/bot-workspace-store.mjs +3 -3
- package/src/channels/shared/connection-error.mjs +29 -4
- package/src/channels/shared/diagnostic-details.mjs +6 -2
- package/src/channels/shared/harness-client.mjs +138 -19
- package/src/channels/shared/harness-mux.mjs +6 -4
- package/src/channels/shared/i18n-en/diagnostics.mjs +24 -0
- package/src/channels/shared/i18n-en/wecom.mjs +3 -0
- package/src/channels/shared/markdown-chunks.mjs +338 -0
- package/src/channels/shared/message-failure.mjs +31 -11
- package/src/channels/shared/text-harness-bridge.mjs +4 -1
- package/src/channels/shared/workspace-session.mjs +10 -1
- package/src/channels/slack/slack-api.mjs +2 -1
- package/src/channels/slack/slack-runtime.mjs +30 -0
- package/src/channels/telegram/telegram-runtime.mjs +1 -0
- package/src/channels/wecom/wecom-bridge.mjs +45 -10
- package/src/channels/wecom/wecom-runtime.mjs +11 -5
- package/src/channels/weixin/weixin-runtime.mjs +17 -10
- package/src/channels/whatsapp/whatsapp-markdown.mjs +59 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +26 -1
package/locale/en.json
CHANGED
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.38.
|
|
3
|
+
"version": "4.38.2",
|
|
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": [
|
|
@@ -4,6 +4,8 @@ import { normalizeDiagnosticDetails } from '../../src/channels/shared/diagnostic
|
|
|
4
4
|
import { CONFIG_ISSUE_LABELS } from '../../src/channels/shared/diagnostic-details.mjs';
|
|
5
5
|
|
|
6
6
|
const STAGE_LABELS = {
|
|
7
|
+
'session.prepare': '准备会话', 'prompt.prepare': '准备消息', 'interaction.open': '建立交互通道',
|
|
8
|
+
'prompt.submit': '提交消息', 'reply.wait': '等待处理结果',
|
|
7
9
|
'credential.verify': '验证凭据', 'permission.check': '检查权限', 'sdk.load': '加载渠道组件',
|
|
8
10
|
operation: '执行渠道操作', 'message.receive': '接收消息', 'message.send': '发送消息', 'connection.test': '发送测试消息',
|
|
9
11
|
'startup.load': '加载渠道配置', 'qr.begin': '申请二维码', 'qr.encode': '生成二维码图片', 'qr.poll': '查询扫码状态',
|
|
@@ -15,6 +17,11 @@ const STAGE_LABELS = {
|
|
|
15
17
|
'connection.poll': '监测连接与消息', 'connection.stop': '停止连接', 'status.read': '读取连接状态',
|
|
16
18
|
rollback: '恢复原账号状态', 'management.request': '访问 DSH 管理接口',
|
|
17
19
|
};
|
|
20
|
+
const SUBMISSION_LABELS = { 'not-submitted': '未提交', accepted: '已确认接收', unknown: '提交结果未知' };
|
|
21
|
+
const STREAM_REASON_LABELS = {
|
|
22
|
+
'stream-closed-before-open': '打开前关闭', 'stream-failed-before-open': '打开前失败',
|
|
23
|
+
'stream-failed': '通道连接失败', unknown: '暂未识别',
|
|
24
|
+
};
|
|
18
25
|
|
|
19
26
|
export function provisioningErrorTitle(error) {
|
|
20
27
|
const stage = error?.details?.stage;
|
|
@@ -30,7 +37,7 @@ export function formatConnectionDiagnostic(value) {
|
|
|
30
37
|
return [
|
|
31
38
|
localizeText(error.message), details.hint ? localizeText(details.hint) : null,
|
|
32
39
|
localizeText('错误码') + ': ' + error.code,
|
|
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']
|
|
40
|
+
...['channel', 'operation', 'stage', 'method', 'transport', 'submissionState', 'attempts', 'lastFailureReason', 'reason', 'reasons', 'host', 'durationMs', 'timeoutMs', 'nodeVersion', 'dshVersion', 'platform', 'dependencies', 'proxyConfigured', 'truncated', 'httpStatus', 'providerCode', 'resource', 'file', 'field', 'issue', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
|
|
34
41
|
.filter(field => details[field] !== undefined)
|
|
35
42
|
.map(field => `${field}: ${typeof details[field] === 'object' ? JSON.stringify(details[field]) : details[field]}`),
|
|
36
43
|
].filter(Boolean).join('\n');
|
|
@@ -51,7 +58,10 @@ export function ConnectionError({ error: value, warning = false, showMessage = t
|
|
|
51
58
|
};
|
|
52
59
|
const fields = [
|
|
53
60
|
['错误码', error.code], ['失败阶段', localizeText(STAGE_LABELS[details.stage] ?? details.stage ?? '')],
|
|
54
|
-
['RPC 方法', details.method], ['调用方式', details.transport === 'host-api' ? localizeText('宿主 API(进程内)') : details.transport === 'http' ? 'HTTP' : undefined],
|
|
61
|
+
['RPC 方法', details.method], ['调用方式', details.transport === 'host-api' ? localizeText('宿主 API(进程内)') : details.transport === 'http' ? 'HTTP' : details.transport === 'websocket' ? 'WebSocket' : undefined],
|
|
62
|
+
['消息提交状态', localizeText(SUBMISSION_LABELS[details.submissionState] ?? '')],
|
|
63
|
+
['连接尝试次数', details.attempts],
|
|
64
|
+
['最近一次连接失败', localizeText(STREAM_REASON_LABELS[details.lastFailureReason] ?? details.lastFailureReason ?? '')],
|
|
55
65
|
['底层原因', 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],
|
|
56
66
|
['配置文件', details.file], ['配置字段', details.field],
|
|
57
67
|
['校验原因', details.issue ? localizeText(CONFIG_ISSUE_LABELS[details.issue]) : undefined],
|
|
@@ -116,7 +116,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
116
116
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
117
117
|
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
118
118
|
});
|
|
119
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
119
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
120
120
|
const coreController = new Controller({
|
|
121
121
|
deviceAuth,
|
|
122
122
|
credentials: ctx.credentials,
|
|
@@ -219,7 +219,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
219
219
|
const proxyEnv = internals.proxyEnv ?? process.env;
|
|
220
220
|
const wsAgent = createFeishuWebSocketAgent(proxyEnv, internals.createProxyAgent);
|
|
221
221
|
|
|
222
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
222
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
223
223
|
const coreController = new Controller({
|
|
224
224
|
logger,
|
|
225
225
|
registerApp: (options) => lark.registerApp(options),
|
|
@@ -127,7 +127,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
127
127
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
128
128
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
129
129
|
});
|
|
130
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
130
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
131
131
|
const knownCommandNames = new Set(
|
|
132
132
|
commandsForChannel('matrix').flatMap((entry) => [entry.name, ...entry.aliases.map((alias) => alias.name)]),
|
|
133
133
|
);
|
|
@@ -108,7 +108,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
108
108
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
109
109
|
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
110
110
|
});
|
|
111
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
111
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
112
112
|
const coreController = new Controller({
|
|
113
113
|
qrAuth,
|
|
114
114
|
credentials: ctx.credentials,
|
|
@@ -126,7 +126,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
126
126
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
127
127
|
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
128
128
|
});
|
|
129
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
129
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
130
130
|
// Assigned just below. A transport may need to write a rotated token back
|
|
131
131
|
// through the controller, which is only constructible after createRuntime is
|
|
132
132
|
// defined, so the reference is held in a slot rather than captured directly.
|
|
@@ -95,7 +95,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
95
95
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
96
96
|
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
97
97
|
});
|
|
98
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
98
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
99
99
|
const coreController = new ResolvedController({
|
|
100
100
|
credentials: ctx.credentials,
|
|
101
101
|
configStore: observedConfigStore,
|
|
@@ -110,7 +110,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
110
110
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
111
111
|
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
112
112
|
});
|
|
113
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
113
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
114
114
|
const coreController = new Controller({
|
|
115
115
|
qrAuth,
|
|
116
116
|
credentials: ctx.credentials,
|
|
@@ -144,7 +144,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
144
144
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
145
145
|
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
146
146
|
});
|
|
147
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
147
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
148
148
|
const coreController = new Controller({
|
|
149
149
|
credentials: ctx.credentials,
|
|
150
150
|
configStore: observedConfigStore,
|
|
@@ -150,7 +150,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
150
150
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
151
151
|
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
152
152
|
});
|
|
153
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
153
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
154
154
|
const coreController = new Controller({
|
|
155
155
|
api,
|
|
156
156
|
credentials: ctx.credentials,
|
|
@@ -116,7 +116,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
116
116
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
117
117
|
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
118
118
|
});
|
|
119
|
-
const modelCatalog = () => listModelCatalog(harness);
|
|
119
|
+
const modelCatalog = (options) => listModelCatalog(harness, options);
|
|
120
120
|
const coreController = new Controller({
|
|
121
121
|
configStore: observedConfigStore,
|
|
122
122
|
authPath: paths.authPath,
|
|
@@ -404,7 +404,15 @@ export function createDeliveryAdapter({ channel, workspaces, coreController, sta
|
|
|
404
404
|
if (typeof coreController.sendProactiveText !== 'function') {
|
|
405
405
|
throw new TypeError('delivery controller cannot send proactive text');
|
|
406
406
|
}
|
|
407
|
-
await coreController.sendProactiveText(botId, target, text, options);
|
|
407
|
+
const result = await coreController.sendProactiveText(botId, target, text, options);
|
|
408
|
+
// Rich senders may report failure without throwing. Never report these
|
|
409
|
+
// as success, or retry an uncertain send that may already be visible.
|
|
410
|
+
if (result === false || result?.sent === false
|
|
411
|
+
|| result?.deliveryOutcome === 'failed' || result?.deliveryOutcome === 'unknown') {
|
|
412
|
+
const error = new Error('Session sync delivery was not confirmed');
|
|
413
|
+
error.code = result?.deliveryOutcome === 'unknown' ? 'send-result-unknown' : 'delivery-failed';
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
408
416
|
return { sent: true };
|
|
409
417
|
},
|
|
410
418
|
});
|
|
@@ -341,11 +341,12 @@ export class DeliveryService {
|
|
|
341
341
|
return false;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
async sendSessionSyncText(botId, targetId, sessionId, text, { signal } = {}) {
|
|
344
|
+
async sendSessionSyncText(botId, targetId, sessionId, text, { signal, format = 'plain' } = {}) {
|
|
345
345
|
const id = botIdOf(botId);
|
|
346
346
|
const targetKey = targetIdOf(targetId);
|
|
347
347
|
if (typeof sessionId !== 'string' || !sessionId
|
|
348
|
-
|| typeof text !== 'string' || !text.trim()
|
|
348
|
+
|| typeof text !== 'string' || !text.trim()
|
|
349
|
+
|| (format !== 'plain' && format !== 'markdown')) {
|
|
349
350
|
throw deliveryError('bad-request', 'Invalid session sync delivery');
|
|
350
351
|
}
|
|
351
352
|
cancellation(signal);
|
|
@@ -354,7 +355,7 @@ export class DeliveryService {
|
|
|
354
355
|
throw deliveryError('session-sync-unavailable', 'Session sync is unavailable');
|
|
355
356
|
}
|
|
356
357
|
try {
|
|
357
|
-
await adapter.sendSessionSyncText(id, targetKey, sessionId, text, { signal });
|
|
358
|
+
await adapter.sendSessionSyncText(id, targetKey, sessionId, text, { signal, format });
|
|
358
359
|
return { sent: true };
|
|
359
360
|
} catch (error) {
|
|
360
361
|
if (signal?.aborted || error?.name === 'AbortError' || error?.code === 'ABORT_ERR') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
import { activeHarnessInteractionOwner, hasActiveHarnessInteractionOwner, harnessToolCallFromEvent, HarnessInteractionError } from '../../src/channels/shared/harness-client.mjs';
|
|
3
|
+
import { activeHarnessInteractionOwner, clearModelCatalogCache, hasActiveHarnessInteractionOwner, harnessToolCallFromEvent, HarnessInteractionError } from '../../src/channels/shared/harness-client.mjs';
|
|
4
4
|
import { extractConnectionEvidence } from '../../src/channels/shared/connection-error.mjs';
|
|
5
5
|
|
|
6
6
|
const modernApis = new WeakMap();
|
|
@@ -857,6 +857,7 @@ class ModernHarnessApi {
|
|
|
857
857
|
dispose() {
|
|
858
858
|
if (this.#disposed) return;
|
|
859
859
|
this.#disposed = true;
|
|
860
|
+
clearModelCatalogCache(this);
|
|
860
861
|
for (const subscription of [...this.#mux]) subscription.close();
|
|
861
862
|
this.#assistantStreams.clear();
|
|
862
863
|
for (const pending of [...this.#pendingApprovals.values()]) pending.settle('cancelled');
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { deliverSessionSyncMirror } from '../../src/channels/shared/session-sync-registry.mjs';
|
|
7
7
|
|
|
8
8
|
const DSH_USER_PREFIX = '[来自 DSH]\n';
|
|
9
|
-
const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n';
|
|
9
|
+
const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n\n';
|
|
10
10
|
|
|
11
11
|
function completedTurn(reason) {
|
|
12
12
|
return (typeof reason === 'string' ? reason : reason?.kind) === 'completed';
|
|
@@ -55,7 +55,7 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
|
|
|
55
55
|
);
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
const deliver = async (sessionId, targets, text, phase) => {
|
|
58
|
+
const deliver = async (sessionId, targets, text, phase, format = 'plain') => {
|
|
59
59
|
const unique = new Map();
|
|
60
60
|
for (const target of targets) {
|
|
61
61
|
if (validRecipient(target)) unique.set(recipientKey(target), target);
|
|
@@ -67,6 +67,9 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
|
|
|
67
67
|
target.targetId,
|
|
68
68
|
sessionId,
|
|
69
69
|
text,
|
|
70
|
+
// Feishu has already tried its dedicated mirror card. Preserve its
|
|
71
|
+
// existing plain fallback instead of attempting another rich card.
|
|
72
|
+
{ format: target.channel === 'feishu' ? 'plain' : format },
|
|
70
73
|
)
|
|
71
74
|
)));
|
|
72
75
|
const successful = new Map();
|
|
@@ -165,13 +168,14 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
|
|
|
165
168
|
return false;
|
|
166
169
|
}
|
|
167
170
|
}));
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
171
|
+
const fallback = recipients.filter((_target, index) => !rendered[index]);
|
|
172
|
+
if (fallback.length === 0) return;
|
|
170
173
|
await deliver(
|
|
171
174
|
sessionId,
|
|
172
|
-
|
|
175
|
+
fallback,
|
|
173
176
|
`${DSH_ASSISTANT_PREFIX}${state.assistant.text}`,
|
|
174
177
|
'assistant delivery',
|
|
178
|
+
'markdown',
|
|
175
179
|
);
|
|
176
180
|
};
|
|
177
181
|
|
|
@@ -3,6 +3,7 @@ import { extname } from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
|
|
5
5
|
import { t } from '../shared/i18n.mjs';
|
|
6
|
+
import { chunkMarkdownParts } from '../shared/markdown-chunks.mjs';
|
|
6
7
|
import { DINGTALK_MENU_TEMPLATE_ID, dingtalkMenuCardData } from './dingtalk-menu.mjs';
|
|
7
8
|
|
|
8
9
|
export const DINGTALK_REGISTRATION_BASE_URL = 'https://oapi.dingtalk.com/';
|
|
@@ -402,6 +403,7 @@ function positiveNumber(value, fallback) {
|
|
|
402
403
|
}
|
|
403
404
|
|
|
404
405
|
export function createDingtalkApi({
|
|
406
|
+
logger = console,
|
|
405
407
|
fetchImpl = fetch,
|
|
406
408
|
registrationBaseUrl = process.env.DINGTALK_REGISTRATION_BASE_URL
|
|
407
409
|
|| DINGTALK_REGISTRATION_BASE_URL,
|
|
@@ -1075,37 +1077,52 @@ export function createDingtalkApi({
|
|
|
1075
1077
|
return true;
|
|
1076
1078
|
},
|
|
1077
1079
|
|
|
1078
|
-
async sendRobotText({ clientId, clientSecret, target, text, signal }) {
|
|
1080
|
+
async sendRobotText({ clientId, clientSecret, target, text, signal, format = 'plain' }) {
|
|
1079
1081
|
if (typeof text !== 'string' || !text.trim()) throw new TypeError('text is required');
|
|
1080
|
-
const content = text;
|
|
1081
1082
|
const normalizedTarget = normalizeFileTarget(target);
|
|
1082
1083
|
const token = await accessToken({ clientId, clientSecret, signal });
|
|
1083
|
-
const body = {
|
|
1084
|
-
robotCode: normalizedTarget.robotCode,
|
|
1085
|
-
msgKey: 'sampleText',
|
|
1086
|
-
msgParam: JSON.stringify({ content }),
|
|
1087
|
-
...(normalizedTarget.type === 'group'
|
|
1088
|
-
? { openConversationId: normalizedTarget.openConversationId }
|
|
1089
|
-
: { userIds: [normalizedTarget.userId] }),
|
|
1090
|
-
};
|
|
1091
1084
|
const pathname = normalizedTarget.type === 'group'
|
|
1092
1085
|
? 'v1.0/robot/groupMessages/send'
|
|
1093
1086
|
: 'v1.0/robot/oToMessages/batchSend';
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
'
|
|
1105
|
-
|
|
1106
|
-
|
|
1087
|
+
const send = async (content, markdown) => {
|
|
1088
|
+
const response = await requestJson(fetchImpl, endpoint(apiBase, pathname), {
|
|
1089
|
+
body: {
|
|
1090
|
+
robotCode: normalizedTarget.robotCode,
|
|
1091
|
+
msgKey: markdown ? 'sampleMarkdown' : 'sampleText',
|
|
1092
|
+
msgParam: JSON.stringify(markdown ? { title: 'DSH', text: content } : { content }),
|
|
1093
|
+
...(normalizedTarget.type === 'group'
|
|
1094
|
+
? { openConversationId: normalizedTarget.openConversationId }
|
|
1095
|
+
: { userIds: [normalizedTarget.userId] }),
|
|
1096
|
+
},
|
|
1097
|
+
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1098
|
+
signal,
|
|
1099
|
+
action: '主动文字消息发送',
|
|
1100
|
+
});
|
|
1101
|
+
const rejection = rejectedProviderResponse(response);
|
|
1102
|
+
if (rejection) {
|
|
1103
|
+
throw new DingtalkApiError('send-rejected', '钉钉服务拒绝了主动文字消息。', {
|
|
1104
|
+
providerCode: rejection,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return response;
|
|
1108
|
+
};
|
|
1109
|
+
if (format !== 'markdown') return send(text, false);
|
|
1110
|
+
// Conservative Unicode payload size; reuse QQ's fence/table-aware split.
|
|
1111
|
+
const chunks = chunkMarkdownParts(text, 1_000);
|
|
1112
|
+
let result;
|
|
1113
|
+
for (const chunk of chunks) {
|
|
1114
|
+
try {
|
|
1115
|
+
result = await send(chunk.markdown, true);
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
// A missing message model is a definite format rejection. Do not
|
|
1118
|
+
// retry auth/target failures, rate limits or ambiguous transport errors.
|
|
1119
|
+
if (signal?.aborted || error?.providerCode !== 'sendMessage.model.notMatch'
|
|
1120
|
+
|| !(error.code === 'send-rejected' || error.status === 400)) throw error;
|
|
1121
|
+
logger.warn?.('[dsh-im:dingtalk] Markdown rejected; sending this chunk as text');
|
|
1122
|
+
if (chunk.plain) result = await send(chunk.plain, false);
|
|
1123
|
+
}
|
|
1107
1124
|
}
|
|
1108
|
-
return
|
|
1125
|
+
return result;
|
|
1109
1126
|
},
|
|
1110
1127
|
|
|
1111
1128
|
async sendFile(request) {
|
|
@@ -163,7 +163,7 @@ export class DingtalkRuntime {
|
|
|
163
163
|
maxMessageChars = 4_000,
|
|
164
164
|
connectTimeoutMs = 15_000,
|
|
165
165
|
connectPollIntervalMs = 25,
|
|
166
|
-
api = createDingtalkApi(),
|
|
166
|
+
api = createDingtalkApi({ logger }),
|
|
167
167
|
streamFactory = defaultStreamFactory,
|
|
168
168
|
}) {
|
|
169
169
|
if (!config || !nonEmptyString(config.clientId) || !nonEmptyString(clientSecret)) {
|
|
@@ -443,7 +443,7 @@ export class DingtalkRuntime {
|
|
|
443
443
|
}, options);
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
async sendProactiveText(target, text, { signal } = {}) {
|
|
446
|
+
async sendProactiveText(target, text, { signal, format = 'plain' } = {}) {
|
|
447
447
|
const userId = typeof target?.route?.userId === 'string'
|
|
448
448
|
? target.route.userId.trim() : '';
|
|
449
449
|
const openConversationId = typeof target?.route?.openConversationId === 'string'
|
|
@@ -471,6 +471,7 @@ export class DingtalkRuntime {
|
|
|
471
471
|
...(target.kind === 'user' ? { userId } : { openConversationId }),
|
|
472
472
|
},
|
|
473
473
|
text,
|
|
474
|
+
format,
|
|
474
475
|
signal: signal ?? this.#abortController.signal,
|
|
475
476
|
});
|
|
476
477
|
} catch (cause) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
2
2
|
import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
3
|
+
import { chunkMarkdownText } from '../shared/markdown-chunks.mjs';
|
|
4
|
+
import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
|
|
3
5
|
import { fetchFileStream } from '../shared/file-download.mjs';
|
|
4
6
|
import { fetchImageBuffer } from '../shared/image-prompt.mjs';
|
|
5
7
|
import { t } from '../shared/i18n.mjs';
|
|
@@ -428,10 +430,19 @@ export class DiscordBotClient {
|
|
|
428
430
|
this.#signal = signal;
|
|
429
431
|
}
|
|
430
432
|
|
|
431
|
-
|
|
433
|
+
sendText(target, text) {
|
|
434
|
+
return this.#sendText(target, text, false);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
sendDelivery(target, value) {
|
|
438
|
+
const block = createTextDeliveryBlock(value);
|
|
439
|
+
return this.#sendText(target, block.text, block.format === 'markdown');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async #sendText(target, text, markdown) {
|
|
432
443
|
const notice = !this.#deliveredNotices.has(target) && target?.notice
|
|
433
444
|
? String(target.notice) : null;
|
|
434
|
-
const chunks = splitMessageText(notice ? `${notice}\n\n${text}` : text, 1_900);
|
|
445
|
+
const chunks = (markdown ? chunkMarkdownText : splitMessageText)(notice ? `${notice}\n\n${text}` : text, 1_900);
|
|
435
446
|
const providerMessageIds = [];
|
|
436
447
|
for (const [index, chunk] of chunks.entries()) {
|
|
437
448
|
const result = await this.#api.createMessage({
|