@senguoyun/dsh-arkme 0.1.14 → 0.1.16
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/assets/desktop_call/avatar-lin-xiaoman.jpeg +0 -0
- package/assets/desktop_call/avatar-mother.jpg +0 -0
- package/assets/desktop_call/avatar-self.png +0 -0
- package/assets/desktop_call/call-add-linear.svg +13 -0
- package/assets/desktop_call/icon-scan-add-contact.svg +4 -0
- package/assets/desktop_call/profile-circle-linear.svg +11 -0
- package/assets/desktop_call/user-add-linear.svg +15 -0
- package/lib/client.js +18343 -10390
- package/lib/client.js.map +1 -1
- package/lib/extension-profile-restart-helper.js +2 -2
- package/lib/index.js +713 -87
- package/lib/{install-store-Sn3Ccxfu.js → install-store-Beau1y-o.js} +12 -6
- package/lib/persistent-extension.js +1 -1
- package/lib/{persistent-runtime-BAArZI-P.js → persistent-runtime-D_hNgTX3.js} +1001 -142
- package/lib/{plugin-updater-helper-CiPlNqBc.js → plugin-updater-helper-BEYBTOld.js} +5 -5
- package/lib/plugin-updater-helper.js +1 -1
- package/lib/{profile-package-manager-DvzGgkqe.js → profile-package-manager-DwuQu8ZL.js} +91 -3
- package/lib/sdk.js +23 -2
- package/lib/types/arkme-service.d.ts +49 -1
- package/lib/types/arkme-service.d.ts.map +1 -1
- package/lib/types/client/ArkmeArkoSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
- package/lib/types/client/ArkmeCalendarSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeCallHistorySurface.d.ts +57 -0
- package/lib/types/client/ArkmeCallHistorySurface.d.ts.map +1 -0
- package/lib/types/client/ArkmeChatMemberActions.d.ts +48 -0
- package/lib/types/client/ArkmeChatMemberActions.d.ts.map +1 -0
- package/lib/types/client/ArkmeExtensionReviews.d.ts +4 -2
- package/lib/types/client/ArkmeExtensionReviews.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/ArkmeMarketplace.d.ts +18 -4
- package/lib/types/client/ArkmeMarketplace.d.ts.map +1 -1
- package/lib/types/client/ArkmeMentionTextarea.d.ts +13 -0
- package/lib/types/client/ArkmeMentionTextarea.d.ts.map +1 -0
- package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
- package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -1
- package/lib/types/client/ArkmeRichContent.d.ts +7 -1
- package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
- package/lib/types/client/ArkmeSidebar.d.ts +2 -0
- package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
- package/lib/types/client/ArkmeStartupAuthGate.d.ts +4 -2
- package/lib/types/client/ArkmeStartupAuthGate.d.ts.map +1 -1
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts +6 -1
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
- package/lib/types/client/ArkmeVoiceprintSurface.d.ts +61 -0
- package/lib/types/client/ArkmeVoiceprintSurface.d.ts.map +1 -0
- package/lib/types/client/ArkmeWorldSurface.d.ts +43 -2
- package/lib/types/client/ArkmeWorldSurface.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +1 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/composer-draft-store.d.ts +18 -0
- package/lib/types/client/composer-draft-store.d.ts.map +1 -1
- package/lib/types/client/composer-focus.d.ts +8 -0
- package/lib/types/client/composer-focus.d.ts.map +1 -0
- package/lib/types/client/conversation-composer-presentation.d.ts +47 -0
- package/lib/types/client/conversation-composer-presentation.d.ts.map +1 -0
- package/lib/types/client/conversation-memory-cache.d.ts +30 -1
- package/lib/types/client/conversation-memory-cache.d.ts.map +1 -1
- package/lib/types/client/demo-audio.d.ts +2 -0
- package/lib/types/client/demo-audio.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/interwoven-moments.d.ts +2 -1
- package/lib/types/client/interwoven-moments.d.ts.map +1 -1
- package/lib/types/client/marketplace-sort-preference.d.ts +5 -0
- package/lib/types/client/marketplace-sort-preference.d.ts.map +1 -0
- package/lib/types/client/navigation-cache.d.ts +4 -0
- package/lib/types/client/navigation-cache.d.ts.map +1 -1
- package/lib/types/client/provider-instance-runtime.d.ts +20 -0
- package/lib/types/client/provider-instance-runtime.d.ts.map +1 -0
- package/lib/types/client/redesign/ArkmeRootFrame.d.ts +1 -1
- package/lib/types/client/redesign/ArkmeRootFrame.d.ts.map +1 -1
- package/lib/types/client/redesign/styles.d.ts.map +1 -1
- package/lib/types/client/related-recordings.d.ts +1 -0
- package/lib/types/client/related-recordings.d.ts.map +1 -1
- package/lib/types/client/source-list.d.ts +4 -0
- package/lib/types/client/source-list.d.ts.map +1 -1
- package/lib/types/client/ui-controller.d.ts +3 -1
- package/lib/types/client/ui-controller.d.ts.map +1 -1
- package/lib/types/client/voiceprint-enrollment-client.d.ts +12 -0
- package/lib/types/client/voiceprint-enrollment-client.d.ts.map +1 -0
- package/lib/types/client/voiceprint-recorder.d.ts +23 -0
- package/lib/types/client/voiceprint-recorder.d.ts.map +1 -0
- package/lib/types/client/world-voiceprint-playback.d.ts +41 -0
- package/lib/types/client/world-voiceprint-playback.d.ts.map +1 -0
- package/lib/types/extensions/install-store.d.ts +1 -0
- package/lib/types/extensions/install-store.d.ts.map +1 -1
- package/lib/types/extensions/manager.d.ts +2 -0
- package/lib/types/extensions/manager.d.ts.map +1 -1
- package/lib/types/extensions/owned-inventory.d.ts +3 -1
- package/lib/types/extensions/owned-inventory.d.ts.map +1 -1
- package/lib/types/extensions/owned-store.d.ts +1 -0
- package/lib/types/extensions/owned-store.d.ts.map +1 -1
- package/lib/types/extensions/profile-installer.d.ts.map +1 -1
- package/lib/types/extensions/publish-client.d.ts +2 -0
- package/lib/types/extensions/publish-client.d.ts.map +1 -1
- package/lib/types/host-api.d.ts +1 -1
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/local-database.d.ts +1 -0
- package/lib/types/local-database.d.ts.map +1 -1
- package/lib/types/openclaw/secret-provider.d.ts.map +1 -1
- package/lib/types/outgoing-call-assets.d.ts.map +1 -1
- package/lib/types/plugin-update-install-state.d.ts.map +1 -1
- package/lib/types/plugin-update-state.d.ts.map +1 -1
- package/lib/types/plugin-update.d.ts +6 -0
- package/lib/types/plugin-update.d.ts.map +1 -1
- package/lib/types/private-filesystem.d.ts +5 -0
- package/lib/types/private-filesystem.d.ts.map +1 -0
- package/lib/types/sdk/index.d.ts +11 -2
- package/lib/types/sdk/index.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 +21 -2
- package/lib/types/services/chat-service.d.ts.map +1 -1
- package/lib/types/services/contact-service.d.ts +2 -0
- package/lib/types/services/contact-service.d.ts.map +1 -1
- package/lib/types/services/group-service.d.ts.map +1 -1
- package/lib/types/services/service.d.ts +5 -2
- package/lib/types/services/service.d.ts.map +1 -1
- package/lib/types/services/source-service.d.ts.map +1 -1
- package/lib/types/services/voiceprint-service.d.ts +79 -0
- package/lib/types/services/voiceprint-service.d.ts.map +1 -0
- package/lib/types/services/world-service.d.ts +2 -1
- package/lib/types/services/world-service.d.ts.map +1 -1
- package/lib/types/state-store.d.ts.map +1 -1
- package/lib/types/tools/business/conversation/index.d.ts.map +1 -1
- package/lib/types/tools/business/conversation/member-records.d.ts +3 -0
- package/lib/types/tools/business/conversation/member-records.d.ts.map +1 -0
- package/lib/types/tools/business/index.d.ts.map +1 -1
- package/lib/types/tools/business/voiceprint/grants.d.ts +2 -0
- package/lib/types/tools/business/voiceprint/grants.d.ts.map +1 -0
- package/lib/types/tools/business/voiceprint/index.d.ts +3 -0
- package/lib/types/tools/business/voiceprint/index.d.ts.map +1 -0
- package/lib/types/tools/business/voiceprint/invite.d.ts +2 -0
- package/lib/types/tools/business/voiceprint/invite.d.ts.map +1 -0
- package/lib/types/tools/business/voiceprint/recognized-invite.d.ts +2 -0
- package/lib/types/tools/business/voiceprint/recognized-invite.d.ts.map +1 -0
- package/lib/types/tools/business/voiceprint/recognized-people.d.ts +2 -0
- package/lib/types/tools/business/voiceprint/recognized-people.d.ts.map +1 -0
- package/lib/types/tools/business/voiceprint/restore.d.ts +2 -0
- package/lib/types/tools/business/voiceprint/restore.d.ts.map +1 -0
- package/lib/types/tools/business/voiceprint/revoke.d.ts +2 -0
- package/lib/types/tools/business/voiceprint/revoke.d.ts.map +1 -0
- package/lib/types/tools/business/voiceprint/status.d.ts +2 -0
- package/lib/types/tools/business/voiceprint/status.d.ts.map +1 -0
- package/lib/types/tools/extensions/index.d.ts.map +1 -1
- package/lib/types/tools/ports/conversations.d.ts +10 -1
- package/lib/types/tools/ports/conversations.d.ts.map +1 -1
- package/lib/types/tools/ports/index.d.ts +3 -2
- package/lib/types/tools/ports/index.d.ts.map +1 -1
- package/lib/types/tools/ports/voiceprint.d.ts +37 -0
- package/lib/types/tools/ports/voiceprint.d.ts.map +1 -0
- package/lib/types/tools/prompts/business.d.ts.map +1 -1
- package/lib/types/tools/registry/registrar.d.ts.map +1 -1
- package/lib/types/types.d.ts +113 -2
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/voiceprint-routes.d.ts +11 -0
- package/lib/types/voiceprint-routes.d.ts.map +1 -0
- package/lib/types/voiceprint-wav.d.ts +6 -0
- package/lib/types/voiceprint-wav.d.ts.map +1 -0
- package/package.json +4 -4
|
@@ -815,15 +815,16 @@ var ServiceRuntime = class {
|
|
|
815
815
|
else signal.addEventListener("abort", abort, { once: true });
|
|
816
816
|
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
817
817
|
try {
|
|
818
|
+
const multipart = body instanceof FormData;
|
|
818
819
|
const response = await this.fetchImpl(joinUrl(baseUrl, path), {
|
|
819
820
|
method: "POST",
|
|
820
821
|
headers: {
|
|
821
|
-
"Content-Type": "application/json",
|
|
822
|
+
...multipart ? {} : { "Content-Type": "application/json" },
|
|
822
823
|
"Accept-Language": "zh-CN",
|
|
823
824
|
Usersource: "3",
|
|
824
825
|
...bearer === void 0 ? {} : { Authorization: `Bearer ${bearer}` }
|
|
825
826
|
},
|
|
826
|
-
body: JSON.stringify(body),
|
|
827
|
+
body: multipart ? body : JSON.stringify(body),
|
|
827
828
|
signal: controller.signal
|
|
828
829
|
});
|
|
829
830
|
if (response.status === 401 || response.status === 403 && !preserveForbiddenError) throw new ArkmePluginError(`auth-http-${response.status}`, "Arkme 登录凭据已失效", false, response.status);
|
|
@@ -1033,6 +1034,17 @@ var ServiceRuntime = class {
|
|
|
1033
1034
|
return await this.post(this.config.audioBaseUrl, path, body, session.accessToken, [200], signal, false, requestOptions());
|
|
1034
1035
|
}
|
|
1035
1036
|
}
|
|
1037
|
+
async authenticatedAudioMultipartPost(path, body, initialSession, signal, options = {}) {
|
|
1038
|
+
let session = initialSession ?? await this.requireSession();
|
|
1039
|
+
const requestOptions = () => this.authenticatedRequestOptions(session, "audio", "write", options);
|
|
1040
|
+
try {
|
|
1041
|
+
return await this.post(this.config.audioBaseUrl, path, body, session.accessToken, [200], signal, false, requestOptions());
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (!(error instanceof ArkmePluginError) || !["auth-http-401", "auth-http-403"].includes(error.code)) throw error;
|
|
1044
|
+
session = await this.refreshAccessToken(session);
|
|
1045
|
+
return await this.post(this.config.audioBaseUrl, path, body, session.accessToken, [200], signal, false, requestOptions());
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1036
1048
|
async authenticatedRelationPost(path, body, initialSession, signal, options = {}) {
|
|
1037
1049
|
let session = initialSession ?? await this.requireSession();
|
|
1038
1050
|
const requestOptions = () => this.authenticatedRequestOptions(session, "relation", "interactive-read", options);
|
|
@@ -2482,10 +2494,10 @@ function booleanValue$10(value) {
|
|
|
2482
2494
|
function listValue$18(value) {
|
|
2483
2495
|
return Array.isArray(value) ? value : [];
|
|
2484
2496
|
}
|
|
2485
|
-
function encodeOpaqueJson$
|
|
2497
|
+
function encodeOpaqueJson$7(value) {
|
|
2486
2498
|
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
2487
2499
|
}
|
|
2488
|
-
function decodeOpaqueJson$
|
|
2500
|
+
function decodeOpaqueJson$7(value) {
|
|
2489
2501
|
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
2490
2502
|
}
|
|
2491
2503
|
var BotService = class {
|
|
@@ -2677,7 +2689,7 @@ var BotService = class {
|
|
|
2677
2689
|
};
|
|
2678
2690
|
}
|
|
2679
2691
|
async sealBotRef(userId, botId) {
|
|
2680
|
-
const payload = encodeOpaqueJson$
|
|
2692
|
+
const payload = encodeOpaqueJson$7({
|
|
2681
2693
|
version: 1,
|
|
2682
2694
|
userId,
|
|
2683
2695
|
botId
|
|
@@ -2693,7 +2705,7 @@ var BotService = class {
|
|
|
2693
2705
|
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false);
|
|
2694
2706
|
let raw;
|
|
2695
2707
|
try {
|
|
2696
|
-
raw = objectValue$1(decodeOpaqueJson$
|
|
2708
|
+
raw = objectValue$1(decodeOpaqueJson$7(payload));
|
|
2697
2709
|
} catch (error) {
|
|
2698
2710
|
throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false, 400, { cause: error });
|
|
2699
2711
|
}
|
|
@@ -3477,7 +3489,7 @@ var ChatRealtimeService = class {
|
|
|
3477
3489
|
lane: "background-read",
|
|
3478
3490
|
key: `projection:tail:${uid}:${String(afterSequence)}:${String(notificationAttempt)}`
|
|
3479
3491
|
});
|
|
3480
|
-
return [uid, await this.projectionReader.chatTimelineItems(data, session)];
|
|
3492
|
+
return [uid, await this.projectionReader.chatTimelineItems(data, session, uid)];
|
|
3481
3493
|
}))).forEach((result, index) => {
|
|
3482
3494
|
const uid = chunk[index]?.[0];
|
|
3483
3495
|
if (uid === void 0) return;
|
|
@@ -3577,6 +3589,18 @@ function booleanValue$8(value) {
|
|
|
3577
3589
|
function listValue$15(value) {
|
|
3578
3590
|
return Array.isArray(value) ? value : [];
|
|
3579
3591
|
}
|
|
3592
|
+
function chatMemberRole$1(value) {
|
|
3593
|
+
if (value === "owner" || value === 1) return "owner";
|
|
3594
|
+
if (value === "admin" || value === 2) return "admin";
|
|
3595
|
+
if (value === "member" || value === "participant" || value === 3) return "member";
|
|
3596
|
+
return "unknown";
|
|
3597
|
+
}
|
|
3598
|
+
function chatMemberStatus$1(value) {
|
|
3599
|
+
if (value === "active" || value === 1) return "active";
|
|
3600
|
+
if (value === "left" || value === 2) return "left";
|
|
3601
|
+
if (value === "removed" || value === 3) return "removed";
|
|
3602
|
+
return "unknown";
|
|
3603
|
+
}
|
|
3580
3604
|
function integerLikeValue(value) {
|
|
3581
3605
|
if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
|
|
3582
3606
|
if (typeof value === "string" && value.trim() !== "") {
|
|
@@ -3620,10 +3644,10 @@ function timelineAgentSource(relation, record, payload) {
|
|
|
3620
3644
|
label: agentSourceLabel(displayName)
|
|
3621
3645
|
};
|
|
3622
3646
|
}
|
|
3623
|
-
function encodeOpaqueJson$
|
|
3647
|
+
function encodeOpaqueJson$6(value) {
|
|
3624
3648
|
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
3625
3649
|
}
|
|
3626
|
-
function decodeOpaqueJson$
|
|
3650
|
+
function decodeOpaqueJson$6(value) {
|
|
3627
3651
|
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
3628
3652
|
}
|
|
3629
3653
|
var ChatService = class {
|
|
@@ -3647,6 +3671,65 @@ var ChatService = class {
|
|
|
3647
3671
|
this.aiPolish = aiPolish;
|
|
3648
3672
|
this.realtime = realtime;
|
|
3649
3673
|
}
|
|
3674
|
+
async listSourceMembers(sourceRef, options = {}) {
|
|
3675
|
+
const session = await this.runtime.requireSession();
|
|
3676
|
+
const source = await this.source.openSourceRef(sourceRef, session.userId);
|
|
3677
|
+
if (source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("chat-members-source-invalid", "仅支持查看群聊或私聊成员", false);
|
|
3678
|
+
const rawItems = await this.rawChatMembers(source.ownerRef, options.activeOnly !== false, session, options.signal);
|
|
3679
|
+
const members = await this.projectChatMembers(source.ownerRef, rawItems, session, options.signal);
|
|
3680
|
+
return {
|
|
3681
|
+
source: await this.source.sourceItem(source),
|
|
3682
|
+
items: members,
|
|
3683
|
+
total: members.length,
|
|
3684
|
+
activeCount: members.filter((item) => item.status === "active").length
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
async sourceMemberRecords(sourceRef, memberRef, mode, options = {}) {
|
|
3688
|
+
const session = await this.runtime.requireSession();
|
|
3689
|
+
const source = await this.source.openSourceRef(sourceRef, session.userId);
|
|
3690
|
+
if (source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("chat-members-source-invalid", "仅支持查看群聊或私聊成员快记", false);
|
|
3691
|
+
if (mode !== "owner" && mode !== "mentioned") throw new ArkmePluginError("chat-member-record-mode-invalid", "成员快记模式无效", false);
|
|
3692
|
+
const reference = await this.openChatMemberRef(memberRef, session.userId, source.ownerRef);
|
|
3693
|
+
const rawMembers = await this.rawChatMembers(source.ownerRef, false, session, options.signal);
|
|
3694
|
+
const member = (await this.projectChatMembers(source.ownerRef, rawMembers, session, options.signal)).find((item) => item.memberRef === memberRef);
|
|
3695
|
+
if (member === void 0) throw new ArkmePluginError("chat-member-ref-stale", "该成员已不属于当前会话,请刷新后重试", false, 409);
|
|
3696
|
+
const limit = Math.min(50, Math.max(1, Math.trunc(options.limit ?? 30)));
|
|
3697
|
+
const data = await this.runtime.authenticatedChatPost("/api/v1/chats/members/records/page", {
|
|
3698
|
+
chat_session_uid: source.ownerRef,
|
|
3699
|
+
member_user_id: reference.targetUserId,
|
|
3700
|
+
mode: mode === "owner" ? "sent" : "mentioned",
|
|
3701
|
+
before_seq: Math.max(0, Math.trunc(options.beforeSequence ?? 0)),
|
|
3702
|
+
limit
|
|
3703
|
+
}, session, options.signal, {
|
|
3704
|
+
lane: "interactive-read",
|
|
3705
|
+
key: `member-records:${source.ownerRef}:${String(reference.targetUserId)}:${mode}:${String(Math.max(0, Math.trunc(options.beforeSequence ?? 0)))}`,
|
|
3706
|
+
failureCooldownMs: 2e3
|
|
3707
|
+
});
|
|
3708
|
+
const items = await this.projectChatTimelineItems(listValue$15(data.items), source, session, options.signal);
|
|
3709
|
+
const nextBeforeSequence = Math.max(0, Math.trunc(numberValue$16(data.next_before_seq)));
|
|
3710
|
+
return {
|
|
3711
|
+
source: await this.source.sourceItem(source),
|
|
3712
|
+
member,
|
|
3713
|
+
mode,
|
|
3714
|
+
items,
|
|
3715
|
+
hasMore: data.has_more === true,
|
|
3716
|
+
...nextBeforeSequence > 0 ? { nextCursor: { beforeSequence: nextBeforeSequence } } : {}
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
async openPrivateChatFromMember(sourceRef, memberRef, options = {}) {
|
|
3720
|
+
const session = await this.runtime.requireSession();
|
|
3721
|
+
const source = await this.source.openSourceRef(sourceRef, session.userId);
|
|
3722
|
+
if (source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("chat-members-source-invalid", "仅支持从聊天成员发起私聊", false);
|
|
3723
|
+
const reference = await this.openChatMemberRef(memberRef, session.userId, source.ownerRef);
|
|
3724
|
+
if (reference.targetUserId === session.userId) throw new ArkmePluginError("private-chat-self-invalid", "不能给自己发起私聊", false, 409);
|
|
3725
|
+
const rawMember = (await this.rawChatMembers(source.ownerRef, true, session, options.signal)).find((item) => Math.trunc(numberValue$16(item.user_id)) === reference.targetUserId);
|
|
3726
|
+
if (rawMember === void 0) throw new ArkmePluginError("chat-member-ref-stale", "该成员已不属于当前会话,请刷新后重试", false, 409);
|
|
3727
|
+
const displayName = stringValue$1(rawMember.remark).trim() || stringValue$1(rawMember.display_name_snapshot).trim() || "群成员";
|
|
3728
|
+
return await this.openPrivateChatFromUser(reference.targetUserId, {
|
|
3729
|
+
displayName,
|
|
3730
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3650
3733
|
async openPrivateChatFromUser(peerUserId, options = {}) {
|
|
3651
3734
|
const session = await this.runtime.requireSession();
|
|
3652
3735
|
if (!Number.isSafeInteger(peerUserId) || peerUserId <= 0) throw new ArkmePluginError("private-chat-peer-invalid", "私聊用户参数无效", false);
|
|
@@ -3771,56 +3854,7 @@ var ChatService = class {
|
|
|
3771
3854
|
key: `timeline:${source.ownerRef}:${String(Math.max(0, Math.trunc(options.cursor?.beforeSequence ?? 0)))}:${String(limit)}`,
|
|
3772
3855
|
failureCooldownMs: 2e3
|
|
3773
3856
|
});
|
|
3774
|
-
const items =
|
|
3775
|
-
const senderUserIdByIndex = /* @__PURE__ */ new Map();
|
|
3776
|
-
const messageRefSigningKey = source.kind === "group_chat" ? await this.runtime.stateStore.uniqueCode() : void 0;
|
|
3777
|
-
for (const raw of listValue$15(data.items)) {
|
|
3778
|
-
const item = objectValue$1(raw);
|
|
3779
|
-
const relation = objectValue$1(item.relation);
|
|
3780
|
-
const record = objectValue$1(item.record);
|
|
3781
|
-
const payload = objectValue$1(record.payload);
|
|
3782
|
-
const recordStatus = numberValue$16(record.status);
|
|
3783
|
-
if (recordStatus !== 1) continue;
|
|
3784
|
-
const uid = stringValue$1(relation.record_uid ?? payload.record_uid).trim();
|
|
3785
|
-
if (uid === "") continue;
|
|
3786
|
-
const relationUid = stringValue$1(relation.rel_uid).trim();
|
|
3787
|
-
const senderUserId = numberValue$16(relation.sender_user_id);
|
|
3788
|
-
const aiPolish = this.aiPolish.timelineAiPolish(record, payload);
|
|
3789
|
-
const sendAtMillis = numberValue$16(relation.attach_at ?? payload.send_at);
|
|
3790
|
-
const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
|
|
3791
|
-
const rawAgentSource = timelineAgentSource(relation, record, payload);
|
|
3792
|
-
const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
|
|
3793
|
-
const itemIndex = items.push({
|
|
3794
|
-
itemUid: uid,
|
|
3795
|
-
...source.kind !== "group_chat" || relationUid === "" || senderUserId === session.userId ? {} : { messageRef: this.sealMessageRef(session.userId, source.ownerRef, relationUid, messageRefSigningKey) },
|
|
3796
|
-
senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
|
|
3797
|
-
...agentSource === void 0 ? {} : { agentSource },
|
|
3798
|
-
isMe: senderUserId === session.userId,
|
|
3799
|
-
sendAtMillis,
|
|
3800
|
-
title: stringValue$1(payload.title),
|
|
3801
|
-
textContent: stringValue$1(payload.text_content),
|
|
3802
|
-
status: recordStatus,
|
|
3803
|
-
sequence: numberValue$16(relation.seq),
|
|
3804
|
-
...numberValue$16(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$16(record.version ?? payload.version) } : {},
|
|
3805
|
-
...aiPolish === void 0 ? {} : { aiPolish },
|
|
3806
|
-
...forwardRecords === void 0 ? {} : { forwardRecords },
|
|
3807
|
-
templateKind: numberValue$16(payload.template_kind),
|
|
3808
|
-
displayKind: numberValue$16(payload.display_kind),
|
|
3809
|
-
version: numberValue$16(payload.version ?? record.version),
|
|
3810
|
-
updateAtMillis: numberValue$16(payload.update_at ?? record.update_at),
|
|
3811
|
-
recordDurationMillis: numberValue$16(payload.record_duration_millis),
|
|
3812
|
-
editDurationMillis: numberValue$16(payload.edit_duration_millis),
|
|
3813
|
-
contentBlocks: this.media.richContentBlocks(item, session.userId)
|
|
3814
|
-
}) - 1;
|
|
3815
|
-
if (Number.isSafeInteger(senderUserId) && senderUserId > 0) senderUserIdByIndex.set(itemIndex, senderUserId);
|
|
3816
|
-
}
|
|
3817
|
-
try {
|
|
3818
|
-
const profiles = await this.profile.publicProfilesByUserIds([...new Set(senderUserIdByIndex.values())], session, options.signal);
|
|
3819
|
-
for (const [index, senderUserId] of senderUserIdByIndex) {
|
|
3820
|
-
if (!profiles.has(senderUserId) || items[index] === void 0) continue;
|
|
3821
|
-
items[index].avatarRef = await this.profile.sealProfileImageRef(session.userId, senderUserId);
|
|
3822
|
-
}
|
|
3823
|
-
} catch {}
|
|
3857
|
+
const items = await this.projectChatTimelineItems(listValue$15(data.items), source, session, options.signal);
|
|
3824
3858
|
const beforeSequence = numberValue$16(data.next_before_seq);
|
|
3825
3859
|
let aiPolishSettings;
|
|
3826
3860
|
let aiPolishNotices;
|
|
@@ -3871,6 +3905,7 @@ var ChatService = class {
|
|
|
3871
3905
|
const recordUid = options.recordUid?.trim() || randomUUID();
|
|
3872
3906
|
if (source.kind === "send_to_self" || source.kind === "default_category") {
|
|
3873
3907
|
const result = await this.record.createTextForConversation(recordUid, text);
|
|
3908
|
+
if (result.localState !== "failed") this.source.invalidateSourceListCache(session.userId, "send_to_self");
|
|
3874
3909
|
return {
|
|
3875
3910
|
sourceRef,
|
|
3876
3911
|
itemUid: result.recordUid,
|
|
@@ -3888,6 +3923,7 @@ var ChatService = class {
|
|
|
3888
3923
|
text_content: text,
|
|
3889
3924
|
send_at: Date.now()
|
|
3890
3925
|
}, session);
|
|
3926
|
+
this.source.invalidateSourceListCache(session.userId, "send_to_self");
|
|
3891
3927
|
return {
|
|
3892
3928
|
sourceRef,
|
|
3893
3929
|
itemUid: stringValue$1(result.record_uid).trim() || recordUid,
|
|
@@ -3898,16 +3934,68 @@ var ChatService = class {
|
|
|
3898
3934
|
const relationUid = options.relationUid?.trim() || randomUUID();
|
|
3899
3935
|
const agentAuthored = isAgentAuthoredChatSend(options);
|
|
3900
3936
|
let sent;
|
|
3901
|
-
if (source.kind === "group_chat")
|
|
3902
|
-
|
|
3903
|
-
agentAuthored
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3937
|
+
if (source.kind === "group_chat") {
|
|
3938
|
+
if ((options.botRefs?.length ?? 0) > 0 && (options.humanMentions?.length ?? 0) > 0) throw new ArkmePluginError("mention-kind-conflict", "单条消息暂不支持同时新增真人和 Bot mention", false);
|
|
3939
|
+
if (options.botRefs !== void 0 && options.botRefs.length > 0) sent = await this.groupBotMentionSend(sourceRef, source, text, options.botRefs, recordUid, relationUid, session, options.signal, { agentAuthored });
|
|
3940
|
+
else if (options.humanMentions !== void 0 && options.humanMentions.length > 0) {
|
|
3941
|
+
const contentPayload = await this.humanMentionContentPayload(source, textContent, text, options.humanMentions, session, options.signal);
|
|
3942
|
+
sent = await this.sendChatSourceTextRaw(sourceRef, source.ownerRef, text, recordUid, relationUid, session, void 0, contentPayload, options.signal, { agentAuthored });
|
|
3943
|
+
} else sent = await this.aiPolish.sendGroupSourceTextWithAiPolish(sourceRef, source.ownerRef, text, recordUid, relationUid, session, {
|
|
3944
|
+
agentAuthored,
|
|
3945
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
3946
|
+
});
|
|
3947
|
+
} else if ((options.botRefs?.length ?? 0) > 0 || (options.humanMentions?.length ?? 0) > 0) throw new ArkmePluginError("mention-group-required", "Mention 只能发送到群聊", false);
|
|
3907
3948
|
else sent = await this.sendChatSourceTextRaw(sourceRef, source.ownerRef, text, recordUid, relationUid, session, void 0, void 0, options.signal, { agentAuthored });
|
|
3908
3949
|
if (agentAuthored && sent.localState === "synced") this.arko.agentSourceDisplayName(session);
|
|
3909
3950
|
return sent;
|
|
3910
3951
|
}
|
|
3952
|
+
async humanMentionContentPayload(source, rawText, normalizedText, inputs, session, signal) {
|
|
3953
|
+
if (inputs.length > 50) throw new ArkmePluginError("human-mention-invalid", "单条消息 mention 数量过多", false);
|
|
3954
|
+
const leadingTrim = rawText.length - rawText.trimStart().length;
|
|
3955
|
+
const rawMembers = await this.rawChatMembers(source.ownerRef, true, session, signal);
|
|
3956
|
+
const membersByUserId = new Map(rawMembers.map((item) => [Math.trunc(numberValue$16(item.user_id)), item]));
|
|
3957
|
+
const mentions = [];
|
|
3958
|
+
for (const input of [...inputs].sort((left, right) => left.startIndex - right.startIndex)) {
|
|
3959
|
+
const memberRef = input.memberRef.trim();
|
|
3960
|
+
const startIndex = Math.trunc(input.startIndex) - leadingTrim;
|
|
3961
|
+
const length = Math.trunc(input.length);
|
|
3962
|
+
if (memberRef === "" || startIndex < 0 || length < 2 || startIndex + length > normalizedText.length) throw new ArkmePluginError("human-mention-invalid", "真人 mention 引用或文本区间无效", false);
|
|
3963
|
+
const reference = await this.openChatMemberRef(memberRef, session.userId, source.ownerRef);
|
|
3964
|
+
if (reference.targetUserId === session.userId) throw new ArkmePluginError("human-mention-self-invalid", "不能 @ 自己", false);
|
|
3965
|
+
const rawMember = membersByUserId.get(reference.targetUserId);
|
|
3966
|
+
if (rawMember === void 0) throw new ArkmePluginError("chat-member-ref-stale", "被 @ 成员已不在当前群聊", false, 409);
|
|
3967
|
+
const visible = normalizedText.slice(startIndex, startIndex + length);
|
|
3968
|
+
const displayName = visible.startsWith("@") ? visible.slice(1).trim() : "";
|
|
3969
|
+
const expectedNames = [stringValue$1(rawMember.remark).trim(), stringValue$1(rawMember.display_name_snapshot).trim()].filter((value) => value !== "");
|
|
3970
|
+
if (displayName === "" || !expectedNames.includes(displayName)) throw new ArkmePluginError("human-mention-text-mismatch", "真人 mention 文本已变化,请重新选择成员", false, 409);
|
|
3971
|
+
const previous = mentions.at(-1);
|
|
3972
|
+
if (previous !== void 0 && previous.start_index + previous.length > startIndex) throw new ArkmePluginError("human-mention-overlap", "真人 mention 文本区间重叠", false);
|
|
3973
|
+
mentions.push({
|
|
3974
|
+
user_id: reference.targetUserId,
|
|
3975
|
+
display_name_snapshot: displayName,
|
|
3976
|
+
start_index: startIndex,
|
|
3977
|
+
length
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
const checksumInput = {
|
|
3981
|
+
text_content: normalizedText,
|
|
3982
|
+
human_mentions: mentions.map((mention) => ({
|
|
3983
|
+
user_id: mention.user_id,
|
|
3984
|
+
start_index: mention.start_index,
|
|
3985
|
+
length: mention.length
|
|
3986
|
+
})),
|
|
3987
|
+
bot_mentions: []
|
|
3988
|
+
};
|
|
3989
|
+
return {
|
|
3990
|
+
payload_kind: 2,
|
|
3991
|
+
schema_version: 1,
|
|
3992
|
+
mention_metadata: {
|
|
3993
|
+
schema_version: 1,
|
|
3994
|
+
source_checksum: createHash("sha256").update(JSON.stringify(checksumInput)).digest("hex"),
|
|
3995
|
+
human_mentions: mentions
|
|
3996
|
+
}
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3911
3999
|
async groupBotMentionSend(sourceRef, source, text, botRefs, recordUid, relationUid, session, signal, options = {}) {
|
|
3912
4000
|
const uniqueRefs = new Set(botRefs.map((ref) => ref.trim()));
|
|
3913
4001
|
if (uniqueRefs.has("") || uniqueRefs.size !== botRefs.length) throw new ArkmePluginError("bot-mention-ref-invalid", "Bot mention 引用为空或重复", false);
|
|
@@ -4006,7 +4094,7 @@ var ChatService = class {
|
|
|
4006
4094
|
const recordUid = options.recordUid?.trim() || randomUUID();
|
|
4007
4095
|
const relationUid = options.relationUid?.trim() || randomUUID();
|
|
4008
4096
|
const templateKind = longArticle ? 1 : assets.length === 0 ? 1 : 2;
|
|
4009
|
-
const
|
|
4097
|
+
const mediaContentPayload = assets.length === 0 ? void 0 : {
|
|
4010
4098
|
payload_kind: 2,
|
|
4011
4099
|
schema_version: 1,
|
|
4012
4100
|
text_state: textContent === "" ? 3 : 1,
|
|
@@ -4018,6 +4106,16 @@ var ChatService = class {
|
|
|
4018
4106
|
file_name: asset.fileName
|
|
4019
4107
|
}))
|
|
4020
4108
|
};
|
|
4109
|
+
let contentPayload = mediaContentPayload;
|
|
4110
|
+
if ((input.humanMentions?.length ?? 0) > 0) {
|
|
4111
|
+
if (source.kind !== "group_chat") throw new ArkmePluginError("mention-group-required", "Mention 只能发送到群聊", false);
|
|
4112
|
+
if (longArticle) throw new ArkmePluginError("human-mention-rich-invalid", "长文暂不支持成员 mention", false);
|
|
4113
|
+
const mentionPayload = await this.humanMentionContentPayload(source, input.textContent ?? "", textContent, input.humanMentions, session);
|
|
4114
|
+
contentPayload = {
|
|
4115
|
+
...mediaContentPayload ?? {},
|
|
4116
|
+
...mentionPayload
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4021
4119
|
const commonBody = {
|
|
4022
4120
|
record_uid: recordUid,
|
|
4023
4121
|
template_kind: templateKind,
|
|
@@ -4030,6 +4128,7 @@ var ChatService = class {
|
|
|
4030
4128
|
};
|
|
4031
4129
|
if (source.kind === "send_to_self" || source.kind === "default_category") {
|
|
4032
4130
|
const result = await this.runtime.authenticatedPost("/api/v1/records/create", commonBody, session);
|
|
4131
|
+
this.source.invalidateSourceListCache(session.userId, "send_to_self");
|
|
4033
4132
|
return {
|
|
4034
4133
|
sourceRef,
|
|
4035
4134
|
itemUid: stringValue$1(result.record_uid).trim() || recordUid,
|
|
@@ -4042,6 +4141,7 @@ var ChatService = class {
|
|
|
4042
4141
|
topic_uid: source.ownerRef,
|
|
4043
4142
|
...commonBody
|
|
4044
4143
|
}, session);
|
|
4144
|
+
this.source.invalidateSourceListCache(session.userId, "send_to_self");
|
|
4045
4145
|
return {
|
|
4046
4146
|
sourceRef,
|
|
4047
4147
|
itemUid: stringValue$1(result.record_uid).trim() || recordUid,
|
|
@@ -4165,7 +4265,7 @@ var ChatService = class {
|
|
|
4165
4265
|
async chatSourceFromBundle(bundle, session, cached, timelineItems) {
|
|
4166
4266
|
return await this.source.chatSourceFromBundle(bundle, session, cached, timelineItems);
|
|
4167
4267
|
}
|
|
4168
|
-
async chatTimelineItems(data, session) {
|
|
4268
|
+
async chatTimelineItems(data, session, chatSessionUid) {
|
|
4169
4269
|
const items = [];
|
|
4170
4270
|
for (const raw of listValue$15(data.items)) {
|
|
4171
4271
|
const item = objectValue$1(raw);
|
|
@@ -4182,6 +4282,7 @@ var ChatService = class {
|
|
|
4182
4282
|
const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
|
|
4183
4283
|
items.push({
|
|
4184
4284
|
itemUid: uid,
|
|
4285
|
+
...senderUserId > 0 ? { memberRef: await this.sealChatMemberRef(session.userId, chatSessionUid, senderUserId) } : {},
|
|
4185
4286
|
senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
|
|
4186
4287
|
...agentSource === void 0 ? {} : { agentSource },
|
|
4187
4288
|
...senderUserId > 0 ? { avatarRef: await this.profile.sealProfileImageRef(session.userId, senderUserId) } : {},
|
|
@@ -4249,8 +4350,143 @@ var ChatService = class {
|
|
|
4249
4350
|
items: projectedItems
|
|
4250
4351
|
};
|
|
4251
4352
|
}
|
|
4353
|
+
async rawChatMembers(chatSessionUid, activeOnly, session, signal) {
|
|
4354
|
+
return listValue$15((await this.runtime.authenticatedChatPost("/api/v1/chats/members/list", {
|
|
4355
|
+
chat_session_uid: chatSessionUid,
|
|
4356
|
+
active_only: activeOnly
|
|
4357
|
+
}, session, signal)).items).map(objectValue$1);
|
|
4358
|
+
}
|
|
4359
|
+
async projectChatMembers(chatSessionUid, rawItems, session, signal) {
|
|
4360
|
+
const userIds = rawItems.map((item) => Math.trunc(numberValue$16(item.user_id))).filter((userId) => Number.isSafeInteger(userId) && userId > 0);
|
|
4361
|
+
const profiles = await this.profile.publicProfileSummariesByUserIds(userIds, session, signal).catch(() => /* @__PURE__ */ new Map());
|
|
4362
|
+
const members = [];
|
|
4363
|
+
for (const item of rawItems) {
|
|
4364
|
+
const userId = Math.trunc(numberValue$16(item.user_id));
|
|
4365
|
+
if (!Number.isSafeInteger(userId) || userId <= 0) continue;
|
|
4366
|
+
const profile = profiles.get(userId);
|
|
4367
|
+
const remarkName = stringValue$1(item.remark).trim();
|
|
4368
|
+
const memberName = stringValue$1(item.display_name_snapshot).trim();
|
|
4369
|
+
const profileDisplayName = profile?.displayName.trim() ?? "";
|
|
4370
|
+
const publicDisplayName = profileDisplayName === `用户 ${String(userId)}` ? "" : profileDisplayName;
|
|
4371
|
+
const displayName = [
|
|
4372
|
+
remarkName,
|
|
4373
|
+
memberName,
|
|
4374
|
+
publicDisplayName
|
|
4375
|
+
].find((value) => value !== "" && value !== "成员" && value !== "群成员") ?? "群成员";
|
|
4376
|
+
const secondaryName = [
|
|
4377
|
+
memberName,
|
|
4378
|
+
publicDisplayName,
|
|
4379
|
+
remarkName
|
|
4380
|
+
].find((value) => value !== "" && value !== "成员" && value !== "群成员" && value !== displayName) ?? "";
|
|
4381
|
+
const role = chatMemberRole$1(item.role);
|
|
4382
|
+
const status = chatMemberStatus$1(item.status);
|
|
4383
|
+
const extra = objectValue$1(item.extra);
|
|
4384
|
+
members.push({
|
|
4385
|
+
memberRef: await this.sealChatMemberRef(session.userId, chatSessionUid, userId),
|
|
4386
|
+
displayName,
|
|
4387
|
+
...memberName === "" ? {} : { memberName },
|
|
4388
|
+
...secondaryName === "" ? {} : { secondaryName },
|
|
4389
|
+
...profile?.avatarUrl === void 0 ? {} : { avatarRef: await this.profile.sealProfileImageRef(session.userId, userId) },
|
|
4390
|
+
role,
|
|
4391
|
+
status,
|
|
4392
|
+
isSelf: userId === session.userId,
|
|
4393
|
+
isOwner: role === "owner",
|
|
4394
|
+
joinedAtMillis: Math.max(0, Math.trunc(numberValue$16(item.join_at))),
|
|
4395
|
+
recordCount: Math.max(0, Math.trunc(numberValue$16(extra.record_count))),
|
|
4396
|
+
mentionCount: Math.max(0, Math.trunc(numberValue$16(extra.mention_count)))
|
|
4397
|
+
});
|
|
4398
|
+
}
|
|
4399
|
+
const roleRank = (role) => role === "owner" ? 0 : role === "admin" ? 1 : role === "member" ? 2 : 3;
|
|
4400
|
+
members.sort((left, right) => roleRank(left.role) - roleRank(right.role) || (right.status === "active" ? 1 : 0) - (left.status === "active" ? 1 : 0) || left.joinedAtMillis - right.joinedAtMillis || left.displayName.localeCompare(right.displayName));
|
|
4401
|
+
return members;
|
|
4402
|
+
}
|
|
4403
|
+
async projectChatTimelineItems(rawItems, source, session, signal) {
|
|
4404
|
+
const items = [];
|
|
4405
|
+
const senderUserIdByIndex = /* @__PURE__ */ new Map();
|
|
4406
|
+
const signingKey = await this.runtime.stateStore.uniqueCode();
|
|
4407
|
+
for (const raw of rawItems) {
|
|
4408
|
+
const item = objectValue$1(raw);
|
|
4409
|
+
const relation = objectValue$1(item.relation);
|
|
4410
|
+
const record = objectValue$1(item.record);
|
|
4411
|
+
const payload = objectValue$1(record.payload);
|
|
4412
|
+
const recordStatus = numberValue$16(record.status);
|
|
4413
|
+
if (recordStatus !== 1) continue;
|
|
4414
|
+
const uid = stringValue$1(relation.record_uid ?? payload.record_uid).trim();
|
|
4415
|
+
if (uid === "") continue;
|
|
4416
|
+
const relationUid = stringValue$1(relation.rel_uid).trim();
|
|
4417
|
+
const senderUserId = Math.trunc(numberValue$16(relation.sender_user_id));
|
|
4418
|
+
const aiPolish = this.aiPolish.timelineAiPolish(record, payload);
|
|
4419
|
+
const sendAtMillis = numberValue$16(relation.attach_at ?? payload.send_at);
|
|
4420
|
+
const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
|
|
4421
|
+
const rawAgentSource = timelineAgentSource(relation, record, payload);
|
|
4422
|
+
const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
|
|
4423
|
+
const itemIndex = items.push({
|
|
4424
|
+
itemUid: uid,
|
|
4425
|
+
...source.kind !== "group_chat" || relationUid === "" || senderUserId === session.userId ? {} : { messageRef: this.sealMessageRef(session.userId, source.ownerRef, relationUid, signingKey) },
|
|
4426
|
+
...senderUserId > 0 ? { memberRef: await this.sealChatMemberRef(session.userId, source.ownerRef, senderUserId) } : {},
|
|
4427
|
+
senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
|
|
4428
|
+
...agentSource === void 0 ? {} : { agentSource },
|
|
4429
|
+
isMe: senderUserId === session.userId,
|
|
4430
|
+
sendAtMillis,
|
|
4431
|
+
title: stringValue$1(payload.title),
|
|
4432
|
+
textContent: stringValue$1(payload.text_content),
|
|
4433
|
+
status: recordStatus,
|
|
4434
|
+
sequence: numberValue$16(relation.seq),
|
|
4435
|
+
...numberValue$16(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$16(record.version ?? payload.version) } : {},
|
|
4436
|
+
...aiPolish === void 0 ? {} : { aiPolish },
|
|
4437
|
+
templateKind: numberValue$16(payload.template_kind),
|
|
4438
|
+
displayKind: numberValue$16(payload.display_kind),
|
|
4439
|
+
version: numberValue$16(payload.version ?? record.version),
|
|
4440
|
+
updateAtMillis: numberValue$16(payload.update_at ?? record.update_at),
|
|
4441
|
+
recordDurationMillis: numberValue$16(payload.record_duration_millis),
|
|
4442
|
+
editDurationMillis: numberValue$16(payload.edit_duration_millis),
|
|
4443
|
+
contentBlocks: this.media.richContentBlocks(item, session.userId),
|
|
4444
|
+
...forwardRecords === void 0 ? {} : { forwardRecords }
|
|
4445
|
+
}) - 1;
|
|
4446
|
+
if (senderUserId > 0) senderUserIdByIndex.set(itemIndex, senderUserId);
|
|
4447
|
+
}
|
|
4448
|
+
try {
|
|
4449
|
+
const profiles = await this.profile.publicProfilesByUserIds([...new Set(senderUserIdByIndex.values())], session, signal);
|
|
4450
|
+
for (const [index, senderUserId] of senderUserIdByIndex) {
|
|
4451
|
+
if (!profiles.has(senderUserId) || items[index] === void 0) continue;
|
|
4452
|
+
items[index].avatarRef = await this.profile.sealProfileImageRef(session.userId, senderUserId);
|
|
4453
|
+
}
|
|
4454
|
+
} catch {}
|
|
4455
|
+
return items;
|
|
4456
|
+
}
|
|
4457
|
+
async sealChatMemberRef(viewerUserId, chatSessionUid, targetUserId) {
|
|
4458
|
+
const encoded = encodeOpaqueJson$6({
|
|
4459
|
+
version: 1,
|
|
4460
|
+
viewerUserId,
|
|
4461
|
+
chatSessionUid,
|
|
4462
|
+
targetUserId
|
|
4463
|
+
});
|
|
4464
|
+
return `arkme-chat-member-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
|
|
4465
|
+
}
|
|
4466
|
+
async openChatMemberRef(memberRef, expectedViewerUserId, expectedChatSessionUid) {
|
|
4467
|
+
const parts = memberRef.trim().split(".");
|
|
4468
|
+
if (parts.length !== 3 || parts[0] !== "arkme-chat-member-v1") throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用无效", false);
|
|
4469
|
+
const encoded = parts[1] ?? "";
|
|
4470
|
+
const supplied = Buffer.from(parts[2] ?? "", "base64url");
|
|
4471
|
+
const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest();
|
|
4472
|
+
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用无效", false);
|
|
4473
|
+
let raw;
|
|
4474
|
+
try {
|
|
4475
|
+
raw = objectValue$1(decodeOpaqueJson$6(encoded));
|
|
4476
|
+
} catch (error) {
|
|
4477
|
+
throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用无效", false, 400, { cause: error });
|
|
4478
|
+
}
|
|
4479
|
+
const result = {
|
|
4480
|
+
version: 1,
|
|
4481
|
+
viewerUserId: Math.trunc(numberValue$16(raw.viewerUserId)),
|
|
4482
|
+
chatSessionUid: stringValue$1(raw.chatSessionUid).trim(),
|
|
4483
|
+
targetUserId: Math.trunc(numberValue$16(raw.targetUserId))
|
|
4484
|
+
};
|
|
4485
|
+
if (raw.version !== 1 || result.viewerUserId !== expectedViewerUserId || result.chatSessionUid !== expectedChatSessionUid || !Number.isSafeInteger(result.targetUserId) || result.targetUserId <= 0) throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用与当前账号或会话不匹配", false, 403);
|
|
4486
|
+
return result;
|
|
4487
|
+
}
|
|
4252
4488
|
sealMessageRef(userId, chatSessionUid, relationUid, signingKey) {
|
|
4253
|
-
const payload = encodeOpaqueJson$
|
|
4489
|
+
const payload = encodeOpaqueJson$6({
|
|
4254
4490
|
version: 1,
|
|
4255
4491
|
userId,
|
|
4256
4492
|
chatSessionUid,
|
|
@@ -4267,7 +4503,7 @@ var ChatService = class {
|
|
|
4267
4503
|
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false);
|
|
4268
4504
|
let parsed;
|
|
4269
4505
|
try {
|
|
4270
|
-
parsed = objectValue$1(decodeOpaqueJson$
|
|
4506
|
+
parsed = objectValue$1(decodeOpaqueJson$6(payload));
|
|
4271
4507
|
} catch (error) {
|
|
4272
4508
|
throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false, 400, { cause: error });
|
|
4273
4509
|
}
|
|
@@ -4398,6 +4634,19 @@ var ContactService = class {
|
|
|
4398
4634
|
source
|
|
4399
4635
|
};
|
|
4400
4636
|
}
|
|
4637
|
+
async resolveRegisteredContactUserId(contactRef, session, signal) {
|
|
4638
|
+
this.prune();
|
|
4639
|
+
const normalizedContactRef = contactRef.trim();
|
|
4640
|
+
if (!CONTACT_REF_PATTERN.test(normalizedContactRef)) throw new ArkmePluginError("contact-ref-invalid", "联系人搜索引用无效,请重新搜索", false);
|
|
4641
|
+
const cached = this.candidates.get(normalizedContactRef);
|
|
4642
|
+
if (cached === void 0 || cached.userId !== session.userId || cached.expiresAtMillis <= Date.now()) throw new ArkmePluginError("contact-ref-expired", "联系人搜索结果已过期,请重新搜索", false, 410);
|
|
4643
|
+
const candidate = await this.lookup(cached.normalized, session, signal, normalizedContactRef);
|
|
4644
|
+
if (candidate.targetUserId === void 0 || !candidate.result.registered || candidate.result.isSelf || candidate.targetUserId !== cached.targetUserId) {
|
|
4645
|
+
this.candidates.delete(normalizedContactRef);
|
|
4646
|
+
throw new ArkmePluginError("contact-candidate-changed", "联系人身份已发生变化,请重新搜索后确认", false, 409);
|
|
4647
|
+
}
|
|
4648
|
+
return candidate.targetUserId;
|
|
4649
|
+
}
|
|
4401
4650
|
async lookup(normalized, session, signal, contactRef = `arkme-contact-v1.${randomUUID()}`) {
|
|
4402
4651
|
const data = await this.runtime.authenticatedAuthPost("/api/v1/auth/search-contact-account", {
|
|
4403
4652
|
identifier_type: normalized.identifierType,
|
|
@@ -5204,10 +5453,10 @@ function numberValue$12(value) {
|
|
|
5204
5453
|
function listValue$12(value) {
|
|
5205
5454
|
return Array.isArray(value) ? value : [];
|
|
5206
5455
|
}
|
|
5207
|
-
function encodeOpaqueJson$
|
|
5456
|
+
function encodeOpaqueJson$5(value) {
|
|
5208
5457
|
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
5209
5458
|
}
|
|
5210
|
-
function decodeOpaqueJson$
|
|
5459
|
+
function decodeOpaqueJson$5(value) {
|
|
5211
5460
|
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
5212
5461
|
}
|
|
5213
5462
|
function chatMessageDnd$1(value) {
|
|
@@ -5502,7 +5751,7 @@ var GroupService = class {
|
|
|
5502
5751
|
};
|
|
5503
5752
|
}
|
|
5504
5753
|
async sealCandidateRef(payload) {
|
|
5505
|
-
const encoded = encodeOpaqueJson$
|
|
5754
|
+
const encoded = encodeOpaqueJson$5(payload);
|
|
5506
5755
|
return `arkme-group-candidate-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
|
|
5507
5756
|
}
|
|
5508
5757
|
async openCandidateRef(candidateRef, expectedUserId, expectedGroupChatSessionUid) {
|
|
@@ -5514,7 +5763,7 @@ var GroupService = class {
|
|
|
5514
5763
|
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false);
|
|
5515
5764
|
let raw;
|
|
5516
5765
|
try {
|
|
5517
|
-
raw = objectValue$1(decodeOpaqueJson$
|
|
5766
|
+
raw = objectValue$1(decodeOpaqueJson$5(encoded));
|
|
5518
5767
|
} catch (error) {
|
|
5519
5768
|
throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false, 400, { cause: error });
|
|
5520
5769
|
}
|
|
@@ -5593,7 +5842,8 @@ var GroupService = class {
|
|
|
5593
5842
|
isSelf: userId === session.userId,
|
|
5594
5843
|
isOwner: role === "owner",
|
|
5595
5844
|
joinedAtMillis: numberValue$12(item.join_at),
|
|
5596
|
-
recordCount: Math.max(0, Math.trunc(numberValue$12(objectValue$1(item.extra).record_count)))
|
|
5845
|
+
recordCount: Math.max(0, Math.trunc(numberValue$12(objectValue$1(item.extra).record_count))),
|
|
5846
|
+
mentionCount: Math.max(0, Math.trunc(numberValue$12(objectValue$1(item.extra).mention_count)))
|
|
5597
5847
|
});
|
|
5598
5848
|
}
|
|
5599
5849
|
const roleRank = (role) => role === "owner" ? 0 : role === "admin" ? 1 : role === "member" ? 2 : 3;
|
|
@@ -6799,10 +7049,10 @@ function chunksOf$1(values, size) {
|
|
|
6799
7049
|
for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
|
|
6800
7050
|
return chunks;
|
|
6801
7051
|
}
|
|
6802
|
-
function encodeOpaqueJson$
|
|
7052
|
+
function encodeOpaqueJson$4(value) {
|
|
6803
7053
|
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
6804
7054
|
}
|
|
6805
|
-
function decodeOpaqueJson$
|
|
7055
|
+
function decodeOpaqueJson$4(value) {
|
|
6806
7056
|
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
6807
7057
|
}
|
|
6808
7058
|
function arkmeIdAvailabilityReason(value) {
|
|
@@ -7145,7 +7395,7 @@ var ProfileService = class {
|
|
|
7145
7395
|
return profiles;
|
|
7146
7396
|
}
|
|
7147
7397
|
async sealProfileImageRef(viewerUserId, targetUserId) {
|
|
7148
|
-
const payload = encodeOpaqueJson$
|
|
7398
|
+
const payload = encodeOpaqueJson$4({
|
|
7149
7399
|
version: 1,
|
|
7150
7400
|
viewerUserId,
|
|
7151
7401
|
targetUserId
|
|
@@ -7161,7 +7411,7 @@ var ProfileService = class {
|
|
|
7161
7411
|
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("image-ref-invalid", "Arkme头像引用无效", false);
|
|
7162
7412
|
let parsed;
|
|
7163
7413
|
try {
|
|
7164
|
-
parsed = objectValue$1(decodeOpaqueJson$
|
|
7414
|
+
parsed = objectValue$1(decodeOpaqueJson$4(payload));
|
|
7165
7415
|
} catch (error) {
|
|
7166
7416
|
throw new ArkmePluginError("image-ref-invalid", "Arkme头像引用无效", false, 400, { cause: error });
|
|
7167
7417
|
}
|
|
@@ -7851,10 +8101,10 @@ function numberValue$5(value) {
|
|
|
7851
8101
|
function listValue$5(value) {
|
|
7852
8102
|
return Array.isArray(value) ? value : [];
|
|
7853
8103
|
}
|
|
7854
|
-
function encodeOpaqueJson$
|
|
8104
|
+
function encodeOpaqueJson$3(value) {
|
|
7855
8105
|
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
7856
8106
|
}
|
|
7857
|
-
function decodeOpaqueJson$
|
|
8107
|
+
function decodeOpaqueJson$3(value) {
|
|
7858
8108
|
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
7859
8109
|
}
|
|
7860
8110
|
function safeFailureMessage$2(error) {
|
|
@@ -7934,7 +8184,7 @@ var RecordingService = class {
|
|
|
7934
8184
|
}
|
|
7935
8185
|
async sealRecordingCursor(payload) {
|
|
7936
8186
|
const session = await this.runtime.requireSession();
|
|
7937
|
-
const encoded = encodeOpaqueJson$
|
|
8187
|
+
const encoded = encodeOpaqueJson$3(payload);
|
|
7938
8188
|
return `arkme-recording-cursor-v1.${encoded}.${createHmac("sha256", await this.recordingCursorKey(session.userId)).update(encoded).digest("base64url")}`;
|
|
7939
8189
|
}
|
|
7940
8190
|
async openRecordingCursor(cursor) {
|
|
@@ -7946,7 +8196,7 @@ var RecordingService = class {
|
|
|
7946
8196
|
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("recording-cursor-invalid", "录音分页游标无效", false);
|
|
7947
8197
|
let raw;
|
|
7948
8198
|
try {
|
|
7949
|
-
raw = objectValue$1(decodeOpaqueJson$
|
|
8199
|
+
raw = objectValue$1(decodeOpaqueJson$3(encoded));
|
|
7950
8200
|
} catch (error) {
|
|
7951
8201
|
throw new ArkmePluginError("recording-cursor-invalid", "录音分页游标无效", false, 400, { cause: error });
|
|
7952
8202
|
}
|
|
@@ -8498,10 +8748,10 @@ function booleanValue$1(value) {
|
|
|
8498
8748
|
function listValue$2(value) {
|
|
8499
8749
|
return Array.isArray(value) ? value : [];
|
|
8500
8750
|
}
|
|
8501
|
-
function encodeOpaqueJson$
|
|
8751
|
+
function encodeOpaqueJson$2(value) {
|
|
8502
8752
|
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
8503
8753
|
}
|
|
8504
|
-
function decodeOpaqueJson$
|
|
8754
|
+
function decodeOpaqueJson$2(value) {
|
|
8505
8755
|
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
8506
8756
|
}
|
|
8507
8757
|
function isSourceKind(value) {
|
|
@@ -8683,13 +8933,6 @@ var SourceService = class {
|
|
|
8683
8933
|
...defaultLatestPreview === "" ? {} : { latestPreview: defaultLatestPreview },
|
|
8684
8934
|
...defaultRecordCount === void 0 ? {} : { recordCount: defaultRecordCount }
|
|
8685
8935
|
};
|
|
8686
|
-
const aggregateSource = {
|
|
8687
|
-
sourceRef: await this.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
|
|
8688
|
-
kind: "send_to_self",
|
|
8689
|
-
displayName: "发给自己",
|
|
8690
|
-
activeAtMillis: 0,
|
|
8691
|
-
unreadCount: 0
|
|
8692
|
-
};
|
|
8693
8936
|
const topicDescriptors = [];
|
|
8694
8937
|
const seenTopicUids = /* @__PURE__ */ new Set();
|
|
8695
8938
|
const parentTopicUidByChild = /* @__PURE__ */ new Map();
|
|
@@ -8717,30 +8960,44 @@ var SourceService = class {
|
|
|
8717
8960
|
...parentTopicUid === "" || parentTopicUid === topicUid ? {} : { parentTopicUid },
|
|
8718
8961
|
title,
|
|
8719
8962
|
latestPreview: textPreview(latest),
|
|
8963
|
+
latestMessageAtMillis: numberValue$2(latest.send_at ?? summary.latest_send_at),
|
|
8720
8964
|
activeAtMillis: numberValue$2(latest.send_at ?? summary.latest_send_at ?? core.update_at),
|
|
8721
8965
|
recordCount: numberValue$2(summary.record_count)
|
|
8722
8966
|
});
|
|
8723
8967
|
}
|
|
8724
8968
|
const sourceRefByTopicUid = /* @__PURE__ */ new Map();
|
|
8725
8969
|
for (const topic of topicDescriptors) sourceRefByTopicUid.set(topic.topicUid, await this.sealSourceRef(session.userId, "topic", topic.topicUid, topic.title));
|
|
8970
|
+
const topics = topicDescriptors.map((topic) => {
|
|
8971
|
+
const parentSourceRef = topic.parentTopicUid === void 0 ? void 0 : sourceRefByTopicUid.get(topic.parentTopicUid);
|
|
8972
|
+
return {
|
|
8973
|
+
sourceRef: sourceRefByTopicUid.get(topic.topicUid),
|
|
8974
|
+
...parentSourceRef === void 0 ? {} : { parentSourceRef },
|
|
8975
|
+
kind: "topic",
|
|
8976
|
+
displayName: topic.title,
|
|
8977
|
+
...topic.latestPreview === "" ? {} : { latestPreview: topic.latestPreview },
|
|
8978
|
+
activeAtMillis: topic.activeAtMillis,
|
|
8979
|
+
unreadCount: 0,
|
|
8980
|
+
recordCount: topic.recordCount
|
|
8981
|
+
};
|
|
8982
|
+
});
|
|
8983
|
+
const latestAggregateItem = [defaultCategory, ...topics.map((source, index) => ({
|
|
8984
|
+
...source,
|
|
8985
|
+
activeAtMillis: topicDescriptors[index]?.latestMessageAtMillis ?? 0
|
|
8986
|
+
}))].reduce((latest, source) => latest === void 0 || source.activeAtMillis > latest.activeAtMillis ? source : latest, void 0);
|
|
8987
|
+
const aggregateLatestPreview = latestAggregateItem === void 0 || latestAggregateItem.activeAtMillis <= 0 ? "" : latestAggregateItem.latestPreview?.trim() || "非文本内容";
|
|
8726
8988
|
return {
|
|
8727
8989
|
directory,
|
|
8728
8990
|
items: [
|
|
8729
|
-
|
|
8991
|
+
{
|
|
8992
|
+
sourceRef: await this.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
|
|
8993
|
+
kind: "send_to_self",
|
|
8994
|
+
displayName: "发给自己",
|
|
8995
|
+
...aggregateLatestPreview === "" ? {} : { latestPreview: aggregateLatestPreview },
|
|
8996
|
+
activeAtMillis: latestAggregateItem?.activeAtMillis ?? 0,
|
|
8997
|
+
unreadCount: 0
|
|
8998
|
+
},
|
|
8730
8999
|
defaultCategory,
|
|
8731
|
-
...
|
|
8732
|
-
const parentSourceRef = topic.parentTopicUid === void 0 ? void 0 : sourceRefByTopicUid.get(topic.parentTopicUid);
|
|
8733
|
-
return {
|
|
8734
|
-
sourceRef: sourceRefByTopicUid.get(topic.topicUid),
|
|
8735
|
-
...parentSourceRef === void 0 ? {} : { parentSourceRef },
|
|
8736
|
-
kind: "topic",
|
|
8737
|
-
displayName: topic.title,
|
|
8738
|
-
...topic.latestPreview === "" ? {} : { latestPreview: topic.latestPreview },
|
|
8739
|
-
activeAtMillis: topic.activeAtMillis,
|
|
8740
|
-
unreadCount: 0,
|
|
8741
|
-
recordCount: topic.recordCount
|
|
8742
|
-
};
|
|
8743
|
-
})
|
|
9000
|
+
...topics
|
|
8744
9001
|
],
|
|
8745
9002
|
hasMore: false
|
|
8746
9003
|
};
|
|
@@ -8906,7 +9163,7 @@ var SourceService = class {
|
|
|
8906
9163
|
return `arkme-chat-source-v1.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`chat-source-key-v1:${String(userId)}:${chatSessionUid.trim()}`).digest("base64url")}`;
|
|
8907
9164
|
}
|
|
8908
9165
|
async sealSourceRef(userId, kind, ownerRef, displayName) {
|
|
8909
|
-
const payload = encodeOpaqueJson$
|
|
9166
|
+
const payload = encodeOpaqueJson$2({
|
|
8910
9167
|
version: 1,
|
|
8911
9168
|
userId,
|
|
8912
9169
|
kind,
|
|
@@ -8924,7 +9181,7 @@ var SourceService = class {
|
|
|
8924
9181
|
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("source-ref-invalid", "Arkme 数据源引用无效", false);
|
|
8925
9182
|
let parsed;
|
|
8926
9183
|
try {
|
|
8927
|
-
parsed = objectValue$1(decodeOpaqueJson$
|
|
9184
|
+
parsed = objectValue$1(decodeOpaqueJson$2(payload));
|
|
8928
9185
|
} catch (error) {
|
|
8929
9186
|
throw new ArkmePluginError("source-ref-invalid", "Arkme 数据源引用无效", false, 400, { cause: error });
|
|
8930
9187
|
}
|
|
@@ -8980,13 +9237,13 @@ var SourceService = class {
|
|
|
8980
9237
|
};
|
|
8981
9238
|
}
|
|
8982
9239
|
encodeCursor(value) {
|
|
8983
|
-
return `arkme-cursor-v1.${encodeOpaqueJson$
|
|
9240
|
+
return `arkme-cursor-v1.${encodeOpaqueJson$2(value)}`;
|
|
8984
9241
|
}
|
|
8985
9242
|
decodeCursor(cursor) {
|
|
8986
9243
|
const [prefix, payload, ...extra] = cursor.trim().split(".");
|
|
8987
9244
|
if (prefix !== "arkme-cursor-v1" || payload === void 0 || extra.length > 0) throw new ArkmePluginError("source-cursor-invalid", "Arkme 数据源分页游标无效", false);
|
|
8988
9245
|
try {
|
|
8989
|
-
const decoded = objectValue$1(decodeOpaqueJson$
|
|
9246
|
+
const decoded = objectValue$1(decodeOpaqueJson$2(payload));
|
|
8990
9247
|
if (Object.keys(decoded).length === 0) throw new Error("empty cursor");
|
|
8991
9248
|
return decoded;
|
|
8992
9249
|
} catch (error) {
|
|
@@ -9037,10 +9294,10 @@ function optionalString(value) {
|
|
|
9037
9294
|
const text = stringValue$1(value).trim();
|
|
9038
9295
|
return text === "" ? void 0 : text;
|
|
9039
9296
|
}
|
|
9040
|
-
function encodeOpaqueJson(value) {
|
|
9297
|
+
function encodeOpaqueJson$1(value) {
|
|
9041
9298
|
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
9042
9299
|
}
|
|
9043
|
-
function decodeOpaqueJson(value) {
|
|
9300
|
+
function decodeOpaqueJson$1(value) {
|
|
9044
9301
|
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
9045
9302
|
}
|
|
9046
9303
|
var WechatService = class {
|
|
@@ -9348,7 +9605,7 @@ var WechatService = class {
|
|
|
9348
9605
|
};
|
|
9349
9606
|
}
|
|
9350
9607
|
async sealWechatConversationRef(userId, importSessionKey) {
|
|
9351
|
-
const payload = encodeOpaqueJson({
|
|
9608
|
+
const payload = encodeOpaqueJson$1({
|
|
9352
9609
|
version: 1,
|
|
9353
9610
|
userId,
|
|
9354
9611
|
importSessionKey
|
|
@@ -9364,7 +9621,7 @@ var WechatService = class {
|
|
|
9364
9621
|
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("wechat-conversation-ref-invalid", "微信会话引用无效,请先重新查询微信会话列表", false);
|
|
9365
9622
|
let parsed;
|
|
9366
9623
|
try {
|
|
9367
|
-
parsed = objectValue$1(decodeOpaqueJson(payload));
|
|
9624
|
+
parsed = objectValue$1(decodeOpaqueJson$1(payload));
|
|
9368
9625
|
} catch (error) {
|
|
9369
9626
|
throw new ArkmePluginError("wechat-conversation-ref-invalid", "微信会话引用无效,请先重新查询微信会话列表", false, 400, { cause: error });
|
|
9370
9627
|
}
|
|
@@ -9377,7 +9634,7 @@ var WechatService = class {
|
|
|
9377
9634
|
return result;
|
|
9378
9635
|
}
|
|
9379
9636
|
async sealWechatCursor(userId, scope, offset) {
|
|
9380
|
-
const payload = encodeOpaqueJson({
|
|
9637
|
+
const payload = encodeOpaqueJson$1({
|
|
9381
9638
|
version: 1,
|
|
9382
9639
|
userId,
|
|
9383
9640
|
scope,
|
|
@@ -9395,7 +9652,7 @@ var WechatService = class {
|
|
|
9395
9652
|
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("wechat-cursor-invalid", "微信数据分页游标无效,请从第一页重新查询", false);
|
|
9396
9653
|
let parsed;
|
|
9397
9654
|
try {
|
|
9398
|
-
parsed = objectValue$1(decodeOpaqueJson(payload));
|
|
9655
|
+
parsed = objectValue$1(decodeOpaqueJson$1(payload));
|
|
9399
9656
|
} catch (error) {
|
|
9400
9657
|
throw new ArkmePluginError("wechat-cursor-invalid", "微信数据分页游标无效,请从第一页重新查询", false, 400, { cause: error });
|
|
9401
9658
|
}
|
|
@@ -9413,12 +9670,527 @@ var WechatService = class {
|
|
|
9413
9670
|
//#region src/types.ts
|
|
9414
9671
|
const ARKME_PROVIDER_CONTRACT_VERSION = 1;
|
|
9415
9672
|
const ARKME_DEFAULT_SHARE_WEBSITE = "https://app.arkme.ai";
|
|
9673
|
+
const ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS = 3e3;
|
|
9674
|
+
const ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS = 6e4;
|
|
9675
|
+
const ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES = 10 * 1024 * 1024;
|
|
9416
9676
|
const ARKME_WORLD_PUBLISH_MAX_IMAGES = 9;
|
|
9417
9677
|
const ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
9418
9678
|
//#endregion
|
|
9679
|
+
//#region src/voiceprint-wav.ts
|
|
9680
|
+
function ascii(bytes, offset, length) {
|
|
9681
|
+
return String.fromCharCode(...bytes.subarray(offset, offset + length));
|
|
9682
|
+
}
|
|
9683
|
+
function validateVoiceprintPcm16Wav(wav, declaredDurationMs) {
|
|
9684
|
+
if (wav.byteLength < 46 || !Number.isSafeInteger(declaredDurationMs) || declaredDurationMs <= 0) throw new TypeError("声纹 WAV 元数据无效");
|
|
9685
|
+
const view = new DataView(wav.buffer, wav.byteOffset, wav.byteLength);
|
|
9686
|
+
const sampleRate = view.getUint32(24, true);
|
|
9687
|
+
const dataBytes = view.getUint32(40, true);
|
|
9688
|
+
if (!(ascii(wav, 0, 4) === "RIFF" && view.getUint32(4, true) === wav.byteLength - 8 && ascii(wav, 8, 4) === "WAVE" && ascii(wav, 12, 4) === "fmt " && view.getUint32(16, true) === 16 && view.getUint16(20, true) === 1 && view.getUint16(22, true) === 1 && sampleRate >= 8e3 && sampleRate <= 192e3 && view.getUint32(28, true) === sampleRate * 2 && view.getUint16(32, true) === 2 && view.getUint16(34, true) === 16 && ascii(wav, 36, 4) === "data" && dataBytes > 0 && dataBytes === wav.byteLength - 44)) throw new TypeError("声纹录音必须是单声道 PCM16 WAV");
|
|
9689
|
+
const durationMs = Math.round(dataBytes * 1e3 / (sampleRate * 2));
|
|
9690
|
+
if (Math.abs(durationMs - declaredDurationMs) > 250) throw new TypeError("声纹 WAV 时长与声明不一致");
|
|
9691
|
+
return {
|
|
9692
|
+
sampleRate,
|
|
9693
|
+
durationMs
|
|
9694
|
+
};
|
|
9695
|
+
}
|
|
9696
|
+
//#endregion
|
|
9697
|
+
//#region src/services/voiceprint-service.ts
|
|
9698
|
+
const GRANT_REF_PREFIX = "arkme-voiceprint-grant-v1";
|
|
9699
|
+
const PERSON_REF_PREFIX = "arkme-voiceprint-person-v1";
|
|
9700
|
+
const SPEAKER_ID_PATTERN = /^[0-9a-f]{24}$/;
|
|
9701
|
+
const SHORT_INVITE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
9702
|
+
function encodeOpaqueJson(value) {
|
|
9703
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
9704
|
+
}
|
|
9705
|
+
function decodeOpaqueJson(value) {
|
|
9706
|
+
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
9707
|
+
}
|
|
9708
|
+
function integerValue$1(value) {
|
|
9709
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : NaN;
|
|
9710
|
+
}
|
|
9711
|
+
function requiredBoolean(value, code) {
|
|
9712
|
+
if (typeof value !== "boolean") throw contractError(code);
|
|
9713
|
+
return value;
|
|
9714
|
+
}
|
|
9715
|
+
function requiredInteger(value, code, minimum = 0) {
|
|
9716
|
+
const parsed = integerValue$1(value);
|
|
9717
|
+
if (!Number.isSafeInteger(parsed) || parsed < minimum) throw contractError(code);
|
|
9718
|
+
return parsed;
|
|
9719
|
+
}
|
|
9720
|
+
function requiredString(value, code) {
|
|
9721
|
+
if (typeof value !== "string") throw contractError(code);
|
|
9722
|
+
return value;
|
|
9723
|
+
}
|
|
9724
|
+
function contractError(code) {
|
|
9725
|
+
return new ArkmePluginError(code, "声纹服务返回的数据不完整,请稍后重试", true, 502);
|
|
9726
|
+
}
|
|
9727
|
+
function validOpaqueText(value, maxBytes) {
|
|
9728
|
+
return value === value.trim() && Buffer.byteLength(value, "utf8") <= maxBytes && !/[\u0000-\u001f\u007f]/u.test(value);
|
|
9729
|
+
}
|
|
9730
|
+
function validSpeakerId(value) {
|
|
9731
|
+
return SPEAKER_ID_PATTERN.test(value) && value !== "000000000000000000000000";
|
|
9732
|
+
}
|
|
9733
|
+
var VoiceprintService = class {
|
|
9734
|
+
runtime;
|
|
9735
|
+
profile;
|
|
9736
|
+
inviteTarget;
|
|
9737
|
+
constructor(runtime, profile, inviteTarget) {
|
|
9738
|
+
this.runtime = runtime;
|
|
9739
|
+
this.profile = profile;
|
|
9740
|
+
this.inviteTarget = inviteTarget;
|
|
9741
|
+
}
|
|
9742
|
+
async myVoiceprint(options = {}) {
|
|
9743
|
+
const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/my", {}, void 0, options.signal, {
|
|
9744
|
+
key: "voiceprint:self",
|
|
9745
|
+
cacheMs: 3e3,
|
|
9746
|
+
failureCooldownMs: 2e3
|
|
9747
|
+
});
|
|
9748
|
+
const hasVoiceprint = requiredBoolean(data.has_voiceprint, "voiceprint-self-contract-invalid");
|
|
9749
|
+
const canIdentify = requiredBoolean(data.can_identify, "voiceprint-self-contract-invalid");
|
|
9750
|
+
const canPlay = requiredBoolean(data.can_play, "voiceprint-self-contract-invalid");
|
|
9751
|
+
const canRestorePlayback = requiredBoolean(data.can_restore_playback, "voiceprint-self-contract-invalid");
|
|
9752
|
+
const rawStatus = requiredString(data.enrollment_status ?? "none", "voiceprint-self-contract-invalid").trim();
|
|
9753
|
+
if (![
|
|
9754
|
+
"none",
|
|
9755
|
+
"processing",
|
|
9756
|
+
"ready"
|
|
9757
|
+
].includes(rawStatus)) throw contractError("voiceprint-self-contract-invalid");
|
|
9758
|
+
const enrollmentStatus = rawStatus;
|
|
9759
|
+
const pendingSessionId = requiredString(data.pending_session_id ?? "", "voiceprint-self-contract-invalid").trim();
|
|
9760
|
+
const nickname = requiredString(data.nick_name ?? "", "voiceprint-self-contract-invalid").trim();
|
|
9761
|
+
const updatedAtMillis = requiredInteger(data.updated_at ?? 0, "voiceprint-self-contract-invalid");
|
|
9762
|
+
if (!validOpaqueText(pendingSessionId, 2048) || !hasVoiceprint && (canIdentify || canPlay || canRestorePlayback) || canPlay && !hasVoiceprint) throw contractError("voiceprint-self-contract-invalid");
|
|
9763
|
+
return {
|
|
9764
|
+
hasVoiceprint,
|
|
9765
|
+
nickname,
|
|
9766
|
+
updatedAtMillis,
|
|
9767
|
+
canIdentify,
|
|
9768
|
+
canPlay,
|
|
9769
|
+
canRestorePlayback,
|
|
9770
|
+
enrollmentStatus,
|
|
9771
|
+
enrollmentPending: enrollmentStatus === "processing" || pendingSessionId !== ""
|
|
9772
|
+
};
|
|
9773
|
+
}
|
|
9774
|
+
async outboundGrants(input, options = {}) {
|
|
9775
|
+
if (!validOpaqueText(input.cursor, 512) || !Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new ArkmePluginError("voiceprint-grant-page-invalid", "声纹授权分页参数无效", false);
|
|
9776
|
+
const session = await this.runtime.requireSession();
|
|
9777
|
+
const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/grants", {
|
|
9778
|
+
direction: "outbound",
|
|
9779
|
+
cursor: input.cursor,
|
|
9780
|
+
limit: input.limit
|
|
9781
|
+
}, session, options.signal, {
|
|
9782
|
+
key: `voiceprint:grants:${input.cursor}:${String(input.limit)}`,
|
|
9783
|
+
failureCooldownMs: 2e3
|
|
9784
|
+
});
|
|
9785
|
+
const rawItems = data.grant_ls;
|
|
9786
|
+
if (!Array.isArray(rawItems)) throw contractError("voiceprint-grant-contract-invalid");
|
|
9787
|
+
const grants = rawItems.map((raw) => this.parseOutboundGrant(objectValue$1(raw), session.userId));
|
|
9788
|
+
const granteeUserIds = [...new Set(grants.map((item) => item.granteeUserId))];
|
|
9789
|
+
const profiles = await this.loadPublicProfiles(granteeUserIds, session, options.signal);
|
|
9790
|
+
const items = [];
|
|
9791
|
+
for (const grant of grants) {
|
|
9792
|
+
const publicProfile = profiles.get(grant.granteeUserId);
|
|
9793
|
+
const avatarRef = publicProfile?.avatarUrl === void 0 ? void 0 : await this.optionalAvatarRef(session.userId, grant.granteeUserId);
|
|
9794
|
+
items.push({
|
|
9795
|
+
grantRef: await this.sealRef(GRANT_REF_PREFIX, {
|
|
9796
|
+
version: 1,
|
|
9797
|
+
viewerUserId: session.userId,
|
|
9798
|
+
granteeUserId: grant.granteeUserId
|
|
9799
|
+
}),
|
|
9800
|
+
displayName: publicProfile?.displayName.trim() || "用户资料不可用",
|
|
9801
|
+
...avatarRef === void 0 ? {} : { avatarRef },
|
|
9802
|
+
identifyEnabled: grant.identifyEnabled,
|
|
9803
|
+
playEnabled: grant.playEnabled,
|
|
9804
|
+
grantedAtMillis: grant.grantedAtMillis,
|
|
9805
|
+
updatedAtMillis: grant.updatedAtMillis
|
|
9806
|
+
});
|
|
9807
|
+
}
|
|
9808
|
+
const nextCursor = requiredString(data.next_cursor, "voiceprint-grant-contract-invalid");
|
|
9809
|
+
const hasMore = requiredBoolean(data.has_more, "voiceprint-grant-contract-invalid");
|
|
9810
|
+
if (!validOpaqueText(nextCursor, 512) || hasMore !== (nextCursor !== "")) throw contractError("voiceprint-grant-contract-invalid");
|
|
9811
|
+
return {
|
|
9812
|
+
items,
|
|
9813
|
+
nextCursor,
|
|
9814
|
+
hasMore
|
|
9815
|
+
};
|
|
9816
|
+
}
|
|
9817
|
+
async recognizedPeople(input, options = {}) {
|
|
9818
|
+
if (!validOpaqueText(input.cursor, 2048) || !Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 50) throw new ArkmePluginError("voiceprint-person-page-invalid", "已识别人分页参数无效", false);
|
|
9819
|
+
const session = await this.runtime.requireSession();
|
|
9820
|
+
const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/recognized-speakers/list", {
|
|
9821
|
+
cursor: input.cursor,
|
|
9822
|
+
limit: input.limit
|
|
9823
|
+
}, session, options.signal, {
|
|
9824
|
+
key: `voiceprint:people:${input.cursor}:${String(input.limit)}`,
|
|
9825
|
+
failureCooldownMs: 2e3
|
|
9826
|
+
});
|
|
9827
|
+
requiredBoolean(data.capability_enabled, "voiceprint-person-contract-invalid");
|
|
9828
|
+
const rawItems = data.items;
|
|
9829
|
+
if (!Array.isArray(rawItems)) throw contractError("voiceprint-person-contract-invalid");
|
|
9830
|
+
const projections = rawItems.map((raw) => this.parseRecognizedPerson(objectValue$1(raw)));
|
|
9831
|
+
if (projections.some((item) => item.targetUserId === session.userId)) throw contractError("voiceprint-person-contract-invalid");
|
|
9832
|
+
const targetUserIds = [...new Set(projections.map((item) => item.targetUserId).filter((userId) => userId > 0))];
|
|
9833
|
+
const profiles = await this.loadPublicProfiles(targetUserIds, session, options.signal);
|
|
9834
|
+
const items = await Promise.all(projections.map(async (item) => await this.projectRecognizedPerson(item, session, profiles)));
|
|
9835
|
+
if (new Set(items.map((item) => item.personRef)).size !== items.length) throw contractError("voiceprint-person-contract-invalid");
|
|
9836
|
+
let reachedUnauthorized = false;
|
|
9837
|
+
for (const item of items) if (!item.playGranted) reachedUnauthorized = true;
|
|
9838
|
+
else if (reachedUnauthorized) throw contractError("voiceprint-person-contract-invalid");
|
|
9839
|
+
const nextCursor = requiredString(data.next_cursor, "voiceprint-person-contract-invalid");
|
|
9840
|
+
const hasMore = requiredBoolean(data.has_more, "voiceprint-person-contract-invalid");
|
|
9841
|
+
if (!validOpaqueText(nextCursor, 2048) || hasMore !== (nextCursor !== "")) throw contractError("voiceprint-person-contract-invalid");
|
|
9842
|
+
return {
|
|
9843
|
+
items,
|
|
9844
|
+
nextCursor,
|
|
9845
|
+
hasMore
|
|
9846
|
+
};
|
|
9847
|
+
}
|
|
9848
|
+
async recognizedPerson(personRef, options = {}) {
|
|
9849
|
+
const session = await this.runtime.requireSession();
|
|
9850
|
+
const reference = await this.openPersonRef(personRef, session.userId);
|
|
9851
|
+
return (await this.loadRecognizedPerson(reference, personRef, session, options)).detail;
|
|
9852
|
+
}
|
|
9853
|
+
async loadRecognizedPerson(reference, personRef, session, options) {
|
|
9854
|
+
const selector = reference.identityKind === "speaker" ? { speaker_id: reference.speakerId } : { target_user_id: reference.targetUserId };
|
|
9855
|
+
let data;
|
|
9856
|
+
try {
|
|
9857
|
+
data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/recognized-speakers/detail", selector, session, options.signal, {
|
|
9858
|
+
key: `voiceprint:person:${personRef}`,
|
|
9859
|
+
failureCooldownMs: 2e3
|
|
9860
|
+
});
|
|
9861
|
+
} catch (error) {
|
|
9862
|
+
if (error instanceof ArkmePluginError && error.code === "arkme-code-1003") throw new ArkmePluginError("voiceprint-person-unavailable", "这条已识别人已不可访问,请刷新列表", false, 410, { cause: error });
|
|
9863
|
+
throw error;
|
|
9864
|
+
}
|
|
9865
|
+
const projection = this.parseRecognizedPerson(data);
|
|
9866
|
+
if (reference.identityKind === "speaker" && projection.speakerId !== reference.speakerId || reference.identityKind === "authorized_user" && projection.targetUserId !== reference.targetUserId || projection.targetUserId === session.userId) throw contractError("voiceprint-person-contract-invalid");
|
|
9867
|
+
const profiles = await this.loadPublicProfiles(projection.targetUserId > 0 ? [projection.targetUserId] : [], session, options.signal);
|
|
9868
|
+
if (requiredBoolean(data.claim_required, "voiceprint-person-contract-invalid") !== (projection.speakerId !== "" && projection.targetUserId === 0)) throw contractError("voiceprint-person-contract-invalid");
|
|
9869
|
+
return {
|
|
9870
|
+
detail: await this.projectRecognizedPerson(projection, session, profiles),
|
|
9871
|
+
targetUserId: projection.targetUserId
|
|
9872
|
+
};
|
|
9873
|
+
}
|
|
9874
|
+
async recognizedPersonVoiceprints(personRef, options = {}) {
|
|
9875
|
+
const session = await this.runtime.requireSession();
|
|
9876
|
+
const reference = await this.openPersonRef(personRef, session.userId);
|
|
9877
|
+
if (reference.identityKind !== "speaker") throw new ArkmePluginError("voiceprint-library-unavailable", "只有录音中识别到的声音可以查看声纹记录", false, 409);
|
|
9878
|
+
const selector = { speaker_id: reference.speakerId };
|
|
9879
|
+
const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/recognized-speakers/voiceprints/list", selector, session, options.signal, {
|
|
9880
|
+
key: `voiceprint:library:${personRef}`,
|
|
9881
|
+
failureCooldownMs: 2e3
|
|
9882
|
+
});
|
|
9883
|
+
if (!Array.isArray(data.items)) throw contractError("voiceprint-library-contract-invalid");
|
|
9884
|
+
const items = [];
|
|
9885
|
+
const refs = /* @__PURE__ */ new Set();
|
|
9886
|
+
let reachedLocal = false;
|
|
9887
|
+
let previousHitCount;
|
|
9888
|
+
let previousAuthorized;
|
|
9889
|
+
for (const raw of data.items) {
|
|
9890
|
+
const item = objectValue$1(raw);
|
|
9891
|
+
const voiceprintRef = requiredString(item.voiceprint_id, "voiceprint-library-contract-invalid");
|
|
9892
|
+
const kindCode = requiredInteger(item.kind, "voiceprint-library-contract-invalid", 1);
|
|
9893
|
+
const authorized = requiredBoolean(item.is_authorized, "voiceprint-library-contract-invalid");
|
|
9894
|
+
const hitCount = requiredInteger(item.hit_count, "voiceprint-library-contract-invalid");
|
|
9895
|
+
const createdAtMillis = item.created_at === void 0 ? void 0 : requiredInteger(item.created_at, "voiceprint-library-contract-invalid", 1);
|
|
9896
|
+
if (!validOpaqueText(voiceprintRef, 2048) || voiceprintRef === "" || refs.has(voiceprintRef) || ![
|
|
9897
|
+
1,
|
|
9898
|
+
2,
|
|
9899
|
+
3
|
|
9900
|
+
].includes(kindCode) || authorized !== (kindCode === 3) || authorized && hitCount !== 0) throw contractError("voiceprint-library-contract-invalid");
|
|
9901
|
+
if (!authorized) reachedLocal = true;
|
|
9902
|
+
else if (reachedLocal) throw contractError("voiceprint-library-contract-invalid");
|
|
9903
|
+
if (previousAuthorized === authorized && previousHitCount !== void 0 && hitCount > previousHitCount) throw contractError("voiceprint-library-contract-invalid");
|
|
9904
|
+
refs.add(voiceprintRef);
|
|
9905
|
+
previousAuthorized = authorized;
|
|
9906
|
+
previousHitCount = hitCount;
|
|
9907
|
+
items.push({
|
|
9908
|
+
kind: kindCode === 3 ? "authorized" : kindCode === 2 ? "legacy" : "local",
|
|
9909
|
+
hitCount,
|
|
9910
|
+
...createdAtMillis === void 0 ? {} : { createdAtMillis }
|
|
9911
|
+
});
|
|
9912
|
+
}
|
|
9913
|
+
return { items };
|
|
9914
|
+
}
|
|
9915
|
+
async createInvitation(options = {}) {
|
|
9916
|
+
const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/invites/create", { scope: 2 }, void 0, options.signal, {
|
|
9917
|
+
lane: "write",
|
|
9918
|
+
key: `voiceprint:invite:${Date.now().toString(36)}`,
|
|
9919
|
+
bypassCache: true
|
|
9920
|
+
});
|
|
9921
|
+
const inviteToken = requiredString(data.invite_token, "voiceprint-invite-contract-invalid").trim();
|
|
9922
|
+
const previewToken = requiredString(data.preview_token, "voiceprint-invite-contract-invalid").trim();
|
|
9923
|
+
const shortInviteToken = stringValue$1(data.short_invite_token).trim();
|
|
9924
|
+
const shortPreviewToken = stringValue$1(data.short_preview_token).trim();
|
|
9925
|
+
const expiresAtMillis = requiredInteger(data.expires_at, "voiceprint-invite-contract-invalid", 1);
|
|
9926
|
+
if (requiredInteger(data.scope, "voiceprint-invite-contract-invalid", 1) !== 2 || !validOpaqueText(inviteToken, 2048) || inviteToken === "" || !validOpaqueText(previewToken, 2048) || previewToken === "") throw contractError("voiceprint-invite-contract-invalid");
|
|
9927
|
+
const useShortPair = SHORT_INVITE_TOKEN_PATTERN.test(shortInviteToken) && SHORT_INVITE_TOKEN_PATTERN.test(shortPreviewToken);
|
|
9928
|
+
const token = useShortPair ? shortInviteToken : inviteToken;
|
|
9929
|
+
const preview = useShortPair ? shortPreviewToken : previewToken;
|
|
9930
|
+
return {
|
|
9931
|
+
inviteUrl: this.invitationUrl(token, preview, useShortPair),
|
|
9932
|
+
expiresAtMillis
|
|
9933
|
+
};
|
|
9934
|
+
}
|
|
9935
|
+
async createRecognizedPersonInvitation(personRef, targetContactRef, options = {}) {
|
|
9936
|
+
const session = await this.runtime.requireSession();
|
|
9937
|
+
const reference = await this.openPersonRef(personRef, session.userId);
|
|
9938
|
+
if (reference.identityKind !== "speaker") throw new ArkmePluginError("voiceprint-person-invite-unavailable", "授权用户投影不能作为待认领声纹邀请", false, 409);
|
|
9939
|
+
const current = await this.loadRecognizedPerson(reference, personRef, session, options);
|
|
9940
|
+
if (!current.detail.canInvite || current.detail.playGranted) throw new ArkmePluginError("voiceprint-person-invite-unavailable", "这条已识别人当前不能生成邀请", false, 409);
|
|
9941
|
+
const normalizedContactRef = targetContactRef?.trim() ?? "";
|
|
9942
|
+
const targetSelectionRequired = current.targetUserId === 0;
|
|
9943
|
+
if (!targetSelectionRequired && normalizedContactRef !== "") throw new ArkmePluginError("voiceprint-invite-target-unexpected", "这条已识别人已绑定用户,无需重新选择邀请对象", false);
|
|
9944
|
+
let targetUserId = current.targetUserId;
|
|
9945
|
+
if (targetSelectionRequired) {
|
|
9946
|
+
if (this.inviteTarget === void 0) throw new ArkmePluginError("voiceprint-invite-target-unavailable", "当前运行时无法选择邀请对象", false, 503);
|
|
9947
|
+
if (!validOpaqueText(normalizedContactRef, 256) || normalizedContactRef === "") throw new ArkmePluginError("voiceprint-invite-target-invalid", "邀请对象引用无效,请重新搜索", false);
|
|
9948
|
+
targetUserId = await this.inviteTarget.resolveRegisteredContactUserId(normalizedContactRef, session, options.signal);
|
|
9949
|
+
if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0 || targetUserId === session.userId) throw new ArkmePluginError("voiceprint-invite-target-invalid", "请选择其他已注册的 Arkme 用户", false);
|
|
9950
|
+
}
|
|
9951
|
+
const request = targetSelectionRequired ? {
|
|
9952
|
+
speaker_id: reference.speakerId,
|
|
9953
|
+
target_user_id: targetUserId
|
|
9954
|
+
} : { speaker_id: reference.speakerId };
|
|
9955
|
+
const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/recognized-speakers/invites/create", request, session, options.signal, {
|
|
9956
|
+
lane: "write",
|
|
9957
|
+
key: `voiceprint:person-invite:${personRef}:${normalizedContactRef || "bound"}`,
|
|
9958
|
+
bypassCache: true
|
|
9959
|
+
});
|
|
9960
|
+
const inviteToken = requiredString(data.invite_token, "voiceprint-person-invite-contract-invalid").trim();
|
|
9961
|
+
const previewToken = requiredString(data.preview_token, "voiceprint-person-invite-contract-invalid").trim();
|
|
9962
|
+
const shortInviteToken = stringValue$1(data.short_invite_token).trim();
|
|
9963
|
+
const shortPreviewToken = stringValue$1(data.short_preview_token).trim();
|
|
9964
|
+
const expiresAtMillis = requiredInteger(data.expires_at, "voiceprint-person-invite-contract-invalid", 1);
|
|
9965
|
+
if (requiredInteger(data.token_version, "voiceprint-person-invite-contract-invalid", 1) !== 2 || requiredString(data.speaker_id, "voiceprint-person-invite-contract-invalid").trim().toLowerCase() !== reference.speakerId || requiredInteger(data.target_user_id, "voiceprint-person-invite-contract-invalid", 1) !== targetUserId || requiredBoolean(data.claim_required, "voiceprint-person-invite-contract-invalid") !== targetSelectionRequired || requiredInteger(data.scope, "voiceprint-person-invite-contract-invalid", 1) !== 2 || !validOpaqueText(inviteToken, 2048) || inviteToken === "" || !validOpaqueText(previewToken, 2048) || previewToken === "") throw contractError("voiceprint-person-invite-contract-invalid");
|
|
9966
|
+
const useShortPair = SHORT_INVITE_TOKEN_PATTERN.test(shortInviteToken) && SHORT_INVITE_TOKEN_PATTERN.test(shortPreviewToken);
|
|
9967
|
+
return {
|
|
9968
|
+
inviteUrl: this.invitationUrl(useShortPair ? shortInviteToken : inviteToken, useShortPair ? shortPreviewToken : previewToken, useShortPair),
|
|
9969
|
+
expiresAtMillis
|
|
9970
|
+
};
|
|
9971
|
+
}
|
|
9972
|
+
async revokePlaybackGrant(grantRef, options = {}) {
|
|
9973
|
+
const session = await this.runtime.requireSession();
|
|
9974
|
+
const reference = await this.openGrantRef(grantRef, session.userId);
|
|
9975
|
+
if (requiredBoolean((await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/grants/set-scope", {
|
|
9976
|
+
grantee_user_id: reference.granteeUserId,
|
|
9977
|
+
scope: 2,
|
|
9978
|
+
enabled: false
|
|
9979
|
+
}, session, options.signal, {
|
|
9980
|
+
lane: "write",
|
|
9981
|
+
key: `voiceprint:revoke:${grantRef}`,
|
|
9982
|
+
bypassCache: true
|
|
9983
|
+
})).revoked, "voiceprint-revoke-contract-invalid") !== true) throw contractError("voiceprint-revoke-contract-invalid");
|
|
9984
|
+
return { revoked: true };
|
|
9985
|
+
}
|
|
9986
|
+
async restorePlayback(options = {}) {
|
|
9987
|
+
const session = await this.runtime.requireSession();
|
|
9988
|
+
this.runtime.invalidateKey(this.runtime.requestScope(session.userId), "voiceprint:self");
|
|
9989
|
+
const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/restore-playback", {}, session, options.signal, {
|
|
9990
|
+
lane: "write",
|
|
9991
|
+
key: `voiceprint:restore:${Date.now().toString(36)}`,
|
|
9992
|
+
bypassCache: true
|
|
9993
|
+
});
|
|
9994
|
+
const result = {
|
|
9995
|
+
canPlay: requiredBoolean(data.can_play, "voiceprint-restore-contract-invalid"),
|
|
9996
|
+
restored: requiredBoolean(data.restored, "voiceprint-restore-contract-invalid"),
|
|
9997
|
+
updatedAtMillis: requiredInteger(data.updated_at ?? 0, "voiceprint-restore-contract-invalid")
|
|
9998
|
+
};
|
|
9999
|
+
if (result.restored && !result.canPlay) throw contractError("voiceprint-restore-contract-invalid");
|
|
10000
|
+
return result;
|
|
10001
|
+
}
|
|
10002
|
+
async bindEnrollment() {
|
|
10003
|
+
const viewerUserId = (await this.runtime.requireSession()).userId;
|
|
10004
|
+
return { enrollVoiceprintWav: async (input, options = {}) => await this.enrollWav(input, {
|
|
10005
|
+
...options,
|
|
10006
|
+
expectedViewerUserId: viewerUserId
|
|
10007
|
+
}) };
|
|
10008
|
+
}
|
|
10009
|
+
async enrollWav(input, options = {}) {
|
|
10010
|
+
const { wav, durationMs } = input;
|
|
10011
|
+
if (!Number.isSafeInteger(durationMs) || durationMs < 3e3 || durationMs > 6e4 || wav.byteLength > 10485760 || wav.byteLength < 46) throw new ArkmePluginError("voiceprint-enrollment-input-invalid", "声纹录音需为 3 至 60 秒的 WAV 音频", false);
|
|
10012
|
+
try {
|
|
10013
|
+
validateVoiceprintPcm16Wav(wav, durationMs);
|
|
10014
|
+
} catch (error) {
|
|
10015
|
+
throw new ArkmePluginError("voiceprint-enrollment-input-invalid", "声纹录音需为 3 至 60 秒的单声道 PCM16 WAV 音频", false, 400, { cause: error });
|
|
10016
|
+
}
|
|
10017
|
+
const session = await this.runtime.requireSession();
|
|
10018
|
+
if (options.expectedViewerUserId !== void 0 && session.userId !== options.expectedViewerUserId) throw new ArkmePluginError("account-changed", "登录账号已切换,请在当前账号下重新录入", false, 409);
|
|
10019
|
+
const form = new FormData();
|
|
10020
|
+
const wavBuffer = new Uint8Array(wav).buffer;
|
|
10021
|
+
form.set("audio_file", new Blob([wavBuffer], { type: "audio/wav" }), "voiceprint.wav");
|
|
10022
|
+
form.set("duration_ms", String(durationMs));
|
|
10023
|
+
this.runtime.invalidateKey(this.runtime.requestScope(session.userId), "voiceprint:self");
|
|
10024
|
+
const data = await this.runtime.authenticatedAudioMultipartPost("/api/v1/audio/voiceprint/enroll-from-audio", form, session, options.signal, {
|
|
10025
|
+
key: `voiceprint:enroll:${Date.now().toString(36)}`,
|
|
10026
|
+
bypassCache: true
|
|
10027
|
+
});
|
|
10028
|
+
const status = requiredString(data.status, "voiceprint-enrollment-contract-invalid").trim();
|
|
10029
|
+
if (status !== "processing") throw contractError("voiceprint-enrollment-contract-invalid");
|
|
10030
|
+
return {
|
|
10031
|
+
status,
|
|
10032
|
+
cloneReady: requiredBoolean(data.clone_ok, "voiceprint-enrollment-contract-invalid"),
|
|
10033
|
+
updatedAtMillis: requiredInteger(data.updated_at, "voiceprint-enrollment-contract-invalid", 1)
|
|
10034
|
+
};
|
|
10035
|
+
}
|
|
10036
|
+
parseOutboundGrant(raw, viewerUserId) {
|
|
10037
|
+
const ownerUserId = requiredInteger(raw.owner_user_id, "voiceprint-grant-contract-invalid", 1);
|
|
10038
|
+
const granteeUserId = requiredInteger(raw.grantee_user_id, "voiceprint-grant-contract-invalid", 1);
|
|
10039
|
+
const status = requiredInteger(raw.status, "voiceprint-grant-contract-invalid");
|
|
10040
|
+
if (ownerUserId !== viewerUserId || granteeUserId === viewerUserId || status !== 1) throw contractError("voiceprint-grant-contract-invalid");
|
|
10041
|
+
return {
|
|
10042
|
+
granteeUserId,
|
|
10043
|
+
identifyEnabled: requiredBoolean(raw.identify_enabled, "voiceprint-grant-contract-invalid"),
|
|
10044
|
+
playEnabled: requiredBoolean(raw.play_enabled, "voiceprint-grant-contract-invalid"),
|
|
10045
|
+
grantedAtMillis: requiredInteger(raw.play_granted_at ?? 0, "voiceprint-grant-contract-invalid"),
|
|
10046
|
+
updatedAtMillis: requiredInteger(raw.update_at, "voiceprint-grant-contract-invalid")
|
|
10047
|
+
};
|
|
10048
|
+
}
|
|
10049
|
+
parseRecognizedPerson(raw) {
|
|
10050
|
+
const speakerId = requiredString(raw.speaker_id, "voiceprint-person-contract-invalid").trim().toLowerCase();
|
|
10051
|
+
const targetUserId = requiredInteger(raw.target_user_id, "voiceprint-person-contract-invalid");
|
|
10052
|
+
const displayName = requiredString(raw.display_name, "voiceprint-person-contract-invalid").trim();
|
|
10053
|
+
const playGranted = requiredBoolean(raw.play_granted, "voiceprint-person-contract-invalid");
|
|
10054
|
+
const previewAvailable = requiredBoolean(raw.preview_available, "voiceprint-person-contract-invalid");
|
|
10055
|
+
const canInvite = requiredBoolean(raw.can_invite, "voiceprint-person-contract-invalid");
|
|
10056
|
+
const hasSpeaker = validSpeakerId(speakerId);
|
|
10057
|
+
if (speakerId !== "" && !hasSpeaker || !hasSpeaker && targetUserId <= 0 || canInvite === playGranted || previewAvailable && !playGranted || playGranted && targetUserId <= 0 || !hasSpeaker && canInvite) throw contractError("voiceprint-person-contract-invalid");
|
|
10058
|
+
return {
|
|
10059
|
+
speakerId,
|
|
10060
|
+
targetUserId,
|
|
10061
|
+
displayName,
|
|
10062
|
+
playGranted,
|
|
10063
|
+
previewAvailable,
|
|
10064
|
+
canInvite
|
|
10065
|
+
};
|
|
10066
|
+
}
|
|
10067
|
+
async projectRecognizedPerson(item, session, profiles) {
|
|
10068
|
+
const identityKind = item.speakerId === "" ? "authorized_user" : "speaker";
|
|
10069
|
+
const payload = identityKind === "speaker" ? {
|
|
10070
|
+
version: 1,
|
|
10071
|
+
viewerUserId: session.userId,
|
|
10072
|
+
identityKind,
|
|
10073
|
+
speakerId: item.speakerId
|
|
10074
|
+
} : {
|
|
10075
|
+
version: 1,
|
|
10076
|
+
viewerUserId: session.userId,
|
|
10077
|
+
identityKind,
|
|
10078
|
+
targetUserId: item.targetUserId
|
|
10079
|
+
};
|
|
10080
|
+
const avatarRef = (item.targetUserId > 0 ? profiles.get(item.targetUserId) : void 0)?.avatarUrl === void 0 ? void 0 : await this.optionalAvatarRef(session.userId, item.targetUserId);
|
|
10081
|
+
return {
|
|
10082
|
+
personRef: await this.sealRef(PERSON_REF_PREFIX, payload),
|
|
10083
|
+
identityKind,
|
|
10084
|
+
displayName: item.displayName || "未命名声纹",
|
|
10085
|
+
...avatarRef === void 0 ? {} : { avatarRef },
|
|
10086
|
+
playGranted: item.playGranted,
|
|
10087
|
+
previewAvailable: item.previewAvailable,
|
|
10088
|
+
canInvite: item.canInvite,
|
|
10089
|
+
inviteTargetSelectionRequired: item.canInvite && item.targetUserId === 0
|
|
10090
|
+
};
|
|
10091
|
+
}
|
|
10092
|
+
async loadPublicProfiles(userIds, session, signal) {
|
|
10093
|
+
try {
|
|
10094
|
+
return await this.profile.publicProfileSummariesByUserIds(userIds, session, signal);
|
|
10095
|
+
} catch (error) {
|
|
10096
|
+
if (signal?.aborted === true) throw error;
|
|
10097
|
+
return /* @__PURE__ */ new Map();
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
async optionalAvatarRef(viewerUserId, targetUserId) {
|
|
10101
|
+
try {
|
|
10102
|
+
return await this.profile.sealProfileImageRef(viewerUserId, targetUserId);
|
|
10103
|
+
} catch {
|
|
10104
|
+
return;
|
|
10105
|
+
}
|
|
10106
|
+
}
|
|
10107
|
+
async sealRef(prefix, payload) {
|
|
10108
|
+
const encoded = encodeOpaqueJson(payload);
|
|
10109
|
+
return `${prefix}.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
|
|
10110
|
+
}
|
|
10111
|
+
async openPersonRef(personRef, expectedViewerUserId) {
|
|
10112
|
+
const normalized = personRef.trim();
|
|
10113
|
+
if (!validOpaqueText(normalized, 2048)) throw this.invalidPersonRef();
|
|
10114
|
+
const parts = normalized.split(".");
|
|
10115
|
+
if (parts.length !== 3 || parts[0] !== PERSON_REF_PREFIX) throw this.invalidPersonRef();
|
|
10116
|
+
const encoded = parts[1] ?? "";
|
|
10117
|
+
const supplied = Buffer.from(parts[2] ?? "", "base64url");
|
|
10118
|
+
const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest();
|
|
10119
|
+
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw this.invalidPersonRef();
|
|
10120
|
+
let raw;
|
|
10121
|
+
try {
|
|
10122
|
+
raw = objectValue$1(decodeOpaqueJson(encoded));
|
|
10123
|
+
} catch (error) {
|
|
10124
|
+
throw new ArkmePluginError("voiceprint-person-ref-invalid", "已识别人引用无效", false, 400, { cause: error });
|
|
10125
|
+
}
|
|
10126
|
+
const viewerUserId = integerValue$1(raw.viewerUserId);
|
|
10127
|
+
if (raw.version !== 1 || viewerUserId !== expectedViewerUserId) throw this.invalidPersonRef(true);
|
|
10128
|
+
if (raw.identityKind === "speaker") {
|
|
10129
|
+
const speakerId = stringValue$1(raw.speakerId).trim().toLowerCase();
|
|
10130
|
+
if (!validSpeakerId(speakerId)) throw this.invalidPersonRef();
|
|
10131
|
+
return {
|
|
10132
|
+
version: 1,
|
|
10133
|
+
viewerUserId,
|
|
10134
|
+
identityKind: "speaker",
|
|
10135
|
+
speakerId
|
|
10136
|
+
};
|
|
10137
|
+
}
|
|
10138
|
+
if (raw.identityKind === "authorized_user") {
|
|
10139
|
+
const targetUserId = integerValue$1(raw.targetUserId);
|
|
10140
|
+
if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0) throw this.invalidPersonRef();
|
|
10141
|
+
return {
|
|
10142
|
+
version: 1,
|
|
10143
|
+
viewerUserId,
|
|
10144
|
+
identityKind: "authorized_user",
|
|
10145
|
+
targetUserId
|
|
10146
|
+
};
|
|
10147
|
+
}
|
|
10148
|
+
throw this.invalidPersonRef();
|
|
10149
|
+
}
|
|
10150
|
+
async openGrantRef(grantRef, expectedViewerUserId) {
|
|
10151
|
+
const normalized = grantRef.trim();
|
|
10152
|
+
if (!validOpaqueText(normalized, 2048)) throw this.invalidGrantRef();
|
|
10153
|
+
const parts = normalized.split(".");
|
|
10154
|
+
if (parts.length !== 3 || parts[0] !== GRANT_REF_PREFIX) throw this.invalidGrantRef();
|
|
10155
|
+
const encoded = parts[1] ?? "";
|
|
10156
|
+
const supplied = Buffer.from(parts[2] ?? "", "base64url");
|
|
10157
|
+
const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest();
|
|
10158
|
+
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw this.invalidGrantRef();
|
|
10159
|
+
let raw;
|
|
10160
|
+
try {
|
|
10161
|
+
raw = objectValue$1(decodeOpaqueJson(encoded));
|
|
10162
|
+
} catch (error) {
|
|
10163
|
+
throw new ArkmePluginError("voiceprint-grant-ref-invalid", "声纹授权引用无效", false, 400, { cause: error });
|
|
10164
|
+
}
|
|
10165
|
+
const viewerUserId = integerValue$1(raw.viewerUserId);
|
|
10166
|
+
const granteeUserId = integerValue$1(raw.granteeUserId);
|
|
10167
|
+
if (raw.version !== 1 || viewerUserId !== expectedViewerUserId || !Number.isSafeInteger(granteeUserId) || granteeUserId <= 0 || granteeUserId === expectedViewerUserId) throw this.invalidGrantRef(viewerUserId !== expectedViewerUserId);
|
|
10168
|
+
return {
|
|
10169
|
+
version: 1,
|
|
10170
|
+
viewerUserId,
|
|
10171
|
+
granteeUserId
|
|
10172
|
+
};
|
|
10173
|
+
}
|
|
10174
|
+
invitationUrl(token, previewToken, compact) {
|
|
10175
|
+
const base = this.runtime.config.environment === "prod" ? "https://jiwo.cc" : "https://jotmo-app.senguo.me";
|
|
10176
|
+
const url = new URL(compact ? "/v" : "/app/voiceprint/invite", base);
|
|
10177
|
+
url.searchParams.set("p", previewToken);
|
|
10178
|
+
url.hash = `t=${token}`;
|
|
10179
|
+
return url.toString();
|
|
10180
|
+
}
|
|
10181
|
+
invalidPersonRef(accountMismatch = false) {
|
|
10182
|
+
return new ArkmePluginError("voiceprint-person-ref-invalid", accountMismatch ? "已识别人引用与当前账号不匹配" : "已识别人引用无效", false, accountMismatch ? 403 : 400);
|
|
10183
|
+
}
|
|
10184
|
+
invalidGrantRef(accountMismatch = false) {
|
|
10185
|
+
return new ArkmePluginError("voiceprint-grant-ref-invalid", accountMismatch ? "声纹授权引用与当前账号不匹配" : "声纹授权引用无效", false, accountMismatch ? 403 : 400);
|
|
10186
|
+
}
|
|
10187
|
+
};
|
|
10188
|
+
//#endregion
|
|
9419
10189
|
//#region src/services/world-service.ts
|
|
9420
10190
|
const ARKME_WORLD_IMAGE_REF_TTL_MILLIS = 900 * 1e3;
|
|
9421
10191
|
const MAX_ARKME_WORLD_IMAGE_REFS = 2048;
|
|
10192
|
+
const ARKME_WORLD_AVATAR_RESOLUTION_CACHE_TTL_MILLIS = 300 * 1e3;
|
|
10193
|
+
const MAX_ARKME_WORLD_AVATAR_RESOLUTION_CACHE_ENTRIES = 2048;
|
|
9422
10194
|
const ARKME_WORLD_RECORD_REF_TTL_MILLIS = 900 * 1e3;
|
|
9423
10195
|
const MAX_ARKME_WORLD_RECORD_REFS = 4096;
|
|
9424
10196
|
const ARKME_VOICEPRINT_PLAY_SCOPE = 2;
|
|
@@ -9470,6 +10242,9 @@ function integerValue(value) {
|
|
|
9470
10242
|
function listValue(value) {
|
|
9471
10243
|
return Array.isArray(value) ? value : [];
|
|
9472
10244
|
}
|
|
10245
|
+
function isTopLevelWorldRecord(raw) {
|
|
10246
|
+
return stringValue$1(objectValue$1(raw).parent_record_uid).trim() === "";
|
|
10247
|
+
}
|
|
9473
10248
|
function safeFailureMessage(error) {
|
|
9474
10249
|
if (error instanceof ArkmePluginError) return error.message;
|
|
9475
10250
|
if (error instanceof Error && error.message.trim() !== "") return error.message;
|
|
@@ -9565,12 +10340,21 @@ function worldAvatarResolutionKey(ownerUserId, avatarRef) {
|
|
|
9565
10340
|
if (!Number.isSafeInteger(ownerUserId) || ownerUserId <= 0 || !normalized.startsWith("file_asset://")) return "";
|
|
9566
10341
|
return `${String(ownerUserId)}|${normalized}`;
|
|
9567
10342
|
}
|
|
10343
|
+
function worldImageAssetIdentity(raw) {
|
|
10344
|
+
try {
|
|
10345
|
+
const parsed = new URL(raw.trim());
|
|
10346
|
+
return `${parsed.hostname.toLowerCase()}${parsed.pathname}`;
|
|
10347
|
+
} catch {
|
|
10348
|
+
return raw.trim();
|
|
10349
|
+
}
|
|
10350
|
+
}
|
|
9568
10351
|
var WorldService = class {
|
|
9569
10352
|
runtime;
|
|
9570
10353
|
profile;
|
|
9571
10354
|
media;
|
|
9572
10355
|
record;
|
|
9573
10356
|
worldImageRefs = /* @__PURE__ */ new Map();
|
|
10357
|
+
worldAvatarResolutionCache = /* @__PURE__ */ new Map();
|
|
9574
10358
|
worldRecordRefs = /* @__PURE__ */ new Map();
|
|
9575
10359
|
voiceprintSocialCache = /* @__PURE__ */ new Map();
|
|
9576
10360
|
voiceprintSocialInFlight = /* @__PURE__ */ new Map();
|
|
@@ -9582,6 +10366,7 @@ var WorldService = class {
|
|
|
9582
10366
|
}
|
|
9583
10367
|
dispose() {
|
|
9584
10368
|
this.worldImageRefs.clear();
|
|
10369
|
+
this.worldAvatarResolutionCache.clear();
|
|
9585
10370
|
this.worldRecordRefs.clear();
|
|
9586
10371
|
this.voiceprintSocialCache.clear();
|
|
9587
10372
|
this.voiceprintSocialInFlight.clear();
|
|
@@ -9629,22 +10414,30 @@ var WorldService = class {
|
|
|
9629
10414
|
async listMyWorldFeed(options = {}) {
|
|
9630
10415
|
const session = await this.runtime.requireSession();
|
|
9631
10416
|
const limit = Math.min(20, Math.max(1, Math.trunc(options.limit ?? 20)));
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
10417
|
+
let offset = Math.max(0, Math.trunc(options.offset ?? 0));
|
|
10418
|
+
let total = 0;
|
|
10419
|
+
let items = [];
|
|
10420
|
+
let hasMore = false;
|
|
10421
|
+
let attempts = 0;
|
|
10422
|
+
do {
|
|
10423
|
+
const data = await this.runtime.authenticatedWorldPost("/api/v1/public-record/my-list", {
|
|
10424
|
+
limit,
|
|
10425
|
+
offset
|
|
10426
|
+
}, session, options.signal);
|
|
10427
|
+
const rawItems = listValue(data.list);
|
|
10428
|
+
const rootItems = rawItems.filter(isTopLevelWorldRecord);
|
|
10429
|
+
const resolvedAvatars = await this.resolveWorldAvatarUrls(rootItems, session, options.signal);
|
|
10430
|
+
items = (await Promise.all(rootItems.map((raw) => this.worldFeedItem(raw, session.userId, resolvedAvatars)))).filter((item) => item !== void 0);
|
|
10431
|
+
total = Math.max(0, Math.trunc(numberValue(data.total)));
|
|
10432
|
+
offset += rawItems.length;
|
|
10433
|
+
hasMore = rawItems.length > 0 && offset < total;
|
|
10434
|
+
attempts += 1;
|
|
10435
|
+
} while (items.length === 0 && hasMore && attempts < 4);
|
|
9643
10436
|
return {
|
|
9644
10437
|
items,
|
|
9645
10438
|
total,
|
|
9646
10439
|
hasMore,
|
|
9647
|
-
...hasMore ? { nextOffset } : {}
|
|
10440
|
+
...hasMore ? { nextOffset: offset } : {}
|
|
9648
10441
|
};
|
|
9649
10442
|
}
|
|
9650
10443
|
async listUserWorldFeed(userId, options = {}) {
|
|
@@ -9664,7 +10457,7 @@ var WorldService = class {
|
|
|
9664
10457
|
}, void 0, [200], options.signal);
|
|
9665
10458
|
const rawItems = listValue(data.list);
|
|
9666
10459
|
total = Math.max(0, Math.trunc(numberValue(data.total)));
|
|
9667
|
-
const rootItems = rawItems.filter(
|
|
10460
|
+
const rootItems = rawItems.filter(isTopLevelWorldRecord);
|
|
9668
10461
|
const resolvedAvatars = await this.resolveWorldAvatarUrls(rootItems, session, options.signal);
|
|
9669
10462
|
items = (await Promise.all(rootItems.map((raw) => this.worldFeedItem(raw, session.userId, resolvedAvatars)))).filter((item) => item !== void 0);
|
|
9670
10463
|
offset += rawItems.length;
|
|
@@ -9714,7 +10507,8 @@ var WorldService = class {
|
|
|
9714
10507
|
const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/generate-playback", {
|
|
9715
10508
|
source_scene: 4,
|
|
9716
10509
|
source_id: entry.recordUid,
|
|
9717
|
-
source_chunk_index: chunkIndex
|
|
10510
|
+
source_chunk_index: chunkIndex,
|
|
10511
|
+
source_chunk_max_runes: 120
|
|
9718
10512
|
}, session, input.signal, {
|
|
9719
10513
|
lane: "write",
|
|
9720
10514
|
bypassCache: true
|
|
@@ -10053,7 +10847,7 @@ var WorldService = class {
|
|
|
10053
10847
|
async readWorldImage(imageRef, options = {}) {
|
|
10054
10848
|
const session = await this.runtime.requireSession();
|
|
10055
10849
|
const entry = await this.openWorldImageRef(imageRef, session.userId);
|
|
10056
|
-
const byteLimit = Math.min(
|
|
10850
|
+
const byteLimit = Math.min(ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES, Math.max(1, Math.trunc(options.maxBytes ?? 20971520)));
|
|
10057
10851
|
return await this.media.downloadSignedImage(trustedWorldImageUrl(this.runtime.config.environment, entry.sourceUrl), byteLimit, options.signal);
|
|
10058
10852
|
}
|
|
10059
10853
|
async publishWorldTextForConversation(recordUid, textContent, signal) {
|
|
@@ -10243,11 +11037,11 @@ var WorldService = class {
|
|
|
10243
11037
|
const avatarUrl = rawAvatar.startsWith("file_asset://") ? resolvedAvatars.get(worldAvatarResolutionKey(ownerUserId, rawAvatar)) ?? "" : rawAvatar;
|
|
10244
11038
|
const avatarFallback = worldPhoneDefaultAvatar(rawAvatar);
|
|
10245
11039
|
const imageRefs = [];
|
|
10246
|
-
for (const signedUrl of rawImages.slice(0, 9)) {
|
|
11040
|
+
for (const [index, signedUrl] of rawImages.slice(0, 9).entries()) {
|
|
10247
11041
|
if (!this.isTrustedWorldImageUrl(signedUrl)) continue;
|
|
10248
|
-
imageRefs.push(await this.sealWorldImageRef(viewerUserId, signedUrl));
|
|
11042
|
+
imageRefs.push(await this.sealWorldImageRef(viewerUserId, signedUrl, `record:${recordUid}:image:${String(index)}:${worldImageAssetIdentity(signedUrl)}`));
|
|
10249
11043
|
}
|
|
10250
|
-
const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl) : void 0;
|
|
11044
|
+
const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl, `avatar:${String(ownerUserId)}:${rawAvatar.startsWith("file_asset://") ? rawAvatar : worldImageAssetIdentity(avatarUrl)}`) : void 0;
|
|
10251
11045
|
return {
|
|
10252
11046
|
recordRef: await this.worldRecordRef(viewerUserId, recordUid, {
|
|
10253
11047
|
ownerUserId,
|
|
@@ -10295,19 +11089,40 @@ var WorldService = class {
|
|
|
10295
11089
|
});
|
|
10296
11090
|
}
|
|
10297
11091
|
if (requested.size === 0) return /* @__PURE__ */ new Map();
|
|
11092
|
+
const now = Date.now();
|
|
11093
|
+
for (const [key, entry] of this.worldAvatarResolutionCache) if (entry.expiresAtMillis <= now) this.worldAvatarResolutionCache.delete(key);
|
|
11094
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
11095
|
+
for (const [key] of requested) {
|
|
11096
|
+
const cached = this.worldAvatarResolutionCache.get(key);
|
|
11097
|
+
if (cached === void 0) continue;
|
|
11098
|
+
this.worldAvatarResolutionCache.delete(key);
|
|
11099
|
+
this.worldAvatarResolutionCache.set(key, cached);
|
|
11100
|
+
resolved.set(key, cached.sourceUrl);
|
|
11101
|
+
}
|
|
11102
|
+
const unresolved = [...requested].filter(([key]) => !resolved.has(key)).map(([, value]) => value);
|
|
11103
|
+
if (unresolved.length === 0) return resolved;
|
|
10298
11104
|
let data;
|
|
10299
11105
|
try {
|
|
10300
|
-
data = await this.runtime.authenticatedAuthPost("/api/v1/auth/resolve-avatar-refs", { items:
|
|
11106
|
+
data = await this.runtime.authenticatedAuthPost("/api/v1/auth/resolve-avatar-refs", { items: unresolved }, session, signal);
|
|
10301
11107
|
} catch {
|
|
10302
|
-
return
|
|
11108
|
+
return resolved;
|
|
10303
11109
|
}
|
|
10304
|
-
const resolved = /* @__PURE__ */ new Map();
|
|
10305
11110
|
for (const raw of listValue(data.items)) {
|
|
10306
11111
|
const item = objectValue$1(raw);
|
|
10307
11112
|
const key = worldAvatarResolutionKey(Math.trunc(numberValue(item.owner_user_id)), stringValue$1(item.avatar_ref).trim());
|
|
10308
11113
|
const url = stringValue$1(item.url).trim();
|
|
10309
11114
|
if (!requested.has(key) || !this.isTrustedWorldImageUrl(url)) continue;
|
|
10310
11115
|
resolved.set(key, url);
|
|
11116
|
+
this.worldAvatarResolutionCache.delete(key);
|
|
11117
|
+
this.worldAvatarResolutionCache.set(key, {
|
|
11118
|
+
sourceUrl: url,
|
|
11119
|
+
expiresAtMillis: Date.now() + ARKME_WORLD_AVATAR_RESOLUTION_CACHE_TTL_MILLIS
|
|
11120
|
+
});
|
|
11121
|
+
}
|
|
11122
|
+
while (this.worldAvatarResolutionCache.size > MAX_ARKME_WORLD_AVATAR_RESOLUTION_CACHE_ENTRIES) {
|
|
11123
|
+
const oldest = this.worldAvatarResolutionCache.keys().next().value;
|
|
11124
|
+
if (oldest === void 0) break;
|
|
11125
|
+
this.worldAvatarResolutionCache.delete(oldest);
|
|
10311
11126
|
}
|
|
10312
11127
|
return resolved;
|
|
10313
11128
|
}
|
|
@@ -10324,7 +11139,7 @@ var WorldService = class {
|
|
|
10324
11139
|
const rawAvatar = stringValue$1(item.avatar ?? item.head_img).trim();
|
|
10325
11140
|
const avatarUrl = rawAvatar.startsWith("file_asset://") ? resolvedAvatars.get(worldAvatarResolutionKey(ownerUserId, rawAvatar)) ?? "" : rawAvatar;
|
|
10326
11141
|
const avatarFallback = worldPhoneDefaultAvatar(rawAvatar);
|
|
10327
|
-
const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl) : void 0;
|
|
11142
|
+
const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl, `avatar:${String(ownerUserId)}:${rawAvatar.startsWith("file_asset://") ? rawAvatar : worldImageAssetIdentity(avatarUrl)}`) : void 0;
|
|
10328
11143
|
return {
|
|
10329
11144
|
interactionRef: await this.worldRecordRef(viewerUserId, recordUid, {
|
|
10330
11145
|
ownerUserId,
|
|
@@ -10389,11 +11204,12 @@ var WorldService = class {
|
|
|
10389
11204
|
this.worldImageRefs.delete(oldest);
|
|
10390
11205
|
}
|
|
10391
11206
|
}
|
|
10392
|
-
async sealWorldImageRef(viewerUserId, sourceUrl) {
|
|
11207
|
+
async sealWorldImageRef(viewerUserId, sourceUrl, stableIdentity) {
|
|
10393
11208
|
const now = Date.now();
|
|
10394
11209
|
this.pruneWorldImageRefs(now);
|
|
10395
|
-
const
|
|
10396
|
-
const
|
|
11210
|
+
const uniqueCode = await this.runtime.stateStore.uniqueCode();
|
|
11211
|
+
const token = createHmac("sha256", uniqueCode).update(`world-image-token-v1:${String(viewerUserId)}:${stableIdentity}`).digest("base64url");
|
|
11212
|
+
const signature = createHmac("sha256", uniqueCode).update(`world-image-v1:${String(viewerUserId)}:${token}`).digest("base64url");
|
|
10397
11213
|
this.worldImageRefs.set(token, {
|
|
10398
11214
|
viewerUserId,
|
|
10399
11215
|
sourceUrl,
|
|
@@ -10480,6 +11296,7 @@ var ArkmeService = class {
|
|
|
10480
11296
|
aiPolish;
|
|
10481
11297
|
chat;
|
|
10482
11298
|
contact;
|
|
11299
|
+
voiceprint;
|
|
10483
11300
|
constructor(config, sessionStore, stateStore, fetchImpl = fetch, pendingSessionStore, outgoingCallBroker = new ArkmeOutgoingCallBroker()) {
|
|
10484
11301
|
this.config = config;
|
|
10485
11302
|
this.sessionStore = sessionStore;
|
|
@@ -10514,9 +11331,10 @@ var ArkmeService = class {
|
|
|
10514
11331
|
this.community = new CommunityService(this.runtime, this.source, this.profile);
|
|
10515
11332
|
this.interwoven = new InterwovenService(this.runtime, this.source, this.profile);
|
|
10516
11333
|
this.aiPolish = new GroupAiPolishService(this.runtime, this.source, { sendChatSourceTextRaw: async (...args) => await this.chat.sendChatSourceTextRaw(...args) });
|
|
10517
|
-
this.realtime = new ChatRealtimeService(this.runtime, this.source, { chatTimelineItems: async (data, session) => await this.chat.chatTimelineItems(data, session) });
|
|
11334
|
+
this.realtime = new ChatRealtimeService(this.runtime, this.source, { chatTimelineItems: async (data, session, chatSessionUid) => await this.chat.chatTimelineItems(data, session, chatSessionUid) });
|
|
10518
11335
|
this.chat = new ChatService(this.runtime, this.source, this.profile, this.media, this.record, this.bot, this.arko, this.aiPolish, this.realtime);
|
|
10519
11336
|
this.contact = new ContactService(this.runtime, this.source, this.profile, this.realtime);
|
|
11337
|
+
this.voiceprint = new VoiceprintService(this.runtime, this.profile, { resolveRegisteredContactUserId: async (contactRef, session, signal) => await this.contact.resolveRegisteredContactUserId(contactRef, session, signal) });
|
|
10520
11338
|
this.auth = new AuthService(this.runtime, this.profile, {
|
|
10521
11339
|
reconnectChatRealtime: () => {
|
|
10522
11340
|
this.realtime.reconnect();
|
|
@@ -10599,6 +11417,7 @@ var ArkmeService = class {
|
|
|
10599
11417
|
environment: this.config.environment,
|
|
10600
11418
|
testLoginEnabled: this.config.environment === "test",
|
|
10601
11419
|
callAssetBasePath: `${this.config.routePath}/call`,
|
|
11420
|
+
voiceprintEnrollmentPath: `${this.config.routePath}/voiceprint/enroll`,
|
|
10602
11421
|
shareWebsite: this.config.shareWebsite ?? "https://app.arkme.ai"
|
|
10603
11422
|
};
|
|
10604
11423
|
}
|
|
@@ -10644,6 +11463,7 @@ var ArkmeService = class {
|
|
|
10644
11463
|
worldVoiceprintPlayback: true,
|
|
10645
11464
|
worldVoiceprintInvite: true,
|
|
10646
11465
|
worldVoiceprintSocialContext: true,
|
|
11466
|
+
voiceprintManagement: true,
|
|
10647
11467
|
arrangements: true,
|
|
10648
11468
|
myExtensions: true,
|
|
10649
11469
|
extensionPublish: true,
|
|
@@ -10857,6 +11677,12 @@ var ArkmeService = class {
|
|
|
10857
11677
|
async listGroupMembers(sourceRef, options = {}) {
|
|
10858
11678
|
return await this.group.listGroupMembers(sourceRef, options);
|
|
10859
11679
|
}
|
|
11680
|
+
async listSourceMembers(sourceRef, options = {}) {
|
|
11681
|
+
return await this.chat.listSourceMembers(sourceRef, options);
|
|
11682
|
+
}
|
|
11683
|
+
async sourceMemberRecords(sourceRef, memberRef, mode, options = {}) {
|
|
11684
|
+
return await this.chat.sourceMemberRecords(sourceRef, memberRef, mode, options);
|
|
11685
|
+
}
|
|
10860
11686
|
async listGroupMemberCandidates(sourceRef, options = {}) {
|
|
10861
11687
|
return await this.group.listGroupMemberCandidates(sourceRef, options);
|
|
10862
11688
|
}
|
|
@@ -10893,6 +11719,9 @@ var ArkmeService = class {
|
|
|
10893
11719
|
async openPrivateChatFromUser(peerUserId, options = {}) {
|
|
10894
11720
|
return await this.chat.openPrivateChatFromUser(peerUserId, options);
|
|
10895
11721
|
}
|
|
11722
|
+
async openPrivateChatFromMember(sourceRef, memberRef, options = {}) {
|
|
11723
|
+
return await this.chat.openPrivateChatFromMember(sourceRef, memberRef, options);
|
|
11724
|
+
}
|
|
10896
11725
|
async readSource(sourceRef, options = {}) {
|
|
10897
11726
|
return await this.chat.readSource(sourceRef, options);
|
|
10898
11727
|
}
|
|
@@ -11122,6 +11951,36 @@ var ArkmeService = class {
|
|
|
11122
11951
|
throw error;
|
|
11123
11952
|
}
|
|
11124
11953
|
}
|
|
11954
|
+
async myVoiceprint(options = {}) {
|
|
11955
|
+
return await this.voiceprint.myVoiceprint(options);
|
|
11956
|
+
}
|
|
11957
|
+
async outboundVoiceprintGrants(input, options = {}) {
|
|
11958
|
+
return await this.voiceprint.outboundGrants(input, options);
|
|
11959
|
+
}
|
|
11960
|
+
async recognizedVoiceprintPeople(input, options = {}) {
|
|
11961
|
+
return await this.voiceprint.recognizedPeople(input, options);
|
|
11962
|
+
}
|
|
11963
|
+
async recognizedVoiceprintPerson(personRef, options = {}) {
|
|
11964
|
+
return await this.voiceprint.recognizedPerson(personRef, options);
|
|
11965
|
+
}
|
|
11966
|
+
async recognizedPersonVoiceprints(personRef, options = {}) {
|
|
11967
|
+
return await this.voiceprint.recognizedPersonVoiceprints(personRef, options);
|
|
11968
|
+
}
|
|
11969
|
+
async createVoiceprintInvitation(options = {}) {
|
|
11970
|
+
return await this.voiceprint.createInvitation(options);
|
|
11971
|
+
}
|
|
11972
|
+
async createRecognizedPersonVoiceprintInvitation(personRef, targetContactRef, options = {}) {
|
|
11973
|
+
return await this.voiceprint.createRecognizedPersonInvitation(personRef, targetContactRef, options);
|
|
11974
|
+
}
|
|
11975
|
+
async revokeVoiceprintPlaybackGrant(grantRef, options = {}) {
|
|
11976
|
+
return await this.voiceprint.revokePlaybackGrant(grantRef, options);
|
|
11977
|
+
}
|
|
11978
|
+
async restoreVoiceprintPlayback(options = {}) {
|
|
11979
|
+
return await this.voiceprint.restorePlayback(options);
|
|
11980
|
+
}
|
|
11981
|
+
async bindVoiceprintEnrollment() {
|
|
11982
|
+
return await this.voiceprint.bindEnrollment();
|
|
11983
|
+
}
|
|
11125
11984
|
/** Read the authenticated comment/reply tree behind one account-bound World reference. */
|
|
11126
11985
|
async listWorldInteractions(recordRef, options = {}) {
|
|
11127
11986
|
return await this.world.listWorldInteractions(recordRef, options);
|
|
@@ -12527,4 +13386,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
|
|
|
12527
13386
|
return jsonValue(await handler(jsonValue(args)));
|
|
12528
13387
|
}
|
|
12529
13388
|
//#endregion
|
|
12530
|
-
export { unpackArkmeExtension as A,
|
|
13389
|
+
export { unpackArkmeExtension as A, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as B, packBundleFiles as C, assertExtensionArtifactSize as D, renderArkmeSandboxHostEntry as E, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as F, isArkmeBotAvatarRef as G, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as H, ArkmeService as I, ArkmePluginError as K, validateVoiceprintPcm16Wav as L, ARKME_EXTENSION_ICON_MAX_BYTES as M, ARKME_EXTENSION_MAX_BYTES as N, canonicalExtensionJson as O, ARKME_EXTENSION_PREVIEW_MAX_BYTES as P, ARKME_DEFAULT_SHARE_WEBSITE as R, inspectNativeBundleArtifactV3 as S, readNativeBundleTarballV3 as T, ARKME_WORLD_PUBLISH_MAX_IMAGES as U, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as V, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as W, ArkmeBundleArtifactError as _, persistentArkmeExtensionActive as a, canonicalBundleJson as b, readPersistentExtensionActivation as c, verifyBundleResolutionSignature as d, verifyExtensionResolutionSignature as f, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as g, ARKME_BUNDLE_MAX_BYTES as h, invokePersistentArkmeExtension as i, ARKME_EXTENSION_FORMAT as j, sha256Hex as k, writePersistentExtensionActivation as l, ARKME_BUNDLE_ARTIFACT_KIND as m, applyPersistentArkmeHostExtension as n, persistentArkmeExtensionRuntimeState as o, verifyNativeBundleResolutionSignature as p, ArkmeOutgoingCallError as q, deactivatePersistentArkmeExtension as r, materializePersistentExtensionBundle as s, activatePersistentArkmeExtension as t, renderArkmeBundleClientBundle as u, arkmeSandboxEntryId as v, packLocalNativeBundleDirectoryV3 as w, inspectBundleArtifact as x, bundleSha256 as y, ARKME_PROVIDER_CONTRACT_VERSION as z };
|