@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.
@@ -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
- return Buffer.concat([cipher.update(bytes), cipher.final()]);
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, ciphertext, { signal } = {}) {
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
- const response = await fetchImpl(url, {
383
+ response = await fetchImpl(url, {
353
384
  method: 'POST',
354
- headers: { 'content-type': 'application/octet-stream' },
355
- body: ciphertext,
356
- signal: signal
357
- ? AbortSignal.any([signal, AbortSignal.timeout(60_000)])
358
- : AbortSignal.timeout(60_000),
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 ciphertext = encryptWeixinUpload(file.bytes, aesKey);
566
+ const ciphertextSize = aesEcbPaddedSize(file.bytes.byteLength);
528
567
  let downloadParam;
529
568
  try {
530
- downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl, ciphertext, { signal });
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: ciphertext.byteLength })],
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 { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
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 },