@musnows/scriverse 0.5.4 → 0.5.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/app.js CHANGED
@@ -27,7 +27,7 @@ import { accountReference, logger, sanitizeError } from "./logger.js";
27
27
  import { currentRequestActor, runWithRequestActor } from "./request-context.js";
28
28
  import { APP_VERSION } from "./version.js";
29
29
  import { fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
30
- import { CollaborationPresence, editorPageKey, entityEditorPageKey, modulePageKey, pageLabelForKey, presencePageKinds } from "./collaboration-presence.js";
30
+ import { CollaborationPresence, entityEditorPageKey, presencePageKinds } from "./collaboration-presence.js";
31
31
  import { clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, relationshipAnalysisReadModules, setSessionCookie, UserAuthService } from "./user-auth.js";
32
32
  const nonEmpty = z.string().trim().min(1);
33
33
  const identifier = z.string().trim().min(1).max(200);
@@ -95,7 +95,7 @@ const presenceHeartbeatSchema = z.object({
95
95
  z.object({ kind: z.literal(presencePageKinds[0]) }).strict(),
96
96
  z.object({ kind: z.literal(presencePageKinds[1]), resourceId: identifier }).strict(),
97
97
  z.object({ kind: z.literal(presencePageKinds[2]), module: z.enum(["settings", "characters", "races", "organizations", "timeline", "relationships", "outlines", "reviews", "tasks", "ai-settings"]) }).strict(),
98
- z.object({ kind: z.literal(presencePageKinds[3]), module: z.enum(["setting", "character", "race", "organization"]), resourceId: identifier.optional() }).strict(),
98
+ z.object({ kind: z.literal(presencePageKinds[3]), module: z.enum(["setting", "character", "race", "organization", "relationship"]), resourceId: identifier.optional() }).strict(),
99
99
  z.object({ kind: z.literal(presencePageKinds[4]) }).strict()
100
100
  ])
101
101
  }).strict();
@@ -296,8 +296,18 @@ const modelSchema = z.object({
296
296
  const aiPromptSchema = z.object({
297
297
  systemPrompt: z.string().max(100_000).optional()
298
298
  });
299
+ const aiUsageQuerySchema = z.object({
300
+ timezoneOffset: z.coerce.number().int().min(-840).max(840).default(0)
301
+ }).strict();
299
302
  const platformPageSizesSchema = z.object({
303
+ settings: z.number().int().min(10).max(100).optional(),
300
304
  characters: z.number().int().min(10).max(100).optional(),
305
+ races: z.number().int().min(10).max(100).optional(),
306
+ organizations: z.number().int().min(10).max(100).optional(),
307
+ timeline: z.number().int().min(10).max(100).optional(),
308
+ outlines: z.number().int().min(10).max(100).optional(),
309
+ relationships: z.number().int().min(10).max(100).optional(),
310
+ reviews: z.number().int().min(10).max(100).optional(),
301
311
  analysisTasks: z.number().int().min(10).max(100).optional(),
302
312
  fileVersions: z.number().int().min(10).max(100).optional()
303
313
  }).strict();
@@ -358,12 +368,20 @@ const contextSchema = z.object({
358
368
  includeBookSummary: z.boolean().optional()
359
369
  });
360
370
  const analysisTaskTypeSchema = z.enum(["structure", "chapter-analysis", "character-extraction", "character-summary", "character-identity-audit", "timeline-analysis", "worldview-analysis", "setting-extraction", "consistency-check", "report-update", "book-analysis"]);
371
+ const relationshipSourceRefSchema = z.object({
372
+ sourceType: z.string().trim().min(1).max(50).regex(/^[a-z][a-z-]*$/u),
373
+ sourceId: identifier,
374
+ sourceVersion: z.string().trim().min(1).max(200)
375
+ }).strict();
361
376
  const relationshipAnalysisScopeSchema = z.object({
362
377
  type: z.enum(["chapter", "book", "settings"]),
363
378
  chapterId: identifier.optional(),
364
379
  includeAllSettings: z.boolean().optional(),
365
380
  additionalPrompt: z.string().trim().max(10_000).optional(),
366
381
  characterIds: z.array(identifier).max(20).optional(),
382
+ preFilterRelationshipSources: z.boolean().optional(),
383
+ previewRelationshipChanges: z.boolean().optional(),
384
+ relationshipSourceRefs: z.array(relationshipSourceRefSchema).max(5_000).optional(),
367
385
  replaceExistingRelationships: z.boolean().optional()
368
386
  }).strict().superRefine((scope, context) => {
369
387
  if (scope.type === "chapter" && !scope.chapterId) {
@@ -378,6 +396,18 @@ const relationshipAnalysisScopeSchema = z.object({
378
396
  if (scope.replaceExistingRelationships && !scope.characterIds?.length) {
379
397
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["replaceExistingRelationships"], message: "覆盖已有关系前必须选择被分析角色" });
380
398
  }
399
+ if (scope.preFilterRelationshipSources !== undefined && !scope.characterIds?.length) {
400
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["preFilterRelationshipSources"], message: "来源前置过滤仅支持定向人物关系分析" });
401
+ }
402
+ if (scope.relationshipSourceRefs !== undefined && !scope.characterIds?.length) {
403
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["relationshipSourceRefs"], message: "预检来源仅支持定向人物关系分析" });
404
+ }
405
+ if (scope.relationshipSourceRefs) {
406
+ const keys = scope.relationshipSourceRefs.map((ref) => `${ref.sourceType}:${ref.sourceId}`);
407
+ if (new Set(keys).size !== keys.length) {
408
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["relationshipSourceRefs"], message: "预检来源不能重复" });
409
+ }
410
+ }
381
411
  });
382
412
  const analysisTaskSchema = z.union([
383
413
  z.object({ taskType: z.literal("relationship-analysis"), scope: relationshipAnalysisScopeSchema.optional(), modelId: identifier.optional() }).strict(),
@@ -388,6 +418,15 @@ const analysisTaskSchema = z.union([
388
418
  if (input.scope?.additionalPrompt !== undefined) {
389
419
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "additionalPrompt"], message: "额外分析提示仅支持人物关系分析" });
390
420
  }
421
+ if (input.scope?.preFilterRelationshipSources !== undefined) {
422
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "preFilterRelationshipSources"], message: "来源前置过滤仅支持人物关系分析" });
423
+ }
424
+ if (input.scope?.previewRelationshipChanges !== undefined) {
425
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "previewRelationshipChanges"], message: "变更预览仅支持人物关系分析" });
426
+ }
427
+ if (input.scope?.relationshipSourceRefs !== undefined) {
428
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "relationshipSourceRefs"], message: "预检来源仅支持人物关系分析" });
429
+ }
391
430
  if (input.scope?.characterIds !== undefined || input.scope?.replaceExistingRelationships !== undefined) {
392
431
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "characterIds"], message: "被分析角色仅支持人物关系分析" });
393
432
  }
@@ -492,6 +531,11 @@ function redactTaskCharacterNames(record, permissions) {
492
531
  return redactedRelationship;
493
532
  });
494
533
  }
534
+ const changePreview = recordValue(taskResult.relationshipChangePreview);
535
+ if (changePreview) {
536
+ const { operations: _operations, ...redactedChangePreview } = changePreview;
537
+ redactedTaskResult.relationshipChangePreview = redactedChangePreview;
538
+ }
495
539
  const analysisTarget = recordValue(taskResult.analysisTarget);
496
540
  if (analysisTarget) {
497
541
  const { characterNames: _characterNames, ...redactedAnalysisTarget } = analysisTarget;
@@ -500,6 +544,14 @@ function redactTaskCharacterNames(record, permissions) {
500
544
  result.result = redactedTaskResult;
501
545
  }
502
546
  }
547
+ if (permissions.relationships === "none") {
548
+ const taskResult = recordValue(result.result);
549
+ const changePreview = recordValue(taskResult?.relationshipChangePreview);
550
+ if (taskResult && changePreview) {
551
+ const { operations: _operations, ...redactedChangePreview } = changePreview;
552
+ result.result = { ...taskResult, relationshipChangePreview: redactedChangePreview };
553
+ }
554
+ }
503
555
  const resultSummary = recordValue(result.resultSummary);
504
556
  const characterSensitiveTask = ["character-extraction", "character-summary", "character-identity-audit", "relationship-analysis"]
505
557
  .includes(String(result.taskType));
@@ -549,14 +601,14 @@ export function createRuntime(options) {
549
601
  mkdirSync(attachmentStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
550
602
  const auth = new UserAuthService(database);
551
603
  const collaborationPresence = new CollaborationPresence();
552
- const publishCollaborativeChange = (workId, pageKey, label = pageLabelForKey(pageKey)) => {
604
+ const publishRelationshipChange = (workId, relationshipId) => {
553
605
  const actor = currentRequestActor();
554
- if (!actor || !workId || !pageKey)
606
+ if (!actor || !workId || !relationshipId)
555
607
  return;
556
- collaborationPresence.publishChange(workId, pageKey, {
608
+ collaborationPresence.publishChange(workId, entityEditorPageKey("relationship", relationshipId), {
557
609
  userId: actor.userId,
558
610
  displayName: actor.displayName
559
- }, label);
611
+ });
560
612
  };
561
613
  const getDevelopmentUser = () => options.devAuthBypass
562
614
  ? auth.listUsers().find((user) => user.status === "active") ?? null
@@ -941,19 +993,22 @@ export function createRuntime(options) {
941
993
  const input = parse(z.object({ volumeId: identifier, title: nonEmpty.max(300), content: z.string().max(2_000_000).optional(), chapterType: chapterTypeSchema.optional() }), request.body);
942
994
  data(response, store.createChapter(request.params.workId, input), 201);
943
995
  });
996
+ app.get("/api/works/:workId/deleted-chapters", (request, response) => {
997
+ const pagination = parsePagination(request.query);
998
+ data(response, pagination
999
+ ? store.listDeletedChaptersPage(request.params.workId, pagination)
1000
+ : store.listDeletedChapters(request.params.workId));
1001
+ });
944
1002
  app.get("/api/chapters/:chapterId", (request, response) => data(response, store.getChapter(request.params.chapterId)));
945
1003
  app.patch("/api/chapters/:chapterId", (request, response) => {
946
1004
  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);
947
1005
  const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
948
1006
  const chapter = store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo);
949
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
950
1007
  data(response, chapter);
951
1008
  });
952
1009
  app.delete("/api/chapters/:chapterId", (request, response) => {
953
1010
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
954
- const chapter = store.getChapter(request.params.chapterId);
955
1011
  store.deleteChapter(request.params.chapterId, input.expectedVersionNo);
956
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
957
1012
  noContent(response);
958
1013
  });
959
1014
  app.get("/api/chapters/:chapterId/versions", (request, response) => {
@@ -964,10 +1019,29 @@ export function createRuntime(options) {
964
1019
  const pagination = parsePagination(request.query);
965
1020
  data(response, pagination ? store.listChapterInsightsPage(request.params.chapterId, pagination) : store.listChapterInsights(request.params.chapterId));
966
1021
  });
1022
+ app.get("/api/chapters/:chapterId/annotations", (request, response) => data(response, store.listChapterAnnotations(request.params.chapterId)));
1023
+ app.post("/api/chapters/:chapterId/annotations", (request, response) => {
1024
+ const input = parse(z.object({
1025
+ kind: z.enum(["note", "todo"]),
1026
+ startLine: z.number().int().positive(),
1027
+ endLine: z.number().int().positive(),
1028
+ note: z.string().trim().min(1).max(2000)
1029
+ }).strict().refine((value) => value.endLine >= value.startLine, { message: "结束行不能早于开始行", path: ["endLine"] }), request.body);
1030
+ data(response, store.createChapterAnnotation(request.params.chapterId, input), 201);
1031
+ });
1032
+ app.patch("/api/chapter-annotations/:annotationId", (request, response) => {
1033
+ const input = parse(z.object({ note: z.string().trim().min(1).max(2000).optional(), status: z.enum(["open", "resolved"]).optional(), expectedVersionNo: expectedVersionNoSchema }).strict().refine((value) => value.note !== undefined || value.status !== undefined, { message: "至少需要修改一项" }), request.body);
1034
+ const { expectedVersionNo, ...update } = input;
1035
+ data(response, store.updateChapterAnnotation(request.params.annotationId, update, expectedVersionNo));
1036
+ });
1037
+ app.delete("/api/chapter-annotations/:annotationId", (request, response) => {
1038
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1039
+ store.deleteChapterAnnotation(request.params.annotationId, input.expectedVersionNo);
1040
+ noContent(response);
1041
+ });
967
1042
  app.post("/api/chapters/:chapterId/restore", (request, response) => {
968
1043
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
969
1044
  const chapter = store.restoreChapter(request.params.chapterId, input.versionNo, input.expectedVersionNo);
970
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
971
1045
  data(response, chapter);
972
1046
  });
973
1047
  app.post("/api/chapters/:chapterId/move", (request, response) => {
@@ -975,6 +1049,17 @@ export function createRuntime(options) {
975
1049
  const { expectedVersionNo, ...moveInput } = input;
976
1050
  data(response, store.moveChapter(request.params.chapterId, moveInput, expectedVersionNo));
977
1051
  });
1052
+ app.post("/api/works/:workId/chapters/batch", (request, response) => {
1053
+ const selectedChapters = z.array(z.object({ id: identifier, expectedVersionNo: z.number().int().positive() }).strict()).min(1).max(200);
1054
+ const action = z.discriminatedUnion("type", [
1055
+ z.object({ type: z.literal("move"), volumeId: identifier }).strict(),
1056
+ z.object({ type: z.literal("setType"), chapterType: chapterTypeSchema }).strict(),
1057
+ z.object({ type: z.literal("setAnalysisExclusion"), excludedFromAnalysis: z.boolean() }).strict(),
1058
+ z.object({ type: z.literal("delete") }).strict()
1059
+ ]);
1060
+ const input = parse(z.object({ chapters: selectedChapters, action }).strict(), request.body);
1061
+ data(response, store.batchManageChapters(request.params.workId, input.chapters, input.action));
1062
+ });
978
1063
  app.get("/api/works/:workId/outlines", (request, response) => {
979
1064
  const pagination = parsePagination(request.query);
980
1065
  data(response, pagination ? store.listChapterOutlinesPage(request.params.workId, pagination) : store.listChapterOutlines(request.params.workId));
@@ -983,14 +1068,11 @@ export function createRuntime(options) {
983
1068
  app.put("/api/chapters/:chapterId/outline", (request, response) => {
984
1069
  const { changeNote, expectedVersionNo, ...input } = parse(chapterOutlineSchema.extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
985
1070
  const outline = store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote, expectedVersionNo);
986
- publishCollaborativeChange(String(outline.workId), modulePageKey("outlines"));
987
1071
  data(response, outline);
988
1072
  });
989
1073
  app.delete("/api/chapters/:chapterId/outline", (request, response) => {
990
1074
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
991
- const chapter = store.getChapter(request.params.chapterId);
992
1075
  store.deleteChapterOutline(request.params.chapterId, input.expectedVersionNo);
993
- publishCollaborativeChange(String(chapter.workId), modulePageKey("outlines"));
994
1076
  noContent(response);
995
1077
  });
996
1078
  app.get("/api/works/:workId/foreshadows", (request, response) => {
@@ -1005,42 +1087,34 @@ export function createRuntime(options) {
1005
1087
  });
1006
1088
  app.post("/api/works/:workId/foreshadows", (request, response) => {
1007
1089
  const foreshadow = store.createForeshadow(request.params.workId, parse(foreshadowSchema, request.body));
1008
- publishCollaborativeChange(request.params.workId, modulePageKey("outlines"));
1009
1090
  data(response, foreshadow, 201);
1010
1091
  });
1011
1092
  app.get("/api/foreshadows/:foreshadowId", (request, response) => data(response, store.getForeshadow(request.params.foreshadowId)));
1012
1093
  app.patch("/api/foreshadows/:foreshadowId", (request, response) => {
1013
1094
  const { changeNote, expectedVersionNo, ...input } = parse(foreshadowSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1014
1095
  const foreshadow = store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote, expectedVersionNo);
1015
- publishCollaborativeChange(String(foreshadow.workId), modulePageKey("outlines"));
1016
1096
  data(response, foreshadow);
1017
1097
  });
1018
1098
  app.delete("/api/foreshadows/:foreshadowId", (request, response) => {
1019
1099
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1020
- const foreshadow = store.getForeshadow(request.params.foreshadowId);
1021
1100
  store.deleteForeshadow(request.params.foreshadowId, input.expectedVersionNo);
1022
- publishCollaborativeChange(String(foreshadow.workId), modulePageKey("outlines"));
1023
1101
  noContent(response);
1024
1102
  });
1025
1103
  app.post("/api/foreshadows/:foreshadowId/occurrences", (request, response) => {
1026
1104
  const input = parse(foreshadowOccurrenceSchema.extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1027
1105
  const { expectedVersionNo, ...occurrenceInput } = input;
1028
1106
  const occurrence = store.createForeshadowOccurrence(request.params.foreshadowId, occurrenceInput, expectedVersionNo);
1029
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1030
1107
  data(response, occurrence, 201);
1031
1108
  });
1032
1109
  app.patch("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
1033
1110
  const input = parse(foreshadowOccurrenceSchema.partial().extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1034
1111
  const { expectedVersionNo, ...occurrenceInput } = input;
1035
1112
  const occurrence = store.updateForeshadowOccurrence(request.params.occurrenceId, occurrenceInput, expectedVersionNo);
1036
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1037
1113
  data(response, occurrence);
1038
1114
  });
1039
1115
  app.delete("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
1040
1116
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1041
- const occurrence = store.getForeshadowOccurrence(request.params.occurrenceId);
1042
1117
  store.deleteForeshadowOccurrence(request.params.occurrenceId, input.expectedVersionNo);
1043
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1044
1118
  noContent(response);
1045
1119
  });
1046
1120
  app.get("/api/works/:workId/settings", (request, response) => {
@@ -1050,7 +1124,6 @@ export function createRuntime(options) {
1050
1124
  });
1051
1125
  app.post("/api/works/:workId/settings", (request, response) => {
1052
1126
  const setting = store.createSetting(request.params.workId, parse(settingSchema, request.body));
1053
- publishCollaborativeChange(request.params.workId, modulePageKey("settings"));
1054
1127
  data(response, setting, 201);
1055
1128
  });
1056
1129
  app.get("/api/works/:workId/settings/context", (request, response) => data(response, store.listSettings(request.params.workId, true)));
@@ -1058,14 +1131,11 @@ export function createRuntime(options) {
1058
1131
  app.patch("/api/settings/:settingId", (request, response) => {
1059
1132
  const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1060
1133
  const setting = store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo);
1061
- publishCollaborativeChange(String(setting.workId), entityEditorPageKey("setting", String(setting.id)));
1062
1134
  data(response, setting);
1063
1135
  });
1064
1136
  app.delete("/api/settings/:settingId", (request, response) => {
1065
1137
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1066
- const setting = store.getSetting(request.params.settingId);
1067
1138
  store.deleteSetting(request.params.settingId, input.expectedVersionNo);
1068
- publishCollaborativeChange(String(setting.workId), entityEditorPageKey("setting", String(setting.id)));
1069
1139
  noContent(response);
1070
1140
  });
1071
1141
  app.get("/api/works/:workId/characters", (request, response) => {
@@ -1083,7 +1153,6 @@ export function createRuntime(options) {
1083
1153
  });
1084
1154
  app.post("/api/works/:workId/characters", (request, response) => {
1085
1155
  const character = store.createCharacter(request.params.workId, parse(characterSchema, request.body));
1086
- publishCollaborativeChange(request.params.workId, modulePageKey("characters"));
1087
1156
  data(response, redactCharacterLinks(character, requestPermissions(request, request.params.workId)), 201);
1088
1157
  });
1089
1158
  app.get("/api/characters/:characterId", (request, response) => {
@@ -1092,7 +1161,6 @@ export function createRuntime(options) {
1092
1161
  app.patch("/api/characters/:characterId", (request, response) => {
1093
1162
  const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
1094
1163
  const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
1095
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1096
1164
  data(response, redactCharacterLinks(character, requestPermissions(request)));
1097
1165
  });
1098
1166
  app.get("/api/characters/:characterId/versions", (request, response) => {
@@ -1104,14 +1172,11 @@ export function createRuntime(options) {
1104
1172
  app.post("/api/characters/:characterId/restore", (request, response) => {
1105
1173
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1106
1174
  const character = store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo);
1107
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1108
1175
  data(response, redactCharacterLinks(character, requestPermissions(request)));
1109
1176
  });
1110
1177
  app.delete("/api/characters/:characterId", (request, response) => {
1111
1178
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1112
- const character = store.getCharacter(request.params.characterId);
1113
1179
  store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
1114
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1115
1180
  noContent(response);
1116
1181
  });
1117
1182
  app.post("/api/characters/:characterId/merge", (request, response) => {
@@ -1136,7 +1201,6 @@ export function createRuntime(options) {
1136
1201
  });
1137
1202
  app.post("/api/characters/:characterId/sections", (request, response) => {
1138
1203
  const section = store.createCharacterProfileSection(request.params.characterId, parse(characterProfileSectionSchema, request.body));
1139
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1140
1204
  data(response, section, 201);
1141
1205
  });
1142
1206
  app.get("/api/character-sections/:sectionId", (request, response) => {
@@ -1145,14 +1209,11 @@ export function createRuntime(options) {
1145
1209
  app.patch("/api/character-sections/:sectionId", (request, response) => {
1146
1210
  const { changeNote, expectedVersionNo, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1147
1211
  const section = store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo);
1148
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1149
1212
  data(response, section);
1150
1213
  });
1151
1214
  app.delete("/api/character-sections/:sectionId", (request, response) => {
1152
1215
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1153
- const section = store.getCharacterProfileSection(request.params.sectionId);
1154
1216
  store.deleteCharacterProfileSection(request.params.sectionId, input.expectedVersionNo);
1155
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1156
1217
  noContent(response);
1157
1218
  });
1158
1219
  app.get("/api/character-sections/:sectionId/versions", (request, response) => {
@@ -1164,7 +1225,6 @@ export function createRuntime(options) {
1164
1225
  app.post("/api/character-sections/:sectionId/restore", (request, response) => {
1165
1226
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1166
1227
  const section = store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo, input.expectedVersionNo);
1167
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1168
1228
  data(response, section);
1169
1229
  });
1170
1230
  app.get("/api/works/:workId/attachments", (request, response) => {
@@ -1221,7 +1281,6 @@ export function createRuntime(options) {
1221
1281
  });
1222
1282
  app.post("/api/works/:workId/races", (request, response) => {
1223
1283
  const race = store.createRace(request.params.workId, parse(raceSchema, request.body));
1224
- publishCollaborativeChange(request.params.workId, modulePageKey("races"));
1225
1284
  data(response, redactRaceMembers(race, requestPermissions(request, request.params.workId)), 201);
1226
1285
  });
1227
1286
  app.get("/api/races/:raceId", (request, response) => {
@@ -1230,14 +1289,11 @@ export function createRuntime(options) {
1230
1289
  app.patch("/api/races/:raceId", (request, response) => {
1231
1290
  const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1232
1291
  const race = store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo);
1233
- publishCollaborativeChange(String(race.workId), entityEditorPageKey("race", String(race.id)));
1234
1292
  data(response, redactRaceMembers(race, requestPermissions(request)));
1235
1293
  });
1236
1294
  app.delete("/api/races/:raceId", (request, response) => {
1237
1295
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1238
- const race = store.getRace(request.params.raceId);
1239
1296
  store.deleteRace(request.params.raceId, input.expectedVersionNo);
1240
- publishCollaborativeChange(String(race.workId), entityEditorPageKey("race", String(race.id)));
1241
1297
  noContent(response);
1242
1298
  });
1243
1299
  app.post("/api/races/:raceId/merge", (request, response) => {
@@ -1255,7 +1311,6 @@ export function createRuntime(options) {
1255
1311
  });
1256
1312
  app.post("/api/works/:workId/organizations", (request, response) => {
1257
1313
  const organization = store.createOrganization(request.params.workId, parse(organizationSchema, request.body));
1258
- publishCollaborativeChange(request.params.workId, modulePageKey("organizations"));
1259
1314
  data(response, redactOrganizationMembers(organization, requestPermissions(request, request.params.workId)), 201);
1260
1315
  });
1261
1316
  app.get("/api/organizations/:organizationId", (request, response) => {
@@ -1264,14 +1319,11 @@ export function createRuntime(options) {
1264
1319
  app.patch("/api/organizations/:organizationId", (request, response) => {
1265
1320
  const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1266
1321
  const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
1267
- publishCollaborativeChange(String(organization.workId), entityEditorPageKey("organization", String(organization.id)));
1268
1322
  data(response, redactOrganizationMembers(organization, requestPermissions(request)));
1269
1323
  });
1270
1324
  app.delete("/api/organizations/:organizationId", (request, response) => {
1271
1325
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1272
- const organization = store.getOrganization(request.params.organizationId);
1273
1326
  store.deleteOrganization(request.params.organizationId, input.expectedVersionNo);
1274
- publishCollaborativeChange(String(organization.workId), entityEditorPageKey("organization", String(organization.id)));
1275
1327
  noContent(response);
1276
1328
  });
1277
1329
  app.post("/api/organizations/:organizationId/merge", (request, response) => {
@@ -1286,21 +1338,17 @@ export function createRuntime(options) {
1286
1338
  });
1287
1339
  app.post("/api/works/:workId/timeline-tracks", (request, response) => {
1288
1340
  const track = store.createTimelineTrack(request.params.workId, parse(timelineTrackSchema, request.body));
1289
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1290
1341
  data(response, track, 201);
1291
1342
  });
1292
1343
  app.get("/api/timeline-tracks/:trackId", (request, response) => data(response, store.getTimelineTrack(request.params.trackId)));
1293
1344
  app.patch("/api/timeline-tracks/:trackId", (request, response) => {
1294
1345
  const { changeNote, expectedVersionNo, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1295
1346
  const track = store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo);
1296
- publishCollaborativeChange(String(track.workId), modulePageKey("timeline"));
1297
1347
  data(response, track);
1298
1348
  });
1299
1349
  app.delete("/api/timeline-tracks/:trackId", (request, response) => {
1300
1350
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1301
- const track = store.getTimelineTrack(request.params.trackId);
1302
1351
  store.deleteTimelineTrack(request.params.trackId, input.expectedVersionNo);
1303
- publishCollaborativeChange(String(track.workId), modulePageKey("timeline"));
1304
1352
  noContent(response);
1305
1353
  });
1306
1354
  app.get("/api/works/:workId/timeline", (request, response) => {
@@ -1309,7 +1357,6 @@ export function createRuntime(options) {
1309
1357
  });
1310
1358
  app.post("/api/works/:workId/timeline", (request, response) => {
1311
1359
  const event = store.createTimelineEvent(request.params.workId, parse(timelineSchema, request.body));
1312
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1313
1360
  data(response, event, 201);
1314
1361
  });
1315
1362
  app.post("/api/works/:workId/timeline/merge", (request, response) => {
@@ -1323,14 +1370,12 @@ export function createRuntime(options) {
1323
1370
  }).strict(), request.body);
1324
1371
  const { expectedVersionNos, ...mergeInput } = input;
1325
1372
  const merged = store.mergeTimelineEvents(request.params.workId, input.eventIds, mergeInput, expectedVersionNos);
1326
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1327
1373
  data(response, merged, 201);
1328
1374
  });
1329
1375
  app.get("/api/timeline/:eventId", (request, response) => data(response, store.getTimelineEvent(request.params.eventId)));
1330
1376
  app.patch("/api/timeline/:eventId", (request, response) => {
1331
1377
  const { changeNote, expectedVersionNo, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1332
1378
  const event = store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo);
1333
- publishCollaborativeChange(String(event.workId), modulePageKey("timeline"));
1334
1379
  data(response, event);
1335
1380
  });
1336
1381
  app.post("/api/timeline/:eventId/split", (request, response) => {
@@ -1344,16 +1389,11 @@ export function createRuntime(options) {
1344
1389
  expectedVersionNo: expectedVersionNoSchema
1345
1390
  }).strict(), request.body);
1346
1391
  const split = store.splitTimelineEvent(request.params.eventId, input.parts, input.expectedVersionNo);
1347
- const first = split[0];
1348
- if (first)
1349
- publishCollaborativeChange(String(first.workId), modulePageKey("timeline"));
1350
1392
  data(response, split, 201);
1351
1393
  });
1352
1394
  app.delete("/api/timeline/:eventId", (request, response) => {
1353
1395
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1354
- const event = store.getTimelineEvent(request.params.eventId);
1355
1396
  store.deleteTimelineEvent(request.params.eventId, input.expectedVersionNo);
1356
- publishCollaborativeChange(String(event.workId), modulePageKey("timeline"));
1357
1397
  noContent(response);
1358
1398
  });
1359
1399
  app.get("/api/works/:workId/relationships", (request, response) => {
@@ -1367,21 +1407,20 @@ export function createRuntime(options) {
1367
1407
  });
1368
1408
  app.post("/api/works/:workId/relationships", (request, response) => {
1369
1409
  const relationship = store.createRelationship(request.params.workId, parse(relationshipSchema, request.body));
1370
- publishCollaborativeChange(request.params.workId, modulePageKey("relationships"));
1371
1410
  data(response, relationship, 201);
1372
1411
  });
1373
1412
  app.get("/api/relationships/:relationshipId", (request, response) => data(response, store.getRelationship(request.params.relationshipId)));
1374
1413
  app.patch("/api/relationships/:relationshipId", (request, response) => {
1375
1414
  const { changeNote, expectedVersionNo, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1376
1415
  const relationship = store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo);
1377
- publishCollaborativeChange(String(relationship.workId), modulePageKey("relationships"));
1416
+ publishRelationshipChange(String(relationship.workId), String(relationship.id));
1378
1417
  data(response, relationship);
1379
1418
  });
1380
1419
  app.delete("/api/relationships/:relationshipId", (request, response) => {
1381
1420
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1382
1421
  const relationship = store.getRelationship(request.params.relationshipId);
1383
1422
  store.deleteRelationship(request.params.relationshipId, input.expectedVersionNo);
1384
- publishCollaborativeChange(String(relationship.workId), modulePageKey("relationships"));
1423
+ publishRelationshipChange(String(relationship.workId), String(relationship.id));
1385
1424
  noContent(response);
1386
1425
  });
1387
1426
  app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
@@ -1434,6 +1473,20 @@ export function createRuntime(options) {
1434
1473
  const permissions = requestPermissions(request, request.params.workId);
1435
1474
  data(response, mapRecords(store.listTaskSummariesPage(request.params.workId, parsePagination(request.query) ?? { page: 1, limit: 30, offset: 0 }), (task) => redactTaskCharacterNames(task, permissions)));
1436
1475
  });
1476
+ app.post("/api/works/:workId/tasks/relationship-source-preview", async (request, response) => {
1477
+ const input = parse(z.object({
1478
+ scope: relationshipAnalysisScopeSchema,
1479
+ modelId: identifier.optional()
1480
+ }).strict(), request.body);
1481
+ const permissions = requestPermissions(request, request.params.workId);
1482
+ const deniedModules = relationshipAnalysisReadModules(input.scope).filter((module) => permissions[module] === "none");
1483
+ if (deniedModules.length > 0) {
1484
+ throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取人物关系来源预检所需资料模块的权限", {
1485
+ modules: deniedModules
1486
+ });
1487
+ }
1488
+ data(response, await ai.previewRelationshipSources(request.params.workId, input.scope, input.modelId));
1489
+ });
1437
1490
  app.post("/api/works/:workId/tasks", (request, response) => {
1438
1491
  const input = parse(analysisTaskSchema, request.body);
1439
1492
  data(response, redactTaskCharacterNames(ai.createTask(request.params.workId, input), requestPermissions(request, request.params.workId)), 201);
@@ -1485,7 +1538,30 @@ export function createRuntime(options) {
1485
1538
  allowAdminAccess: request.authMethod !== "api-key"
1486
1539
  } : undefined), requestPermissions(request)));
1487
1540
  });
1541
+ app.post("/api/tasks/:taskId/rerun", (request, response) => {
1542
+ parse(z.object({}).strict(), request.body ?? {});
1543
+ const task = store.getTask(request.params.taskId);
1544
+ if (task.taskType === "relationship-analysis") {
1545
+ const permissions = requestPermissions(request, String(task.workId));
1546
+ const deniedModules = relationshipAnalysisReadModules(task.scope).filter((module) => permissions[module] === "none");
1547
+ if (deniedModules.length > 0) {
1548
+ throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取本次定向人物关系分析所需资料模块的权限", {
1549
+ modules: deniedModules
1550
+ });
1551
+ }
1552
+ }
1553
+ data(response, redactTaskCharacterNames(ai.rerunTask(request.params.taskId), requestPermissions(request)), 201);
1554
+ });
1488
1555
  app.post("/api/tasks/:taskId/cancel", (request, response) => data(response, redactTaskCharacterNames(ai.cancelTask(request.params.taskId), requestPermissions(request))));
1556
+ app.post("/api/tasks/:taskId/relationship-changes/apply", (request, response) => {
1557
+ parse(z.object({}).strict(), request.body ?? {});
1558
+ const applied = ai.applyRelationshipChangePreview(request.params.taskId);
1559
+ data(response, redactTaskCharacterNames(applied, requestPermissions(request)));
1560
+ });
1561
+ app.post("/api/tasks/:taskId/relationship-changes/discard", (request, response) => {
1562
+ parse(z.object({}).strict(), request.body ?? {});
1563
+ data(response, redactTaskCharacterNames(ai.discardRelationshipChangePreview(request.params.taskId), requestPermissions(request)));
1564
+ });
1489
1565
  app.get("/api/platform/ai/providers", (request, response) => {
1490
1566
  const pagination = parsePagination(request.query);
1491
1567
  data(response, pagination ? ai.listProvidersPage(pagination) : ai.listProviders());
@@ -1497,12 +1573,39 @@ export function createRuntime(options) {
1497
1573
  });
1498
1574
  app.get("/api/platform/ai/settings", (_request, response) => data(response, store.getPlatformAiSettings()));
1499
1575
  app.patch("/api/platform/ai/settings", (request, response) => data(response, store.updatePlatformAiSettings(parse(aiPromptSchema, request.body))));
1576
+ app.get("/api/platform/ai/usage", (request, response) => {
1577
+ const query = parse(aiUsageQuerySchema, request.query);
1578
+ data(response, ai.getPlatformTokenUsage(query.timezoneOffset));
1579
+ });
1500
1580
  app.get("/api/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
1501
1581
  app.get("/api/platform/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
1502
1582
  app.patch("/api/platform/ui-settings", (request, response) => {
1503
1583
  data(response, store.updatePlatformUiSettings(parse(platformUiSettingsSchema, request.body)));
1504
1584
  });
1505
1585
  app.get("/api/works/:workId/ai-settings", (request, response) => data(response, store.getWorkAiSettings(request.params.workId)));
1586
+ app.get("/api/works/:workId/ai-settings/usage", (request, response) => {
1587
+ const query = parse(aiUsageQuerySchema, request.query);
1588
+ data(response, ai.getWorkTokenUsage(request.params.workId, query.timezoneOffset));
1589
+ });
1590
+ app.get("/api/works/:workId/ai-settings/relationship-search-index", (request, response) => {
1591
+ data(response, ai.getRelationshipSearchIndexStatus(request.params.workId));
1592
+ });
1593
+ app.post("/api/works/:workId/ai-settings/relationship-search-index/sync", (request, response) => {
1594
+ const workId = request.params.workId;
1595
+ const result = ai.syncRelationshipSearchIndex(workId);
1596
+ store.audit(workId, "relationship.search-index.incremental-sync-queued", "work-ai-settings", workId, {
1597
+ queuedSourceCount: result.queuedSourceCount
1598
+ });
1599
+ data(response, result, 202);
1600
+ });
1601
+ app.post("/api/works/:workId/ai-settings/relationship-search-index/rebuild", (request, response) => {
1602
+ const workId = request.params.workId;
1603
+ const result = ai.rebuildRelationshipSearchIndex(workId);
1604
+ store.audit(workId, "relationship.search-index.rebuild-queued", "work-ai-settings", workId, {
1605
+ queuedSourceCount: result.queuedSourceCount
1606
+ });
1607
+ data(response, result, 202);
1608
+ });
1506
1609
  app.patch("/api/works/:workId/ai-settings", (request, response) => {
1507
1610
  const workId = request.params.workId;
1508
1611
  const before = store.getWorkAiSettings(workId);
@@ -1767,6 +1870,15 @@ export function createRuntime(options) {
1767
1870
  const pagination = parsePagination(request.query);
1768
1871
  data(response, pagination ? store.listAuditLogsPage(request.params.workId, pagination) : store.listAuditLogs(request.params.workId));
1769
1872
  });
1873
+ app.get("/api/works/:workId/writing-progress", (request, response) => data(response, store.getWritingProgress(request.params.workId)));
1874
+ app.put("/api/works/:workId/writing-goal", (request, response) => {
1875
+ const input = parse(z.object({
1876
+ dailyGoal: z.number().int().min(0).max(1_000_000),
1877
+ targetTotal: z.number().int().min(0).max(100_000_000),
1878
+ deadline: z.string().date().nullable()
1879
+ }).strict(), request.body);
1880
+ data(response, store.updateWritingGoal(request.params.workId, input));
1881
+ });
1770
1882
  if (options.serveUi ?? true) {
1771
1883
  const publicPath = options.publicPath ?? join(process.cwd(), "src", "public");
1772
1884
  // index.html 按登录态动态下发:未登录时注入 login-route 类,首帧直接渲染登录页;