@senguoyun/dsh-arkme 0.1.29 → 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 (133) 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 +8486 -1506
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +633 -28
  8. package/lib/persistent-extension.js +1 -1
  9. package/lib/{persistent-runtime-DLKV_LTp.js → persistent-runtime-BJ8konXI.js} +2319 -144
  10. package/lib/sdk.js +198 -0
  11. package/lib/types/arkme-service.d.ts +58 -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/ArkmeNoteDetails.d.ts +3 -1
  31. package/lib/types/client/ArkmeNoteDetails.d.ts.map +1 -1
  32. package/lib/types/client/ArkmePersistentShell.d.ts +3 -0
  33. package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
  34. package/lib/types/client/ArkmeProductNavigation.d.ts +2 -1
  35. package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -1
  36. package/lib/types/client/ArkmeRichContent.d.ts +28 -4
  37. package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
  38. package/lib/types/client/ArkmeSearchSurface.d.ts.map +1 -1
  39. package/lib/types/client/ArkmeSettingsSurface.d.ts +5 -1
  40. package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -1
  41. package/lib/types/client/ArkmeSidebar.d.ts +15 -0
  42. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  43. package/lib/types/client/ArkmeUpdateSurfaces.d.ts +5 -39
  44. package/lib/types/client/ArkmeUpdateSurfaces.d.ts.map +1 -1
  45. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +9 -2
  46. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  47. package/lib/types/client/ArkmeWebLoginOverlay.d.ts +6 -0
  48. package/lib/types/client/ArkmeWebLoginOverlay.d.ts.map +1 -0
  49. package/lib/types/client/DeepSeekHarnessSurface.d.ts +7 -4
  50. package/lib/types/client/DeepSeekHarnessSurface.d.ts.map +1 -1
  51. package/lib/types/client/api.d.ts +1 -1
  52. package/lib/types/client/api.d.ts.map +1 -1
  53. package/lib/types/client/arkme-auth-flow.d.ts +4 -0
  54. package/lib/types/client/arkme-auth-flow.d.ts.map +1 -1
  55. package/lib/types/client/composer-draft-store.d.ts +25 -2
  56. package/lib/types/client/composer-draft-store.d.ts.map +1 -1
  57. package/lib/types/client/file-send-tasks.d.ts +12 -0
  58. package/lib/types/client/file-send-tasks.d.ts.map +1 -0
  59. package/lib/types/client/index.d.ts +1 -0
  60. package/lib/types/client/index.d.ts.map +1 -1
  61. package/lib/types/client/plugin-update-store.d.ts +28 -1
  62. package/lib/types/client/plugin-update-store.d.ts.map +1 -1
  63. package/lib/types/client/read-retry.d.ts +7 -0
  64. package/lib/types/client/read-retry.d.ts.map +1 -0
  65. package/lib/types/client/ui-controller.d.ts +4 -0
  66. package/lib/types/client/ui-controller.d.ts.map +1 -1
  67. package/lib/types/client/update-presentation.d.ts +42 -0
  68. package/lib/types/client/update-presentation.d.ts.map +1 -0
  69. package/lib/types/extensions/manager.d.ts +4 -0
  70. package/lib/types/extensions/manager.d.ts.map +1 -1
  71. package/lib/types/extensions/persistent-bundle.d.ts +14 -0
  72. package/lib/types/extensions/persistent-bundle.d.ts.map +1 -1
  73. package/lib/types/extensions/persistent-client-bundle.d.ts +1 -1
  74. package/lib/types/extensions/persistent-client-bundle.d.ts.map +1 -1
  75. package/lib/types/extensions/persistent-runtime.d.ts.map +1 -1
  76. package/lib/types/extensions/profile-installer.d.ts.map +1 -1
  77. package/lib/types/file-transfer-contract.d.ts +63 -0
  78. package/lib/types/file-transfer-contract.d.ts.map +1 -0
  79. package/lib/types/file-transfer-owner.d.ts +17 -0
  80. package/lib/types/file-transfer-owner.d.ts.map +1 -0
  81. package/lib/types/harness-embed-contract.d.ts +2 -0
  82. package/lib/types/harness-embed-contract.d.ts.map +1 -0
  83. package/lib/types/harness-embed-route.d.ts +32 -0
  84. package/lib/types/harness-embed-route.d.ts.map +1 -0
  85. package/lib/types/host-api.d.ts.map +1 -1
  86. package/lib/types/index.d.ts.map +1 -1
  87. package/lib/types/plugin-update.d.ts.map +1 -1
  88. package/lib/types/rich-media-routes.d.ts +2 -1
  89. package/lib/types/rich-media-routes.d.ts.map +1 -1
  90. package/lib/types/runtime-instance.d.ts +8 -0
  91. package/lib/types/runtime-instance.d.ts.map +1 -0
  92. package/lib/types/sdk/index.d.ts +65 -2
  93. package/lib/types/sdk/index.d.ts.map +1 -1
  94. package/lib/types/services/bot-service.d.ts +15 -1
  95. package/lib/types/services/bot-service.d.ts.map +1 -1
  96. package/lib/types/services/chat-realtime-service.d.ts +1 -1
  97. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  98. package/lib/types/services/chat-service.d.ts +39 -4
  99. package/lib/types/services/chat-service.d.ts.map +1 -1
  100. package/lib/types/services/file-transfers.d.ts +65 -0
  101. package/lib/types/services/file-transfers.d.ts.map +1 -0
  102. package/lib/types/services/group-ai-polish-service.d.ts +17 -2
  103. package/lib/types/services/group-ai-polish-service.d.ts.map +1 -1
  104. package/lib/types/services/media-service.d.ts +11 -1
  105. package/lib/types/services/media-service.d.ts.map +1 -1
  106. package/lib/types/services/profile-service.d.ts +1 -0
  107. package/lib/types/services/profile-service.d.ts.map +1 -1
  108. package/lib/types/services/search-service.d.ts +6 -0
  109. package/lib/types/services/search-service.d.ts.map +1 -1
  110. package/lib/types/services/service.d.ts +1 -0
  111. package/lib/types/services/service.d.ts.map +1 -1
  112. package/lib/types/services/source-service.d.ts +2 -0
  113. package/lib/types/services/source-service.d.ts.map +1 -1
  114. package/lib/types/tools/business/conversation/copy-link-extend.d.ts +2 -0
  115. package/lib/types/tools/business/conversation/copy-link-extend.d.ts.map +1 -0
  116. package/lib/types/tools/business/conversation/group-ai-polish.d.ts.map +1 -1
  117. package/lib/types/tools/business/index.d.ts.map +1 -1
  118. package/lib/types/tools/business/media/files.d.ts +2 -0
  119. package/lib/types/tools/business/media/files.d.ts.map +1 -0
  120. package/lib/types/tools/business/media/index.d.ts.map +1 -1
  121. package/lib/types/tools/ports/bots.d.ts +1 -1
  122. package/lib/types/tools/ports/bots.d.ts.map +1 -1
  123. package/lib/types/tools/ports/conversations.d.ts +7 -1
  124. package/lib/types/tools/ports/conversations.d.ts.map +1 -1
  125. package/lib/types/tools/ports/files.d.ts +22 -0
  126. package/lib/types/tools/ports/files.d.ts.map +1 -0
  127. package/lib/types/tools/ports/index.d.ts +2 -1
  128. package/lib/types/tools/ports/index.d.ts.map +1 -1
  129. package/lib/types/tools/prompts/business.d.ts.map +1 -1
  130. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  131. package/lib/types/types.d.ts +124 -2
  132. package/lib/types/types.d.ts.map +1 -1
  133. package/package.json +5 -5
@@ -1,10 +1,11 @@
1
1
  import { chmodSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicKey, randomBytes, randomUUID, timingSafeEqual, verify } from "node:crypto";
3
- import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
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)));
@@ -7077,6 +7496,18 @@ const ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES = 20 * 1024 * 1024;
7077
7496
  //#region src/services/chat-service.ts
7078
7497
  const OFFICIAL_AUTHOR_USER_ID = 11;
7079
7498
  const OFFICIAL_AUTHOR_FALLBACK_DISPLAY_NAME = "即我作者";
7499
+ const MAX_MESSAGE_COPY_LINK_ITEMS = 100;
7500
+ const LINK_METADATA_MAX_BYTES = 128 * 1024;
7501
+ const LINK_METADATA_MAX_REDIRECTS = 3;
7502
+ const RECORD_UID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7503
+ const HTML_ENTITY_MAP = {
7504
+ amp: "&",
7505
+ lt: "<",
7506
+ gt: ">",
7507
+ quot: "\"",
7508
+ apos: "'",
7509
+ nbsp: " "
7510
+ };
7080
7511
  function numberValue$16(value) {
7081
7512
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
7082
7513
  }
@@ -7101,6 +7532,88 @@ function favoriteStickerPersistenceItem(item) {
7101
7532
  function listValue$16(value) {
7102
7533
  return Array.isArray(value) ? value : [];
7103
7534
  }
7535
+ function htmlTextValue(value) {
7536
+ return value.replace(/<script[\s\S]*?<\/script>/giu, " ").replace(/<style[\s\S]*?<\/style>/giu, " ").replace(/<[^>]+>/gu, " ").replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/giu, (_match, entity) => {
7537
+ const normalized = entity.toLowerCase();
7538
+ if (normalized.startsWith("#x")) {
7539
+ const code = Number.parseInt(normalized.slice(2), 16);
7540
+ return Number.isFinite(code) ? String.fromCodePoint(code) : " ";
7541
+ }
7542
+ if (normalized.startsWith("#")) {
7543
+ const code = Number.parseInt(normalized.slice(1), 10);
7544
+ return Number.isFinite(code) ? String.fromCodePoint(code) : " ";
7545
+ }
7546
+ return HTML_ENTITY_MAP[normalized] ?? " ";
7547
+ }).replace(/\s+/gu, " ").trim();
7548
+ }
7549
+ function firstHtmlMetaContent(html, key) {
7550
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
7551
+ const patterns = [new RegExp(`<meta\\b(?=[^>]*(?:property|name)=["']${escaped}["'])(?=[^>]*content=["']([^"']*)["'])[^>]*>`, "iu"), new RegExp(`<meta\\b(?=[^>]*content=["']([^"']*)["'])(?=[^>]*(?:property|name)=["']${escaped}["'])[^>]*>`, "iu")];
7552
+ for (const pattern of patterns) {
7553
+ const value = pattern.exec(html)?.[1];
7554
+ if (value !== void 0) return htmlTextValue(value);
7555
+ }
7556
+ return "";
7557
+ }
7558
+ function fallbackLinkTitle(url) {
7559
+ const host = url.hostname.replace(/^www\./iu, "");
7560
+ if (host === "github.com") {
7561
+ const parts = url.pathname.split("/").filter(Boolean);
7562
+ if (parts.length >= 4 && parts[2] === "pull") return `Pull Request #${parts[3]} · ${parts[0]}/${parts[1]}`;
7563
+ if (parts.length >= 2) return `${parts[0]}/${parts[1]}`;
7564
+ }
7565
+ if (host === "codeup.aliyun.com" || host.endsWith(".codeup.aliyun.com")) {
7566
+ const parts = url.pathname.split("/").filter(Boolean);
7567
+ const changeIndex = parts.indexOf("change");
7568
+ const repository = changeIndex >= 2 ? parts[changeIndex - 1] : parts.at(-1);
7569
+ const changeNo = changeIndex >= 0 ? parts[changeIndex + 1] : void 0;
7570
+ if (repository !== void 0 && changeNo !== void 0) return `${repository} · Change #${changeNo}`;
7571
+ }
7572
+ return "分享链接";
7573
+ }
7574
+ function ensureMetadataUrl(rawUrl) {
7575
+ let url;
7576
+ try {
7577
+ url = new URL(rawUrl.trim());
7578
+ } catch {
7579
+ throw new ArkmePluginError("link-metadata-url-invalid", "链接暂不可预览", false, 400);
7580
+ }
7581
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new ArkmePluginError("link-metadata-url-invalid", "链接暂不可预览", false, 400);
7582
+ if (url.username !== "" || url.password !== "") throw new ArkmePluginError("link-metadata-url-forbidden", "链接暂不可预览", false, 403);
7583
+ const hostname = url.hostname.toLowerCase();
7584
+ if (hostname === "localhost" || hostname.endsWith(".local")) throw new ArkmePluginError("link-metadata-url-forbidden", "链接暂不可预览", false, 403);
7585
+ if (/^(?:127|10|0|169\.254|192\.168)\./u.test(hostname) || /^172\.(?:1[6-9]|2\d|3[0-1])\./u.test(hostname)) throw new ArkmePluginError("link-metadata-url-forbidden", "链接暂不可预览", false, 403);
7586
+ if (hostname === "::1" || hostname.startsWith("fc") || hostname.startsWith("fd") || hostname.startsWith("fe80")) throw new ArkmePluginError("link-metadata-url-forbidden", "链接暂不可预览", false, 403);
7587
+ return url;
7588
+ }
7589
+ async function readResponseTextPrefix(response) {
7590
+ if (response.body === null) return await response.text();
7591
+ const reader = response.body.getReader();
7592
+ const chunks = [];
7593
+ let bytes = 0;
7594
+ try {
7595
+ while (bytes < LINK_METADATA_MAX_BYTES) {
7596
+ const next = await reader.read();
7597
+ if (next.done) break;
7598
+ chunks.push(next.value);
7599
+ bytes += next.value.byteLength;
7600
+ }
7601
+ } finally {
7602
+ reader.releaseLock();
7603
+ }
7604
+ return new TextDecoder("utf-8", { fatal: false }).decode(Uint8Array.from(chunks.flatMap((chunk) => [...chunk])));
7605
+ }
7606
+ function linkMetadataFromHtml(url, html) {
7607
+ const title = firstHtmlMetaContent(html, "og:title") || firstHtmlMetaContent(html, "twitter:title") || htmlTextValue(/<title\b[^>]*>([\s\S]*?)<\/title>/iu.exec(html)?.[1] ?? "") || fallbackLinkTitle(url);
7608
+ const description = firstHtmlMetaContent(html, "og:description") || firstHtmlMetaContent(html, "description");
7609
+ const siteName = firstHtmlMetaContent(html, "og:site_name") || url.hostname.replace(/^www\./iu, "");
7610
+ return {
7611
+ url: url.toString(),
7612
+ title,
7613
+ ...description === "" ? {} : { description },
7614
+ ...siteName === "" ? {} : { siteName }
7615
+ };
7616
+ }
7104
7617
  function chatMemberRole$1(value) {
7105
7618
  if (value === "owner" || value === 1) return "owner";
7106
7619
  if (value === "admin" || value === 2) return "admin";
@@ -7121,6 +7634,185 @@ function integerLikeValue$1(value) {
7121
7634
  }
7122
7635
  return 0;
7123
7636
  }
7637
+ function epochMillisValue(value) {
7638
+ const timestamp = integerLikeValue$1(value);
7639
+ return timestamp > 0 && timestamp < 1e11 ? timestamp * 1e3 : timestamp;
7640
+ }
7641
+ function messageCopyLinkMediaItemFromData(value) {
7642
+ const data = objectValue$1(value);
7643
+ return {
7644
+ fileKind: integerLikeValue$1(data.file_kind),
7645
+ fileName: stringValue$1(data.file_name),
7646
+ size: Math.max(0, integerLikeValue$1(data.size))
7647
+ };
7648
+ }
7649
+ function messageCopyLinkStructuredContentFromData(value) {
7650
+ const data = objectValue$1(value);
7651
+ if (Object.keys(data).length === 0) return void 0;
7652
+ return {
7653
+ structuredKind: integerLikeValue$1(data.structured_kind),
7654
+ durationMillis: Math.max(0, integerLikeValue$1(data.duration_millis))
7655
+ };
7656
+ }
7657
+ function messageCopyLinkSnapshotItemFromData(value) {
7658
+ const data = objectValue$1(value);
7659
+ const structuredContent = messageCopyLinkStructuredContentFromData(data.structured_content);
7660
+ const recordUid = stringValue$1(data.record_uid ?? data.recordUid ?? data.uid).trim();
7661
+ const item = {
7662
+ ...recordUid === "" ? {} : { recordUid },
7663
+ sourceKind: stringValue$1(data.source_kind),
7664
+ senderDisplayName: stringValue$1(data.sender_display_name),
7665
+ senderAvatarUrl: stringValue$1(data.sender_avatar_url),
7666
+ title: stringValue$1(data.title),
7667
+ textContent: stringValue$1(data.text_content),
7668
+ sendAtMillis: epochMillisValue(data.send_at),
7669
+ templateKind: integerLikeValue$1(data.template_kind),
7670
+ displayKind: integerLikeValue$1(data.display_kind),
7671
+ officialMark: integerLikeValue$1(data.official_mark),
7672
+ mediaItems: listValue$16(data.media_items).map(messageCopyLinkMediaItemFromData),
7673
+ ...structuredContent === void 0 ? {} : { structuredContent }
7674
+ };
7675
+ if (item.sendAtMillis <= 0 || item.senderDisplayName.trim() === "" && item.sourceKind !== "agent_message") throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
7676
+ if (item.title.trim() === "" && item.textContent.trim() === "" && item.mediaItems.length === 0 && item.structuredContent === void 0) throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
7677
+ return item;
7678
+ }
7679
+ function messageCopyLinkPresentationNodesFromData(value, itemCount, depth = 1, seenItemIndexes = /* @__PURE__ */ new Set()) {
7680
+ const rawNodes = listValue$16(value);
7681
+ if (rawNodes.length === 0) return [];
7682
+ if (depth > 8) throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
7683
+ return rawNodes.map((rawNode) => {
7684
+ const data = objectValue$1(rawNode);
7685
+ const kind = stringValue$1(data.kind);
7686
+ if (kind === "item") {
7687
+ const itemIndex = integerLikeValue$1(data.item_index);
7688
+ if (itemIndex < 0 || itemIndex >= itemCount || seenItemIndexes.has(itemIndex)) throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
7689
+ seenItemIndexes.add(itemIndex);
7690
+ return {
7691
+ kind: "item",
7692
+ itemIndex
7693
+ };
7694
+ }
7695
+ if (kind === "forward_bundle") return {
7696
+ kind: "forward_bundle",
7697
+ title: stringValue$1(data.title),
7698
+ commentText: stringValue$1(data.comment_text),
7699
+ createdAtMillis: integerLikeValue$1(data.created_at),
7700
+ senderDisplayName: stringValue$1(data.sender_display_name),
7701
+ children: messageCopyLinkPresentationNodesFromData(data.children, itemCount, depth + 1, seenItemIndexes)
7702
+ };
7703
+ throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
7704
+ });
7705
+ }
7706
+ function messageCopyLinkSourceAnchorFromData(value) {
7707
+ const data = objectValue$1(value);
7708
+ return {
7709
+ relationUid: stringValue$1(data.rel_uid),
7710
+ recordUid: stringValue$1(data.record_uid),
7711
+ recordOwnerUserId: integerLikeValue$1(data.record_owner_user_id),
7712
+ sequence: integerLikeValue$1(data.seq)
7713
+ };
7714
+ }
7715
+ function firstTextValue(source, keys) {
7716
+ for (const key of keys) {
7717
+ const value = stringValue$1(source[key]).trim();
7718
+ if (value !== "") return value;
7719
+ }
7720
+ return "";
7721
+ }
7722
+ function messageCopyLinkExtensionItemFromData(value) {
7723
+ const data = objectValue$1(value);
7724
+ const core = objectValue$1(data.record_core ?? data.recordCore);
7725
+ const recordUid = firstTextValue(data, [
7726
+ "record_uid",
7727
+ "recordUid",
7728
+ "uid"
7729
+ ]) || firstTextValue(core, [
7730
+ "record_uid",
7731
+ "recordUid",
7732
+ "uid"
7733
+ ]);
7734
+ const senderDisplayName = firstTextValue(data, [
7735
+ "sender_display_name",
7736
+ "senderDisplayName",
7737
+ "nickname",
7738
+ "nick_name",
7739
+ "nickName",
7740
+ "owner_name",
7741
+ "ownerName"
7742
+ ]) || firstTextValue(core, [
7743
+ "sender_display_name",
7744
+ "senderDisplayName",
7745
+ "nickname",
7746
+ "nick_name",
7747
+ "nickName",
7748
+ "owner_name",
7749
+ "ownerName"
7750
+ ]) || "Arkme用户";
7751
+ const sendAtMillis = epochMillisValue(data.send_at ?? data.sendAt ?? data.created_at ?? data.createdAt ?? data.published_at ?? data.publishedAt ?? core.send_at ?? core.sendAt ?? core.created_at ?? core.createdAt ?? core.published_at ?? core.publishedAt);
7752
+ const title = stringValue$1(data.title ?? core.title);
7753
+ const textContent = stringValue$1(data.text_content ?? data.textContent ?? data.content ?? core.text_content ?? core.textContent ?? core.content);
7754
+ const mediaItems = listValue$16(data.media_items ?? data.mediaItems ?? core.media_items ?? core.mediaItems).map(messageCopyLinkMediaItemFromData);
7755
+ const structuredContent = messageCopyLinkStructuredContentFromData(data.structured_content ?? data.structuredContent ?? core.structured_content ?? core.structuredContent);
7756
+ if (recordUid === "" || sendAtMillis <= 0 || title.trim() === "" && textContent.trim() === "" && mediaItems.length === 0 && structuredContent === void 0) return;
7757
+ return {
7758
+ recordUid,
7759
+ level: Math.max(2, integerLikeValue$1(data.level) || 2),
7760
+ sourceKind: stringValue$1(data.source_kind ?? data.sourceKind) || "record_extension",
7761
+ senderDisplayName,
7762
+ senderAvatarUrl: firstTextValue(data, [
7763
+ "sender_avatar_url",
7764
+ "senderAvatarUrl",
7765
+ "avatar_ref",
7766
+ "avatarRef",
7767
+ "avatar_url",
7768
+ "avatarUrl",
7769
+ "avatar"
7770
+ ]),
7771
+ title,
7772
+ textContent,
7773
+ sendAtMillis,
7774
+ templateKind: integerLikeValue$1(data.template_kind ?? data.templateKind ?? core.template_kind ?? core.templateKind) || 1,
7775
+ displayKind: integerLikeValue$1(data.display_kind ?? data.displayKind ?? core.display_kind ?? core.displayKind),
7776
+ officialMark: integerLikeValue$1(data.official_mark ?? data.officialMark ?? core.official_mark ?? core.officialMark),
7777
+ mediaItems,
7778
+ ...structuredContent === void 0 ? {} : { structuredContent }
7779
+ };
7780
+ }
7781
+ function messageCopyLinkRecordContextFromData(data) {
7782
+ const context = objectValue$1(data.record_context ?? data.recordContext);
7783
+ const extensions = listValue$16(context.extensions ?? context.extend_records ?? context.extendRecords ?? context.list ?? data.extensions ?? data.extend_records ?? data.extendRecords ?? data.list).map(messageCopyLinkExtensionItemFromData).filter((item) => item !== void 0);
7784
+ const extensionCount = Math.max(integerLikeValue$1(context.extension_count ?? context.extensionCount ?? context.total ?? data.extension_count ?? data.extensionCount ?? data.total), extensions.length);
7785
+ if (extensionCount <= 0 && extensions.length === 0) return void 0;
7786
+ extensions.sort((left, right) => right.sendAtMillis - left.sendAtMillis);
7787
+ return {
7788
+ extensionCount,
7789
+ extensions
7790
+ };
7791
+ }
7792
+ function mergeMessageCopyLinkRecordContexts(left, right) {
7793
+ if (left === void 0) return right;
7794
+ if (right === void 0) return left;
7795
+ const byUid = /* @__PURE__ */ new Map();
7796
+ for (const item of [...left.extensions, ...right.extensions]) byUid.set(item.recordUid, item);
7797
+ const extensions = [...byUid.values()].sort((a, b) => b.sendAtMillis - a.sendAtMillis);
7798
+ return {
7799
+ extensionCount: Math.max(left.extensionCount, right.extensionCount, extensions.length),
7800
+ extensions
7801
+ };
7802
+ }
7803
+ function messageCopyLinkWorldTags(text) {
7804
+ return [...text.matchAll(/#(\S+)/gu)].map((match) => match[1]?.trim() ?? "").filter((tag) => tag !== "");
7805
+ }
7806
+ function messageCopyLinkRecordUidCandidates(items, anchors) {
7807
+ const candidates = [];
7808
+ const push = (value) => {
7809
+ const normalized = value?.trim() ?? "";
7810
+ if (normalized !== "" && !candidates.includes(normalized)) candidates.push(normalized);
7811
+ };
7812
+ for (const item of items) push(item.recordUid);
7813
+ for (const anchor of anchors) push(anchor.recordUid);
7814
+ return candidates;
7815
+ }
7124
7816
  function optionalString$2(value) {
7125
7817
  const text = stringValue$1(value).trim();
7126
7818
  return text === "" ? void 0 : text;
@@ -7479,6 +8171,8 @@ var ChatService = class {
7479
8171
  }
7480
8172
  async openOfficialAuthorPrivateChat(options = {}) {
7481
8173
  const session = await this.runtime.requireSession();
8174
+ const existing = await this.findPrivateChatByPeerUserId(OFFICIAL_AUTHOR_USER_ID, session, options.signal);
8175
+ if (existing !== void 0) return { source: existing };
7482
8176
  const created = await this.runtime.authenticatedSubjectPost("/api/v1/private/create-chat-ref-asen", {
7483
8177
  subject_uid: `dsh_official_author_${randomUUID().replace(/-/g, "")}`,
7484
8178
  network: "dsh",
@@ -7500,6 +8194,25 @@ var ChatService = class {
7500
8194
  ...options.signal === void 0 ? {} : { signal: options.signal }
7501
8195
  });
7502
8196
  }
8197
+ /**
8198
+ * Contact-author is an entry point, not a second conversation type. Scan the
8199
+ * ordinary directory first so repeated clicks always reuse the existing chat.
8200
+ */
8201
+ async findPrivateChatByPeerUserId(peerUserId, session, signal) {
8202
+ let cursor;
8203
+ for (let pageIndex = 0; pageIndex < 10; pageIndex += 1) {
8204
+ const page = await this.source.listSources("root", {
8205
+ limit: 50,
8206
+ refresh: true,
8207
+ ...cursor === void 0 ? {} : { cursor },
8208
+ ...signal === void 0 ? {} : { signal }
8209
+ });
8210
+ const existing = page.items.find((source) => source.kind === "private_chat" && source.peerUserId === peerUserId);
8211
+ if (existing !== void 0) return existing;
8212
+ if (!page.hasMore || page.nextCursor === void 0) return void 0;
8213
+ cursor = page.nextCursor;
8214
+ }
8215
+ }
7503
8216
  async officialAuthorProfile(options = {}) {
7504
8217
  const session = await this.runtime.requireSession();
7505
8218
  const profile = (await this.profile.publicProfileSummariesByUserIds([OFFICIAL_AUTHOR_USER_ID], session, options.signal)).get(OFFICIAL_AUTHOR_USER_ID);
@@ -7562,9 +8275,10 @@ var ChatService = class {
7562
8275
  sendAtMillis: options.cursor.sendAtMillis,
7563
8276
  recordUid: options.cursor.itemUid
7564
8277
  } : void 0);
8278
+ const signingKey = await this.runtime.stateStore.uniqueCode();
7565
8279
  return {
7566
8280
  source: await this.source.sourceItem(source),
7567
- items: page.items.map((item) => this.record.recordTimelineItem(item)),
8281
+ items: page.items.map((item) => this.withRecordMessageActionRef(source, this.record.recordTimelineItem(item), session.userId, signingKey)),
7568
8282
  hasMore: page.hasMore,
7569
8283
  ...page.nextCursor === void 0 ? {} : { nextCursor: {
7570
8284
  sendAtMillis: page.nextCursor.sendAtMillis,
@@ -7800,6 +8514,329 @@ var ChatService = class {
7800
8514
  readReceiptItemKey(itemUid, sequence) {
7801
8515
  return `${itemUid}\u0000${String(sequence)}`;
7802
8516
  }
8517
+ async copySourceMessageLink(sourceRef, actionRefs, options = {}) {
8518
+ const session = await this.runtime.requireSession();
8519
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
8520
+ const references = this.forwardReferencesOrderedForCommand(await this.openMessageActionRefs(actionRefs, session.userId, source));
8521
+ if (references.length === 0 || references.length > 100) throw new ArkmePluginError("message-actions-selection-invalid", "请选择 1 至 100 条消息", false);
8522
+ this.validateForwardReferences(references);
8523
+ const sources = references.map((reference) => reference.sourceKind === "chat_relation" ? {
8524
+ kind: "chat_relation",
8525
+ chat_session_uid: reference.chatSessionUid,
8526
+ relation_uid: reference.relationUid
8527
+ } : {
8528
+ kind: "record",
8529
+ record_owner_user_id: reference.recordOwnerUserId,
8530
+ record_uid: reference.recordUid
8531
+ });
8532
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/messages/copy-link/get-or-create", { sources }, session, options.signal);
8533
+ const sid = stringValue$1(data.sid).trim();
8534
+ const url = stringValue$1(data.url).trim();
8535
+ if (sid === "" || !/^https?:\/\//i.test(url)) throw new ArkmePluginError("message-copy-link-response-invalid", "复制链接失败,请稍后重试", true, 502);
8536
+ return {
8537
+ sid,
8538
+ url
8539
+ };
8540
+ }
8541
+ async resolveMessageCopyLink(sid, options = {}) {
8542
+ const normalizedSid = sid.trim();
8543
+ if (!/^[0-9A-Za-z]{16}$/.test(normalizedSid)) throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
8544
+ const session = await this.runtime.requireSession();
8545
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/messages/copy-link/resolve", { sid: normalizedSid }, session, options.signal);
8546
+ const rawItems = listValue$16(data.items);
8547
+ if (rawItems.length === 0 && stringValue$1(data.audit_status) === "auditing") throw new ArkmePluginError("message-copy-link-auditing", "审核中", true, 409);
8548
+ const items = rawItems.map(messageCopyLinkSnapshotItemFromData);
8549
+ if (items.length === 0 || items.length > MAX_MESSAGE_COPY_LINK_ITEMS) throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
8550
+ const presentation = messageCopyLinkPresentationNodesFromData(data.presentation, items.length);
8551
+ const sourceContext = objectValue$1(data.source_context);
8552
+ const accessMode = stringValue$1(data.access_mode);
8553
+ if (accessMode !== "normal" && accessMode !== "link_read_only") throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
8554
+ const anchors = accessMode === "normal" ? listValue$16(sourceContext.anchors).map(messageCopyLinkSourceAnchorFromData).filter((anchor) => anchor.relationUid.trim() !== "" && anchor.recordUid.trim() !== "" && anchor.recordOwnerUserId > 0) : [];
8555
+ const recordContext = mergeMessageCopyLinkRecordContexts(messageCopyLinkRecordContextFromData(data), accessMode === "normal" ? await this.loadMessageCopyLinkPublicExtensions(messageCopyLinkRecordUidCandidates(items, anchors), session, options.signal) : void 0);
8556
+ const result = {
8557
+ sid: stringValue$1(data.sid),
8558
+ displayTitle: stringValue$1(data.display_title),
8559
+ generatedAtMillis: epochMillisValue(data.generated_at),
8560
+ accessMode,
8561
+ items,
8562
+ presentation,
8563
+ ...accessMode === "normal" ? {
8564
+ sourceSessionUid: stringValue$1(sourceContext.chat_session_uid),
8565
+ sourceAnchors: anchors
8566
+ } : {},
8567
+ ...recordContext === void 0 ? {} : { recordContext }
8568
+ };
8569
+ if (result.sid !== normalizedSid || accessMode === "normal" && (result.sourceSessionUid === void 0 || result.sourceSessionUid.trim() === "" || anchors.length === 0)) throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
8570
+ return result;
8571
+ }
8572
+ async loadMessageCopyLinkPublicExtensions(recordUids, session, signal) {
8573
+ const worldPost = this.runtime.authenticatedWorldPost;
8574
+ const candidates = recordUids.map((recordUid) => recordUid.trim()).filter((recordUid, index, array) => recordUid !== "" && array.indexOf(recordUid) === index).slice(0, 3);
8575
+ if (candidates.length === 0 || worldPost === void 0) return void 0;
8576
+ for (const normalizedRecordUid of candidates) {
8577
+ const extensions = [];
8578
+ let offset = 0;
8579
+ let total = 0;
8580
+ let hasMore;
8581
+ try {
8582
+ while (true) {
8583
+ const data = await worldPost.call(this.runtime, "/api/v1/public-record/extend-list", {
8584
+ record_uid: normalizedRecordUid,
8585
+ limit: 50,
8586
+ offset
8587
+ }, session, signal, {
8588
+ lane: "interactive-read",
8589
+ bypassCache: true
8590
+ });
8591
+ const rawItems = listValue$16(data.list ?? data.items);
8592
+ if (offset === 0) total = Math.max(0, integerLikeValue$1(data.total));
8593
+ const pageItems = rawItems.map((raw) => messageCopyLinkExtensionItemFromData(raw)).filter((item) => item !== void 0);
8594
+ extensions.push(...pageItems);
8595
+ if (rawItems.length === 0) break;
8596
+ offset += rawItems.length;
8597
+ hasMore = data.has_more === true || data.hasMore === true ? true : data.has_more === false || data.hasMore === false ? false : void 0;
8598
+ if (total > 0 && offset >= total) break;
8599
+ if (hasMore === false) break;
8600
+ if (hasMore === void 0 && total <= 0) break;
8601
+ }
8602
+ } catch {
8603
+ continue;
8604
+ }
8605
+ const extensionCount = Math.max(total, extensions.length);
8606
+ if (extensionCount <= 0 && extensions.length === 0) continue;
8607
+ extensions.sort((left, right) => right.sendAtMillis - left.sendAtMillis);
8608
+ return {
8609
+ extensionCount,
8610
+ extensions
8611
+ };
8612
+ }
8613
+ }
8614
+ async extendMessageCopyLink(sid, itemIndex, textContent, recordUid, options = {}) {
8615
+ const normalizedSid = sid.trim();
8616
+ const normalizedIndex = Math.trunc(itemIndex);
8617
+ const normalizedUid = recordUid.trim();
8618
+ const normalizedText = textContent.trim();
8619
+ if (!/^[0-9A-Za-z]{16}$/.test(normalizedSid) || normalizedIndex < 0 || normalizedIndex >= MAX_MESSAGE_COPY_LINK_ITEMS) throw new ArkmePluginError("message-copy-link-unavailable", "链接暂不可用", false, 404);
8620
+ if (!RECORD_UID_PATTERN.test(normalizedUid)) throw new ArkmePluginError("record-uid-invalid", "写入标识无效,请重试", false);
8621
+ if (normalizedText === "") throw new ArkmePluginError("record-text-empty", "请输入延展内容", false);
8622
+ if (normalizedText.length > this.runtime.config.maxTextLength) throw new ArkmePluginError("record-text-too-long", `内容不能超过 ${this.runtime.config.maxTextLength} 个字符`, false);
8623
+ const session = await this.runtime.requireSession();
8624
+ const detail = await this.resolveMessageCopyLink(normalizedSid, options);
8625
+ const item = detail.items[normalizedIndex];
8626
+ const anchor = detail.accessMode === "normal" ? detail.sourceAnchors?.[normalizedIndex] : void 0;
8627
+ const parentRecordUid = item === void 0 ? "" : messageCopyLinkRecordUidCandidates([item], anchor === void 0 ? [] : [anchor])[0] ?? "";
8628
+ if (item === void 0 || parentRecordUid === "") throw new ArkmePluginError("message-copy-link-extension-unavailable", "链接暂不可延展", false, 409);
8629
+ const profile = (await this.profile.refreshProfile()).profile;
8630
+ if (profile === null) throw new ArkmePluginError("profile-unavailable", "无法读取当前 Arkme 账号资料", true);
8631
+ if (profile.contact.phoneMasked === void 0) throw new ArkmePluginError("world-phone-binding-required", "请先在 Arkme 客户端绑定手机号,再延展快记", false);
8632
+ const sendAtMillis = Date.now();
8633
+ const recordResult = await this.record.createTextForConversation(normalizedUid, normalizedText);
8634
+ if (recordResult.localState !== "synced") throw new ArkmePluginError("message-copy-link-extension-record-pending", recordResult.error ?? "延展已保存到待重试队列,请稍后重试", true);
8635
+ const createdRecordUid = recordResult.recordUid.trim() || normalizedUid;
8636
+ const data = await this.runtime.authenticatedWorldPost("/api/v1/public-record/publish", {
8637
+ record_uid: createdRecordUid,
8638
+ content: normalizedText,
8639
+ text_content: normalizedText,
8640
+ tags: messageCopyLinkWorldTags(normalizedText),
8641
+ original_topic_id: 0,
8642
+ created_at: sendAtMillis,
8643
+ nick_name: profile.nickname || profile.displayName,
8644
+ avatar: profile.avatarRef,
8645
+ template_kind: 1,
8646
+ parent_record_uid: parentRecordUid
8647
+ }, session, options.signal);
8648
+ const publishedRecordUid = stringValue$1(data.record_uid).trim() || createdRecordUid;
8649
+ const senderDisplayName = profile.nickname.trim() || profile.displayName.trim() || "我";
8650
+ const senderAvatarUrl = profile.avatarRef.trim();
8651
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
8652
+ this.realtime.emitChatClientEvent({
8653
+ type: "projection-invalidated",
8654
+ revision: this.realtime.nextChatClientRevision(),
8655
+ projection: "record"
8656
+ });
8657
+ return {
8658
+ sid: detail.sid,
8659
+ recordUid: publishedRecordUid,
8660
+ parentRecordUid,
8661
+ status: numberValue$16(data.status ?? data.check_status ?? recordResult.status),
8662
+ localState: "synced",
8663
+ extension: {
8664
+ recordUid: publishedRecordUid,
8665
+ level: 2,
8666
+ sourceKind: "record_extension",
8667
+ senderDisplayName,
8668
+ ...senderAvatarUrl === "" ? {} : { senderAvatarUrl },
8669
+ title: "",
8670
+ textContent: normalizedText,
8671
+ sendAtMillis,
8672
+ templateKind: 1,
8673
+ displayKind: 0,
8674
+ officialMark: 0,
8675
+ mediaItems: []
8676
+ }
8677
+ };
8678
+ }
8679
+ async resolveLinkMetadata(rawUrl, options = {}) {
8680
+ const url = ensureMetadataUrl(rawUrl);
8681
+ try {
8682
+ return await this.fetchLinkMetadata(url, 0, options.signal);
8683
+ } catch (error) {
8684
+ if (error instanceof ArkmePluginError && (error.code === "link-metadata-url-invalid" || error.code === "link-metadata-url-forbidden")) throw error;
8685
+ return {
8686
+ url: url.toString(),
8687
+ title: fallbackLinkTitle(url),
8688
+ siteName: url.hostname.replace(/^www\./iu, "")
8689
+ };
8690
+ }
8691
+ }
8692
+ async fetchLinkMetadata(url, redirectCount, signal) {
8693
+ const controller = new AbortController();
8694
+ const abort = () => {
8695
+ controller.abort(signal?.reason);
8696
+ };
8697
+ if (signal?.aborted === true) abort();
8698
+ else signal?.addEventListener("abort", abort, { once: true });
8699
+ const timeout = setTimeout(() => {
8700
+ controller.abort();
8701
+ }, Math.min(8e3, this.runtime.config.requestTimeoutMs));
8702
+ try {
8703
+ const response = await this.runtime.fetchImpl(url, {
8704
+ method: "GET",
8705
+ redirect: "manual",
8706
+ headers: {
8707
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8708
+ "Accept-Language": "zh-CN",
8709
+ "User-Agent": "Mozilla/5.0 ArkmeDSHPlugin/0.1 LinkPreview"
8710
+ },
8711
+ signal: controller.signal
8712
+ });
8713
+ if (response.status >= 300 && response.status < 400) {
8714
+ if (redirectCount >= LINK_METADATA_MAX_REDIRECTS) return {
8715
+ url: url.toString(),
8716
+ title: fallbackLinkTitle(url),
8717
+ siteName: url.hostname.replace(/^www\./iu, "")
8718
+ };
8719
+ const location = response.headers.get("location");
8720
+ if (location === null || location.trim() === "") return {
8721
+ url: url.toString(),
8722
+ title: fallbackLinkTitle(url),
8723
+ siteName: url.hostname.replace(/^www\./iu, "")
8724
+ };
8725
+ return await this.fetchLinkMetadata(ensureMetadataUrl(new URL(location, url).toString()), redirectCount + 1, signal);
8726
+ }
8727
+ if (!response.ok) return {
8728
+ url: url.toString(),
8729
+ title: fallbackLinkTitle(url),
8730
+ siteName: url.hostname.replace(/^www\./iu, "")
8731
+ };
8732
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
8733
+ if (contentType !== "" && !contentType.includes("html") && !contentType.includes("xml") && !contentType.includes("text/")) return {
8734
+ url: url.toString(),
8735
+ title: fallbackLinkTitle(url),
8736
+ siteName: url.hostname.replace(/^www\./iu, "")
8737
+ };
8738
+ return linkMetadataFromHtml(url, await readResponseTextPrefix(response));
8739
+ } finally {
8740
+ clearTimeout(timeout);
8741
+ signal?.removeEventListener("abort", abort);
8742
+ }
8743
+ }
8744
+ async forwardSourceMessages(sourceRef, actionRefs, options = {}) {
8745
+ const session = await this.runtime.requireSession();
8746
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
8747
+ const targetSourceRef = options.targetSourceRef?.trim() || sourceRef;
8748
+ const targetSource = targetSourceRef === sourceRef ? source : await this.source.openSourceRef(targetSourceRef, session.userId);
8749
+ const commentText = options.commentText?.trim() ?? "";
8750
+ if (commentText.length > this.runtime.config.maxTextLength) throw new ArkmePluginError("source-text-invalid", "发送内容超过长度限制", false);
8751
+ if (targetSource.kind !== "private_chat" && targetSource.kind !== "group_chat" && targetSource.kind !== "send_to_self" && targetSource.kind !== "default_category" && targetSource.kind !== "topic") throw new ArkmePluginError("message-actions-source-invalid", "当前数据源暂不支持转发", false, 409);
8752
+ const references = this.forwardReferencesOrderedForCommand(await this.openMessageActionRefs(actionRefs, session.userId, source));
8753
+ if (references.length === 0 || references.length > 100) throw new ArkmePluginError("message-actions-selection-invalid", "请选择 1 至 100 条消息", false);
8754
+ this.validateForwardReferences(references);
8755
+ const recordUid = options.recordUid?.trim() || randomUUID();
8756
+ const title = references.length === 1 ? "转发快记" : `转发 ${String(references.length)} 条快记`;
8757
+ const contentPayload = {
8758
+ payload_kind: 1,
8759
+ schema_version: 1,
8760
+ text_state: 1,
8761
+ forward_records: {
8762
+ render_kind: "forward_records",
8763
+ schema_version: 1,
8764
+ source_type: "quick_records",
8765
+ title: "转发快记",
8766
+ source_record_uids: references.map((reference) => reference.recordUid).filter((recordUid) => recordUid.trim() !== ""),
8767
+ created_at: Date.now(),
8768
+ summary_lines: references.slice(0, 3).map((reference) => `${reference.senderName}: ${reference.textContent.trim() || reference.title.trim() || "非文本内容"}`),
8769
+ items: references.map((reference, index) => this.forwardPayloadItem(reference, index))
8770
+ }
8771
+ };
8772
+ const appendCommentWarning = async (sent) => {
8773
+ if (commentText === "") return sent;
8774
+ try {
8775
+ await this.sendSourceText(targetSourceRef, commentText, { ...options.signal === void 0 ? {} : { signal: options.signal } });
8776
+ return sent;
8777
+ } catch {
8778
+ return {
8779
+ ...sent,
8780
+ warningText: "转发已完成,附言发送失败"
8781
+ };
8782
+ }
8783
+ };
8784
+ if (targetSource.kind === "private_chat" || targetSource.kind === "group_chat") {
8785
+ const sendAtMillis = Date.now();
8786
+ const clientRequestId = this.forwardClientRequestId(targetSource.ownerRef, references, sendAtMillis);
8787
+ const sourceRecordUids = references.some((reference) => reference.sourceKind === "chat_relation") ? [] : references.map((reference) => reference.recordUid).filter((recordUid) => recordUid.trim() !== "");
8788
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/records/forward", {
8789
+ chat_session_uid: targetSource.ownerRef,
8790
+ client_request_id: clientRequestId,
8791
+ ...sourceRecordUids.length === 0 ? {} : { source_record_uids: sourceRecordUids },
8792
+ source_items: references.map((reference) => this.forwardSourceItemBody(reference)),
8793
+ ...commentText === "" ? {} : { comment_text: commentText },
8794
+ send_at: sendAtMillis
8795
+ }, session, options.signal);
8796
+ const sequence = numberValue$16(data.seq ?? data.sequence);
8797
+ this.realtime.scheduleChatSessionProjection(targetSource.ownerRef, sequence);
8798
+ return {
8799
+ sourceRef: targetSourceRef,
8800
+ itemUid: stringValue$1(data.record_uid).trim() || recordUid,
8801
+ status: numberValue$16(data.audit_status ?? data.status) || 1,
8802
+ ...sequence > 0 ? { sequence } : {},
8803
+ localState: "synced"
8804
+ };
8805
+ }
8806
+ if (targetSource.kind === "topic") {
8807
+ const data = await this.runtime.authenticatedPost("/api/v1/topics/records/create", {
8808
+ topic_uid: targetSource.ownerRef,
8809
+ record_uid: recordUid,
8810
+ template_kind: 1,
8811
+ title: "",
8812
+ text_content: title,
8813
+ content_payload: contentPayload,
8814
+ send_at: Date.now()
8815
+ }, session, options.signal);
8816
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
8817
+ return await appendCommentWarning({
8818
+ sourceRef: targetSourceRef,
8819
+ itemUid: stringValue$1(data.record_uid).trim() || recordUid,
8820
+ status: numberValue$16(data.status),
8821
+ localState: "synced"
8822
+ });
8823
+ }
8824
+ const data = await this.runtime.authenticatedPost("/api/v1/records/create", {
8825
+ record_uid: recordUid,
8826
+ template_kind: 1,
8827
+ title: "",
8828
+ text_content: title,
8829
+ content_payload: contentPayload,
8830
+ send_at: Date.now()
8831
+ }, session, options.signal);
8832
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
8833
+ return await appendCommentWarning({
8834
+ sourceRef: targetSourceRef,
8835
+ itemUid: stringValue$1(data.record_uid).trim() || recordUid,
8836
+ status: numberValue$16(data.status),
8837
+ localState: "synced"
8838
+ });
8839
+ }
7803
8840
  async sendSourceText(sourceRef, textContent, options = {}) {
7804
8841
  const session = await this.runtime.requireSession();
7805
8842
  const source = await this.source.openSourceRef(sourceRef, session.userId);
@@ -8053,12 +9090,11 @@ var ChatService = class {
8053
9090
  localState: "synced"
8054
9091
  };
8055
9092
  }
8056
- async sendGroupSourceTextWithAiPolish(sourceRef, chatSessionUid, originalText, recordUid, relationUid, session, options = {}) {
8057
- return await this.aiPolish.sendGroupSourceTextWithAiPolish(sourceRef, chatSessionUid, originalText, recordUid, relationUid, session, options);
8058
- }
8059
9093
  async sendSourceRich(sourceRef, input, options = {}) {
8060
9094
  if (this.runtime.config.richMediaSendEnabled === false) throw new ArkmePluginError("rich-content-disabled", "富内容发送已被插件配置关闭", false, 403);
8061
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();
8062
9098
  const source = await this.source.openSourceRef(sourceRef, session.userId);
8063
9099
  const title = input.title?.trim() ?? "";
8064
9100
  const textContent = input.textContent?.trim() ?? "";
@@ -8087,7 +9123,10 @@ var ChatService = class {
8087
9123
  content_file_role: 1,
8088
9124
  render_role: 1,
8089
9125
  sort_order: index,
8090
- file_name: asset.fileName
9126
+ file_name: asset.fileName,
9127
+ file_kind: asset.fileKind,
9128
+ mime_type: asset.mimeType,
9129
+ size: asset.size
8091
9130
  }))
8092
9131
  };
8093
9132
  let contentPayload = mediaContentPayload;
@@ -8095,7 +9134,7 @@ var ChatService = class {
8095
9134
  if ((input.humanMentions?.length ?? 0) > 0 && source.kind !== "group_chat") throw new ArkmePluginError("mention-group-required", "真人 mention 只能发送到群聊", false);
8096
9135
  if ((input.botMentions?.length ?? 0) > 0 && source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("mention-chat-required", "Bot mention 只能发送到聊天", false);
8097
9136
  if (longArticle) throw new ArkmePluginError("mention-rich-invalid", "长文暂不支持 mention", false);
8098
- 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 ?? []);
8099
9138
  contentPayload = {
8100
9139
  ...mediaContentPayload ?? {},
8101
9140
  ...mentionPayload
@@ -8112,7 +9151,7 @@ var ChatService = class {
8112
9151
  send_at: Date.now()
8113
9152
  };
8114
9153
  if (source.kind === "send_to_self" || source.kind === "default_category") {
8115
- 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);
8116
9155
  this.source.invalidateSourceListCache(session.userId, "send_to_self");
8117
9156
  return {
8118
9157
  sourceRef,
@@ -8125,7 +9164,7 @@ var ChatService = class {
8125
9164
  const result = await this.runtime.authenticatedPost("/api/v1/topics/records/create", {
8126
9165
  topic_uid: source.ownerRef,
8127
9166
  ...commonBody
8128
- }, session);
9167
+ }, session, options.signal);
8129
9168
  this.source.invalidateSourceListCache(session.userId, "send_to_self");
8130
9169
  return {
8131
9170
  sourceRef,
@@ -8138,7 +9177,7 @@ var ChatService = class {
8138
9177
  chat_session_uid: source.ownerRef,
8139
9178
  rel_uid: relationUid,
8140
9179
  ...commonBody
8141
- }, session);
9180
+ }, session, options.signal);
8142
9181
  const sequence = numberValue$16(result.seq);
8143
9182
  this.realtime.scheduleChatSessionProjection(source.ownerRef, sequence);
8144
9183
  return {
@@ -8285,12 +9324,6 @@ var ChatService = class {
8285
9324
  async fetchMedia(mediaRef, range, signal) {
8286
9325
  return await this.media.fetchMedia(mediaRef, range, signal);
8287
9326
  }
8288
- currentUserAgentSourceFallback(userId, source) {
8289
- return this.arko.currentUserAgentSourceFallback(userId, source);
8290
- }
8291
- async agentSourceDisplayName(session) {
8292
- return await this.arko.agentSourceDisplayName(session);
8293
- }
8294
9327
  async sendDirectText(recipientArkmeId, textContent, options = {}) {
8295
9328
  const session = await this.runtime.requireSession();
8296
9329
  const recipient = recipientArkmeId.trim();
@@ -8383,6 +9416,7 @@ var ChatService = class {
8383
9416
  const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
8384
9417
  const rawAgentSource = timelineAgentSource(relation, record, payload);
8385
9418
  const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
9419
+ const contentBlocks = this.media.richContentBlocks(item, session.userId);
8386
9420
  const mentionsViewer = senderUserId !== session.userId && arkmeMentionMetadataMentionsViewer(record, payload, session.userId);
8387
9421
  items.push({
8388
9422
  itemUid: uid,
@@ -8401,7 +9435,7 @@ var ChatService = class {
8401
9435
  ...aiPolish === void 0 ? {} : { aiPolish },
8402
9436
  ...forwardRecords === void 0 ? {} : { forwardRecords },
8403
9437
  displayKind: numberValue$16(payload.display_kind),
8404
- contentBlocks: this.media.richContentBlocks(item, session.userId)
9438
+ contentBlocks
8405
9439
  });
8406
9440
  }
8407
9441
  return items;
@@ -8571,11 +9605,31 @@ var ChatService = class {
8571
9605
  const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
8572
9606
  const rawAgentSource = timelineAgentSource(relation, record, payload);
8573
9607
  const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
9608
+ const contentBlocks = this.media.richContentBlocks(item, session.userId);
9609
+ const senderName = stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户";
8574
9610
  const itemIndex = items.push({
8575
9611
  itemUid: uid,
8576
9612
  ...source.kind !== "group_chat" || relationUid === "" || senderUserId === session.userId ? {} : { messageRef: this.sealMessageRef(session.userId, source.ownerRef, relationUid, signingKey) },
9613
+ ...relationUid === "" ? {} : { messageActionRef: this.sealMessageActionRef(this.messageActionRefPayload({
9614
+ sourceKind: "chat_relation",
9615
+ userId: session.userId,
9616
+ sourceOwnerRef: source.ownerRef,
9617
+ chatSessionUid: source.ownerRef,
9618
+ relationUid,
9619
+ recordOwnerUserId: 0,
9620
+ recordUid: uid,
9621
+ senderUserId,
9622
+ senderName,
9623
+ title: stringValue$1(payload.title),
9624
+ textContent: stringValue$1(payload.text_content),
9625
+ sendAtMillis,
9626
+ sourceSequence: numberValue$16(relation.seq),
9627
+ templateKind: numberValue$16(payload.template_kind),
9628
+ displayKind: numberValue$16(payload.display_kind),
9629
+ contentBlocks
9630
+ }), signingKey) },
8577
9631
  ...senderUserId > 0 ? { memberRef: await this.sealChatMemberRef(session.userId, source.ownerRef, senderUserId) } : {},
8578
- senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
9632
+ senderName,
8579
9633
  ...agentSource === void 0 ? {} : { agentSource },
8580
9634
  isMe: senderUserId === session.userId,
8581
9635
  sendAtMillis,
@@ -8591,7 +9645,7 @@ var ChatService = class {
8591
9645
  updateAtMillis: numberValue$16(payload.update_at ?? record.update_at),
8592
9646
  recordDurationMillis: numberValue$16(payload.record_duration_millis),
8593
9647
  editDurationMillis: numberValue$16(payload.edit_duration_millis),
8594
- contentBlocks: this.media.richContentBlocks(item, session.userId),
9648
+ contentBlocks,
8595
9649
  ...forwardRecords === void 0 ? {} : { forwardRecords }
8596
9650
  }) - 1;
8597
9651
  if (senderUserId > 0) senderUserIdByIndex.set(itemIndex, senderUserId);
@@ -8636,6 +9690,176 @@ var ChatService = class {
8636
9690
  if (raw.version !== 1 || result.viewerUserId !== expectedViewerUserId || result.chatSessionUid !== expectedChatSessionUid || !Number.isSafeInteger(result.targetUserId) || result.targetUserId <= 0) throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用与当前账号或会话不匹配", false, 403);
8637
9691
  return result;
8638
9692
  }
9693
+ messageActionRefPayload(input) {
9694
+ const contentBlocks = input.contentBlocks ?? [];
9695
+ const fileNames = contentBlocks.filter((block) => block.kind === "file" && block.fileName.trim() !== "").slice(0, 5).map((block) => block.fileName.trim().slice(0, 256));
9696
+ return {
9697
+ version: 1,
9698
+ sourceKind: input.sourceKind,
9699
+ userId: input.userId,
9700
+ sourceOwnerRef: input.sourceOwnerRef,
9701
+ chatSessionUid: input.chatSessionUid,
9702
+ relationUid: input.relationUid,
9703
+ recordOwnerUserId: Math.trunc(input.recordOwnerUserId),
9704
+ recordUid: input.recordUid,
9705
+ senderUserId: Math.trunc(input.senderUserId),
9706
+ senderName: input.senderName.trim() || "Arkme用户",
9707
+ title: input.title.slice(0, 500),
9708
+ textContent: input.textContent.slice(0, this.runtime.config.maxTextLength),
9709
+ sendAtMillis: Math.trunc(input.sendAtMillis),
9710
+ sourceSequence: Math.max(0, Math.trunc(numberValue$16(input.sourceSequence))),
9711
+ templateKind: Math.trunc(input.templateKind),
9712
+ displayKind: Math.trunc(input.displayKind),
9713
+ imageCount: contentBlocks.filter((block) => block.kind === "image").length,
9714
+ voiceCount: contentBlocks.filter((block) => block.kind === "audio").length,
9715
+ fileCount: contentBlocks.filter((block) => block.kind === "file").length,
9716
+ fileNames
9717
+ };
9718
+ }
9719
+ withRecordMessageActionRef(source, item, userId, signingKey) {
9720
+ if (source.kind !== "send_to_self" && source.kind !== "default_category" && source.kind !== "topic" || item.itemUid.trim() === "" || !item.isMe) return item;
9721
+ return {
9722
+ ...item,
9723
+ messageActionRef: this.sealMessageActionRef(this.messageActionRefPayload({
9724
+ sourceKind: "record",
9725
+ userId,
9726
+ sourceOwnerRef: source.ownerRef,
9727
+ chatSessionUid: "",
9728
+ relationUid: "",
9729
+ recordOwnerUserId: userId,
9730
+ recordUid: item.itemUid,
9731
+ senderUserId: userId,
9732
+ senderName: item.senderName,
9733
+ title: item.title,
9734
+ textContent: item.textContent,
9735
+ sendAtMillis: item.sendAtMillis,
9736
+ templateKind: item.templateKind ?? 1,
9737
+ displayKind: item.displayKind ?? 0,
9738
+ contentBlocks: item.contentBlocks
9739
+ }), signingKey)
9740
+ };
9741
+ }
9742
+ sealMessageActionRef(payload, signingKey) {
9743
+ const encoded = encodeOpaqueJson$5(payload);
9744
+ return `arkme-message-action-v1.${encoded}.${createHmac("sha256", signingKey).update(encoded).digest("base64url")}`;
9745
+ }
9746
+ async openMessageActionRefs(actionRefs, expectedUserId, source) {
9747
+ const normalizedRefs = actionRefs.map((value) => value.trim()).filter((value) => value !== "");
9748
+ const uniqueRefs = new Set(normalizedRefs);
9749
+ if (normalizedRefs.length !== uniqueRefs.size) throw new ArkmePluginError("message-actions-selection-invalid", "不能选择重复消息", false);
9750
+ const references = [];
9751
+ const relationIdentities = /* @__PURE__ */ new Set();
9752
+ for (const actionRef of normalizedRefs) {
9753
+ const reference = await this.openMessageActionRef(actionRef, expectedUserId, source);
9754
+ const relationIdentity = reference.sourceKind === "chat_relation" ? `chat_relation\u0000${reference.chatSessionUid}\u0000${reference.relationUid}` : `record\u0000${String(reference.recordOwnerUserId)}\u0000${reference.recordUid}`;
9755
+ if (!relationIdentities.add(relationIdentity)) throw new ArkmePluginError("message-actions-selection-invalid", "不能选择重复消息", false);
9756
+ references.push(reference);
9757
+ }
9758
+ return references;
9759
+ }
9760
+ async openMessageActionRef(actionRef, expectedUserId, source) {
9761
+ const parts = actionRef.trim().split(".");
9762
+ if (parts.length !== 3 || parts[0] !== "arkme-message-action-v1") throw new ArkmePluginError("message-action-ref-invalid", "消息操作引用无效,请刷新后重试", false);
9763
+ const payload = parts[1] ?? "";
9764
+ const supplied = Buffer.from(parts[2] ?? "", "base64url");
9765
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(payload).digest();
9766
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("message-action-ref-invalid", "消息操作引用无效,请刷新后重试", false);
9767
+ let parsed;
9768
+ try {
9769
+ parsed = objectValue$1(decodeOpaqueJson$5(payload));
9770
+ } catch (error) {
9771
+ throw new ArkmePluginError("message-action-ref-invalid", "消息操作引用无效,请刷新后重试", false, 400, { cause: error });
9772
+ }
9773
+ const fileNames = listValue$16(parsed.fileNames).map((value) => stringValue$1(value).trim()).filter((value) => value !== "").slice(0, 5);
9774
+ const result = {
9775
+ version: 1,
9776
+ sourceKind: stringValue$1(parsed.sourceKind).trim() === "record" ? "record" : "chat_relation",
9777
+ userId: Math.trunc(numberValue$16(parsed.userId)),
9778
+ sourceOwnerRef: stringValue$1(parsed.sourceOwnerRef).trim(),
9779
+ chatSessionUid: stringValue$1(parsed.chatSessionUid).trim(),
9780
+ relationUid: stringValue$1(parsed.relationUid).trim(),
9781
+ recordOwnerUserId: Math.trunc(numberValue$16(parsed.recordOwnerUserId)),
9782
+ recordUid: stringValue$1(parsed.recordUid).trim(),
9783
+ senderUserId: Math.trunc(numberValue$16(parsed.senderUserId)),
9784
+ senderName: stringValue$1(parsed.senderName).trim() || "Arkme用户",
9785
+ title: stringValue$1(parsed.title),
9786
+ textContent: stringValue$1(parsed.textContent),
9787
+ sendAtMillis: Math.trunc(numberValue$16(parsed.sendAtMillis)),
9788
+ sourceSequence: Math.max(0, Math.trunc(numberValue$16(parsed.sourceSequence))),
9789
+ templateKind: Math.trunc(numberValue$16(parsed.templateKind)),
9790
+ displayKind: Math.trunc(numberValue$16(parsed.displayKind)),
9791
+ imageCount: Math.max(0, Math.trunc(numberValue$16(parsed.imageCount))),
9792
+ voiceCount: Math.max(0, Math.trunc(numberValue$16(parsed.voiceCount))),
9793
+ fileCount: Math.max(0, Math.trunc(numberValue$16(parsed.fileCount))),
9794
+ fileNames
9795
+ };
9796
+ const matchesChatSource = result.sourceKind === "chat_relation" && (source.kind === "private_chat" || source.kind === "group_chat") && result.sourceOwnerRef === source.ownerRef && result.chatSessionUid === source.ownerRef && result.relationUid !== "" && result.recordUid !== "";
9797
+ const matchesRecordSource = result.sourceKind === "record" && (source.kind === "send_to_self" || source.kind === "default_category" || source.kind === "topic") && result.sourceOwnerRef === source.ownerRef && result.chatSessionUid === "" && result.relationUid === "" && result.recordOwnerUserId === expectedUserId && result.recordUid !== "";
9798
+ if (parsed.version !== 1 || result.userId !== expectedUserId || !matchesChatSource && !matchesRecordSource) throw new ArkmePluginError("message-action-ref-invalid", "消息操作引用与当前账号或会话不匹配,请刷新后重试", false, 403);
9799
+ return result;
9800
+ }
9801
+ forwardPayloadItem(reference, itemOrder) {
9802
+ return {
9803
+ item_order: itemOrder,
9804
+ source_kind: reference.sourceKind,
9805
+ record_uid: reference.recordUid,
9806
+ source_type: reference.sourceKind === "chat_relation" ? "chat_record" : "record",
9807
+ ...reference.recordOwnerUserId > 0 ? { owner_id: reference.recordOwnerUserId } : {},
9808
+ ...reference.relationUid === "" ? {} : { source_rel_uid: reference.relationUid },
9809
+ ...reference.chatSessionUid === "" ? {} : { source_chat_session_uid: reference.chatSessionUid },
9810
+ ...reference.sourceSequence > 0 ? { source_seq: reference.sourceSequence } : {},
9811
+ source_sender_user_id: reference.senderUserId,
9812
+ source_display_name: reference.senderName,
9813
+ owner_name: reference.senderName,
9814
+ send_at: reference.sendAtMillis,
9815
+ title: reference.title,
9816
+ text: reference.textContent,
9817
+ text_preview: reference.textContent.trim().slice(0, 500),
9818
+ template_kind: reference.templateKind,
9819
+ display_kind: reference.displayKind,
9820
+ image_count: reference.imageCount,
9821
+ voice_count: reference.voiceCount,
9822
+ file_count: reference.fileCount,
9823
+ file_names: reference.fileNames
9824
+ };
9825
+ }
9826
+ forwardClientRequestId(targetChatSessionUid, references, sendAtMillis) {
9827
+ const sourceIdentity = references.map((reference) => reference.sourceKind === "chat_relation" ? `chat_record:${reference.chatSessionUid}:${reference.relationUid}` : reference.recordUid).join("|");
9828
+ const sourceHash = createHash("sha256").update(sourceIdentity).digest("hex").slice(0, 16);
9829
+ return `forward_${targetChatSessionUid}_${String(Math.trunc(sendAtMillis))}_${sourceHash}`;
9830
+ }
9831
+ forwardReferencesOrderedForCommand(references) {
9832
+ if (references.length < 2 || !references.every((reference) => reference.sourceKind === "chat_relation" && reference.sourceSequence > 0 && reference.chatSessionUid.trim() !== "")) return references;
9833
+ const sourceSessionUid = references[0]?.chatSessionUid.trim() ?? "";
9834
+ if (sourceSessionUid === "" || !references.every((reference) => reference.chatSessionUid.trim() === sourceSessionUid)) return references;
9835
+ const ordered = references.map((reference, index) => ({
9836
+ reference,
9837
+ index
9838
+ })).sort((left, right) => {
9839
+ const sequenceCompare = left.reference.sourceSequence - right.reference.sourceSequence;
9840
+ return sequenceCompare === 0 ? left.index - right.index : sequenceCompare;
9841
+ }).map((item) => item.reference);
9842
+ return ordered.every((reference, index) => reference === references[index]) ? references : ordered;
9843
+ }
9844
+ validateForwardReferences(references) {
9845
+ const sourceTypes = new Set(references.map((reference) => reference.sourceKind));
9846
+ if (sourceTypes.size > 1) throw new ArkmePluginError("message-actions-selection-invalid", "暂不支持混合转发不同类型的消息", false);
9847
+ if (!sourceTypes.has("chat_relation")) return;
9848
+ if (new Set(references.map((reference) => reference.chatSessionUid.trim()).filter((value) => value !== "")).size > 1) throw new ArkmePluginError("message-actions-selection-invalid", "暂不支持跨会话转发聊天消息", false);
9849
+ }
9850
+ forwardSourceItemBody(reference) {
9851
+ if (reference.sourceKind === "chat_relation") return {
9852
+ source_type: "chat_record",
9853
+ record_uid: reference.recordUid,
9854
+ source_chat_session_uid: reference.chatSessionUid,
9855
+ source_rel_uid: reference.relationUid,
9856
+ ...reference.sourceSequence > 0 ? { source_seq: reference.sourceSequence } : {}
9857
+ };
9858
+ return {
9859
+ source_type: "record",
9860
+ record_uid: reference.recordUid
9861
+ };
9862
+ }
8639
9863
  sealMessageRef(userId, chatSessionUid, relationUid, signingKey) {
8640
9864
  const payload = encodeOpaqueJson$5({
8641
9865
  version: 1,
@@ -9572,6 +10796,37 @@ function booleanValue$4(value) {
9572
10796
  function listValue$13(value) {
9573
10797
  return Array.isArray(value) ? value : [];
9574
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
+ }
9575
10830
  function compactAiPolishActorLabel(value) {
9576
10831
  const normalized = stringValue$1(value).replace(/\s+/g, " ").trim();
9577
10832
  if (normalized === "") return "";
@@ -9610,31 +10865,181 @@ var GroupAiPolishService = class {
9610
10865
  const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal);
9611
10866
  return this.groupAiPolishSnapshot(sourceRef, source.displayName, config);
9612
10867
  }
9613
- async inspectGroupAiPolishByName(groupName, options = {}) {
9614
- const source = await this.resolveUniqueGroupByName(groupName, options.signal);
9615
- return await this.inspectGroupAiPolish(source.sourceRef, options);
10868
+ async inspectGroupAiPolishByName(groupName, options = {}) {
10869
+ const source = await this.resolveUniqueGroupByName(groupName, options.signal);
10870
+ return await this.inspectGroupAiPolish(source.sourceRef, options);
10871
+ }
10872
+ async readGroupAiPolishNotices(sourceRef, options = {}) {
10873
+ const session = await this.runtime.requireSession();
10874
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
10875
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色通知仅支持群聊", false);
10876
+ return await this.queryGroupAiPolishNotices(source.ownerRef, session, options.signal);
10877
+ }
10878
+ async generateGroupAiPolishRuleForSource(sourceRef, requirement, options = {}) {
10879
+ const session = await this.runtime.requireSession();
10880
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
10881
+ const instruction = requirement.trim();
10882
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
10883
+ if (instruction === "" || [...instruction].length > 2e3) throw new ArkmePluginError("group-ai-polish-requirement-invalid", "请提供不超过 2000 字的润色要求", false);
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);
10889
+ const generated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/rules/generate", {
10890
+ chat_session_uid: source.ownerRef,
10891
+ instruction
10892
+ }, session, options.signal);
10893
+ const candidate = objectValue$1(generated.candidate ?? generated.rule ?? generated.generated_rule ?? generated);
10894
+ const ruleName = stringValue$1(candidate.name).trim();
10895
+ const ruleText = stringValue$1(candidate.rule_text).trim();
10896
+ if (ruleName === "" || ruleText === "") throw new ArkmePluginError("group-ai-polish-generate-invalid", "AI 没有生成可用的润色规则,请换一种描述重试", true, 502);
10897
+ this.cleanupAiPolishState();
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);
10909
+ this.aiPolishConfirmations.set(confirmationRef, {
10910
+ userId: session.userId,
10911
+ chatSessionUid: source.ownerRef,
10912
+ groupName: source.displayName,
10913
+ action: "enable",
10914
+ expiresAtMillis: Date.now() + 10 * 6e4,
10915
+ candidateUid: targetRule?.ruleUid ?? stringValue$1(candidate.candidate_uid).trim(),
10916
+ ...targetRule === void 0 ? {} : {
10917
+ editingRuleUid: targetRule.ruleUid,
10918
+ editingRuleVersion: targetRule.ruleVersion
10919
+ },
10920
+ ruleName,
10921
+ ruleText,
10922
+ promptVersion: stringValue$1(candidate.prompt_version).trim(),
10923
+ extra: threadMessages.length === 0 ? candidateExtra : {
10924
+ ...candidateExtra,
10925
+ rule_thread_messages: threadMessages.map(serializeAiPolishThreadMessage),
10926
+ active_rule_message_id: candidateMessage.id
10927
+ }
10928
+ });
10929
+ return {
10930
+ groupName: source.displayName,
10931
+ ruleName,
10932
+ ruleText,
10933
+ confirmationRef,
10934
+ ...threadMessages.length === 0 ? {} : { threadMessages }
10935
+ };
10936
+ }
10937
+ async generateGroupAiPolishRule(groupName, requirement, options = {}) {
10938
+ const source = await this.resolveUniqueGroupByName(groupName, options.signal);
10939
+ return await this.generateGroupAiPolishRuleForSource(source.sourceRef, requirement, options);
10940
+ }
10941
+ async confirmEnableGroupAiPolish(confirmationRef, options = {}) {
10942
+ const session = await this.runtime.requireSession();
10943
+ const pending = this.requireAiPolishConfirmation(confirmationRef, session.userId, "enable");
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);
9616
11003
  }
9617
- async readGroupAiPolishNotices(sourceRef, options = {}) {
11004
+ async prepareEnableGroupAiPolishForSource(sourceRef, ruleName = "", options = {}) {
9618
11005
  const session = await this.runtime.requireSession();
9619
11006
  const source = await this.source.openSourceRef(sourceRef, session.userId);
9620
- if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色通知仅支持群聊", false);
9621
- return await this.queryGroupAiPolishNotices(source.ownerRef, session, options.signal);
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
+ });
11027
+ return {
11028
+ groupName: source.displayName,
11029
+ ruleName: rule.name,
11030
+ ruleText: rule.ruleText,
11031
+ confirmationRef
11032
+ };
9622
11033
  }
9623
- async generateGroupAiPolishRuleForSource(sourceRef, requirement, options = {}) {
11034
+ async prepareEnableGroupAiPolishRuleForSource(sourceRef, ruleRef, options = {}) {
9624
11035
  const session = await this.runtime.requireSession();
9625
11036
  const source = await this.source.openSourceRef(sourceRef, session.userId);
9626
- const instruction = requirement.trim();
9627
11037
  if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
9628
- if (instruction === "" || [...instruction].length > 2e3) throw new ArkmePluginError("group-ai-polish-requirement-invalid", "请提供不超过 2000 字的润色要求", false);
9629
- if (!(await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal)).canManage) throw new ArkmePluginError("group-ai-polish-forbidden", "当前无法确认有效群成员身份,请稍后重试", false, 403);
9630
- const generated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/rules/generate", {
9631
- chat_session_uid: source.ownerRef,
9632
- instruction
9633
- }, session, options.signal);
9634
- const candidate = objectValue$1(generated.candidate ?? generated.rule ?? generated.generated_rule ?? generated);
9635
- const ruleName = stringValue$1(candidate.name).trim();
9636
- const ruleText = stringValue$1(candidate.rule_text).trim();
9637
- if (ruleName === "" || ruleText === "") throw new ArkmePluginError("group-ai-polish-generate-invalid", "AI 没有生成可用的润色规则,请换一种描述重试", true, 502);
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);
9638
11043
  this.cleanupAiPolishState();
9639
11044
  const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
9640
11045
  this.aiPolishConfirmations.set(confirmationRef, {
@@ -9643,62 +11048,27 @@ var GroupAiPolishService = class {
9643
11048
  groupName: source.displayName,
9644
11049
  action: "enable",
9645
11050
  expiresAtMillis: Date.now() + 10 * 6e4,
9646
- candidateUid: stringValue$1(candidate.candidate_uid).trim(),
9647
- ruleName,
9648
- ruleText,
9649
- promptVersion: stringValue$1(candidate.prompt_version).trim(),
9650
- extra: objectValue$1(candidate.extra)
11051
+ savedRuleUid: rule.ruleUid,
11052
+ ruleName: rule.name,
11053
+ ruleText: rule.ruleText
9651
11054
  });
9652
11055
  return {
9653
11056
  groupName: source.displayName,
9654
- ruleName,
9655
- ruleText,
11057
+ ruleName: rule.name,
11058
+ ruleText: rule.ruleText,
9656
11059
  confirmationRef
9657
11060
  };
9658
11061
  }
9659
- async generateGroupAiPolishRule(groupName, requirement, options = {}) {
11062
+ async prepareEnableGroupAiPolish(groupName, ruleName = "", options = {}) {
9660
11063
  const source = await this.resolveUniqueGroupByName(groupName, options.signal);
9661
- return await this.generateGroupAiPolishRuleForSource(source.sourceRef, requirement, options);
9662
- }
9663
- async confirmEnableGroupAiPolish(confirmationRef, options = {}) {
9664
- const session = await this.runtime.requireSession();
9665
- const pending = this.requireAiPolishConfirmation(confirmationRef, session.userId, "enable");
9666
- if (!(await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal)).canManage) throw new ArkmePluginError("group-ai-polish-forbidden", "当前无法确认有效群成员身份,请稍后重试", false, 403);
9667
- const updateAt = Date.now();
9668
- const upserted = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/rules/upsert", {
9669
- chat_session_uid: pending.chatSessionUid,
9670
- ...pending.candidateUid === void 0 || pending.candidateUid === "" ? {} : { rule_uid: pending.candidateUid },
9671
- name: pending.ruleName,
9672
- rule_text: pending.ruleText,
9673
- ...pending.promptVersion === void 0 || pending.promptVersion === "" ? {} : { prompt_version: pending.promptVersion },
9674
- update_at: updateAt,
9675
- ...pending.extra === void 0 || Object.keys(pending.extra).length === 0 ? {} : { extra: pending.extra }
9676
- }, session, options.signal);
9677
- const ruleUid = stringValue$1(objectValue$1(upserted.rule ?? upserted).rule_uid).trim();
9678
- if (ruleUid === "") throw new ArkmePluginError("group-ai-polish-rule-invalid", "保存润色规则后未返回有效规则", true, 502);
9679
- const updated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/update", {
9680
- chat_session_uid: pending.chatSessionUid,
9681
- enabled: true,
9682
- active_rule_uid: ruleUid,
9683
- update_at: Math.max(Date.now(), updateAt + 1)
9684
- }, session, options.signal);
9685
- const savedConfig = objectValue$1(updated.config ?? updated);
9686
- if (!booleanValue$4(savedConfig.enabled) || stringValue$1(savedConfig.active_rule_uid).trim() !== ruleUid) throw new ArkmePluginError("group-ai-polish-enable-invalid", "润色规则已保存,但开启状态确认失败,请重试", true, 502);
9687
- this.invalidateAiPolishReadCache(session.userId, pending.chatSessionUid);
9688
- this.aiPolishConfirmations.delete(confirmationRef.trim());
9689
- return {
9690
- groupName: pending.groupName,
9691
- enabled: true,
9692
- ruleName: pending.ruleName ?? "",
9693
- changed: true
9694
- };
11064
+ return await this.prepareEnableGroupAiPolishForSource(source.sourceRef, ruleName, options);
9695
11065
  }
9696
11066
  async prepareDisableGroupAiPolishForSource(sourceRef, options = {}) {
9697
11067
  const session = await this.runtime.requireSession();
9698
11068
  const source = await this.source.openSourceRef(sourceRef, session.userId);
9699
11069
  if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
9700
11070
  const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal);
9701
- if (!config.canManage) throw new ArkmePluginError("group-ai-polish-forbidden", "当前无法确认有效群成员身份,请稍后重试", false, 403);
11071
+ if (!config.canManage) throw this.permissionDenied();
9702
11072
  this.cleanupAiPolishState();
9703
11073
  const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
9704
11074
  this.aiPolishConfirmations.set(confirmationRef, {
@@ -9723,8 +11093,8 @@ var GroupAiPolishService = class {
9723
11093
  async confirmDisableGroupAiPolish(confirmationRef, options = {}) {
9724
11094
  const session = await this.runtime.requireSession();
9725
11095
  const pending = this.requireAiPolishConfirmation(confirmationRef, session.userId, "disable");
9726
- const current = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal);
9727
- 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();
9728
11098
  const updated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/update", {
9729
11099
  chat_session_uid: pending.chatSessionUid,
9730
11100
  enabled: false,
@@ -9885,8 +11255,8 @@ var GroupAiPolishService = class {
9885
11255
  polishedText
9886
11256
  };
9887
11257
  }
9888
- async queryGroupAiPolishConfig(chatSessionUid, session, signal) {
9889
- 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" } : {
9890
11260
  lane: "background-read",
9891
11261
  key: `ai-polish:settings:${chatSessionUid}`,
9892
11262
  cacheMs: 15e3,
@@ -9894,12 +11264,16 @@ var GroupAiPolishService = class {
9894
11264
  });
9895
11265
  const config = objectValue$1(data.config ?? data.setting ?? data.settings ?? data);
9896
11266
  const activeRuleUid = stringValue$1(config.active_rule_uid).trim();
9897
- const rules = listValue$13(data.rules).map((raw) => objectValue$1(raw)).map((rule) => ({
9898
- ruleUid: stringValue$1(rule.rule_uid).trim(),
9899
- name: stringValue$1(rule.name).trim() || "未命名规则",
9900
- ruleText: stringValue$1(rule.rule_text).trim(),
9901
- ruleVersion: numberValue$12(rule.rule_version)
9902
- })).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 !== "");
9903
11277
  return {
9904
11278
  enabled: booleanValue$4(config.enabled ?? config.is_enabled),
9905
11279
  canManage: booleanValue$4(data.can_manage),
@@ -9922,7 +11296,8 @@ var GroupAiPolishService = class {
9922
11296
  ruleRef: rule.ruleUid,
9923
11297
  name: rule.name,
9924
11298
  ruleText: rule.ruleText,
9925
- isActive: rule.ruleUid === config.activeRuleUid
11299
+ isActive: rule.ruleUid === config.activeRuleUid,
11300
+ ...rule.threadMessages.length === 0 ? {} : { threadMessages: rule.threadMessages }
9926
11301
  })),
9927
11302
  updatedAtMillis: config.updatedAtMillis
9928
11303
  };
@@ -10002,7 +11377,7 @@ var GroupAiPolishService = class {
10002
11377
  };
10003
11378
  //#endregion
10004
11379
  //#region src/services/group-service.ts
10005
- 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;
10006
11381
  function numberValue$11(value) {
10007
11382
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
10008
11383
  }
@@ -10338,7 +11713,7 @@ var GroupService = class {
10338
11713
  async createGroup(title, clientMutationId, options = {}) {
10339
11714
  const normalizedTitle = title.trim();
10340
11715
  if (normalizedTitle === "" || Array.from(normalizedTitle).length > 80) throw new ArkmePluginError("group-title-invalid", "群聊名称需为 1-80 个字符", false);
10341
- 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);
10342
11717
  const session = await this.runtime.requireSession();
10343
11718
  let data;
10344
11719
  try {
@@ -10994,11 +12369,19 @@ var MediaService = class {
10994
12369
  };
10995
12370
  }).filter((item) => item.fileAssetUid !== "");
10996
12371
  }
10997
- async uploadLocalFile(filePath, metadata) {
12372
+ async uploadLocalFile(filePath, metadata, options = {}) {
10998
12373
  if (this.runtime.config.richMediaSendEnabled === false) throw new ArkmePluginError("rich-content-disabled", "文件上传已被插件配置关闭", false, 403);
10999
12374
  const maxBytes = this.runtime.config.maxUploadBytes ?? 100 * 1024 * 1024;
11000
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);
11001
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);
11002
12385
  const uploadMode = metadata.size > 16 * 1024 * 1024 ? 2 : 1;
11003
12386
  const prepared = await this.runtime.authenticatedPost("/api/v1/files/prepare-upload", {
11004
12387
  planned_size: metadata.size,
@@ -11007,7 +12390,7 @@ var MediaService = class {
11007
12390
  file_kind: metadata.fileKind,
11008
12391
  upload_mode: uploadMode,
11009
12392
  display_name: metadata.fileName
11010
- }, session);
12393
+ }, session, options.signal);
11011
12394
  const uploadSessionUid = stringValue$1(prepared.upload_session_uid).trim();
11012
12395
  if (uploadSessionUid === "") throw new ArkmePluginError("upload-prepare-invalid", "上传准备响应无效", true, 502);
11013
12396
  try {
@@ -11016,12 +12399,23 @@ var MediaService = class {
11016
12399
  if (uploadMode === 1) {
11017
12400
  const uploadUrl = stringValue$1(prepared.upload_url).trim();
11018
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
+ })());
11019
12412
  const response = await this.runtime.fetchImpl(uploadUrl, {
11020
12413
  method: "PUT",
11021
12414
  headers: Object.fromEntries(Object.entries(objectValue$1(prepared.upload_headers)).map(([key, value]) => [key, stringValue$1(value)])),
11022
- body: createReadStream(filePath),
12415
+ body,
11023
12416
  duplex: "half",
11024
- redirect: "error"
12417
+ redirect: "error",
12418
+ ...signal === void 0 ? {} : { signal }
11025
12419
  });
11026
12420
  if (!response.ok) throw new ArkmePluginError("upload-storage-failed", `对象存储上传失败(${String(response.status)})`, true, 502);
11027
12421
  storageETag = response.headers.get("etag") ?? "";
@@ -11032,6 +12426,7 @@ var MediaService = class {
11032
12426
  const handle = await open(filePath, "r");
11033
12427
  try {
11034
12428
  for (const part of parts) {
12429
+ options.signal?.throwIfAborted();
11035
12430
  const partNumber = Math.trunc(numberValue$9(part.part_number));
11036
12431
  const uploadUrl = stringValue$1(part.upload_url).trim();
11037
12432
  const offset = (partNumber - 1) * partSize;
@@ -11043,26 +12438,30 @@ var MediaService = class {
11043
12438
  method: "PUT",
11044
12439
  headers: Object.fromEntries(Object.entries(objectValue$1(part.upload_headers)).map(([key, value]) => [key, stringValue$1(value)])),
11045
12440
  body: buffer,
11046
- redirect: "error"
12441
+ redirect: "error",
12442
+ ...options.signal === void 0 ? {} : { signal: options.signal }
11047
12443
  });
11048
12444
  if (!response.ok) throw new ArkmePluginError("upload-storage-failed", `对象存储分片上传失败(${String(response.status)})`, true, 502);
11049
12445
  completedParts.push({
11050
12446
  part_number: partNumber,
11051
12447
  etag: response.headers.get("etag") ?? ""
11052
12448
  });
12449
+ progress("uploading", Math.min(metadata.size, offset + length));
11053
12450
  }
11054
12451
  } finally {
11055
12452
  await handle.close();
11056
12453
  }
11057
12454
  }
12455
+ progress("completing", metadata.size);
11058
12456
  const completed = await this.runtime.authenticatedPost("/api/v1/files/complete-upload", {
11059
12457
  upload_session_uid: uploadSessionUid,
11060
12458
  uploaded_size: metadata.size,
11061
12459
  storage_etag: storageETag,
11062
12460
  multipart_parts: completedParts
11063
- }, session);
12461
+ }, session, options.signal);
11064
12462
  const fileAssetUid = stringValue$1(completed.file_asset_uid).trim();
11065
12463
  if (fileAssetUid === "") throw new ArkmePluginError("upload-complete-invalid", "上传完成响应无效", true, 502);
12464
+ progress("ready", metadata.size);
11066
12465
  return {
11067
12466
  fileAssetUid,
11068
12467
  fileName: metadata.fileName,
@@ -11075,7 +12474,7 @@ var MediaService = class {
11075
12474
  throw error;
11076
12475
  }
11077
12476
  }
11078
- async fetchMedia(mediaRef, range, signal) {
12477
+ async fetchMedia(mediaRef, range, signal, originalOnly = false) {
11079
12478
  const session = await this.runtime.requireSession();
11080
12479
  const descriptor = this.mediaRefs.get(mediaRef);
11081
12480
  if (descriptor === void 0 || descriptor.viewerUserId !== session.userId || descriptor.expiresAtMillis <= Date.now()) {
@@ -11083,6 +12482,7 @@ var MediaService = class {
11083
12482
  if (descriptor?.stableKey !== void 0) this.stableMediaRefs.delete(descriptor.stableKey);
11084
12483
  throw new ArkmePluginError("media-ref-invalid", "媒体引用已失效,请刷新对话后重试", false, 404);
11085
12484
  }
12485
+ if (originalOnly && descriptor.purpose !== "original") throw new ArkmePluginError("original-unavailable", "原文件不可用,请刷新后重试;不会用预览图代替原文件", false, 404);
11086
12486
  const url = new URL(descriptor.remoteUrl);
11087
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);
11088
12488
  const response = await this.runtime.fetchImpl(url, {
@@ -11244,6 +12644,15 @@ var MediaService = class {
11244
12644
  if ((snapshot.profile?.avatarRef.trim() ?? "") === "") try {
11245
12645
  snapshot = await this.profile.refreshProfileForSession(session);
11246
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
+ }
11247
12656
  const ownAvatarRef = snapshot.profile?.avatarRef.trim() ?? "";
11248
12657
  if (ownAvatarRef !== "" && !ownAvatarRef.startsWith("arkme-profile-image-v1.")) return await this.readImage(ownAvatarRef, {
11249
12658
  maxBytes: byteLimit,
@@ -11309,10 +12718,14 @@ var MediaService = class {
11309
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);
11310
12719
  return await this.downloadSignedImage(signedUrl, byteLimit, signal, this.runtime.requestScope(session.userId));
11311
12720
  }
11312
- mediaKind(fileKind, mimeType) {
11313
- if (fileKind === 1 || mimeType.startsWith("image/")) return "image";
11314
- if (fileKind === 2 || mimeType.startsWith("audio/")) return "audio";
11315
- 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
+ }
11316
12729
  return "file";
11317
12730
  }
11318
12731
  recordContentPayload(raw) {
@@ -11429,6 +12842,29 @@ var MediaService = class {
11429
12842
  }
11430
12843
  return mediaRefs;
11431
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
+ }
11432
12868
  async hydrateRecordMediaPage(rawItems, session, signal) {
11433
12869
  const displayItemsByRecordUid = /* @__PURE__ */ new Map();
11434
12870
  const expectedRecordUids = [...new Set(rawItems.flatMap((raw) => {
@@ -11555,14 +12991,23 @@ var MediaService = class {
11555
12991
  return Math.trunc(numberValue$9(item.content_file_role)) !== RECORD_CONTENT_FILE_ROLE_BACKGROUND_SOUND;
11556
12992
  }).flatMap((item, index) => {
11557
12993
  const fileAssetUid = stringValue$1(item.file_asset_uid).trim();
11558
- const mimeType = stringValue$1(item.mime_type).trim() || "application/octet-stream";
11559
12994
  const fileName = stringValue$1(item.file_name).trim() || `附件-${String(index + 1)}`;
11560
12995
  const fileKind = Math.trunc(numberValue$9(item.file_kind));
11561
- 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);
11562
12998
  const remoteUrl = stringValue$1(kind === "image" ? item.preview_url ?? item.download_url : item.download_url).trim();
11563
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}`);
11564
13008
  return [{
11565
13009
  kind,
13010
+ ...originalRef === void 0 ? {} : { originalRef },
11566
13011
  mediaRef: this.issueMediaRef(viewerUserId, {
11567
13012
  remoteUrl,
11568
13013
  mimeType,
@@ -11965,6 +13410,10 @@ function phoneDefaultAvatarFallback(raw) {
11965
13410
  label
11966
13411
  };
11967
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
+ }
11968
13417
  function allowedSignedImageHost$1(environment, hostname) {
11969
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());
11970
13419
  }
@@ -12016,7 +13465,9 @@ var ProfileService = class {
12016
13465
  }
12017
13466
  async cachedProfile() {
12018
13467
  const session = await this.runtime.requireAuthFlowSession();
12019
- 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;
12020
13471
  }
12021
13472
  async refreshProfile() {
12022
13473
  const session = await this.runtime.requireAuthFlowSession();
@@ -12095,6 +13546,7 @@ var ProfileService = class {
12095
13546
  if (memory !== void 0 && memory.expiresAtMillis > Date.now()) return memory.value;
12096
13547
  const persisted = await this.runtime.stateStore.cachedProfile(session.userId).catch(() => void 0);
12097
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);
12098
13550
  this.profileCache.set(session.userId, {
12099
13551
  value: persisted,
12100
13552
  expiresAtMillis: persisted.cachedAtMillis + PROFILE_CACHE_TTL_MS
@@ -12116,16 +13568,21 @@ var ProfileService = class {
12116
13568
  if (userId <= 0 || userId !== session.userId) throw new ArkmePluginError("profile-contract-invalid", "Arkme 个人资料响应缺少有效用户标识", false, 502);
12117
13569
  const nickname = stringValue$1(data.nick_name).trim();
12118
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用户";
12119
- 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;
12120
13575
  const phone = maskedPhone(stringValue$1(data.phone));
12121
13576
  const email = maskedEmail(stringValue$1(data.email));
13577
+ const wechatName = stringValue$1(data.wechat_nick_name).trim();
12122
13578
  const canUpdateArkmeId = optionalBooleanValue(data.can_update_jotmo_id);
12123
13579
  const profile = {
12124
13580
  userId,
12125
13581
  displayName,
12126
13582
  nickname,
12127
13583
  avatarRef,
12128
- .../^https?:\/\//i.test(avatarRef) ? { avatarUrl: avatarRef } : {},
13584
+ ...avatarAssetRef === void 0 ? {} : { avatarAssetRef },
13585
+ ...avatarUrl === void 0 ? {} : { avatarUrl },
12129
13586
  arkmeId: stringValue$1(data.jotmo_id).trim() || stringValue$1(data.name_slug).trim(),
12130
13587
  ...canUpdateArkmeId === void 0 ? {} : { canUpdateArkmeId },
12131
13588
  accountType: numberValue$7(data.type),
@@ -12135,6 +13592,7 @@ var ProfileService = class {
12135
13592
  wechat: booleanValue$2(data.has_bind_wechat),
12136
13593
  google: booleanValue$2(data.has_bind_google)
12137
13594
  },
13595
+ ...wechatName === "" ? {} : { bindingNames: { wechat: wechatName } },
12138
13596
  contact: {
12139
13597
  ...phone === void 0 ? {} : { phoneMasked: phone },
12140
13598
  ...email === void 0 ? {} : { emailMasked: email }
@@ -12154,6 +13612,12 @@ var ProfileService = class {
12154
13612
  if (this.profileInFlight.get(session.userId) === pending) this.profileInFlight.delete(session.userId);
12155
13613
  }
12156
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
+ }
12157
13621
  async publicProfileSummariesByUserIds(userIds, session, signal) {
12158
13622
  const normalized = [...new Set(userIds.filter((userId) => Number.isSafeInteger(userId) && userId > 0))].sort((left, right) => left - right);
12159
13623
  const profiles = /* @__PURE__ */ new Map();
@@ -13487,6 +14951,38 @@ var SearchService = class {
13487
14951
  }, session, options.signal);
13488
14952
  return await this.withNavigationTargets(this.recordSearchResult(data, lockedRecordUids), options.signal);
13489
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
+ }
13490
14986
  async withAudioMediaRefs(result, signal) {
13491
14987
  const requests = result.items.flatMap((item) => item.voice === void 0 || item.sourceKind === 3 ? [] : [{
13492
14988
  recordUid: item.recordUid,
@@ -13789,6 +15285,561 @@ var SearchService = class {
13789
15285
  }
13790
15286
  };
13791
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
13792
15843
  //#region src/services/unmarked-speaker-service.ts
13793
15844
  const REF_TTL_MS = 30 * 6e4;
13794
15845
  const REF_CAP = 2e3;
@@ -16459,6 +18510,8 @@ var ArkmeService = class {
16459
18510
  contactDirectory;
16460
18511
  unmarkedSpeaker;
16461
18512
  voiceprint;
18513
+ fileTransfers;
18514
+ localFileOpener;
16462
18515
  worldVoiceprintInviteVariantIndex = 0;
16463
18516
  constructor(config, sessionStore, stateStore, fetchImpl = fetch, pendingSessionStore, outgoingCallBroker = new ArkmeOutgoingCallBroker(), billingGateway) {
16464
18517
  this.config = config;
@@ -16513,9 +18566,74 @@ var ArkmeService = class {
16513
18566
  this.clearAccountState(userIds);
16514
18567
  }
16515
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);
16516
18633
  }
16517
18634
  clearAccountState(userIds) {
16518
18635
  for (const userId of userIds) this.privacy.clear(userId);
18636
+ this.fileTransfers?.cancelActive();
16519
18637
  for (const userId of userIds) this.outgoingCall.clearUser(userId, "账号已退出,呼叫已取消");
16520
18638
  this.source.dispose();
16521
18639
  this.media.dispose();
@@ -16598,7 +18716,9 @@ var ArkmeService = class {
16598
18716
  return await this.bot.openBotPrivateChat(botRef, options);
16599
18717
  }
16600
18718
  async sendBotPrivateChatMessage(botRef, content, options = {}) {
16601
- 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;
16602
18722
  }
16603
18723
  async listGroupBots(groupSourceRef, options = {}) {
16604
18724
  return await this.bot.listGroupBots(groupSourceRef, options);
@@ -16661,6 +18781,7 @@ var ArkmeService = class {
16661
18781
  retryOutbox: true,
16662
18782
  revisionPolling: true,
16663
18783
  userProfile: true,
18784
+ accountSettings: true,
16664
18785
  imageRead: true,
16665
18786
  recordCalendar: true,
16666
18787
  imageLibrary: true,
@@ -16681,6 +18802,7 @@ var ArkmeService = class {
16681
18802
  contactAdd: true,
16682
18803
  conversationQuickAdd: true,
16683
18804
  groupSettings: true,
18805
+ groupAiPolish: true,
16684
18806
  extensionManagement: true,
16685
18807
  extensionMetadataEdit: true,
16686
18808
  extensionIcons: true,
@@ -16750,6 +18872,7 @@ var ArkmeService = class {
16750
18872
  return await this.callHistory.retryCallSummary(callRef, signal);
16751
18873
  }
16752
18874
  dispose() {
18875
+ this.fileTransfers?.cancelActive();
16753
18876
  this.contact.dispose();
16754
18877
  this.contactDirectory.dispose();
16755
18878
  this.unmarkedSpeaker.dispose();
@@ -16947,6 +19070,12 @@ var ArkmeService = class {
16947
19070
  async generateGroupAiPolishRule(groupName, requirement, options = {}) {
16948
19071
  return await this.aiPolish.generateGroupAiPolishRule(groupName, requirement, options);
16949
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
+ }
16950
19079
  async confirmEnableGroupAiPolish(confirmationRef, options = {}) {
16951
19080
  return await this.aiPolish.confirmEnableGroupAiPolish(confirmationRef, options);
16952
19081
  }
@@ -17059,6 +19188,21 @@ var ArkmeService = class {
17059
19188
  async reportMessage(messageRef, reportType, options = {}) {
17060
19189
  return await this.chat.reportMessage(messageRef, reportType, options);
17061
19190
  }
19191
+ async copySourceMessageLink(sourceRef, actionRefs, options = {}) {
19192
+ return await this.chat.copySourceMessageLink(sourceRef, actionRefs, options);
19193
+ }
19194
+ async resolveMessageCopyLink(sid, options = {}) {
19195
+ return await this.chat.resolveMessageCopyLink(sid, options);
19196
+ }
19197
+ async extendMessageCopyLink(sid, itemIndex, textContent, recordUid, options = {}) {
19198
+ return await this.chat.extendMessageCopyLink(sid, itemIndex, textContent, recordUid, options);
19199
+ }
19200
+ async resolveLinkMetadata(rawUrl, options = {}) {
19201
+ return await this.chat.resolveLinkMetadata(rawUrl, options);
19202
+ }
19203
+ async forwardSourceMessages(sourceRef, actionRefs, options = {}) {
19204
+ return await this.chat.forwardSourceMessages(sourceRef, actionRefs, options);
19205
+ }
17062
19206
  async sendSourceText(sourceRef, textContent, options = {}) {
17063
19207
  return await this.chat.sendSourceText(sourceRef, textContent, options);
17064
19208
  }
@@ -18306,6 +20450,7 @@ function persistentClientFactory(requireModule, spec) {
18306
20450
  return {
18307
20451
  name: `arkme-extension-client:${spec.extensionId}`,
18308
20452
  async apply(ctx) {
20453
+ if ((typeof document.location?.search === "string" ? document.location.search : "").split(/[?&]/).includes("arkme-harness-embed=1")) return;
18309
20454
  const styles = new Styles();
18310
20455
  const taggedConsole = {
18311
20456
  ...console,
@@ -18420,7 +20565,7 @@ function renderPersistentClientBundle(packageName, spec) {
18420
20565
  const renderedSpec = {
18421
20566
  ...spec,
18422
20567
  clientContentDigest: spec.clientContentDigest ?? arkmeClientContentDigest(spec.code),
18423
- wrapperVersion: 3
20568
+ wrapperVersion: 4
18424
20569
  };
18425
20570
  return [
18426
20571
  `window.__ModuleLoader__.load({ id: ${JSON.stringify(packageName)}, factory: (require) => {`,
@@ -18438,13 +20583,16 @@ function renderArkmeBundleClientBundle(packageName, spec) {
18438
20583
  identityKey: "packageName"
18439
20584
  });
18440
20585
  }
18441
- function packageIdentity(extensionId) {
20586
+ function persistentExtensionPackageIdentity(extensionId) {
18442
20587
  const suffix = createHash("sha256").update(extensionId).digest("hex").slice(0, 16);
18443
20588
  return {
18444
20589
  packageName: `@arkme-local/ext-${suffix}`,
18445
20590
  entryId: `arkme-extension-${suffix}`
18446
20591
  };
18447
20592
  }
20593
+ function persistentExtensionBundleDirectory(profileDirectory, extensionId, version) {
20594
+ return join(profileDirectory, "arkme-extensions", createHash("sha256").update(extensionId).digest("hex").slice(0, 16), version);
20595
+ }
18448
20596
  function writeSecure(path, content) {
18449
20597
  writeFileSync(path, content, {
18450
20598
  encoding: "utf8",
@@ -18541,9 +20689,22 @@ function readPersistentExtensionActivation(installationUrl) {
18541
20689
  }
18542
20690
  return state;
18543
20691
  }
20692
+ function repairPersistentExtensionInstallation(input) {
20693
+ const installationPath = join(input.bundleDirectory, "installation.json");
20694
+ const installation = JSON.parse(readFileSync(installationPath, "utf8"));
20695
+ if (installation.format_version !== 1 || installation.extension_id !== input.extensionId || installation.version !== input.version || typeof installation.artifact_path !== "string" || installation.artifact_path.trim() === "" || installation.artifact_sha256 !== input.artifactSha256 || installation.manifest_sha256 !== input.manifestSha256 || typeof installation.signature !== "string" || installation.signature.trim() === "" || typeof installation.signing_key_id !== "string" || installation.signing_key_id.trim() === "" || typeof installation.trusted_public_key !== "string" || installation.trusted_public_key.trim() === "" || !Number.isSafeInteger(installation.published_at) || (installation.published_at ?? -1) < 0) throw new Error("Arkme persistent extension installation identity mismatch");
20696
+ const manifest = JSON.parse(readFileSync(join(input.bundleDirectory, "package.json"), "utf8"));
20697
+ if (manifest.name !== input.packageName || manifest.version !== input.version) throw new Error("Arkme persistent extension package identity mismatch");
20698
+ if (installation.artifact_path === input.artifactPath) return false;
20699
+ replaceSecure(installationPath, `${JSON.stringify({
20700
+ ...installation,
20701
+ artifact_path: input.artifactPath
20702
+ }, void 0, 2)}\n`);
20703
+ return true;
20704
+ }
18544
20705
  function materializePersistentExtensionBundle(input) {
18545
- const { packageName, entryId } = packageIdentity(input.resolution.extension_id);
18546
- const bundleDirectory = join(input.profileDirectory, "arkme-extensions", createHash("sha256").update(input.resolution.extension_id).digest("hex").slice(0, 16), input.resolution.version);
20706
+ const { packageName, entryId } = persistentExtensionPackageIdentity(input.resolution.extension_id);
20707
+ const bundleDirectory = persistentExtensionBundleDirectory(input.profileDirectory, input.resolution.extension_id, input.resolution.version);
18547
20708
  const installation = {
18548
20709
  format_version: 1,
18549
20710
  extension_id: input.resolution.extension_id,
@@ -18568,7 +20729,7 @@ function materializePersistentExtensionBundle(input) {
18568
20729
  try {
18569
20730
  const manifest = JSON.parse(readFileSync(join(bundleDirectory, "package.json"), "utf8"));
18570
20731
  const clientBundle = input.clientCode === void 0 ? void 0 : readFileSync(join(bundleDirectory, "lib", "client.js"), "utf8");
18571
- const clientReady = input.clientCode === void 0 || manifest.exports?.["./client"] === "./lib/client.js" && manifest.dsh?.client?.inject?.length === 0 && clientBundle?.includes("extensions.persistent.invoke") === true && clientBundle.includes("extensions.persistent.client-state") && clientBundle.includes(`\"wrapperVersion\":${String(3)}`);
20732
+ const clientReady = input.clientCode === void 0 || manifest.exports?.["./client"] === "./lib/client.js" && manifest.dsh?.client?.inject?.length === 0 && clientBundle?.includes("extensions.persistent.invoke") === true && clientBundle.includes("extensions.persistent.client-state") && clientBundle.includes(`\"wrapperVersion\":${String(4)}`);
18572
20733
  if (manifest.exports?.["."] === "./lib/index.js" && manifest.exports?.["./package.json"] === "./package.json" && clientReady) {
18573
20734
  if (!existsSync(activationPath)) writeSecure(activationPath, activationText(input.resolution.extension_id, true));
18574
20735
  return {
@@ -18654,7 +20815,21 @@ const VM_TIMEOUT_MS = 5e3;
18654
20815
  function readInstallation(url) {
18655
20816
  const value = JSON.parse(readFileSync(url, "utf8"));
18656
20817
  if (value.format_version !== 1 || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value.extension_id) || value.version.trim() === "" || value.artifact_path.trim() === "" || value.trusted_public_key.trim() === "") throw new Error("Arkme persistent extension installation metadata is invalid");
18657
- return value;
20818
+ return {
20819
+ ...value,
20820
+ artifact_path: resolveRuntimeArtifactPath(value, url)
20821
+ };
20822
+ }
20823
+ function resolveRuntimeArtifactPath(installation, installationUrl) {
20824
+ const dshHome = process.env.DSH_HOME?.trim();
20825
+ const filename = basename(installation.artifact_path);
20826
+ if (dshHome !== void 0 && dshHome !== "" && filename !== "" && filename !== "." && filename !== "..") try {
20827
+ const root = realpathSync(join(dshHome, "arkme-self", "extensions"));
20828
+ const candidate = realpathSync(join(root, installation.extension_id, installation.version, filename));
20829
+ const fromRoot = relative(root, candidate);
20830
+ if (fromRoot !== "" && fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot) && sha256Hex(readFileSync(candidate)) === installation.artifact_sha256) return candidate;
20831
+ } catch {}
20832
+ return isAbsolute(installation.artifact_path) ? installation.artifact_path : resolve(dirname(fileURLToPath(installationUrl)), installation.artifact_path);
18658
20833
  }
18659
20834
  function jsonValue(value) {
18660
20835
  if (value === void 0) return null;
@@ -18922,4 +21097,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
18922
21097
  return jsonValue(await handler(jsonValue(args)));
18923
21098
  }
18924
21099
  //#endregion
18925
- export { readNativeBundleTarballV3 as A, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as B, arkmeSandboxEntryId as C, inspectNativeBundleArtifactV3 as D, inspectBundleArtifact as E, unpackArkmeExtension as F, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as G, validateVoiceprintPcm16Wav as H, ARKME_EXTENSION_FORMAT as I, ARKME_WORLD_PUBLISH_MAX_IMAGES as J, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as K, ARKME_EXTENSION_ICON_MAX_BYTES as L, assertExtensionArtifactSize as M, canonicalExtensionJson as N, packBundleFiles as O, sha256Hex as P, ArkmeOutgoingCallError as Q, ARKME_EXTENSION_MAX_BYTES as R, ArkmeBundleArtifactError as S, canonicalBundleJson as T, ARKME_DEFAULT_SHARE_WEBSITE as U, ArkmeService as V, ARKME_PROVIDER_CONTRACT_VERSION as W, isArkmeBotAvatarRef as X, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as Y, ArkmePluginError as Z, verifyExtensionResolutionSignature as _, persistentArkmeExtensionActive as a, ARKME_BUNDLE_MAX_BYTES as b, quarantinePersistentExtension as c, writePersistentExtensionActivation as d, renderArkmeBundleClientBundle as f, verifyBundleResolutionSignature as g, arkmeClientContentDigestFromSource as h, invokePersistentArkmeExtension as i, renderArkmeSandboxHostEntry as j, packLocalNativeBundleDirectoryV3 as k, readPersistentExtensionActivation as l, arkmeClientContentDigest as m, applyPersistentArkmeHostExtension as n, persistentArkmeExtensionRuntimeState as o, activeProfileExtensionOwnerConflicts as p, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as q, deactivatePersistentArkmeExtension as r, materializePersistentExtensionBundle as s, activatePersistentArkmeExtension as t, refreshPersistentClientWrapper as u, verifyNativeBundleResolutionSignature as v, bundleSha256 as w, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as x, ARKME_BUNDLE_ARTIFACT_KIND as y, ARKME_EXTENSION_PREVIEW_MAX_BYTES 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 };