@musnows/scriverse 0.7.1 → 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 +194 -40
- 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 +215 -4
- 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 +1018 -292
- package/dist/public/background-task-center.js +1 -1
- package/dist/public/chapter-editor-virtualization.js +52 -0
- package/dist/public/index.html +44 -22
- package/dist/public/presence-client-id.d.ts +10 -0
- package/dist/public/presence-client-id.js +31 -0
- package/dist/public/relationship-graph.js +24 -4
- 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 +39 -9
- 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 +77 -18
- 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"]),
|
|
@@ -389,7 +407,16 @@ const platformPageSizesSchema = z.object({
|
|
|
389
407
|
const platformUiSettingsSchema = z.object({
|
|
390
408
|
toastPosition: z.enum(["bottom-right", "top-right"]).optional(),
|
|
391
409
|
pageSizes: platformPageSizesSchema.optional(),
|
|
392
|
-
galaxyFrameRate: z.union([
|
|
410
|
+
galaxyFrameRate: z.union([
|
|
411
|
+
z.literal(24),
|
|
412
|
+
z.literal(30),
|
|
413
|
+
z.literal(60),
|
|
414
|
+
z.literal(90),
|
|
415
|
+
z.literal(120),
|
|
416
|
+
z.literal(144),
|
|
417
|
+
z.literal(165),
|
|
418
|
+
z.literal(240)
|
|
419
|
+
]).optional()
|
|
393
420
|
}).strict().refine((input) => input.toastPosition !== undefined || input.pageSizes !== undefined || input.galaxyFrameRate !== undefined, {
|
|
394
421
|
message: "至少需要提供一项界面设置"
|
|
395
422
|
});
|
|
@@ -437,6 +464,12 @@ const s3BackupRunQuerySchema = z.object({
|
|
|
437
464
|
afterSequence: z.coerce.number().int().min(0).optional(),
|
|
438
465
|
limit: z.coerce.number().int().min(1).max(100).optional()
|
|
439
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();
|
|
440
473
|
const aiToolCallResultSchema = z.object({
|
|
441
474
|
id: z.string().min(1).max(300),
|
|
442
475
|
name: z.string().min(1).max(200),
|
|
@@ -508,7 +541,8 @@ const contextSchema = z.object({
|
|
|
508
541
|
includeBookSummary: z.boolean().optional(),
|
|
509
542
|
includeSettingInfo: z.boolean().optional()
|
|
510
543
|
});
|
|
511
|
-
|
|
544
|
+
/** 创建任务 API 的分析类型校验:仅允许可新建类型,历史类型在运行层保留防御性拒绝。 */
|
|
545
|
+
export const creatableAnalysisTaskTypeSchema = z.enum(CREATABLE_ANALYSIS_TASK_TYPES);
|
|
512
546
|
const relationshipSourceRefSchema = z.object({
|
|
513
547
|
sourceType: z.string().trim().min(1).max(50).regex(/^[a-z][a-z-]*$/u),
|
|
514
548
|
sourceId: identifier,
|
|
@@ -552,7 +586,7 @@ const relationshipAnalysisScopeSchema = z.object({
|
|
|
552
586
|
});
|
|
553
587
|
const analysisTaskSchema = z.union([
|
|
554
588
|
z.object({ taskType: z.literal("relationship-analysis"), scope: relationshipAnalysisScopeSchema.optional(), modelId: identifier.optional() }).strict(),
|
|
555
|
-
z.object({ taskType:
|
|
589
|
+
z.object({ taskType: creatableAnalysisTaskTypeSchema, scope: jsonObject.optional(), modelId: identifier.optional() }).strict().superRefine((input, context) => {
|
|
556
590
|
if (input.scope?.includeAllSettings !== undefined) {
|
|
557
591
|
context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "includeAllSettings"], message: "包含所有设定仅支持人物关系分析" });
|
|
558
592
|
}
|
|
@@ -573,6 +607,7 @@ const analysisTaskSchema = z.union([
|
|
|
573
607
|
}
|
|
574
608
|
})
|
|
575
609
|
]);
|
|
610
|
+
export const RUNTIME_BACKUP_IDLE_TIMEOUT_MS = 9_000;
|
|
576
611
|
function data(response, value, status = 200) {
|
|
577
612
|
response.status(status).json({ data: value });
|
|
578
613
|
}
|
|
@@ -781,11 +816,18 @@ function redactSuggestion(record, permissions) {
|
|
|
781
816
|
};
|
|
782
817
|
}
|
|
783
818
|
function redactAiConversationMessage(item, permissions) {
|
|
784
|
-
if (permissions.prose !== "none")
|
|
785
|
-
return item;
|
|
786
819
|
const message = recordValue(item);
|
|
787
820
|
if (!message)
|
|
788
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
|
+
}
|
|
789
831
|
return {
|
|
790
832
|
...message,
|
|
791
833
|
content: proseRestrictedPlaceholder,
|
|
@@ -798,25 +840,25 @@ function redactAiConversationMessage(item, permissions) {
|
|
|
798
840
|
function redactAiConversation(record, permissions) {
|
|
799
841
|
const readableRecord = permissions.characters === "none" ? { ...record, roleplayCharacter: null } : record;
|
|
800
842
|
const scopedRecord = redactAiCallContext(readableRecord, permissions);
|
|
801
|
-
if (permissions.prose !== "none")
|
|
802
|
-
return scopedRecord;
|
|
803
843
|
const result = {
|
|
804
|
-
...scopedRecord
|
|
805
|
-
title: proseRestrictedPlaceholder
|
|
844
|
+
...scopedRecord
|
|
806
845
|
};
|
|
807
|
-
if (
|
|
808
|
-
result.preview = proseRestrictedPlaceholder;
|
|
809
|
-
}
|
|
810
|
-
if (Array.isArray(result.messages)) {
|
|
846
|
+
if ((permissions.prose === "none" || permissions.characters === "none") && Array.isArray(result.messages)) {
|
|
811
847
|
result.messages = result.messages.map((item) => redactAiConversationMessage(item, permissions));
|
|
812
848
|
}
|
|
813
849
|
const messagesPage = recordValue(result.messagesPage);
|
|
814
|
-
if (messagesPage && Array.isArray(messagesPage.items)) {
|
|
850
|
+
if ((permissions.prose === "none" || permissions.characters === "none") && messagesPage && Array.isArray(messagesPage.items)) {
|
|
815
851
|
result.messagesPage = {
|
|
816
852
|
...messagesPage,
|
|
817
853
|
items: messagesPage.items.map((item) => redactAiConversationMessage(item, permissions))
|
|
818
854
|
};
|
|
819
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
|
+
}
|
|
820
862
|
return { ...result, restricted: true };
|
|
821
863
|
}
|
|
822
864
|
/** SSE 错误事件只暴露 AppError 的公开信息;AI_CALL_FAILED 的 failure 已在 AI 层完成密钥脱敏。 */
|
|
@@ -856,6 +898,7 @@ function redactVersionSnapshots(value, mapper) {
|
|
|
856
898
|
});
|
|
857
899
|
}
|
|
858
900
|
export function createRuntime(options) {
|
|
901
|
+
const uploadLimits = options.uploadLimits ?? DEFAULT_IMAGE_UPLOAD_LIMITS;
|
|
859
902
|
logger.info("runtime.initializing", {
|
|
860
903
|
databasePath: options.databasePath,
|
|
861
904
|
serveUi: options.serveUi ?? true,
|
|
@@ -869,19 +912,35 @@ export function createRuntime(options) {
|
|
|
869
912
|
const temporaryAttachmentRoot = options.databasePath === ":memory:" && !options.attachmentDirectory
|
|
870
913
|
? mkdtempSync(join(tmpdir(), "scriverse-attachments-"))
|
|
871
914
|
: null;
|
|
872
|
-
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);
|
|
873
916
|
mkdirSync(attachmentStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
|
|
874
917
|
const auth = new UserAuthService(database);
|
|
875
|
-
const collaborationPresence = new CollaborationPresence();
|
|
876
|
-
const
|
|
918
|
+
const collaborationPresence = new CollaborationPresence(45_000, Date.now, 120_000, 50, { store: new PresenceStore(database) });
|
|
919
|
+
const publishCollaborativeChange = (workId, pageKey, options = {}) => {
|
|
877
920
|
const actor = currentRequestActor();
|
|
878
|
-
if (!actor || !workId || !
|
|
921
|
+
if (!actor || !workId || !pageKey)
|
|
879
922
|
return;
|
|
880
|
-
collaborationPresence.publishChange(workId,
|
|
923
|
+
collaborationPresence.publishChange(workId, pageKey, {
|
|
881
924
|
userId: actor.userId,
|
|
882
925
|
displayName: actor.displayName
|
|
883
|
-
});
|
|
926
|
+
}, options);
|
|
884
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 };
|
|
885
944
|
const getDevelopmentUser = () => options.devAuthBypass
|
|
886
945
|
? auth.listUsers().find((user) => user.status === "active") ?? null
|
|
887
946
|
: null;
|
|
@@ -933,7 +992,9 @@ export function createRuntime(options) {
|
|
|
933
992
|
timeoutMs: options.releaseCheckTimeoutMs,
|
|
934
993
|
retries: options.releaseCheckRetries
|
|
935
994
|
});
|
|
936
|
-
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) => {
|
|
937
998
|
const requiredModules = analysisTaskReadModules(task.taskType, task.scope);
|
|
938
999
|
const creator = actor ? null : database.get("SELECT created_by_user_id FROM analysis_tasks WHERE id = ?", String(task.id));
|
|
939
1000
|
const userId = actor?.userId ?? (typeof creator?.created_by_user_id === "string" ? creator.created_by_user_id : null);
|
|
@@ -955,18 +1016,18 @@ export function createRuntime(options) {
|
|
|
955
1016
|
});
|
|
956
1017
|
const coverUpload = multer({
|
|
957
1018
|
storage: multer.memoryStorage(),
|
|
958
|
-
limits: { fileSize:
|
|
1019
|
+
limits: { fileSize: uploadLimits.coverBytes + 1, files: 1, fields: 4, fieldSize: 16 * 1024, parts: 5, headerPairs: 100 }
|
|
959
1020
|
});
|
|
960
1021
|
const avatarUpload = multer({
|
|
961
1022
|
storage: multer.memoryStorage(),
|
|
962
|
-
limits: { fileSize:
|
|
1023
|
+
limits: { fileSize: uploadLimits.avatarBytes + 1, files: 1, fields: 1, fieldSize: 1024, parts: 2, headerPairs: 50 }
|
|
963
1024
|
});
|
|
964
1025
|
const attachmentUpload = multer({
|
|
965
1026
|
storage: multer.diskStorage({
|
|
966
1027
|
destination: attachmentStorage.temporaryDirectory,
|
|
967
1028
|
filename: (_request, _file, callback) => callback(null, randomUUID())
|
|
968
1029
|
}),
|
|
969
|
-
limits: { fileSize:
|
|
1030
|
+
limits: { fileSize: uploadLimits.attachmentBytes + 1, files: 1, fields: 4, fieldSize: 16 * 1024, parts: 5, headerPairs: 100 }
|
|
970
1031
|
});
|
|
971
1032
|
app.disable("x-powered-by");
|
|
972
1033
|
const trustProxy = resolveTrustProxySetting(options.security?.trustProxy);
|
|
@@ -984,7 +1045,8 @@ export function createRuntime(options) {
|
|
|
984
1045
|
version: APP_VERSION,
|
|
985
1046
|
protocol: "openai-chat-completions",
|
|
986
1047
|
protocols: [...AI_PROVIDER_PROTOCOLS],
|
|
987
|
-
development: options.developmentServer === true
|
|
1048
|
+
development: options.developmentServer === true,
|
|
1049
|
+
uploadLimits
|
|
988
1050
|
});
|
|
989
1051
|
});
|
|
990
1052
|
app.get("/api/update-check", async (_request, response) => {
|
|
@@ -1077,7 +1139,8 @@ export function createRuntime(options) {
|
|
|
1077
1139
|
if (!request.authUser)
|
|
1078
1140
|
throw new AppError(401, "AUTH_REQUIRED", "请先登录");
|
|
1079
1141
|
if (!request.file)
|
|
1080
|
-
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)}`);
|
|
1081
1144
|
try {
|
|
1082
1145
|
const metadata = readRasterImageMetadata(request.file.buffer);
|
|
1083
1146
|
const updated = database.transaction(() => {
|
|
@@ -1262,8 +1325,11 @@ export function createRuntime(options) {
|
|
|
1262
1325
|
if (!request.file)
|
|
1263
1326
|
throw new AppError(400, "FILE_REQUIRED", "请选择 PNG、JPEG 或 WebP 封面");
|
|
1264
1327
|
const bytes = request.file.buffer;
|
|
1328
|
+
assertImageUploadSize(bytes.byteLength, uploadLimits.coverBytes, `封面图片不能超过 ${formatUploadLimit(uploadLimits.coverBytes)}`);
|
|
1265
1329
|
try {
|
|
1266
1330
|
const metadata = readRasterImageMetadata(bytes);
|
|
1331
|
+
if (metadata.mimeType === "image/gif")
|
|
1332
|
+
throw new AppError(415, "UNSUPPORTED_COVER_FORMAT", "封面不支持 GIF 图片");
|
|
1267
1333
|
const expectedVersionNo = parse(expectedVersionNoSchema, request.body.expectedVersionNo);
|
|
1268
1334
|
data(response, store.setWorkCover(String(request.params.workId), metadata.mimeType, bytes, expectedVersionNo));
|
|
1269
1335
|
}
|
|
@@ -1344,11 +1410,14 @@ export function createRuntime(options) {
|
|
|
1344
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);
|
|
1345
1411
|
const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
|
|
1346
1412
|
const chapter = store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo);
|
|
1413
|
+
publishEditorChange(String(chapter.workId), String(chapter.id));
|
|
1347
1414
|
data(response, chapter);
|
|
1348
1415
|
});
|
|
1349
1416
|
app.delete("/api/chapters/:chapterId", (request, response) => {
|
|
1350
1417
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1418
|
+
const chapter = store.getChapter(request.params.chapterId);
|
|
1351
1419
|
store.deleteChapter(request.params.chapterId, input.expectedVersionNo);
|
|
1420
|
+
publishEditorChange(String(chapter.workId), String(chapter.id), deletedPageChange);
|
|
1352
1421
|
noContent(response);
|
|
1353
1422
|
});
|
|
1354
1423
|
app.delete("/api/chapters/:chapterId/permanent", (request, response) => {
|
|
@@ -1509,11 +1578,14 @@ export function createRuntime(options) {
|
|
|
1509
1578
|
app.patch("/api/settings/:settingId", (request, response) => {
|
|
1510
1579
|
const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1511
1580
|
const setting = store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1581
|
+
publishEntityChange(String(setting.workId), "setting", String(setting.id));
|
|
1512
1582
|
data(response, setting);
|
|
1513
1583
|
});
|
|
1514
1584
|
app.delete("/api/settings/:settingId", (request, response) => {
|
|
1515
1585
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1586
|
+
const setting = store.getSetting(request.params.settingId);
|
|
1516
1587
|
store.deleteSetting(request.params.settingId, input.expectedVersionNo);
|
|
1588
|
+
publishEntityChange(String(setting.workId), "setting", String(setting.id), deletedPageChange);
|
|
1517
1589
|
noContent(response);
|
|
1518
1590
|
});
|
|
1519
1591
|
app.get("/api/works/:workId/characters", (request, response) => {
|
|
@@ -1539,6 +1611,7 @@ export function createRuntime(options) {
|
|
|
1539
1611
|
app.patch("/api/characters/:characterId", (request, response) => {
|
|
1540
1612
|
const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
|
|
1541
1613
|
const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1614
|
+
publishEntityChange(String(character.workId), "character", String(character.id));
|
|
1542
1615
|
data(response, redactCharacterLinks(character, requestPermissions(request)));
|
|
1543
1616
|
});
|
|
1544
1617
|
app.get("/api/characters/:characterId/versions", (request, response) => {
|
|
@@ -1554,7 +1627,9 @@ export function createRuntime(options) {
|
|
|
1554
1627
|
});
|
|
1555
1628
|
app.delete("/api/characters/:characterId", (request, response) => {
|
|
1556
1629
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1630
|
+
const character = store.getCharacter(request.params.characterId);
|
|
1557
1631
|
store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
|
|
1632
|
+
publishEntityChange(String(character.workId), "character", String(character.id), deletedPageChange);
|
|
1558
1633
|
noContent(response);
|
|
1559
1634
|
});
|
|
1560
1635
|
app.post("/api/characters/:characterId/merge", (request, response) => {
|
|
@@ -1587,11 +1662,17 @@ export function createRuntime(options) {
|
|
|
1587
1662
|
app.patch("/api/character-sections/:sectionId", (request, response) => {
|
|
1588
1663
|
const { changeNote, expectedVersionNo, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1589
1664
|
const section = store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1665
|
+
publishEntityChange(String(section.workId), "character", String(section.characterId));
|
|
1590
1666
|
data(response, section);
|
|
1591
1667
|
});
|
|
1592
1668
|
app.delete("/api/character-sections/:sectionId", (request, response) => {
|
|
1593
1669
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1670
|
+
const section = store.getCharacterProfileSection(request.params.sectionId);
|
|
1594
1671
|
store.deleteCharacterProfileSection(request.params.sectionId, input.expectedVersionNo);
|
|
1672
|
+
publishEntityChange(String(section.workId), "character", String(section.characterId), {
|
|
1673
|
+
action: "delete",
|
|
1674
|
+
label: "角色档案章节"
|
|
1675
|
+
});
|
|
1595
1676
|
noContent(response);
|
|
1596
1677
|
});
|
|
1597
1678
|
app.get("/api/character-sections/:sectionId/versions", (request, response) => {
|
|
@@ -1690,11 +1771,14 @@ export function createRuntime(options) {
|
|
|
1690
1771
|
app.patch("/api/races/:raceId", (request, response) => {
|
|
1691
1772
|
const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1692
1773
|
const race = store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1774
|
+
publishEntityChange(String(race.workId), "race", String(race.id));
|
|
1693
1775
|
data(response, redactRaceMembers(race, requestPermissions(request)));
|
|
1694
1776
|
});
|
|
1695
1777
|
app.delete("/api/races/:raceId", (request, response) => {
|
|
1696
1778
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1779
|
+
const race = store.getRace(request.params.raceId);
|
|
1697
1780
|
store.deleteRace(request.params.raceId, input.expectedVersionNo);
|
|
1781
|
+
publishEntityChange(String(race.workId), "race", String(race.id), deletedPageChange);
|
|
1698
1782
|
noContent(response);
|
|
1699
1783
|
});
|
|
1700
1784
|
app.post("/api/races/:raceId/merge", (request, response) => {
|
|
@@ -1720,11 +1804,14 @@ export function createRuntime(options) {
|
|
|
1720
1804
|
app.patch("/api/organizations/:organizationId", (request, response) => {
|
|
1721
1805
|
const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1722
1806
|
const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1807
|
+
publishEntityChange(String(organization.workId), "organization", String(organization.id));
|
|
1723
1808
|
data(response, redactOrganizationMembers(organization, requestPermissions(request)));
|
|
1724
1809
|
});
|
|
1725
1810
|
app.delete("/api/organizations/:organizationId", (request, response) => {
|
|
1726
1811
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1812
|
+
const organization = store.getOrganization(request.params.organizationId);
|
|
1727
1813
|
store.deleteOrganization(request.params.organizationId, input.expectedVersionNo);
|
|
1814
|
+
publishEntityChange(String(organization.workId), "organization", String(organization.id), deletedPageChange);
|
|
1728
1815
|
noContent(response);
|
|
1729
1816
|
});
|
|
1730
1817
|
app.post("/api/organizations/:organizationId/merge", (request, response) => {
|
|
@@ -1745,11 +1832,14 @@ export function createRuntime(options) {
|
|
|
1745
1832
|
app.patch("/api/timeline-tracks/:trackId", (request, response) => {
|
|
1746
1833
|
const { changeNote, expectedVersionNo, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1747
1834
|
const track = store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1835
|
+
publishModuleChange(String(track.workId), "timeline");
|
|
1748
1836
|
data(response, track);
|
|
1749
1837
|
});
|
|
1750
1838
|
app.delete("/api/timeline-tracks/:trackId", (request, response) => {
|
|
1751
1839
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1840
|
+
const track = store.getTimelineTrack(request.params.trackId);
|
|
1752
1841
|
store.deleteTimelineTrack(request.params.trackId, input.expectedVersionNo);
|
|
1842
|
+
publishModuleChange(String(track.workId), "timeline", { action: "delete", label: "时间轴轨道" });
|
|
1753
1843
|
noContent(response);
|
|
1754
1844
|
});
|
|
1755
1845
|
app.get("/api/works/:workId/timeline", (request, response) => {
|
|
@@ -1777,6 +1867,7 @@ export function createRuntime(options) {
|
|
|
1777
1867
|
app.patch("/api/timeline/:eventId", (request, response) => {
|
|
1778
1868
|
const { changeNote, expectedVersionNo, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1779
1869
|
const event = store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1870
|
+
publishModuleChange(String(event.workId), "timeline");
|
|
1780
1871
|
data(response, event);
|
|
1781
1872
|
});
|
|
1782
1873
|
app.post("/api/timeline/:eventId/split", (request, response) => {
|
|
@@ -1794,7 +1885,9 @@ export function createRuntime(options) {
|
|
|
1794
1885
|
});
|
|
1795
1886
|
app.delete("/api/timeline/:eventId", (request, response) => {
|
|
1796
1887
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1888
|
+
const event = store.getTimelineEvent(request.params.eventId);
|
|
1797
1889
|
store.deleteTimelineEvent(request.params.eventId, input.expectedVersionNo);
|
|
1890
|
+
publishModuleChange(String(event.workId), "timeline", { action: "delete", label: "时间轴事件" });
|
|
1798
1891
|
noContent(response);
|
|
1799
1892
|
});
|
|
1800
1893
|
app.get("/api/works/:workId/relationships", (request, response) => {
|
|
@@ -1814,14 +1907,14 @@ export function createRuntime(options) {
|
|
|
1814
1907
|
app.patch("/api/relationships/:relationshipId", (request, response) => {
|
|
1815
1908
|
const { changeNote, expectedVersionNo, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1816
1909
|
const relationship = store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo);
|
|
1817
|
-
|
|
1910
|
+
publishEntityChange(String(relationship.workId), "relationship", String(relationship.id));
|
|
1818
1911
|
data(response, relationship);
|
|
1819
1912
|
});
|
|
1820
1913
|
app.delete("/api/relationships/:relationshipId", (request, response) => {
|
|
1821
1914
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1822
1915
|
const relationship = store.getRelationship(request.params.relationshipId);
|
|
1823
1916
|
store.deleteRelationship(request.params.relationshipId, input.expectedVersionNo);
|
|
1824
|
-
|
|
1917
|
+
publishEntityChange(String(relationship.workId), "relationship", String(relationship.id), deletedPageChange);
|
|
1825
1918
|
noContent(response);
|
|
1826
1919
|
});
|
|
1827
1920
|
app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
|
|
@@ -1983,6 +2076,17 @@ export function createRuntime(options) {
|
|
|
1983
2076
|
app.patch("/api/platform/ui-settings", (request, response) => {
|
|
1984
2077
|
data(response, store.updatePlatformUiSettings(parse(platformUiSettingsSchema, request.body)));
|
|
1985
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
|
+
});
|
|
1986
2090
|
app.get("/api/platform/backups/targets", (_request, response) => data(response, backups.listTargets()));
|
|
1987
2091
|
app.post("/api/platform/backups/targets", (request, response) => {
|
|
1988
2092
|
data(response, backups.createTarget(parse(s3BackupTargetBaseSchema, request.body)), 201);
|
|
@@ -2242,6 +2346,7 @@ export function createRuntime(options) {
|
|
|
2242
2346
|
if (store.getChapter(citation.chapterId).workId !== request.params.workId)
|
|
2243
2347
|
throw new AppError(400, "CITATION_WORK_MISMATCH", "引用章节不属于当前作品");
|
|
2244
2348
|
}
|
|
2349
|
+
const resolvedInstruction = instructionWithCitations(input.instruction, citations);
|
|
2245
2350
|
const controller = new AbortController();
|
|
2246
2351
|
response.on("close", () => {
|
|
2247
2352
|
if (!response.writableEnded)
|
|
@@ -2272,7 +2377,7 @@ export function createRuntime(options) {
|
|
|
2272
2377
|
workId: request.params.workId,
|
|
2273
2378
|
modelId: input.modelId,
|
|
2274
2379
|
scope: input.scope,
|
|
2275
|
-
instruction:
|
|
2380
|
+
instruction: resolvedInstruction,
|
|
2276
2381
|
excludeConversationMessageId: input.currentMessageId
|
|
2277
2382
|
});
|
|
2278
2383
|
sendEvent("context", {
|
|
@@ -2284,19 +2389,34 @@ export function createRuntime(options) {
|
|
|
2284
2389
|
});
|
|
2285
2390
|
if (prepared.action === "warn")
|
|
2286
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
|
+
])];
|
|
2287
2405
|
const userMessage = input.currentMessageId
|
|
2288
2406
|
? null
|
|
2289
2407
|
: store.addAiConversationMessage(conversationId, {
|
|
2290
2408
|
role: "user",
|
|
2291
2409
|
content: input.instruction,
|
|
2292
|
-
citations
|
|
2410
|
+
citations,
|
|
2411
|
+
...(mentionCharacterIds.length ? { metadata: { mentionCharacterIds } } : {})
|
|
2293
2412
|
});
|
|
2294
2413
|
const currentMessageId = input.currentMessageId ?? String(userMessage?.id ?? "");
|
|
2295
2414
|
if (userMessage)
|
|
2296
2415
|
sendEvent("user_message", { message: redactAiConversationMessage(userMessage, permissions) });
|
|
2297
2416
|
const suggestion = await ai.createStreamingChat({
|
|
2298
2417
|
workId: request.params.workId,
|
|
2299
|
-
instruction:
|
|
2418
|
+
instruction: resolvedInstruction,
|
|
2419
|
+
// 仍由生成路径基于原始范围持久化累计注入,保证预解析不会吞掉本轮自动命中。
|
|
2300
2420
|
scope: input.scope,
|
|
2301
2421
|
signal: controller.signal,
|
|
2302
2422
|
onToolCall: (toolCall, round) => sendEvent("tool_call", { ...toolCall, round }),
|
|
@@ -2504,6 +2624,13 @@ export function createRuntime(options) {
|
|
|
2504
2624
|
}
|
|
2505
2625
|
if (error instanceof multer.MulterError) {
|
|
2506
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
|
+
}
|
|
2507
2634
|
response.status(400).json({ error: { code: "UPLOAD_ERROR", message: error.message } });
|
|
2508
2635
|
return;
|
|
2509
2636
|
}
|
|
@@ -2542,14 +2669,41 @@ export function createRuntime(options) {
|
|
|
2542
2669
|
});
|
|
2543
2670
|
backups.startScheduler();
|
|
2544
2671
|
logger.info("runtime.ready", { serveUi: options.serveUi ?? true });
|
|
2545
|
-
|
|
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;
|
|
2546
2682
|
logger.info("runtime.closing");
|
|
2547
2683
|
backups.dispose();
|
|
2548
2684
|
ai.dispose();
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
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 };
|
|
2554
2708
|
}
|
|
2555
2709
|
//# sourceMappingURL=app.js.map
|