@xmanrui/dsh-im 4.21.0 → 4.21.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/lib/client.js +9 -1
- package/lib/index.js +281 -280
- package/package.json +1 -1
- package/plugin-src/host/index.mjs +7 -0
- package/plugin-src/host/injected-context.mjs +104 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +104 -27
- package/src/channels/feishu/bridge.mjs +35 -2
- package/src/channels/qq/qq-bridge.mjs +27 -2
- package/src/channels/shared/context-enhancement.mjs +40 -3
- package/src/channels/shared/control-command.mjs +8 -1
- package/src/channels/shared/harness-client.mjs +7 -0
- package/src/channels/shared/i18n-en/dingtalk.mjs +1 -0
- package/src/channels/shared/i18n-en/weixin.mjs +2 -0
- package/src/channels/shared/im-source-guidance.mjs +65 -0
- package/src/channels/shared/injected-context.mjs +362 -0
- package/src/channels/shared/semantic/artifact.mjs +3 -3
- package/src/channels/shared/semantic/reply-reference.mjs +2 -1
- package/src/channels/shared/text-harness-bridge.mjs +23 -2
- package/src/channels/shared/workspace-session.mjs +9 -0
- package/src/channels/wecom/wecom-bridge.mjs +11 -1
- package/src/channels/wecom-app/wecom-app-bridge.mjs +11 -1
- package/src/channels/weixin/weixin-api.mjs +52 -13
- package/src/channels/weixin/weixin-bridge.mjs +13 -1
|
@@ -21,6 +21,8 @@ const ILINK_CLIENT_VERSION = (2 << 16) | (4 << 8) | 6;
|
|
|
21
21
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
22
22
|
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
23
23
|
const WEIXIN_CDN_UPLOAD_RETRIES = 3;
|
|
24
|
+
const WEIXIN_CDN_UPLOAD_IDLE_TIMEOUT_MS = 60_000;
|
|
25
|
+
const WEIXIN_CDN_UPLOAD_CHUNK_BYTES = 64 * 1024;
|
|
24
26
|
const WEIXIN_MESSAGE_ID_TIMESTAMP_SHIFT = 22n;
|
|
25
27
|
const WEIXIN_MESSAGE_ID_MIN_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
|
|
26
28
|
const WEIXIN_MESSAGE_ID_MAX_FUTURE_MS = 24 * 60 * 60 * 1_000;
|
|
@@ -79,6 +81,9 @@ function weixinArtifactError(cause, { fallback = 'artifact-provider-rejected' }
|
|
|
79
81
|
|| /(?:rate.?limit|too.?many)/i.test(providerText)) {
|
|
80
82
|
code = 'artifact-rate-limited';
|
|
81
83
|
message = 'Weixin rate-limited file delivery.';
|
|
84
|
+
} else if (cause?.code === 'upload-timeout') {
|
|
85
|
+
code = 'artifact-upload-timeout';
|
|
86
|
+
message = 'Weixin file upload stalled; the file message was not sent.';
|
|
82
87
|
} else if (fallback === 'artifact-provider-rejected') {
|
|
83
88
|
message = 'Weixin rejected the file message.';
|
|
84
89
|
}
|
|
@@ -339,25 +344,54 @@ function weixinCdnUploadUrl(response, fileKey) {
|
|
|
339
344
|
return trustedWeixinCdnUploadUrl(url);
|
|
340
345
|
}
|
|
341
346
|
|
|
342
|
-
function encryptWeixinUpload(bytes, key) {
|
|
347
|
+
async function* encryptWeixinUpload(bytes, key, { signal, onProgress }) {
|
|
343
348
|
const cipher = createCipheriv('aes-128-ecb', key, null);
|
|
344
|
-
|
|
349
|
+
// Let fetch backpressure drive encryption, without keeping whole-file
|
|
350
|
+
// ciphertext copies alongside a potentially large artifact buffer.
|
|
351
|
+
for (let offset = 0; offset < bytes.byteLength; offset += WEIXIN_CDN_UPLOAD_CHUNK_BYTES) {
|
|
352
|
+
signal.throwIfAborted();
|
|
353
|
+
const chunk = cipher.update(bytes.subarray(offset, offset + WEIXIN_CDN_UPLOAD_CHUNK_BYTES));
|
|
354
|
+
onProgress();
|
|
355
|
+
if (chunk.byteLength) yield chunk;
|
|
356
|
+
}
|
|
357
|
+
signal.throwIfAborted();
|
|
358
|
+
onProgress();
|
|
359
|
+
yield cipher.final();
|
|
345
360
|
}
|
|
346
361
|
|
|
347
|
-
async function uploadWeixinCdn(fetchImpl, url,
|
|
362
|
+
async function uploadWeixinCdn(fetchImpl, url, bytes, key, { signal } = {}) {
|
|
348
363
|
let lastError;
|
|
349
364
|
for (let attempt = 1; attempt <= WEIXIN_CDN_UPLOAD_RETRIES; attempt += 1) {
|
|
350
365
|
signal?.throwIfAborted();
|
|
366
|
+
const idleController = new AbortController();
|
|
367
|
+
const uploadSignal = signal
|
|
368
|
+
? AbortSignal.any([signal, idleController.signal])
|
|
369
|
+
: idleController.signal;
|
|
370
|
+
let timer;
|
|
371
|
+
let active = true;
|
|
372
|
+
const onProgress = () => {
|
|
373
|
+
if (!active) return;
|
|
374
|
+
clearTimeout(timer);
|
|
375
|
+
timer = setTimeout(() => idleController.abort(new WeixinApiError(
|
|
376
|
+
'upload-timeout', '微信文件上传长时间没有进展,已超时。',
|
|
377
|
+
)), WEIXIN_CDN_UPLOAD_IDLE_TIMEOUT_MS);
|
|
378
|
+
};
|
|
379
|
+
const body = encryptWeixinUpload(bytes, key, { signal: uploadSignal, onProgress });
|
|
380
|
+
let response;
|
|
381
|
+
onProgress();
|
|
351
382
|
try {
|
|
352
|
-
|
|
383
|
+
response = await fetchImpl(url, {
|
|
353
384
|
method: 'POST',
|
|
354
|
-
headers: {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
385
|
+
headers: {
|
|
386
|
+
'content-type': 'application/octet-stream',
|
|
387
|
+
'content-length': String(aesEcbPaddedSize(bytes.byteLength)),
|
|
388
|
+
},
|
|
389
|
+
body,
|
|
390
|
+
duplex: 'half',
|
|
391
|
+
signal: uploadSignal,
|
|
359
392
|
redirect: 'error',
|
|
360
393
|
});
|
|
394
|
+
uploadSignal.throwIfAborted();
|
|
361
395
|
if (response.status >= 400 && response.status < 500) {
|
|
362
396
|
throw new WeixinApiError(
|
|
363
397
|
'upload-rejected',
|
|
@@ -373,16 +407,21 @@ async function uploadWeixinCdn(fetchImpl, url, ciphertext, { signal } = {}) {
|
|
|
373
407
|
);
|
|
374
408
|
}
|
|
375
409
|
const downloadParam = nonEmptyString(response.headers.get('x-encrypted-param'));
|
|
376
|
-
await response.body?.cancel?.().catch(() => undefined);
|
|
377
410
|
if (!downloadParam) {
|
|
378
411
|
throw new WeixinApiError('invalid-upload-response', '微信文件上传响应缺少下载参数。');
|
|
379
412
|
}
|
|
380
413
|
return downloadParam;
|
|
381
414
|
} catch (error) {
|
|
382
415
|
if (signal?.aborted) throw abortError(signal);
|
|
416
|
+
if (idleController.signal.aborted) error = idleController.signal.reason;
|
|
383
417
|
if (error instanceof WeixinApiError
|
|
384
418
|
&& (error.code === 'upload-rejected' || error.status < 500)) throw error;
|
|
385
419
|
lastError = error;
|
|
420
|
+
} finally {
|
|
421
|
+
active = false;
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
await body.return();
|
|
424
|
+
await response?.body?.cancel?.().catch(() => undefined);
|
|
386
425
|
}
|
|
387
426
|
}
|
|
388
427
|
if (lastError instanceof WeixinApiError) throw lastError;
|
|
@@ -524,10 +563,10 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
524
563
|
));
|
|
525
564
|
}
|
|
526
565
|
const uploadUrl = weixinCdnUploadUrl(upload, fileKey);
|
|
527
|
-
const
|
|
566
|
+
const ciphertextSize = aesEcbPaddedSize(file.bytes.byteLength);
|
|
528
567
|
let downloadParam;
|
|
529
568
|
try {
|
|
530
|
-
downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl,
|
|
569
|
+
downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl, file.bytes, aesKey, { signal });
|
|
531
570
|
} catch (error) {
|
|
532
571
|
if (signal?.aborted) throw abortError(signal);
|
|
533
572
|
const status = Number(error?.status);
|
|
@@ -564,7 +603,7 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
564
603
|
client_id: clientId,
|
|
565
604
|
message_type: 2,
|
|
566
605
|
message_state: 2,
|
|
567
|
-
item_list: [createItem({ file, media, ciphertextSize
|
|
606
|
+
item_list: [createItem({ file, media, ciphertextSize })],
|
|
568
607
|
...(nonEmptyString(contextToken) ? { context_token: contextToken.trim() } : {}),
|
|
569
608
|
...(nonEmptyString(runId) ? { run_id: runId.trim() } : {}),
|
|
570
609
|
},
|
|
@@ -36,7 +36,11 @@ import {
|
|
|
36
36
|
} from '../shared/preset-command.mjs';
|
|
37
37
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
38
38
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
captureContextEnhancement,
|
|
41
|
+
captureContextEnhancementSource,
|
|
42
|
+
enhanceContextContent,
|
|
43
|
+
} from '../shared/context-enhancement.mjs';
|
|
40
44
|
import {
|
|
41
45
|
hasInboundImages,
|
|
42
46
|
imagePromptDiagnostic,
|
|
@@ -262,6 +266,8 @@ function artifactFailureText(fileName, error) {
|
|
|
262
266
|
return t('结果文件「{name}」已生成,但微信机器人当前没有文件消息发送权限,请检查机器人文件消息能力。', { name });
|
|
263
267
|
case 'artifact-too-large':
|
|
264
268
|
return t('结果文件「{name}」超过当前微信会话可发送的文件大小,未发送。', { name });
|
|
269
|
+
case 'artifact-upload-timeout':
|
|
270
|
+
return t('结果文件「{name}」上传微信时长时间没有进展,已超时,文件尚未发送。请检查网络后重试,或压缩、拆分文件后发送。', { name });
|
|
265
271
|
case 'artifact-rate-limited':
|
|
266
272
|
return t('结果文件「{name}」暂时被微信限流,未能发送,请稍后重试。', { name });
|
|
267
273
|
case 'artifact-provider-rejected':
|
|
@@ -707,6 +713,11 @@ export class WeixinHarnessBridge {
|
|
|
707
713
|
|| this.#approvals.hasPending(key),
|
|
708
714
|
control: { owner: this, key },
|
|
709
715
|
deferredDelivery: this.#deferred,
|
|
716
|
+
enhancement: captureContextEnhancementSource(
|
|
717
|
+
this.#contextEnhancement,
|
|
718
|
+
'direct',
|
|
719
|
+
() => ({ channel: 'weixin', senderId: sender, chatId: sender }),
|
|
720
|
+
),
|
|
710
721
|
});
|
|
711
722
|
if (result?.stopped) {
|
|
712
723
|
await Promise.allSettled([
|
|
@@ -826,6 +837,7 @@ export class WeixinHarnessBridge {
|
|
|
826
837
|
text,
|
|
827
838
|
content,
|
|
828
839
|
titleText: batchSubmission?.title,
|
|
840
|
+
sourceGuidance: snapshot?.config?.guidance,
|
|
829
841
|
contextEnhanced,
|
|
830
842
|
createOptions: { signal: this.#signal },
|
|
831
843
|
existsOptions: { signal: this.#signal },
|