@senguoyun/dsh-arkme 0.1.30 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4989 -1057
- package/lib/client.js.map +1 -1
- package/lib/index.js +402 -25
- package/lib/persistent-extension.js +1 -1
- package/lib/{persistent-runtime-D-NwFt9z.js → persistent-runtime-BJ8konXI.js} +1424 -95
- package/lib/sdk.js +154 -0
- package/lib/types/arkme-service.d.ts +39 -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/ArkmeRichContent.d.ts +13 -3
- 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 +1 -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.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/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.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/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-route.d.ts.map +1 -1
- 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 +50 -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 +2 -0
- 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/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 +3 -0
- 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/registry/registrar.d.ts.map +1 -1
- package/lib/types/types.d.ts +43 -2
- package/lib/types/types.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -2,9 +2,10 @@ import { chmodSync, createReadStream, existsSync, lstatSync, mkdirSync, readFile
|
|
|
2
2
|
import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicKey, randomBytes, randomUUID, timingSafeEqual, verify } from "node:crypto";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { open } from "node:fs/promises";
|
|
5
|
+
import { chmod as chmod$1, copyFile, link, mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
6
|
import { pinyin } from "pinyin-pro";
|
|
7
7
|
import OSS from "ali-oss";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
8
9
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
9
10
|
import { createContext, runInContext } from "node:vm";
|
|
10
11
|
import { Context } from "@deepseek-ai/cordis";
|
|
@@ -3445,6 +3446,258 @@ function arkmeMentionMetadataMentionsViewer(record, payload, viewerUserId) {
|
|
|
3445
3446
|
return false;
|
|
3446
3447
|
}
|
|
3447
3448
|
//#endregion
|
|
3449
|
+
//#region src/file-transfer-contract.ts
|
|
3450
|
+
const ARKME_TOOL_FILE_MAX_BYTES = 64 * 1024;
|
|
3451
|
+
const mediaTypesByExtension = {
|
|
3452
|
+
jpg: {
|
|
3453
|
+
mimeType: "image/jpeg",
|
|
3454
|
+
kind: "image"
|
|
3455
|
+
},
|
|
3456
|
+
jpeg: {
|
|
3457
|
+
mimeType: "image/jpeg",
|
|
3458
|
+
kind: "image"
|
|
3459
|
+
},
|
|
3460
|
+
png: {
|
|
3461
|
+
mimeType: "image/png",
|
|
3462
|
+
kind: "image"
|
|
3463
|
+
},
|
|
3464
|
+
gif: {
|
|
3465
|
+
mimeType: "image/gif",
|
|
3466
|
+
kind: "image"
|
|
3467
|
+
},
|
|
3468
|
+
webp: {
|
|
3469
|
+
mimeType: "image/webp",
|
|
3470
|
+
kind: "image"
|
|
3471
|
+
},
|
|
3472
|
+
avif: {
|
|
3473
|
+
mimeType: "image/avif",
|
|
3474
|
+
kind: "image"
|
|
3475
|
+
},
|
|
3476
|
+
bmp: {
|
|
3477
|
+
mimeType: "image/bmp",
|
|
3478
|
+
kind: "image"
|
|
3479
|
+
},
|
|
3480
|
+
heic: {
|
|
3481
|
+
mimeType: "image/heic",
|
|
3482
|
+
kind: "image"
|
|
3483
|
+
},
|
|
3484
|
+
heif: {
|
|
3485
|
+
mimeType: "image/heif",
|
|
3486
|
+
kind: "image"
|
|
3487
|
+
},
|
|
3488
|
+
svg: {
|
|
3489
|
+
mimeType: "image/svg+xml",
|
|
3490
|
+
kind: "image"
|
|
3491
|
+
},
|
|
3492
|
+
tif: {
|
|
3493
|
+
mimeType: "image/tiff",
|
|
3494
|
+
kind: "image"
|
|
3495
|
+
},
|
|
3496
|
+
tiff: {
|
|
3497
|
+
mimeType: "image/tiff",
|
|
3498
|
+
kind: "image"
|
|
3499
|
+
},
|
|
3500
|
+
mp4: {
|
|
3501
|
+
mimeType: "video/mp4",
|
|
3502
|
+
kind: "video"
|
|
3503
|
+
},
|
|
3504
|
+
m4v: {
|
|
3505
|
+
mimeType: "video/mp4",
|
|
3506
|
+
kind: "video"
|
|
3507
|
+
},
|
|
3508
|
+
mov: {
|
|
3509
|
+
mimeType: "video/quicktime",
|
|
3510
|
+
kind: "video"
|
|
3511
|
+
},
|
|
3512
|
+
webm: {
|
|
3513
|
+
mimeType: "video/webm",
|
|
3514
|
+
kind: "video"
|
|
3515
|
+
},
|
|
3516
|
+
ogv: {
|
|
3517
|
+
mimeType: "video/ogg",
|
|
3518
|
+
kind: "video"
|
|
3519
|
+
},
|
|
3520
|
+
mkv: {
|
|
3521
|
+
mimeType: "video/x-matroska",
|
|
3522
|
+
kind: "video"
|
|
3523
|
+
},
|
|
3524
|
+
avi: {
|
|
3525
|
+
mimeType: "video/x-msvideo",
|
|
3526
|
+
kind: "video"
|
|
3527
|
+
},
|
|
3528
|
+
mpg: {
|
|
3529
|
+
mimeType: "video/mpeg",
|
|
3530
|
+
kind: "video"
|
|
3531
|
+
},
|
|
3532
|
+
mpeg: {
|
|
3533
|
+
mimeType: "video/mpeg",
|
|
3534
|
+
kind: "video"
|
|
3535
|
+
},
|
|
3536
|
+
mp3: {
|
|
3537
|
+
mimeType: "audio/mpeg",
|
|
3538
|
+
kind: "audio"
|
|
3539
|
+
},
|
|
3540
|
+
m4a: {
|
|
3541
|
+
mimeType: "audio/mp4",
|
|
3542
|
+
kind: "audio"
|
|
3543
|
+
},
|
|
3544
|
+
aac: {
|
|
3545
|
+
mimeType: "audio/aac",
|
|
3546
|
+
kind: "audio"
|
|
3547
|
+
},
|
|
3548
|
+
wav: {
|
|
3549
|
+
mimeType: "audio/wav",
|
|
3550
|
+
kind: "audio"
|
|
3551
|
+
},
|
|
3552
|
+
ogg: {
|
|
3553
|
+
mimeType: "audio/ogg",
|
|
3554
|
+
kind: "audio"
|
|
3555
|
+
},
|
|
3556
|
+
oga: {
|
|
3557
|
+
mimeType: "audio/ogg",
|
|
3558
|
+
kind: "audio"
|
|
3559
|
+
},
|
|
3560
|
+
flac: {
|
|
3561
|
+
mimeType: "audio/flac",
|
|
3562
|
+
kind: "audio"
|
|
3563
|
+
},
|
|
3564
|
+
opus: {
|
|
3565
|
+
mimeType: "audio/opus",
|
|
3566
|
+
kind: "audio"
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
const safeVisualTypesByExtension = {
|
|
3570
|
+
jpg: {
|
|
3571
|
+
mimeType: "image/jpeg",
|
|
3572
|
+
kind: "image"
|
|
3573
|
+
},
|
|
3574
|
+
jpeg: {
|
|
3575
|
+
mimeType: "image/jpeg",
|
|
3576
|
+
kind: "image"
|
|
3577
|
+
},
|
|
3578
|
+
png: {
|
|
3579
|
+
mimeType: "image/png",
|
|
3580
|
+
kind: "image"
|
|
3581
|
+
},
|
|
3582
|
+
gif: {
|
|
3583
|
+
mimeType: "image/gif",
|
|
3584
|
+
kind: "image"
|
|
3585
|
+
},
|
|
3586
|
+
webp: {
|
|
3587
|
+
mimeType: "image/webp",
|
|
3588
|
+
kind: "image"
|
|
3589
|
+
},
|
|
3590
|
+
avif: {
|
|
3591
|
+
mimeType: "image/avif",
|
|
3592
|
+
kind: "image"
|
|
3593
|
+
},
|
|
3594
|
+
bmp: {
|
|
3595
|
+
mimeType: "image/bmp",
|
|
3596
|
+
kind: "image"
|
|
3597
|
+
},
|
|
3598
|
+
mp4: {
|
|
3599
|
+
mimeType: "video/mp4",
|
|
3600
|
+
kind: "video"
|
|
3601
|
+
},
|
|
3602
|
+
m4v: {
|
|
3603
|
+
mimeType: "video/mp4",
|
|
3604
|
+
kind: "video"
|
|
3605
|
+
},
|
|
3606
|
+
mov: {
|
|
3607
|
+
mimeType: "video/quicktime",
|
|
3608
|
+
kind: "video"
|
|
3609
|
+
},
|
|
3610
|
+
webm: {
|
|
3611
|
+
mimeType: "video/webm",
|
|
3612
|
+
kind: "video"
|
|
3613
|
+
},
|
|
3614
|
+
ogv: {
|
|
3615
|
+
mimeType: "video/ogg",
|
|
3616
|
+
kind: "video"
|
|
3617
|
+
}
|
|
3618
|
+
};
|
|
3619
|
+
const safeImageMimeTypes = /* @__PURE__ */ new Set([
|
|
3620
|
+
"image/jpeg",
|
|
3621
|
+
"image/png",
|
|
3622
|
+
"image/gif",
|
|
3623
|
+
"image/webp",
|
|
3624
|
+
"image/avif",
|
|
3625
|
+
"image/bmp"
|
|
3626
|
+
]);
|
|
3627
|
+
const safeVideoMimeTypes = /* @__PURE__ */ new Set([
|
|
3628
|
+
"video/mp4",
|
|
3629
|
+
"video/x-m4v",
|
|
3630
|
+
"video/quicktime",
|
|
3631
|
+
"video/webm",
|
|
3632
|
+
"video/ogg"
|
|
3633
|
+
]);
|
|
3634
|
+
const safeAudioMimeTypes = /* @__PURE__ */ new Set([
|
|
3635
|
+
"audio/mpeg",
|
|
3636
|
+
"audio/mp4",
|
|
3637
|
+
"audio/wav",
|
|
3638
|
+
"audio/ogg"
|
|
3639
|
+
]);
|
|
3640
|
+
function fileExtension(fileName) {
|
|
3641
|
+
return /\.([A-Za-z0-9]+)$/u.exec(fileName.trim())?.[1]?.toLowerCase();
|
|
3642
|
+
}
|
|
3643
|
+
/** Recover the real media MIME when the platform supplied no useful metadata. */
|
|
3644
|
+
function arkmeNormalizedFileMimeType(mimeType, fileName = "") {
|
|
3645
|
+
const declared = mimeType.trim().toLowerCase();
|
|
3646
|
+
if (declared !== "" && declared !== "application/octet-stream") return declared;
|
|
3647
|
+
const extension = fileExtension(fileName);
|
|
3648
|
+
return (extension === void 0 ? void 0 : mediaTypesByExtension[extension]?.mimeType) ?? (declared || "application/octet-stream");
|
|
3649
|
+
}
|
|
3650
|
+
/** Classify presentation independently from whether this browser can decode it inline. */
|
|
3651
|
+
function arkmeMediaKind(mimeType, fileName = "") {
|
|
3652
|
+
const declared = mimeType.trim().toLowerCase();
|
|
3653
|
+
const extension = fileExtension(fileName);
|
|
3654
|
+
if (extension !== void 0) {
|
|
3655
|
+
const byExtension = mediaTypesByExtension[extension];
|
|
3656
|
+
if (byExtension === void 0) return void 0;
|
|
3657
|
+
if (declared === "" || declared === "application/octet-stream") return byExtension.kind;
|
|
3658
|
+
if (byExtension.kind === "image" && declared.startsWith("image/")) return "image";
|
|
3659
|
+
if (byExtension.kind === "video" && declared.startsWith("video/")) return "video";
|
|
3660
|
+
if (byExtension.kind === "audio" && declared.startsWith("audio/")) return "audio";
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3663
|
+
if (declared.startsWith("image/")) return "image";
|
|
3664
|
+
if (declared.startsWith("video/")) return "video";
|
|
3665
|
+
if (declared.startsWith("audio/")) return "audio";
|
|
3666
|
+
}
|
|
3667
|
+
/** A filename suffix is authoritative when it contradicts legacy visual metadata. */
|
|
3668
|
+
function arkmeBrowserVisualKind(mimeType, fileName = "") {
|
|
3669
|
+
const declared = mimeType.trim().toLowerCase();
|
|
3670
|
+
const extension = fileExtension(fileName);
|
|
3671
|
+
if (extension !== void 0) {
|
|
3672
|
+
const byExtension = safeVisualTypesByExtension[extension];
|
|
3673
|
+
if (byExtension === void 0) return void 0;
|
|
3674
|
+
if (declared === "" || declared === "application/octet-stream") return byExtension.kind;
|
|
3675
|
+
if (byExtension.kind === "image" && safeImageMimeTypes.has(declared)) return "image";
|
|
3676
|
+
if (byExtension.kind === "video" && safeVideoMimeTypes.has(declared)) return "video";
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
if (safeImageMimeTypes.has(declared)) return "image";
|
|
3680
|
+
if (safeVideoMimeTypes.has(declared)) return "video";
|
|
3681
|
+
}
|
|
3682
|
+
function arkmeCanInlineLocalFile(mimeType, fileName = "") {
|
|
3683
|
+
if (arkmeBrowserVisualKind(mimeType, fileName) !== void 0) return true;
|
|
3684
|
+
const declared = mimeType.trim().toLowerCase();
|
|
3685
|
+
const extension = fileExtension(fileName);
|
|
3686
|
+
if (!safeAudioMimeTypes.has(declared)) return false;
|
|
3687
|
+
return extension === void 0 || [
|
|
3688
|
+
"mp3",
|
|
3689
|
+
"m4a",
|
|
3690
|
+
"wav",
|
|
3691
|
+
"ogg",
|
|
3692
|
+
"oga"
|
|
3693
|
+
].includes(extension);
|
|
3694
|
+
}
|
|
3695
|
+
/** Picked audio keeps generic upload metadata; presentation still recognizes it as audio. */
|
|
3696
|
+
function arkmePickedFileKind(mimeType, fileName = "") {
|
|
3697
|
+
const mediaKind = arkmeMediaKind(arkmeNormalizedFileMimeType(mimeType, fileName), fileName);
|
|
3698
|
+
return mediaKind === "image" ? 1 : mediaKind === "video" ? 3 : 4;
|
|
3699
|
+
}
|
|
3700
|
+
//#endregion
|
|
3448
3701
|
//#region src/services/source-service.ts
|
|
3449
3702
|
const SOURCE_LIST_CACHE_TTL_MS = 3e4;
|
|
3450
3703
|
const SOURCE_LIST_CACHE_MAX_ENTRIES = 200;
|
|
@@ -3539,17 +3792,47 @@ function chatMessageDnd$1(value) {
|
|
|
3539
3792
|
const policy = value;
|
|
3540
3793
|
return numberValue$20(policy.mute_state) === 2 || numberValue$20(policy.notify_state) === 2;
|
|
3541
3794
|
}
|
|
3542
|
-
function
|
|
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)));
|
|
@@ -8674,6 +9093,8 @@ var ChatService = class {
|
|
|
8674
9093
|
async sendSourceRich(sourceRef, input, options = {}) {
|
|
8675
9094
|
if (this.runtime.config.richMediaSendEnabled === false) throw new ArkmePluginError("rich-content-disabled", "富内容发送已被插件配置关闭", false, 403);
|
|
8676
9095
|
const session = await this.runtime.requireSession();
|
|
9096
|
+
if (options.expectedUserId !== void 0 && options.expectedUserId !== session.userId) throw new ArkmePluginError("file-account-changed", "账号已切换", false, 403);
|
|
9097
|
+
options.signal?.throwIfAborted();
|
|
8677
9098
|
const source = await this.source.openSourceRef(sourceRef, session.userId);
|
|
8678
9099
|
const title = input.title?.trim() ?? "";
|
|
8679
9100
|
const textContent = input.textContent?.trim() ?? "";
|
|
@@ -8702,7 +9123,10 @@ var ChatService = class {
|
|
|
8702
9123
|
content_file_role: 1,
|
|
8703
9124
|
render_role: 1,
|
|
8704
9125
|
sort_order: index,
|
|
8705
|
-
file_name: asset.fileName
|
|
9126
|
+
file_name: asset.fileName,
|
|
9127
|
+
file_kind: asset.fileKind,
|
|
9128
|
+
mime_type: asset.mimeType,
|
|
9129
|
+
size: asset.size
|
|
8706
9130
|
}))
|
|
8707
9131
|
};
|
|
8708
9132
|
let contentPayload = mediaContentPayload;
|
|
@@ -8710,7 +9134,7 @@ var ChatService = class {
|
|
|
8710
9134
|
if ((input.humanMentions?.length ?? 0) > 0 && source.kind !== "group_chat") throw new ArkmePluginError("mention-group-required", "真人 mention 只能发送到群聊", false);
|
|
8711
9135
|
if ((input.botMentions?.length ?? 0) > 0 && source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("mention-chat-required", "Bot mention 只能发送到聊天", false);
|
|
8712
9136
|
if (longArticle) throw new ArkmePluginError("mention-rich-invalid", "长文暂不支持 mention", false);
|
|
8713
|
-
const mentionPayload = await this.humanMentionContentPayload(source, input.textContent ?? "", textContent, input.humanMentions ?? [], session,
|
|
9137
|
+
const mentionPayload = await this.humanMentionContentPayload(source, input.textContent ?? "", textContent, input.humanMentions ?? [], session, options.signal, input.botMentions ?? []);
|
|
8714
9138
|
contentPayload = {
|
|
8715
9139
|
...mediaContentPayload ?? {},
|
|
8716
9140
|
...mentionPayload
|
|
@@ -8727,7 +9151,7 @@ var ChatService = class {
|
|
|
8727
9151
|
send_at: Date.now()
|
|
8728
9152
|
};
|
|
8729
9153
|
if (source.kind === "send_to_self" || source.kind === "default_category") {
|
|
8730
|
-
const result = await this.runtime.authenticatedPost("/api/v1/records/create", commonBody, session);
|
|
9154
|
+
const result = await this.runtime.authenticatedPost("/api/v1/records/create", commonBody, session, options.signal);
|
|
8731
9155
|
this.source.invalidateSourceListCache(session.userId, "send_to_self");
|
|
8732
9156
|
return {
|
|
8733
9157
|
sourceRef,
|
|
@@ -8740,7 +9164,7 @@ var ChatService = class {
|
|
|
8740
9164
|
const result = await this.runtime.authenticatedPost("/api/v1/topics/records/create", {
|
|
8741
9165
|
topic_uid: source.ownerRef,
|
|
8742
9166
|
...commonBody
|
|
8743
|
-
}, session);
|
|
9167
|
+
}, session, options.signal);
|
|
8744
9168
|
this.source.invalidateSourceListCache(session.userId, "send_to_self");
|
|
8745
9169
|
return {
|
|
8746
9170
|
sourceRef,
|
|
@@ -8753,7 +9177,7 @@ var ChatService = class {
|
|
|
8753
9177
|
chat_session_uid: source.ownerRef,
|
|
8754
9178
|
rel_uid: relationUid,
|
|
8755
9179
|
...commonBody
|
|
8756
|
-
}, session);
|
|
9180
|
+
}, session, options.signal);
|
|
8757
9181
|
const sequence = numberValue$16(result.seq);
|
|
8758
9182
|
this.realtime.scheduleChatSessionProjection(source.ownerRef, sequence);
|
|
8759
9183
|
return {
|
|
@@ -10372,6 +10796,37 @@ function booleanValue$4(value) {
|
|
|
10372
10796
|
function listValue$13(value) {
|
|
10373
10797
|
return Array.isArray(value) ? value : [];
|
|
10374
10798
|
}
|
|
10799
|
+
function aiPolishThreadMessages(value) {
|
|
10800
|
+
let totalCharacters = 0;
|
|
10801
|
+
const messages = [];
|
|
10802
|
+
for (const raw of listValue$13(value).slice(-40)) {
|
|
10803
|
+
const item = objectValue$1(raw);
|
|
10804
|
+
const id = stringValue$1(item.id).trim();
|
|
10805
|
+
const role = stringValue$1(item.role).trim();
|
|
10806
|
+
const text = stringValue$1(item.text).trim().slice(0, 4e3);
|
|
10807
|
+
if (id === "" || role !== "ai" && role !== "user" || text === "") continue;
|
|
10808
|
+
totalCharacters += [...text].length;
|
|
10809
|
+
if (totalCharacters > 2e4) break;
|
|
10810
|
+
const ruleRef = stringValue$1(item.rule_uid ?? item.ruleRef).trim();
|
|
10811
|
+
messages.push({
|
|
10812
|
+
id,
|
|
10813
|
+
role,
|
|
10814
|
+
text,
|
|
10815
|
+
...item.is_rule === true || item.isRule === true ? { isRule: true } : {},
|
|
10816
|
+
...ruleRef === "" ? {} : { ruleRef }
|
|
10817
|
+
});
|
|
10818
|
+
}
|
|
10819
|
+
return messages;
|
|
10820
|
+
}
|
|
10821
|
+
function serializeAiPolishThreadMessage(message) {
|
|
10822
|
+
return {
|
|
10823
|
+
id: message.id,
|
|
10824
|
+
role: message.role,
|
|
10825
|
+
text: message.text,
|
|
10826
|
+
...message.isRule === true ? { is_rule: true } : {},
|
|
10827
|
+
...message.ruleRef === void 0 || message.ruleRef === "" ? {} : { rule_uid: message.ruleRef }
|
|
10828
|
+
};
|
|
10829
|
+
}
|
|
10375
10830
|
function compactAiPolishActorLabel(value) {
|
|
10376
10831
|
const normalized = stringValue$1(value).replace(/\s+/g, " ").trim();
|
|
10377
10832
|
if (normalized === "") return "";
|
|
@@ -10426,7 +10881,11 @@ var GroupAiPolishService = class {
|
|
|
10426
10881
|
const instruction = requirement.trim();
|
|
10427
10882
|
if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
|
|
10428
10883
|
if (instruction === "" || [...instruction].length > 2e3) throw new ArkmePluginError("group-ai-polish-requirement-invalid", "请提供不超过 2000 字的润色要求", false);
|
|
10429
|
-
|
|
10884
|
+
const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal, options.targetRuleRef?.trim() !== "");
|
|
10885
|
+
if (!config.canManage) throw this.permissionDenied();
|
|
10886
|
+
const targetRuleRef = options.targetRuleRef?.trim() ?? "";
|
|
10887
|
+
const targetRule = targetRuleRef === "" ? void 0 : config.rules.find((rule) => rule.ruleUid === targetRuleRef);
|
|
10888
|
+
if (targetRuleRef !== "" && targetRule === void 0) throw new ArkmePluginError("group-ai-polish-rule-not-found", "规则已删除或不可用,请重新读取规则列表", false, 404);
|
|
10430
10889
|
const generated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/rules/generate", {
|
|
10431
10890
|
chat_session_uid: source.ownerRef,
|
|
10432
10891
|
instruction
|
|
@@ -10437,23 +10896,42 @@ var GroupAiPolishService = class {
|
|
|
10437
10896
|
if (ruleName === "" || ruleText === "") throw new ArkmePluginError("group-ai-polish-generate-invalid", "AI 没有生成可用的润色规则,请换一种描述重试", true, 502);
|
|
10438
10897
|
this.cleanupAiPolishState();
|
|
10439
10898
|
const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
|
|
10899
|
+
const suppliedThread = aiPolishThreadMessages(options.threadMessages);
|
|
10900
|
+
const candidateMessage = {
|
|
10901
|
+
id: `arkme-${randomUUID()}`,
|
|
10902
|
+
role: "ai",
|
|
10903
|
+
text: ruleText,
|
|
10904
|
+
isRule: true,
|
|
10905
|
+
...(targetRule?.ruleUid ?? stringValue$1(candidate.candidate_uid).trim()) === "" ? {} : { ruleRef: targetRule?.ruleUid ?? stringValue$1(candidate.candidate_uid).trim() }
|
|
10906
|
+
};
|
|
10907
|
+
const threadMessages = suppliedThread.length === 0 ? [] : [...suppliedThread, candidateMessage];
|
|
10908
|
+
const candidateExtra = objectValue$1(candidate.extra);
|
|
10440
10909
|
this.aiPolishConfirmations.set(confirmationRef, {
|
|
10441
10910
|
userId: session.userId,
|
|
10442
10911
|
chatSessionUid: source.ownerRef,
|
|
10443
10912
|
groupName: source.displayName,
|
|
10444
10913
|
action: "enable",
|
|
10445
10914
|
expiresAtMillis: Date.now() + 10 * 6e4,
|
|
10446
|
-
candidateUid: stringValue$1(candidate.candidate_uid).trim(),
|
|
10915
|
+
candidateUid: targetRule?.ruleUid ?? stringValue$1(candidate.candidate_uid).trim(),
|
|
10916
|
+
...targetRule === void 0 ? {} : {
|
|
10917
|
+
editingRuleUid: targetRule.ruleUid,
|
|
10918
|
+
editingRuleVersion: targetRule.ruleVersion
|
|
10919
|
+
},
|
|
10447
10920
|
ruleName,
|
|
10448
10921
|
ruleText,
|
|
10449
10922
|
promptVersion: stringValue$1(candidate.prompt_version).trim(),
|
|
10450
|
-
extra:
|
|
10923
|
+
extra: threadMessages.length === 0 ? candidateExtra : {
|
|
10924
|
+
...candidateExtra,
|
|
10925
|
+
rule_thread_messages: threadMessages.map(serializeAiPolishThreadMessage),
|
|
10926
|
+
active_rule_message_id: candidateMessage.id
|
|
10927
|
+
}
|
|
10451
10928
|
});
|
|
10452
10929
|
return {
|
|
10453
10930
|
groupName: source.displayName,
|
|
10454
10931
|
ruleName,
|
|
10455
10932
|
ruleText,
|
|
10456
|
-
confirmationRef
|
|
10933
|
+
confirmationRef,
|
|
10934
|
+
...threadMessages.length === 0 ? {} : { threadMessages }
|
|
10457
10935
|
};
|
|
10458
10936
|
}
|
|
10459
10937
|
async generateGroupAiPolishRule(groupName, requirement, options = {}) {
|
|
@@ -10463,42 +10941,134 @@ var GroupAiPolishService = class {
|
|
|
10463
10941
|
async confirmEnableGroupAiPolish(confirmationRef, options = {}) {
|
|
10464
10942
|
const session = await this.runtime.requireSession();
|
|
10465
10943
|
const pending = this.requireAiPolishConfirmation(confirmationRef, session.userId, "enable");
|
|
10466
|
-
if (
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10944
|
+
if (pending.confirming) throw new ArkmePluginError("group-ai-polish-confirm-busy", "此操作正在确认,请稍后重试", true, 409);
|
|
10945
|
+
pending.confirming = true;
|
|
10946
|
+
try {
|
|
10947
|
+
const current = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal, true);
|
|
10948
|
+
if (!current.canManage) throw this.permissionDenied();
|
|
10949
|
+
const updateAt = Date.now();
|
|
10950
|
+
let ruleUid = pending.savedRuleUid ?? "";
|
|
10951
|
+
let ruleChanged = false;
|
|
10952
|
+
if (ruleUid !== "") {
|
|
10953
|
+
const saved = current.rules.find((rule) => rule.ruleUid === ruleUid);
|
|
10954
|
+
if (saved === void 0 || saved.name !== pending.ruleName || saved.ruleText !== pending.ruleText) throw new ArkmePluginError("group-ai-polish-preview-stale", "规则已被修改或删除,请重新预览并确认", false, 409);
|
|
10955
|
+
} else {
|
|
10956
|
+
if (pending.editingRuleUid !== void 0) {
|
|
10957
|
+
const currentRule = current.rules.find((rule) => rule.ruleUid === pending.editingRuleUid);
|
|
10958
|
+
if (currentRule === void 0 || currentRule.ruleVersion !== pending.editingRuleVersion) throw new ArkmePluginError("group-ai-polish-preview-stale", "规则已被其他成员修改或删除,请重新读取后再编辑", false, 409);
|
|
10959
|
+
}
|
|
10960
|
+
const upserted = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/rules/upsert", {
|
|
10961
|
+
chat_session_uid: pending.chatSessionUid,
|
|
10962
|
+
...pending.candidateUid === void 0 || pending.candidateUid === "" ? {} : { rule_uid: pending.candidateUid },
|
|
10963
|
+
name: pending.ruleName,
|
|
10964
|
+
rule_text: pending.ruleText,
|
|
10965
|
+
...pending.promptVersion === void 0 || pending.promptVersion === "" ? {} : { prompt_version: pending.promptVersion },
|
|
10966
|
+
update_at: updateAt,
|
|
10967
|
+
...pending.extra === void 0 || Object.keys(pending.extra).length === 0 ? {} : { extra: pending.extra }
|
|
10968
|
+
}, session, options.signal);
|
|
10969
|
+
ruleUid = stringValue$1(objectValue$1(upserted.rule ?? upserted).rule_uid).trim();
|
|
10970
|
+
if (ruleUid === "") throw new ArkmePluginError("group-ai-polish-rule-invalid", "保存润色规则后未返回有效规则", true, 502);
|
|
10971
|
+
ruleChanged = true;
|
|
10972
|
+
pending.savedRuleUid = ruleUid;
|
|
10973
|
+
this.invalidateAiPolishReadCache(session.userId, pending.chatSessionUid);
|
|
10974
|
+
}
|
|
10975
|
+
try {
|
|
10976
|
+
if (!current.enabled || current.activeRuleUid !== ruleUid) await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/update", {
|
|
10977
|
+
chat_session_uid: pending.chatSessionUid,
|
|
10978
|
+
enabled: true,
|
|
10979
|
+
active_rule_uid: ruleUid,
|
|
10980
|
+
update_at: Math.max(Date.now(), updateAt + 1)
|
|
10981
|
+
}, session, options.signal);
|
|
10982
|
+
this.invalidateAiPolishReadCache(session.userId, pending.chatSessionUid);
|
|
10983
|
+
const verified = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal, true);
|
|
10984
|
+
const verifiedRule = verified.rules.find((rule) => rule.ruleUid === ruleUid);
|
|
10985
|
+
if (!verified.enabled || verified.activeRuleUid !== ruleUid || verifiedRule === void 0 || verifiedRule.name !== pending.ruleName || verifiedRule.ruleText !== pending.ruleText) throw new Error("AI polish state did not match the approved preview");
|
|
10986
|
+
} catch {
|
|
10987
|
+
this.invalidateAiPolishReadCache(session.userId, pending.chatSessionUid);
|
|
10988
|
+
throw new ArkmePluginError("group-ai-polish-enable-unverified", "规则已保存,但未能确认开启生效;请查询当前状态后重试,不需要重新生成规则", true, 502);
|
|
10989
|
+
}
|
|
10990
|
+
this.aiPolishConfirmations.delete(confirmationRef.trim());
|
|
10991
|
+
return {
|
|
10992
|
+
groupName: pending.groupName,
|
|
10993
|
+
enabled: true,
|
|
10994
|
+
ruleName: pending.ruleName ?? "",
|
|
10995
|
+
changed: ruleChanged || !current.enabled || current.activeRuleUid !== ruleUid
|
|
10996
|
+
};
|
|
10997
|
+
} finally {
|
|
10998
|
+
pending.confirming = false;
|
|
10999
|
+
}
|
|
11000
|
+
}
|
|
11001
|
+
permissionDenied() {
|
|
11002
|
+
return new ArkmePluginError("group-ai-polish-forbidden", "服务端未授予当前账号该群的 AI 润色设置权限;请确认账号仍在群内,以及服务端已支持全体群成员设置", false, 403);
|
|
11003
|
+
}
|
|
11004
|
+
async prepareEnableGroupAiPolishForSource(sourceRef, ruleName = "", options = {}) {
|
|
11005
|
+
const session = await this.runtime.requireSession();
|
|
11006
|
+
const source = await this.source.openSourceRef(sourceRef, session.userId);
|
|
11007
|
+
if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
|
|
11008
|
+
const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal, true);
|
|
11009
|
+
if (!config.canManage) throw this.permissionDenied();
|
|
11010
|
+
const name = ruleName.trim();
|
|
11011
|
+
const matches = name !== "" ? config.rules.filter((rule) => rule.name === name) : config.activeRuleUid !== "" ? config.rules.filter((rule) => rule.ruleUid === config.activeRuleUid) : config.rules;
|
|
11012
|
+
if (matches.length === 0) throw new ArkmePluginError("group-ai-polish-rule-not-found", "没有找到可开启的规则,请先查询规则或提供新的润色要求", false, 404);
|
|
11013
|
+
if (matches.length !== 1) throw new ArkmePluginError("group-ai-polish-rule-ambiguous", "存在多个候选规则,请查询并指定唯一的规则名称", false, 409);
|
|
11014
|
+
const rule = matches[0];
|
|
11015
|
+
this.cleanupAiPolishState();
|
|
11016
|
+
const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
|
|
11017
|
+
this.aiPolishConfirmations.set(confirmationRef, {
|
|
11018
|
+
userId: session.userId,
|
|
11019
|
+
chatSessionUid: source.ownerRef,
|
|
11020
|
+
groupName: source.displayName,
|
|
11021
|
+
action: "enable",
|
|
11022
|
+
expiresAtMillis: Date.now() + 10 * 6e4,
|
|
11023
|
+
savedRuleUid: rule.ruleUid,
|
|
11024
|
+
ruleName: rule.name,
|
|
11025
|
+
ruleText: rule.ruleText
|
|
11026
|
+
});
|
|
10489
11027
|
return {
|
|
10490
|
-
groupName:
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
11028
|
+
groupName: source.displayName,
|
|
11029
|
+
ruleName: rule.name,
|
|
11030
|
+
ruleText: rule.ruleText,
|
|
11031
|
+
confirmationRef
|
|
10494
11032
|
};
|
|
10495
11033
|
}
|
|
11034
|
+
async prepareEnableGroupAiPolishRuleForSource(sourceRef, ruleRef, options = {}) {
|
|
11035
|
+
const session = await this.runtime.requireSession();
|
|
11036
|
+
const source = await this.source.openSourceRef(sourceRef, session.userId);
|
|
11037
|
+
if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
|
|
11038
|
+
const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal, true);
|
|
11039
|
+
if (!config.canManage) throw this.permissionDenied();
|
|
11040
|
+
const normalizedRuleRef = ruleRef.trim();
|
|
11041
|
+
const rule = config.rules.find((candidate) => candidate.ruleUid === normalizedRuleRef);
|
|
11042
|
+
if (normalizedRuleRef === "" || rule === void 0) throw new ArkmePluginError("group-ai-polish-rule-not-found", "规则已删除或不可用,请重新读取规则列表", false, 404);
|
|
11043
|
+
this.cleanupAiPolishState();
|
|
11044
|
+
const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
|
|
11045
|
+
this.aiPolishConfirmations.set(confirmationRef, {
|
|
11046
|
+
userId: session.userId,
|
|
11047
|
+
chatSessionUid: source.ownerRef,
|
|
11048
|
+
groupName: source.displayName,
|
|
11049
|
+
action: "enable",
|
|
11050
|
+
expiresAtMillis: Date.now() + 10 * 6e4,
|
|
11051
|
+
savedRuleUid: rule.ruleUid,
|
|
11052
|
+
ruleName: rule.name,
|
|
11053
|
+
ruleText: rule.ruleText
|
|
11054
|
+
});
|
|
11055
|
+
return {
|
|
11056
|
+
groupName: source.displayName,
|
|
11057
|
+
ruleName: rule.name,
|
|
11058
|
+
ruleText: rule.ruleText,
|
|
11059
|
+
confirmationRef
|
|
11060
|
+
};
|
|
11061
|
+
}
|
|
11062
|
+
async prepareEnableGroupAiPolish(groupName, ruleName = "", options = {}) {
|
|
11063
|
+
const source = await this.resolveUniqueGroupByName(groupName, options.signal);
|
|
11064
|
+
return await this.prepareEnableGroupAiPolishForSource(source.sourceRef, ruleName, options);
|
|
11065
|
+
}
|
|
10496
11066
|
async prepareDisableGroupAiPolishForSource(sourceRef, options = {}) {
|
|
10497
11067
|
const session = await this.runtime.requireSession();
|
|
10498
11068
|
const source = await this.source.openSourceRef(sourceRef, session.userId);
|
|
10499
11069
|
if (source.kind !== "group_chat") throw new ArkmePluginError("group-ai-polish-source-invalid", "AI 表达润色仅支持群聊", false);
|
|
10500
11070
|
const config = await this.queryGroupAiPolishConfig(source.ownerRef, session, options.signal);
|
|
10501
|
-
if (!config.canManage) throw
|
|
11071
|
+
if (!config.canManage) throw this.permissionDenied();
|
|
10502
11072
|
this.cleanupAiPolishState();
|
|
10503
11073
|
const confirmationRef = `arkme-ai-polish-confirm-v1.${randomUUID()}`;
|
|
10504
11074
|
this.aiPolishConfirmations.set(confirmationRef, {
|
|
@@ -10523,8 +11093,8 @@ var GroupAiPolishService = class {
|
|
|
10523
11093
|
async confirmDisableGroupAiPolish(confirmationRef, options = {}) {
|
|
10524
11094
|
const session = await this.runtime.requireSession();
|
|
10525
11095
|
const pending = this.requireAiPolishConfirmation(confirmationRef, session.userId, "disable");
|
|
10526
|
-
const current = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal);
|
|
10527
|
-
if (!current.canManage) throw
|
|
11096
|
+
const current = await this.queryGroupAiPolishConfig(pending.chatSessionUid, session, options.signal, true);
|
|
11097
|
+
if (!current.canManage) throw this.permissionDenied();
|
|
10528
11098
|
const updated = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/update", {
|
|
10529
11099
|
chat_session_uid: pending.chatSessionUid,
|
|
10530
11100
|
enabled: false,
|
|
@@ -10685,8 +11255,8 @@ var GroupAiPolishService = class {
|
|
|
10685
11255
|
polishedText
|
|
10686
11256
|
};
|
|
10687
11257
|
}
|
|
10688
|
-
async queryGroupAiPolishConfig(chatSessionUid, session, signal) {
|
|
10689
|
-
const data = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/query", { chat_session_uid: chatSessionUid }, session, signal, {
|
|
11258
|
+
async queryGroupAiPolishConfig(chatSessionUid, session, signal, fresh = false) {
|
|
11259
|
+
const data = await this.runtime.authenticatedChatPost("/api/v1/chats/ai-polish/settings/query", { chat_session_uid: chatSessionUid }, session, signal, fresh ? { lane: "interactive-read" } : {
|
|
10690
11260
|
lane: "background-read",
|
|
10691
11261
|
key: `ai-polish:settings:${chatSessionUid}`,
|
|
10692
11262
|
cacheMs: 15e3,
|
|
@@ -10694,12 +11264,16 @@ var GroupAiPolishService = class {
|
|
|
10694
11264
|
});
|
|
10695
11265
|
const config = objectValue$1(data.config ?? data.setting ?? data.settings ?? data);
|
|
10696
11266
|
const activeRuleUid = stringValue$1(config.active_rule_uid).trim();
|
|
10697
|
-
const rules = listValue$13(data.rules).map((raw) => objectValue$1(raw)).map((rule) =>
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
11267
|
+
const rules = listValue$13(data.rules).map((raw) => objectValue$1(raw)).map((rule) => {
|
|
11268
|
+
const extra = objectValue$1(rule.extra);
|
|
11269
|
+
return {
|
|
11270
|
+
ruleUid: stringValue$1(rule.rule_uid).trim(),
|
|
11271
|
+
name: stringValue$1(rule.name).trim() || "未命名规则",
|
|
11272
|
+
ruleText: stringValue$1(rule.rule_text).trim(),
|
|
11273
|
+
ruleVersion: numberValue$12(rule.rule_version),
|
|
11274
|
+
threadMessages: aiPolishThreadMessages(extra.rule_thread_messages)
|
|
11275
|
+
};
|
|
11276
|
+
}).filter((rule) => rule.ruleUid !== "" && rule.ruleText !== "");
|
|
10703
11277
|
return {
|
|
10704
11278
|
enabled: booleanValue$4(config.enabled ?? config.is_enabled),
|
|
10705
11279
|
canManage: booleanValue$4(data.can_manage),
|
|
@@ -10722,7 +11296,8 @@ var GroupAiPolishService = class {
|
|
|
10722
11296
|
ruleRef: rule.ruleUid,
|
|
10723
11297
|
name: rule.name,
|
|
10724
11298
|
ruleText: rule.ruleText,
|
|
10725
|
-
isActive: rule.ruleUid === config.activeRuleUid
|
|
11299
|
+
isActive: rule.ruleUid === config.activeRuleUid,
|
|
11300
|
+
...rule.threadMessages.length === 0 ? {} : { threadMessages: rule.threadMessages }
|
|
10726
11301
|
})),
|
|
10727
11302
|
updatedAtMillis: config.updatedAtMillis
|
|
10728
11303
|
};
|
|
@@ -10802,7 +11377,7 @@ var GroupAiPolishService = class {
|
|
|
10802
11377
|
};
|
|
10803
11378
|
//#endregion
|
|
10804
11379
|
//#region src/services/group-service.ts
|
|
10805
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
11380
|
+
const UUID$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
10806
11381
|
function numberValue$11(value) {
|
|
10807
11382
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
10808
11383
|
}
|
|
@@ -11138,7 +11713,7 @@ var GroupService = class {
|
|
|
11138
11713
|
async createGroup(title, clientMutationId, options = {}) {
|
|
11139
11714
|
const normalizedTitle = title.trim();
|
|
11140
11715
|
if (normalizedTitle === "" || Array.from(normalizedTitle).length > 80) throw new ArkmePluginError("group-title-invalid", "群聊名称需为 1-80 个字符", false);
|
|
11141
|
-
if (!UUID.test(clientMutationId)) throw new ArkmePluginError("client-mutation-id-invalid", "群聊操作标识无效", false);
|
|
11716
|
+
if (!UUID$1.test(clientMutationId)) throw new ArkmePluginError("client-mutation-id-invalid", "群聊操作标识无效", false);
|
|
11142
11717
|
const session = await this.runtime.requireSession();
|
|
11143
11718
|
let data;
|
|
11144
11719
|
try {
|
|
@@ -11794,11 +12369,19 @@ var MediaService = class {
|
|
|
11794
12369
|
};
|
|
11795
12370
|
}).filter((item) => item.fileAssetUid !== "");
|
|
11796
12371
|
}
|
|
11797
|
-
async uploadLocalFile(filePath, metadata) {
|
|
12372
|
+
async uploadLocalFile(filePath, metadata, options = {}) {
|
|
11798
12373
|
if (this.runtime.config.richMediaSendEnabled === false) throw new ArkmePluginError("rich-content-disabled", "文件上传已被插件配置关闭", false, 403);
|
|
11799
12374
|
const maxBytes = this.runtime.config.maxUploadBytes ?? 100 * 1024 * 1024;
|
|
11800
12375
|
if (!Number.isSafeInteger(metadata.size) || metadata.size <= 0 || metadata.size > maxBytes || !/^[a-f0-9]{64}$/.test(metadata.sha256) || metadata.fileName.trim() === "") throw new ArkmePluginError("upload-metadata-invalid", "文件为空、过大或元数据无效", false, 400);
|
|
11801
12376
|
const session = await this.runtime.requireSession();
|
|
12377
|
+
if (options.expectedUserId !== void 0 && session.userId !== options.expectedUserId) throw new ArkmePluginError("file-account-changed", "账号已切换", false, 403);
|
|
12378
|
+
options.signal?.throwIfAborted();
|
|
12379
|
+
const progress = (phase, sentBytes) => options.onProgress?.({
|
|
12380
|
+
phase,
|
|
12381
|
+
sentBytes,
|
|
12382
|
+
totalBytes: metadata.size
|
|
12383
|
+
});
|
|
12384
|
+
progress("preparing", 0);
|
|
11802
12385
|
const uploadMode = metadata.size > 16 * 1024 * 1024 ? 2 : 1;
|
|
11803
12386
|
const prepared = await this.runtime.authenticatedPost("/api/v1/files/prepare-upload", {
|
|
11804
12387
|
planned_size: metadata.size,
|
|
@@ -11807,7 +12390,7 @@ var MediaService = class {
|
|
|
11807
12390
|
file_kind: metadata.fileKind,
|
|
11808
12391
|
upload_mode: uploadMode,
|
|
11809
12392
|
display_name: metadata.fileName
|
|
11810
|
-
}, session);
|
|
12393
|
+
}, session, options.signal);
|
|
11811
12394
|
const uploadSessionUid = stringValue$1(prepared.upload_session_uid).trim();
|
|
11812
12395
|
if (uploadSessionUid === "") throw new ArkmePluginError("upload-prepare-invalid", "上传准备响应无效", true, 502);
|
|
11813
12396
|
try {
|
|
@@ -11816,12 +12399,23 @@ var MediaService = class {
|
|
|
11816
12399
|
if (uploadMode === 1) {
|
|
11817
12400
|
const uploadUrl = stringValue$1(prepared.upload_url).trim();
|
|
11818
12401
|
if (uploadUrl === "") throw new ArkmePluginError("upload-url-missing", "对象存储上传地址缺失", true, 502);
|
|
12402
|
+
const signal = options.signal;
|
|
12403
|
+
const body = options.onProgress === void 0 ? createReadStream(filePath) : Readable.from((async function* () {
|
|
12404
|
+
let sent = 0;
|
|
12405
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
12406
|
+
signal?.throwIfAborted();
|
|
12407
|
+
sent += chunk.length;
|
|
12408
|
+
progress("uploading", sent);
|
|
12409
|
+
yield chunk;
|
|
12410
|
+
}
|
|
12411
|
+
})());
|
|
11819
12412
|
const response = await this.runtime.fetchImpl(uploadUrl, {
|
|
11820
12413
|
method: "PUT",
|
|
11821
12414
|
headers: Object.fromEntries(Object.entries(objectValue$1(prepared.upload_headers)).map(([key, value]) => [key, stringValue$1(value)])),
|
|
11822
|
-
body
|
|
12415
|
+
body,
|
|
11823
12416
|
duplex: "half",
|
|
11824
|
-
redirect: "error"
|
|
12417
|
+
redirect: "error",
|
|
12418
|
+
...signal === void 0 ? {} : { signal }
|
|
11825
12419
|
});
|
|
11826
12420
|
if (!response.ok) throw new ArkmePluginError("upload-storage-failed", `对象存储上传失败(${String(response.status)})`, true, 502);
|
|
11827
12421
|
storageETag = response.headers.get("etag") ?? "";
|
|
@@ -11832,6 +12426,7 @@ var MediaService = class {
|
|
|
11832
12426
|
const handle = await open(filePath, "r");
|
|
11833
12427
|
try {
|
|
11834
12428
|
for (const part of parts) {
|
|
12429
|
+
options.signal?.throwIfAborted();
|
|
11835
12430
|
const partNumber = Math.trunc(numberValue$9(part.part_number));
|
|
11836
12431
|
const uploadUrl = stringValue$1(part.upload_url).trim();
|
|
11837
12432
|
const offset = (partNumber - 1) * partSize;
|
|
@@ -11843,26 +12438,30 @@ var MediaService = class {
|
|
|
11843
12438
|
method: "PUT",
|
|
11844
12439
|
headers: Object.fromEntries(Object.entries(objectValue$1(part.upload_headers)).map(([key, value]) => [key, stringValue$1(value)])),
|
|
11845
12440
|
body: buffer,
|
|
11846
|
-
redirect: "error"
|
|
12441
|
+
redirect: "error",
|
|
12442
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
11847
12443
|
});
|
|
11848
12444
|
if (!response.ok) throw new ArkmePluginError("upload-storage-failed", `对象存储分片上传失败(${String(response.status)})`, true, 502);
|
|
11849
12445
|
completedParts.push({
|
|
11850
12446
|
part_number: partNumber,
|
|
11851
12447
|
etag: response.headers.get("etag") ?? ""
|
|
11852
12448
|
});
|
|
12449
|
+
progress("uploading", Math.min(metadata.size, offset + length));
|
|
11853
12450
|
}
|
|
11854
12451
|
} finally {
|
|
11855
12452
|
await handle.close();
|
|
11856
12453
|
}
|
|
11857
12454
|
}
|
|
12455
|
+
progress("completing", metadata.size);
|
|
11858
12456
|
const completed = await this.runtime.authenticatedPost("/api/v1/files/complete-upload", {
|
|
11859
12457
|
upload_session_uid: uploadSessionUid,
|
|
11860
12458
|
uploaded_size: metadata.size,
|
|
11861
12459
|
storage_etag: storageETag,
|
|
11862
12460
|
multipart_parts: completedParts
|
|
11863
|
-
}, session);
|
|
12461
|
+
}, session, options.signal);
|
|
11864
12462
|
const fileAssetUid = stringValue$1(completed.file_asset_uid).trim();
|
|
11865
12463
|
if (fileAssetUid === "") throw new ArkmePluginError("upload-complete-invalid", "上传完成响应无效", true, 502);
|
|
12464
|
+
progress("ready", metadata.size);
|
|
11866
12465
|
return {
|
|
11867
12466
|
fileAssetUid,
|
|
11868
12467
|
fileName: metadata.fileName,
|
|
@@ -11875,7 +12474,7 @@ var MediaService = class {
|
|
|
11875
12474
|
throw error;
|
|
11876
12475
|
}
|
|
11877
12476
|
}
|
|
11878
|
-
async fetchMedia(mediaRef, range, signal) {
|
|
12477
|
+
async fetchMedia(mediaRef, range, signal, originalOnly = false) {
|
|
11879
12478
|
const session = await this.runtime.requireSession();
|
|
11880
12479
|
const descriptor = this.mediaRefs.get(mediaRef);
|
|
11881
12480
|
if (descriptor === void 0 || descriptor.viewerUserId !== session.userId || descriptor.expiresAtMillis <= Date.now()) {
|
|
@@ -11883,6 +12482,7 @@ var MediaService = class {
|
|
|
11883
12482
|
if (descriptor?.stableKey !== void 0) this.stableMediaRefs.delete(descriptor.stableKey);
|
|
11884
12483
|
throw new ArkmePluginError("media-ref-invalid", "媒体引用已失效,请刷新对话后重试", false, 404);
|
|
11885
12484
|
}
|
|
12485
|
+
if (originalOnly && descriptor.purpose !== "original") throw new ArkmePluginError("original-unavailable", "原文件不可用,请刷新后重试;不会用预览图代替原文件", false, 404);
|
|
11886
12486
|
const url = new URL(descriptor.remoteUrl);
|
|
11887
12487
|
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || !(allowedSignedImageHost$2(this.runtime.config.environment, url.hostname) || allowedSignedAudioHost(this.runtime.config.environment, url.hostname))) throw new ArkmePluginError("media-host-rejected", "媒体来源不受信任", false, 403);
|
|
11888
12488
|
const response = await this.runtime.fetchImpl(url, {
|
|
@@ -12044,6 +12644,15 @@ var MediaService = class {
|
|
|
12044
12644
|
if ((snapshot.profile?.avatarRef.trim() ?? "") === "") try {
|
|
12045
12645
|
snapshot = await this.profile.refreshProfileForSession(session);
|
|
12046
12646
|
} catch {}
|
|
12647
|
+
const ownAvatarUrl = snapshot.profile?.avatarUrl;
|
|
12648
|
+
if (ownAvatarUrl !== void 0 && ownAvatarUrl !== "") return await this.downloadSignedImage(trustedSignedImageUrl$1(this.runtime.config.environment, ownAvatarUrl), byteLimit, signal, this.runtime.requestScope(session.userId));
|
|
12649
|
+
const ownAvatarAssetRef = snapshot.profile?.avatarAssetRef?.trim() ?? "";
|
|
12650
|
+
const assetMatch = /^file_asset:\/\/([A-Za-z0-9_-]{8,128})$/.exec(ownAvatarAssetRef);
|
|
12651
|
+
if (assetMatch !== null) {
|
|
12652
|
+
const asset = (await this.queryFileAssets([assetMatch[1]], signal)).find((item) => item.fileAssetUid === assetMatch[1]);
|
|
12653
|
+
const assetUrl = asset?.previewUrl ?? asset?.downloadUrl;
|
|
12654
|
+
if (assetUrl !== void 0) return await this.downloadSignedImage(trustedSignedImageUrl$1(this.runtime.config.environment, assetUrl), byteLimit, signal, this.runtime.requestScope(session.userId));
|
|
12655
|
+
}
|
|
12047
12656
|
const ownAvatarRef = snapshot.profile?.avatarRef.trim() ?? "";
|
|
12048
12657
|
if (ownAvatarRef !== "" && !ownAvatarRef.startsWith("arkme-profile-image-v1.")) return await this.readImage(ownAvatarRef, {
|
|
12049
12658
|
maxBytes: byteLimit,
|
|
@@ -12109,10 +12718,14 @@ var MediaService = class {
|
|
|
12109
12718
|
if (signedUrl.protocol !== "https:" || signedUrl.username !== "" || signedUrl.password !== "" || !allowedSignedImageHost$2(this.runtime.config.environment, signedUrl.hostname) || signedPath !== objectPath) throw new ArkmePluginError("image-sign-target-rejected", "Arkme 图片授权目标不受信任", false, 502);
|
|
12110
12719
|
return await this.downloadSignedImage(signedUrl, byteLimit, signal, this.runtime.requestScope(session.userId));
|
|
12111
12720
|
}
|
|
12112
|
-
mediaKind(fileKind, mimeType) {
|
|
12113
|
-
|
|
12114
|
-
if (
|
|
12115
|
-
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
|
+
}
|
|
12116
12729
|
return "file";
|
|
12117
12730
|
}
|
|
12118
12731
|
recordContentPayload(raw) {
|
|
@@ -12229,6 +12842,29 @@ var MediaService = class {
|
|
|
12229
12842
|
}
|
|
12230
12843
|
return mediaRefs;
|
|
12231
12844
|
}
|
|
12845
|
+
async issueSearchFileMediaRefs(requests, signal) {
|
|
12846
|
+
const normalized = [...new Map(requests.filter((item) => item.recordUid !== "" && item.fileAssetUid !== "").map((item) => [`${item.recordUid}\0${item.fileAssetUid}`, item])).values()].slice(0, 50);
|
|
12847
|
+
const refs = /* @__PURE__ */ new Map();
|
|
12848
|
+
if (normalized.length === 0 || this.runtime.config.richMediaRenderEnabled === false) return refs;
|
|
12849
|
+
const session = await this.runtime.requireSession();
|
|
12850
|
+
const display = await this.queryRecordMediaDisplayItems(normalized.map((item) => item.recordUid), session, signal);
|
|
12851
|
+
for (const request of normalized) {
|
|
12852
|
+
const item = objectValue$1((display.get(request.recordUid) ?? []).find((raw) => stringValue$1(objectValue$1(raw).file_asset_uid) === request.fileAssetUid));
|
|
12853
|
+
const remoteUrl = safeHttpsUrl(item.download_url);
|
|
12854
|
+
if (remoteUrl === void 0) continue;
|
|
12855
|
+
const host = new URL(remoteUrl).hostname;
|
|
12856
|
+
if (!allowedSignedImageHost$2(this.runtime.config.environment, host) && !allowedSignedAudioHost(this.runtime.config.environment, host)) continue;
|
|
12857
|
+
const key = `${request.recordUid}\0${request.fileAssetUid}`;
|
|
12858
|
+
refs.set(key, this.issueMediaRef(session.userId, {
|
|
12859
|
+
remoteUrl,
|
|
12860
|
+
purpose: "original",
|
|
12861
|
+
mimeType: stringValue$1(item.mime_type) || "application/octet-stream",
|
|
12862
|
+
fileName: stringValue$1(item.file_name) || "文件",
|
|
12863
|
+
size: Math.max(0, numberValue$9(item.size))
|
|
12864
|
+
}, `search-file\0${key}`));
|
|
12865
|
+
}
|
|
12866
|
+
return refs;
|
|
12867
|
+
}
|
|
12232
12868
|
async hydrateRecordMediaPage(rawItems, session, signal) {
|
|
12233
12869
|
const displayItemsByRecordUid = /* @__PURE__ */ new Map();
|
|
12234
12870
|
const expectedRecordUids = [...new Set(rawItems.flatMap((raw) => {
|
|
@@ -12355,14 +12991,23 @@ var MediaService = class {
|
|
|
12355
12991
|
return Math.trunc(numberValue$9(item.content_file_role)) !== RECORD_CONTENT_FILE_ROLE_BACKGROUND_SOUND;
|
|
12356
12992
|
}).flatMap((item, index) => {
|
|
12357
12993
|
const fileAssetUid = stringValue$1(item.file_asset_uid).trim();
|
|
12358
|
-
const mimeType = stringValue$1(item.mime_type).trim() || "application/octet-stream";
|
|
12359
12994
|
const fileName = stringValue$1(item.file_name).trim() || `附件-${String(index + 1)}`;
|
|
12360
12995
|
const fileKind = Math.trunc(numberValue$9(item.file_kind));
|
|
12361
|
-
const
|
|
12996
|
+
const mimeType = arkmeNormalizedFileMimeType(stringValue$1(item.mime_type).trim().toLowerCase(), fileName);
|
|
12997
|
+
const kind = this.mediaKind(fileKind, mimeType, fileName);
|
|
12362
12998
|
const remoteUrl = stringValue$1(kind === "image" ? item.preview_url ?? item.download_url : item.download_url).trim();
|
|
12363
12999
|
if (remoteUrl === "") return [];
|
|
13000
|
+
const originalUrl = stringValue$1(item.download_url).trim();
|
|
13001
|
+
const originalRef = originalUrl === "" ? void 0 : this.issueMediaRef(viewerUserId, {
|
|
13002
|
+
remoteUrl: originalUrl,
|
|
13003
|
+
purpose: "original",
|
|
13004
|
+
mimeType,
|
|
13005
|
+
fileName,
|
|
13006
|
+
size: Math.max(0, Math.trunc(numberValue$9(item.size)))
|
|
13007
|
+
}, fileAssetUid === "" ? void 0 : `original\0${fileAssetUid}`);
|
|
12364
13008
|
return [{
|
|
12365
13009
|
kind,
|
|
13010
|
+
...originalRef === void 0 ? {} : { originalRef },
|
|
12366
13011
|
mediaRef: this.issueMediaRef(viewerUserId, {
|
|
12367
13012
|
remoteUrl,
|
|
12368
13013
|
mimeType,
|
|
@@ -12765,6 +13410,10 @@ function phoneDefaultAvatarFallback(raw) {
|
|
|
12765
13410
|
label
|
|
12766
13411
|
};
|
|
12767
13412
|
}
|
|
13413
|
+
function fileAssetAvatarRef(raw) {
|
|
13414
|
+
const normalized = raw.trim();
|
|
13415
|
+
return /^file_asset:\/\/[A-Za-z0-9_-]{8,128}$/.test(normalized) ? normalized : void 0;
|
|
13416
|
+
}
|
|
12768
13417
|
function allowedSignedImageHost$1(environment, hostname) {
|
|
12769
13418
|
return (environment === "prod" ? ["jotmo-userfiles.oss-cn-hangzhou.aliyuncs.com", "userfiles.jotmo.cc"] : ["jotmo-userfiles-test.oss-cn-hangzhou.aliyuncs.com", "jotmo-userfiles.senguo.me"]).includes(hostname.toLowerCase());
|
|
12770
13419
|
}
|
|
@@ -12816,7 +13465,9 @@ var ProfileService = class {
|
|
|
12816
13465
|
}
|
|
12817
13466
|
async cachedProfile() {
|
|
12818
13467
|
const session = await this.runtime.requireAuthFlowSession();
|
|
12819
|
-
|
|
13468
|
+
const cached = await this.runtime.stateStore.cachedProfile(session.userId);
|
|
13469
|
+
if (this.profileNeedsAvatarRefresh(cached, session.userId)) return await this.refreshProfileForSession(session);
|
|
13470
|
+
return cached;
|
|
12820
13471
|
}
|
|
12821
13472
|
async refreshProfile() {
|
|
12822
13473
|
const session = await this.runtime.requireAuthFlowSession();
|
|
@@ -12895,6 +13546,7 @@ var ProfileService = class {
|
|
|
12895
13546
|
if (memory !== void 0 && memory.expiresAtMillis > Date.now()) return memory.value;
|
|
12896
13547
|
const persisted = await this.runtime.stateStore.cachedProfile(session.userId).catch(() => void 0);
|
|
12897
13548
|
if (persisted?.profile?.userId === session.userId && persisted.cachedAtMillis > 0 && Date.now() - persisted.cachedAtMillis < PROFILE_CACHE_TTL_MS) {
|
|
13549
|
+
if (this.profileNeedsAvatarRefresh(persisted, session.userId)) return await this.refreshProfileForSession(session);
|
|
12898
13550
|
this.profileCache.set(session.userId, {
|
|
12899
13551
|
value: persisted,
|
|
12900
13552
|
expiresAtMillis: persisted.cachedAtMillis + PROFILE_CACHE_TTL_MS
|
|
@@ -12916,16 +13568,21 @@ var ProfileService = class {
|
|
|
12916
13568
|
if (userId <= 0 || userId !== session.userId) throw new ArkmePluginError("profile-contract-invalid", "Arkme 个人资料响应缺少有效用户标识", false, 502);
|
|
12917
13569
|
const nickname = stringValue$1(data.nick_name).trim();
|
|
12918
13570
|
const displayName = nickname || stringValue$1(data.apple_nick_name).trim() || stringValue$1(data.wechat_nick_name).trim() || stringValue$1(data.google_given_name).trim() || stringValue$1(data.name_slug).trim() || "Arkme用户";
|
|
12919
|
-
const
|
|
13571
|
+
const rawAvatarRef = stringValue$1(data.head_img).trim();
|
|
13572
|
+
const avatarAssetRef = fileAssetAvatarRef(rawAvatarRef);
|
|
13573
|
+
const avatarUrl = await this.publicProfilesByUserIds([session.userId], session).then((profiles) => profiles.get(session.userId)?.avatarUrl).catch(() => void 0) ?? (/^https?:\/\//i.test(rawAvatarRef) ? rawAvatarRef : void 0);
|
|
13574
|
+
const avatarRef = avatarUrl !== void 0 || avatarAssetRef !== void 0 ? await this.sealProfileImageRef(session.userId, session.userId) : rawAvatarRef;
|
|
12920
13575
|
const phone = maskedPhone(stringValue$1(data.phone));
|
|
12921
13576
|
const email = maskedEmail(stringValue$1(data.email));
|
|
13577
|
+
const wechatName = stringValue$1(data.wechat_nick_name).trim();
|
|
12922
13578
|
const canUpdateArkmeId = optionalBooleanValue(data.can_update_jotmo_id);
|
|
12923
13579
|
const profile = {
|
|
12924
13580
|
userId,
|
|
12925
13581
|
displayName,
|
|
12926
13582
|
nickname,
|
|
12927
13583
|
avatarRef,
|
|
12928
|
-
|
|
13584
|
+
...avatarAssetRef === void 0 ? {} : { avatarAssetRef },
|
|
13585
|
+
...avatarUrl === void 0 ? {} : { avatarUrl },
|
|
12929
13586
|
arkmeId: stringValue$1(data.jotmo_id).trim() || stringValue$1(data.name_slug).trim(),
|
|
12930
13587
|
...canUpdateArkmeId === void 0 ? {} : { canUpdateArkmeId },
|
|
12931
13588
|
accountType: numberValue$7(data.type),
|
|
@@ -12935,6 +13592,7 @@ var ProfileService = class {
|
|
|
12935
13592
|
wechat: booleanValue$2(data.has_bind_wechat),
|
|
12936
13593
|
google: booleanValue$2(data.has_bind_google)
|
|
12937
13594
|
},
|
|
13595
|
+
...wechatName === "" ? {} : { bindingNames: { wechat: wechatName } },
|
|
12938
13596
|
contact: {
|
|
12939
13597
|
...phone === void 0 ? {} : { phoneMasked: phone },
|
|
12940
13598
|
...email === void 0 ? {} : { emailMasked: email }
|
|
@@ -12954,6 +13612,12 @@ var ProfileService = class {
|
|
|
12954
13612
|
if (this.profileInFlight.get(session.userId) === pending) this.profileInFlight.delete(session.userId);
|
|
12955
13613
|
}
|
|
12956
13614
|
}
|
|
13615
|
+
profileNeedsAvatarRefresh(snapshot, userId) {
|
|
13616
|
+
const profile = snapshot.profile;
|
|
13617
|
+
if (profile?.userId !== userId) return false;
|
|
13618
|
+
const avatarRef = profile.avatarRef.trim();
|
|
13619
|
+
return /^https?:\/\//i.test(avatarRef) || fileAssetAvatarRef(avatarRef) !== void 0;
|
|
13620
|
+
}
|
|
12957
13621
|
async publicProfileSummariesByUserIds(userIds, session, signal) {
|
|
12958
13622
|
const normalized = [...new Set(userIds.filter((userId) => Number.isSafeInteger(userId) && userId > 0))].sort((left, right) => left - right);
|
|
12959
13623
|
const profiles = /* @__PURE__ */ new Map();
|
|
@@ -14287,6 +14951,38 @@ var SearchService = class {
|
|
|
14287
14951
|
}, session, options.signal);
|
|
14288
14952
|
return await this.withNavigationTargets(this.recordSearchResult(data, lockedRecordUids), options.signal);
|
|
14289
14953
|
}
|
|
14954
|
+
async searchFiles(options) {
|
|
14955
|
+
const page = options.query?.trim() ? await this.searchRemote({
|
|
14956
|
+
...options,
|
|
14957
|
+
query: options.query
|
|
14958
|
+
}) : await this.searchScene({
|
|
14959
|
+
...options,
|
|
14960
|
+
scene: "file"
|
|
14961
|
+
});
|
|
14962
|
+
const items = page.items.filter((item) => item.files.length > 0);
|
|
14963
|
+
const requests = items.flatMap((item) => item.files.map((file) => ({
|
|
14964
|
+
recordUid: item.recordUid,
|
|
14965
|
+
fileAssetUid: file.fileAssetUid
|
|
14966
|
+
})));
|
|
14967
|
+
const refs = /* @__PURE__ */ new Map();
|
|
14968
|
+
for (let offset = 0; offset < requests.length; offset += 50) {
|
|
14969
|
+
const part = await this.media.issueSearchFileMediaRefs(requests.slice(offset, offset + 50), options.signal);
|
|
14970
|
+
for (const [key, value] of part) refs.set(key, value);
|
|
14971
|
+
}
|
|
14972
|
+
return {
|
|
14973
|
+
...page,
|
|
14974
|
+
items: items.map((item) => ({
|
|
14975
|
+
...item,
|
|
14976
|
+
files: item.files.map((file) => {
|
|
14977
|
+
const mediaRef = refs.get(`${item.recordUid}\0${file.fileAssetUid}`);
|
|
14978
|
+
return mediaRef === void 0 ? file : {
|
|
14979
|
+
...file,
|
|
14980
|
+
mediaRef
|
|
14981
|
+
};
|
|
14982
|
+
})
|
|
14983
|
+
}))
|
|
14984
|
+
};
|
|
14985
|
+
}
|
|
14290
14986
|
async withAudioMediaRefs(result, signal) {
|
|
14291
14987
|
const requests = result.items.flatMap((item) => item.voice === void 0 || item.sourceKind === 3 ? [] : [{
|
|
14292
14988
|
recordUid: item.recordUid,
|
|
@@ -14589,6 +15285,561 @@ var SearchService = class {
|
|
|
14589
15285
|
}
|
|
14590
15286
|
};
|
|
14591
15287
|
//#endregion
|
|
15288
|
+
//#region src/services/file-transfers.ts
|
|
15289
|
+
const REF = /^arkme-file-v1\.[0-9a-f-]{36}$/;
|
|
15290
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
15291
|
+
const fail = (code, message) => new ArkmePluginError(code, message, false, 400);
|
|
15292
|
+
const clone = (value) => structuredClone(value);
|
|
15293
|
+
const publicFile = (file) => ({
|
|
15294
|
+
fileRef: file.fileRef,
|
|
15295
|
+
fileName: file.fileName,
|
|
15296
|
+
mimeType: file.mimeType,
|
|
15297
|
+
size: file.size,
|
|
15298
|
+
fileKind: file.fileKind
|
|
15299
|
+
});
|
|
15300
|
+
const nativeOpenFileName = (fileName) => {
|
|
15301
|
+
const cleaned = fileName.replace(/[<>:"/\\|?*\u0000-\u001f\u007f]/g, "_").trim().replace(/^[. ]+|[. ]+$/g, "");
|
|
15302
|
+
const extension = /\.[a-zA-Z0-9]{1,16}$/.exec(cleaned)?.[0] ?? "";
|
|
15303
|
+
let stem = (extension === "" ? cleaned : cleaned.slice(0, -extension.length)).replace(/[. ]+$/g, "");
|
|
15304
|
+
if (stem === "" || stem === "." || stem === "..") stem = "file";
|
|
15305
|
+
stem = stem.slice(0, Math.max(1, 180 - extension.length));
|
|
15306
|
+
if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(stem)) stem = `_${stem}`;
|
|
15307
|
+
return `${stem}${extension}`;
|
|
15308
|
+
};
|
|
15309
|
+
/** Plugin-owned files, not DSH session attachments. No client-controlled path is persisted. */
|
|
15310
|
+
var FileTransfers = class {
|
|
15311
|
+
directory;
|
|
15312
|
+
ports;
|
|
15313
|
+
states = /* @__PURE__ */ new Map();
|
|
15314
|
+
writes = Promise.resolve();
|
|
15315
|
+
mutations = Promise.resolve();
|
|
15316
|
+
queue = Promise.resolve();
|
|
15317
|
+
jobs = /* @__PURE__ */ new Set();
|
|
15318
|
+
controllers = /* @__PURE__ */ new Set();
|
|
15319
|
+
receptions = /* @__PURE__ */ new Map();
|
|
15320
|
+
policy;
|
|
15321
|
+
constructor(directory, ports, maxBytes) {
|
|
15322
|
+
this.directory = directory;
|
|
15323
|
+
this.ports = ports;
|
|
15324
|
+
this.policy = {
|
|
15325
|
+
version: 1,
|
|
15326
|
+
maxFileBytes: maxBytes,
|
|
15327
|
+
maxImageBytes: Math.min(maxBytes, 50 * 1024 * 1024),
|
|
15328
|
+
maxAttachments: 9
|
|
15329
|
+
};
|
|
15330
|
+
}
|
|
15331
|
+
capabilities() {
|
|
15332
|
+
return { ...this.policy };
|
|
15333
|
+
}
|
|
15334
|
+
cancelActive() {
|
|
15335
|
+
for (const controller of this.controllers) controller.abort();
|
|
15336
|
+
this.receptions.clear();
|
|
15337
|
+
}
|
|
15338
|
+
async settled() {
|
|
15339
|
+
await this.queue;
|
|
15340
|
+
await Promise.all(this.jobs);
|
|
15341
|
+
await this.writes;
|
|
15342
|
+
}
|
|
15343
|
+
root(userId) {
|
|
15344
|
+
return join(this.directory, String(userId));
|
|
15345
|
+
}
|
|
15346
|
+
path(userId, ref) {
|
|
15347
|
+
if (!REF.test(ref)) throw fail("file-ref-invalid", "文件引用无效或不属于当前账号");
|
|
15348
|
+
return join(this.root(userId), ref);
|
|
15349
|
+
}
|
|
15350
|
+
openDirectory(userId, ref) {
|
|
15351
|
+
if (!REF.test(ref)) throw fail("file-ref-invalid", "文件引用无效或不属于当前账号");
|
|
15352
|
+
return join(this.root(userId), ".open", ref);
|
|
15353
|
+
}
|
|
15354
|
+
async nativeOpenPath(userId, ref, file) {
|
|
15355
|
+
const source = this.path(userId, ref);
|
|
15356
|
+
const directory = this.openDirectory(userId, ref);
|
|
15357
|
+
const target = join(directory, nativeOpenFileName(file.fileName));
|
|
15358
|
+
await mkdir(directory, {
|
|
15359
|
+
recursive: true,
|
|
15360
|
+
mode: 448
|
|
15361
|
+
});
|
|
15362
|
+
try {
|
|
15363
|
+
await link(source, target);
|
|
15364
|
+
} catch (error) {
|
|
15365
|
+
if (error.code !== "EEXIST") throw error;
|
|
15366
|
+
const info = await stat(target).catch(() => void 0);
|
|
15367
|
+
if (!info?.isFile() || info.size !== file.size) {
|
|
15368
|
+
await unlink(target).catch(() => {});
|
|
15369
|
+
await link(source, target);
|
|
15370
|
+
}
|
|
15371
|
+
}
|
|
15372
|
+
await chmod$1(target, 384);
|
|
15373
|
+
return target;
|
|
15374
|
+
}
|
|
15375
|
+
async assertUser(userId, signal) {
|
|
15376
|
+
signal?.throwIfAborted();
|
|
15377
|
+
if (await this.ports.currentUser() !== userId) throw fail("file-account-changed", "账号已切换,请在原账号中重试");
|
|
15378
|
+
signal?.throwIfAborted();
|
|
15379
|
+
}
|
|
15380
|
+
state(userId) {
|
|
15381
|
+
let loaded = this.states.get(userId);
|
|
15382
|
+
if (loaded === void 0) {
|
|
15383
|
+
loaded = (async () => {
|
|
15384
|
+
await mkdir(this.root(userId), {
|
|
15385
|
+
recursive: true,
|
|
15386
|
+
mode: 448
|
|
15387
|
+
});
|
|
15388
|
+
let state;
|
|
15389
|
+
try {
|
|
15390
|
+
state = JSON.parse(await readFile(join(this.root(userId), "state.json"), "utf8"));
|
|
15391
|
+
} catch (error) {
|
|
15392
|
+
if (error.code !== "ENOENT") throw fail("file-state-invalid", "本地文件状态无法读取,请勿重复发送");
|
|
15393
|
+
return {
|
|
15394
|
+
version: 1,
|
|
15395
|
+
files: {},
|
|
15396
|
+
tasks: [],
|
|
15397
|
+
originals: {}
|
|
15398
|
+
};
|
|
15399
|
+
}
|
|
15400
|
+
if (state.version !== 1 || !state.files || !Array.isArray(state.tasks) || !state.originals) throw fail("file-state-invalid", "本地文件状态版本无效");
|
|
15401
|
+
for (const task of state.tasks) if (task.state === "sending") {
|
|
15402
|
+
task.state = "uncertain";
|
|
15403
|
+
task.error = "发送结果待确认,请先核对会话,避免重复发送";
|
|
15404
|
+
} else if (task.state === "queued" || task.state === "uploading") {
|
|
15405
|
+
task.state = "failed";
|
|
15406
|
+
task.error = "上次传输已中断,请重试";
|
|
15407
|
+
}
|
|
15408
|
+
return state;
|
|
15409
|
+
})();
|
|
15410
|
+
this.states.set(userId, loaded);
|
|
15411
|
+
}
|
|
15412
|
+
return loaded;
|
|
15413
|
+
}
|
|
15414
|
+
save(userId, state) {
|
|
15415
|
+
const data = JSON.stringify(state);
|
|
15416
|
+
const write = this.writes.catch(() => {}).then(async () => {
|
|
15417
|
+
const target = join(this.root(userId), "state.json");
|
|
15418
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
15419
|
+
try {
|
|
15420
|
+
await writeFile(temporary, data, { mode: 384 });
|
|
15421
|
+
await rename(temporary, target);
|
|
15422
|
+
} finally {
|
|
15423
|
+
await unlink(temporary).catch(() => {});
|
|
15424
|
+
}
|
|
15425
|
+
});
|
|
15426
|
+
this.writes = write;
|
|
15427
|
+
return write;
|
|
15428
|
+
}
|
|
15429
|
+
exclusive(action) {
|
|
15430
|
+
const work = this.mutations.catch(() => {}).then(action);
|
|
15431
|
+
this.mutations = work;
|
|
15432
|
+
return work;
|
|
15433
|
+
}
|
|
15434
|
+
validateMetadata(metadata) {
|
|
15435
|
+
const limit = metadata.mimeType.startsWith("image/") ? this.policy.maxImageBytes : this.policy.maxFileBytes;
|
|
15436
|
+
if (!metadata.fileName.trim() || metadata.fileName.length > 255 || /[\u0000-\u001f]/.test(metadata.fileName) || metadata.mimeType.length > 200 || !/^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/.test(metadata.mimeType) || !Number.isSafeInteger(metadata.size) || metadata.size <= 0 || metadata.size > limit) throw fail("file-input-invalid", `文件为空或超出限制(${Math.floor(limit / 1024 / 1024)} MiB)`);
|
|
15437
|
+
}
|
|
15438
|
+
async stage(temporaryPath, metadata, expectedUserId) {
|
|
15439
|
+
const userId = await this.ports.currentUser();
|
|
15440
|
+
if (expectedUserId !== void 0 && expectedUserId !== userId) throw fail("file-account-changed", "账号已切换,本次文件导入已取消");
|
|
15441
|
+
const normalizedMetadata = {
|
|
15442
|
+
...metadata,
|
|
15443
|
+
mimeType: arkmeNormalizedFileMimeType(metadata.mimeType, metadata.fileName)
|
|
15444
|
+
};
|
|
15445
|
+
this.validateMetadata(normalizedMetadata);
|
|
15446
|
+
return this.exclusive(async () => {
|
|
15447
|
+
const state = await this.state(userId);
|
|
15448
|
+
await this.prune(userId, state);
|
|
15449
|
+
if (Object.values(state.files).reduce((sum, file) => sum + file.size, 0) + metadata.size > Math.max(this.policy.maxFileBytes, 1024 * 1024 * 1024) || Object.keys(state.files).length >= 256) throw fail("file-cache-full", "本地附件空间不足,请移除不再需要的草稿或失败任务");
|
|
15450
|
+
const info = await stat(temporaryPath);
|
|
15451
|
+
if (!info.isFile() || info.size !== metadata.size) throw fail("file-size-mismatch", "本地文件不完整");
|
|
15452
|
+
const hash = createHash("sha256");
|
|
15453
|
+
for await (const chunk of createReadStream(temporaryPath)) hash.update(chunk);
|
|
15454
|
+
await this.assertUser(userId);
|
|
15455
|
+
const ref = `arkme-file-v1.${randomUUID()}`;
|
|
15456
|
+
const file = {
|
|
15457
|
+
...normalizedMetadata,
|
|
15458
|
+
fileRef: ref,
|
|
15459
|
+
fileKind: arkmePickedFileKind(normalizedMetadata.mimeType, normalizedMetadata.fileName),
|
|
15460
|
+
sha256: hash.digest("hex"),
|
|
15461
|
+
createdAtMillis: Date.now()
|
|
15462
|
+
};
|
|
15463
|
+
await copyFile(temporaryPath, this.path(userId, ref));
|
|
15464
|
+
await chmod$1(this.path(userId, ref), 384);
|
|
15465
|
+
state.files[ref] = file;
|
|
15466
|
+
try {
|
|
15467
|
+
await this.save(userId, state);
|
|
15468
|
+
} catch (error) {
|
|
15469
|
+
delete state.files[ref];
|
|
15470
|
+
await unlink(this.path(userId, ref)).catch(() => {});
|
|
15471
|
+
throw error;
|
|
15472
|
+
}
|
|
15473
|
+
return publicFile(file);
|
|
15474
|
+
});
|
|
15475
|
+
}
|
|
15476
|
+
async prune(userId, state) {
|
|
15477
|
+
const retained = new Set(state.tasks.filter((task) => task.state !== "sent").flatMap((task) => task.fileRefs));
|
|
15478
|
+
const completed = new Set(state.tasks.filter((task) => task.state === "sent").flatMap((task) => task.fileRefs));
|
|
15479
|
+
for (const file of Object.values(state.files)) {
|
|
15480
|
+
if (Date.now() - file.createdAtMillis < 168 * 36e5 || retained.has(file.fileRef) || !completed.has(file.fileRef)) continue;
|
|
15481
|
+
delete state.files[file.fileRef];
|
|
15482
|
+
await unlink(this.path(userId, file.fileRef)).catch(() => {});
|
|
15483
|
+
await rm(this.openDirectory(userId, file.fileRef), {
|
|
15484
|
+
recursive: true,
|
|
15485
|
+
force: true
|
|
15486
|
+
});
|
|
15487
|
+
}
|
|
15488
|
+
state.tasks = state.tasks.filter((task) => task.state !== "sent" || Date.now() - task.createdAtMillis < 168 * 36e5);
|
|
15489
|
+
}
|
|
15490
|
+
async readLocal(ref) {
|
|
15491
|
+
const userId = await this.ports.currentUser();
|
|
15492
|
+
const file = (await this.state(userId)).files[ref];
|
|
15493
|
+
if (file === void 0) throw fail("file-ref-invalid", "文件引用无效或不属于当前账号");
|
|
15494
|
+
const path = this.path(userId, ref);
|
|
15495
|
+
const info = await stat(path).catch(() => void 0);
|
|
15496
|
+
if (!info?.isFile() || info.size !== file.size) throw fail("file-local-missing", "本地文件已不存在,请重新添加或接收");
|
|
15497
|
+
await this.assertUser(userId);
|
|
15498
|
+
return {
|
|
15499
|
+
path,
|
|
15500
|
+
file: publicFile(file)
|
|
15501
|
+
};
|
|
15502
|
+
}
|
|
15503
|
+
async openLocal(ref) {
|
|
15504
|
+
if (this.ports.openPath === void 0) throw fail("file-open-unavailable", "当前宿主不能使用本机应用打开文件");
|
|
15505
|
+
const controller = new AbortController();
|
|
15506
|
+
this.controllers.add(controller);
|
|
15507
|
+
try {
|
|
15508
|
+
const userId = await this.ports.currentUser();
|
|
15509
|
+
const { file } = await this.readLocal(ref);
|
|
15510
|
+
await this.assertUser(userId, controller.signal);
|
|
15511
|
+
const path = await this.nativeOpenPath(userId, ref, file);
|
|
15512
|
+
await this.assertUser(userId, controller.signal);
|
|
15513
|
+
await this.ports.openPath(path, controller.signal);
|
|
15514
|
+
await this.assertUser(userId, controller.signal);
|
|
15515
|
+
return {
|
|
15516
|
+
opened: true,
|
|
15517
|
+
file
|
|
15518
|
+
};
|
|
15519
|
+
} catch (error) {
|
|
15520
|
+
if (error instanceof ArkmePluginError) throw error;
|
|
15521
|
+
throw fail("file-open-failed", "文件打开失败,请重试");
|
|
15522
|
+
} finally {
|
|
15523
|
+
this.controllers.delete(controller);
|
|
15524
|
+
}
|
|
15525
|
+
}
|
|
15526
|
+
async files() {
|
|
15527
|
+
const state = await this.state(await this.ports.currentUser());
|
|
15528
|
+
return Object.values(state.files).map(publicFile);
|
|
15529
|
+
}
|
|
15530
|
+
async remove(ref) {
|
|
15531
|
+
const userId = await this.ports.currentUser();
|
|
15532
|
+
await this.exclusive(async () => {
|
|
15533
|
+
const state = await this.state(userId);
|
|
15534
|
+
await this.assertUser(userId);
|
|
15535
|
+
if (state.tasks.some((task) => task.fileRefs.includes(ref))) throw fail("file-in-use", "文件仍被本地发送任务引用,请先移除该任务");
|
|
15536
|
+
if (!state.files[ref]) return;
|
|
15537
|
+
delete state.files[ref];
|
|
15538
|
+
await this.save(userId, state);
|
|
15539
|
+
await unlink(this.path(userId, ref)).catch(() => {});
|
|
15540
|
+
await rm(this.openDirectory(userId, ref), {
|
|
15541
|
+
recursive: true,
|
|
15542
|
+
force: true
|
|
15543
|
+
});
|
|
15544
|
+
});
|
|
15545
|
+
}
|
|
15546
|
+
async tasks(sourceRef) {
|
|
15547
|
+
const state = await this.state(await this.ports.currentUser());
|
|
15548
|
+
return clone(state.tasks.filter((task) => !sourceRef || task.sourceRef === sourceRef));
|
|
15549
|
+
}
|
|
15550
|
+
async stageBytes(contentBase64, metadata) {
|
|
15551
|
+
if (contentBase64.length > Math.ceil(65536 / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(contentBase64)) throw fail("file-tool-input-invalid", "工具暂存只接受最多 64 KiB 的 Base64 内容,不接受本机路径");
|
|
15552
|
+
const data = Buffer.from(contentBase64, "base64");
|
|
15553
|
+
if (data.length > 65536) throw fail("file-tool-input-invalid", "工具暂存文件超过 64 KiB");
|
|
15554
|
+
const userId = await this.ports.currentUser();
|
|
15555
|
+
await this.state(userId);
|
|
15556
|
+
const temporary = join(this.root(userId), `${randomUUID()}.import`);
|
|
15557
|
+
try {
|
|
15558
|
+
await writeFile(temporary, data, {
|
|
15559
|
+
mode: 384,
|
|
15560
|
+
flag: "wx"
|
|
15561
|
+
});
|
|
15562
|
+
await this.assertUser(userId);
|
|
15563
|
+
return await this.stage(temporary, {
|
|
15564
|
+
...metadata,
|
|
15565
|
+
size: data.length
|
|
15566
|
+
}, userId);
|
|
15567
|
+
} finally {
|
|
15568
|
+
await unlink(temporary).catch(() => {});
|
|
15569
|
+
}
|
|
15570
|
+
}
|
|
15571
|
+
async discard(taskRef) {
|
|
15572
|
+
const userId = await this.ports.currentUser();
|
|
15573
|
+
await this.exclusive(async () => {
|
|
15574
|
+
const state = await this.state(userId);
|
|
15575
|
+
await this.assertUser(userId);
|
|
15576
|
+
const task = state.tasks.find((value) => value.taskRef === taskRef);
|
|
15577
|
+
if (!task) return;
|
|
15578
|
+
if ([
|
|
15579
|
+
"queued",
|
|
15580
|
+
"uploading",
|
|
15581
|
+
"sending"
|
|
15582
|
+
].includes(task.state)) throw fail("file-task-active", "任务仍在进行中,不能移除");
|
|
15583
|
+
state.tasks = state.tasks.filter((value) => value !== task);
|
|
15584
|
+
await this.save(userId, state);
|
|
15585
|
+
});
|
|
15586
|
+
}
|
|
15587
|
+
async reconcile(taskRef) {
|
|
15588
|
+
const userId = await this.ports.currentUser();
|
|
15589
|
+
return this.exclusive(async () => {
|
|
15590
|
+
const state = await this.state(userId);
|
|
15591
|
+
const task = state.tasks.find((value) => value.taskRef === taskRef);
|
|
15592
|
+
if (!task) throw fail("file-task-missing", "发送任务不存在");
|
|
15593
|
+
if (task.state !== "uncertain") return clone(task);
|
|
15594
|
+
const controller = new AbortController();
|
|
15595
|
+
this.controllers.add(controller);
|
|
15596
|
+
try {
|
|
15597
|
+
await this.assertUser(userId, controller.signal);
|
|
15598
|
+
const result = await this.ports.reconcile?.(task, controller.signal);
|
|
15599
|
+
await this.assertUser(userId, controller.signal);
|
|
15600
|
+
if (result) {
|
|
15601
|
+
task.result = result;
|
|
15602
|
+
task.state = "sent";
|
|
15603
|
+
delete task.error;
|
|
15604
|
+
await this.save(userId, state);
|
|
15605
|
+
}
|
|
15606
|
+
return clone(task);
|
|
15607
|
+
} finally {
|
|
15608
|
+
this.controllers.delete(controller);
|
|
15609
|
+
}
|
|
15610
|
+
});
|
|
15611
|
+
}
|
|
15612
|
+
async enqueue(input) {
|
|
15613
|
+
const userId = await this.ports.currentUser();
|
|
15614
|
+
if (!UUID.test(input.recordUid) || !UUID.test(input.relationUid) || !input.sourceRef.trim() || input.fileRefs.length < 1 || input.fileRefs.length > this.policy.maxAttachments || new Set(input.fileRefs).size !== input.fileRefs.length || (input.content.textContent?.length ?? 0) > 2e4) throw fail("file-send-invalid", "发送标识、正文或附件数量无效");
|
|
15615
|
+
await this.ports.validateSource(input.sourceRef);
|
|
15616
|
+
return this.exclusive(async () => {
|
|
15617
|
+
await this.assertUser(userId);
|
|
15618
|
+
const state = await this.state(userId);
|
|
15619
|
+
const prior = state.tasks.find((task) => task.recordUid === input.recordUid);
|
|
15620
|
+
if (prior) {
|
|
15621
|
+
if (prior.sourceRef !== input.sourceRef || prior.relationUid !== input.relationUid || JSON.stringify(prior.fileRefs) !== JSON.stringify(input.fileRefs) || JSON.stringify(prior.content) !== JSON.stringify(input.content)) throw fail("file-send-conflict", "发送标识已用于另一份内容");
|
|
15622
|
+
return clone(prior);
|
|
15623
|
+
}
|
|
15624
|
+
if (state.tasks.filter((task) => task.state !== "sent").length >= 100) throw fail("file-queue-full", "待发送文件过多,请先处理失败任务");
|
|
15625
|
+
const files = [];
|
|
15626
|
+
for (const ref of input.fileRefs) {
|
|
15627
|
+
const { file } = await this.readLocal(ref);
|
|
15628
|
+
files.push({
|
|
15629
|
+
...file,
|
|
15630
|
+
progress: {
|
|
15631
|
+
phase: "preparing",
|
|
15632
|
+
sentBytes: 0,
|
|
15633
|
+
totalBytes: file.size
|
|
15634
|
+
}
|
|
15635
|
+
});
|
|
15636
|
+
}
|
|
15637
|
+
await this.assertUser(userId);
|
|
15638
|
+
const task = {
|
|
15639
|
+
...clone(input),
|
|
15640
|
+
taskRef: `arkme-send-v1.${randomUUID()}`,
|
|
15641
|
+
createdAtMillis: Date.now(),
|
|
15642
|
+
state: "queued",
|
|
15643
|
+
files
|
|
15644
|
+
};
|
|
15645
|
+
state.tasks.push(task);
|
|
15646
|
+
try {
|
|
15647
|
+
await this.save(userId, state);
|
|
15648
|
+
} catch (error) {
|
|
15649
|
+
state.tasks = state.tasks.filter((value) => value !== task);
|
|
15650
|
+
throw error;
|
|
15651
|
+
}
|
|
15652
|
+
this.schedule(userId, state, task);
|
|
15653
|
+
return clone(task);
|
|
15654
|
+
});
|
|
15655
|
+
}
|
|
15656
|
+
async retry(taskRef) {
|
|
15657
|
+
const userId = await this.ports.currentUser();
|
|
15658
|
+
return this.exclusive(async () => {
|
|
15659
|
+
const state = await this.state(userId);
|
|
15660
|
+
const task = state.tasks.find((value) => value.taskRef === taskRef);
|
|
15661
|
+
if (!task) throw fail("file-task-missing", "发送任务不存在");
|
|
15662
|
+
await this.assertUser(userId);
|
|
15663
|
+
if (task.state === "uncertain") throw fail("file-send-uncertain", "发送结果待确认,请先核对原会话,不能自动重复发送");
|
|
15664
|
+
if (task.state !== "failed") return clone(task);
|
|
15665
|
+
task.state = "queued";
|
|
15666
|
+
delete task.error;
|
|
15667
|
+
for (const file of task.files) if (!file.asset) file.progress = {
|
|
15668
|
+
phase: "preparing",
|
|
15669
|
+
sentBytes: 0,
|
|
15670
|
+
totalBytes: file.size
|
|
15671
|
+
};
|
|
15672
|
+
await this.save(userId, state);
|
|
15673
|
+
this.schedule(userId, state, task);
|
|
15674
|
+
return clone(task);
|
|
15675
|
+
});
|
|
15676
|
+
}
|
|
15677
|
+
schedule(userId, state, task) {
|
|
15678
|
+
const controller = new AbortController();
|
|
15679
|
+
this.controllers.add(controller);
|
|
15680
|
+
this.queue = this.queue.catch(() => {}).then(async () => {
|
|
15681
|
+
try {
|
|
15682
|
+
await this.assertUser(userId, controller.signal);
|
|
15683
|
+
task.state = "uploading";
|
|
15684
|
+
await this.save(userId, state);
|
|
15685
|
+
for (const file of task.files) {
|
|
15686
|
+
await this.assertUser(userId, controller.signal);
|
|
15687
|
+
const stored = state.files[file.fileRef];
|
|
15688
|
+
if (!stored) throw fail("file-local-missing", "本地附件已不存在");
|
|
15689
|
+
if (!file.asset) {
|
|
15690
|
+
const cached = stored.asset ?? Object.values(state.files).find((other) => other.sha256 === stored.sha256 && other.fileKind === stored.fileKind && other.asset)?.asset;
|
|
15691
|
+
if (cached) file.asset = cached;
|
|
15692
|
+
if (!file.asset) file.asset = await this.ports.upload(this.path(userId, file.fileRef), stored, (progress) => {
|
|
15693
|
+
file.progress = progress;
|
|
15694
|
+
}, userId, controller.signal);
|
|
15695
|
+
file.asset = {
|
|
15696
|
+
...file.asset,
|
|
15697
|
+
fileName: file.fileName
|
|
15698
|
+
};
|
|
15699
|
+
stored.asset = file.asset;
|
|
15700
|
+
file.progress = {
|
|
15701
|
+
phase: "ready",
|
|
15702
|
+
sentBytes: file.size,
|
|
15703
|
+
totalBytes: file.size
|
|
15704
|
+
};
|
|
15705
|
+
await this.save(userId, state);
|
|
15706
|
+
}
|
|
15707
|
+
}
|
|
15708
|
+
await this.assertUser(userId, controller.signal);
|
|
15709
|
+
task.state = "sending";
|
|
15710
|
+
await this.save(userId, state);
|
|
15711
|
+
task.result = await this.ports.send(task, task.files.map((file) => file.asset), userId, controller.signal);
|
|
15712
|
+
task.state = "sent";
|
|
15713
|
+
delete task.error;
|
|
15714
|
+
} catch (error) {
|
|
15715
|
+
const sending = task.state === "sending";
|
|
15716
|
+
task.state = sending ? "uncertain" : "failed";
|
|
15717
|
+
task.error = sending ? "发送结果待确认,请先核对会话,避免重复发送" : error instanceof ArkmePluginError ? error.message : "文件传输中断,请重试";
|
|
15718
|
+
} finally {
|
|
15719
|
+
this.controllers.delete(controller);
|
|
15720
|
+
await this.save(userId, state);
|
|
15721
|
+
}
|
|
15722
|
+
}).catch(() => {
|
|
15723
|
+
task.state = "uncertain";
|
|
15724
|
+
task.error = "本地发送状态保存失败,请先核对会话";
|
|
15725
|
+
});
|
|
15726
|
+
}
|
|
15727
|
+
async reception(mediaRef, start = false) {
|
|
15728
|
+
const userId = await this.ports.currentUser();
|
|
15729
|
+
if (!mediaRef.startsWith("arkme-media-v1.") || mediaRef.length > 100) throw fail("media-ref-invalid", "原文件引用无效");
|
|
15730
|
+
const state = await this.state(userId);
|
|
15731
|
+
const key = `${userId}:${mediaRef}`;
|
|
15732
|
+
const localRef = state.originals[mediaRef];
|
|
15733
|
+
if (localRef) try {
|
|
15734
|
+
const { file } = await this.readLocal(localRef);
|
|
15735
|
+
return {
|
|
15736
|
+
state: "ready",
|
|
15737
|
+
receivedBytes: file.size,
|
|
15738
|
+
totalBytes: file.size,
|
|
15739
|
+
file
|
|
15740
|
+
};
|
|
15741
|
+
} catch {
|
|
15742
|
+
delete state.originals[mediaRef];
|
|
15743
|
+
}
|
|
15744
|
+
const existing = this.receptions.get(key);
|
|
15745
|
+
if (existing?.state === "receiving" || !start) return clone(existing ?? {
|
|
15746
|
+
state: "missing",
|
|
15747
|
+
receivedBytes: 0,
|
|
15748
|
+
totalBytes: 0
|
|
15749
|
+
});
|
|
15750
|
+
if ([...this.receptions.values()].filter((value) => value.state === "receiving").length >= 2) throw fail("file-download-busy", "已有两个文件正在接收,请稍后重试");
|
|
15751
|
+
const progress = {
|
|
15752
|
+
state: "receiving",
|
|
15753
|
+
receivedBytes: 0,
|
|
15754
|
+
totalBytes: 0
|
|
15755
|
+
};
|
|
15756
|
+
this.receptions.set(key, progress);
|
|
15757
|
+
const controller = new AbortController();
|
|
15758
|
+
this.controllers.add(controller);
|
|
15759
|
+
const temporary = join(this.root(userId), `${randomUUID()}.download`);
|
|
15760
|
+
const job = (async () => {
|
|
15761
|
+
try {
|
|
15762
|
+
const { response, descriptor } = await this.ports.fetchMedia(mediaRef, controller.signal);
|
|
15763
|
+
if (!response.ok || !response.body) throw fail("file-download-failed", "原文件接收失败");
|
|
15764
|
+
const declared = Number(response.headers.get("content-length")) || descriptor.size;
|
|
15765
|
+
if (declared > this.policy.maxFileBytes) throw fail("file-download-too-large", "原文件超出本地文件大小限制");
|
|
15766
|
+
progress.totalBytes = Math.max(0, declared);
|
|
15767
|
+
const handle = await open(temporary, "wx", 384);
|
|
15768
|
+
try {
|
|
15769
|
+
for await (const chunk of response.body) {
|
|
15770
|
+
await this.assertUser(userId, controller.signal);
|
|
15771
|
+
progress.receivedBytes += chunk.length;
|
|
15772
|
+
if (progress.receivedBytes > this.policy.maxFileBytes) throw fail("file-download-too-large", "原文件超出本地文件大小限制");
|
|
15773
|
+
await handle.write(chunk);
|
|
15774
|
+
}
|
|
15775
|
+
} finally {
|
|
15776
|
+
await handle.close();
|
|
15777
|
+
}
|
|
15778
|
+
if (!progress.receivedBytes || declared > 0 && progress.receivedBytes !== declared || descriptor.size > 0 && progress.receivedBytes !== descriptor.size) throw fail("file-download-incomplete", "原文件接收不完整,请重试");
|
|
15779
|
+
await this.assertUser(userId, controller.signal);
|
|
15780
|
+
const file = await this.stage(temporary, {
|
|
15781
|
+
fileName: descriptor.fileName,
|
|
15782
|
+
mimeType: descriptor.mimeType,
|
|
15783
|
+
size: progress.receivedBytes
|
|
15784
|
+
}, userId);
|
|
15785
|
+
state.originals[mediaRef] = file.fileRef;
|
|
15786
|
+
await this.save(userId, state);
|
|
15787
|
+
progress.file = file;
|
|
15788
|
+
progress.state = "ready";
|
|
15789
|
+
} catch (error) {
|
|
15790
|
+
progress.state = "failed";
|
|
15791
|
+
progress.error = error instanceof ArkmePluginError ? error.message : "文件接收失败,请重试";
|
|
15792
|
+
} finally {
|
|
15793
|
+
await unlink(temporary).catch(() => {});
|
|
15794
|
+
this.controllers.delete(controller);
|
|
15795
|
+
}
|
|
15796
|
+
})();
|
|
15797
|
+
this.jobs.add(job);
|
|
15798
|
+
job.finally(() => this.jobs.delete(job));
|
|
15799
|
+
return clone(progress);
|
|
15800
|
+
}
|
|
15801
|
+
};
|
|
15802
|
+
//#endregion
|
|
15803
|
+
//#region src/file-transfer-owner.ts
|
|
15804
|
+
function createArkmeFileTransfers(options) {
|
|
15805
|
+
if (options.directory === void 0) return void 0;
|
|
15806
|
+
return new FileTransfers(options.directory, {
|
|
15807
|
+
currentUser: async () => (await options.runtime.requireSession()).userId,
|
|
15808
|
+
validateSource: async (sourceRef) => {
|
|
15809
|
+
await options.source.openSourceRef(sourceRef, (await options.runtime.requireSession()).userId);
|
|
15810
|
+
},
|
|
15811
|
+
upload: async (path, metadata, onProgress, expectedUserId, signal) => await options.media.uploadLocalFile(path, metadata, {
|
|
15812
|
+
onProgress,
|
|
15813
|
+
expectedUserId,
|
|
15814
|
+
signal
|
|
15815
|
+
}),
|
|
15816
|
+
send: async (input, assets, expectedUserId, signal) => await options.chat.sendSourceRich(input.sourceRef, {
|
|
15817
|
+
...input.content,
|
|
15818
|
+
assets
|
|
15819
|
+
}, {
|
|
15820
|
+
recordUid: input.recordUid,
|
|
15821
|
+
relationUid: input.relationUid,
|
|
15822
|
+
expectedUserId,
|
|
15823
|
+
signal
|
|
15824
|
+
}),
|
|
15825
|
+
fetchMedia: async (ref, signal) => await options.media.fetchMedia(ref, void 0, signal, true),
|
|
15826
|
+
...options.openPath === void 0 ? {} : { openPath: options.openPath },
|
|
15827
|
+
reconcile: async (input, signal) => {
|
|
15828
|
+
const item = (await options.chat.readSource(input.sourceRef, {
|
|
15829
|
+
limit: 100,
|
|
15830
|
+
signal
|
|
15831
|
+
})).items.find((value) => value.itemUid === input.recordUid && value.isMe);
|
|
15832
|
+
return item === void 0 ? void 0 : {
|
|
15833
|
+
sourceRef: input.sourceRef,
|
|
15834
|
+
itemUid: item.itemUid,
|
|
15835
|
+
status: item.status,
|
|
15836
|
+
...item.sequence === void 0 ? {} : { sequence: item.sequence },
|
|
15837
|
+
localState: "synced"
|
|
15838
|
+
};
|
|
15839
|
+
}
|
|
15840
|
+
}, options.maxUploadBytes ?? 100 * 1024 * 1024);
|
|
15841
|
+
}
|
|
15842
|
+
//#endregion
|
|
14592
15843
|
//#region src/services/unmarked-speaker-service.ts
|
|
14593
15844
|
const REF_TTL_MS = 30 * 6e4;
|
|
14594
15845
|
const REF_CAP = 2e3;
|
|
@@ -17259,6 +18510,8 @@ var ArkmeService = class {
|
|
|
17259
18510
|
contactDirectory;
|
|
17260
18511
|
unmarkedSpeaker;
|
|
17261
18512
|
voiceprint;
|
|
18513
|
+
fileTransfers;
|
|
18514
|
+
localFileOpener;
|
|
17262
18515
|
worldVoiceprintInviteVariantIndex = 0;
|
|
17263
18516
|
constructor(config, sessionStore, stateStore, fetchImpl = fetch, pendingSessionStore, outgoingCallBroker = new ArkmeOutgoingCallBroker(), billingGateway) {
|
|
17264
18517
|
this.config = config;
|
|
@@ -17313,9 +18566,74 @@ var ArkmeService = class {
|
|
|
17313
18566
|
this.clearAccountState(userIds);
|
|
17314
18567
|
}
|
|
17315
18568
|
});
|
|
18569
|
+
this.fileTransfers = createArkmeFileTransfers({
|
|
18570
|
+
directory: config.fileStateDirectory,
|
|
18571
|
+
maxUploadBytes: config.maxUploadBytes,
|
|
18572
|
+
runtime: this.runtime,
|
|
18573
|
+
source: this.source,
|
|
18574
|
+
media: this.media,
|
|
18575
|
+
chat: this.chat,
|
|
18576
|
+
openPath: async (path, signal) => {
|
|
18577
|
+
if (this.localFileOpener === void 0) throw new Error("当前 DSH 宿主未提供本机文件打开能力");
|
|
18578
|
+
await this.localFileOpener(path, signal);
|
|
18579
|
+
}
|
|
18580
|
+
});
|
|
18581
|
+
}
|
|
18582
|
+
filesOwner() {
|
|
18583
|
+
if (!this.fileTransfers) throw new ArkmePluginError("file-flow-unavailable", "当前宿主不支持本地文件流程,请升级插件", false, 501);
|
|
18584
|
+
return this.fileTransfers;
|
|
18585
|
+
}
|
|
18586
|
+
fileCapabilities() {
|
|
18587
|
+
return this.filesOwner().capabilities();
|
|
18588
|
+
}
|
|
18589
|
+
async fileSearch(options) {
|
|
18590
|
+
return await this.search.searchFiles(options);
|
|
18591
|
+
}
|
|
18592
|
+
async fileSessionUser() {
|
|
18593
|
+
return (await this.runtime.requireSession()).userId;
|
|
18594
|
+
}
|
|
18595
|
+
async fileStage(path, metadata, expectedUserId) {
|
|
18596
|
+
return await this.filesOwner().stage(path, metadata, expectedUserId);
|
|
18597
|
+
}
|
|
18598
|
+
async fileList() {
|
|
18599
|
+
return await this.filesOwner().files();
|
|
18600
|
+
}
|
|
18601
|
+
async fileReadLocal(ref) {
|
|
18602
|
+
return await this.filesOwner().readLocal(ref);
|
|
18603
|
+
}
|
|
18604
|
+
attachLocalFileOpener(openPath) {
|
|
18605
|
+
this.localFileOpener = openPath;
|
|
18606
|
+
}
|
|
18607
|
+
async fileOpenLocal(ref) {
|
|
18608
|
+
return await this.filesOwner().openLocal(ref);
|
|
18609
|
+
}
|
|
18610
|
+
async fileRemove(ref) {
|
|
18611
|
+
await this.filesOwner().remove(ref);
|
|
18612
|
+
}
|
|
18613
|
+
async fileSend(input) {
|
|
18614
|
+
return await this.filesOwner().enqueue(input);
|
|
18615
|
+
}
|
|
18616
|
+
async fileSendTasks(sourceRef) {
|
|
18617
|
+
return await this.filesOwner().tasks(sourceRef);
|
|
18618
|
+
}
|
|
18619
|
+
async fileSendRetry(taskRef) {
|
|
18620
|
+
return await this.filesOwner().retry(taskRef);
|
|
18621
|
+
}
|
|
18622
|
+
async fileStageBytes(contentBase64, metadata) {
|
|
18623
|
+
return await this.filesOwner().stageBytes(contentBase64, metadata);
|
|
18624
|
+
}
|
|
18625
|
+
async fileSendDiscard(taskRef) {
|
|
18626
|
+
return await this.filesOwner().discard(taskRef);
|
|
18627
|
+
}
|
|
18628
|
+
async fileSendReconcile(taskRef) {
|
|
18629
|
+
return await this.filesOwner().reconcile(taskRef);
|
|
18630
|
+
}
|
|
18631
|
+
async fileReceive(mediaRef, start = false) {
|
|
18632
|
+
return await this.filesOwner().reception(mediaRef, start);
|
|
17316
18633
|
}
|
|
17317
18634
|
clearAccountState(userIds) {
|
|
17318
18635
|
for (const userId of userIds) this.privacy.clear(userId);
|
|
18636
|
+
this.fileTransfers?.cancelActive();
|
|
17319
18637
|
for (const userId of userIds) this.outgoingCall.clearUser(userId, "账号已退出,呼叫已取消");
|
|
17320
18638
|
this.source.dispose();
|
|
17321
18639
|
this.media.dispose();
|
|
@@ -17398,7 +18716,9 @@ var ArkmeService = class {
|
|
|
17398
18716
|
return await this.bot.openBotPrivateChat(botRef, options);
|
|
17399
18717
|
}
|
|
17400
18718
|
async sendBotPrivateChatMessage(botRef, content, options = {}) {
|
|
17401
|
-
|
|
18719
|
+
const outcome = await this.bot.sendBotPrivateChatMessage(botRef, content, options);
|
|
18720
|
+
if (outcome.recordProjectionChanged) await this.realtime.invalidateRecordProjection();
|
|
18721
|
+
return outcome.result;
|
|
17402
18722
|
}
|
|
17403
18723
|
async listGroupBots(groupSourceRef, options = {}) {
|
|
17404
18724
|
return await this.bot.listGroupBots(groupSourceRef, options);
|
|
@@ -17461,6 +18781,7 @@ var ArkmeService = class {
|
|
|
17461
18781
|
retryOutbox: true,
|
|
17462
18782
|
revisionPolling: true,
|
|
17463
18783
|
userProfile: true,
|
|
18784
|
+
accountSettings: true,
|
|
17464
18785
|
imageRead: true,
|
|
17465
18786
|
recordCalendar: true,
|
|
17466
18787
|
imageLibrary: true,
|
|
@@ -17481,6 +18802,7 @@ var ArkmeService = class {
|
|
|
17481
18802
|
contactAdd: true,
|
|
17482
18803
|
conversationQuickAdd: true,
|
|
17483
18804
|
groupSettings: true,
|
|
18805
|
+
groupAiPolish: true,
|
|
17484
18806
|
extensionManagement: true,
|
|
17485
18807
|
extensionMetadataEdit: true,
|
|
17486
18808
|
extensionIcons: true,
|
|
@@ -17550,6 +18872,7 @@ var ArkmeService = class {
|
|
|
17550
18872
|
return await this.callHistory.retryCallSummary(callRef, signal);
|
|
17551
18873
|
}
|
|
17552
18874
|
dispose() {
|
|
18875
|
+
this.fileTransfers?.cancelActive();
|
|
17553
18876
|
this.contact.dispose();
|
|
17554
18877
|
this.contactDirectory.dispose();
|
|
17555
18878
|
this.unmarkedSpeaker.dispose();
|
|
@@ -17747,6 +19070,12 @@ var ArkmeService = class {
|
|
|
17747
19070
|
async generateGroupAiPolishRule(groupName, requirement, options = {}) {
|
|
17748
19071
|
return await this.aiPolish.generateGroupAiPolishRule(groupName, requirement, options);
|
|
17749
19072
|
}
|
|
19073
|
+
prepareEnableGroupAiPolish(groupName, ruleName = "", options = {}) {
|
|
19074
|
+
return this.aiPolish.prepareEnableGroupAiPolish(groupName, ruleName, options);
|
|
19075
|
+
}
|
|
19076
|
+
prepareEnableGroupAiPolishRuleForSource(sourceRef, ruleRef, options = {}) {
|
|
19077
|
+
return this.aiPolish.prepareEnableGroupAiPolishRuleForSource(sourceRef, ruleRef, options);
|
|
19078
|
+
}
|
|
17750
19079
|
async confirmEnableGroupAiPolish(confirmationRef, options = {}) {
|
|
17751
19080
|
return await this.aiPolish.confirmEnableGroupAiPolish(confirmationRef, options);
|
|
17752
19081
|
}
|
|
@@ -19768,4 +21097,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
|
|
|
19768
21097
|
return jsonValue(await handler(jsonValue(args)));
|
|
19769
21098
|
}
|
|
19770
21099
|
//#endregion
|
|
19771
|
-
export {
|
|
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 };
|