@musnows/scriverse 0.7.2 → 0.7.3
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/README.md +5 -0
- package/dist/ai.js +248 -250
- package/dist/ai.js.map +1 -1
- package/dist/app.js +184 -39
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +28 -12
- package/dist/attachment-storage.js.map +1 -1
- package/dist/backup-encryption.js +129 -0
- package/dist/backup-encryption.js.map +1 -0
- package/dist/collaboration-presence.js +200 -18
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/database.js +162 -3
- package/dist/database.js.map +1 -1
- package/dist/docx-export.js +1 -1
- package/dist/docx-export.js.map +1 -1
- package/dist/domain.js +18 -0
- package/dist/domain.js.map +1 -1
- package/dist/image-metadata.js +17 -2
- package/dist/image-metadata.js.map +1 -1
- package/dist/presence-store.js +160 -0
- package/dist/presence-store.js.map +1 -0
- package/dist/public/ai-context-meter.js +27 -0
- package/dist/public/ai-mentions.js +14 -0
- package/dist/public/app.js +1017 -291
- package/dist/public/background-task-center.js +1 -1
- package/dist/public/chapter-editor-virtualization.js +52 -0
- package/dist/public/index.html +43 -21
- package/dist/public/presence-client-id.d.ts +10 -0
- package/dist/public/presence-client-id.js +31 -0
- package/dist/public/s3-backup-ui.d.ts +10 -0
- package/dist/public/s3-backup-ui.js +29 -0
- package/dist/public/setting-filters.d.ts +16 -0
- package/dist/public/setting-filters.js +19 -0
- package/dist/public/styles.css +37 -7
- package/dist/s3-backup.js +197 -12
- package/dist/s3-backup.js.map +1 -1
- package/dist/security.js +82 -20
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +36 -18
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +75 -17
- package/dist/store.js.map +1 -1
- package/dist/upload-limits.js +35 -0
- package/dist/upload-limits.js.map +1 -0
- package/dist/user-auth.js +11 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/dist/app.js
CHANGED
|
@@ -16,7 +16,7 @@ import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
|
|
|
16
16
|
import { CredentialVault } from "./credential-vault.js";
|
|
17
17
|
import { Database } from "./database.js";
|
|
18
18
|
import { assertSafeDocxArchive } from "./docx-security.js";
|
|
19
|
-
import { DRAFT_SETTING_MODULES, TASK_TYPES } from "./domain.js";
|
|
19
|
+
import { CREATABLE_ANALYSIS_TASK_TYPES, DRAFT_SETTING_MODULES, TASK_TYPES } from "./domain.js";
|
|
20
20
|
import { AppError } from "./errors.js";
|
|
21
21
|
import { isOfficialGoogleVertexBaseUrl, parseGoogleServiceAccount } from "./google-vertex-auth.js";
|
|
22
22
|
import { HYBRID_SEARCH_TYPES } from "./hybrid-search.js";
|
|
@@ -34,8 +34,10 @@ import { currentRequestActor, runWithRequestActor } from "./request-context.js";
|
|
|
34
34
|
import { S3BackupManager } from "./s3-backup.js";
|
|
35
35
|
import { APP_VERSION } from "./version.js";
|
|
36
36
|
import { ReleaseUpdateChecker } from "./release-update.js";
|
|
37
|
+
import { DEFAULT_IMAGE_UPLOAD_LIMITS, formatUploadLimit } from "./upload-limits.js";
|
|
37
38
|
import { canReadWorkModule, canWriteWorkModule, fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
|
|
38
|
-
import { CollaborationPresence, entityEditorPageKey, presencePageKinds } from "./collaboration-presence.js";
|
|
39
|
+
import { CollaborationPresence, editorPageKey, entityEditorPageKey, modulePageKey, presencePageKinds } from "./collaboration-presence.js";
|
|
40
|
+
import { PresenceStore } from "./presence-store.js";
|
|
39
41
|
import { analysisTaskReadModules, clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, relationshipAnalysisReadModules, setSessionCookie, UserAuthService } from "./user-auth.js";
|
|
40
42
|
const nonEmpty = z.string().trim().min(1);
|
|
41
43
|
const identifier = z.string().trim().min(1).max(200);
|
|
@@ -47,6 +49,22 @@ const versionedEntityTypeSchema = z.enum(versionedEntityTypes);
|
|
|
47
49
|
const attachmentPermissionModuleSchema = z.enum(attachmentPermissionModules);
|
|
48
50
|
const maximumImportedTextLength = 20_000_000;
|
|
49
51
|
const maximumKnowledgeSectionsLength = 4_000_000;
|
|
52
|
+
function assertImageUploadSize(byteLength, maximumBytes, message) {
|
|
53
|
+
if (byteLength <= maximumBytes)
|
|
54
|
+
return;
|
|
55
|
+
throw new AppError(413, "IMAGE_TOO_LARGE", message);
|
|
56
|
+
}
|
|
57
|
+
function uploadSizeError(pathname, limits) {
|
|
58
|
+
if (pathname === "/api/auth/avatar")
|
|
59
|
+
return { code: "IMAGE_TOO_LARGE", message: `头像图片不能超过 ${formatUploadLimit(limits.avatarBytes)}` };
|
|
60
|
+
if (/^\/api\/works\/[^/]+\/cover$/u.test(pathname)) {
|
|
61
|
+
return { code: "IMAGE_TOO_LARGE", message: `封面图片不能超过 ${formatUploadLimit(limits.coverBytes)}` };
|
|
62
|
+
}
|
|
63
|
+
if (/^\/api\/works\/[^/]+\/attachments$/u.test(pathname)) {
|
|
64
|
+
return { code: "ATTACHMENT_TOO_LARGE", message: `图片附件不能超过 ${formatUploadLimit(limits.attachmentBytes)}` };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
50
68
|
const captchaFields = {
|
|
51
69
|
captchaId: z.string().trim().min(1).max(200),
|
|
52
70
|
captchaAnswer: z.string().trim().min(1).max(16)
|
|
@@ -153,7 +171,7 @@ const settingSchema = z.object({
|
|
|
153
171
|
const globalReplaceSchema = z.object({
|
|
154
172
|
find: z.string().min(1).max(500),
|
|
155
173
|
replacement: z.string().max(200_000),
|
|
156
|
-
scope: z.enum(["prose", "settings", "prose-and-settings"])
|
|
174
|
+
scope: z.enum(["prose", "settings", "prose-and-settings"])
|
|
157
175
|
}).strict();
|
|
158
176
|
const draftSchema = z.object({
|
|
159
177
|
draftType: z.enum(["prose", "setting"]),
|
|
@@ -446,6 +464,12 @@ const s3BackupRunQuerySchema = z.object({
|
|
|
446
464
|
afterSequence: z.coerce.number().int().min(0).optional(),
|
|
447
465
|
limit: z.coerce.number().int().min(1).max(100).optional()
|
|
448
466
|
}).strict();
|
|
467
|
+
const s3BackupEncryptionSchema = z.object({
|
|
468
|
+
enabled: z.boolean()
|
|
469
|
+
}).strict();
|
|
470
|
+
const s3BackupEncryptionConfirmationSchema = z.object({
|
|
471
|
+
confirmationToken: z.string().regex(/^[A-Za-z0-9_-]{43}$/u, "备份加密确认令牌格式无效")
|
|
472
|
+
}).strict();
|
|
449
473
|
const aiToolCallResultSchema = z.object({
|
|
450
474
|
id: z.string().min(1).max(300),
|
|
451
475
|
name: z.string().min(1).max(200),
|
|
@@ -517,7 +541,8 @@ const contextSchema = z.object({
|
|
|
517
541
|
includeBookSummary: z.boolean().optional(),
|
|
518
542
|
includeSettingInfo: z.boolean().optional()
|
|
519
543
|
});
|
|
520
|
-
|
|
544
|
+
/** 创建任务 API 的分析类型校验:仅允许可新建类型,历史类型在运行层保留防御性拒绝。 */
|
|
545
|
+
export const creatableAnalysisTaskTypeSchema = z.enum(CREATABLE_ANALYSIS_TASK_TYPES);
|
|
521
546
|
const relationshipSourceRefSchema = z.object({
|
|
522
547
|
sourceType: z.string().trim().min(1).max(50).regex(/^[a-z][a-z-]*$/u),
|
|
523
548
|
sourceId: identifier,
|
|
@@ -561,7 +586,7 @@ const relationshipAnalysisScopeSchema = z.object({
|
|
|
561
586
|
});
|
|
562
587
|
const analysisTaskSchema = z.union([
|
|
563
588
|
z.object({ taskType: z.literal("relationship-analysis"), scope: relationshipAnalysisScopeSchema.optional(), modelId: identifier.optional() }).strict(),
|
|
564
|
-
z.object({ taskType:
|
|
589
|
+
z.object({ taskType: creatableAnalysisTaskTypeSchema, scope: jsonObject.optional(), modelId: identifier.optional() }).strict().superRefine((input, context) => {
|
|
565
590
|
if (input.scope?.includeAllSettings !== undefined) {
|
|
566
591
|
context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "includeAllSettings"], message: "包含所有设定仅支持人物关系分析" });
|
|
567
592
|
}
|
|
@@ -582,6 +607,7 @@ const analysisTaskSchema = z.union([
|
|
|
582
607
|
}
|
|
583
608
|
})
|
|
584
609
|
]);
|
|
610
|
+
export const RUNTIME_BACKUP_IDLE_TIMEOUT_MS = 9_000;
|
|
585
611
|
function data(response, value, status = 200) {
|
|
586
612
|
response.status(status).json({ data: value });
|
|
587
613
|
}
|
|
@@ -790,11 +816,18 @@ function redactSuggestion(record, permissions) {
|
|
|
790
816
|
};
|
|
791
817
|
}
|
|
792
818
|
function redactAiConversationMessage(item, permissions) {
|
|
793
|
-
if (permissions.prose !== "none")
|
|
794
|
-
return item;
|
|
795
819
|
const message = recordValue(item);
|
|
796
820
|
if (!message)
|
|
797
821
|
return item;
|
|
822
|
+
if (permissions.prose !== "none") {
|
|
823
|
+
if (permissions.characters !== "none")
|
|
824
|
+
return item;
|
|
825
|
+
const metadata = recordValue(message.metadata);
|
|
826
|
+
if (!metadata || !("mentionCharacterIds" in metadata))
|
|
827
|
+
return item;
|
|
828
|
+
const { mentionCharacterIds: _mentionCharacterIds, ...readableMetadata } = metadata;
|
|
829
|
+
return { ...message, metadata: readableMetadata };
|
|
830
|
+
}
|
|
798
831
|
return {
|
|
799
832
|
...message,
|
|
800
833
|
content: proseRestrictedPlaceholder,
|
|
@@ -807,25 +840,25 @@ function redactAiConversationMessage(item, permissions) {
|
|
|
807
840
|
function redactAiConversation(record, permissions) {
|
|
808
841
|
const readableRecord = permissions.characters === "none" ? { ...record, roleplayCharacter: null } : record;
|
|
809
842
|
const scopedRecord = redactAiCallContext(readableRecord, permissions);
|
|
810
|
-
if (permissions.prose !== "none")
|
|
811
|
-
return scopedRecord;
|
|
812
843
|
const result = {
|
|
813
|
-
...scopedRecord
|
|
814
|
-
title: proseRestrictedPlaceholder
|
|
844
|
+
...scopedRecord
|
|
815
845
|
};
|
|
816
|
-
if (
|
|
817
|
-
result.preview = proseRestrictedPlaceholder;
|
|
818
|
-
}
|
|
819
|
-
if (Array.isArray(result.messages)) {
|
|
846
|
+
if ((permissions.prose === "none" || permissions.characters === "none") && Array.isArray(result.messages)) {
|
|
820
847
|
result.messages = result.messages.map((item) => redactAiConversationMessage(item, permissions));
|
|
821
848
|
}
|
|
822
849
|
const messagesPage = recordValue(result.messagesPage);
|
|
823
|
-
if (messagesPage && Array.isArray(messagesPage.items)) {
|
|
850
|
+
if ((permissions.prose === "none" || permissions.characters === "none") && messagesPage && Array.isArray(messagesPage.items)) {
|
|
824
851
|
result.messagesPage = {
|
|
825
852
|
...messagesPage,
|
|
826
853
|
items: messagesPage.items.map((item) => redactAiConversationMessage(item, permissions))
|
|
827
854
|
};
|
|
828
855
|
}
|
|
856
|
+
if (permissions.prose !== "none")
|
|
857
|
+
return result;
|
|
858
|
+
result.title = proseRestrictedPlaceholder;
|
|
859
|
+
if (typeof result.preview === "string" && result.preview.length > 0) {
|
|
860
|
+
result.preview = proseRestrictedPlaceholder;
|
|
861
|
+
}
|
|
829
862
|
return { ...result, restricted: true };
|
|
830
863
|
}
|
|
831
864
|
/** SSE 错误事件只暴露 AppError 的公开信息;AI_CALL_FAILED 的 failure 已在 AI 层完成密钥脱敏。 */
|
|
@@ -865,6 +898,7 @@ function redactVersionSnapshots(value, mapper) {
|
|
|
865
898
|
});
|
|
866
899
|
}
|
|
867
900
|
export function createRuntime(options) {
|
|
901
|
+
const uploadLimits = options.uploadLimits ?? DEFAULT_IMAGE_UPLOAD_LIMITS;
|
|
868
902
|
logger.info("runtime.initializing", {
|
|
869
903
|
databasePath: options.databasePath,
|
|
870
904
|
serveUi: options.serveUi ?? true,
|
|
@@ -878,19 +912,35 @@ export function createRuntime(options) {
|
|
|
878
912
|
const temporaryAttachmentRoot = options.databasePath === ":memory:" && !options.attachmentDirectory
|
|
879
913
|
? mkdtempSync(join(tmpdir(), "scriverse-attachments-"))
|
|
880
914
|
: null;
|
|
881
|
-
const attachmentStorage = new AttachmentStorage(options.attachmentDirectory ?? temporaryAttachmentRoot ?? join(dirname(options.databasePath), "attachments"));
|
|
915
|
+
const attachmentStorage = new AttachmentStorage(options.attachmentDirectory ?? temporaryAttachmentRoot ?? join(dirname(options.databasePath), "attachments"), uploadLimits.attachmentBytes);
|
|
882
916
|
mkdirSync(attachmentStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
|
|
883
917
|
const auth = new UserAuthService(database);
|
|
884
|
-
const collaborationPresence = new CollaborationPresence();
|
|
885
|
-
const
|
|
918
|
+
const collaborationPresence = new CollaborationPresence(45_000, Date.now, 120_000, 50, { store: new PresenceStore(database) });
|
|
919
|
+
const publishCollaborativeChange = (workId, pageKey, options = {}) => {
|
|
886
920
|
const actor = currentRequestActor();
|
|
887
|
-
if (!actor || !workId || !
|
|
921
|
+
if (!actor || !workId || !pageKey)
|
|
888
922
|
return;
|
|
889
|
-
collaborationPresence.publishChange(workId,
|
|
923
|
+
collaborationPresence.publishChange(workId, pageKey, {
|
|
890
924
|
userId: actor.userId,
|
|
891
925
|
displayName: actor.displayName
|
|
892
|
-
});
|
|
926
|
+
}, options);
|
|
893
927
|
};
|
|
928
|
+
const publishEditorChange = (workId, chapterId, options = {}) => {
|
|
929
|
+
if (!chapterId)
|
|
930
|
+
return;
|
|
931
|
+
publishCollaborativeChange(workId, editorPageKey(chapterId), options);
|
|
932
|
+
};
|
|
933
|
+
const publishEntityChange = (workId, module, resourceId, options = {}) => {
|
|
934
|
+
if (!resourceId)
|
|
935
|
+
return;
|
|
936
|
+
publishCollaborativeChange(workId, entityEditorPageKey(module, resourceId), options);
|
|
937
|
+
};
|
|
938
|
+
const publishModuleChange = (workId, module, options = {}) => {
|
|
939
|
+
if (!module)
|
|
940
|
+
return;
|
|
941
|
+
publishCollaborativeChange(workId, modulePageKey(module), options);
|
|
942
|
+
};
|
|
943
|
+
const deletedPageChange = { action: "delete", pageDeleted: true };
|
|
894
944
|
const getDevelopmentUser = () => options.devAuthBypass
|
|
895
945
|
? auth.listUsers().find((user) => user.status === "active") ?? null
|
|
896
946
|
: null;
|
|
@@ -942,7 +992,9 @@ export function createRuntime(options) {
|
|
|
942
992
|
timeoutMs: options.releaseCheckTimeoutMs,
|
|
943
993
|
retries: options.releaseCheckRetries
|
|
944
994
|
});
|
|
945
|
-
const ai = new AiManager(store, credentialVault, options.fetchImpl ?? fetch, options.
|
|
995
|
+
const ai = new AiManager(store, credentialVault, options.fetchImpl ?? fetch, options.developmentServer === true
|
|
996
|
+
? undefined
|
|
997
|
+
: options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined, (task, actor) => {
|
|
946
998
|
const requiredModules = analysisTaskReadModules(task.taskType, task.scope);
|
|
947
999
|
const creator = actor ? null : database.get("SELECT created_by_user_id FROM analysis_tasks WHERE id = ?", String(task.id));
|
|
948
1000
|
const userId = actor?.userId ?? (typeof creator?.created_by_user_id === "string" ? creator.created_by_user_id : null);
|
|
@@ -964,18 +1016,18 @@ export function createRuntime(options) {
|
|
|
964
1016
|
});
|
|
965
1017
|
const coverUpload = multer({
|
|
966
1018
|
storage: multer.memoryStorage(),
|
|
967
|
-
limits: { fileSize:
|
|
1019
|
+
limits: { fileSize: uploadLimits.coverBytes + 1, files: 1, fields: 4, fieldSize: 16 * 1024, parts: 5, headerPairs: 100 }
|
|
968
1020
|
});
|
|
969
1021
|
const avatarUpload = multer({
|
|
970
1022
|
storage: multer.memoryStorage(),
|
|
971
|
-
limits: { fileSize:
|
|
1023
|
+
limits: { fileSize: uploadLimits.avatarBytes + 1, files: 1, fields: 1, fieldSize: 1024, parts: 2, headerPairs: 50 }
|
|
972
1024
|
});
|
|
973
1025
|
const attachmentUpload = multer({
|
|
974
1026
|
storage: multer.diskStorage({
|
|
975
1027
|
destination: attachmentStorage.temporaryDirectory,
|
|
976
1028
|
filename: (_request, _file, callback) => callback(null, randomUUID())
|
|
977
1029
|
}),
|
|
978
|
-
limits: { fileSize:
|
|
1030
|
+
limits: { fileSize: uploadLimits.attachmentBytes + 1, files: 1, fields: 4, fieldSize: 16 * 1024, parts: 5, headerPairs: 100 }
|
|
979
1031
|
});
|
|
980
1032
|
app.disable("x-powered-by");
|
|
981
1033
|
const trustProxy = resolveTrustProxySetting(options.security?.trustProxy);
|
|
@@ -993,7 +1045,8 @@ export function createRuntime(options) {
|
|
|
993
1045
|
version: APP_VERSION,
|
|
994
1046
|
protocol: "openai-chat-completions",
|
|
995
1047
|
protocols: [...AI_PROVIDER_PROTOCOLS],
|
|
996
|
-
development: options.developmentServer === true
|
|
1048
|
+
development: options.developmentServer === true,
|
|
1049
|
+
uploadLimits
|
|
997
1050
|
});
|
|
998
1051
|
});
|
|
999
1052
|
app.get("/api/update-check", async (_request, response) => {
|
|
@@ -1086,7 +1139,8 @@ export function createRuntime(options) {
|
|
|
1086
1139
|
if (!request.authUser)
|
|
1087
1140
|
throw new AppError(401, "AUTH_REQUIRED", "请先登录");
|
|
1088
1141
|
if (!request.file)
|
|
1089
|
-
throw new AppError(400, "FILE_REQUIRED", "请选择 PNG、JPEG 或
|
|
1142
|
+
throw new AppError(400, "FILE_REQUIRED", "请选择 PNG、JPEG、WebP 或 GIF 头像");
|
|
1143
|
+
assertImageUploadSize(request.file.buffer.byteLength, uploadLimits.avatarBytes, `头像图片不能超过 ${formatUploadLimit(uploadLimits.avatarBytes)}`);
|
|
1090
1144
|
try {
|
|
1091
1145
|
const metadata = readRasterImageMetadata(request.file.buffer);
|
|
1092
1146
|
const updated = database.transaction(() => {
|
|
@@ -1271,8 +1325,11 @@ export function createRuntime(options) {
|
|
|
1271
1325
|
if (!request.file)
|
|
1272
1326
|
throw new AppError(400, "FILE_REQUIRED", "请选择 PNG、JPEG 或 WebP 封面");
|
|
1273
1327
|
const bytes = request.file.buffer;
|
|
1328
|
+
assertImageUploadSize(bytes.byteLength, uploadLimits.coverBytes, `封面图片不能超过 ${formatUploadLimit(uploadLimits.coverBytes)}`);
|
|
1274
1329
|
try {
|
|
1275
1330
|
const metadata = readRasterImageMetadata(bytes);
|
|
1331
|
+
if (metadata.mimeType === "image/gif")
|
|
1332
|
+
throw new AppError(415, "UNSUPPORTED_COVER_FORMAT", "封面不支持 GIF 图片");
|
|
1276
1333
|
const expectedVersionNo = parse(expectedVersionNoSchema, request.body.expectedVersionNo);
|
|
1277
1334
|
data(response, store.setWorkCover(String(request.params.workId), metadata.mimeType, bytes, expectedVersionNo));
|
|
1278
1335
|
}
|
|
@@ -1353,11 +1410,14 @@ export function createRuntime(options) {
|
|
|
1353
1410
|
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);
|
|
1354
1411
|
const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
|
|
1355
1412
|
const chapter = store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo);
|
|
1413
|
+
publishEditorChange(String(chapter.workId), String(chapter.id));
|
|
1356
1414
|
data(response, chapter);
|
|
1357
1415
|
});
|
|
1358
1416
|
app.delete("/api/chapters/:chapterId", (request, response) => {
|
|
1359
1417
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1418
|
+
const chapter = store.getChapter(request.params.chapterId);
|
|
1360
1419
|
store.deleteChapter(request.params.chapterId, input.expectedVersionNo);
|
|
1420
|
+
publishEditorChange(String(chapter.workId), String(chapter.id), deletedPageChange);
|
|
1361
1421
|
noContent(response);
|
|
1362
1422
|
});
|
|
1363
1423
|
app.delete("/api/chapters/:chapterId/permanent", (request, response) => {
|
|
@@ -1518,11 +1578,14 @@ export function createRuntime(options) {
|
|
|
1518
1578
|
app.patch("/api/settings/:settingId", (request, response) => {
|
|
1519
1579
|
const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1520
1580
|
const setting = store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1581
|
+
publishEntityChange(String(setting.workId), "setting", String(setting.id));
|
|
1521
1582
|
data(response, setting);
|
|
1522
1583
|
});
|
|
1523
1584
|
app.delete("/api/settings/:settingId", (request, response) => {
|
|
1524
1585
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1586
|
+
const setting = store.getSetting(request.params.settingId);
|
|
1525
1587
|
store.deleteSetting(request.params.settingId, input.expectedVersionNo);
|
|
1588
|
+
publishEntityChange(String(setting.workId), "setting", String(setting.id), deletedPageChange);
|
|
1526
1589
|
noContent(response);
|
|
1527
1590
|
});
|
|
1528
1591
|
app.get("/api/works/:workId/characters", (request, response) => {
|
|
@@ -1548,6 +1611,7 @@ export function createRuntime(options) {
|
|
|
1548
1611
|
app.patch("/api/characters/:characterId", (request, response) => {
|
|
1549
1612
|
const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
|
|
1550
1613
|
const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1614
|
+
publishEntityChange(String(character.workId), "character", String(character.id));
|
|
1551
1615
|
data(response, redactCharacterLinks(character, requestPermissions(request)));
|
|
1552
1616
|
});
|
|
1553
1617
|
app.get("/api/characters/:characterId/versions", (request, response) => {
|
|
@@ -1563,7 +1627,9 @@ export function createRuntime(options) {
|
|
|
1563
1627
|
});
|
|
1564
1628
|
app.delete("/api/characters/:characterId", (request, response) => {
|
|
1565
1629
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1630
|
+
const character = store.getCharacter(request.params.characterId);
|
|
1566
1631
|
store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
|
|
1632
|
+
publishEntityChange(String(character.workId), "character", String(character.id), deletedPageChange);
|
|
1567
1633
|
noContent(response);
|
|
1568
1634
|
});
|
|
1569
1635
|
app.post("/api/characters/:characterId/merge", (request, response) => {
|
|
@@ -1596,11 +1662,17 @@ export function createRuntime(options) {
|
|
|
1596
1662
|
app.patch("/api/character-sections/:sectionId", (request, response) => {
|
|
1597
1663
|
const { changeNote, expectedVersionNo, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1598
1664
|
const section = store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1665
|
+
publishEntityChange(String(section.workId), "character", String(section.characterId));
|
|
1599
1666
|
data(response, section);
|
|
1600
1667
|
});
|
|
1601
1668
|
app.delete("/api/character-sections/:sectionId", (request, response) => {
|
|
1602
1669
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1670
|
+
const section = store.getCharacterProfileSection(request.params.sectionId);
|
|
1603
1671
|
store.deleteCharacterProfileSection(request.params.sectionId, input.expectedVersionNo);
|
|
1672
|
+
publishEntityChange(String(section.workId), "character", String(section.characterId), {
|
|
1673
|
+
action: "delete",
|
|
1674
|
+
label: "角色档案章节"
|
|
1675
|
+
});
|
|
1604
1676
|
noContent(response);
|
|
1605
1677
|
});
|
|
1606
1678
|
app.get("/api/character-sections/:sectionId/versions", (request, response) => {
|
|
@@ -1699,11 +1771,14 @@ export function createRuntime(options) {
|
|
|
1699
1771
|
app.patch("/api/races/:raceId", (request, response) => {
|
|
1700
1772
|
const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1701
1773
|
const race = store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1774
|
+
publishEntityChange(String(race.workId), "race", String(race.id));
|
|
1702
1775
|
data(response, redactRaceMembers(race, requestPermissions(request)));
|
|
1703
1776
|
});
|
|
1704
1777
|
app.delete("/api/races/:raceId", (request, response) => {
|
|
1705
1778
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1779
|
+
const race = store.getRace(request.params.raceId);
|
|
1706
1780
|
store.deleteRace(request.params.raceId, input.expectedVersionNo);
|
|
1781
|
+
publishEntityChange(String(race.workId), "race", String(race.id), deletedPageChange);
|
|
1707
1782
|
noContent(response);
|
|
1708
1783
|
});
|
|
1709
1784
|
app.post("/api/races/:raceId/merge", (request, response) => {
|
|
@@ -1729,11 +1804,14 @@ export function createRuntime(options) {
|
|
|
1729
1804
|
app.patch("/api/organizations/:organizationId", (request, response) => {
|
|
1730
1805
|
const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1731
1806
|
const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1807
|
+
publishEntityChange(String(organization.workId), "organization", String(organization.id));
|
|
1732
1808
|
data(response, redactOrganizationMembers(organization, requestPermissions(request)));
|
|
1733
1809
|
});
|
|
1734
1810
|
app.delete("/api/organizations/:organizationId", (request, response) => {
|
|
1735
1811
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1812
|
+
const organization = store.getOrganization(request.params.organizationId);
|
|
1736
1813
|
store.deleteOrganization(request.params.organizationId, input.expectedVersionNo);
|
|
1814
|
+
publishEntityChange(String(organization.workId), "organization", String(organization.id), deletedPageChange);
|
|
1737
1815
|
noContent(response);
|
|
1738
1816
|
});
|
|
1739
1817
|
app.post("/api/organizations/:organizationId/merge", (request, response) => {
|
|
@@ -1754,11 +1832,14 @@ export function createRuntime(options) {
|
|
|
1754
1832
|
app.patch("/api/timeline-tracks/:trackId", (request, response) => {
|
|
1755
1833
|
const { changeNote, expectedVersionNo, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1756
1834
|
const track = store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1835
|
+
publishModuleChange(String(track.workId), "timeline");
|
|
1757
1836
|
data(response, track);
|
|
1758
1837
|
});
|
|
1759
1838
|
app.delete("/api/timeline-tracks/:trackId", (request, response) => {
|
|
1760
1839
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1840
|
+
const track = store.getTimelineTrack(request.params.trackId);
|
|
1761
1841
|
store.deleteTimelineTrack(request.params.trackId, input.expectedVersionNo);
|
|
1842
|
+
publishModuleChange(String(track.workId), "timeline", { action: "delete", label: "时间轴轨道" });
|
|
1762
1843
|
noContent(response);
|
|
1763
1844
|
});
|
|
1764
1845
|
app.get("/api/works/:workId/timeline", (request, response) => {
|
|
@@ -1786,6 +1867,7 @@ export function createRuntime(options) {
|
|
|
1786
1867
|
app.patch("/api/timeline/:eventId", (request, response) => {
|
|
1787
1868
|
const { changeNote, expectedVersionNo, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1788
1869
|
const event = store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1870
|
+
publishModuleChange(String(event.workId), "timeline");
|
|
1789
1871
|
data(response, event);
|
|
1790
1872
|
});
|
|
1791
1873
|
app.post("/api/timeline/:eventId/split", (request, response) => {
|
|
@@ -1803,7 +1885,9 @@ export function createRuntime(options) {
|
|
|
1803
1885
|
});
|
|
1804
1886
|
app.delete("/api/timeline/:eventId", (request, response) => {
|
|
1805
1887
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1888
|
+
const event = store.getTimelineEvent(request.params.eventId);
|
|
1806
1889
|
store.deleteTimelineEvent(request.params.eventId, input.expectedVersionNo);
|
|
1890
|
+
publishModuleChange(String(event.workId), "timeline", { action: "delete", label: "时间轴事件" });
|
|
1807
1891
|
noContent(response);
|
|
1808
1892
|
});
|
|
1809
1893
|
app.get("/api/works/:workId/relationships", (request, response) => {
|
|
@@ -1823,14 +1907,14 @@ export function createRuntime(options) {
|
|
|
1823
1907
|
app.patch("/api/relationships/:relationshipId", (request, response) => {
|
|
1824
1908
|
const { changeNote, expectedVersionNo, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1825
1909
|
const relationship = store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1826
|
-
|
|
1910
|
+
publishEntityChange(String(relationship.workId), "relationship", String(relationship.id));
|
|
1827
1911
|
data(response, relationship);
|
|
1828
1912
|
});
|
|
1829
1913
|
app.delete("/api/relationships/:relationshipId", (request, response) => {
|
|
1830
1914
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1831
1915
|
const relationship = store.getRelationship(request.params.relationshipId);
|
|
1832
1916
|
store.deleteRelationship(request.params.relationshipId, input.expectedVersionNo);
|
|
1833
|
-
|
|
1917
|
+
publishEntityChange(String(relationship.workId), "relationship", String(relationship.id), deletedPageChange);
|
|
1834
1918
|
noContent(response);
|
|
1835
1919
|
});
|
|
1836
1920
|
app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
|
|
@@ -1992,6 +2076,17 @@ export function createRuntime(options) {
|
|
|
1992
2076
|
app.patch("/api/platform/ui-settings", (request, response) => {
|
|
1993
2077
|
data(response, store.updatePlatformUiSettings(parse(platformUiSettingsSchema, request.body)));
|
|
1994
2078
|
});
|
|
2079
|
+
app.get("/api/platform/backups/encryption", (_request, response) => {
|
|
2080
|
+
data(response, backups.getEncryptionState());
|
|
2081
|
+
});
|
|
2082
|
+
app.post("/api/platform/backups/encryption", (request, response) => {
|
|
2083
|
+
const input = parse(s3BackupEncryptionSchema, request.body);
|
|
2084
|
+
data(response, backups.setEncryptionEnabled(input.enabled));
|
|
2085
|
+
});
|
|
2086
|
+
app.post("/api/platform/backups/encryption/confirm", (request, response) => {
|
|
2087
|
+
const input = parse(s3BackupEncryptionConfirmationSchema, request.body);
|
|
2088
|
+
data(response, backups.confirmEncryptionEnabled(input.confirmationToken));
|
|
2089
|
+
});
|
|
1995
2090
|
app.get("/api/platform/backups/targets", (_request, response) => data(response, backups.listTargets()));
|
|
1996
2091
|
app.post("/api/platform/backups/targets", (request, response) => {
|
|
1997
2092
|
data(response, backups.createTarget(parse(s3BackupTargetBaseSchema, request.body)), 201);
|
|
@@ -2251,6 +2346,7 @@ export function createRuntime(options) {
|
|
|
2251
2346
|
if (store.getChapter(citation.chapterId).workId !== request.params.workId)
|
|
2252
2347
|
throw new AppError(400, "CITATION_WORK_MISMATCH", "引用章节不属于当前作品");
|
|
2253
2348
|
}
|
|
2349
|
+
const resolvedInstruction = instructionWithCitations(input.instruction, citations);
|
|
2254
2350
|
const controller = new AbortController();
|
|
2255
2351
|
response.on("close", () => {
|
|
2256
2352
|
if (!response.writableEnded)
|
|
@@ -2281,7 +2377,7 @@ export function createRuntime(options) {
|
|
|
2281
2377
|
workId: request.params.workId,
|
|
2282
2378
|
modelId: input.modelId,
|
|
2283
2379
|
scope: input.scope,
|
|
2284
|
-
instruction:
|
|
2380
|
+
instruction: resolvedInstruction,
|
|
2285
2381
|
excludeConversationMessageId: input.currentMessageId
|
|
2286
2382
|
});
|
|
2287
2383
|
sendEvent("context", {
|
|
@@ -2293,19 +2389,34 @@ export function createRuntime(options) {
|
|
|
2293
2389
|
});
|
|
2294
2390
|
if (prepared.action === "warn")
|
|
2295
2391
|
return;
|
|
2392
|
+
const resolvedScope = input.currentMessageId
|
|
2393
|
+
? input.scope
|
|
2394
|
+
: ai.resolveInstructionMentions({
|
|
2395
|
+
workId: request.params.workId,
|
|
2396
|
+
taskType: "chat",
|
|
2397
|
+
instruction: resolvedInstruction,
|
|
2398
|
+
scope: input.scope,
|
|
2399
|
+
conversationId
|
|
2400
|
+
});
|
|
2401
|
+
const mentionCharacterIds = [...new Set([
|
|
2402
|
+
...(resolvedScope.characterIds ?? []),
|
|
2403
|
+
...(resolvedScope.mentionCharacterIds ?? [])
|
|
2404
|
+
])];
|
|
2296
2405
|
const userMessage = input.currentMessageId
|
|
2297
2406
|
? null
|
|
2298
2407
|
: store.addAiConversationMessage(conversationId, {
|
|
2299
2408
|
role: "user",
|
|
2300
2409
|
content: input.instruction,
|
|
2301
|
-
citations
|
|
2410
|
+
citations,
|
|
2411
|
+
...(mentionCharacterIds.length ? { metadata: { mentionCharacterIds } } : {})
|
|
2302
2412
|
});
|
|
2303
2413
|
const currentMessageId = input.currentMessageId ?? String(userMessage?.id ?? "");
|
|
2304
2414
|
if (userMessage)
|
|
2305
2415
|
sendEvent("user_message", { message: redactAiConversationMessage(userMessage, permissions) });
|
|
2306
2416
|
const suggestion = await ai.createStreamingChat({
|
|
2307
2417
|
workId: request.params.workId,
|
|
2308
|
-
instruction:
|
|
2418
|
+
instruction: resolvedInstruction,
|
|
2419
|
+
// 仍由生成路径基于原始范围持久化累计注入,保证预解析不会吞掉本轮自动命中。
|
|
2309
2420
|
scope: input.scope,
|
|
2310
2421
|
signal: controller.signal,
|
|
2311
2422
|
onToolCall: (toolCall, round) => sendEvent("tool_call", { ...toolCall, round }),
|
|
@@ -2513,6 +2624,13 @@ export function createRuntime(options) {
|
|
|
2513
2624
|
}
|
|
2514
2625
|
if (error instanceof multer.MulterError) {
|
|
2515
2626
|
logger.warn("http.request.upload_rejected", { ...commonFields, uploadCode: error.code });
|
|
2627
|
+
if (error.code === "LIMIT_FILE_SIZE") {
|
|
2628
|
+
const sizeError = uploadSizeError(request.path, uploadLimits);
|
|
2629
|
+
if (sizeError) {
|
|
2630
|
+
response.status(413).json({ error: sizeError });
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2516
2634
|
response.status(400).json({ error: { code: "UPLOAD_ERROR", message: error.message } });
|
|
2517
2635
|
return;
|
|
2518
2636
|
}
|
|
@@ -2551,14 +2669,41 @@ export function createRuntime(options) {
|
|
|
2551
2669
|
});
|
|
2552
2670
|
backups.startScheduler();
|
|
2553
2671
|
logger.info("runtime.ready", { serveUi: options.serveUi ?? true });
|
|
2554
|
-
|
|
2672
|
+
let closePromise = null;
|
|
2673
|
+
let stopping = false;
|
|
2674
|
+
let closed = false;
|
|
2675
|
+
const close = () => {
|
|
2676
|
+
if (closed)
|
|
2677
|
+
return Promise.resolve();
|
|
2678
|
+
if (closePromise)
|
|
2679
|
+
return closePromise;
|
|
2680
|
+
if (!stopping) {
|
|
2681
|
+
stopping = true;
|
|
2555
2682
|
logger.info("runtime.closing");
|
|
2556
2683
|
backups.dispose();
|
|
2557
2684
|
ai.dispose();
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2685
|
+
}
|
|
2686
|
+
closePromise = (async () => {
|
|
2687
|
+
try {
|
|
2688
|
+
await backups.waitForIdle(RUNTIME_BACKUP_IDLE_TIMEOUT_MS);
|
|
2689
|
+
collaborationPresence.close();
|
|
2690
|
+
database.close();
|
|
2691
|
+
if (temporaryAttachmentRoot)
|
|
2692
|
+
rmSync(temporaryAttachmentRoot, { recursive: true, force: true });
|
|
2693
|
+
closed = true;
|
|
2694
|
+
logger.info("runtime.closed");
|
|
2695
|
+
}
|
|
2696
|
+
catch (error) {
|
|
2697
|
+
logger.error("runtime.close_failed", { error: sanitizeError(error) });
|
|
2698
|
+
throw error;
|
|
2699
|
+
}
|
|
2700
|
+
finally {
|
|
2701
|
+
if (!closed)
|
|
2702
|
+
closePromise = null;
|
|
2703
|
+
}
|
|
2704
|
+
})();
|
|
2705
|
+
return closePromise;
|
|
2706
|
+
};
|
|
2707
|
+
return { app, database, store, ai, backups, auth, attachmentStorage, cleanupAttachments, close };
|
|
2563
2708
|
}
|
|
2564
2709
|
//# sourceMappingURL=app.js.map
|