@xmanrui/dsh-im 1.3.0 → 1.5.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 +3 -3
- package/README.md +3 -3
- package/lib/index.js +168 -158
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +103 -81
- package/src/channels/dingtalk/dingtalk-bridge.mjs +34 -70
- package/src/channels/feishu/bridge.mjs +38 -57
- package/src/channels/feishu/feishu-channel.mjs +52 -17
- package/src/channels/qq/markdown-reply.mjs +176 -0
- package/src/channels/qq/qq-bridge.mjs +136 -105
- package/src/channels/shared/harness-client.mjs +66 -13
- package/src/channels/shared/semantic/artifact-delivery.mjs +170 -0
- package/src/channels/shared/semantic/artifact.mjs +2 -2
- package/src/channels/shared/semantic/delivery.mjs +2 -0
- package/src/channels/shared/text-harness-bridge.mjs +24 -73
- package/src/channels/telegram/telegram-api.mjs +39 -13
- package/src/channels/telegram/telegram-runtime.mjs +10 -0
- package/src/channels/wecom/wecom-bridge.mjs +95 -98
- package/src/channels/weixin/weixin-api.mjs +140 -107
- package/src/channels/weixin/weixin-bridge.mjs +36 -74
- package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -9
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
materializeOutboundArtifact,
|
|
3
|
+
releaseOutboundArtifact,
|
|
4
|
+
} from './artifact.mjs';
|
|
5
|
+
import {
|
|
6
|
+
createArtifactFailureReceipt,
|
|
7
|
+
createDeliveryReceipt,
|
|
8
|
+
mergeDeliveryReceipts,
|
|
9
|
+
providerMessageIdsFor,
|
|
10
|
+
} from './delivery.mjs';
|
|
11
|
+
|
|
12
|
+
function unavailableError() {
|
|
13
|
+
const error = new Error('Native file delivery is unavailable');
|
|
14
|
+
error.code = 'artifact-provider-unavailable';
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isAbort(error, signal) {
|
|
19
|
+
return signal?.aborted
|
|
20
|
+
|| error?.name === 'AbortError'
|
|
21
|
+
|| error?.code === 'ABORT_ERR';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function providerIds(value) {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return [...new Set(value
|
|
27
|
+
.filter((candidate) => (
|
|
28
|
+
(typeof candidate === 'string' && candidate.trim())
|
|
29
|
+
|| Number.isSafeInteger(candidate)
|
|
30
|
+
))
|
|
31
|
+
.map(String))];
|
|
32
|
+
}
|
|
33
|
+
return providerMessageIdsFor(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function sendMaterializedArtifact(file, {
|
|
37
|
+
sendFile,
|
|
38
|
+
sendImage,
|
|
39
|
+
signal,
|
|
40
|
+
}) {
|
|
41
|
+
if (file.mediaType?.startsWith('image/') && typeof sendImage === 'function') {
|
|
42
|
+
try {
|
|
43
|
+
return {
|
|
44
|
+
presentation: 'image',
|
|
45
|
+
result: await sendImage(file),
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (isAbort(error, signal) || error?.code === 'artifact-delivery-uncertain') {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
|
+
if (typeof sendFile !== 'function') throw unavailableError();
|
|
55
|
+
return {
|
|
56
|
+
presentation: 'file',
|
|
57
|
+
result: await sendFile(file),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Deliver registered artifacts with one shared image-first policy while keeping
|
|
63
|
+
* provider protocol details inside the channel-supplied send closures.
|
|
64
|
+
*/
|
|
65
|
+
export async function deliverOutboundArtifacts({
|
|
66
|
+
artifacts = [],
|
|
67
|
+
baseReceipt = null,
|
|
68
|
+
deliveryId,
|
|
69
|
+
aggregatePresentation,
|
|
70
|
+
alwaysMerge = false,
|
|
71
|
+
channelKey,
|
|
72
|
+
signal,
|
|
73
|
+
sendFile,
|
|
74
|
+
sendImage,
|
|
75
|
+
sendFailureNotice,
|
|
76
|
+
logger,
|
|
77
|
+
}) {
|
|
78
|
+
const receipts = baseReceipt ? [baseReceipt] : [];
|
|
79
|
+
let userVisible = Boolean(baseReceipt);
|
|
80
|
+
let failureNoticeVisible = false;
|
|
81
|
+
let artifactsSent = 0;
|
|
82
|
+
let artifactSendErrors = 0;
|
|
83
|
+
|
|
84
|
+
let artifactIndex = 0;
|
|
85
|
+
try {
|
|
86
|
+
while (artifactIndex < artifacts.length) {
|
|
87
|
+
const artifact = artifacts[artifactIndex];
|
|
88
|
+
artifactIndex += 1;
|
|
89
|
+
try {
|
|
90
|
+
signal?.throwIfAborted();
|
|
91
|
+
const file = await materializeOutboundArtifact(artifact, { signal });
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
const sent = await sendMaterializedArtifact(file, {
|
|
94
|
+
sendFile,
|
|
95
|
+
sendImage,
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
signal?.throwIfAborted();
|
|
99
|
+
receipts.push(createDeliveryReceipt({
|
|
100
|
+
deliveryId: file.deliveryKey,
|
|
101
|
+
presentation: `${channelKey}-${sent.presentation}`,
|
|
102
|
+
providerMessageIds: providerIds(sent.result),
|
|
103
|
+
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
|
|
104
|
+
}));
|
|
105
|
+
artifactsSent += 1;
|
|
106
|
+
userVisible = true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (isAbort(error, signal)) throw error;
|
|
109
|
+
artifactSendErrors += 1;
|
|
110
|
+
logger?.warn?.(
|
|
111
|
+
`[dsh-im:${channelKey}] result artifact delivery failed (${error?.code ?? 'unknown'})`,
|
|
112
|
+
);
|
|
113
|
+
let messageIds = [];
|
|
114
|
+
if (typeof sendFailureNotice === 'function') {
|
|
115
|
+
try {
|
|
116
|
+
signal?.throwIfAborted();
|
|
117
|
+
const notice = await sendFailureNotice(artifact, error);
|
|
118
|
+
signal?.throwIfAborted();
|
|
119
|
+
messageIds = providerIds(notice);
|
|
120
|
+
failureNoticeVisible = true;
|
|
121
|
+
} catch (noticeError) {
|
|
122
|
+
if (isAbort(noticeError, signal)) throw noticeError;
|
|
123
|
+
logger?.warn?.(
|
|
124
|
+
`[dsh-im:${channelKey}] unable to send the safe artifact failure notice`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const failureReceipt = createArtifactFailureReceipt({
|
|
129
|
+
artifactId: artifact?.artifactId ?? 'unknown',
|
|
130
|
+
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
131
|
+
error,
|
|
132
|
+
providerMessageIds: messageIds,
|
|
133
|
+
});
|
|
134
|
+
receipts.push(failureReceipt);
|
|
135
|
+
if (failureNoticeVisible || failureReceipt.artifacts[0]?.outcome === 'unknown') {
|
|
136
|
+
userVisible = true;
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
releaseOutboundArtifact(artifact);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
while (artifactIndex < artifacts.length) {
|
|
144
|
+
releaseOutboundArtifact(artifacts[artifactIndex]);
|
|
145
|
+
artifactIndex += 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let receipt = null;
|
|
150
|
+
if (receipts.length === 1 && !alwaysMerge) {
|
|
151
|
+
[receipt] = receipts;
|
|
152
|
+
} else if (receipts.length > 0) {
|
|
153
|
+
receipt = mergeDeliveryReceipts({
|
|
154
|
+
deliveryId: deliveryId
|
|
155
|
+
?? baseReceipt?.deliveryId
|
|
156
|
+
?? artifacts[0]?.deliveryKey,
|
|
157
|
+
presentation: aggregatePresentation
|
|
158
|
+
?? `${channelKey}-${baseReceipt ? 'text-and-files' : 'files'}`,
|
|
159
|
+
receipts,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
receipt,
|
|
165
|
+
userVisible,
|
|
166
|
+
failureNoticeVisible,
|
|
167
|
+
artifactsSent,
|
|
168
|
+
artifactSendErrors,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -576,7 +576,7 @@ export function createOutboundArtifactTool({ registry = outboundArtifactRegistry
|
|
|
576
576
|
};
|
|
577
577
|
const definition = Object.freeze({
|
|
578
578
|
name: OUTBOUND_ARTIFACT_TOOL,
|
|
579
|
-
description: 'Send a readable file to the user through the current conversation. Existing and newly created files are both valid.',
|
|
579
|
+
description: 'Send a readable file or generated image to the user through the current conversation. Existing and newly created files are both valid.',
|
|
580
580
|
parameters: {
|
|
581
581
|
type: 'object',
|
|
582
582
|
additionalProperties: false,
|
|
@@ -654,7 +654,7 @@ export function installOutboundArtifactTool(ctx, { registry = outboundArtifactRe
|
|
|
654
654
|
ctx.systemPrompt.section({
|
|
655
655
|
name: 'dsh-im:return-file',
|
|
656
656
|
order: 115,
|
|
657
|
-
text: `When the user asks to receive a file, call ${OUTBOUND_ARTIFACT_TOOL} with its path. Existing files can be sent directly; do not recreate or rename a file solely for delivery.`,
|
|
657
|
+
text: `When the user asks to receive a file or generated image, call ${OUTBOUND_ARTIFACT_TOOL} with its path. Existing files can be sent directly; do not recreate or rename a file solely for delivery.`,
|
|
658
658
|
});
|
|
659
659
|
return true;
|
|
660
660
|
}
|
|
@@ -34,6 +34,8 @@ export function providerMessageIdsFor(value) {
|
|
|
34
34
|
value.message?.ts,
|
|
35
35
|
value.key?.id,
|
|
36
36
|
value.data?.message_id,
|
|
37
|
+
value.body?.msgid,
|
|
38
|
+
value.body?.message_id,
|
|
37
39
|
];
|
|
38
40
|
const id = candidates.find((candidate) => (
|
|
39
41
|
(typeof candidate === 'string' && candidate.trim())
|
|
@@ -32,14 +32,9 @@ import {
|
|
|
32
32
|
harnessQuestionText,
|
|
33
33
|
validHarnessQuestion,
|
|
34
34
|
} from './harness-question.mjs';
|
|
35
|
+
import { deliverOutboundArtifacts } from './semantic/artifact-delivery.mjs';
|
|
35
36
|
import {
|
|
36
|
-
materializeOutboundArtifact,
|
|
37
|
-
releaseOutboundArtifact,
|
|
38
|
-
} from './semantic/artifact.mjs';
|
|
39
|
-
import {
|
|
40
|
-
createArtifactFailureReceipt,
|
|
41
37
|
createDeliveryReceipt,
|
|
42
|
-
mergeDeliveryReceipts,
|
|
43
38
|
providerMessageIdsFor,
|
|
44
39
|
} from './semantic/delivery.mjs';
|
|
45
40
|
|
|
@@ -339,73 +334,29 @@ export class TextHarnessBridge {
|
|
|
339
334
|
}
|
|
340
335
|
|
|
341
336
|
async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt) {
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
} catch (error) {
|
|
366
|
-
if (this.#signal?.aborted) throw error;
|
|
367
|
-
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
|
|
368
|
-
this.#logger.warn?.(
|
|
369
|
-
`[dsh-im:${this.#descriptor.key}] result file delivery failed (${error?.code ?? 'unknown'})`,
|
|
370
|
-
);
|
|
371
|
-
let providerMessageIds = [];
|
|
372
|
-
let noticeSent = false;
|
|
373
|
-
try {
|
|
374
|
-
const notice = await this.#bot.sendText(
|
|
375
|
-
target,
|
|
376
|
-
artifactFailureText(artifact?.fileName, error, this.#descriptor),
|
|
377
|
-
);
|
|
378
|
-
providerMessageIds = providerMessageIdsFor(notice);
|
|
379
|
-
noticeSent = true;
|
|
380
|
-
} catch {
|
|
381
|
-
this.#logger.warn?.(
|
|
382
|
-
`[dsh-im:${this.#descriptor.key}] unable to send the safe result-file failure notice`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
const failureReceipt = createArtifactFailureReceipt({
|
|
386
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
387
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
388
|
-
error,
|
|
389
|
-
providerMessageIds,
|
|
390
|
-
});
|
|
391
|
-
receipts.push(failureReceipt);
|
|
392
|
-
if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
|
|
393
|
-
} finally {
|
|
394
|
-
releaseOutboundArtifact(artifact);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
const receipt = receipts.length === 0
|
|
398
|
-
? null
|
|
399
|
-
: receipts.length === 1
|
|
400
|
-
? receipts[0]
|
|
401
|
-
: mergeDeliveryReceipts({
|
|
402
|
-
deliveryId: replyTo,
|
|
403
|
-
presentation: baseReceipt
|
|
404
|
-
? `${this.#descriptor.key}-text-and-files`
|
|
405
|
-
: `${this.#descriptor.key}-files`,
|
|
406
|
-
receipts,
|
|
407
|
-
});
|
|
408
|
-
return { receipt, userVisible };
|
|
337
|
+
const delivery = await deliverOutboundArtifacts({
|
|
338
|
+
artifacts,
|
|
339
|
+
baseReceipt,
|
|
340
|
+
deliveryId: replyTo,
|
|
341
|
+
channelKey: this.#descriptor.key,
|
|
342
|
+
signal: this.#signal,
|
|
343
|
+
sendImage: typeof this.#bot.sendImage === 'function'
|
|
344
|
+
? (file) => this.#bot.sendImage(target, file)
|
|
345
|
+
: undefined,
|
|
346
|
+
sendFile: typeof this.#bot.sendFile === 'function'
|
|
347
|
+
? (file) => this.#bot.sendFile(target, file)
|
|
348
|
+
: undefined,
|
|
349
|
+
sendFailureNotice: (artifact, error) => this.#bot.sendText(
|
|
350
|
+
target,
|
|
351
|
+
artifactFailureText(artifact?.fileName, error, this.#descriptor),
|
|
352
|
+
),
|
|
353
|
+
logger: this.#logger,
|
|
354
|
+
});
|
|
355
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
356
|
+
+ delivery.artifactsSent;
|
|
357
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
358
|
+
+ delivery.artifactSendErrors;
|
|
359
|
+
return { receipt: delivery.receipt, userVisible: delivery.userVisible };
|
|
409
360
|
}
|
|
410
361
|
|
|
411
362
|
async #process(message, messageId, senderId, conversationKey, {
|
|
@@ -35,33 +35,33 @@ function preserveProviderMetadata(target, source) {
|
|
|
35
35
|
return target;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function telegramArtifactProviderError(cause) {
|
|
38
|
+
function telegramArtifactProviderError(cause, mediaLabel = 'document') {
|
|
39
39
|
const providerCode = Number(cause?.providerCode);
|
|
40
40
|
const status = Number(cause?.status);
|
|
41
41
|
const message = cleanString(cause?.message) ?? '';
|
|
42
42
|
let code = 'artifact-provider-rejected';
|
|
43
|
-
let summary =
|
|
43
|
+
let summary = `Telegram rejected the ${mediaLabel}.`;
|
|
44
44
|
if (providerCode === 401 || providerCode === 403 || status === 401 || status === 403) {
|
|
45
45
|
code = 'artifact-permission-required';
|
|
46
|
-
summary =
|
|
46
|
+
summary = `Telegram denied permission to send the ${mediaLabel}.`;
|
|
47
47
|
} else if (providerCode === 413 || status === 413
|
|
48
48
|
|| /(?:file|request|entity).{0,20}(?:too (?:big|large)|size limit)|too (?:big|large)/i.test(message)) {
|
|
49
49
|
code = 'artifact-too-large';
|
|
50
|
-
summary =
|
|
50
|
+
summary = `The ${mediaLabel} exceeds Telegram's size limit.`;
|
|
51
51
|
} else if (providerCode === 429 || status === 429) {
|
|
52
52
|
code = 'artifact-rate-limited';
|
|
53
|
-
summary =
|
|
53
|
+
summary = `Telegram rate-limited ${mediaLabel} delivery.`;
|
|
54
54
|
} else if (providerCode >= 500 || status >= 500) {
|
|
55
55
|
code = 'artifact-delivery-uncertain';
|
|
56
|
-
summary =
|
|
56
|
+
summary = `Telegram ${mediaLabel} delivery result is uncertain.`;
|
|
57
57
|
}
|
|
58
58
|
const error = new Error(summary, { cause });
|
|
59
59
|
error.code = code;
|
|
60
60
|
return preserveProviderMetadata(error, cause);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function uncertainTelegramDelivery(cause) {
|
|
64
|
-
const error = new Error(
|
|
63
|
+
function uncertainTelegramDelivery(cause, mediaLabel = 'document') {
|
|
64
|
+
const error = new Error(`Telegram ${mediaLabel} delivery result is uncertain`, { cause });
|
|
65
65
|
error.code = 'artifact-delivery-uncertain';
|
|
66
66
|
return preserveProviderMetadata(error, cause);
|
|
67
67
|
}
|
|
@@ -184,15 +184,41 @@ export class TelegramApi {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
async sendDocument({ chatId, file, replyToMessageId, messageThreadId, signal }) {
|
|
187
|
+
return this.#sendArtifact('sendDocument', 'document', 'document', {
|
|
188
|
+
chatId,
|
|
189
|
+
file,
|
|
190
|
+
replyToMessageId,
|
|
191
|
+
messageThreadId,
|
|
192
|
+
signal,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async sendPhoto({ chatId, file, replyToMessageId, messageThreadId, signal }) {
|
|
197
|
+
return this.#sendArtifact('sendPhoto', 'photo', 'photo', {
|
|
198
|
+
chatId,
|
|
199
|
+
file,
|
|
200
|
+
replyToMessageId,
|
|
201
|
+
messageThreadId,
|
|
202
|
+
signal,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async #sendArtifact(method, fieldName, mediaLabel, {
|
|
207
|
+
chatId,
|
|
208
|
+
file,
|
|
209
|
+
replyToMessageId,
|
|
210
|
+
messageThreadId,
|
|
211
|
+
signal,
|
|
212
|
+
}) {
|
|
187
213
|
if (!file || typeof file !== 'object'
|
|
188
214
|
|| typeof file.fileName !== 'string' || !file.fileName
|
|
189
215
|
|| !Buffer.isBuffer(file.bytes)) {
|
|
190
|
-
throw new TypeError(
|
|
216
|
+
throw new TypeError(`A Telegram ${mediaLabel} is required`);
|
|
191
217
|
}
|
|
192
218
|
const payload = new FormData();
|
|
193
219
|
payload.append('chat_id', String(chatId));
|
|
194
220
|
payload.append(
|
|
195
|
-
|
|
221
|
+
fieldName,
|
|
196
222
|
new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
|
|
197
223
|
file.fileName,
|
|
198
224
|
);
|
|
@@ -206,7 +232,7 @@ export class TelegramApi {
|
|
|
206
232
|
if (signal?.aborted) throw abortReason(signal);
|
|
207
233
|
const uploadSignal = requestSignal(signal, this.#fileUploadTimeoutMs);
|
|
208
234
|
try {
|
|
209
|
-
return await this.#call(
|
|
235
|
+
return await this.#call(method, payload, {
|
|
210
236
|
signal: uploadSignal,
|
|
211
237
|
timeoutMs: this.#fileUploadTimeoutMs,
|
|
212
238
|
multipart: true,
|
|
@@ -214,9 +240,9 @@ export class TelegramApi {
|
|
|
214
240
|
} catch (error) {
|
|
215
241
|
if (signal?.aborted) throw abortReason(signal);
|
|
216
242
|
if (error?.code?.startsWith?.('telegram-')) {
|
|
217
|
-
throw telegramArtifactProviderError(error);
|
|
243
|
+
throw telegramArtifactProviderError(error, mediaLabel);
|
|
218
244
|
}
|
|
219
|
-
throw uncertainTelegramDelivery(error);
|
|
245
|
+
throw uncertainTelegramDelivery(error, mediaLabel);
|
|
220
246
|
}
|
|
221
247
|
}
|
|
222
248
|
|
|
@@ -211,6 +211,16 @@ export class TelegramBotClient {
|
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
sendImage(target, file) {
|
|
215
|
+
return this.#api.sendPhoto({
|
|
216
|
+
chatId: target.chatId,
|
|
217
|
+
file,
|
|
218
|
+
replyToMessageId: target.replyToMessageId,
|
|
219
|
+
messageThreadId: target.messageThreadId,
|
|
220
|
+
signal: this.#signal,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
214
224
|
async openStream(target) {
|
|
215
225
|
const stream = createEditableMessageStream({
|
|
216
226
|
limit: 4_000,
|
|
@@ -31,15 +31,10 @@ import {
|
|
|
31
31
|
inboundFileUserMessage,
|
|
32
32
|
} from '../shared/inbound-file.mjs';
|
|
33
33
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
34
|
+
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
|
35
|
+
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
34
36
|
import {
|
|
35
|
-
materializeOutboundArtifact,
|
|
36
|
-
releaseOutboundArtifact,
|
|
37
|
-
trackOutboundArtifactProviderPromise,
|
|
38
|
-
} from '../shared/semantic/artifact.mjs';
|
|
39
|
-
import {
|
|
40
|
-
createArtifactFailureReceipt,
|
|
41
37
|
createDeliveryReceipt,
|
|
42
|
-
mergeDeliveryReceipts,
|
|
43
38
|
} from '../shared/semantic/delivery.mjs';
|
|
44
39
|
|
|
45
40
|
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
@@ -353,6 +348,71 @@ function wecomArtifactError(error, { dispatched = false } = {}) {
|
|
|
353
348
|
return wrapped;
|
|
354
349
|
}
|
|
355
350
|
|
|
351
|
+
async function sendWecomMedia(
|
|
352
|
+
client,
|
|
353
|
+
chatId,
|
|
354
|
+
file,
|
|
355
|
+
mediaType,
|
|
356
|
+
{ signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
|
|
357
|
+
) {
|
|
358
|
+
signal?.throwIfAborted();
|
|
359
|
+
if (typeof client?.uploadMedia !== 'function'
|
|
360
|
+
|| typeof client?.sendMediaMessage !== 'function') {
|
|
361
|
+
const unavailable = new Error(`Enterprise WeChat ${mediaType} delivery is unavailable`);
|
|
362
|
+
unavailable.code = 'artifact-provider-unavailable';
|
|
363
|
+
throw unavailable;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
367
|
+
const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
368
|
+
let uploaded;
|
|
369
|
+
try {
|
|
370
|
+
const pending = client.uploadMedia(file.bytes, {
|
|
371
|
+
type: mediaType,
|
|
372
|
+
filename: file.fileName,
|
|
373
|
+
});
|
|
374
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
375
|
+
uploaded = await waitWithSignal(pending, waitSignal);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
378
|
+
throw wecomArtifactError(error);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
signal?.throwIfAborted();
|
|
382
|
+
const mediaId = nonEmptyString(uploaded?.media_id);
|
|
383
|
+
if (!mediaId) {
|
|
384
|
+
const rejected = new Error(`Enterprise WeChat ${mediaType} upload returned no media id`);
|
|
385
|
+
rejected.code = 'artifact-provider-rejected';
|
|
386
|
+
throw rejected;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let sent;
|
|
390
|
+
try {
|
|
391
|
+
const pending = client.sendMediaMessage(chatId, mediaType, mediaId);
|
|
392
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
393
|
+
sent = await waitWithSignal(pending, waitSignal);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
396
|
+
throw wecomArtifactError(error, { dispatched: true });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
signal?.throwIfAborted();
|
|
400
|
+
const providerCode = Number(sent?.body?.errcode ?? sent?.errcode);
|
|
401
|
+
if (Number.isFinite(providerCode) && providerCode !== 0) {
|
|
402
|
+
throw wecomArtifactError({ providerCode });
|
|
403
|
+
}
|
|
404
|
+
return sent;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Send one materialized artifact through Enterprise WeChat's native image message. */
|
|
408
|
+
export function sendWecomImage(client, chatId, file, options) {
|
|
409
|
+
return sendWecomMedia(client, chatId, file, 'image', options);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function sendWecomFile(client, chatId, file, options) {
|
|
413
|
+
return sendWecomMedia(client, chatId, file, 'file', options);
|
|
414
|
+
}
|
|
415
|
+
|
|
356
416
|
function answerTextForDelivery(answer, artifacts) {
|
|
357
417
|
if (typeof answer === 'string' && answer.trim()) return answer;
|
|
358
418
|
return artifacts.length > 0 ? '结果文件已生成。' : '任务已完成,但没有生成可显示的文本。';
|
|
@@ -650,98 +710,35 @@ export class WecomHarnessBridge {
|
|
|
650
710
|
if (artifacts.length === 0) {
|
|
651
711
|
return { receipt: baseReceipt, failureNoticeVisible: false };
|
|
652
712
|
}
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const file = await materializeOutboundArtifact(artifact, {
|
|
665
|
-
signal: this.#signal,
|
|
666
|
-
});
|
|
667
|
-
this.#signal?.throwIfAborted();
|
|
668
|
-
const timeout = AbortSignal.timeout(this.#fileUploadTimeoutMs);
|
|
669
|
-
const waitSignal = this.#signal ? AbortSignal.any([this.#signal, timeout]) : timeout;
|
|
670
|
-
let uploaded;
|
|
671
|
-
try {
|
|
672
|
-
const pending = this.#client.uploadMedia(file.bytes, {
|
|
673
|
-
type: 'file',
|
|
674
|
-
filename: file.fileName,
|
|
675
|
-
});
|
|
676
|
-
trackOutboundArtifactProviderPromise(file, pending);
|
|
677
|
-
uploaded = await waitWithSignal(pending, waitSignal);
|
|
678
|
-
} catch (error) {
|
|
679
|
-
if (this.#signal?.aborted) throw abortReason(this.#signal);
|
|
680
|
-
throw wecomArtifactError(error);
|
|
681
|
-
}
|
|
682
|
-
this.#signal?.throwIfAborted();
|
|
683
|
-
const mediaId = nonEmptyString(uploaded?.media_id);
|
|
684
|
-
if (!mediaId) {
|
|
685
|
-
const rejected = new Error('Enterprise WeChat upload returned no media id');
|
|
686
|
-
rejected.code = 'artifact-provider-rejected';
|
|
687
|
-
throw rejected;
|
|
688
|
-
}
|
|
689
|
-
let sent;
|
|
690
|
-
try {
|
|
691
|
-
const pending = this.#client.sendMediaMessage(chatId, 'file', mediaId);
|
|
692
|
-
trackOutboundArtifactProviderPromise(file, pending);
|
|
693
|
-
sent = await waitWithSignal(pending, waitSignal);
|
|
694
|
-
} catch (error) {
|
|
695
|
-
if (this.#signal?.aborted) throw abortReason(this.#signal);
|
|
696
|
-
throw wecomArtifactError(error, { dispatched: true });
|
|
697
|
-
}
|
|
698
|
-
this.#signal?.throwIfAborted();
|
|
699
|
-
const providerCode = Number(sent?.body?.errcode ?? sent?.errcode);
|
|
700
|
-
if (Number.isFinite(providerCode) && providerCode !== 0) {
|
|
701
|
-
throw wecomArtifactError({ providerCode });
|
|
702
|
-
}
|
|
703
|
-
const messageId = providerMessageId(sent);
|
|
704
|
-
receipts.push(createDeliveryReceipt({
|
|
705
|
-
deliveryId: file.deliveryKey,
|
|
706
|
-
presentation: 'wecom-file',
|
|
707
|
-
providerMessageIds: messageId ? [messageId] : [],
|
|
708
|
-
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
|
|
709
|
-
}));
|
|
710
|
-
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
|
|
711
|
-
} catch (error) {
|
|
712
|
-
if (this.#signal?.aborted) throw error;
|
|
713
|
-
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
|
|
714
|
-
this.#logger.warn?.(
|
|
715
|
-
`[dsh-im:wecom] result file delivery failed (${error?.code ?? 'unknown'})`,
|
|
716
|
-
);
|
|
717
|
-
let providerMessageIds = [];
|
|
718
|
-
try {
|
|
719
|
-
providerMessageIds = await this.#sendActive(
|
|
720
|
-
chatId,
|
|
721
|
-
artifactFailureText(artifact?.fileName, error),
|
|
722
|
-
);
|
|
723
|
-
failureNoticeVisible = true;
|
|
724
|
-
} catch (noticeError) {
|
|
725
|
-
if (this.#signal?.aborted) throw noticeError;
|
|
726
|
-
this.#logger.warn?.('[dsh-im:wecom] unable to send the safe result-file failure notice');
|
|
727
|
-
}
|
|
728
|
-
receipts.push(createArtifactFailureReceipt({
|
|
729
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
730
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
731
|
-
error,
|
|
732
|
-
providerMessageIds,
|
|
733
|
-
}));
|
|
734
|
-
} finally {
|
|
735
|
-
releaseOutboundArtifact(artifact);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
return {
|
|
739
|
-
receipt: mergeDeliveryReceipts({
|
|
740
|
-
deliveryId: replyTo,
|
|
741
|
-
presentation: baseReceipt ? 'wecom-text-and-files' : 'wecom-files',
|
|
742
|
-
receipts,
|
|
713
|
+
const delivery = await deliverOutboundArtifacts({
|
|
714
|
+
artifacts,
|
|
715
|
+
baseReceipt,
|
|
716
|
+
deliveryId: replyTo,
|
|
717
|
+
aggregatePresentation: baseReceipt ? 'wecom-text-and-files' : 'wecom-files',
|
|
718
|
+
alwaysMerge: true,
|
|
719
|
+
channelKey: 'wecom',
|
|
720
|
+
signal: this.#signal,
|
|
721
|
+
sendImage: (file) => sendWecomImage(this.#client, chatId, file, {
|
|
722
|
+
signal: this.#signal,
|
|
723
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
743
724
|
}),
|
|
744
|
-
|
|
725
|
+
sendFile: (file) => sendWecomFile(this.#client, chatId, file, {
|
|
726
|
+
signal: this.#signal,
|
|
727
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
728
|
+
}),
|
|
729
|
+
sendFailureNotice: (artifact, error) => this.#sendActive(
|
|
730
|
+
chatId,
|
|
731
|
+
artifactFailureText(artifact?.fileName, error),
|
|
732
|
+
),
|
|
733
|
+
logger: this.#logger,
|
|
734
|
+
});
|
|
735
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
736
|
+
+ delivery.artifactsSent;
|
|
737
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
738
|
+
+ delivery.artifactSendErrors;
|
|
739
|
+
return {
|
|
740
|
+
receipt: delivery.receipt,
|
|
741
|
+
failureNoticeVisible: delivery.failureNoticeVisible,
|
|
745
742
|
};
|
|
746
743
|
}
|
|
747
744
|
|