@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.
- package/docs/consumer-plugin-contract.md +35 -1
- package/docs/file-flow-parity.md +213 -0
- package/docs/file-icon-licenses.md +54 -0
- package/docs/group-ai-polish-member-enablement.md +49 -0
- package/lib/client.js +8486 -1506
- package/lib/client.js.map +1 -1
- package/lib/index.js +633 -28
- package/lib/persistent-extension.js +1 -1
- package/lib/{persistent-runtime-DLKV_LTp.js → persistent-runtime-BJ8konXI.js} +2319 -144
- package/lib/sdk.js +198 -0
- package/lib/types/arkme-service.d.ts +58 -1
- package/lib/types/arkme-service.d.ts.map +1 -1
- package/lib/types/chat-realtime.d.ts +7 -1
- package/lib/types/chat-realtime.d.ts.map +1 -1
- package/lib/types/client/ArkmeAttachmentStrip.d.ts +11 -0
- package/lib/types/client/ArkmeAttachmentStrip.d.ts.map +1 -0
- package/lib/types/client/ArkmeBillingSettings.d.ts.map +1 -1
- package/lib/types/client/ArkmeBotConversationSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeFileIcon.d.ts +24 -0
- package/lib/types/client/ArkmeFileIcon.d.ts.map +1 -0
- package/lib/types/client/ArkmeFileQuickView.d.ts +6 -0
- package/lib/types/client/ArkmeFileQuickView.d.ts.map +1 -0
- package/lib/types/client/ArkmeFileViewer.d.ts +22 -0
- package/lib/types/client/ArkmeFileViewer.d.ts.map +1 -0
- package/lib/types/client/ArkmeFooterAction.d.ts +3 -1
- package/lib/types/client/ArkmeFooterAction.d.ts.map +1 -1
- package/lib/types/client/ArkmeFooterDropdown.d.ts.map +1 -1
- package/lib/types/client/ArkmeGroupChatControls.d.ts +3 -1
- package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
- package/lib/types/client/ArkmeNoteDetails.d.ts +3 -1
- package/lib/types/client/ArkmeNoteDetails.d.ts.map +1 -1
- package/lib/types/client/ArkmePersistentShell.d.ts +3 -0
- package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
- package/lib/types/client/ArkmeProductNavigation.d.ts +2 -1
- package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -1
- package/lib/types/client/ArkmeRichContent.d.ts +28 -4
- package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
- package/lib/types/client/ArkmeSearchSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeSettingsSurface.d.ts +5 -1
- package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeSidebar.d.ts +15 -0
- package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
- package/lib/types/client/ArkmeUpdateSurfaces.d.ts +5 -39
- package/lib/types/client/ArkmeUpdateSurfaces.d.ts.map +1 -1
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts +9 -2
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
- package/lib/types/client/ArkmeWebLoginOverlay.d.ts +6 -0
- package/lib/types/client/ArkmeWebLoginOverlay.d.ts.map +1 -0
- package/lib/types/client/DeepSeekHarnessSurface.d.ts +7 -4
- package/lib/types/client/DeepSeekHarnessSurface.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +1 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/arkme-auth-flow.d.ts +4 -0
- package/lib/types/client/arkme-auth-flow.d.ts.map +1 -1
- package/lib/types/client/composer-draft-store.d.ts +25 -2
- package/lib/types/client/composer-draft-store.d.ts.map +1 -1
- package/lib/types/client/file-send-tasks.d.ts +12 -0
- package/lib/types/client/file-send-tasks.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/plugin-update-store.d.ts +28 -1
- package/lib/types/client/plugin-update-store.d.ts.map +1 -1
- package/lib/types/client/read-retry.d.ts +7 -0
- package/lib/types/client/read-retry.d.ts.map +1 -0
- package/lib/types/client/ui-controller.d.ts +4 -0
- package/lib/types/client/ui-controller.d.ts.map +1 -1
- package/lib/types/client/update-presentation.d.ts +42 -0
- package/lib/types/client/update-presentation.d.ts.map +1 -0
- package/lib/types/extensions/manager.d.ts +4 -0
- package/lib/types/extensions/manager.d.ts.map +1 -1
- package/lib/types/extensions/persistent-bundle.d.ts +14 -0
- package/lib/types/extensions/persistent-bundle.d.ts.map +1 -1
- package/lib/types/extensions/persistent-client-bundle.d.ts +1 -1
- package/lib/types/extensions/persistent-client-bundle.d.ts.map +1 -1
- package/lib/types/extensions/persistent-runtime.d.ts.map +1 -1
- package/lib/types/extensions/profile-installer.d.ts.map +1 -1
- package/lib/types/file-transfer-contract.d.ts +63 -0
- package/lib/types/file-transfer-contract.d.ts.map +1 -0
- package/lib/types/file-transfer-owner.d.ts +17 -0
- package/lib/types/file-transfer-owner.d.ts.map +1 -0
- package/lib/types/harness-embed-contract.d.ts +2 -0
- package/lib/types/harness-embed-contract.d.ts.map +1 -0
- package/lib/types/harness-embed-route.d.ts +32 -0
- package/lib/types/harness-embed-route.d.ts.map +1 -0
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/plugin-update.d.ts.map +1 -1
- package/lib/types/rich-media-routes.d.ts +2 -1
- package/lib/types/rich-media-routes.d.ts.map +1 -1
- package/lib/types/runtime-instance.d.ts +8 -0
- package/lib/types/runtime-instance.d.ts.map +1 -0
- package/lib/types/sdk/index.d.ts +65 -2
- package/lib/types/sdk/index.d.ts.map +1 -1
- package/lib/types/services/bot-service.d.ts +15 -1
- package/lib/types/services/bot-service.d.ts.map +1 -1
- package/lib/types/services/chat-realtime-service.d.ts +1 -1
- package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
- package/lib/types/services/chat-service.d.ts +39 -4
- package/lib/types/services/chat-service.d.ts.map +1 -1
- package/lib/types/services/file-transfers.d.ts +65 -0
- package/lib/types/services/file-transfers.d.ts.map +1 -0
- package/lib/types/services/group-ai-polish-service.d.ts +17 -2
- package/lib/types/services/group-ai-polish-service.d.ts.map +1 -1
- package/lib/types/services/media-service.d.ts +11 -1
- package/lib/types/services/media-service.d.ts.map +1 -1
- package/lib/types/services/profile-service.d.ts +1 -0
- package/lib/types/services/profile-service.d.ts.map +1 -1
- package/lib/types/services/search-service.d.ts +6 -0
- package/lib/types/services/search-service.d.ts.map +1 -1
- package/lib/types/services/service.d.ts +1 -0
- package/lib/types/services/service.d.ts.map +1 -1
- package/lib/types/services/source-service.d.ts +2 -0
- package/lib/types/services/source-service.d.ts.map +1 -1
- package/lib/types/tools/business/conversation/copy-link-extend.d.ts +2 -0
- package/lib/types/tools/business/conversation/copy-link-extend.d.ts.map +1 -0
- package/lib/types/tools/business/conversation/group-ai-polish.d.ts.map +1 -1
- package/lib/types/tools/business/index.d.ts.map +1 -1
- package/lib/types/tools/business/media/files.d.ts +2 -0
- package/lib/types/tools/business/media/files.d.ts.map +1 -0
- package/lib/types/tools/business/media/index.d.ts.map +1 -1
- package/lib/types/tools/ports/bots.d.ts +1 -1
- package/lib/types/tools/ports/bots.d.ts.map +1 -1
- package/lib/types/tools/ports/conversations.d.ts +7 -1
- package/lib/types/tools/ports/conversations.d.ts.map +1 -1
- package/lib/types/tools/ports/files.d.ts +22 -0
- package/lib/types/tools/ports/files.d.ts.map +1 -0
- package/lib/types/tools/ports/index.d.ts +2 -1
- package/lib/types/tools/ports/index.d.ts.map +1 -1
- package/lib/types/tools/prompts/business.d.ts.map +1 -1
- package/lib/types/tools/registry/registrar.d.ts.map +1 -1
- package/lib/types/types.d.ts +124 -2
- package/lib/types/types.d.ts.map +1 -1
- 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
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
-
|
|
4748
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
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
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
6582
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
9621
|
-
|
|
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
|
|
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
|
-
|
|
9629
|
-
if (!
|
|
9630
|
-
const
|
|
9631
|
-
|
|
9632
|
-
|
|
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
|
-
|
|
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
|
|
11062
|
+
async prepareEnableGroupAiPolish(groupName, ruleName = "", options = {}) {
|
|
9660
11063
|
const source = await this.resolveUniqueGroupByName(groupName, options.signal);
|
|
9661
|
-
return await this.
|
|
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
|
|
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
|
|
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
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
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
|
|
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
|
-
|
|
11314
|
-
if (
|
|
11315
|
-
if (fileKind ===
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 } =
|
|
18546
|
-
const bundleDirectory =
|
|
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(
|
|
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
|
|
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 {
|
|
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 };
|