@musnows/scriverse 0.9.5 → 0.9.7
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/dist/ai-protocol.js +6 -0
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai-skills.js +134 -0
- package/dist/ai-skills.js.map +1 -0
- package/dist/ai.js +1546 -62
- package/dist/ai.js.map +1 -1
- package/dist/app.js +143 -19
- package/dist/app.js.map +1 -1
- package/dist/chapter-title-numbering.js +56 -0
- package/dist/chapter-title-numbering.js.map +1 -0
- package/dist/database.js +142 -2
- package/dist/database.js.map +1 -1
- package/dist/public/ai-skill-menu.js +42 -0
- package/dist/public/ai-usage.d.ts +5 -2
- package/dist/public/ai-usage.js +50 -37
- package/dist/public/app.js +917 -242
- package/dist/public/chapter-line-id-tracker.d.ts +6 -0
- package/dist/public/chapter-line-id-tracker.js +24 -0
- package/dist/public/index.html +22 -11
- package/dist/public/model-config.d.ts +1 -0
- package/dist/public/model-config.js +9 -4
- package/dist/public/styles.css +135 -11
- package/dist/remote-mcp.js +496 -0
- package/dist/remote-mcp.js.map +1 -0
- package/dist/security.js +4 -0
- package/dist/security.js.map +1 -1
- package/dist/semantic-search.js +225 -0
- package/dist/semantic-search.js.map +1 -0
- package/dist/skills/continue-writing/SKILL.md +23 -0
- package/dist/skills/polish-writing/SKILL.md +23 -0
- package/dist/store.js +246 -33
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +11 -0
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
package/dist/app.js
CHANGED
|
@@ -17,9 +17,10 @@ import { DEFAULT_AI_CHAT_TAB_LIMIT } from "./ai-chat-tab-limit.js";
|
|
|
17
17
|
import { MAX_AI_STREAM_IDLE_TIMEOUT_SECONDS, MIN_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
|
|
18
18
|
import { AttachmentStorage } from "./attachment-storage.js";
|
|
19
19
|
import { attachmentDownloadFileName, inlineContentDisposition } from "./attachment-download.js";
|
|
20
|
-
import { AiManager } from "./ai.js";
|
|
20
|
+
import { AI_MODEL_KINDS, AiManager } from "./ai.js";
|
|
21
21
|
import { LiteLlmPriceCache } from "./ai-model-pricing.js";
|
|
22
22
|
import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
|
|
23
|
+
import { SEMANTIC_SOURCE_TYPES } from "./semantic-search.js";
|
|
23
24
|
import { CHARACTER_EXTRACTION_MAX_ALIASES, CHARACTER_EXTRACTION_MAX_CANDIDATES, CHARACTER_EXTRACTION_MAX_IDENTITY_LENGTH, CHARACTER_EXTRACTION_MAX_NAME_LENGTH, CHARACTER_EXTRACTION_MAX_SPECIES_LENGTH } from "./character-extraction.js";
|
|
24
25
|
import { AiWritePlanManager, AI_USER_QUESTION_STATUSES, AI_WRITE_PLAN_STATUSES, aiWriteToolDescriptions, aiWriteToolLabels, aiWriteToolsUpdateSchema, answerAiUserQuestionSchema, resolveAiWritePlanMaxOperations } from "./ai-write-plans.js";
|
|
25
26
|
import { CredentialVault } from "./credential-vault.js";
|
|
@@ -32,6 +33,7 @@ import { isOfficialGoogleVertexBaseUrl, parseGoogleServiceAccount } from "./goog
|
|
|
32
33
|
import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, readableHybridSearchTypes } from "./hybrid-search.js";
|
|
33
34
|
import { applyImportFileHints, parseNovelText } from "./parser.js";
|
|
34
35
|
import { MAX_CHAPTER_LINE_IDS } from "./chapter-annotation-anchor.js";
|
|
36
|
+
import { isChapterNumberTemplate } from "./chapter-title-numbering.js";
|
|
35
37
|
import { aiConversationTaskTypes, attachmentPermissionModules, RECYCLE_BIN_RETENTION_DAYS, Store, versionedEntityTypes, WORK_AGENT_TOOL_IDS } from "./store.js";
|
|
36
38
|
import { composeRoleplayStoredUserContent } from "./roleplay-turn.js";
|
|
37
39
|
import { ROLEPLAY_MEMORY_CATEGORIES, ROLEPLAY_MEMORY_CERTAINTY, ROLEPLAY_MEMORY_IMPORTANCE, ROLEPLAY_MEMORY_STATUSES } from "./roleplay-memory.js";
|
|
@@ -485,6 +487,7 @@ const providerUpdateSchema = providerBaseSchema.partial().superRefine((value, ct
|
|
|
485
487
|
const modelSchema = z.object({
|
|
486
488
|
displayName: nonEmpty.max(200),
|
|
487
489
|
modelId: nonEmpty.max(300),
|
|
490
|
+
modelKind: z.enum(AI_MODEL_KINDS).optional(),
|
|
488
491
|
purposes: optionalStrings,
|
|
489
492
|
contextNote: z.string().max(10_000).optional(),
|
|
490
493
|
contextWindow: z.number().int().min(32_768, "模型上下文不能低于 32768 Token").max(2_000_000).optional(),
|
|
@@ -503,7 +506,7 @@ const aiPromptSchema = z.object({
|
|
|
503
506
|
streamIdleTimeoutSeconds: z.number().int().min(MIN_AI_STREAM_IDLE_TIMEOUT_SECONDS).max(MAX_AI_STREAM_IDLE_TIMEOUT_SECONDS).optional()
|
|
504
507
|
});
|
|
505
508
|
const aiUsageQuerySchema = z.object({
|
|
506
|
-
timezoneOffset: z.coerce.number().int().min(-840).max(840).
|
|
509
|
+
timezoneOffset: z.coerce.number().int().min(-840).max(840).optional()
|
|
507
510
|
}).strict();
|
|
508
511
|
const adminAiConversationQuerySchema = z.object({
|
|
509
512
|
page: z.string().optional(),
|
|
@@ -651,11 +654,38 @@ const workAiSettingsSchema = z.object({
|
|
|
651
654
|
titleGenerationModelId: z.string().trim().max(200).optional(),
|
|
652
655
|
imageToolModelId: identifier.nullable().optional()
|
|
653
656
|
}).strict();
|
|
657
|
+
const semanticSearchSettingsSchema = z.object({
|
|
658
|
+
enabled: z.boolean().optional(),
|
|
659
|
+
embeddingModelId: identifier.nullable().optional(),
|
|
660
|
+
rerankModelId: identifier.nullable().optional(),
|
|
661
|
+
vectorDimension: z.number().int().min(1).max(65_536).optional(),
|
|
662
|
+
recallLimit: z.number().int().min(1).max(200).optional(),
|
|
663
|
+
resultLimit: z.number().int().min(1).max(100).optional(),
|
|
664
|
+
budgetTokens: z.number().int().min(256).max(100_000).optional(),
|
|
665
|
+
channelWeight: z.number().min(0.1).max(5).optional()
|
|
666
|
+
}).strict();
|
|
667
|
+
const semanticSearchRequestSchema = z.object({
|
|
668
|
+
query: z.string().trim().min(1).max(2_000),
|
|
669
|
+
types: z.array(z.enum(SEMANTIC_SOURCE_TYPES)).max(SEMANTIC_SOURCE_TYPES.length).optional(),
|
|
670
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
671
|
+
includeKeyword: z.boolean().optional(),
|
|
672
|
+
currentChapterId: identifier.optional(),
|
|
673
|
+
selection: z.string().max(4_000).optional()
|
|
674
|
+
}).strict();
|
|
675
|
+
const semanticSnapshotSchema = z.object({
|
|
676
|
+
query: z.string().trim().min(1).max(2_000),
|
|
677
|
+
entryIds: z.array(identifier).min(1).max(30),
|
|
678
|
+
scope: jsonObject.optional(),
|
|
679
|
+
conversationId: identifier.optional()
|
|
680
|
+
}).strict();
|
|
654
681
|
const contextSchema = z.object({
|
|
655
682
|
type: z.enum(["none", "selection", "chapter", "volume", "book", "settings-catalog", "entities"]),
|
|
656
683
|
chapterId: identifier.optional(),
|
|
657
684
|
volumeId: identifier.optional(),
|
|
658
685
|
selection: z.string().max(200_000).optional(),
|
|
686
|
+
selectionStart: z.number().int().min(0).max(10_000_000).optional(),
|
|
687
|
+
selectionEnd: z.number().int().min(0).max(10_000_000).optional(),
|
|
688
|
+
writingChapterVersion: z.number().int().min(1).max(2_000_000_000).optional(),
|
|
659
689
|
chapterIds: z.array(identifier).max(20).optional(),
|
|
660
690
|
volumeIds: z.array(identifier).max(20).optional(),
|
|
661
691
|
characterIds: optionalStrings,
|
|
@@ -663,6 +693,7 @@ const contextSchema = z.object({
|
|
|
663
693
|
settingIds: optionalStrings,
|
|
664
694
|
raceIds: optionalStrings,
|
|
665
695
|
organizationIds: optionalStrings,
|
|
696
|
+
semanticSnapshotId: identifier.optional(),
|
|
666
697
|
includeBookSummary: z.boolean().optional(),
|
|
667
698
|
includeSettingInfo: z.boolean().optional()
|
|
668
699
|
});
|
|
@@ -1021,7 +1052,7 @@ function redactAiCallContext(record, permissions) {
|
|
|
1021
1052
|
const redactedScope = { ...scope };
|
|
1022
1053
|
let restricted = false;
|
|
1023
1054
|
if (permissions.prose === "none") {
|
|
1024
|
-
for (const field of ["selection", "chapterId", "volumeId", "chapterIds", "includeBookSummary"]) {
|
|
1055
|
+
for (const field of ["selection", "selectionStart", "selectionEnd", "writingChapterVersion", "chapterId", "volumeId", "chapterIds", "includeBookSummary"]) {
|
|
1025
1056
|
if (field in redactedScope) {
|
|
1026
1057
|
delete redactedScope[field];
|
|
1027
1058
|
restricted = true;
|
|
@@ -1977,10 +2008,13 @@ export function createRuntime(options) {
|
|
|
1977
2008
|
});
|
|
1978
2009
|
app.get("/api/works/:workId/chapter-annotations", (request, response) => {
|
|
1979
2010
|
const pagination = parsePagination(request.query);
|
|
2011
|
+
const chapterId = request.query.chapterId === undefined ? undefined : parse(identifier, request.query.chapterId);
|
|
2012
|
+
const query = request.query.q === undefined ? undefined : parse(z.string().trim().max(100), request.query.q);
|
|
2013
|
+
const filters = { chapterId, query };
|
|
1980
2014
|
const permissions = requestPermissions(request, String(request.params.workId));
|
|
1981
2015
|
data(response, pagination
|
|
1982
|
-
? store.listWorkChapterAnnotationsPage(request.params.workId, pagination, readableChapterAnnotationKinds(permissions))
|
|
1983
|
-
: store.listWorkChapterAnnotations(request.params.workId, readableChapterAnnotationKinds(permissions)));
|
|
2016
|
+
? store.listWorkChapterAnnotationsPage(request.params.workId, pagination, readableChapterAnnotationKinds(permissions), filters)
|
|
2017
|
+
: store.listWorkChapterAnnotations(request.params.workId, readableChapterAnnotationKinds(permissions), filters));
|
|
1984
2018
|
});
|
|
1985
2019
|
app.post("/api/chapters/:chapterId/annotations", (request, response) => {
|
|
1986
2020
|
const input = parse(z.object({
|
|
@@ -2017,6 +2051,12 @@ export function createRuntime(options) {
|
|
|
2017
2051
|
z.object({ type: z.literal("move"), volumeId: identifier }).strict(),
|
|
2018
2052
|
z.object({ type: z.literal("setType"), chapterType: chapterTypeSchema }).strict(),
|
|
2019
2053
|
z.object({ type: z.literal("setAnalysisExclusion"), excludedFromAnalysis: z.boolean() }).strict(),
|
|
2054
|
+
z.object({
|
|
2055
|
+
type: z.literal("renumberTitles"),
|
|
2056
|
+
template: z.string().min(1).max(50).refine(isChapterNumberTemplate, "标题格式必须且只能包含一个 {n} 占位符,且不能包含换行或控制字符"),
|
|
2057
|
+
numberStyle: z.enum(["arabic", "chinese"]),
|
|
2058
|
+
startAt: z.number().int().min(1).max(999_999)
|
|
2059
|
+
}).strict(),
|
|
2020
2060
|
z.object({ type: z.literal("delete") }).strict()
|
|
2021
2061
|
]);
|
|
2022
2062
|
const input = parse(z.object({ chapters: selectedChapters, action }).strict(), request.body);
|
|
@@ -2779,8 +2819,8 @@ export function createRuntime(options) {
|
|
|
2779
2819
|
data(response, settings);
|
|
2780
2820
|
});
|
|
2781
2821
|
app.get("/api/platform/ai/usage", (request, response) => {
|
|
2782
|
-
|
|
2783
|
-
data(response, ai.getPlatformTokenUsage(
|
|
2822
|
+
parse(aiUsageQuerySchema, request.query);
|
|
2823
|
+
data(response, ai.getPlatformTokenUsage());
|
|
2784
2824
|
});
|
|
2785
2825
|
app.post("/api/platform/ai/usage/pricing/refresh", async (request, response) => {
|
|
2786
2826
|
if (!request.authUser)
|
|
@@ -2846,13 +2886,33 @@ export function createRuntime(options) {
|
|
|
2846
2886
|
data(response, { ...queued, queuedAt: new Date().toISOString() }, 202);
|
|
2847
2887
|
});
|
|
2848
2888
|
app.get("/api/works/:workId/ai-settings", (request, response) => data(response, store.getWorkAiSettings(request.params.workId)));
|
|
2889
|
+
app.get("/api/works/:workId/ai-settings/mcp-servers", (request, response) => {
|
|
2890
|
+
data(response, ai.getRemoteMcpSettings(request.params.workId));
|
|
2891
|
+
});
|
|
2892
|
+
app.put("/api/works/:workId/ai-settings/mcp-servers", async (request, response) => {
|
|
2893
|
+
data(response, await ai.updateRemoteMcpSettings(request.params.workId, request.body));
|
|
2894
|
+
});
|
|
2849
2895
|
app.get("/api/works/:workId/ai-settings/usage", (request, response) => {
|
|
2850
|
-
|
|
2851
|
-
data(response, ai.getWorkTokenUsage(request.params.workId
|
|
2896
|
+
parse(aiUsageQuerySchema, request.query);
|
|
2897
|
+
data(response, ai.getWorkTokenUsage(request.params.workId));
|
|
2852
2898
|
});
|
|
2853
2899
|
app.get("/api/works/:workId/ai-settings/relationship-search-index", (request, response) => {
|
|
2854
2900
|
data(response, ai.getRelationshipSearchIndexStatus(request.params.workId));
|
|
2855
2901
|
});
|
|
2902
|
+
app.get("/api/works/:workId/ai-settings/semantic-search-index", (request, response) => {
|
|
2903
|
+
data(response, ai.getSemanticSearchIndexStatus(request.params.workId));
|
|
2904
|
+
});
|
|
2905
|
+
app.patch("/api/works/:workId/ai-settings/semantic-search", async (request, response) => {
|
|
2906
|
+
data(response, await ai.updateSemanticSearchSettings(request.params.workId, parse(semanticSearchSettingsSchema, request.body)));
|
|
2907
|
+
});
|
|
2908
|
+
app.post("/api/works/:workId/ai-settings/semantic-search-index/sync", (request, response) => {
|
|
2909
|
+
parse(z.object({}).strict(), request.body ?? {});
|
|
2910
|
+
data(response, ai.syncSemanticSearchIndex(request.params.workId), 202);
|
|
2911
|
+
});
|
|
2912
|
+
app.post("/api/works/:workId/ai-settings/semantic-search-index/rebuild", (request, response) => {
|
|
2913
|
+
parse(z.object({}).strict(), request.body ?? {});
|
|
2914
|
+
data(response, ai.rebuildSemanticSearchIndex(request.params.workId), 202);
|
|
2915
|
+
});
|
|
2856
2916
|
app.post("/api/works/:workId/ai-settings/relationship-search-index/sync", (request, response) => {
|
|
2857
2917
|
const workId = request.params.workId;
|
|
2858
2918
|
const result = ai.syncRelationshipSearchIndex(workId);
|
|
@@ -3032,12 +3092,24 @@ export function createRuntime(options) {
|
|
|
3032
3092
|
ignoreContextWarning: z.boolean().optional()
|
|
3033
3093
|
}).strict(), request.body ?? {});
|
|
3034
3094
|
const conversation = store.getAiConversation(request.params.conversationId);
|
|
3095
|
+
const permissions = requestPermissions(request, String(conversation.workId));
|
|
3096
|
+
const writingSkillRequest = conversation.roleplayCharacter
|
|
3097
|
+
? { skillName: null, instruction: input.instruction }
|
|
3098
|
+
: ai.resolveWritingSkillInstruction(input.instruction);
|
|
3099
|
+
if (writingSkillRequest.skillName && !canReadWorkModule(permissions, "prose")) {
|
|
3100
|
+
throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取当前章节正文以使用写作 Skill 的权限");
|
|
3101
|
+
}
|
|
3102
|
+
const citedInstruction = instructionWithCitations(writingSkillRequest.instruction, input.citations ?? []);
|
|
3103
|
+
const preparedScope = conversation.roleplayCharacter
|
|
3104
|
+
? input.scope
|
|
3105
|
+
: ai.resolveWritingSkillScope(String(conversation.workId), "chat", input.instruction, input.scope);
|
|
3035
3106
|
data(response, await ai.prepareConversationContext({
|
|
3036
3107
|
conversationId: request.params.conversationId,
|
|
3037
3108
|
workId: String(conversation.workId),
|
|
3038
3109
|
modelId: input.modelId,
|
|
3039
|
-
scope:
|
|
3040
|
-
instruction:
|
|
3110
|
+
scope: preparedScope,
|
|
3111
|
+
instruction: citedInstruction,
|
|
3112
|
+
skillInstruction: input.instruction
|
|
3041
3113
|
}, { ignoreWarning: input.ignoreContextWarning === true }));
|
|
3042
3114
|
});
|
|
3043
3115
|
app.post("/api/ai-conversations/:conversationId/compact", async (request, response) => {
|
|
@@ -3226,6 +3298,9 @@ export function createRuntime(options) {
|
|
|
3226
3298
|
const pagination = parsePagination(request.query);
|
|
3227
3299
|
data(response, pagination ? ai.listWorkModelsPage(request.params.workId, pagination) : ai.listWorkModels(request.params.workId));
|
|
3228
3300
|
});
|
|
3301
|
+
app.get("/api/works/:workId/semantic-models", (request, response) => {
|
|
3302
|
+
data(response, ai.listWorkSemanticModels(request.params.workId));
|
|
3303
|
+
});
|
|
3229
3304
|
app.get("/api/works/:workId/task-defaults", (request, response) => {
|
|
3230
3305
|
const pagination = parsePagination(request.query);
|
|
3231
3306
|
data(response, pagination ? ai.listTaskDefaultsPage(request.params.workId, pagination) : ai.listTaskDefaults(request.params.workId));
|
|
@@ -3381,10 +3456,19 @@ export function createRuntime(options) {
|
|
|
3381
3456
|
const storedUserContent = isRoleplay
|
|
3382
3457
|
? composeRoleplayStoredUserContent(sceneDirection, instructionText)
|
|
3383
3458
|
: instructionText;
|
|
3384
|
-
const
|
|
3459
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
3460
|
+
const writingSkillRequest = isRoleplay
|
|
3461
|
+
? { skillName: null, instruction: instructionText }
|
|
3462
|
+
: ai.resolveWritingSkillInstruction(instructionText);
|
|
3463
|
+
const resolvedInstruction = instructionWithCitations(writingSkillRequest.instruction, citations);
|
|
3464
|
+
if (writingSkillRequest.skillName && !canReadWorkModule(permissions, "prose")) {
|
|
3465
|
+
throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取当前章节正文以使用写作 Skill 的权限");
|
|
3466
|
+
}
|
|
3467
|
+
const requestScope = isRoleplay
|
|
3468
|
+
? input.scope
|
|
3469
|
+
: ai.resolveWritingSkillScope(request.params.workId, "chat", instructionText, input.scope);
|
|
3385
3470
|
const mentionSource = [sceneDirection, resolvedInstruction].filter(Boolean).join("\n");
|
|
3386
3471
|
const modelId = resolveConversationModelId(request.params.workId, conversationId, input.modelId);
|
|
3387
|
-
const permissions = requestPermissions(request, request.params.workId);
|
|
3388
3472
|
const controller = new AbortController();
|
|
3389
3473
|
let streamRequestId = null;
|
|
3390
3474
|
let streamRequestFinished = false;
|
|
@@ -3439,8 +3523,9 @@ export function createRuntime(options) {
|
|
|
3439
3523
|
conversationId,
|
|
3440
3524
|
workId: request.params.workId,
|
|
3441
3525
|
modelId,
|
|
3442
|
-
scope:
|
|
3526
|
+
scope: requestScope,
|
|
3443
3527
|
instruction: mentionSource,
|
|
3528
|
+
skillInstruction: instructionText,
|
|
3444
3529
|
excludeConversationMessageId: input.currentMessageId
|
|
3445
3530
|
}, { ignoreWarning: input.ignoreContextWarning === true });
|
|
3446
3531
|
preparedConversation = redactAiConversation(store.getAiConversationSummary(conversationId), permissions);
|
|
@@ -3461,12 +3546,12 @@ export function createRuntime(options) {
|
|
|
3461
3546
|
}
|
|
3462
3547
|
}
|
|
3463
3548
|
const resolvedScope = input.currentMessageId
|
|
3464
|
-
?
|
|
3549
|
+
? requestScope
|
|
3465
3550
|
: ai.resolveInstructionMentions({
|
|
3466
3551
|
workId: request.params.workId,
|
|
3467
3552
|
taskType: "chat",
|
|
3468
3553
|
instruction: mentionSource,
|
|
3469
|
-
scope:
|
|
3554
|
+
scope: requestScope,
|
|
3470
3555
|
conversationId
|
|
3471
3556
|
});
|
|
3472
3557
|
const mentionCharacterIds = [...new Set([
|
|
@@ -3485,11 +3570,12 @@ export function createRuntime(options) {
|
|
|
3485
3570
|
content: storedUserContent,
|
|
3486
3571
|
citations,
|
|
3487
3572
|
...(input.currentMessageId ? { existingMessageId: input.currentMessageId } : {}),
|
|
3488
|
-
...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length || input.imageAttachmentIds?.length) ? { metadata: {
|
|
3573
|
+
...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length || input.imageAttachmentIds?.length || input.scope.semanticSnapshotId) ? { metadata: {
|
|
3489
3574
|
...(modelId ? { modelId } : {}),
|
|
3490
3575
|
...(mentionCharacterIds.length ? { mentionCharacterIds } : {}),
|
|
3491
3576
|
...(mentionRaceIds.length ? { mentionRaceIds } : {}),
|
|
3492
3577
|
...(mentionOrganizationIds.length ? { mentionOrganizationIds } : {}),
|
|
3578
|
+
...(input.scope.semanticSnapshotId ? { semanticSnapshotId: input.scope.semanticSnapshotId } : {}),
|
|
3493
3579
|
...(input.imageAttachmentIds?.length ? { chatImageAttachmentIds: [...new Set(input.imageAttachmentIds)] } : {})
|
|
3494
3580
|
} } : {})
|
|
3495
3581
|
}
|
|
@@ -3504,6 +3590,15 @@ export function createRuntime(options) {
|
|
|
3504
3590
|
}
|
|
3505
3591
|
if (begun.request.status === "completed" && begun.assistantMessage) {
|
|
3506
3592
|
const content = String(begun.assistantMessage.content ?? "");
|
|
3593
|
+
const assistantMetadata = begun.assistantMessage.metadata && typeof begun.assistantMessage.metadata === "object"
|
|
3594
|
+
? begun.assistantMessage.metadata
|
|
3595
|
+
: {};
|
|
3596
|
+
const writingSuggestionId = typeof assistantMetadata.writingSuggestionId === "string"
|
|
3597
|
+
? assistantMetadata.writingSuggestionId
|
|
3598
|
+
: "";
|
|
3599
|
+
const writingSuggestion = writingSuggestionId
|
|
3600
|
+
? redactSuggestion(ai.getSuggestion(writingSuggestionId), permissions)
|
|
3601
|
+
: null;
|
|
3507
3602
|
if (content)
|
|
3508
3603
|
sendEvent("delta", { delta: content, replayed: true });
|
|
3509
3604
|
sendEvent("complete", {
|
|
@@ -3511,7 +3606,8 @@ export function createRuntime(options) {
|
|
|
3511
3606
|
conversationId,
|
|
3512
3607
|
conversationTitle: store.getAiConversationSummary(conversationId).title,
|
|
3513
3608
|
messageId: begun.assistantMessage.id,
|
|
3514
|
-
messageCreatedAt: begun.assistantMessage.createdAt
|
|
3609
|
+
messageCreatedAt: begun.assistantMessage.createdAt,
|
|
3610
|
+
...(writingSuggestion ? { writingSuggestion } : {})
|
|
3515
3611
|
});
|
|
3516
3612
|
}
|
|
3517
3613
|
else {
|
|
@@ -3543,9 +3639,10 @@ export function createRuntime(options) {
|
|
|
3543
3639
|
const suggestion = await ai.createStreamingChat({
|
|
3544
3640
|
workId: request.params.workId,
|
|
3545
3641
|
instruction: resolvedInstruction,
|
|
3642
|
+
skillInstruction: instructionText,
|
|
3546
3643
|
...(sceneDirection ? { sceneDirection } : {}),
|
|
3547
3644
|
// 仍由生成路径基于原始范围持久化累计注入,保证预解析不会吞掉本轮自动命中。
|
|
3548
|
-
scope:
|
|
3645
|
+
scope: requestScope,
|
|
3549
3646
|
signal: controller.signal,
|
|
3550
3647
|
onToolCall: (toolCall, round) => sendEvent("tool_call", { ...toolCall, round }),
|
|
3551
3648
|
onProcessStep: (step) => sendEvent("process_step", step),
|
|
@@ -3578,6 +3675,7 @@ export function createRuntime(options) {
|
|
|
3578
3675
|
contextUsage: suggestion.contextUsage,
|
|
3579
3676
|
conversationId,
|
|
3580
3677
|
conversationTitle: suggestion.conversationTitle,
|
|
3678
|
+
...(suggestion.action !== "note" ? { writingSuggestion: redactSuggestion(suggestion, permissions) } : {}),
|
|
3581
3679
|
messageId: typeof suggestion.conversationMessage === "object" && suggestion.conversationMessage !== null
|
|
3582
3680
|
? suggestion.conversationMessage.id
|
|
3583
3681
|
: undefined,
|
|
@@ -3640,6 +3738,11 @@ export function createRuntime(options) {
|
|
|
3640
3738
|
});
|
|
3641
3739
|
app.post("/api/suggestions/:suggestionId/accept", (request, response) => {
|
|
3642
3740
|
const input = parse(z.object({ content: z.string().max(2_000_000).optional() }), request.body ?? {});
|
|
3741
|
+
const suggestion = ai.getSuggestion(request.params.suggestionId);
|
|
3742
|
+
const permissions = requestPermissions(request, String(suggestion.workId));
|
|
3743
|
+
if (!canWriteWorkModule(permissions, "prose")) {
|
|
3744
|
+
throw new AppError(403, "WORK_MODULE_WRITE_DENIED", "你没有将 AI 建议写入正文的权限");
|
|
3745
|
+
}
|
|
3643
3746
|
data(response, ai.acceptSuggestion(request.params.suggestionId, input.content));
|
|
3644
3747
|
});
|
|
3645
3748
|
app.post("/api/suggestions/:suggestionId/reject", (request, response) => {
|
|
@@ -3668,6 +3771,27 @@ export function createRuntime(options) {
|
|
|
3668
3771
|
conversationOwnerUserId: request.authUser?.userId
|
|
3669
3772
|
}));
|
|
3670
3773
|
});
|
|
3774
|
+
app.post("/api/works/:workId/semantic-search", async (request, response) => {
|
|
3775
|
+
const input = parse(semanticSearchRequestSchema, request.body);
|
|
3776
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
3777
|
+
const allowedTypes = readableHybridSearchTypes(permissions)
|
|
3778
|
+
.filter((type) => SEMANTIC_SOURCE_TYPES.includes(type));
|
|
3779
|
+
data(response, await ai.semanticSearchStory(request.params.workId, input.query, {
|
|
3780
|
+
allowedTypes,
|
|
3781
|
+
types: input.types,
|
|
3782
|
+
limit: input.limit,
|
|
3783
|
+
includeKeyword: input.includeKeyword,
|
|
3784
|
+
conversationOwnerUserId: request.authUser?.userId,
|
|
3785
|
+
currentChapterId: input.currentChapterId,
|
|
3786
|
+
selection: input.selection
|
|
3787
|
+
}));
|
|
3788
|
+
});
|
|
3789
|
+
app.post("/api/works/:workId/semantic-search/snapshots", (request, response) => {
|
|
3790
|
+
const input = parse(semanticSnapshotSchema, request.body);
|
|
3791
|
+
if (input.conversationId)
|
|
3792
|
+
assertRequestAiConversationOwner(request, input.conversationId);
|
|
3793
|
+
data(response, ai.createSemanticContextSnapshot(request.params.workId, input), 201);
|
|
3794
|
+
});
|
|
3671
3795
|
app.head("/api/works/:workId/export", (request, response) => {
|
|
3672
3796
|
parse(z.enum(["epub"]), request.query.format ?? "epub");
|
|
3673
3797
|
store.getWork(request.params.workId);
|