@musnows/scriverse 0.5.4 → 0.5.5

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
@@ -946,14 +998,11 @@ export function createRuntime(options) {
946
998
  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
999
  const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
948
1000
  const chapter = store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo);
949
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
950
1001
  data(response, chapter);
951
1002
  });
952
1003
  app.delete("/api/chapters/:chapterId", (request, response) => {
953
1004
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
954
- const chapter = store.getChapter(request.params.chapterId);
955
1005
  store.deleteChapter(request.params.chapterId, input.expectedVersionNo);
956
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
957
1006
  noContent(response);
958
1007
  });
959
1008
  app.get("/api/chapters/:chapterId/versions", (request, response) => {
@@ -967,7 +1016,6 @@ export function createRuntime(options) {
967
1016
  app.post("/api/chapters/:chapterId/restore", (request, response) => {
968
1017
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
969
1018
  const chapter = store.restoreChapter(request.params.chapterId, input.versionNo, input.expectedVersionNo);
970
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
971
1019
  data(response, chapter);
972
1020
  });
973
1021
  app.post("/api/chapters/:chapterId/move", (request, response) => {
@@ -983,14 +1031,11 @@ export function createRuntime(options) {
983
1031
  app.put("/api/chapters/:chapterId/outline", (request, response) => {
984
1032
  const { changeNote, expectedVersionNo, ...input } = parse(chapterOutlineSchema.extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
985
1033
  const outline = store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote, expectedVersionNo);
986
- publishCollaborativeChange(String(outline.workId), modulePageKey("outlines"));
987
1034
  data(response, outline);
988
1035
  });
989
1036
  app.delete("/api/chapters/:chapterId/outline", (request, response) => {
990
1037
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
991
- const chapter = store.getChapter(request.params.chapterId);
992
1038
  store.deleteChapterOutline(request.params.chapterId, input.expectedVersionNo);
993
- publishCollaborativeChange(String(chapter.workId), modulePageKey("outlines"));
994
1039
  noContent(response);
995
1040
  });
996
1041
  app.get("/api/works/:workId/foreshadows", (request, response) => {
@@ -1005,42 +1050,34 @@ export function createRuntime(options) {
1005
1050
  });
1006
1051
  app.post("/api/works/:workId/foreshadows", (request, response) => {
1007
1052
  const foreshadow = store.createForeshadow(request.params.workId, parse(foreshadowSchema, request.body));
1008
- publishCollaborativeChange(request.params.workId, modulePageKey("outlines"));
1009
1053
  data(response, foreshadow, 201);
1010
1054
  });
1011
1055
  app.get("/api/foreshadows/:foreshadowId", (request, response) => data(response, store.getForeshadow(request.params.foreshadowId)));
1012
1056
  app.patch("/api/foreshadows/:foreshadowId", (request, response) => {
1013
1057
  const { changeNote, expectedVersionNo, ...input } = parse(foreshadowSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1014
1058
  const foreshadow = store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote, expectedVersionNo);
1015
- publishCollaborativeChange(String(foreshadow.workId), modulePageKey("outlines"));
1016
1059
  data(response, foreshadow);
1017
1060
  });
1018
1061
  app.delete("/api/foreshadows/:foreshadowId", (request, response) => {
1019
1062
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1020
- const foreshadow = store.getForeshadow(request.params.foreshadowId);
1021
1063
  store.deleteForeshadow(request.params.foreshadowId, input.expectedVersionNo);
1022
- publishCollaborativeChange(String(foreshadow.workId), modulePageKey("outlines"));
1023
1064
  noContent(response);
1024
1065
  });
1025
1066
  app.post("/api/foreshadows/:foreshadowId/occurrences", (request, response) => {
1026
1067
  const input = parse(foreshadowOccurrenceSchema.extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1027
1068
  const { expectedVersionNo, ...occurrenceInput } = input;
1028
1069
  const occurrence = store.createForeshadowOccurrence(request.params.foreshadowId, occurrenceInput, expectedVersionNo);
1029
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1030
1070
  data(response, occurrence, 201);
1031
1071
  });
1032
1072
  app.patch("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
1033
1073
  const input = parse(foreshadowOccurrenceSchema.partial().extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1034
1074
  const { expectedVersionNo, ...occurrenceInput } = input;
1035
1075
  const occurrence = store.updateForeshadowOccurrence(request.params.occurrenceId, occurrenceInput, expectedVersionNo);
1036
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1037
1076
  data(response, occurrence);
1038
1077
  });
1039
1078
  app.delete("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
1040
1079
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1041
- const occurrence = store.getForeshadowOccurrence(request.params.occurrenceId);
1042
1080
  store.deleteForeshadowOccurrence(request.params.occurrenceId, input.expectedVersionNo);
1043
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1044
1081
  noContent(response);
1045
1082
  });
1046
1083
  app.get("/api/works/:workId/settings", (request, response) => {
@@ -1050,7 +1087,6 @@ export function createRuntime(options) {
1050
1087
  });
1051
1088
  app.post("/api/works/:workId/settings", (request, response) => {
1052
1089
  const setting = store.createSetting(request.params.workId, parse(settingSchema, request.body));
1053
- publishCollaborativeChange(request.params.workId, modulePageKey("settings"));
1054
1090
  data(response, setting, 201);
1055
1091
  });
1056
1092
  app.get("/api/works/:workId/settings/context", (request, response) => data(response, store.listSettings(request.params.workId, true)));
@@ -1058,14 +1094,11 @@ export function createRuntime(options) {
1058
1094
  app.patch("/api/settings/:settingId", (request, response) => {
1059
1095
  const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1060
1096
  const setting = store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo);
1061
- publishCollaborativeChange(String(setting.workId), entityEditorPageKey("setting", String(setting.id)));
1062
1097
  data(response, setting);
1063
1098
  });
1064
1099
  app.delete("/api/settings/:settingId", (request, response) => {
1065
1100
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1066
- const setting = store.getSetting(request.params.settingId);
1067
1101
  store.deleteSetting(request.params.settingId, input.expectedVersionNo);
1068
- publishCollaborativeChange(String(setting.workId), entityEditorPageKey("setting", String(setting.id)));
1069
1102
  noContent(response);
1070
1103
  });
1071
1104
  app.get("/api/works/:workId/characters", (request, response) => {
@@ -1083,7 +1116,6 @@ export function createRuntime(options) {
1083
1116
  });
1084
1117
  app.post("/api/works/:workId/characters", (request, response) => {
1085
1118
  const character = store.createCharacter(request.params.workId, parse(characterSchema, request.body));
1086
- publishCollaborativeChange(request.params.workId, modulePageKey("characters"));
1087
1119
  data(response, redactCharacterLinks(character, requestPermissions(request, request.params.workId)), 201);
1088
1120
  });
1089
1121
  app.get("/api/characters/:characterId", (request, response) => {
@@ -1092,7 +1124,6 @@ export function createRuntime(options) {
1092
1124
  app.patch("/api/characters/:characterId", (request, response) => {
1093
1125
  const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
1094
1126
  const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
1095
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1096
1127
  data(response, redactCharacterLinks(character, requestPermissions(request)));
1097
1128
  });
1098
1129
  app.get("/api/characters/:characterId/versions", (request, response) => {
@@ -1104,14 +1135,11 @@ export function createRuntime(options) {
1104
1135
  app.post("/api/characters/:characterId/restore", (request, response) => {
1105
1136
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1106
1137
  const character = store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo);
1107
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1108
1138
  data(response, redactCharacterLinks(character, requestPermissions(request)));
1109
1139
  });
1110
1140
  app.delete("/api/characters/:characterId", (request, response) => {
1111
1141
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1112
- const character = store.getCharacter(request.params.characterId);
1113
1142
  store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
1114
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1115
1143
  noContent(response);
1116
1144
  });
1117
1145
  app.post("/api/characters/:characterId/merge", (request, response) => {
@@ -1136,7 +1164,6 @@ export function createRuntime(options) {
1136
1164
  });
1137
1165
  app.post("/api/characters/:characterId/sections", (request, response) => {
1138
1166
  const section = store.createCharacterProfileSection(request.params.characterId, parse(characterProfileSectionSchema, request.body));
1139
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1140
1167
  data(response, section, 201);
1141
1168
  });
1142
1169
  app.get("/api/character-sections/:sectionId", (request, response) => {
@@ -1145,14 +1172,11 @@ export function createRuntime(options) {
1145
1172
  app.patch("/api/character-sections/:sectionId", (request, response) => {
1146
1173
  const { changeNote, expectedVersionNo, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1147
1174
  const section = store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo);
1148
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1149
1175
  data(response, section);
1150
1176
  });
1151
1177
  app.delete("/api/character-sections/:sectionId", (request, response) => {
1152
1178
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1153
- const section = store.getCharacterProfileSection(request.params.sectionId);
1154
1179
  store.deleteCharacterProfileSection(request.params.sectionId, input.expectedVersionNo);
1155
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1156
1180
  noContent(response);
1157
1181
  });
1158
1182
  app.get("/api/character-sections/:sectionId/versions", (request, response) => {
@@ -1164,7 +1188,6 @@ export function createRuntime(options) {
1164
1188
  app.post("/api/character-sections/:sectionId/restore", (request, response) => {
1165
1189
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1166
1190
  const section = store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo, input.expectedVersionNo);
1167
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1168
1191
  data(response, section);
1169
1192
  });
1170
1193
  app.get("/api/works/:workId/attachments", (request, response) => {
@@ -1221,7 +1244,6 @@ export function createRuntime(options) {
1221
1244
  });
1222
1245
  app.post("/api/works/:workId/races", (request, response) => {
1223
1246
  const race = store.createRace(request.params.workId, parse(raceSchema, request.body));
1224
- publishCollaborativeChange(request.params.workId, modulePageKey("races"));
1225
1247
  data(response, redactRaceMembers(race, requestPermissions(request, request.params.workId)), 201);
1226
1248
  });
1227
1249
  app.get("/api/races/:raceId", (request, response) => {
@@ -1230,14 +1252,11 @@ export function createRuntime(options) {
1230
1252
  app.patch("/api/races/:raceId", (request, response) => {
1231
1253
  const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1232
1254
  const race = store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo);
1233
- publishCollaborativeChange(String(race.workId), entityEditorPageKey("race", String(race.id)));
1234
1255
  data(response, redactRaceMembers(race, requestPermissions(request)));
1235
1256
  });
1236
1257
  app.delete("/api/races/:raceId", (request, response) => {
1237
1258
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1238
- const race = store.getRace(request.params.raceId);
1239
1259
  store.deleteRace(request.params.raceId, input.expectedVersionNo);
1240
- publishCollaborativeChange(String(race.workId), entityEditorPageKey("race", String(race.id)));
1241
1260
  noContent(response);
1242
1261
  });
1243
1262
  app.post("/api/races/:raceId/merge", (request, response) => {
@@ -1255,7 +1274,6 @@ export function createRuntime(options) {
1255
1274
  });
1256
1275
  app.post("/api/works/:workId/organizations", (request, response) => {
1257
1276
  const organization = store.createOrganization(request.params.workId, parse(organizationSchema, request.body));
1258
- publishCollaborativeChange(request.params.workId, modulePageKey("organizations"));
1259
1277
  data(response, redactOrganizationMembers(organization, requestPermissions(request, request.params.workId)), 201);
1260
1278
  });
1261
1279
  app.get("/api/organizations/:organizationId", (request, response) => {
@@ -1264,14 +1282,11 @@ export function createRuntime(options) {
1264
1282
  app.patch("/api/organizations/:organizationId", (request, response) => {
1265
1283
  const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1266
1284
  const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
1267
- publishCollaborativeChange(String(organization.workId), entityEditorPageKey("organization", String(organization.id)));
1268
1285
  data(response, redactOrganizationMembers(organization, requestPermissions(request)));
1269
1286
  });
1270
1287
  app.delete("/api/organizations/:organizationId", (request, response) => {
1271
1288
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1272
- const organization = store.getOrganization(request.params.organizationId);
1273
1289
  store.deleteOrganization(request.params.organizationId, input.expectedVersionNo);
1274
- publishCollaborativeChange(String(organization.workId), entityEditorPageKey("organization", String(organization.id)));
1275
1290
  noContent(response);
1276
1291
  });
1277
1292
  app.post("/api/organizations/:organizationId/merge", (request, response) => {
@@ -1286,21 +1301,17 @@ export function createRuntime(options) {
1286
1301
  });
1287
1302
  app.post("/api/works/:workId/timeline-tracks", (request, response) => {
1288
1303
  const track = store.createTimelineTrack(request.params.workId, parse(timelineTrackSchema, request.body));
1289
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1290
1304
  data(response, track, 201);
1291
1305
  });
1292
1306
  app.get("/api/timeline-tracks/:trackId", (request, response) => data(response, store.getTimelineTrack(request.params.trackId)));
1293
1307
  app.patch("/api/timeline-tracks/:trackId", (request, response) => {
1294
1308
  const { changeNote, expectedVersionNo, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1295
1309
  const track = store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo);
1296
- publishCollaborativeChange(String(track.workId), modulePageKey("timeline"));
1297
1310
  data(response, track);
1298
1311
  });
1299
1312
  app.delete("/api/timeline-tracks/:trackId", (request, response) => {
1300
1313
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1301
- const track = store.getTimelineTrack(request.params.trackId);
1302
1314
  store.deleteTimelineTrack(request.params.trackId, input.expectedVersionNo);
1303
- publishCollaborativeChange(String(track.workId), modulePageKey("timeline"));
1304
1315
  noContent(response);
1305
1316
  });
1306
1317
  app.get("/api/works/:workId/timeline", (request, response) => {
@@ -1309,7 +1320,6 @@ export function createRuntime(options) {
1309
1320
  });
1310
1321
  app.post("/api/works/:workId/timeline", (request, response) => {
1311
1322
  const event = store.createTimelineEvent(request.params.workId, parse(timelineSchema, request.body));
1312
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1313
1323
  data(response, event, 201);
1314
1324
  });
1315
1325
  app.post("/api/works/:workId/timeline/merge", (request, response) => {
@@ -1323,14 +1333,12 @@ export function createRuntime(options) {
1323
1333
  }).strict(), request.body);
1324
1334
  const { expectedVersionNos, ...mergeInput } = input;
1325
1335
  const merged = store.mergeTimelineEvents(request.params.workId, input.eventIds, mergeInput, expectedVersionNos);
1326
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1327
1336
  data(response, merged, 201);
1328
1337
  });
1329
1338
  app.get("/api/timeline/:eventId", (request, response) => data(response, store.getTimelineEvent(request.params.eventId)));
1330
1339
  app.patch("/api/timeline/:eventId", (request, response) => {
1331
1340
  const { changeNote, expectedVersionNo, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1332
1341
  const event = store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo);
1333
- publishCollaborativeChange(String(event.workId), modulePageKey("timeline"));
1334
1342
  data(response, event);
1335
1343
  });
1336
1344
  app.post("/api/timeline/:eventId/split", (request, response) => {
@@ -1344,16 +1352,11 @@ export function createRuntime(options) {
1344
1352
  expectedVersionNo: expectedVersionNoSchema
1345
1353
  }).strict(), request.body);
1346
1354
  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
1355
  data(response, split, 201);
1351
1356
  });
1352
1357
  app.delete("/api/timeline/:eventId", (request, response) => {
1353
1358
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1354
- const event = store.getTimelineEvent(request.params.eventId);
1355
1359
  store.deleteTimelineEvent(request.params.eventId, input.expectedVersionNo);
1356
- publishCollaborativeChange(String(event.workId), modulePageKey("timeline"));
1357
1360
  noContent(response);
1358
1361
  });
1359
1362
  app.get("/api/works/:workId/relationships", (request, response) => {
@@ -1367,21 +1370,20 @@ export function createRuntime(options) {
1367
1370
  });
1368
1371
  app.post("/api/works/:workId/relationships", (request, response) => {
1369
1372
  const relationship = store.createRelationship(request.params.workId, parse(relationshipSchema, request.body));
1370
- publishCollaborativeChange(request.params.workId, modulePageKey("relationships"));
1371
1373
  data(response, relationship, 201);
1372
1374
  });
1373
1375
  app.get("/api/relationships/:relationshipId", (request, response) => data(response, store.getRelationship(request.params.relationshipId)));
1374
1376
  app.patch("/api/relationships/:relationshipId", (request, response) => {
1375
1377
  const { changeNote, expectedVersionNo, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1376
1378
  const relationship = store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo);
1377
- publishCollaborativeChange(String(relationship.workId), modulePageKey("relationships"));
1379
+ publishRelationshipChange(String(relationship.workId), String(relationship.id));
1378
1380
  data(response, relationship);
1379
1381
  });
1380
1382
  app.delete("/api/relationships/:relationshipId", (request, response) => {
1381
1383
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1382
1384
  const relationship = store.getRelationship(request.params.relationshipId);
1383
1385
  store.deleteRelationship(request.params.relationshipId, input.expectedVersionNo);
1384
- publishCollaborativeChange(String(relationship.workId), modulePageKey("relationships"));
1386
+ publishRelationshipChange(String(relationship.workId), String(relationship.id));
1385
1387
  noContent(response);
1386
1388
  });
1387
1389
  app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
@@ -1434,6 +1436,20 @@ export function createRuntime(options) {
1434
1436
  const permissions = requestPermissions(request, request.params.workId);
1435
1437
  data(response, mapRecords(store.listTaskSummariesPage(request.params.workId, parsePagination(request.query) ?? { page: 1, limit: 30, offset: 0 }), (task) => redactTaskCharacterNames(task, permissions)));
1436
1438
  });
1439
+ app.post("/api/works/:workId/tasks/relationship-source-preview", async (request, response) => {
1440
+ const input = parse(z.object({
1441
+ scope: relationshipAnalysisScopeSchema,
1442
+ modelId: identifier.optional()
1443
+ }).strict(), request.body);
1444
+ const permissions = requestPermissions(request, request.params.workId);
1445
+ const deniedModules = relationshipAnalysisReadModules(input.scope).filter((module) => permissions[module] === "none");
1446
+ if (deniedModules.length > 0) {
1447
+ throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取人物关系来源预检所需资料模块的权限", {
1448
+ modules: deniedModules
1449
+ });
1450
+ }
1451
+ data(response, await ai.previewRelationshipSources(request.params.workId, input.scope, input.modelId));
1452
+ });
1437
1453
  app.post("/api/works/:workId/tasks", (request, response) => {
1438
1454
  const input = parse(analysisTaskSchema, request.body);
1439
1455
  data(response, redactTaskCharacterNames(ai.createTask(request.params.workId, input), requestPermissions(request, request.params.workId)), 201);
@@ -1485,7 +1501,30 @@ export function createRuntime(options) {
1485
1501
  allowAdminAccess: request.authMethod !== "api-key"
1486
1502
  } : undefined), requestPermissions(request)));
1487
1503
  });
1504
+ app.post("/api/tasks/:taskId/rerun", (request, response) => {
1505
+ parse(z.object({}).strict(), request.body ?? {});
1506
+ const task = store.getTask(request.params.taskId);
1507
+ if (task.taskType === "relationship-analysis") {
1508
+ const permissions = requestPermissions(request, String(task.workId));
1509
+ const deniedModules = relationshipAnalysisReadModules(task.scope).filter((module) => permissions[module] === "none");
1510
+ if (deniedModules.length > 0) {
1511
+ throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取本次定向人物关系分析所需资料模块的权限", {
1512
+ modules: deniedModules
1513
+ });
1514
+ }
1515
+ }
1516
+ data(response, redactTaskCharacterNames(ai.rerunTask(request.params.taskId), requestPermissions(request)), 201);
1517
+ });
1488
1518
  app.post("/api/tasks/:taskId/cancel", (request, response) => data(response, redactTaskCharacterNames(ai.cancelTask(request.params.taskId), requestPermissions(request))));
1519
+ app.post("/api/tasks/:taskId/relationship-changes/apply", (request, response) => {
1520
+ parse(z.object({}).strict(), request.body ?? {});
1521
+ const applied = ai.applyRelationshipChangePreview(request.params.taskId);
1522
+ data(response, redactTaskCharacterNames(applied, requestPermissions(request)));
1523
+ });
1524
+ app.post("/api/tasks/:taskId/relationship-changes/discard", (request, response) => {
1525
+ parse(z.object({}).strict(), request.body ?? {});
1526
+ data(response, redactTaskCharacterNames(ai.discardRelationshipChangePreview(request.params.taskId), requestPermissions(request)));
1527
+ });
1489
1528
  app.get("/api/platform/ai/providers", (request, response) => {
1490
1529
  const pagination = parsePagination(request.query);
1491
1530
  data(response, pagination ? ai.listProvidersPage(pagination) : ai.listProviders());
@@ -1497,12 +1536,39 @@ export function createRuntime(options) {
1497
1536
  });
1498
1537
  app.get("/api/platform/ai/settings", (_request, response) => data(response, store.getPlatformAiSettings()));
1499
1538
  app.patch("/api/platform/ai/settings", (request, response) => data(response, store.updatePlatformAiSettings(parse(aiPromptSchema, request.body))));
1539
+ app.get("/api/platform/ai/usage", (request, response) => {
1540
+ const query = parse(aiUsageQuerySchema, request.query);
1541
+ data(response, ai.getPlatformTokenUsage(query.timezoneOffset));
1542
+ });
1500
1543
  app.get("/api/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
1501
1544
  app.get("/api/platform/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
1502
1545
  app.patch("/api/platform/ui-settings", (request, response) => {
1503
1546
  data(response, store.updatePlatformUiSettings(parse(platformUiSettingsSchema, request.body)));
1504
1547
  });
1505
1548
  app.get("/api/works/:workId/ai-settings", (request, response) => data(response, store.getWorkAiSettings(request.params.workId)));
1549
+ app.get("/api/works/:workId/ai-settings/usage", (request, response) => {
1550
+ const query = parse(aiUsageQuerySchema, request.query);
1551
+ data(response, ai.getWorkTokenUsage(request.params.workId, query.timezoneOffset));
1552
+ });
1553
+ app.get("/api/works/:workId/ai-settings/relationship-search-index", (request, response) => {
1554
+ data(response, ai.getRelationshipSearchIndexStatus(request.params.workId));
1555
+ });
1556
+ app.post("/api/works/:workId/ai-settings/relationship-search-index/sync", (request, response) => {
1557
+ const workId = request.params.workId;
1558
+ const result = ai.syncRelationshipSearchIndex(workId);
1559
+ store.audit(workId, "relationship.search-index.incremental-sync-queued", "work-ai-settings", workId, {
1560
+ queuedSourceCount: result.queuedSourceCount
1561
+ });
1562
+ data(response, result, 202);
1563
+ });
1564
+ app.post("/api/works/:workId/ai-settings/relationship-search-index/rebuild", (request, response) => {
1565
+ const workId = request.params.workId;
1566
+ const result = ai.rebuildRelationshipSearchIndex(workId);
1567
+ store.audit(workId, "relationship.search-index.rebuild-queued", "work-ai-settings", workId, {
1568
+ queuedSourceCount: result.queuedSourceCount
1569
+ });
1570
+ data(response, result, 202);
1571
+ });
1506
1572
  app.patch("/api/works/:workId/ai-settings", (request, response) => {
1507
1573
  const workId = request.params.workId;
1508
1574
  const before = store.getWorkAiSettings(workId);