@musnows/scriverse 0.9.4 → 0.9.6
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-write-plans.js +2279 -0
- package/dist/ai-write-plans.js.map +1 -0
- package/dist/ai.js +2781 -154
- package/dist/ai.js.map +1 -1
- package/dist/app.js +328 -17
- package/dist/app.js.map +1 -1
- package/dist/chapter-annotation-anchor.js +327 -0
- package/dist/chapter-annotation-anchor.js.map +1 -0
- package/dist/chapter-title-numbering.js +56 -0
- package/dist/chapter-title-numbering.js.map +1 -0
- package/dist/database.js +478 -3
- package/dist/database.js.map +1 -1
- package/dist/public/ai-context-meter.js +3 -3
- package/dist/public/ai-interactive.js +483 -0
- package/dist/public/app.js +1991 -268
- package/dist/public/chapter-editor-behavior.js +40 -0
- package/dist/public/chapter-line-id-tracker.d.ts +25 -0
- package/dist/public/chapter-line-id-tracker.js +151 -0
- package/dist/public/index.html +79 -19
- package/dist/public/model-config.d.ts +1 -0
- package/dist/public/model-config.js +9 -4
- package/dist/public/styles.css +337 -57
- package/dist/public/theme-init.js +25 -1
- package/dist/remote-mcp.js +496 -0
- package/dist/remote-mcp.js.map +1 -0
- package/dist/roleplay-memory.js +53 -0
- package/dist/roleplay-memory.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 +695 -51
- package/dist/store.js.map +1 -1
- package/dist/ui-module-preload.js +27 -0
- package/dist/ui-module-preload.js.map +1 -0
- package/dist/user-auth.js +33 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
package/dist/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import compression from "compression";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import JSZip from "jszip";
|
|
3
4
|
import multer from "multer";
|
|
@@ -16,10 +17,12 @@ import { DEFAULT_AI_CHAT_TAB_LIMIT } from "./ai-chat-tab-limit.js";
|
|
|
16
17
|
import { MAX_AI_STREAM_IDLE_TIMEOUT_SECONDS, MIN_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
|
|
17
18
|
import { AttachmentStorage } from "./attachment-storage.js";
|
|
18
19
|
import { attachmentDownloadFileName, inlineContentDisposition } from "./attachment-download.js";
|
|
19
|
-
import { AiManager } from "./ai.js";
|
|
20
|
+
import { AI_MODEL_KINDS, AiManager } from "./ai.js";
|
|
20
21
|
import { LiteLlmPriceCache } from "./ai-model-pricing.js";
|
|
21
22
|
import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
|
|
23
|
+
import { SEMANTIC_SOURCE_TYPES } from "./semantic-search.js";
|
|
22
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";
|
|
25
|
+
import { AiWritePlanManager, AI_USER_QUESTION_STATUSES, AI_WRITE_PLAN_STATUSES, aiWriteToolDescriptions, aiWriteToolLabels, aiWriteToolsUpdateSchema, answerAiUserQuestionSchema, resolveAiWritePlanMaxOperations } from "./ai-write-plans.js";
|
|
23
26
|
import { CredentialVault } from "./credential-vault.js";
|
|
24
27
|
import { Database } from "./database.js";
|
|
25
28
|
import { assertSafeDocxArchive } from "./docx-security.js";
|
|
@@ -29,8 +32,11 @@ import { AppError } from "./errors.js";
|
|
|
29
32
|
import { isOfficialGoogleVertexBaseUrl, parseGoogleServiceAccount } from "./google-vertex-auth.js";
|
|
30
33
|
import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, readableHybridSearchTypes } from "./hybrid-search.js";
|
|
31
34
|
import { applyImportFileHints, parseNovelText } from "./parser.js";
|
|
35
|
+
import { MAX_CHAPTER_LINE_IDS } from "./chapter-annotation-anchor.js";
|
|
36
|
+
import { isChapterNumberTemplate } from "./chapter-title-numbering.js";
|
|
32
37
|
import { aiConversationTaskTypes, attachmentPermissionModules, RECYCLE_BIN_RETENTION_DAYS, Store, versionedEntityTypes, WORK_AGENT_TOOL_IDS } from "./store.js";
|
|
33
38
|
import { composeRoleplayStoredUserContent } from "./roleplay-turn.js";
|
|
39
|
+
import { ROLEPLAY_MEMORY_CATEGORIES, ROLEPLAY_MEMORY_CERTAINTY, ROLEPLAY_MEMORY_IMPORTANCE, ROLEPLAY_MEMORY_STATUSES } from "./roleplay-memory.js";
|
|
34
40
|
import { paginated, parsePagination } from "./pagination.js";
|
|
35
41
|
import { normalizeUploadFileName } from "./utils.js";
|
|
36
42
|
import { assertSafeAiEndpoint, assertSafeS3Endpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
|
|
@@ -50,6 +56,7 @@ import { canReadWorkModule, canWriteWorkModule, chapterAnnotationPermissionModul
|
|
|
50
56
|
import { CollaborationPresence, editorPageKey, entityEditorPageKey, modulePageKey, presencePageKinds } from "./collaboration-presence.js";
|
|
51
57
|
import { PresenceStore } from "./presence-store.js";
|
|
52
58
|
import { analysisTaskReadModules, clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, relationshipAnalysisReadModules, setSessionCookie, UserAuthService } from "./user-auth.js";
|
|
59
|
+
import { extractStaticModuleImports, injectModulePreloads } from "./ui-module-preload.js";
|
|
53
60
|
const chapterAnnotationKinds = ["note", "todo"];
|
|
54
61
|
function readableChapterAnnotationKinds(permissions) {
|
|
55
62
|
return chapterAnnotationKinds.filter((kind) => canReadWorkModule(permissions, chapterAnnotationPermissionModule(kind)));
|
|
@@ -62,6 +69,23 @@ const optionalStrings = z.array(z.string()).optional();
|
|
|
62
69
|
const jsonObject = z.record(z.string(), z.unknown());
|
|
63
70
|
const chapterTypeSchema = z.enum(["正文", "设定", "作者的话", "其他"]);
|
|
64
71
|
const aiConversationTaskTypeSchema = z.enum(aiConversationTaskTypes);
|
|
72
|
+
const roleplayMemoryInputSchema = z.object({
|
|
73
|
+
category: z.enum(ROLEPLAY_MEMORY_CATEGORIES),
|
|
74
|
+
content: z.string().trim().min(1).max(2_000),
|
|
75
|
+
importance: z.enum(ROLEPLAY_MEMORY_IMPORTANCE).optional(),
|
|
76
|
+
certainty: z.enum(ROLEPLAY_MEMORY_CERTAINTY).optional(),
|
|
77
|
+
isPinned: z.boolean().optional()
|
|
78
|
+
}).strict();
|
|
79
|
+
const roleplayMemoryListQuerySchema = z.object({
|
|
80
|
+
q: z.string().trim().max(200).optional(),
|
|
81
|
+
categories: z.preprocess((value) => typeof value === "string" ? value.split(",").filter(Boolean) : value, z.array(z.enum(ROLEPLAY_MEMORY_CATEGORIES)).max(ROLEPLAY_MEMORY_CATEGORIES.length).optional()),
|
|
82
|
+
statuses: z.preprocess((value) => typeof value === "string" ? value.split(",").filter(Boolean) : value, z.array(z.enum(ROLEPLAY_MEMORY_STATUSES)).max(ROLEPLAY_MEMORY_STATUSES.length).optional()),
|
|
83
|
+
cursor: z.coerce.number().int().min(0).max(100_000).optional(),
|
|
84
|
+
limit: z.coerce.number().int().min(1).max(100).optional()
|
|
85
|
+
}).strict();
|
|
86
|
+
const roleplayMemoryUpdateSchema = roleplayMemoryInputSchema.partial().extend({
|
|
87
|
+
expectedVersion: z.number().int().min(1)
|
|
88
|
+
}).strict().refine((value) => Object.keys(value).some((key) => key !== "expectedVersion"), "至少提供一个要修改的字段");
|
|
65
89
|
const versionedEntityTypeSchema = z.enum(versionedEntityTypes);
|
|
66
90
|
const attachmentPermissionModuleSchema = z.enum(attachmentPermissionModules);
|
|
67
91
|
const maximumImportedTextLength = 20_000_000;
|
|
@@ -211,7 +235,9 @@ const workSchema = z.object({
|
|
|
211
235
|
description: z.string().max(10_000).optional(),
|
|
212
236
|
language: z.string().max(30).optional(),
|
|
213
237
|
coverUrl: z.string().url().nullable().optional(),
|
|
214
|
-
tags: optionalStrings
|
|
238
|
+
tags: optionalStrings,
|
|
239
|
+
editorAutoIndentEnabled: z.boolean().optional(),
|
|
240
|
+
editorTypewriterModeEnabled: z.boolean().optional()
|
|
215
241
|
});
|
|
216
242
|
const workOfflineAccessSchema = z.object({ enabled: z.boolean() }).strict();
|
|
217
243
|
const settingSchema = z.object({
|
|
@@ -461,6 +487,7 @@ const providerUpdateSchema = providerBaseSchema.partial().superRefine((value, ct
|
|
|
461
487
|
const modelSchema = z.object({
|
|
462
488
|
displayName: nonEmpty.max(200),
|
|
463
489
|
modelId: nonEmpty.max(300),
|
|
490
|
+
modelKind: z.enum(AI_MODEL_KINDS).optional(),
|
|
464
491
|
purposes: optionalStrings,
|
|
465
492
|
contextNote: z.string().max(10_000).optional(),
|
|
466
493
|
contextWindow: z.number().int().min(32_768, "模型上下文不能低于 32768 Token").max(2_000_000).optional(),
|
|
@@ -627,11 +654,38 @@ const workAiSettingsSchema = z.object({
|
|
|
627
654
|
titleGenerationModelId: z.string().trim().max(200).optional(),
|
|
628
655
|
imageToolModelId: identifier.nullable().optional()
|
|
629
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();
|
|
630
681
|
const contextSchema = z.object({
|
|
631
682
|
type: z.enum(["none", "selection", "chapter", "volume", "book", "settings-catalog", "entities"]),
|
|
632
683
|
chapterId: identifier.optional(),
|
|
633
684
|
volumeId: identifier.optional(),
|
|
634
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(),
|
|
635
689
|
chapterIds: z.array(identifier).max(20).optional(),
|
|
636
690
|
volumeIds: z.array(identifier).max(20).optional(),
|
|
637
691
|
characterIds: optionalStrings,
|
|
@@ -639,6 +693,7 @@ const contextSchema = z.object({
|
|
|
639
693
|
settingIds: optionalStrings,
|
|
640
694
|
raceIds: optionalStrings,
|
|
641
695
|
organizationIds: optionalStrings,
|
|
696
|
+
semanticSnapshotId: identifier.optional(),
|
|
642
697
|
includeBookSummary: z.boolean().optional(),
|
|
643
698
|
includeSettingInfo: z.boolean().optional()
|
|
644
699
|
});
|
|
@@ -997,7 +1052,7 @@ function redactAiCallContext(record, permissions) {
|
|
|
997
1052
|
const redactedScope = { ...scope };
|
|
998
1053
|
let restricted = false;
|
|
999
1054
|
if (permissions.prose === "none") {
|
|
1000
|
-
for (const field of ["selection", "chapterId", "volumeId", "chapterIds", "includeBookSummary"]) {
|
|
1055
|
+
for (const field of ["selection", "selectionStart", "selectionEnd", "writingChapterVersion", "chapterId", "volumeId", "chapterIds", "includeBookSummary"]) {
|
|
1001
1056
|
if (field in redactedScope) {
|
|
1002
1057
|
delete redactedScope[field];
|
|
1003
1058
|
restricted = true;
|
|
@@ -1340,6 +1395,15 @@ export function createRuntime(options) {
|
|
|
1340
1395
|
liteLlmPriceCache,
|
|
1341
1396
|
allowPrivateAiEndpoints: options.security?.allowPrivateAiEndpoints === true
|
|
1342
1397
|
});
|
|
1398
|
+
// AI 可写工具与审批工作流:计划创建只能由侧边栏 AI 发起,确认入口只接收审批 ID。
|
|
1399
|
+
const aiWritePlanManager = new AiWritePlanManager({
|
|
1400
|
+
database,
|
|
1401
|
+
store,
|
|
1402
|
+
auth,
|
|
1403
|
+
resolveAnalysisTask: (workId, input) => ai.resolveTaskInput(workId, input),
|
|
1404
|
+
startAnalysisTask: (workId, input) => store.createTask(workId, input)
|
|
1405
|
+
});
|
|
1406
|
+
ai.attachWritePlanManager(aiWritePlanManager);
|
|
1343
1407
|
const app = express();
|
|
1344
1408
|
enforceCaseInsensitiveRouting(app);
|
|
1345
1409
|
const upload = multer({
|
|
@@ -1905,7 +1969,7 @@ export function createRuntime(options) {
|
|
|
1905
1969
|
});
|
|
1906
1970
|
app.get("/api/chapters/:chapterId", (request, response) => data(response, store.getChapter(request.params.chapterId)));
|
|
1907
1971
|
app.patch("/api/chapters/:chapterId", (request, response) => {
|
|
1908
|
-
const input = parse(z.object({ title: nonEmpty.max(300).optional(), content: z.string().max(2_000_000).optional(), excludedFromAnalysis: z.boolean().optional(), chapterType: chapterTypeSchema.optional(), source: z.enum(["manual", "auto"]).optional(), changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1972
|
+
const input = parse(z.object({ title: nonEmpty.max(300).optional(), content: z.string().max(2_000_000).optional(), lineIds: z.array(z.union([identifier, z.null()])).max(MAX_CHAPTER_LINE_IDS).optional(), excludedFromAnalysis: z.boolean().optional(), chapterType: chapterTypeSchema.optional(), source: z.enum(["manual", "auto"]).optional(), changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1909
1973
|
const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
|
|
1910
1974
|
const chapter = store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo);
|
|
1911
1975
|
publishEditorChange(String(chapter.workId), String(chapter.id));
|
|
@@ -1944,10 +2008,13 @@ export function createRuntime(options) {
|
|
|
1944
2008
|
});
|
|
1945
2009
|
app.get("/api/works/:workId/chapter-annotations", (request, response) => {
|
|
1946
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 };
|
|
1947
2014
|
const permissions = requestPermissions(request, String(request.params.workId));
|
|
1948
2015
|
data(response, pagination
|
|
1949
|
-
? store.listWorkChapterAnnotationsPage(request.params.workId, pagination, readableChapterAnnotationKinds(permissions))
|
|
1950
|
-
: 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));
|
|
1951
2018
|
});
|
|
1952
2019
|
app.post("/api/chapters/:chapterId/annotations", (request, response) => {
|
|
1953
2020
|
const input = parse(z.object({
|
|
@@ -1984,6 +2051,12 @@ export function createRuntime(options) {
|
|
|
1984
2051
|
z.object({ type: z.literal("move"), volumeId: identifier }).strict(),
|
|
1985
2052
|
z.object({ type: z.literal("setType"), chapterType: chapterTypeSchema }).strict(),
|
|
1986
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(),
|
|
1987
2060
|
z.object({ type: z.literal("delete") }).strict()
|
|
1988
2061
|
]);
|
|
1989
2062
|
const input = parse(z.object({ chapters: selectedChapters, action }).strict(), request.body);
|
|
@@ -2813,6 +2886,12 @@ export function createRuntime(options) {
|
|
|
2813
2886
|
data(response, { ...queued, queuedAt: new Date().toISOString() }, 202);
|
|
2814
2887
|
});
|
|
2815
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
|
+
});
|
|
2816
2895
|
app.get("/api/works/:workId/ai-settings/usage", (request, response) => {
|
|
2817
2896
|
const query = parse(aiUsageQuerySchema, request.query);
|
|
2818
2897
|
data(response, ai.getWorkTokenUsage(request.params.workId, query.timezoneOffset));
|
|
@@ -2820,6 +2899,20 @@ export function createRuntime(options) {
|
|
|
2820
2899
|
app.get("/api/works/:workId/ai-settings/relationship-search-index", (request, response) => {
|
|
2821
2900
|
data(response, ai.getRelationshipSearchIndexStatus(request.params.workId));
|
|
2822
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
|
+
});
|
|
2823
2916
|
app.post("/api/works/:workId/ai-settings/relationship-search-index/sync", (request, response) => {
|
|
2824
2917
|
const workId = request.params.workId;
|
|
2825
2918
|
const result = ai.syncRelationshipSearchIndex(workId);
|
|
@@ -2875,6 +2968,16 @@ export function createRuntime(options) {
|
|
|
2875
2968
|
data(response, store.createAiConversation(request.params.workId, input.title, input.taskType), 201);
|
|
2876
2969
|
});
|
|
2877
2970
|
app.use("/api/ai-conversations/:conversationId", (request, _response, next) => {
|
|
2971
|
+
const sourceId = typeof request.query.roleplayMemorySourceId === "string"
|
|
2972
|
+
? request.query.roleplayMemorySourceId
|
|
2973
|
+
: null;
|
|
2974
|
+
const messageId = typeof request.query.messageId === "string" ? request.query.messageId : null;
|
|
2975
|
+
if (["GET", "HEAD"].includes(request.method)
|
|
2976
|
+
&& request.authUser?.role === "admin"
|
|
2977
|
+
&& sourceId !== null
|
|
2978
|
+
&& messageId !== null
|
|
2979
|
+
&& store.isRoleplayMemorySourceTarget(sourceId, request.params.conversationId, messageId))
|
|
2980
|
+
return next();
|
|
2878
2981
|
assertRequestAiConversationOwner(request, request.params.conversationId);
|
|
2879
2982
|
next();
|
|
2880
2983
|
});
|
|
@@ -2942,6 +3045,20 @@ export function createRuntime(options) {
|
|
|
2942
3045
|
const permissions = requestPermissions(request, String(updated.workId));
|
|
2943
3046
|
data(response, redactAiConversation(updated, permissions));
|
|
2944
3047
|
});
|
|
3048
|
+
app.get("/api/characters/:characterId/roleplay-memories", (request, response) => {
|
|
3049
|
+
const query = parse(roleplayMemoryListQuerySchema, request.query);
|
|
3050
|
+
data(response, store.listRoleplayMemories(request.params.characterId, {
|
|
3051
|
+
query: query.q,
|
|
3052
|
+
categories: query.categories,
|
|
3053
|
+
statuses: query.statuses,
|
|
3054
|
+
cursor: query.cursor,
|
|
3055
|
+
limit: query.limit
|
|
3056
|
+
}));
|
|
3057
|
+
});
|
|
3058
|
+
app.post("/api/characters/:characterId/roleplay-memories", (request, response) => {
|
|
3059
|
+
const input = parse(roleplayMemoryInputSchema, request.body);
|
|
3060
|
+
data(response, store.createRoleplayMemory(request.params.characterId, input), 201);
|
|
3061
|
+
});
|
|
2945
3062
|
app.post("/api/ai-conversations/:conversationId/messages", (request, response) => {
|
|
2946
3063
|
const input = parse(z.object({
|
|
2947
3064
|
role: z.enum(["user", "assistant"]),
|
|
@@ -2975,12 +3092,24 @@ export function createRuntime(options) {
|
|
|
2975
3092
|
ignoreContextWarning: z.boolean().optional()
|
|
2976
3093
|
}).strict(), request.body ?? {});
|
|
2977
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);
|
|
2978
3106
|
data(response, await ai.prepareConversationContext({
|
|
2979
3107
|
conversationId: request.params.conversationId,
|
|
2980
3108
|
workId: String(conversation.workId),
|
|
2981
3109
|
modelId: input.modelId,
|
|
2982
|
-
scope:
|
|
2983
|
-
instruction:
|
|
3110
|
+
scope: preparedScope,
|
|
3111
|
+
instruction: citedInstruction,
|
|
3112
|
+
skillInstruction: input.instruction
|
|
2984
3113
|
}, { ignoreWarning: input.ignoreContextWarning === true }));
|
|
2985
3114
|
});
|
|
2986
3115
|
app.post("/api/ai-conversations/:conversationId/compact", async (request, response) => {
|
|
@@ -3004,6 +3133,130 @@ export function createRuntime(options) {
|
|
|
3004
3133
|
})
|
|
3005
3134
|
});
|
|
3006
3135
|
});
|
|
3136
|
+
app.patch("/api/roleplay-memories/:memoryId", (request, response) => {
|
|
3137
|
+
const input = parse(roleplayMemoryUpdateSchema, request.body);
|
|
3138
|
+
data(response, store.updateRoleplayMemory(request.params.memoryId, input));
|
|
3139
|
+
});
|
|
3140
|
+
app.delete("/api/roleplay-memories/:memoryId", (request, response) => {
|
|
3141
|
+
const input = parse(z.object({ expectedVersion: z.number().int().min(1) }).strict(), request.body);
|
|
3142
|
+
data(response, store.setRoleplayMemoryArchived(request.params.memoryId, true, input.expectedVersion));
|
|
3143
|
+
});
|
|
3144
|
+
app.post("/api/roleplay-memories/:memoryId/restore", (request, response) => {
|
|
3145
|
+
const input = parse(z.object({ expectedVersion: z.number().int().min(1) }).strict(), request.body);
|
|
3146
|
+
data(response, store.setRoleplayMemoryArchived(request.params.memoryId, false, input.expectedVersion));
|
|
3147
|
+
});
|
|
3148
|
+
// ---------------------------------------------------------------- AI 可写工具与审批中心
|
|
3149
|
+
const planViewer = () => {
|
|
3150
|
+
const actor = currentRequestActor();
|
|
3151
|
+
return actor ? { userId: actor.userId, role: actor.role } : null;
|
|
3152
|
+
};
|
|
3153
|
+
app.get("/api/works/:workId/ai/tools", (request, response) => {
|
|
3154
|
+
store.getWork(request.params.workId);
|
|
3155
|
+
data(response, {
|
|
3156
|
+
tools: aiWritePlanManager.getEnabledTools(request.params.workId),
|
|
3157
|
+
labels: aiWriteToolLabels,
|
|
3158
|
+
descriptions: aiWriteToolDescriptions,
|
|
3159
|
+
maxOperations: resolveAiWritePlanMaxOperations(process.env.AI_WRITE_PLAN_MAX_OPERATIONS)
|
|
3160
|
+
});
|
|
3161
|
+
});
|
|
3162
|
+
app.put("/api/works/:workId/ai/tools", (request, response) => {
|
|
3163
|
+
const input = parse(aiWriteToolsUpdateSchema, request.body ?? {});
|
|
3164
|
+
data(response, {
|
|
3165
|
+
tools: aiWritePlanManager.updateToolSettings(request.params.workId, input.tools, currentRequestActor()?.userId ?? null)
|
|
3166
|
+
});
|
|
3167
|
+
});
|
|
3168
|
+
app.get("/api/works/:workId/ai/write-plans", (request, response) => {
|
|
3169
|
+
const status = typeof request.query.status === "string"
|
|
3170
|
+
? parse(z.enum(AI_WRITE_PLAN_STATUSES), request.query.status)
|
|
3171
|
+
: undefined;
|
|
3172
|
+
const limit = Number.parseInt(typeof request.query.limit === "string" ? request.query.limit : "", 10);
|
|
3173
|
+
data(response, {
|
|
3174
|
+
plans: aiWritePlanManager.listPlansForWork(request.params.workId, planViewer(), {
|
|
3175
|
+
status,
|
|
3176
|
+
...(Number.isFinite(limit) ? { limit } : {})
|
|
3177
|
+
})
|
|
3178
|
+
});
|
|
3179
|
+
});
|
|
3180
|
+
app.get("/api/works/:workId/ai/write-plans/:planId", (request, response) => {
|
|
3181
|
+
data(response, aiWritePlanManager.getPlanDetail(request.params.planId, request.params.workId, planViewer()));
|
|
3182
|
+
});
|
|
3183
|
+
app.post("/api/works/:workId/ai/write-plans/:planId/confirm", async (request, response) => {
|
|
3184
|
+
data(response, await aiWritePlanManager.confirmPlan(request.params.planId, request.params.workId, planViewer()));
|
|
3185
|
+
});
|
|
3186
|
+
app.post("/api/works/:workId/ai/write-plans/:planId/reject", (request, response) => {
|
|
3187
|
+
data(response, aiWritePlanManager.rejectPlan(request.params.planId, request.params.workId, planViewer()));
|
|
3188
|
+
});
|
|
3189
|
+
app.post("/api/works/:workId/ai/write-plans/:planId/undo", (request, response) => {
|
|
3190
|
+
data(response, aiWritePlanManager.createUndoPlan(request.params.planId, request.params.workId, planViewer()), 201);
|
|
3191
|
+
});
|
|
3192
|
+
app.get("/api/works/:workId/ai/questions", (request, response) => {
|
|
3193
|
+
const conversationId = typeof request.query.conversationId === "string"
|
|
3194
|
+
? parse(identifier, request.query.conversationId)
|
|
3195
|
+
: undefined;
|
|
3196
|
+
const status = typeof request.query.status === "string"
|
|
3197
|
+
? parse(z.enum(AI_USER_QUESTION_STATUSES), request.query.status)
|
|
3198
|
+
: undefined;
|
|
3199
|
+
const limit = Number.parseInt(typeof request.query.limit === "string" ? request.query.limit : "", 10);
|
|
3200
|
+
data(response, {
|
|
3201
|
+
questions: aiWritePlanManager.listQuestions(request.params.workId, planViewer(), {
|
|
3202
|
+
...(conversationId !== undefined ? { conversationId } : {}),
|
|
3203
|
+
...(status !== undefined ? { status } : {}),
|
|
3204
|
+
...(Number.isFinite(limit) ? { limit } : {})
|
|
3205
|
+
})
|
|
3206
|
+
});
|
|
3207
|
+
});
|
|
3208
|
+
app.get("/api/works/:workId/ai/questions/:questionId", (request, response) => {
|
|
3209
|
+
data(response, aiWritePlanManager.getQuestion(request.params.questionId, request.params.workId, planViewer()));
|
|
3210
|
+
});
|
|
3211
|
+
const resumeQuestionWorkflow = async (questionId, workId, viewer) => {
|
|
3212
|
+
const continuation = aiWritePlanManager.claimQuestionContinuation(questionId, workId, viewer);
|
|
3213
|
+
if (!continuation)
|
|
3214
|
+
return;
|
|
3215
|
+
try {
|
|
3216
|
+
const conversationId = typeof continuation.conversationId === "string" ? continuation.conversationId : "";
|
|
3217
|
+
if (!conversationId)
|
|
3218
|
+
throw new AppError(409, "AI_QUESTION_CONTINUATION_MISSING", "提问缺少可恢复的对话状态");
|
|
3219
|
+
const scope = parse(contextSchema, continuation.scope ?? { type: "none" });
|
|
3220
|
+
const resumed = await ai.resumeUserQuestion({
|
|
3221
|
+
questionId,
|
|
3222
|
+
workId,
|
|
3223
|
+
conversationId,
|
|
3224
|
+
scope,
|
|
3225
|
+
status: String(continuation.status ?? "rejected"),
|
|
3226
|
+
answerText: String(continuation.answerText ?? ""),
|
|
3227
|
+
selectedOptionLabel: typeof continuation.selectedOptionLabel === "string" ? continuation.selectedOptionLabel : null,
|
|
3228
|
+
supplementalAnswer: typeof continuation.customAnswer === "string" ? continuation.customAnswer : "",
|
|
3229
|
+
...(typeof continuation.modelId === "string" && continuation.modelId ? { modelId: continuation.modelId } : {}),
|
|
3230
|
+
...(typeof continuation.toolCallId === "string" && continuation.toolCallId ? { toolCallId: continuation.toolCallId } : {}),
|
|
3231
|
+
...(typeof continuation.assistantMessageRequestId === "string" && continuation.assistantMessageRequestId
|
|
3232
|
+
? { assistantMessageRequestId: continuation.assistantMessageRequestId }
|
|
3233
|
+
: {}),
|
|
3234
|
+
...(continuation.questionView && typeof continuation.questionView === "object" && !Array.isArray(continuation.questionView)
|
|
3235
|
+
? { questionView: continuation.questionView }
|
|
3236
|
+
: {}),
|
|
3237
|
+
...(typeof continuation.round === "number" ? { round: continuation.round } : {}),
|
|
3238
|
+
...(Array.isArray(continuation.toolMessages) ? { toolMessages: continuation.toolMessages } : {})
|
|
3239
|
+
});
|
|
3240
|
+
aiWritePlanManager.finishQuestionContinuation(questionId, { callId: resumed.callId ?? null, completed: true });
|
|
3241
|
+
}
|
|
3242
|
+
catch (error) {
|
|
3243
|
+
aiWritePlanManager.finishQuestionContinuation(questionId, { message: error instanceof Error ? error.message : "恢复失败" }, true);
|
|
3244
|
+
throw error;
|
|
3245
|
+
}
|
|
3246
|
+
};
|
|
3247
|
+
app.post("/api/works/:workId/ai/questions/:questionId/answer", async (request, response) => {
|
|
3248
|
+
const input = parse(answerAiUserQuestionSchema, request.body ?? {});
|
|
3249
|
+
const viewer = planViewer();
|
|
3250
|
+
aiWritePlanManager.answerQuestion(request.params.questionId, request.params.workId, viewer, { ...(input.selectedOption !== undefined ? { selectedOption: input.selectedOption } : {}), ...(input.customAnswer !== undefined ? { customAnswer: input.customAnswer } : {}) });
|
|
3251
|
+
await resumeQuestionWorkflow(request.params.questionId, request.params.workId, viewer);
|
|
3252
|
+
data(response, aiWritePlanManager.getQuestion(request.params.questionId, request.params.workId, viewer));
|
|
3253
|
+
});
|
|
3254
|
+
app.post("/api/works/:workId/ai/questions/:questionId/reject", async (request, response) => {
|
|
3255
|
+
const viewer = planViewer();
|
|
3256
|
+
aiWritePlanManager.rejectQuestion(request.params.questionId, request.params.workId, viewer);
|
|
3257
|
+
await resumeQuestionWorkflow(request.params.questionId, request.params.workId, viewer);
|
|
3258
|
+
data(response, aiWritePlanManager.getQuestion(request.params.questionId, request.params.workId, viewer));
|
|
3259
|
+
});
|
|
3007
3260
|
app.get("/api/works/:workId/providers", (request, response) => {
|
|
3008
3261
|
store.getWork(request.params.workId);
|
|
3009
3262
|
data(response, ai.listProviders());
|
|
@@ -3045,6 +3298,9 @@ export function createRuntime(options) {
|
|
|
3045
3298
|
const pagination = parsePagination(request.query);
|
|
3046
3299
|
data(response, pagination ? ai.listWorkModelsPage(request.params.workId, pagination) : ai.listWorkModels(request.params.workId));
|
|
3047
3300
|
});
|
|
3301
|
+
app.get("/api/works/:workId/semantic-models", (request, response) => {
|
|
3302
|
+
data(response, ai.listWorkSemanticModels(request.params.workId));
|
|
3303
|
+
});
|
|
3048
3304
|
app.get("/api/works/:workId/task-defaults", (request, response) => {
|
|
3049
3305
|
const pagination = parsePagination(request.query);
|
|
3050
3306
|
data(response, pagination ? ai.listTaskDefaultsPage(request.params.workId, pagination) : ai.listTaskDefaults(request.params.workId));
|
|
@@ -3200,10 +3456,19 @@ export function createRuntime(options) {
|
|
|
3200
3456
|
const storedUserContent = isRoleplay
|
|
3201
3457
|
? composeRoleplayStoredUserContent(sceneDirection, instructionText)
|
|
3202
3458
|
: instructionText;
|
|
3203
|
-
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);
|
|
3204
3470
|
const mentionSource = [sceneDirection, resolvedInstruction].filter(Boolean).join("\n");
|
|
3205
3471
|
const modelId = resolveConversationModelId(request.params.workId, conversationId, input.modelId);
|
|
3206
|
-
const permissions = requestPermissions(request, request.params.workId);
|
|
3207
3472
|
const controller = new AbortController();
|
|
3208
3473
|
let streamRequestId = null;
|
|
3209
3474
|
let streamRequestFinished = false;
|
|
@@ -3258,8 +3523,9 @@ export function createRuntime(options) {
|
|
|
3258
3523
|
conversationId,
|
|
3259
3524
|
workId: request.params.workId,
|
|
3260
3525
|
modelId,
|
|
3261
|
-
scope:
|
|
3526
|
+
scope: requestScope,
|
|
3262
3527
|
instruction: mentionSource,
|
|
3528
|
+
skillInstruction: instructionText,
|
|
3263
3529
|
excludeConversationMessageId: input.currentMessageId
|
|
3264
3530
|
}, { ignoreWarning: input.ignoreContextWarning === true });
|
|
3265
3531
|
preparedConversation = redactAiConversation(store.getAiConversationSummary(conversationId), permissions);
|
|
@@ -3280,12 +3546,12 @@ export function createRuntime(options) {
|
|
|
3280
3546
|
}
|
|
3281
3547
|
}
|
|
3282
3548
|
const resolvedScope = input.currentMessageId
|
|
3283
|
-
?
|
|
3549
|
+
? requestScope
|
|
3284
3550
|
: ai.resolveInstructionMentions({
|
|
3285
3551
|
workId: request.params.workId,
|
|
3286
3552
|
taskType: "chat",
|
|
3287
3553
|
instruction: mentionSource,
|
|
3288
|
-
scope:
|
|
3554
|
+
scope: requestScope,
|
|
3289
3555
|
conversationId
|
|
3290
3556
|
});
|
|
3291
3557
|
const mentionCharacterIds = [...new Set([
|
|
@@ -3304,11 +3570,12 @@ export function createRuntime(options) {
|
|
|
3304
3570
|
content: storedUserContent,
|
|
3305
3571
|
citations,
|
|
3306
3572
|
...(input.currentMessageId ? { existingMessageId: input.currentMessageId } : {}),
|
|
3307
|
-
...((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: {
|
|
3308
3574
|
...(modelId ? { modelId } : {}),
|
|
3309
3575
|
...(mentionCharacterIds.length ? { mentionCharacterIds } : {}),
|
|
3310
3576
|
...(mentionRaceIds.length ? { mentionRaceIds } : {}),
|
|
3311
3577
|
...(mentionOrganizationIds.length ? { mentionOrganizationIds } : {}),
|
|
3578
|
+
...(input.scope.semanticSnapshotId ? { semanticSnapshotId: input.scope.semanticSnapshotId } : {}),
|
|
3312
3579
|
...(input.imageAttachmentIds?.length ? { chatImageAttachmentIds: [...new Set(input.imageAttachmentIds)] } : {})
|
|
3313
3580
|
} } : {})
|
|
3314
3581
|
}
|
|
@@ -3323,6 +3590,15 @@ export function createRuntime(options) {
|
|
|
3323
3590
|
}
|
|
3324
3591
|
if (begun.request.status === "completed" && begun.assistantMessage) {
|
|
3325
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;
|
|
3326
3602
|
if (content)
|
|
3327
3603
|
sendEvent("delta", { delta: content, replayed: true });
|
|
3328
3604
|
sendEvent("complete", {
|
|
@@ -3330,7 +3606,8 @@ export function createRuntime(options) {
|
|
|
3330
3606
|
conversationId,
|
|
3331
3607
|
conversationTitle: store.getAiConversationSummary(conversationId).title,
|
|
3332
3608
|
messageId: begun.assistantMessage.id,
|
|
3333
|
-
messageCreatedAt: begun.assistantMessage.createdAt
|
|
3609
|
+
messageCreatedAt: begun.assistantMessage.createdAt,
|
|
3610
|
+
...(writingSuggestion ? { writingSuggestion } : {})
|
|
3334
3611
|
});
|
|
3335
3612
|
}
|
|
3336
3613
|
else {
|
|
@@ -3356,12 +3633,16 @@ export function createRuntime(options) {
|
|
|
3356
3633
|
const currentMessageId = String(begun.userMessage?.id ?? input.currentMessageId ?? "");
|
|
3357
3634
|
if (begun.userMessage)
|
|
3358
3635
|
sendEvent("user_message", { message: redactAiConversationMessage(begun.userMessage, permissions) });
|
|
3636
|
+
if (aiWritePlanManager.latestPendingQuestion(conversationId)) {
|
|
3637
|
+
throw new AppError(409, "AI_QUESTION_PENDING", "当前对话仍有待回答问题,请先回答或拒绝后再继续");
|
|
3638
|
+
}
|
|
3359
3639
|
const suggestion = await ai.createStreamingChat({
|
|
3360
3640
|
workId: request.params.workId,
|
|
3361
3641
|
instruction: resolvedInstruction,
|
|
3642
|
+
skillInstruction: instructionText,
|
|
3362
3643
|
...(sceneDirection ? { sceneDirection } : {}),
|
|
3363
3644
|
// 仍由生成路径基于原始范围持久化累计注入,保证预解析不会吞掉本轮自动命中。
|
|
3364
|
-
scope:
|
|
3645
|
+
scope: requestScope,
|
|
3365
3646
|
signal: controller.signal,
|
|
3366
3647
|
onToolCall: (toolCall, round) => sendEvent("tool_call", { ...toolCall, round }),
|
|
3367
3648
|
onProcessStep: (step) => sendEvent("process_step", step),
|
|
@@ -3394,6 +3675,7 @@ export function createRuntime(options) {
|
|
|
3394
3675
|
contextUsage: suggestion.contextUsage,
|
|
3395
3676
|
conversationId,
|
|
3396
3677
|
conversationTitle: suggestion.conversationTitle,
|
|
3678
|
+
...(suggestion.action !== "note" ? { writingSuggestion: redactSuggestion(suggestion, permissions) } : {}),
|
|
3397
3679
|
messageId: typeof suggestion.conversationMessage === "object" && suggestion.conversationMessage !== null
|
|
3398
3680
|
? suggestion.conversationMessage.id
|
|
3399
3681
|
: undefined,
|
|
@@ -3456,6 +3738,11 @@ export function createRuntime(options) {
|
|
|
3456
3738
|
});
|
|
3457
3739
|
app.post("/api/suggestions/:suggestionId/accept", (request, response) => {
|
|
3458
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
|
+
}
|
|
3459
3746
|
data(response, ai.acceptSuggestion(request.params.suggestionId, input.content));
|
|
3460
3747
|
});
|
|
3461
3748
|
app.post("/api/suggestions/:suggestionId/reject", (request, response) => {
|
|
@@ -3484,6 +3771,27 @@ export function createRuntime(options) {
|
|
|
3484
3771
|
conversationOwnerUserId: request.authUser?.userId
|
|
3485
3772
|
}));
|
|
3486
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
|
+
});
|
|
3487
3795
|
app.head("/api/works/:workId/export", (request, response) => {
|
|
3488
3796
|
parse(z.enum(["epub"]), request.query.format ?? "epub");
|
|
3489
3797
|
store.getWork(request.params.workId);
|
|
@@ -3540,11 +3848,14 @@ export function createRuntime(options) {
|
|
|
3540
3848
|
});
|
|
3541
3849
|
if (options.serveUi ?? true) {
|
|
3542
3850
|
const publicPath = options.publicPath ?? join(process.cwd(), "src", "public");
|
|
3851
|
+
const publicApplicationModuleImports = extractStaticModuleImports(readFileSync(join(publicPath, "app.js"), "utf8"));
|
|
3852
|
+
// API 与 SSE 路由已经在此前注册;压缩只作用于页面和静态资源,避免缓冲流式响应。
|
|
3853
|
+
app.use(compression());
|
|
3543
3854
|
// index.html 按登录态动态下发:未登录时注入 login-route 类,首帧直接渲染登录页;
|
|
3544
3855
|
// 已登录时保持骨架屏,由前端恢复会话后进入工作台,避免两种闪烁。
|
|
3545
3856
|
const sendIndexHtml = (request, response) => {
|
|
3546
3857
|
const authenticated = options.disableUserAuth === true || auth.authenticate(request) !== null;
|
|
3547
|
-
let html = readFileSync(join(publicPath, "index.html"), "utf8");
|
|
3858
|
+
let html = injectModulePreloads(readFileSync(join(publicPath, "index.html"), "utf8"), publicApplicationModuleImports);
|
|
3548
3859
|
if (!authenticated)
|
|
3549
3860
|
html = html.replace('<html lang="zh-CN">', '<html lang="zh-CN" class="login-route">');
|
|
3550
3861
|
if (options.disableUserAuth === true) {
|