@senguoyun/dsh-arkme 0.1.30 → 0.1.31

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.
Files changed (101) hide show
  1. package/docs/consumer-plugin-contract.md +35 -1
  2. package/docs/file-flow-parity.md +213 -0
  3. package/docs/file-icon-licenses.md +54 -0
  4. package/docs/group-ai-polish-member-enablement.md +49 -0
  5. package/lib/client.js +4989 -1057
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +402 -25
  8. package/lib/persistent-extension.js +1 -1
  9. package/lib/{persistent-runtime-D-NwFt9z.js → persistent-runtime-BJ8konXI.js} +1424 -95
  10. package/lib/sdk.js +154 -0
  11. package/lib/types/arkme-service.d.ts +39 -1
  12. package/lib/types/arkme-service.d.ts.map +1 -1
  13. package/lib/types/chat-realtime.d.ts +7 -1
  14. package/lib/types/chat-realtime.d.ts.map +1 -1
  15. package/lib/types/client/ArkmeAttachmentStrip.d.ts +11 -0
  16. package/lib/types/client/ArkmeAttachmentStrip.d.ts.map +1 -0
  17. package/lib/types/client/ArkmeBillingSettings.d.ts.map +1 -1
  18. package/lib/types/client/ArkmeBotConversationSurface.d.ts.map +1 -1
  19. package/lib/types/client/ArkmeFileIcon.d.ts +24 -0
  20. package/lib/types/client/ArkmeFileIcon.d.ts.map +1 -0
  21. package/lib/types/client/ArkmeFileQuickView.d.ts +6 -0
  22. package/lib/types/client/ArkmeFileQuickView.d.ts.map +1 -0
  23. package/lib/types/client/ArkmeFileViewer.d.ts +22 -0
  24. package/lib/types/client/ArkmeFileViewer.d.ts.map +1 -0
  25. package/lib/types/client/ArkmeFooterAction.d.ts +3 -1
  26. package/lib/types/client/ArkmeFooterAction.d.ts.map +1 -1
  27. package/lib/types/client/ArkmeFooterDropdown.d.ts.map +1 -1
  28. package/lib/types/client/ArkmeGroupChatControls.d.ts +3 -1
  29. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  30. package/lib/types/client/ArkmeRichContent.d.ts +13 -3
  31. package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
  32. package/lib/types/client/ArkmeSearchSurface.d.ts.map +1 -1
  33. package/lib/types/client/ArkmeSettingsSurface.d.ts +5 -1
  34. package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -1
  35. package/lib/types/client/ArkmeSidebar.d.ts +1 -0
  36. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  37. package/lib/types/client/ArkmeUpdateSurfaces.d.ts +5 -39
  38. package/lib/types/client/ArkmeUpdateSurfaces.d.ts.map +1 -1
  39. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  40. package/lib/types/client/composer-draft-store.d.ts +25 -2
  41. package/lib/types/client/composer-draft-store.d.ts.map +1 -1
  42. package/lib/types/client/file-send-tasks.d.ts +12 -0
  43. package/lib/types/client/file-send-tasks.d.ts.map +1 -0
  44. package/lib/types/client/plugin-update-store.d.ts +28 -1
  45. package/lib/types/client/plugin-update-store.d.ts.map +1 -1
  46. package/lib/types/client/read-retry.d.ts +7 -0
  47. package/lib/types/client/read-retry.d.ts.map +1 -0
  48. package/lib/types/client/ui-controller.d.ts.map +1 -1
  49. package/lib/types/client/update-presentation.d.ts +42 -0
  50. package/lib/types/client/update-presentation.d.ts.map +1 -0
  51. package/lib/types/file-transfer-contract.d.ts +63 -0
  52. package/lib/types/file-transfer-contract.d.ts.map +1 -0
  53. package/lib/types/file-transfer-owner.d.ts +17 -0
  54. package/lib/types/file-transfer-owner.d.ts.map +1 -0
  55. package/lib/types/harness-embed-route.d.ts.map +1 -1
  56. package/lib/types/host-api.d.ts.map +1 -1
  57. package/lib/types/index.d.ts.map +1 -1
  58. package/lib/types/plugin-update.d.ts.map +1 -1
  59. package/lib/types/rich-media-routes.d.ts +2 -1
  60. package/lib/types/rich-media-routes.d.ts.map +1 -1
  61. package/lib/types/runtime-instance.d.ts +8 -0
  62. package/lib/types/runtime-instance.d.ts.map +1 -0
  63. package/lib/types/sdk/index.d.ts +50 -2
  64. package/lib/types/sdk/index.d.ts.map +1 -1
  65. package/lib/types/services/bot-service.d.ts +15 -1
  66. package/lib/types/services/bot-service.d.ts.map +1 -1
  67. package/lib/types/services/chat-realtime-service.d.ts +1 -1
  68. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  69. package/lib/types/services/chat-service.d.ts +2 -0
  70. package/lib/types/services/chat-service.d.ts.map +1 -1
  71. package/lib/types/services/file-transfers.d.ts +65 -0
  72. package/lib/types/services/file-transfers.d.ts.map +1 -0
  73. package/lib/types/services/group-ai-polish-service.d.ts +17 -2
  74. package/lib/types/services/group-ai-polish-service.d.ts.map +1 -1
  75. package/lib/types/services/media-service.d.ts +11 -1
  76. package/lib/types/services/media-service.d.ts.map +1 -1
  77. package/lib/types/services/profile-service.d.ts +1 -0
  78. package/lib/types/services/profile-service.d.ts.map +1 -1
  79. package/lib/types/services/search-service.d.ts +6 -0
  80. package/lib/types/services/search-service.d.ts.map +1 -1
  81. package/lib/types/services/service.d.ts +1 -0
  82. package/lib/types/services/service.d.ts.map +1 -1
  83. package/lib/types/services/source-service.d.ts +2 -0
  84. package/lib/types/services/source-service.d.ts.map +1 -1
  85. package/lib/types/tools/business/conversation/group-ai-polish.d.ts.map +1 -1
  86. package/lib/types/tools/business/index.d.ts.map +1 -1
  87. package/lib/types/tools/business/media/files.d.ts +2 -0
  88. package/lib/types/tools/business/media/files.d.ts.map +1 -0
  89. package/lib/types/tools/business/media/index.d.ts.map +1 -1
  90. package/lib/types/tools/ports/bots.d.ts +1 -1
  91. package/lib/types/tools/ports/bots.d.ts.map +1 -1
  92. package/lib/types/tools/ports/conversations.d.ts +3 -0
  93. package/lib/types/tools/ports/conversations.d.ts.map +1 -1
  94. package/lib/types/tools/ports/files.d.ts +22 -0
  95. package/lib/types/tools/ports/files.d.ts.map +1 -0
  96. package/lib/types/tools/ports/index.d.ts +2 -1
  97. package/lib/types/tools/ports/index.d.ts.map +1 -1
  98. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  99. package/lib/types/types.d.ts +43 -2
  100. package/lib/types/types.d.ts.map +1 -1
  101. package/package.json +5 -5
@@ -2,9 +2,10 @@ import { chmodSync, createReadStream, existsSync, lstatSync, mkdirSync, readFile
2
2
  import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicKey, randomBytes, randomUUID, timingSafeEqual, verify } from "node:crypto";
3
3
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { open } from "node:fs/promises";
5
+ import { chmod as chmod$1, copyFile, link, mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import { pinyin } from "pinyin-pro";
7
7
  import OSS from "ali-oss";
8
+ import { Readable } from "node:stream";
8
9
  import { gunzipSync, gzipSync } from "node:zlib";
9
10
  import { createContext, runInContext } from "node:vm";
10
11
  import { Context } from "@deepseek-ai/cordis";
@@ -3445,6 +3446,258 @@ function arkmeMentionMetadataMentionsViewer(record, payload, viewerUserId) {
3445
3446
  return false;
3446
3447
  }
3447
3448
  //#endregion
3449
+ //#region src/file-transfer-contract.ts
3450
+ const ARKME_TOOL_FILE_MAX_BYTES = 64 * 1024;
3451
+ const mediaTypesByExtension = {
3452
+ jpg: {
3453
+ mimeType: "image/jpeg",
3454
+ kind: "image"
3455
+ },
3456
+ jpeg: {
3457
+ mimeType: "image/jpeg",
3458
+ kind: "image"
3459
+ },
3460
+ png: {
3461
+ mimeType: "image/png",
3462
+ kind: "image"
3463
+ },
3464
+ gif: {
3465
+ mimeType: "image/gif",
3466
+ kind: "image"
3467
+ },
3468
+ webp: {
3469
+ mimeType: "image/webp",
3470
+ kind: "image"
3471
+ },
3472
+ avif: {
3473
+ mimeType: "image/avif",
3474
+ kind: "image"
3475
+ },
3476
+ bmp: {
3477
+ mimeType: "image/bmp",
3478
+ kind: "image"
3479
+ },
3480
+ heic: {
3481
+ mimeType: "image/heic",
3482
+ kind: "image"
3483
+ },
3484
+ heif: {
3485
+ mimeType: "image/heif",
3486
+ kind: "image"
3487
+ },
3488
+ svg: {
3489
+ mimeType: "image/svg+xml",
3490
+ kind: "image"
3491
+ },
3492
+ tif: {
3493
+ mimeType: "image/tiff",
3494
+ kind: "image"
3495
+ },
3496
+ tiff: {
3497
+ mimeType: "image/tiff",
3498
+ kind: "image"
3499
+ },
3500
+ mp4: {
3501
+ mimeType: "video/mp4",
3502
+ kind: "video"
3503
+ },
3504
+ m4v: {
3505
+ mimeType: "video/mp4",
3506
+ kind: "video"
3507
+ },
3508
+ mov: {
3509
+ mimeType: "video/quicktime",
3510
+ kind: "video"
3511
+ },
3512
+ webm: {
3513
+ mimeType: "video/webm",
3514
+ kind: "video"
3515
+ },
3516
+ ogv: {
3517
+ mimeType: "video/ogg",
3518
+ kind: "video"
3519
+ },
3520
+ mkv: {
3521
+ mimeType: "video/x-matroska",
3522
+ kind: "video"
3523
+ },
3524
+ avi: {
3525
+ mimeType: "video/x-msvideo",
3526
+ kind: "video"
3527
+ },
3528
+ mpg: {
3529
+ mimeType: "video/mpeg",
3530
+ kind: "video"
3531
+ },
3532
+ mpeg: {
3533
+ mimeType: "video/mpeg",
3534
+ kind: "video"
3535
+ },
3536
+ mp3: {
3537
+ mimeType: "audio/mpeg",
3538
+ kind: "audio"
3539
+ },
3540
+ m4a: {
3541
+ mimeType: "audio/mp4",
3542
+ kind: "audio"
3543
+ },
3544
+ aac: {
3545
+ mimeType: "audio/aac",
3546
+ kind: "audio"
3547
+ },
3548
+ wav: {
3549
+ mimeType: "audio/wav",
3550
+ kind: "audio"
3551
+ },
3552
+ ogg: {
3553
+ mimeType: "audio/ogg",
3554
+ kind: "audio"
3555
+ },
3556
+ oga: {
3557
+ mimeType: "audio/ogg",
3558
+ kind: "audio"
3559
+ },
3560
+ flac: {
3561
+ mimeType: "audio/flac",
3562
+ kind: "audio"
3563
+ },
3564
+ opus: {
3565
+ mimeType: "audio/opus",
3566
+ kind: "audio"
3567
+ }
3568
+ };
3569
+ const safeVisualTypesByExtension = {
3570
+ jpg: {
3571
+ mimeType: "image/jpeg",
3572
+ kind: "image"
3573
+ },
3574
+ jpeg: {
3575
+ mimeType: "image/jpeg",
3576
+ kind: "image"
3577
+ },
3578
+ png: {
3579
+ mimeType: "image/png",
3580
+ kind: "image"
3581
+ },
3582
+ gif: {
3583
+ mimeType: "image/gif",
3584
+ kind: "image"
3585
+ },
3586
+ webp: {
3587
+ mimeType: "image/webp",
3588
+ kind: "image"
3589
+ },
3590
+ avif: {
3591
+ mimeType: "image/avif",
3592
+ kind: "image"
3593
+ },
3594
+ bmp: {
3595
+ mimeType: "image/bmp",
3596
+ kind: "image"
3597
+ },
3598
+ mp4: {
3599
+ mimeType: "video/mp4",
3600
+ kind: "video"
3601
+ },
3602
+ m4v: {
3603
+ mimeType: "video/mp4",
3604
+ kind: "video"
3605
+ },
3606
+ mov: {
3607
+ mimeType: "video/quicktime",
3608
+ kind: "video"
3609
+ },
3610
+ webm: {
3611
+ mimeType: "video/webm",
3612
+ kind: "video"
3613
+ },
3614
+ ogv: {
3615
+ mimeType: "video/ogg",
3616
+ kind: "video"
3617
+ }
3618
+ };
3619
+ const safeImageMimeTypes = /* @__PURE__ */ new Set([
3620
+ "image/jpeg",
3621
+ "image/png",
3622
+ "image/gif",
3623
+ "image/webp",
3624
+ "image/avif",
3625
+ "image/bmp"
3626
+ ]);
3627
+ const safeVideoMimeTypes = /* @__PURE__ */ new Set([
3628
+ "video/mp4",
3629
+ "video/x-m4v",
3630
+ "video/quicktime",
3631
+ "video/webm",
3632
+ "video/ogg"
3633
+ ]);
3634
+ const safeAudioMimeTypes = /* @__PURE__ */ new Set([
3635
+ "audio/mpeg",
3636
+ "audio/mp4",
3637
+ "audio/wav",
3638
+ "audio/ogg"
3639
+ ]);
3640
+ function fileExtension(fileName) {
3641
+ return /\.([A-Za-z0-9]+)$/u.exec(fileName.trim())?.[1]?.toLowerCase();
3642
+ }
3643
+ /** Recover the real media MIME when the platform supplied no useful metadata. */
3644
+ function arkmeNormalizedFileMimeType(mimeType, fileName = "") {
3645
+ const declared = mimeType.trim().toLowerCase();
3646
+ if (declared !== "" && declared !== "application/octet-stream") return declared;
3647
+ const extension = fileExtension(fileName);
3648
+ return (extension === void 0 ? void 0 : mediaTypesByExtension[extension]?.mimeType) ?? (declared || "application/octet-stream");
3649
+ }
3650
+ /** Classify presentation independently from whether this browser can decode it inline. */
3651
+ function arkmeMediaKind(mimeType, fileName = "") {
3652
+ const declared = mimeType.trim().toLowerCase();
3653
+ const extension = fileExtension(fileName);
3654
+ if (extension !== void 0) {
3655
+ const byExtension = mediaTypesByExtension[extension];
3656
+ if (byExtension === void 0) return void 0;
3657
+ if (declared === "" || declared === "application/octet-stream") return byExtension.kind;
3658
+ if (byExtension.kind === "image" && declared.startsWith("image/")) return "image";
3659
+ if (byExtension.kind === "video" && declared.startsWith("video/")) return "video";
3660
+ if (byExtension.kind === "audio" && declared.startsWith("audio/")) return "audio";
3661
+ return;
3662
+ }
3663
+ if (declared.startsWith("image/")) return "image";
3664
+ if (declared.startsWith("video/")) return "video";
3665
+ if (declared.startsWith("audio/")) return "audio";
3666
+ }
3667
+ /** A filename suffix is authoritative when it contradicts legacy visual metadata. */
3668
+ function arkmeBrowserVisualKind(mimeType, fileName = "") {
3669
+ const declared = mimeType.trim().toLowerCase();
3670
+ const extension = fileExtension(fileName);
3671
+ if (extension !== void 0) {
3672
+ const byExtension = safeVisualTypesByExtension[extension];
3673
+ if (byExtension === void 0) return void 0;
3674
+ if (declared === "" || declared === "application/octet-stream") return byExtension.kind;
3675
+ if (byExtension.kind === "image" && safeImageMimeTypes.has(declared)) return "image";
3676
+ if (byExtension.kind === "video" && safeVideoMimeTypes.has(declared)) return "video";
3677
+ return;
3678
+ }
3679
+ if (safeImageMimeTypes.has(declared)) return "image";
3680
+ if (safeVideoMimeTypes.has(declared)) return "video";
3681
+ }
3682
+ function arkmeCanInlineLocalFile(mimeType, fileName = "") {
3683
+ if (arkmeBrowserVisualKind(mimeType, fileName) !== void 0) return true;
3684
+ const declared = mimeType.trim().toLowerCase();
3685
+ const extension = fileExtension(fileName);
3686
+ if (!safeAudioMimeTypes.has(declared)) return false;
3687
+ return extension === void 0 || [
3688
+ "mp3",
3689
+ "m4a",
3690
+ "wav",
3691
+ "ogg",
3692
+ "oga"
3693
+ ].includes(extension);
3694
+ }
3695
+ /** Picked audio keeps generic upload metadata; presentation still recognizes it as audio. */
3696
+ function arkmePickedFileKind(mimeType, fileName = "") {
3697
+ const mediaKind = arkmeMediaKind(arkmeNormalizedFileMimeType(mimeType, fileName), fileName);
3698
+ return mediaKind === "image" ? 1 : mediaKind === "video" ? 3 : 4;
3699
+ }
3700
+ //#endregion
3448
3701
  //#region src/services/source-service.ts
3449
3702
  const SOURCE_LIST_CACHE_TTL_MS = 3e4;
3450
3703
  const SOURCE_LIST_CACHE_MAX_ENTRIES = 200;
@@ -3539,17 +3792,47 @@ function chatMessageDnd$1(value) {
3539
3792
  const policy = value;
3540
3793
  return numberValue$20(policy.mute_state) === 2 || numberValue$20(policy.notify_state) === 2;
3541
3794
  }
3542
- function textPreview(raw) {
3795
+ function attachmentPreviewKind(item) {
3796
+ const fileName = stringValue$1(item.file_name ?? item.fileName).trim();
3797
+ const detectedKind = arkmeMediaKind(stringValue$1(item.mime_type ?? item.mimeType).trim(), fileName);
3798
+ if (detectedKind !== void 0) return detectedKind;
3799
+ const fileKind = integerLikeValue$2(item.file_kind ?? item.fileKind);
3800
+ if (fileKind === 1) return "image";
3801
+ if (fileKind === 2) return "audio";
3802
+ if (fileKind === 3) return "video";
3803
+ return "file";
3804
+ }
3805
+ function arkmeChatConversationPreview(raw) {
3543
3806
  const direct = stringValue$1(raw.text_content ?? raw.title ?? raw.summary).trim();
3544
3807
  if (direct !== "") return direct.slice(0, 300);
3545
3808
  const content = objectValue$1(raw.content_payload ?? raw.payload);
3546
3809
  const nested = stringValue$1(content.text_content ?? content.title ?? content.summary).trim();
3547
3810
  if (nested !== "") return nested.slice(0, 300);
3548
3811
  if (objectValue$1(content.voice).duration !== void 0) return "[语音]";
3549
- if (listValue$21(content.media_refs).length > 0 || listValue$21(raw.media_display_items).length > 0) return "[图片]";
3812
+ const displayItems = listValue$21(raw.media_display_items ?? raw.mediaDisplayItems).map(objectValue$1);
3813
+ const displayByAsset = /* @__PURE__ */ new Map();
3814
+ for (const item of displayItems) {
3815
+ const fileAssetUid = stringValue$1(item.file_asset_uid ?? item.fileAssetUid).trim();
3816
+ if (fileAssetUid !== "") displayByAsset.set(fileAssetUid, item);
3817
+ }
3818
+ const mediaRefs = listValue$21(content.media_refs ?? content.mediaRefs).map(objectValue$1);
3819
+ const firstAttachment = (mediaRefs.length > 0 ? mediaRefs.map((ref) => ({
3820
+ ...displayByAsset.get(stringValue$1(ref.file_asset_uid ?? ref.fileAssetUid).trim()) ?? {},
3821
+ ...ref
3822
+ })) : displayItems).filter((item) => integerLikeValue$2(item.content_file_role ?? item.contentFileRole) !== 4).sort((left, right) => integerLikeValue$2(left.sort_order ?? left.sortOrder) - integerLikeValue$2(right.sort_order ?? right.sortOrder))[0];
3823
+ if (firstAttachment !== void 0) {
3824
+ const kind = attachmentPreviewKind(firstAttachment);
3825
+ return kind === "image" ? "[图片]" : kind === "video" ? "[视频]" : kind === "audio" ? "[语音]" : "[文件]";
3826
+ }
3550
3827
  if (Object.keys(objectValue$1(content.structured_anchor)).length > 0) return "[卡片]";
3551
3828
  return "";
3552
3829
  }
3830
+ function arkmeTimelineConversationPreview(item) {
3831
+ const text = item.textContent.trim() || item.title.trim();
3832
+ if (text !== "") return text;
3833
+ const kind = item.contentBlocks?.[0]?.kind;
3834
+ return kind === "image" ? "[图片]" : kind === "video" ? "[视频]" : kind === "audio" ? "[语音]" : kind === "file" ? "[文件]" : "非文本内容";
3835
+ }
3553
3836
  function chunksOf$1(values, size) {
3554
3837
  const chunks = [];
3555
3838
  for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
@@ -4262,7 +4545,7 @@ var SourceService = class {
4262
4545
  ...parentTopicUid === "" || parentTopicUid === topicUid ? {} : { parentTopicUid },
4263
4546
  siblingOrder: numberValue$20(siblingOrderByChild.get(topicUid) ?? core.sibling_order ?? item.sibling_order),
4264
4547
  title,
4265
- latestPreview: textPreview(latest),
4548
+ latestPreview: arkmeChatConversationPreview(latest),
4266
4549
  latestMessageAtMillis: numberValue$20(latest.send_at ?? summary.latest_send_at),
4267
4550
  activeAtMillis: numberValue$20(latest.send_at ?? summary.latest_send_at ?? core.update_at),
4268
4551
  recordCount: numberValue$20(summary.record_count)
@@ -4360,7 +4643,7 @@ var SourceService = class {
4360
4643
  const kind = sessionKind === 2 ? "group_chat" : sessionKind === 1 || sessionKind === 3 ? "private_chat" : void 0;
4361
4644
  if (uid === "" || kind === void 0) continue;
4362
4645
  const displayName = (kind === "private_chat" ? stringValue$1(supplement.remark ?? supplement.counterpart_name_snapshot ?? counterpart.display_name_snapshot ?? supplement.pending_name ?? counterpart.visible_phone) : stringValue$1(chatSession.title)).trim() || "未命名会话";
4363
- const preview = textPreview(latestPayload);
4646
+ const preview = arkmeChatConversationPreview(latestPayload);
4364
4647
  const unreadCount = Math.max(0, Math.trunc(numberValue$20(unread.unread_count)));
4365
4648
  const latestRelation = objectValue$1(latestPreview.relation);
4366
4649
  const latestPreviewMentionsViewer = integerLikeValue$2(latestRelation.sender_user_id ?? latestRelation.senderUserId) !== session.userId && arkmeMentionMetadataMentionsViewer(latestRecord, latestPayload, session.userId);
@@ -4596,7 +4879,7 @@ var SourceService = class {
4596
4879
  if (uid === "" || kind === void 0) throw new Error("invalid chat display snapshot");
4597
4880
  const displayName = (kind === "private_chat" ? stringValue$1(supplement.remark ?? supplement.counterpart_name_snapshot ?? counterpart.display_name_snapshot ?? supplement.pending_name ?? counterpart.visible_phone) : stringValue$1(chatSession.title)).trim() || cached?.displayName || "未命名会话";
4598
4881
  const latestItem = [...timelineItems].sort((left, right) => (right.sequence ?? 0) - (left.sequence ?? 0))[0];
4599
- const latestPreview = latestItem === void 0 ? cached?.latestPreview : latestItem.forwardRecords === void 0 ? latestItem.textContent || latestItem.title || "非文本内容" : `[转发] ${latestItem.forwardRecords.title}`;
4882
+ const latestPreview = latestItem === void 0 ? cached?.latestPreview : latestItem.forwardRecords === void 0 ? arkmeTimelineConversationPreview(latestItem) : `[转发] ${latestItem.forwardRecords.title}`;
4600
4883
  const latestSequence = Math.max(numberValue$20(unread.session_last_seq ?? chatSession.last_seq), latestItem?.sequence ?? 0, cached?.latestSequence ?? 0);
4601
4884
  const unreadCount = Math.max(0, Math.trunc(numberValue$20(unread.unread_count)));
4602
4885
  const latestMentionsViewer = latestItem?.isMe === false && latestItem.mentionsViewer === true;
@@ -4658,6 +4941,11 @@ var SourceService = class {
4658
4941
  };
4659
4942
  //#endregion
4660
4943
  //#region src/services/bot-service.ts
4944
+ const BOT_CONVERSATION_OWNER = {
4945
+ subject: "jotmo-subject",
4946
+ chat: "jotmo-chat",
4947
+ unavailable: "unavailable"
4948
+ };
4661
4949
  const BOT_REF_TTL_MILLIS = 30 * 6e4;
4662
4950
  const BOT_REF_CAP = 2e3;
4663
4951
  const BOT_REF_PATTERN = /^arkme-bot-v2\.[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -4741,15 +5029,64 @@ function botProfileSubjectUid(data) {
4741
5029
  const bot = botProfileSource(data);
4742
5030
  return stringValue$1(bot.subject_uid ?? bot.topic_uid ?? data.subject_uid ?? data.topic_uid).trim();
4743
5031
  }
5032
+ function botConversationOwner(data) {
5033
+ const hasSubjectOwner = stringValue$1(data.subject_uid).trim() !== "";
5034
+ if (hasSubjectOwner === (stringValue$1(data.chat_session_uid).trim() !== "")) return BOT_CONVERSATION_OWNER.unavailable;
5035
+ return hasSubjectOwner ? BOT_CONVERSATION_OWNER.subject : BOT_CONVERSATION_OWNER.chat;
5036
+ }
5037
+ function botConversationCapabilities(owner, provider) {
5038
+ return {
5039
+ directChatAvailable: owner !== BOT_CONVERSATION_OWNER.unavailable,
5040
+ privateChatOutboundEnabled: owner === BOT_CONVERSATION_OWNER.chat || owner === BOT_CONVERSATION_OWNER.subject && provider === "openclaw",
5041
+ refreshOnRecordChanges: owner === BOT_CONVERSATION_OWNER.subject
5042
+ };
5043
+ }
5044
+ function botPrivateChatAttachments(value) {
5045
+ return listValue$20(value).map(objectValue$1).flatMap((raw) => {
5046
+ const kind = stringValue$1(raw.kind).trim();
5047
+ const fileName = stringValue$1(raw.file_name ?? raw.fileName).trim();
5048
+ const mimeType = stringValue$1(raw.mime_type ?? raw.mimeType).trim();
5049
+ const hasSourceFile = stringValue$1(raw.file_id ?? raw.fileId).trim() !== "";
5050
+ if (kind === "" && fileName === "" && mimeType === "" && !hasSourceFile) return [];
5051
+ return [{
5052
+ kind: kind || "file",
5053
+ fileName,
5054
+ mimeType,
5055
+ size: Math.max(0, Math.trunc(numberValue$19(raw.size))),
5056
+ durationMillis: Math.max(0, Math.trunc(numberValue$19(raw.duration_ms ?? raw.durationMillis))),
5057
+ width: Math.max(0, Math.trunc(numberValue$19(raw.width))),
5058
+ height: Math.max(0, Math.trunc(numberValue$19(raw.height))),
5059
+ sortOrder: Math.max(0, Math.trunc(numberValue$19(raw.order ?? raw.sort_order ?? raw.sortOrder)))
5060
+ }];
5061
+ });
5062
+ }
4744
5063
  function botPrivateChatMessage(value, fallbackContent = "") {
4745
5064
  const raw = objectValue$1(value);
5065
+ const role = stringValue$1(raw.role).trim().toLowerCase() === "user" ? "user" : "assistant";
5066
+ const recordUid = stringValue$1(raw.record_uid ?? raw.recordUid).trim();
5067
+ const content = stringValue$1(raw.content);
4746
5068
  return {
4747
- role: stringValue$1(raw.role).trim().toLowerCase() === "user" ? "user" : "assistant",
4748
- content: stringValue$1(raw.content).trim() || fallbackContent,
5069
+ messageId: stringValue$1(raw.message_id ?? raw.messageId).trim(),
5070
+ ...recordUid === "" ? {} : { recordUid },
5071
+ role,
5072
+ content: content === "" ? fallbackContent : content,
4749
5073
  status: stringValue$1(raw.status).trim() || "sent",
4750
- createdAtMillis: botPrivateChatTimestamp(raw.created_at ?? raw.createdAt)
5074
+ createdAtMillis: botPrivateChatTimestamp(raw.created_at ?? raw.createdAt),
5075
+ attachments: botPrivateChatAttachments(raw.attachments)
4751
5076
  };
4752
5077
  }
5078
+ function dedupeBotPrivateChatMessages(messages) {
5079
+ const seenMessageIds = /* @__PURE__ */ new Set();
5080
+ const result = [];
5081
+ for (const message of messages) {
5082
+ if (message.messageId !== "") {
5083
+ if (seenMessageIds.has(message.messageId)) continue;
5084
+ seenMessageIds.add(message.messageId);
5085
+ }
5086
+ result.push(message);
5087
+ }
5088
+ return result;
5089
+ }
4753
5090
  function botWithPrivateChatActivity(bot, messages) {
4754
5091
  const latest = messages.reduce((current, message) => {
4755
5092
  if (current === void 0 || message.createdAtMillis >= current.createdAtMillis) return message;
@@ -4954,7 +5291,8 @@ var BotService = class {
4954
5291
  }
4955
5292
  async openBotPrivateChatForSummary(bot, session, signal) {
4956
5293
  const reference = await this.openBotRef(bot.botRef, session.userId);
4957
- const messages = listValue$20((await this.runtime.authenticatedBotPost("/api/v1/bot/private-chat/open", { bot_id: reference.botId }, session, signal)).messages).map((message) => botPrivateChatMessage(message));
5294
+ if (reference.conversationOwner === BOT_CONVERSATION_OWNER.unavailable) throw new ArkmePluginError("bot-private-chat-owner-unavailable", "当前 Bot 私聊归属信息不可用,请刷新后重试", false, 409);
5295
+ const messages = dedupeBotPrivateChatMessages(listValue$20((await this.runtime.authenticatedBotPost("/api/v1/bot/private-chat/open", { bot_id: reference.botId }, session, signal)).messages).map((message) => botPrivateChatMessage(message)));
4958
5296
  return {
4959
5297
  bot: botWithPrivateChatActivity(bot, messages),
4960
5298
  messages
@@ -4966,22 +5304,40 @@ var BotService = class {
4966
5304
  if (content.length > 2e4) throw new ArkmePluginError("bot-private-chat-content-invalid", "消息不能超过 20000 个字符", false, 400);
4967
5305
  const session = await this.runtime.requireSession();
4968
5306
  const reference = await this.openBotRef(botRef, session.userId);
4969
- const data = await this.runtime.authenticatedBotPost("/api/v1/bot/private-chat/message/send", {
4970
- bot_id: reference.botId,
4971
- content
4972
- }, session, options.signal);
4973
- const botMessages = [...listValue$20(data.bot_messages).map((message) => botPrivateChatMessage(message)), ...Object.keys(objectValue$1(data.bot_message)).length === 0 ? [] : [botPrivateChatMessage(data.bot_message)]];
5307
+ if (reference.conversationOwner === BOT_CONVERSATION_OWNER.unavailable) throw new ArkmePluginError("bot-private-chat-owner-unavailable", "当前 Bot 私聊归属信息不可用,请刷新后重试", false, 409);
5308
+ if (reference.conversationOwner === BOT_CONVERSATION_OWNER.subject && reference.provider === "webhook") throw new ArkmePluginError("bot-private-chat-send-unsupported", "Webhook Bot 仅接收外部系统推送", false, 400);
5309
+ let data;
5310
+ try {
5311
+ data = await this.runtime.authenticatedBotPost("/api/v1/bot/private-chat/message/send", {
5312
+ bot_id: reference.botId,
5313
+ content
5314
+ }, session, options.signal);
5315
+ } catch (error) {
5316
+ if (reference.conversationOwner === BOT_CONVERSATION_OWNER.subject && error instanceof ArkmePluginError && (error.retryable || [
5317
+ "arkme-network-error",
5318
+ "arkme-timeout",
5319
+ "arkme-response-invalid"
5320
+ ].includes(error.code))) throw new ArkmePluginError("bot-private-chat-send-outcome-unknown", "消息发送结果未知,请刷新会话确认;不会自动重试", false, 409, { cause: error });
5321
+ throw error;
5322
+ }
5323
+ if (reference.conversationOwner === BOT_CONVERSATION_OWNER.subject && stringValue$1(objectValue$1(data.user_message).message_id).trim() === "") throw new ArkmePluginError("bot-private-chat-send-outcome-unknown", "消息可能已发送,但响应缺少确认标识;请刷新会话确认", false, 409);
5324
+ const botMessages = dedupeBotPrivateChatMessages([...listValue$20(data.bot_messages).map((message) => botPrivateChatMessage(message)), ...Object.keys(objectValue$1(data.bot_message)).length === 0 ? [] : [botPrivateChatMessage(data.bot_message)]]);
4974
5325
  return {
4975
- userMessage: botPrivateChatMessage(data.user_message, content),
4976
- botMessages,
4977
- status: stringValue$1(data.status).trim() || "ok"
5326
+ recordProjectionChanged: reference.conversationOwner === BOT_CONVERSATION_OWNER.subject,
5327
+ result: {
5328
+ userMessage: botPrivateChatMessage(data.user_message, content),
5329
+ botMessages,
5330
+ status: stringValue$1(data.status).trim() || "ok"
5331
+ }
4978
5332
  };
4979
5333
  }
4980
5334
  async openBotChat(botRef, options = {}) {
4981
5335
  const session = await this.runtime.requireSession();
4982
- const reference = await this.openBotRef(botRef, session.userId);
5336
+ await this.openBotRef(botRef, session.userId);
4983
5337
  const bot = (await this.listBots(options)).items.find((item) => item.botRef === botRef);
4984
5338
  if (bot === void 0) throw new ArkmePluginError("bot-ref-not-owned", "当前账号不存在该 OpenClaw Bot", false, 404);
5339
+ const reference = await this.openBotRef(botRef, session.userId);
5340
+ if (reference.conversationOwner !== BOT_CONVERSATION_OWNER.chat) throw new ArkmePluginError("bot-chat-source-unavailable", "当前 Bot 不属于 Chat 私聊链路", false, 409);
4985
5341
  const data = await this.runtime.authenticatedBotPost("/api/v1/bot/private-chat/open", { bot_id: reference.botId }, session, options.signal);
4986
5342
  const chatSessionUid = botChatSessionUid(data);
4987
5343
  if (chatSessionUid !== void 0) {
@@ -5030,7 +5386,7 @@ var BotService = class {
5030
5386
  for (const value of listValue$20(data.bots)) {
5031
5387
  const raw = objectValue$1(value);
5032
5388
  if (arkmeNormalizeBotProvider(raw.provider) !== "openclaw") continue;
5033
- const { directChatAvailable: _directChatAvailable, ...summary } = await this.botSummaryFromData(raw, session.userId);
5389
+ const summary = await this.groupBotSummaryFromData(raw, session.userId);
5034
5390
  items.push({
5035
5391
  ...summary,
5036
5392
  installed: booleanValue$10(raw.installed)
@@ -5104,7 +5460,7 @@ var BotService = class {
5104
5460
  botGroupTarget
5105
5461
  });
5106
5462
  }
5107
- async botSummaryFromData(raw, userId) {
5463
+ async botSummaryBaseFromData(raw, userId, conversationOwner) {
5108
5464
  const botId = stringValue$1(raw.bot_id).trim();
5109
5465
  const name = stringValue$1(raw.name).trim();
5110
5466
  const provider = arkmeNormalizeBotProvider(raw.provider);
@@ -5113,17 +5469,27 @@ var BotService = class {
5113
5469
  const status = rawStatus === "online" || rawStatus === "offline" ? rawStatus : "unknown";
5114
5470
  const createdAtMillis = botPrivateChatTimestamp(raw.created_at ?? raw.createdAt);
5115
5471
  return {
5116
- botRef: this.sealBotRef(userId, botId, provider),
5472
+ botRef: this.sealBotRef(userId, botId, provider, conversationOwner),
5117
5473
  directoryKey: await this.botDirectoryKey(userId, botId),
5118
5474
  name,
5119
5475
  provider,
5120
5476
  description: stringValue$1(raw.description).trim(),
5121
5477
  status,
5122
- directChatAvailable: stringValue$1(raw.subject_uid).trim() !== "" || stringValue$1(raw.chat_session_uid).trim() !== "",
5123
5478
  ...createdAtMillis === 0 ? {} : { createdAtMillis },
5124
5479
  ...this.botAvatarProjection(raw, userId, botId)
5125
5480
  };
5126
5481
  }
5482
+ async botSummaryFromData(raw, userId) {
5483
+ const conversationOwner = botConversationOwner(raw);
5484
+ const summary = await this.botSummaryBaseFromData(raw, userId, conversationOwner);
5485
+ return {
5486
+ ...summary,
5487
+ ...botConversationCapabilities(conversationOwner, summary.provider)
5488
+ };
5489
+ }
5490
+ async groupBotSummaryFromData(raw, userId) {
5491
+ return await this.botSummaryBaseFromData(raw, userId, void 0);
5492
+ }
5127
5493
  async botManageProfileFromData(data, userId) {
5128
5494
  const raw = botProfileSource(data);
5129
5495
  const summary = await this.botSummaryFromData(raw, userId);
@@ -5199,7 +5565,7 @@ var BotService = class {
5199
5565
  expiresAtMillis: entry.expiresAtMillis
5200
5566
  };
5201
5567
  }
5202
- sealBotRef(userId, botId, provider) {
5568
+ sealBotRef(userId, botId, provider, conversationOwner) {
5203
5569
  this.pruneBotRefs();
5204
5570
  const key = `${String(userId)}\u0000${provider}\u0000${botId}`;
5205
5571
  const existingRef = this.botRefByKey.get(key);
@@ -5207,6 +5573,7 @@ var BotService = class {
5207
5573
  if (existing !== void 0) {
5208
5574
  this.botRefs.set(existingRef, {
5209
5575
  ...existing,
5576
+ ...conversationOwner === void 0 ? {} : { conversationOwner },
5210
5577
  expiresAtMillis: this.now() + (this.refOptions.ttlMillis ?? BOT_REF_TTL_MILLIS)
5211
5578
  });
5212
5579
  return existingRef;
@@ -5217,6 +5584,7 @@ var BotService = class {
5217
5584
  userId,
5218
5585
  botId,
5219
5586
  provider,
5587
+ conversationOwner: conversationOwner ?? BOT_CONVERSATION_OWNER.unavailable,
5220
5588
  key,
5221
5589
  expiresAtMillis: this.now() + (this.refOptions.ttlMillis ?? BOT_REF_TTL_MILLIS)
5222
5590
  });
@@ -6265,8 +6633,18 @@ var CallHistoryService = class {
6265
6633
  }
6266
6634
  };
6267
6635
  //#endregion
6636
+ //#region src/runtime-instance.ts
6637
+ /**
6638
+ * Arkme Host 进程生命周期身份。
6639
+ *
6640
+ * 模块只在当前 Node 进程内生成一次;SSE 重连与 provider.instance 共用该值。
6641
+ * 该值不持久化,也不参与用户、账号或设备鉴权。
6642
+ */
6643
+ const ARKME_RUNTIME_INSTANCE_ID = randomUUID();
6644
+ //#endregion
6268
6645
  //#region src/chat-realtime.ts
6269
6646
  const ARKME_CHAT_SSE_PATH = "/api/v1/sse/chat/noty";
6647
+ const ARKME_RUNTIME_INSTANCE_ID_HEADER = "X-Jotmo-Runtime-Instance-Id";
6270
6648
  const DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
6271
6649
  const DEFAULT_INACTIVITY_TIMEOUT_MS = 75e3;
6272
6650
  const DEFAULT_LEASE_DURATION_MS = 57e4;
@@ -6276,6 +6654,19 @@ const DEFAULT_MAX_RETRY_MS = 15e3;
6276
6654
  const DEFAULT_STABLE_CONNECTION_MS = 3e4;
6277
6655
  const MAX_SEEN_EVENTS = 256;
6278
6656
  var ArkmeChatRealtimeAuthError = class extends Error {};
6657
+ var ArkmeChatRealtimeRetryAfterError = class extends Error {
6658
+ retryAfterMillis;
6659
+ constructor(retryAfterMillis) {
6660
+ super("chat SSE capacity retry requested");
6661
+ this.retryAfterMillis = retryAfterMillis;
6662
+ this.name = "ArkmeChatRealtimeRetryAfterError";
6663
+ }
6664
+ };
6665
+ function capacityRetryAfterMillis(value) {
6666
+ if (value === null || value.trim() === "") return void 0;
6667
+ const seconds = Number(value.trim());
6668
+ return Number.isFinite(seconds) && seconds >= 0 ? Math.min(6e4, Math.round(seconds * 1e3)) : void 0;
6669
+ }
6279
6670
  function positiveInteger(value) {
6280
6671
  const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : NaN;
6281
6672
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
@@ -6429,6 +6820,7 @@ var ArkmeChatRealtimeRuntime = class {
6429
6820
  connectionGeneration = 0;
6430
6821
  forceReconnectRequested = false;
6431
6822
  lastEventAtMillis;
6823
+ lastIdentityVersion = "";
6432
6824
  seenEventUids = /* @__PURE__ */ new Set();
6433
6825
  listeners = /* @__PURE__ */ new Set();
6434
6826
  constructor(options) {
@@ -6508,6 +6900,7 @@ var ArkmeChatRealtimeRuntime = class {
6508
6900
  }
6509
6901
  blockedAccessToken = void 0;
6510
6902
  let stableConnection = false;
6903
+ let retryAfterMillis = 0;
6511
6904
  this.options.diagnostic?.("reconnect_attempt", { connectionGeneration: this.connectionGeneration + 1 });
6512
6905
  try {
6513
6906
  await this.consume(session, signal);
@@ -6519,6 +6912,7 @@ var ArkmeChatRealtimeRuntime = class {
6519
6912
  stableConnection = this.lastConnectionLifetimeMs >= this.stableConnectionMs;
6520
6913
  if (stableConnection) retryMs = this.retryBaseMs;
6521
6914
  if (this.lastAcceptedAccessToken === session.accessToken) refreshedButUnacceptedToken = void 0;
6915
+ if (error instanceof ArkmeChatRealtimeRetryAfterError) retryAfterMillis = error.retryAfterMillis;
6522
6916
  if (error instanceof ArkmeChatRealtimeAuthError) {
6523
6917
  if (refreshedButUnacceptedToken === session.accessToken) {
6524
6918
  blockedAccessToken = session.accessToken;
@@ -6551,7 +6945,12 @@ var ArkmeChatRealtimeRuntime = class {
6551
6945
  retryMs = this.retryBaseMs;
6552
6946
  continue;
6553
6947
  }
6554
- await this.wait(this.jitteredDelay(retryMs), signal);
6948
+ await this.wait(Math.max(this.jitteredDelay(retryMs), retryAfterMillis), signal);
6949
+ if (this.forceReconnectRequested) {
6950
+ this.forceReconnectRequested = false;
6951
+ retryMs = this.retryBaseMs;
6952
+ continue;
6953
+ }
6555
6954
  if (!stableConnection) retryMs = Math.min(this.maxRetryMs, Math.max(this.retryBaseMs, retryMs * 2));
6556
6955
  }
6557
6956
  }
@@ -6572,18 +6971,29 @@ var ArkmeChatRealtimeRuntime = class {
6572
6971
  "Accept-Language": "zh-CN",
6573
6972
  Authorization: `Bearer ${session.accessToken}`,
6574
6973
  "Content-Type": "application/json",
6974
+ [ARKME_RUNTIME_INSTANCE_ID_HEADER]: ARKME_RUNTIME_INSTANCE_ID,
6575
6975
  Usersource: "3"
6576
6976
  },
6577
6977
  body: "{}",
6578
6978
  signal: controller.signal
6579
6979
  });
6580
6980
  clearTimeout(connectTimer);
6581
- if (response.status === 401 || response.status === 403) throw new ArkmeChatRealtimeAuthError();
6582
- if (!response.ok || response.body === null) throw new Error(`chat SSE returned HTTP ${String(response.status)}`);
6981
+ if (response.status === 401 || response.status === 403) {
6982
+ await response.body?.cancel().catch(() => void 0);
6983
+ throw new ArkmeChatRealtimeAuthError();
6984
+ }
6985
+ if (!response.ok) {
6986
+ const retryAfterMillis = response.status === 429 ? capacityRetryAfterMillis(response.headers.get("retry-after")) : void 0;
6987
+ await response.body?.cancel().catch(() => void 0);
6988
+ if (retryAfterMillis !== void 0) throw new ArkmeChatRealtimeRetryAfterError(retryAfterMillis);
6989
+ throw new Error(`chat SSE returned HTTP ${String(response.status)}`);
6990
+ }
6991
+ if (response.body === null) throw new Error(`chat SSE returned HTTP ${String(response.status)}`);
6583
6992
  if (!(response.headers.get("content-type")?.toLowerCase() ?? "").includes("text/event-stream")) {
6584
6993
  await response.body.cancel().catch(() => void 0);
6585
6994
  throw new Error("chat SSE returned a non-event-stream response");
6586
6995
  }
6996
+ this.reportIdentityCapability(response);
6587
6997
  acceptedAtMillis = this.now();
6588
6998
  this.connected = true;
6589
6999
  this.lastAcceptedAccessToken = session.accessToken;
@@ -6612,6 +7022,15 @@ var ArkmeChatRealtimeRuntime = class {
6612
7022
  if (this.connectionController === controller) this.connectionController = void 0;
6613
7023
  }
6614
7024
  }
7025
+ reportIdentityCapability(response) {
7026
+ const identityVersion = response.headers.get("X-Jotmo-SSE-Identity-Version")?.trim() || "legacy";
7027
+ if (identityVersion === this.lastIdentityVersion) return;
7028
+ this.lastIdentityVersion = identityVersion;
7029
+ this.options.diagnostic?.("sse_identity_capability", {
7030
+ connectionGeneration: this.connectionGeneration + 1,
7031
+ identityVersion
7032
+ });
7033
+ }
6615
7034
  jitteredDelay(milliseconds) {
6616
7035
  const random = Math.min(1, Math.max(0, this.random()));
6617
7036
  return Math.max(0, Math.round(milliseconds * (.8 + random * .4)));
@@ -8674,6 +9093,8 @@ var ChatService = class {
8674
9093
  async sendSourceRich(sourceRef, input, options = {}) {
8675
9094
  if (this.runtime.config.richMediaSendEnabled === false) throw new ArkmePluginError("rich-content-disabled", "富内容发送已被插件配置关闭", false, 403);
8676
9095
  const session = await this.runtime.requireSession();
9096
+ if (options.expectedUserId !== void 0 && options.expectedUserId !== session.userId) throw new ArkmePluginError("file-account-changed", "账号已切换", false, 403);
9097
+ options.signal?.throwIfAborted();
8677
9098
  const source = await this.source.openSourceRef(sourceRef, session.userId);
8678
9099
  const title = input.title?.trim() ?? "";
8679
9100
  const textContent = input.textContent?.trim() ?? "";
@@ -8702,7 +9123,10 @@ var ChatService = class {
8702
9123
  content_file_role: 1,
8703
9124
  render_role: 1,
8704
9125
  sort_order: index,
8705
- file_name: asset.fileName
9126
+ file_name: asset.fileName,
9127
+ file_kind: asset.fileKind,
9128
+ mime_type: asset.mimeType,
9129
+ size: asset.size
8706
9130
  }))
8707
9131
  };
8708
9132
  let contentPayload = mediaContentPayload;
@@ -8710,7 +9134,7 @@ var ChatService = class {
8710
9134
  if ((input.humanMentions?.length ?? 0) > 0 && source.kind !== "group_chat") throw new ArkmePluginError("mention-group-required", "真人 mention 只能发送到群聊", false);
8711
9135
  if ((input.botMentions?.length ?? 0) > 0 && source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("mention-chat-required", "Bot mention 只能发送到聊天", false);
8712
9136
  if (longArticle) throw new ArkmePluginError("mention-rich-invalid", "长文暂不支持 mention", false);
8713
- const mentionPayload = await this.humanMentionContentPayload(source, input.textContent ?? "", textContent, input.humanMentions ?? [], session, void 0, input.botMentions ?? []);
9137
+ const mentionPayload = await this.humanMentionContentPayload(source, input.textContent ?? "", textContent, input.humanMentions ?? [], session, options.signal, input.botMentions ?? []);
8714
9138
  contentPayload = {
8715
9139
  ...mediaContentPayload ?? {},
8716
9140
  ...mentionPayload
@@ -8727,7 +9151,7 @@ var ChatService = class {
8727
9151
  send_at: Date.now()
8728
9152
  };
8729
9153
  if (source.kind === "send_to_self" || source.kind === "default_category") {
8730
- const result = await this.runtime.authenticatedPost("/api/v1/records/create", commonBody, session);
9154
+ const result = await this.runtime.authenticatedPost("/api/v1/records/create", commonBody, session, options.signal);
8731
9155
  this.source.invalidateSourceListCache(session.userId, "send_to_self");
8732
9156
  return {
8733
9157
  sourceRef,
@@ -8740,7 +9164,7 @@ var ChatService = class {
8740
9164
  const result = await this.runtime.authenticatedPost("/api/v1/topics/records/create", {
8741
9165
  topic_uid: source.ownerRef,
8742
9166
  ...commonBody
8743
- }, session);
9167
+ }, session, options.signal);
8744
9168
  this.source.invalidateSourceListCache(session.userId, "send_to_self");
8745
9169
  return {
8746
9170
  sourceRef,
@@ -8753,7 +9177,7 @@ var ChatService = class {
8753
9177
  chat_session_uid: source.ownerRef,
8754
9178
  rel_uid: relationUid,
8755
9179
  ...commonBody
8756
- }, session);
9180
+ }, session, options.signal);
8757
9181
  const sequence = numberValue$16(result.seq);
8758
9182
  this.realtime.scheduleChatSessionProjection(source.ownerRef, sequence);
8759
9183
  return {
@@ -10372,6 +10796,37 @@ function booleanValue$4(value) {
10372
10796
  function listValue$13(value) {
10373
10797
  return Array.isArray(value) ? value : [];
10374
10798
  }
10799
+ function aiPolishThreadMessages(value) {
10800
+ let totalCharacters = 0;
10801
+ const messages = [];
10802
+ for (const raw of listValue$13(value).slice(-40)) {
10803
+ const item = objectValue$1(raw);
10804
+ const id = stringValue$1(item.id).trim();
10805
+ const role = stringValue$1(item.role).trim();
10806
+ const text = stringValue$1(item.text).trim().slice(0, 4e3);
10807
+ if (id === "" || role !== "ai" && role !== "user" || text === "") continue;
10808
+ totalCharacters += [...text].length;
10809
+ if (totalCharacters > 2e4) break;
10810
+ const ruleRef = stringValue$1(item.rule_uid ?? item.ruleRef).trim();
10811
+ messages.push({
10812
+ id,
10813
+ role,
10814
+ text,
10815
+ ...item.is_rule === true || item.isRule === true ? { isRule: true } : {},
10816
+ ...ruleRef === "" ? {} : { ruleRef }
10817
+ });
10818
+ }
10819
+ return messages;
10820
+ }
10821
+ function serializeAiPolishThreadMessage(message) {
10822
+ return {
10823
+ id: message.id,
10824
+ role: message.role,
10825
+ text: message.text,
10826
+ ...message.isRule === true ? { is_rule: true } : {},
10827
+ ...message.ruleRef === void 0 || message.ruleRef === "" ? {} : { rule_uid: message.ruleRef }
10828
+ };
10829
+ }
10375
10830
  function compactAiPolishActorLabel(value) {
10376
10831
  const normalized = stringValue$1(value).replace(/\s+/g, " ").trim();
10377
10832
  if (normalized === "") return "";
@@ -10426,7 +10881,11 @@ var GroupAiPolishService = class {
10426
10881
  const instruction = requirement.trim();
10427
10882
  if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
10428
10883
  if (instruction === "" || [...instruction].length > 2e3) throw new ArkmePluginError("group-ai-polish-requirement-invalid", "请提供不超过 2000 字的润色要求", false);
10429
- if (!(await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal)).canManage) throw new ArkmePluginError("group-ai-polish-forbidden", "当前无法确认有效群成员身份,请稍后重试", false, 403);
10884
+ const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal, options.targetRuleRef?.trim() !== "");
10885
+ if (!config.canManage) throw this.permissionDenied();
10886
+ const targetRuleRef = options.targetRuleRef?.trim() ?? "";
10887
+ const targetRule = targetRuleRef === "" ? void 0 : config.rules.find((rule) => rule.ruleUid === targetRuleRef);
10888
+ if (targetRuleRef !== "" && targetRule === void 0) throw new ArkmePluginError("group-ai-polish-rule-not-found", "规则已删除或不可用,请重新读取规则列表", false, 404);
10430
10889
  const generated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/rules/generate", {
10431
10890
  chat_session_uid: source.ownerRef,
10432
10891
  instruction
@@ -10437,23 +10896,42 @@ var GroupAiPolishService = class {
10437
10896
  if (ruleName === "" || ruleText === "") throw new ArkmePluginError("group-ai-polish-generate-invalid", "AI 没有生成可用的润色规则,请换一种描述重试", true, 502);
10438
10897
  this.cleanupAiPolishState();
10439
10898
  const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
10899
+ const suppliedThread = aiPolishThreadMessages(options.threadMessages);
10900
+ const candidateMessage = {
10901
+ id: `arkme-${randomUUID()}`,
10902
+ role: "ai",
10903
+ text: ruleText,
10904
+ isRule: true,
10905
+ ...(targetRule?.ruleUid ?? stringValue$1(candidate.candidate_uid).trim()) === "" ? {} : { ruleRef: targetRule?.ruleUid ?? stringValue$1(candidate.candidate_uid).trim() }
10906
+ };
10907
+ const threadMessages = suppliedThread.length === 0 ? [] : [...suppliedThread, candidateMessage];
10908
+ const candidateExtra = objectValue$1(candidate.extra);
10440
10909
  this.aiPolishConfirmations.set(confirmationRef, {
10441
10910
  userId: session.userId,
10442
10911
  chatSessionUid: source.ownerRef,
10443
10912
  groupName: source.displayName,
10444
10913
  action: "enable",
10445
10914
  expiresAtMillis: Date.now() + 10 * 6e4,
10446
- candidateUid: stringValue$1(candidate.candidate_uid).trim(),
10915
+ candidateUid: targetRule?.ruleUid ?? stringValue$1(candidate.candidate_uid).trim(),
10916
+ ...targetRule === void 0 ? {} : {
10917
+ editingRuleUid: targetRule.ruleUid,
10918
+ editingRuleVersion: targetRule.ruleVersion
10919
+ },
10447
10920
  ruleName,
10448
10921
  ruleText,
10449
10922
  promptVersion: stringValue$1(candidate.prompt_version).trim(),
10450
- extra: objectValue$1(candidate.extra)
10923
+ extra: threadMessages.length === 0 ? candidateExtra : {
10924
+ ...candidateExtra,
10925
+ rule_thread_messages: threadMessages.map(serializeAiPolishThreadMessage),
10926
+ active_rule_message_id: candidateMessage.id
10927
+ }
10451
10928
  });
10452
10929
  return {
10453
10930
  groupName: source.displayName,
10454
10931
  ruleName,
10455
10932
  ruleText,
10456
- confirmationRef
10933
+ confirmationRef,
10934
+ ...threadMessages.length === 0 ? {} : { threadMessages }
10457
10935
  };
10458
10936
  }
10459
10937
  async generateGroupAiPolishRule(groupName, requirement, options = {}) {
@@ -10463,42 +10941,134 @@ var GroupAiPolishService = class {
10463
10941
  async confirmEnableGroupAiPolish(confirmationRef, options = {}) {
10464
10942
  const session = await this.runtime.requireSession();
10465
10943
  const pending = this.requireAiPolishConfirmation(confirmationRef, session.userId, "enable");
10466
- if (!(await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal)).canManage) throw new ArkmePluginError("group-ai-polish-forbidden", "当前无法确认有效群成员身份,请稍后重试", false, 403);
10467
- const updateAt = Date.now();
10468
- const upserted = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/rules/upsert", {
10469
- chat_session_uid: pending.chatSessionUid,
10470
- ...pending.candidateUid === void 0 || pending.candidateUid === "" ? {} : { rule_uid: pending.candidateUid },
10471
- name: pending.ruleName,
10472
- rule_text: pending.ruleText,
10473
- ...pending.promptVersion === void 0 || pending.promptVersion === "" ? {} : { prompt_version: pending.promptVersion },
10474
- update_at: updateAt,
10475
- ...pending.extra === void 0 || Object.keys(pending.extra).length === 0 ? {} : { extra: pending.extra }
10476
- }, session, options.signal);
10477
- const ruleUid = stringValue$1(objectValue$1(upserted.rule ?? upserted).rule_uid).trim();
10478
- if (ruleUid === "") throw new ArkmePluginError("group-ai-polish-rule-invalid", "保存润色规则后未返回有效规则", true, 502);
10479
- const updated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/update", {
10480
- chat_session_uid: pending.chatSessionUid,
10481
- enabled: true,
10482
- active_rule_uid: ruleUid,
10483
- update_at: Math.max(Date.now(), updateAt + 1)
10484
- }, session, options.signal);
10485
- const savedConfig = objectValue$1(updated.config ?? updated);
10486
- if (!booleanValue$4(savedConfig.enabled) || stringValue$1(savedConfig.active_rule_uid).trim() !== ruleUid) throw new ArkmePluginError("group-ai-polish-enable-invalid", "润色规则已保存,但开启状态确认失败,请重试", true, 502);
10487
- this.invalidateAiPolishReadCache(session.userId, pending.chatSessionUid);
10488
- this.aiPolishConfirmations.delete(confirmationRef.trim());
10944
+ if (pending.confirming) throw new ArkmePluginError("group-ai-polish-confirm-busy", "此操作正在确认,请稍后重试", true, 409);
10945
+ pending.confirming = true;
10946
+ try {
10947
+ const current = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal, true);
10948
+ if (!current.canManage) throw this.permissionDenied();
10949
+ const updateAt = Date.now();
10950
+ let ruleUid = pending.savedRuleUid ?? "";
10951
+ let ruleChanged = false;
10952
+ if (ruleUid !== "") {
10953
+ const saved = current.rules.find((rule) => rule.ruleUid === ruleUid);
10954
+ if (saved === void 0 || saved.name !== pending.ruleName || saved.ruleText !== pending.ruleText) throw new ArkmePluginError("group-ai-polish-preview-stale", "规则已被修改或删除,请重新预览并确认", false, 409);
10955
+ } else {
10956
+ if (pending.editingRuleUid !== void 0) {
10957
+ const currentRule = current.rules.find((rule) => rule.ruleUid === pending.editingRuleUid);
10958
+ if (currentRule === void 0 || currentRule.ruleVersion !== pending.editingRuleVersion) throw new ArkmePluginError("group-ai-polish-preview-stale", "规则已被其他成员修改或删除,请重新读取后再编辑", false, 409);
10959
+ }
10960
+ const upserted = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/rules/upsert", {
10961
+ chat_session_uid: pending.chatSessionUid,
10962
+ ...pending.candidateUid === void 0 || pending.candidateUid === "" ? {} : { rule_uid: pending.candidateUid },
10963
+ name: pending.ruleName,
10964
+ rule_text: pending.ruleText,
10965
+ ...pending.promptVersion === void 0 || pending.promptVersion === "" ? {} : { prompt_version: pending.promptVersion },
10966
+ update_at: updateAt,
10967
+ ...pending.extra === void 0 || Object.keys(pending.extra).length === 0 ? {} : { extra: pending.extra }
10968
+ }, session, options.signal);
10969
+ ruleUid = stringValue$1(objectValue$1(upserted.rule ?? upserted).rule_uid).trim();
10970
+ if (ruleUid === "") throw new ArkmePluginError("group-ai-polish-rule-invalid", "保存润色规则后未返回有效规则", true, 502);
10971
+ ruleChanged = true;
10972
+ pending.savedRuleUid = ruleUid;
10973
+ this.invalidateAiPolishReadCache(session.userId, pending.chatSessionUid);
10974
+ }
10975
+ try {
10976
+ if (!current.enabled || current.activeRuleUid !== ruleUid) await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/update", {
10977
+ chat_session_uid: pending.chatSessionUid,
10978
+ enabled: true,
10979
+ active_rule_uid: ruleUid,
10980
+ update_at: Math.max(Date.now(), updateAt + 1)
10981
+ }, session, options.signal);
10982
+ this.invalidateAiPolishReadCache(session.userId, pending.chatSessionUid);
10983
+ const verified = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal, true);
10984
+ const verifiedRule = verified.rules.find((rule) => rule.ruleUid === ruleUid);
10985
+ if (!verified.enabled || verified.activeRuleUid !== ruleUid || verifiedRule === void 0 || verifiedRule.name !== pending.ruleName || verifiedRule.ruleText !== pending.ruleText) throw new Error("AI polish state did not match the approved preview");
10986
+ } catch {
10987
+ this.invalidateAiPolishReadCache(session.userId, pending.chatSessionUid);
10988
+ throw new ArkmePluginError("group-ai-polish-enable-unverified", "规则已保存,但未能确认开启生效;请查询当前状态后重试,不需要重新生成规则", true, 502);
10989
+ }
10990
+ this.aiPolishConfirmations.delete(confirmationRef.trim());
10991
+ return {
10992
+ groupName: pending.groupName,
10993
+ enabled: true,
10994
+ ruleName: pending.ruleName ?? "",
10995
+ changed: ruleChanged || !current.enabled || current.activeRuleUid !== ruleUid
10996
+ };
10997
+ } finally {
10998
+ pending.confirming = false;
10999
+ }
11000
+ }
11001
+ permissionDenied() {
11002
+ return new ArkmePluginError("group-ai-polish-forbidden", "服务端未授予当前账号该群的 AI 润色设置权限;请确认账号仍在群内,以及服务端已支持全体群成员设置", false, 403);
11003
+ }
11004
+ async prepareEnableGroupAiPolishForSource(sourceRef, ruleName = "", options = {}) {
11005
+ const session = await this.runtime.requireSession();
11006
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
11007
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
11008
+ const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal, true);
11009
+ if (!config.canManage) throw this.permissionDenied();
11010
+ const name = ruleName.trim();
11011
+ const matches = name !== "" ? config.rules.filter((rule) => rule.name === name) : config.activeRuleUid !== "" ? config.rules.filter((rule) => rule.ruleUid === config.activeRuleUid) : config.rules;
11012
+ if (matches.length === 0) throw new ArkmePluginError("group-ai-polish-rule-not-found", "没有找到可开启的规则,请先查询规则或提供新的润色要求", false, 404);
11013
+ if (matches.length !== 1) throw new ArkmePluginError("group-ai-polish-rule-ambiguous", "存在多个候选规则,请查询并指定唯一的规则名称", false, 409);
11014
+ const rule = matches[0];
11015
+ this.cleanupAiPolishState();
11016
+ const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
11017
+ this.aiPolishConfirmations.set(confirmationRef, {
11018
+ userId: session.userId,
11019
+ chatSessionUid: source.ownerRef,
11020
+ groupName: source.displayName,
11021
+ action: "enable",
11022
+ expiresAtMillis: Date.now() + 10 * 6e4,
11023
+ savedRuleUid: rule.ruleUid,
11024
+ ruleName: rule.name,
11025
+ ruleText: rule.ruleText
11026
+ });
10489
11027
  return {
10490
- groupName: pending.groupName,
10491
- enabled: true,
10492
- ruleName: pending.ruleName ?? "",
10493
- changed: true
11028
+ groupName: source.displayName,
11029
+ ruleName: rule.name,
11030
+ ruleText: rule.ruleText,
11031
+ confirmationRef
10494
11032
  };
10495
11033
  }
11034
+ async prepareEnableGroupAiPolishRuleForSource(sourceRef, ruleRef, options = {}) {
11035
+ const session = await this.runtime.requireSession();
11036
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
11037
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
11038
+ const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal, true);
11039
+ if (!config.canManage) throw this.permissionDenied();
11040
+ const normalizedRuleRef = ruleRef.trim();
11041
+ const rule = config.rules.find((candidate) => candidate.ruleUid === normalizedRuleRef);
11042
+ if (normalizedRuleRef === "" || rule === void 0) throw new ArkmePluginError("group-ai-polish-rule-not-found", "规则已删除或不可用,请重新读取规则列表", false, 404);
11043
+ this.cleanupAiPolishState();
11044
+ const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
11045
+ this.aiPolishConfirmations.set(confirmationRef, {
11046
+ userId: session.userId,
11047
+ chatSessionUid: source.ownerRef,
11048
+ groupName: source.displayName,
11049
+ action: "enable",
11050
+ expiresAtMillis: Date.now() + 10 * 6e4,
11051
+ savedRuleUid: rule.ruleUid,
11052
+ ruleName: rule.name,
11053
+ ruleText: rule.ruleText
11054
+ });
11055
+ return {
11056
+ groupName: source.displayName,
11057
+ ruleName: rule.name,
11058
+ ruleText: rule.ruleText,
11059
+ confirmationRef
11060
+ };
11061
+ }
11062
+ async prepareEnableGroupAiPolish(groupName, ruleName = "", options = {}) {
11063
+ const source = await this.resolveUniqueGroupByName(groupName, options.signal);
11064
+ return await this.prepareEnableGroupAiPolishForSource(source.sourceRef, ruleName, options);
11065
+ }
10496
11066
  async prepareDisableGroupAiPolishForSource(sourceRef, options = {}) {
10497
11067
  const session = await this.runtime.requireSession();
10498
11068
  const source = await this.source.openSourceRef(sourceRef, session.userId);
10499
11069
  if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
10500
11070
  const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal);
10501
- if (!config.canManage) throw new ArkmePluginError("group-ai-polish-forbidden", "当前无法确认有效群成员身份,请稍后重试", false, 403);
11071
+ if (!config.canManage) throw this.permissionDenied();
10502
11072
  this.cleanupAiPolishState();
10503
11073
  const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
10504
11074
  this.aiPolishConfirmations.set(confirmationRef, {
@@ -10523,8 +11093,8 @@ var GroupAiPolishService = class {
10523
11093
  async confirmDisableGroupAiPolish(confirmationRef, options = {}) {
10524
11094
  const session = await this.runtime.requireSession();
10525
11095
  const pending = this.requireAiPolishConfirmation(confirmationRef, session.userId, "disable");
10526
- const current = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal);
10527
- if (!current.canManage) throw new ArkmePluginError("group-ai-polish-forbidden", "当前无法确认有效群成员身份,请稍后重试", false, 403);
11096
+ const current = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal, true);
11097
+ if (!current.canManage) throw this.permissionDenied();
10528
11098
  const updated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/update", {
10529
11099
  chat_session_uid: pending.chatSessionUid,
10530
11100
  enabled: false,
@@ -10685,8 +11255,8 @@ var GroupAiPolishService = class {
10685
11255
  polishedText
10686
11256
  };
10687
11257
  }
10688
- async queryGroupAiPolishConfig(chatSessionUid, session, signal) {
10689
- const data = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/query", { chat_session_uid: chatSessionUid }, session, signal, {
11258
+ async queryGroupAiPolishConfig(chatSessionUid, session, signal, fresh = false) {
11259
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/query", { chat_session_uid: chatSessionUid }, session, signal, fresh ? { lane: "interactive-read" } : {
10690
11260
  lane: "background-read",
10691
11261
  key: `ai-polish:settings:${chatSessionUid}`,
10692
11262
  cacheMs: 15e3,
@@ -10694,12 +11264,16 @@ var GroupAiPolishService = class {
10694
11264
  });
10695
11265
  const config = objectValue$1(data.config ?? data.setting ?? data.settings ?? data);
10696
11266
  const activeRuleUid = stringValue$1(config.active_rule_uid).trim();
10697
- const rules = listValue$13(data.rules).map((raw) => objectValue$1(raw)).map((rule) => ({
10698
- ruleUid: stringValue$1(rule.rule_uid).trim(),
10699
- name: stringValue$1(rule.name).trim() || "未命名规则",
10700
- ruleText: stringValue$1(rule.rule_text).trim(),
10701
- ruleVersion: numberValue$12(rule.rule_version)
10702
- })).filter((rule) => rule.ruleUid !== "" && rule.ruleText !== "");
11267
+ const rules = listValue$13(data.rules).map((raw) => objectValue$1(raw)).map((rule) => {
11268
+ const extra = objectValue$1(rule.extra);
11269
+ return {
11270
+ ruleUid: stringValue$1(rule.rule_uid).trim(),
11271
+ name: stringValue$1(rule.name).trim() || "未命名规则",
11272
+ ruleText: stringValue$1(rule.rule_text).trim(),
11273
+ ruleVersion: numberValue$12(rule.rule_version),
11274
+ threadMessages: aiPolishThreadMessages(extra.rule_thread_messages)
11275
+ };
11276
+ }).filter((rule) => rule.ruleUid !== "" && rule.ruleText !== "");
10703
11277
  return {
10704
11278
  enabled: booleanValue$4(config.enabled ?? config.is_enabled),
10705
11279
  canManage: booleanValue$4(data.can_manage),
@@ -10722,7 +11296,8 @@ var GroupAiPolishService = class {
10722
11296
  ruleRef: rule.ruleUid,
10723
11297
  name: rule.name,
10724
11298
  ruleText: rule.ruleText,
10725
- isActive: rule.ruleUid === config.activeRuleUid
11299
+ isActive: rule.ruleUid === config.activeRuleUid,
11300
+ ...rule.threadMessages.length === 0 ? {} : { threadMessages: rule.threadMessages }
10726
11301
  })),
10727
11302
  updatedAtMillis: config.updatedAtMillis
10728
11303
  };
@@ -10802,7 +11377,7 @@ var GroupAiPolishService = class {
10802
11377
  };
10803
11378
  //#endregion
10804
11379
  //#region src/services/group-service.ts
10805
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11380
+ const UUID$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10806
11381
  function numberValue$11(value) {
10807
11382
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
10808
11383
  }
@@ -11138,7 +11713,7 @@ var GroupService = class {
11138
11713
  async createGroup(title, clientMutationId, options = {}) {
11139
11714
  const normalizedTitle = title.trim();
11140
11715
  if (normalizedTitle === "" || Array.from(normalizedTitle).length > 80) throw new ArkmePluginError("group-title-invalid", "群聊名称需为 1-80 个字符", false);
11141
- if (!UUID.test(clientMutationId)) throw new ArkmePluginError("client-mutation-id-invalid", "群聊操作标识无效", false);
11716
+ if (!UUID$1.test(clientMutationId)) throw new ArkmePluginError("client-mutation-id-invalid", "群聊操作标识无效", false);
11142
11717
  const session = await this.runtime.requireSession();
11143
11718
  let data;
11144
11719
  try {
@@ -11794,11 +12369,19 @@ var MediaService = class {
11794
12369
  };
11795
12370
  }).filter((item) => item.fileAssetUid !== "");
11796
12371
  }
11797
- async uploadLocalFile(filePath, metadata) {
12372
+ async uploadLocalFile(filePath, metadata, options = {}) {
11798
12373
  if (this.runtime.config.richMediaSendEnabled === false) throw new ArkmePluginError("rich-content-disabled", "文件上传已被插件配置关闭", false, 403);
11799
12374
  const maxBytes = this.runtime.config.maxUploadBytes ?? 100 * 1024 * 1024;
11800
12375
  if (!Number.isSafeInteger(metadata.size) || metadata.size <= 0 || metadata.size > maxBytes || !/^[a-f0-9]{64}$/.test(metadata.sha256) || metadata.fileName.trim() === "") throw new ArkmePluginError("upload-metadata-invalid", "文件为空、过大或元数据无效", false, 400);
11801
12376
  const session = await this.runtime.requireSession();
12377
+ if (options.expectedUserId !== void 0 && session.userId !== options.expectedUserId) throw new ArkmePluginError("file-account-changed", "账号已切换", false, 403);
12378
+ options.signal?.throwIfAborted();
12379
+ const progress = (phase, sentBytes) => options.onProgress?.({
12380
+ phase,
12381
+ sentBytes,
12382
+ totalBytes: metadata.size
12383
+ });
12384
+ progress("preparing", 0);
11802
12385
  const uploadMode = metadata.size > 16 * 1024 * 1024 ? 2 : 1;
11803
12386
  const prepared = await this.runtime.authenticatedPost("/api/v1/files/prepare-upload", {
11804
12387
  planned_size: metadata.size,
@@ -11807,7 +12390,7 @@ var MediaService = class {
11807
12390
  file_kind: metadata.fileKind,
11808
12391
  upload_mode: uploadMode,
11809
12392
  display_name: metadata.fileName
11810
- }, session);
12393
+ }, session, options.signal);
11811
12394
  const uploadSessionUid = stringValue$1(prepared.upload_session_uid).trim();
11812
12395
  if (uploadSessionUid === "") throw new ArkmePluginError("upload-prepare-invalid", "上传准备响应无效", true, 502);
11813
12396
  try {
@@ -11816,12 +12399,23 @@ var MediaService = class {
11816
12399
  if (uploadMode === 1) {
11817
12400
  const uploadUrl = stringValue$1(prepared.upload_url).trim();
11818
12401
  if (uploadUrl === "") throw new ArkmePluginError("upload-url-missing", "对象存储上传地址缺失", true, 502);
12402
+ const signal = options.signal;
12403
+ const body = options.onProgress === void 0 ? createReadStream(filePath) : Readable.from((async function* () {
12404
+ let sent = 0;
12405
+ for await (const chunk of createReadStream(filePath)) {
12406
+ signal?.throwIfAborted();
12407
+ sent += chunk.length;
12408
+ progress("uploading", sent);
12409
+ yield chunk;
12410
+ }
12411
+ })());
11819
12412
  const response = await this.runtime.fetchImpl(uploadUrl, {
11820
12413
  method: "PUT",
11821
12414
  headers: Object.fromEntries(Object.entries(objectValue$1(prepared.upload_headers)).map(([key, value]) => [key, stringValue$1(value)])),
11822
- body: createReadStream(filePath),
12415
+ body,
11823
12416
  duplex: "half",
11824
- redirect: "error"
12417
+ redirect: "error",
12418
+ ...signal === void 0 ? {} : { signal }
11825
12419
  });
11826
12420
  if (!response.ok) throw new ArkmePluginError("upload-storage-failed", `对象存储上传失败(${String(response.status)})`, true, 502);
11827
12421
  storageETag = response.headers.get("etag") ?? "";
@@ -11832,6 +12426,7 @@ var MediaService = class {
11832
12426
  const handle = await open(filePath, "r");
11833
12427
  try {
11834
12428
  for (const part of parts) {
12429
+ options.signal?.throwIfAborted();
11835
12430
  const partNumber = Math.trunc(numberValue$9(part.part_number));
11836
12431
  const uploadUrl = stringValue$1(part.upload_url).trim();
11837
12432
  const offset = (partNumber - 1) * partSize;
@@ -11843,26 +12438,30 @@ var MediaService = class {
11843
12438
  method: "PUT",
11844
12439
  headers: Object.fromEntries(Object.entries(objectValue$1(part.upload_headers)).map(([key, value]) => [key, stringValue$1(value)])),
11845
12440
  body: buffer,
11846
- redirect: "error"
12441
+ redirect: "error",
12442
+ ...options.signal === void 0 ? {} : { signal: options.signal }
11847
12443
  });
11848
12444
  if (!response.ok) throw new ArkmePluginError("upload-storage-failed", `对象存储分片上传失败(${String(response.status)})`, true, 502);
11849
12445
  completedParts.push({
11850
12446
  part_number: partNumber,
11851
12447
  etag: response.headers.get("etag") ?? ""
11852
12448
  });
12449
+ progress("uploading", Math.min(metadata.size, offset + length));
11853
12450
  }
11854
12451
  } finally {
11855
12452
  await handle.close();
11856
12453
  }
11857
12454
  }
12455
+ progress("completing", metadata.size);
11858
12456
  const completed = await this.runtime.authenticatedPost("/api/v1/files/complete-upload", {
11859
12457
  upload_session_uid: uploadSessionUid,
11860
12458
  uploaded_size: metadata.size,
11861
12459
  storage_etag: storageETag,
11862
12460
  multipart_parts: completedParts
11863
- }, session);
12461
+ }, session, options.signal);
11864
12462
  const fileAssetUid = stringValue$1(completed.file_asset_uid).trim();
11865
12463
  if (fileAssetUid === "") throw new ArkmePluginError("upload-complete-invalid", "上传完成响应无效", true, 502);
12464
+ progress("ready", metadata.size);
11866
12465
  return {
11867
12466
  fileAssetUid,
11868
12467
  fileName: metadata.fileName,
@@ -11875,7 +12474,7 @@ var MediaService = class {
11875
12474
  throw error;
11876
12475
  }
11877
12476
  }
11878
- async fetchMedia(mediaRef, range, signal) {
12477
+ async fetchMedia(mediaRef, range, signal, originalOnly = false) {
11879
12478
  const session = await this.runtime.requireSession();
11880
12479
  const descriptor = this.mediaRefs.get(mediaRef);
11881
12480
  if (descriptor === void 0 || descriptor.viewerUserId !== session.userId || descriptor.expiresAtMillis <= Date.now()) {
@@ -11883,6 +12482,7 @@ var MediaService = class {
11883
12482
  if (descriptor?.stableKey !== void 0) this.stableMediaRefs.delete(descriptor.stableKey);
11884
12483
  throw new ArkmePluginError("media-ref-invalid", "媒体引用已失效,请刷新对话后重试", false, 404);
11885
12484
  }
12485
+ if (originalOnly && descriptor.purpose !== "original") throw new ArkmePluginError("original-unavailable", "原文件不可用,请刷新后重试;不会用预览图代替原文件", false, 404);
11886
12486
  const url = new URL(descriptor.remoteUrl);
11887
12487
  if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || !(allowedSignedImageHost$2(this.runtime.config.environment, url.hostname) || allowedSignedAudioHost(this.runtime.config.environment, url.hostname))) throw new ArkmePluginError("media-host-rejected", "媒体来源不受信任", false, 403);
11888
12488
  const response = await this.runtime.fetchImpl(url, {
@@ -12044,6 +12644,15 @@ var MediaService = class {
12044
12644
  if ((snapshot.profile?.avatarRef.trim() ?? "") === "") try {
12045
12645
  snapshot = await this.profile.refreshProfileForSession(session);
12046
12646
  } catch {}
12647
+ const ownAvatarUrl = snapshot.profile?.avatarUrl;
12648
+ if (ownAvatarUrl !== void 0 && ownAvatarUrl !== "") return await this.downloadSignedImage(trustedSignedImageUrl$1(this.runtime.config.environment, ownAvatarUrl), byteLimit, signal, this.runtime.requestScope(session.userId));
12649
+ const ownAvatarAssetRef = snapshot.profile?.avatarAssetRef?.trim() ?? "";
12650
+ const assetMatch = /^file_asset:\/\/([A-Za-z0-9_-]{8,128})$/.exec(ownAvatarAssetRef);
12651
+ if (assetMatch !== null) {
12652
+ const asset = (await this.queryFileAssets([assetMatch[1]], signal)).find((item) => item.fileAssetUid === assetMatch[1]);
12653
+ const assetUrl = asset?.previewUrl ?? asset?.downloadUrl;
12654
+ if (assetUrl !== void 0) return await this.downloadSignedImage(trustedSignedImageUrl$1(this.runtime.config.environment, assetUrl), byteLimit, signal, this.runtime.requestScope(session.userId));
12655
+ }
12047
12656
  const ownAvatarRef = snapshot.profile?.avatarRef.trim() ?? "";
12048
12657
  if (ownAvatarRef !== "" && !ownAvatarRef.startsWith("arkme-profile-image-v1.")) return await this.readImage(ownAvatarRef, {
12049
12658
  maxBytes: byteLimit,
@@ -12109,10 +12718,14 @@ var MediaService = class {
12109
12718
  if (signedUrl.protocol !== "https:" || signedUrl.username !== "" || signedUrl.password !== "" || !allowedSignedImageHost$2(this.runtime.config.environment, signedUrl.hostname) || signedPath !== objectPath) throw new ArkmePluginError("image-sign-target-rejected", "Arkme 图片授权目标不受信任", false, 502);
12110
12719
  return await this.downloadSignedImage(signedUrl, byteLimit, signal, this.runtime.requestScope(session.userId));
12111
12720
  }
12112
- mediaKind(fileKind, mimeType) {
12113
- if (fileKind === 1 || mimeType.startsWith("image/")) return "image";
12114
- if (fileKind === 2 || mimeType.startsWith("audio/")) return "audio";
12115
- if (fileKind === 3 || mimeType.startsWith("video/")) return "video";
12721
+ mediaKind(fileKind, mimeType, fileName) {
12722
+ const detectedKind = arkmeMediaKind(mimeType, fileName);
12723
+ if (detectedKind !== void 0) return detectedKind;
12724
+ if (fileKind === 2) return "audio";
12725
+ if (!/\.[A-Za-z0-9]+$/u.test(fileName.trim())) {
12726
+ if (fileKind === 1) return "image";
12727
+ if (fileKind === 3) return "video";
12728
+ }
12116
12729
  return "file";
12117
12730
  }
12118
12731
  recordContentPayload(raw) {
@@ -12229,6 +12842,29 @@ var MediaService = class {
12229
12842
  }
12230
12843
  return mediaRefs;
12231
12844
  }
12845
+ async issueSearchFileMediaRefs(requests, signal) {
12846
+ const normalized = [...new Map(requests.filter((item) => item.recordUid !== "" && item.fileAssetUid !== "").map((item) => [`${item.recordUid}\0${item.fileAssetUid}`, item])).values()].slice(0, 50);
12847
+ const refs = /* @__PURE__ */ new Map();
12848
+ if (normalized.length === 0 || this.runtime.config.richMediaRenderEnabled === false) return refs;
12849
+ const session = await this.runtime.requireSession();
12850
+ const display = await this.queryRecordMediaDisplayItems(normalized.map((item) => item.recordUid), session, signal);
12851
+ for (const request of normalized) {
12852
+ const item = objectValue$1((display.get(request.recordUid) ?? []).find((raw) => stringValue$1(objectValue$1(raw).file_asset_uid) === request.fileAssetUid));
12853
+ const remoteUrl = safeHttpsUrl(item.download_url);
12854
+ if (remoteUrl === void 0) continue;
12855
+ const host = new URL(remoteUrl).hostname;
12856
+ if (!allowedSignedImageHost$2(this.runtime.config.environment, host) && !allowedSignedAudioHost(this.runtime.config.environment, host)) continue;
12857
+ const key = `${request.recordUid}\0${request.fileAssetUid}`;
12858
+ refs.set(key, this.issueMediaRef(session.userId, {
12859
+ remoteUrl,
12860
+ purpose: "original",
12861
+ mimeType: stringValue$1(item.mime_type) || "application/octet-stream",
12862
+ fileName: stringValue$1(item.file_name) || "文件",
12863
+ size: Math.max(0, numberValue$9(item.size))
12864
+ }, `search-file\0${key}`));
12865
+ }
12866
+ return refs;
12867
+ }
12232
12868
  async hydrateRecordMediaPage(rawItems, session, signal) {
12233
12869
  const displayItemsByRecordUid = /* @__PURE__ */ new Map();
12234
12870
  const expectedRecordUids = [...new Set(rawItems.flatMap((raw) => {
@@ -12355,14 +12991,23 @@ var MediaService = class {
12355
12991
  return Math.trunc(numberValue$9(item.content_file_role)) !== RECORD_CONTENT_FILE_ROLE_BACKGROUND_SOUND;
12356
12992
  }).flatMap((item, index) => {
12357
12993
  const fileAssetUid = stringValue$1(item.file_asset_uid).trim();
12358
- const mimeType = stringValue$1(item.mime_type).trim() || "application/octet-stream";
12359
12994
  const fileName = stringValue$1(item.file_name).trim() || `附件-${String(index + 1)}`;
12360
12995
  const fileKind = Math.trunc(numberValue$9(item.file_kind));
12361
- const kind = this.mediaKind(fileKind, mimeType);
12996
+ const mimeType = arkmeNormalizedFileMimeType(stringValue$1(item.mime_type).trim().toLowerCase(), fileName);
12997
+ const kind = this.mediaKind(fileKind, mimeType, fileName);
12362
12998
  const remoteUrl = stringValue$1(kind === "image" ? item.preview_url ?? item.download_url : item.download_url).trim();
12363
12999
  if (remoteUrl === "") return [];
13000
+ const originalUrl = stringValue$1(item.download_url).trim();
13001
+ const originalRef = originalUrl === "" ? void 0 : this.issueMediaRef(viewerUserId, {
13002
+ remoteUrl: originalUrl,
13003
+ purpose: "original",
13004
+ mimeType,
13005
+ fileName,
13006
+ size: Math.max(0, Math.trunc(numberValue$9(item.size)))
13007
+ }, fileAssetUid === "" ? void 0 : `original\0${fileAssetUid}`);
12364
13008
  return [{
12365
13009
  kind,
13010
+ ...originalRef === void 0 ? {} : { originalRef },
12366
13011
  mediaRef: this.issueMediaRef(viewerUserId, {
12367
13012
  remoteUrl,
12368
13013
  mimeType,
@@ -12765,6 +13410,10 @@ function phoneDefaultAvatarFallback(raw) {
12765
13410
  label
12766
13411
  };
12767
13412
  }
13413
+ function fileAssetAvatarRef(raw) {
13414
+ const normalized = raw.trim();
13415
+ return /^file_asset:\/\/[A-Za-z0-9_-]{8,128}$/.test(normalized) ? normalized : void 0;
13416
+ }
12768
13417
  function allowedSignedImageHost$1(environment, hostname) {
12769
13418
  return (environment === "prod" ? ["jotmo-userfiles.oss-cn-hangzhou.aliyuncs.com", "userfiles.jotmo.cc"] : ["jotmo-userfiles-test.oss-cn-hangzhou.aliyuncs.com", "jotmo-userfiles.senguo.me"]).includes(hostname.toLowerCase());
12770
13419
  }
@@ -12816,7 +13465,9 @@ var ProfileService = class {
12816
13465
  }
12817
13466
  async cachedProfile() {
12818
13467
  const session = await this.runtime.requireAuthFlowSession();
12819
- return await this.runtime.stateStore.cachedProfile(session.userId);
13468
+ const cached = await this.runtime.stateStore.cachedProfile(session.userId);
13469
+ if (this.profileNeedsAvatarRefresh(cached, session.userId)) return await this.refreshProfileForSession(session);
13470
+ return cached;
12820
13471
  }
12821
13472
  async refreshProfile() {
12822
13473
  const session = await this.runtime.requireAuthFlowSession();
@@ -12895,6 +13546,7 @@ var ProfileService = class {
12895
13546
  if (memory !== void 0 && memory.expiresAtMillis > Date.now()) return memory.value;
12896
13547
  const persisted = await this.runtime.stateStore.cachedProfile(session.userId).catch(() => void 0);
12897
13548
  if (persisted?.profile?.userId === session.userId && persisted.cachedAtMillis > 0 && Date.now() - persisted.cachedAtMillis < PROFILE_CACHE_TTL_MS) {
13549
+ if (this.profileNeedsAvatarRefresh(persisted, session.userId)) return await this.refreshProfileForSession(session);
12898
13550
  this.profileCache.set(session.userId, {
12899
13551
  value: persisted,
12900
13552
  expiresAtMillis: persisted.cachedAtMillis + PROFILE_CACHE_TTL_MS
@@ -12916,16 +13568,21 @@ var ProfileService = class {
12916
13568
  if (userId <= 0 || userId !== session.userId) throw new ArkmePluginError("profile-contract-invalid", "Arkme 个人资料响应缺少有效用户标识", false, 502);
12917
13569
  const nickname = stringValue$1(data.nick_name).trim();
12918
13570
  const displayName = nickname || stringValue$1(data.apple_nick_name).trim() || stringValue$1(data.wechat_nick_name).trim() || stringValue$1(data.google_given_name).trim() || stringValue$1(data.name_slug).trim() || "Arkme用户";
12919
- const avatarRef = stringValue$1(data.head_img).trim();
13571
+ const rawAvatarRef = stringValue$1(data.head_img).trim();
13572
+ const avatarAssetRef = fileAssetAvatarRef(rawAvatarRef);
13573
+ const avatarUrl = await this.publicProfilesByUserIds([session.userId], session).then((profiles) => profiles.get(session.userId)?.avatarUrl).catch(() => void 0) ?? (/^https?:\/\//i.test(rawAvatarRef) ? rawAvatarRef : void 0);
13574
+ const avatarRef = avatarUrl !== void 0 || avatarAssetRef !== void 0 ? await this.sealProfileImageRef(session.userId, session.userId) : rawAvatarRef;
12920
13575
  const phone = maskedPhone(stringValue$1(data.phone));
12921
13576
  const email = maskedEmail(stringValue$1(data.email));
13577
+ const wechatName = stringValue$1(data.wechat_nick_name).trim();
12922
13578
  const canUpdateArkmeId = optionalBooleanValue(data.can_update_jotmo_id);
12923
13579
  const profile = {
12924
13580
  userId,
12925
13581
  displayName,
12926
13582
  nickname,
12927
13583
  avatarRef,
12928
- .../^https?:\/\//i.test(avatarRef) ? { avatarUrl: avatarRef } : {},
13584
+ ...avatarAssetRef === void 0 ? {} : { avatarAssetRef },
13585
+ ...avatarUrl === void 0 ? {} : { avatarUrl },
12929
13586
  arkmeId: stringValue$1(data.jotmo_id).trim() || stringValue$1(data.name_slug).trim(),
12930
13587
  ...canUpdateArkmeId === void 0 ? {} : { canUpdateArkmeId },
12931
13588
  accountType: numberValue$7(data.type),
@@ -12935,6 +13592,7 @@ var ProfileService = class {
12935
13592
  wechat: booleanValue$2(data.has_bind_wechat),
12936
13593
  google: booleanValue$2(data.has_bind_google)
12937
13594
  },
13595
+ ...wechatName === "" ? {} : { bindingNames: { wechat: wechatName } },
12938
13596
  contact: {
12939
13597
  ...phone === void 0 ? {} : { phoneMasked: phone },
12940
13598
  ...email === void 0 ? {} : { emailMasked: email }
@@ -12954,6 +13612,12 @@ var ProfileService = class {
12954
13612
  if (this.profileInFlight.get(session.userId) === pending) this.profileInFlight.delete(session.userId);
12955
13613
  }
12956
13614
  }
13615
+ profileNeedsAvatarRefresh(snapshot, userId) {
13616
+ const profile = snapshot.profile;
13617
+ if (profile?.userId !== userId) return false;
13618
+ const avatarRef = profile.avatarRef.trim();
13619
+ return /^https?:\/\//i.test(avatarRef) || fileAssetAvatarRef(avatarRef) !== void 0;
13620
+ }
12957
13621
  async publicProfileSummariesByUserIds(userIds, session, signal) {
12958
13622
  const normalized = [...new Set(userIds.filter((userId) => Number.isSafeInteger(userId) && userId > 0))].sort((left, right) => left - right);
12959
13623
  const profiles = /* @__PURE__ */ new Map();
@@ -14287,6 +14951,38 @@ var SearchService = class {
14287
14951
  }, session, options.signal);
14288
14952
  return await this.withNavigationTargets(this.recordSearchResult(data, lockedRecordUids), options.signal);
14289
14953
  }
14954
+ async searchFiles(options) {
14955
+ const page = options.query?.trim() ? await this.searchRemote({
14956
+ ...options,
14957
+ query: options.query
14958
+ }) : await this.searchScene({
14959
+ ...options,
14960
+ scene: "file"
14961
+ });
14962
+ const items = page.items.filter((item) => item.files.length > 0);
14963
+ const requests = items.flatMap((item) => item.files.map((file) => ({
14964
+ recordUid: item.recordUid,
14965
+ fileAssetUid: file.fileAssetUid
14966
+ })));
14967
+ const refs = /* @__PURE__ */ new Map();
14968
+ for (let offset = 0; offset < requests.length; offset += 50) {
14969
+ const part = await this.media.issueSearchFileMediaRefs(requests.slice(offset, offset + 50), options.signal);
14970
+ for (const [key, value] of part) refs.set(key, value);
14971
+ }
14972
+ return {
14973
+ ...page,
14974
+ items: items.map((item) => ({
14975
+ ...item,
14976
+ files: item.files.map((file) => {
14977
+ const mediaRef = refs.get(`${item.recordUid}\0${file.fileAssetUid}`);
14978
+ return mediaRef === void 0 ? file : {
14979
+ ...file,
14980
+ mediaRef
14981
+ };
14982
+ })
14983
+ }))
14984
+ };
14985
+ }
14290
14986
  async withAudioMediaRefs(result, signal) {
14291
14987
  const requests = result.items.flatMap((item) => item.voice === void 0 || item.sourceKind === 3 ? [] : [{
14292
14988
  recordUid: item.recordUid,
@@ -14589,6 +15285,561 @@ var SearchService = class {
14589
15285
  }
14590
15286
  };
14591
15287
  //#endregion
15288
+ //#region src/services/file-transfers.ts
15289
+ const REF = /^arkme-file-v1\.[0-9a-f-]{36}$/;
15290
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
15291
+ const fail = (code, message) => new ArkmePluginError(code, message, false, 400);
15292
+ const clone = (value) => structuredClone(value);
15293
+ const publicFile = (file) => ({
15294
+ fileRef: file.fileRef,
15295
+ fileName: file.fileName,
15296
+ mimeType: file.mimeType,
15297
+ size: file.size,
15298
+ fileKind: file.fileKind
15299
+ });
15300
+ const nativeOpenFileName = (fileName) => {
15301
+ const cleaned = fileName.replace(/[<>:"/\\|?*\u0000-\u001f\u007f]/g, "_").trim().replace(/^[. ]+|[. ]+$/g, "");
15302
+ const extension = /\.[a-zA-Z0-9]{1,16}$/.exec(cleaned)?.[0] ?? "";
15303
+ let stem = (extension === "" ? cleaned : cleaned.slice(0, -extension.length)).replace(/[. ]+$/g, "");
15304
+ if (stem === "" || stem === "." || stem === "..") stem = "file";
15305
+ stem = stem.slice(0, Math.max(1, 180 - extension.length));
15306
+ if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(stem)) stem = `_${stem}`;
15307
+ return `${stem}${extension}`;
15308
+ };
15309
+ /** Plugin-owned files, not DSH session attachments. No client-controlled path is persisted. */
15310
+ var FileTransfers = class {
15311
+ directory;
15312
+ ports;
15313
+ states = /* @__PURE__ */ new Map();
15314
+ writes = Promise.resolve();
15315
+ mutations = Promise.resolve();
15316
+ queue = Promise.resolve();
15317
+ jobs = /* @__PURE__ */ new Set();
15318
+ controllers = /* @__PURE__ */ new Set();
15319
+ receptions = /* @__PURE__ */ new Map();
15320
+ policy;
15321
+ constructor(directory, ports, maxBytes) {
15322
+ this.directory = directory;
15323
+ this.ports = ports;
15324
+ this.policy = {
15325
+ version: 1,
15326
+ maxFileBytes: maxBytes,
15327
+ maxImageBytes: Math.min(maxBytes, 50 * 1024 * 1024),
15328
+ maxAttachments: 9
15329
+ };
15330
+ }
15331
+ capabilities() {
15332
+ return { ...this.policy };
15333
+ }
15334
+ cancelActive() {
15335
+ for (const controller of this.controllers) controller.abort();
15336
+ this.receptions.clear();
15337
+ }
15338
+ async settled() {
15339
+ await this.queue;
15340
+ await Promise.all(this.jobs);
15341
+ await this.writes;
15342
+ }
15343
+ root(userId) {
15344
+ return join(this.directory, String(userId));
15345
+ }
15346
+ path(userId, ref) {
15347
+ if (!REF.test(ref)) throw fail("file-ref-invalid", "文件引用无效或不属于当前账号");
15348
+ return join(this.root(userId), ref);
15349
+ }
15350
+ openDirectory(userId, ref) {
15351
+ if (!REF.test(ref)) throw fail("file-ref-invalid", "文件引用无效或不属于当前账号");
15352
+ return join(this.root(userId), ".open", ref);
15353
+ }
15354
+ async nativeOpenPath(userId, ref, file) {
15355
+ const source = this.path(userId, ref);
15356
+ const directory = this.openDirectory(userId, ref);
15357
+ const target = join(directory, nativeOpenFileName(file.fileName));
15358
+ await mkdir(directory, {
15359
+ recursive: true,
15360
+ mode: 448
15361
+ });
15362
+ try {
15363
+ await link(source, target);
15364
+ } catch (error) {
15365
+ if (error.code !== "EEXIST") throw error;
15366
+ const info = await stat(target).catch(() => void 0);
15367
+ if (!info?.isFile() || info.size !== file.size) {
15368
+ await unlink(target).catch(() => {});
15369
+ await link(source, target);
15370
+ }
15371
+ }
15372
+ await chmod$1(target, 384);
15373
+ return target;
15374
+ }
15375
+ async assertUser(userId, signal) {
15376
+ signal?.throwIfAborted();
15377
+ if (await this.ports.currentUser() !== userId) throw fail("file-account-changed", "账号已切换,请在原账号中重试");
15378
+ signal?.throwIfAborted();
15379
+ }
15380
+ state(userId) {
15381
+ let loaded = this.states.get(userId);
15382
+ if (loaded === void 0) {
15383
+ loaded = (async () => {
15384
+ await mkdir(this.root(userId), {
15385
+ recursive: true,
15386
+ mode: 448
15387
+ });
15388
+ let state;
15389
+ try {
15390
+ state = JSON.parse(await readFile(join(this.root(userId), "state.json"), "utf8"));
15391
+ } catch (error) {
15392
+ if (error.code !== "ENOENT") throw fail("file-state-invalid", "本地文件状态无法读取,请勿重复发送");
15393
+ return {
15394
+ version: 1,
15395
+ files: {},
15396
+ tasks: [],
15397
+ originals: {}
15398
+ };
15399
+ }
15400
+ if (state.version !== 1 || !state.files || !Array.isArray(state.tasks) || !state.originals) throw fail("file-state-invalid", "本地文件状态版本无效");
15401
+ for (const task of state.tasks) if (task.state === "sending") {
15402
+ task.state = "uncertain";
15403
+ task.error = "发送结果待确认,请先核对会话,避免重复发送";
15404
+ } else if (task.state === "queued" || task.state === "uploading") {
15405
+ task.state = "failed";
15406
+ task.error = "上次传输已中断,请重试";
15407
+ }
15408
+ return state;
15409
+ })();
15410
+ this.states.set(userId, loaded);
15411
+ }
15412
+ return loaded;
15413
+ }
15414
+ save(userId, state) {
15415
+ const data = JSON.stringify(state);
15416
+ const write = this.writes.catch(() => {}).then(async () => {
15417
+ const target = join(this.root(userId), "state.json");
15418
+ const temporary = `${target}.${randomUUID()}.tmp`;
15419
+ try {
15420
+ await writeFile(temporary, data, { mode: 384 });
15421
+ await rename(temporary, target);
15422
+ } finally {
15423
+ await unlink(temporary).catch(() => {});
15424
+ }
15425
+ });
15426
+ this.writes = write;
15427
+ return write;
15428
+ }
15429
+ exclusive(action) {
15430
+ const work = this.mutations.catch(() => {}).then(action);
15431
+ this.mutations = work;
15432
+ return work;
15433
+ }
15434
+ validateMetadata(metadata) {
15435
+ const limit = metadata.mimeType.startsWith("image/") ? this.policy.maxImageBytes : this.policy.maxFileBytes;
15436
+ if (!metadata.fileName.trim() || metadata.fileName.length > 255 || /[\u0000-\u001f]/.test(metadata.fileName) || metadata.mimeType.length > 200 || !/^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/.test(metadata.mimeType) || !Number.isSafeInteger(metadata.size) || metadata.size <= 0 || metadata.size > limit) throw fail("file-input-invalid", `文件为空或超出限制(${Math.floor(limit / 1024 / 1024)} MiB)`);
15437
+ }
15438
+ async stage(temporaryPath, metadata, expectedUserId) {
15439
+ const userId = await this.ports.currentUser();
15440
+ if (expectedUserId !== void 0 && expectedUserId !== userId) throw fail("file-account-changed", "账号已切换,本次文件导入已取消");
15441
+ const normalizedMetadata = {
15442
+ ...metadata,
15443
+ mimeType: arkmeNormalizedFileMimeType(metadata.mimeType, metadata.fileName)
15444
+ };
15445
+ this.validateMetadata(normalizedMetadata);
15446
+ return this.exclusive(async () => {
15447
+ const state = await this.state(userId);
15448
+ await this.prune(userId, state);
15449
+ if (Object.values(state.files).reduce((sum, file) => sum + file.size, 0) + metadata.size > Math.max(this.policy.maxFileBytes, 1024 * 1024 * 1024) || Object.keys(state.files).length >= 256) throw fail("file-cache-full", "本地附件空间不足,请移除不再需要的草稿或失败任务");
15450
+ const info = await stat(temporaryPath);
15451
+ if (!info.isFile() || info.size !== metadata.size) throw fail("file-size-mismatch", "本地文件不完整");
15452
+ const hash = createHash("sha256");
15453
+ for await (const chunk of createReadStream(temporaryPath)) hash.update(chunk);
15454
+ await this.assertUser(userId);
15455
+ const ref = `arkme-file-v1.${randomUUID()}`;
15456
+ const file = {
15457
+ ...normalizedMetadata,
15458
+ fileRef: ref,
15459
+ fileKind: arkmePickedFileKind(normalizedMetadata.mimeType, normalizedMetadata.fileName),
15460
+ sha256: hash.digest("hex"),
15461
+ createdAtMillis: Date.now()
15462
+ };
15463
+ await copyFile(temporaryPath, this.path(userId, ref));
15464
+ await chmod$1(this.path(userId, ref), 384);
15465
+ state.files[ref] = file;
15466
+ try {
15467
+ await this.save(userId, state);
15468
+ } catch (error) {
15469
+ delete state.files[ref];
15470
+ await unlink(this.path(userId, ref)).catch(() => {});
15471
+ throw error;
15472
+ }
15473
+ return publicFile(file);
15474
+ });
15475
+ }
15476
+ async prune(userId, state) {
15477
+ const retained = new Set(state.tasks.filter((task) => task.state !== "sent").flatMap((task) => task.fileRefs));
15478
+ const completed = new Set(state.tasks.filter((task) => task.state === "sent").flatMap((task) => task.fileRefs));
15479
+ for (const file of Object.values(state.files)) {
15480
+ if (Date.now() - file.createdAtMillis < 168 * 36e5 || retained.has(file.fileRef) || !completed.has(file.fileRef)) continue;
15481
+ delete state.files[file.fileRef];
15482
+ await unlink(this.path(userId, file.fileRef)).catch(() => {});
15483
+ await rm(this.openDirectory(userId, file.fileRef), {
15484
+ recursive: true,
15485
+ force: true
15486
+ });
15487
+ }
15488
+ state.tasks = state.tasks.filter((task) => task.state !== "sent" || Date.now() - task.createdAtMillis < 168 * 36e5);
15489
+ }
15490
+ async readLocal(ref) {
15491
+ const userId = await this.ports.currentUser();
15492
+ const file = (await this.state(userId)).files[ref];
15493
+ if (file === void 0) throw fail("file-ref-invalid", "文件引用无效或不属于当前账号");
15494
+ const path = this.path(userId, ref);
15495
+ const info = await stat(path).catch(() => void 0);
15496
+ if (!info?.isFile() || info.size !== file.size) throw fail("file-local-missing", "本地文件已不存在,请重新添加或接收");
15497
+ await this.assertUser(userId);
15498
+ return {
15499
+ path,
15500
+ file: publicFile(file)
15501
+ };
15502
+ }
15503
+ async openLocal(ref) {
15504
+ if (this.ports.openPath === void 0) throw fail("file-open-unavailable", "当前宿主不能使用本机应用打开文件");
15505
+ const controller = new AbortController();
15506
+ this.controllers.add(controller);
15507
+ try {
15508
+ const userId = await this.ports.currentUser();
15509
+ const { file } = await this.readLocal(ref);
15510
+ await this.assertUser(userId, controller.signal);
15511
+ const path = await this.nativeOpenPath(userId, ref, file);
15512
+ await this.assertUser(userId, controller.signal);
15513
+ await this.ports.openPath(path, controller.signal);
15514
+ await this.assertUser(userId, controller.signal);
15515
+ return {
15516
+ opened: true,
15517
+ file
15518
+ };
15519
+ } catch (error) {
15520
+ if (error instanceof ArkmePluginError) throw error;
15521
+ throw fail("file-open-failed", "文件打开失败,请重试");
15522
+ } finally {
15523
+ this.controllers.delete(controller);
15524
+ }
15525
+ }
15526
+ async files() {
15527
+ const state = await this.state(await this.ports.currentUser());
15528
+ return Object.values(state.files).map(publicFile);
15529
+ }
15530
+ async remove(ref) {
15531
+ const userId = await this.ports.currentUser();
15532
+ await this.exclusive(async () => {
15533
+ const state = await this.state(userId);
15534
+ await this.assertUser(userId);
15535
+ if (state.tasks.some((task) => task.fileRefs.includes(ref))) throw fail("file-in-use", "文件仍被本地发送任务引用,请先移除该任务");
15536
+ if (!state.files[ref]) return;
15537
+ delete state.files[ref];
15538
+ await this.save(userId, state);
15539
+ await unlink(this.path(userId, ref)).catch(() => {});
15540
+ await rm(this.openDirectory(userId, ref), {
15541
+ recursive: true,
15542
+ force: true
15543
+ });
15544
+ });
15545
+ }
15546
+ async tasks(sourceRef) {
15547
+ const state = await this.state(await this.ports.currentUser());
15548
+ return clone(state.tasks.filter((task) => !sourceRef || task.sourceRef === sourceRef));
15549
+ }
15550
+ async stageBytes(contentBase64, metadata) {
15551
+ if (contentBase64.length > Math.ceil(65536 / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(contentBase64)) throw fail("file-tool-input-invalid", "工具暂存只接受最多 64 KiB 的 Base64 内容,不接受本机路径");
15552
+ const data = Buffer.from(contentBase64, "base64");
15553
+ if (data.length > 65536) throw fail("file-tool-input-invalid", "工具暂存文件超过 64 KiB");
15554
+ const userId = await this.ports.currentUser();
15555
+ await this.state(userId);
15556
+ const temporary = join(this.root(userId), `${randomUUID()}.import`);
15557
+ try {
15558
+ await writeFile(temporary, data, {
15559
+ mode: 384,
15560
+ flag: "wx"
15561
+ });
15562
+ await this.assertUser(userId);
15563
+ return await this.stage(temporary, {
15564
+ ...metadata,
15565
+ size: data.length
15566
+ }, userId);
15567
+ } finally {
15568
+ await unlink(temporary).catch(() => {});
15569
+ }
15570
+ }
15571
+ async discard(taskRef) {
15572
+ const userId = await this.ports.currentUser();
15573
+ await this.exclusive(async () => {
15574
+ const state = await this.state(userId);
15575
+ await this.assertUser(userId);
15576
+ const task = state.tasks.find((value) => value.taskRef === taskRef);
15577
+ if (!task) return;
15578
+ if ([
15579
+ "queued",
15580
+ "uploading",
15581
+ "sending"
15582
+ ].includes(task.state)) throw fail("file-task-active", "任务仍在进行中,不能移除");
15583
+ state.tasks = state.tasks.filter((value) => value !== task);
15584
+ await this.save(userId, state);
15585
+ });
15586
+ }
15587
+ async reconcile(taskRef) {
15588
+ const userId = await this.ports.currentUser();
15589
+ return this.exclusive(async () => {
15590
+ const state = await this.state(userId);
15591
+ const task = state.tasks.find((value) => value.taskRef === taskRef);
15592
+ if (!task) throw fail("file-task-missing", "发送任务不存在");
15593
+ if (task.state !== "uncertain") return clone(task);
15594
+ const controller = new AbortController();
15595
+ this.controllers.add(controller);
15596
+ try {
15597
+ await this.assertUser(userId, controller.signal);
15598
+ const result = await this.ports.reconcile?.(task, controller.signal);
15599
+ await this.assertUser(userId, controller.signal);
15600
+ if (result) {
15601
+ task.result = result;
15602
+ task.state = "sent";
15603
+ delete task.error;
15604
+ await this.save(userId, state);
15605
+ }
15606
+ return clone(task);
15607
+ } finally {
15608
+ this.controllers.delete(controller);
15609
+ }
15610
+ });
15611
+ }
15612
+ async enqueue(input) {
15613
+ const userId = await this.ports.currentUser();
15614
+ if (!UUID.test(input.recordUid) || !UUID.test(input.relationUid) || !input.sourceRef.trim() || input.fileRefs.length < 1 || input.fileRefs.length > this.policy.maxAttachments || new Set(input.fileRefs).size !== input.fileRefs.length || (input.content.textContent?.length ?? 0) > 2e4) throw fail("file-send-invalid", "发送标识、正文或附件数量无效");
15615
+ await this.ports.validateSource(input.sourceRef);
15616
+ return this.exclusive(async () => {
15617
+ await this.assertUser(userId);
15618
+ const state = await this.state(userId);
15619
+ const prior = state.tasks.find((task) => task.recordUid === input.recordUid);
15620
+ if (prior) {
15621
+ if (prior.sourceRef !== input.sourceRef || prior.relationUid !== input.relationUid || JSON.stringify(prior.fileRefs) !== JSON.stringify(input.fileRefs) || JSON.stringify(prior.content) !== JSON.stringify(input.content)) throw fail("file-send-conflict", "发送标识已用于另一份内容");
15622
+ return clone(prior);
15623
+ }
15624
+ if (state.tasks.filter((task) => task.state !== "sent").length >= 100) throw fail("file-queue-full", "待发送文件过多,请先处理失败任务");
15625
+ const files = [];
15626
+ for (const ref of input.fileRefs) {
15627
+ const { file } = await this.readLocal(ref);
15628
+ files.push({
15629
+ ...file,
15630
+ progress: {
15631
+ phase: "preparing",
15632
+ sentBytes: 0,
15633
+ totalBytes: file.size
15634
+ }
15635
+ });
15636
+ }
15637
+ await this.assertUser(userId);
15638
+ const task = {
15639
+ ...clone(input),
15640
+ taskRef: `arkme-send-v1.${randomUUID()}`,
15641
+ createdAtMillis: Date.now(),
15642
+ state: "queued",
15643
+ files
15644
+ };
15645
+ state.tasks.push(task);
15646
+ try {
15647
+ await this.save(userId, state);
15648
+ } catch (error) {
15649
+ state.tasks = state.tasks.filter((value) => value !== task);
15650
+ throw error;
15651
+ }
15652
+ this.schedule(userId, state, task);
15653
+ return clone(task);
15654
+ });
15655
+ }
15656
+ async retry(taskRef) {
15657
+ const userId = await this.ports.currentUser();
15658
+ return this.exclusive(async () => {
15659
+ const state = await this.state(userId);
15660
+ const task = state.tasks.find((value) => value.taskRef === taskRef);
15661
+ if (!task) throw fail("file-task-missing", "发送任务不存在");
15662
+ await this.assertUser(userId);
15663
+ if (task.state === "uncertain") throw fail("file-send-uncertain", "发送结果待确认,请先核对原会话,不能自动重复发送");
15664
+ if (task.state !== "failed") return clone(task);
15665
+ task.state = "queued";
15666
+ delete task.error;
15667
+ for (const file of task.files) if (!file.asset) file.progress = {
15668
+ phase: "preparing",
15669
+ sentBytes: 0,
15670
+ totalBytes: file.size
15671
+ };
15672
+ await this.save(userId, state);
15673
+ this.schedule(userId, state, task);
15674
+ return clone(task);
15675
+ });
15676
+ }
15677
+ schedule(userId, state, task) {
15678
+ const controller = new AbortController();
15679
+ this.controllers.add(controller);
15680
+ this.queue = this.queue.catch(() => {}).then(async () => {
15681
+ try {
15682
+ await this.assertUser(userId, controller.signal);
15683
+ task.state = "uploading";
15684
+ await this.save(userId, state);
15685
+ for (const file of task.files) {
15686
+ await this.assertUser(userId, controller.signal);
15687
+ const stored = state.files[file.fileRef];
15688
+ if (!stored) throw fail("file-local-missing", "本地附件已不存在");
15689
+ if (!file.asset) {
15690
+ const cached = stored.asset ?? Object.values(state.files).find((other) => other.sha256 === stored.sha256 && other.fileKind === stored.fileKind && other.asset)?.asset;
15691
+ if (cached) file.asset = cached;
15692
+ if (!file.asset) file.asset = await this.ports.upload(this.path(userId, file.fileRef), stored, (progress) => {
15693
+ file.progress = progress;
15694
+ }, userId, controller.signal);
15695
+ file.asset = {
15696
+ ...file.asset,
15697
+ fileName: file.fileName
15698
+ };
15699
+ stored.asset = file.asset;
15700
+ file.progress = {
15701
+ phase: "ready",
15702
+ sentBytes: file.size,
15703
+ totalBytes: file.size
15704
+ };
15705
+ await this.save(userId, state);
15706
+ }
15707
+ }
15708
+ await this.assertUser(userId, controller.signal);
15709
+ task.state = "sending";
15710
+ await this.save(userId, state);
15711
+ task.result = await this.ports.send(task, task.files.map((file) => file.asset), userId, controller.signal);
15712
+ task.state = "sent";
15713
+ delete task.error;
15714
+ } catch (error) {
15715
+ const sending = task.state === "sending";
15716
+ task.state = sending ? "uncertain" : "failed";
15717
+ task.error = sending ? "发送结果待确认,请先核对会话,避免重复发送" : error instanceof ArkmePluginError ? error.message : "文件传输中断,请重试";
15718
+ } finally {
15719
+ this.controllers.delete(controller);
15720
+ await this.save(userId, state);
15721
+ }
15722
+ }).catch(() => {
15723
+ task.state = "uncertain";
15724
+ task.error = "本地发送状态保存失败,请先核对会话";
15725
+ });
15726
+ }
15727
+ async reception(mediaRef, start = false) {
15728
+ const userId = await this.ports.currentUser();
15729
+ if (!mediaRef.startsWith("arkme-media-v1.") || mediaRef.length > 100) throw fail("media-ref-invalid", "原文件引用无效");
15730
+ const state = await this.state(userId);
15731
+ const key = `${userId}:${mediaRef}`;
15732
+ const localRef = state.originals[mediaRef];
15733
+ if (localRef) try {
15734
+ const { file } = await this.readLocal(localRef);
15735
+ return {
15736
+ state: "ready",
15737
+ receivedBytes: file.size,
15738
+ totalBytes: file.size,
15739
+ file
15740
+ };
15741
+ } catch {
15742
+ delete state.originals[mediaRef];
15743
+ }
15744
+ const existing = this.receptions.get(key);
15745
+ if (existing?.state === "receiving" || !start) return clone(existing ?? {
15746
+ state: "missing",
15747
+ receivedBytes: 0,
15748
+ totalBytes: 0
15749
+ });
15750
+ if ([...this.receptions.values()].filter((value) => value.state === "receiving").length >= 2) throw fail("file-download-busy", "已有两个文件正在接收,请稍后重试");
15751
+ const progress = {
15752
+ state: "receiving",
15753
+ receivedBytes: 0,
15754
+ totalBytes: 0
15755
+ };
15756
+ this.receptions.set(key, progress);
15757
+ const controller = new AbortController();
15758
+ this.controllers.add(controller);
15759
+ const temporary = join(this.root(userId), `${randomUUID()}.download`);
15760
+ const job = (async () => {
15761
+ try {
15762
+ const { response, descriptor } = await this.ports.fetchMedia(mediaRef, controller.signal);
15763
+ if (!response.ok || !response.body) throw fail("file-download-failed", "原文件接收失败");
15764
+ const declared = Number(response.headers.get("content-length")) || descriptor.size;
15765
+ if (declared > this.policy.maxFileBytes) throw fail("file-download-too-large", "原文件超出本地文件大小限制");
15766
+ progress.totalBytes = Math.max(0, declared);
15767
+ const handle = await open(temporary, "wx", 384);
15768
+ try {
15769
+ for await (const chunk of response.body) {
15770
+ await this.assertUser(userId, controller.signal);
15771
+ progress.receivedBytes += chunk.length;
15772
+ if (progress.receivedBytes > this.policy.maxFileBytes) throw fail("file-download-too-large", "原文件超出本地文件大小限制");
15773
+ await handle.write(chunk);
15774
+ }
15775
+ } finally {
15776
+ await handle.close();
15777
+ }
15778
+ if (!progress.receivedBytes || declared > 0 && progress.receivedBytes !== declared || descriptor.size > 0 && progress.receivedBytes !== descriptor.size) throw fail("file-download-incomplete", "原文件接收不完整,请重试");
15779
+ await this.assertUser(userId, controller.signal);
15780
+ const file = await this.stage(temporary, {
15781
+ fileName: descriptor.fileName,
15782
+ mimeType: descriptor.mimeType,
15783
+ size: progress.receivedBytes
15784
+ }, userId);
15785
+ state.originals[mediaRef] = file.fileRef;
15786
+ await this.save(userId, state);
15787
+ progress.file = file;
15788
+ progress.state = "ready";
15789
+ } catch (error) {
15790
+ progress.state = "failed";
15791
+ progress.error = error instanceof ArkmePluginError ? error.message : "文件接收失败,请重试";
15792
+ } finally {
15793
+ await unlink(temporary).catch(() => {});
15794
+ this.controllers.delete(controller);
15795
+ }
15796
+ })();
15797
+ this.jobs.add(job);
15798
+ job.finally(() => this.jobs.delete(job));
15799
+ return clone(progress);
15800
+ }
15801
+ };
15802
+ //#endregion
15803
+ //#region src/file-transfer-owner.ts
15804
+ function createArkmeFileTransfers(options) {
15805
+ if (options.directory === void 0) return void 0;
15806
+ return new FileTransfers(options.directory, {
15807
+ currentUser: async () => (await options.runtime.requireSession()).userId,
15808
+ validateSource: async (sourceRef) => {
15809
+ await options.source.openSourceRef(sourceRef, (await options.runtime.requireSession()).userId);
15810
+ },
15811
+ upload: async (path, metadata, onProgress, expectedUserId, signal) => await options.media.uploadLocalFile(path, metadata, {
15812
+ onProgress,
15813
+ expectedUserId,
15814
+ signal
15815
+ }),
15816
+ send: async (input, assets, expectedUserId, signal) => await options.chat.sendSourceRich(input.sourceRef, {
15817
+ ...input.content,
15818
+ assets
15819
+ }, {
15820
+ recordUid: input.recordUid,
15821
+ relationUid: input.relationUid,
15822
+ expectedUserId,
15823
+ signal
15824
+ }),
15825
+ fetchMedia: async (ref, signal) => await options.media.fetchMedia(ref, void 0, signal, true),
15826
+ ...options.openPath === void 0 ? {} : { openPath: options.openPath },
15827
+ reconcile: async (input, signal) => {
15828
+ const item = (await options.chat.readSource(input.sourceRef, {
15829
+ limit: 100,
15830
+ signal
15831
+ })).items.find((value) => value.itemUid === input.recordUid && value.isMe);
15832
+ return item === void 0 ? void 0 : {
15833
+ sourceRef: input.sourceRef,
15834
+ itemUid: item.itemUid,
15835
+ status: item.status,
15836
+ ...item.sequence === void 0 ? {} : { sequence: item.sequence },
15837
+ localState: "synced"
15838
+ };
15839
+ }
15840
+ }, options.maxUploadBytes ?? 100 * 1024 * 1024);
15841
+ }
15842
+ //#endregion
14592
15843
  //#region src/services/unmarked-speaker-service.ts
14593
15844
  const REF_TTL_MS = 30 * 6e4;
14594
15845
  const REF_CAP = 2e3;
@@ -17259,6 +18510,8 @@ var ArkmeService = class {
17259
18510
  contactDirectory;
17260
18511
  unmarkedSpeaker;
17261
18512
  voiceprint;
18513
+ fileTransfers;
18514
+ localFileOpener;
17262
18515
  worldVoiceprintInviteVariantIndex = 0;
17263
18516
  constructor(config, sessionStore, stateStore, fetchImpl = fetch, pendingSessionStore, outgoingCallBroker = new ArkmeOutgoingCallBroker(), billingGateway) {
17264
18517
  this.config = config;
@@ -17313,9 +18566,74 @@ var ArkmeService = class {
17313
18566
  this.clearAccountState(userIds);
17314
18567
  }
17315
18568
  });
18569
+ this.fileTransfers = createArkmeFileTransfers({
18570
+ directory: config.fileStateDirectory,
18571
+ maxUploadBytes: config.maxUploadBytes,
18572
+ runtime: this.runtime,
18573
+ source: this.source,
18574
+ media: this.media,
18575
+ chat: this.chat,
18576
+ openPath: async (path, signal) => {
18577
+ if (this.localFileOpener === void 0) throw new Error("当前 DSH 宿主未提供本机文件打开能力");
18578
+ await this.localFileOpener(path, signal);
18579
+ }
18580
+ });
18581
+ }
18582
+ filesOwner() {
18583
+ if (!this.fileTransfers) throw new ArkmePluginError("file-flow-unavailable", "当前宿主不支持本地文件流程,请升级插件", false, 501);
18584
+ return this.fileTransfers;
18585
+ }
18586
+ fileCapabilities() {
18587
+ return this.filesOwner().capabilities();
18588
+ }
18589
+ async fileSearch(options) {
18590
+ return await this.search.searchFiles(options);
18591
+ }
18592
+ async fileSessionUser() {
18593
+ return (await this.runtime.requireSession()).userId;
18594
+ }
18595
+ async fileStage(path, metadata, expectedUserId) {
18596
+ return await this.filesOwner().stage(path, metadata, expectedUserId);
18597
+ }
18598
+ async fileList() {
18599
+ return await this.filesOwner().files();
18600
+ }
18601
+ async fileReadLocal(ref) {
18602
+ return await this.filesOwner().readLocal(ref);
18603
+ }
18604
+ attachLocalFileOpener(openPath) {
18605
+ this.localFileOpener = openPath;
18606
+ }
18607
+ async fileOpenLocal(ref) {
18608
+ return await this.filesOwner().openLocal(ref);
18609
+ }
18610
+ async fileRemove(ref) {
18611
+ await this.filesOwner().remove(ref);
18612
+ }
18613
+ async fileSend(input) {
18614
+ return await this.filesOwner().enqueue(input);
18615
+ }
18616
+ async fileSendTasks(sourceRef) {
18617
+ return await this.filesOwner().tasks(sourceRef);
18618
+ }
18619
+ async fileSendRetry(taskRef) {
18620
+ return await this.filesOwner().retry(taskRef);
18621
+ }
18622
+ async fileStageBytes(contentBase64, metadata) {
18623
+ return await this.filesOwner().stageBytes(contentBase64, metadata);
18624
+ }
18625
+ async fileSendDiscard(taskRef) {
18626
+ return await this.filesOwner().discard(taskRef);
18627
+ }
18628
+ async fileSendReconcile(taskRef) {
18629
+ return await this.filesOwner().reconcile(taskRef);
18630
+ }
18631
+ async fileReceive(mediaRef, start = false) {
18632
+ return await this.filesOwner().reception(mediaRef, start);
17316
18633
  }
17317
18634
  clearAccountState(userIds) {
17318
18635
  for (const userId of userIds) this.privacy.clear(userId);
18636
+ this.fileTransfers?.cancelActive();
17319
18637
  for (const userId of userIds) this.outgoingCall.clearUser(userId, "账号已退出,呼叫已取消");
17320
18638
  this.source.dispose();
17321
18639
  this.media.dispose();
@@ -17398,7 +18716,9 @@ var ArkmeService = class {
17398
18716
  return await this.bot.openBotPrivateChat(botRef, options);
17399
18717
  }
17400
18718
  async sendBotPrivateChatMessage(botRef, content, options = {}) {
17401
- return await this.bot.sendBotPrivateChatMessage(botRef, content, options);
18719
+ const outcome = await this.bot.sendBotPrivateChatMessage(botRef, content, options);
18720
+ if (outcome.recordProjectionChanged) await this.realtime.invalidateRecordProjection();
18721
+ return outcome.result;
17402
18722
  }
17403
18723
  async listGroupBots(groupSourceRef, options = {}) {
17404
18724
  return await this.bot.listGroupBots(groupSourceRef, options);
@@ -17461,6 +18781,7 @@ var ArkmeService = class {
17461
18781
  retryOutbox: true,
17462
18782
  revisionPolling: true,
17463
18783
  userProfile: true,
18784
+ accountSettings: true,
17464
18785
  imageRead: true,
17465
18786
  recordCalendar: true,
17466
18787
  imageLibrary: true,
@@ -17481,6 +18802,7 @@ var ArkmeService = class {
17481
18802
  contactAdd: true,
17482
18803
  conversationQuickAdd: true,
17483
18804
  groupSettings: true,
18805
+ groupAiPolish: true,
17484
18806
  extensionManagement: true,
17485
18807
  extensionMetadataEdit: true,
17486
18808
  extensionIcons: true,
@@ -17550,6 +18872,7 @@ var ArkmeService = class {
17550
18872
  return await this.callHistory.retryCallSummary(callRef, signal);
17551
18873
  }
17552
18874
  dispose() {
18875
+ this.fileTransfers?.cancelActive();
17553
18876
  this.contact.dispose();
17554
18877
  this.contactDirectory.dispose();
17555
18878
  this.unmarkedSpeaker.dispose();
@@ -17747,6 +19070,12 @@ var ArkmeService = class {
17747
19070
  async generateGroupAiPolishRule(groupName, requirement, options = {}) {
17748
19071
  return await this.aiPolish.generateGroupAiPolishRule(groupName, requirement, options);
17749
19072
  }
19073
+ prepareEnableGroupAiPolish(groupName, ruleName = "", options = {}) {
19074
+ return this.aiPolish.prepareEnableGroupAiPolish(groupName, ruleName, options);
19075
+ }
19076
+ prepareEnableGroupAiPolishRuleForSource(sourceRef, ruleRef, options = {}) {
19077
+ return this.aiPolish.prepareEnableGroupAiPolishRuleForSource(sourceRef, ruleRef, options);
19078
+ }
17750
19079
  async confirmEnableGroupAiPolish(confirmationRef, options = {}) {
17751
19080
  return await this.aiPolish.confirmEnableGroupAiPolish(confirmationRef, options);
17752
19081
  }
@@ -19768,4 +21097,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
19768
21097
  return jsonValue(await handler(jsonValue(args)));
19769
21098
  }
19770
21099
  //#endregion
19771
- export { isArkmeBotAvatarRef as $, inspectNativeBundleArtifactV3 as A, ARKME_EXTENSION_ICON_MAX_BYTES as B, ARKME_BUNDLE_MAX_BYTES as C, bundleSha256 as D, arkmeSandboxEntryId as E, assertExtensionArtifactSize as F, validateVoiceprintPcm16Wav as G, ARKME_EXTENSION_PREVIEW_MAX_BYTES as H, canonicalExtensionJson as I, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as J, ARKME_DEFAULT_SHARE_WEBSITE as K, sha256Hex as L, packLocalNativeBundleDirectoryV3 as M, readNativeBundleTarballV3 as N, canonicalBundleJson as O, renderArkmeSandboxHostEntry as P, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as Q, unpackArkmeExtension as R, ARKME_BUNDLE_ARTIFACT_KIND as S, ArkmeBundleArtifactError as T, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as U, ARKME_EXTENSION_MAX_BYTES as V, ArkmeService as W, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as X, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as Y, ARKME_WORLD_PUBLISH_MAX_IMAGES as Z, arkmeClientContentDigest as _, persistentArkmeExtensionActive as a, verifyExtensionResolutionSignature as b, persistentExtensionBundleDirectory as c, readPersistentExtensionActivation as d, ArkmePluginError as et, refreshPersistentClientWrapper as f, activeProfileExtensionOwnerConflicts as g, renderArkmeBundleClientBundle as h, invokePersistentArkmeExtension as i, packBundleFiles as j, inspectBundleArtifact as k, persistentExtensionPackageIdentity as l, writePersistentExtensionActivation as m, applyPersistentArkmeHostExtension as n, persistentArkmeExtensionRuntimeState as o, repairPersistentExtensionInstallation as p, ARKME_PROVIDER_CONTRACT_VERSION as q, deactivatePersistentArkmeExtension as r, materializePersistentExtensionBundle as s, activatePersistentArkmeExtension as t, ArkmeOutgoingCallError as tt, quarantinePersistentExtension as u, arkmeClientContentDigestFromSource as v, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as w, verifyNativeBundleResolutionSignature as x, verifyBundleResolutionSignature as y, ARKME_EXTENSION_FORMAT as z };
21100
+ export { ARKME_RUNTIME_INSTANCE_ID as $, inspectNativeBundleArtifactV3 as A, ARKME_EXTENSION_ICON_MAX_BYTES as B, ARKME_BUNDLE_MAX_BYTES as C, bundleSha256 as D, arkmeSandboxEntryId as E, assertExtensionArtifactSize as F, validateVoiceprintPcm16Wav as G, ARKME_EXTENSION_PREVIEW_MAX_BYTES as H, canonicalExtensionJson as I, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as J, ARKME_DEFAULT_SHARE_WEBSITE as K, sha256Hex as L, packLocalNativeBundleDirectoryV3 as M, readNativeBundleTarballV3 as N, canonicalBundleJson as O, renderArkmeSandboxHostEntry as P, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as Q, unpackArkmeExtension as R, ARKME_BUNDLE_ARTIFACT_KIND as S, ArkmeBundleArtifactError as T, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as U, ARKME_EXTENSION_MAX_BYTES as V, ArkmeService as W, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as X, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as Y, ARKME_WORLD_PUBLISH_MAX_IMAGES as Z, arkmeClientContentDigest as _, persistentArkmeExtensionActive as a, ArkmePluginError as at, verifyExtensionResolutionSignature as b, persistentExtensionBundleDirectory as c, readPersistentExtensionActivation as d, ARKME_TOOL_FILE_MAX_BYTES as et, refreshPersistentClientWrapper as f, activeProfileExtensionOwnerConflicts as g, renderArkmeBundleClientBundle as h, invokePersistentArkmeExtension as i, isArkmeBotAvatarRef as it, packBundleFiles as j, inspectBundleArtifact as k, persistentExtensionPackageIdentity as l, writePersistentExtensionActivation as m, applyPersistentArkmeHostExtension as n, arkmeNormalizedFileMimeType as nt, persistentArkmeExtensionRuntimeState as o, ArkmeOutgoingCallError as ot, repairPersistentExtensionInstallation as p, ARKME_PROVIDER_CONTRACT_VERSION as q, deactivatePersistentArkmeExtension as r, arkmePickedFileKind as rt, materializePersistentExtensionBundle as s, activatePersistentArkmeExtension as t, arkmeCanInlineLocalFile as tt, quarantinePersistentExtension as u, arkmeClientContentDigestFromSource as v, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as w, verifyNativeBundleResolutionSignature as x, verifyBundleResolutionSignature as y, ARKME_EXTENSION_FORMAT as z };