@musnows/scriverse 0.5.3 → 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();
@@ -274,6 +274,7 @@ const providerSchema = z.object({
274
274
  name: nonEmpty.max(200),
275
275
  baseUrl: z.string().url().refine((value) => value.startsWith("http://") || value.startsWith("https://"), "接口地址必须使用 HTTP 或 HTTPS"),
276
276
  apiKey: nonEmpty.max(10_000),
277
+ protocol: z.enum(["openai-chat-completions", "anthropic-messages"]).optional(),
277
278
  status: z.enum(["enabled", "disabled"]).optional(),
278
279
  note: z.string().max(10_000).optional(),
279
280
  concurrencyLimit: z.number().int().min(1).max(100).optional(),
@@ -295,8 +296,18 @@ const modelSchema = z.object({
295
296
  const aiPromptSchema = z.object({
296
297
  systemPrompt: z.string().max(100_000).optional()
297
298
  });
299
+ const aiUsageQuerySchema = z.object({
300
+ timezoneOffset: z.coerce.number().int().min(-840).max(840).default(0)
301
+ }).strict();
298
302
  const platformPageSizesSchema = z.object({
303
+ settings: z.number().int().min(10).max(100).optional(),
299
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(),
300
311
  analysisTasks: z.number().int().min(10).max(100).optional(),
301
312
  fileVersions: z.number().int().min(10).max(100).optional()
302
313
  }).strict();
@@ -357,12 +368,20 @@ const contextSchema = z.object({
357
368
  includeBookSummary: z.boolean().optional()
358
369
  });
359
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();
360
376
  const relationshipAnalysisScopeSchema = z.object({
361
377
  type: z.enum(["chapter", "book", "settings"]),
362
378
  chapterId: identifier.optional(),
363
379
  includeAllSettings: z.boolean().optional(),
364
380
  additionalPrompt: z.string().trim().max(10_000).optional(),
365
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(),
366
385
  replaceExistingRelationships: z.boolean().optional()
367
386
  }).strict().superRefine((scope, context) => {
368
387
  if (scope.type === "chapter" && !scope.chapterId) {
@@ -377,6 +396,18 @@ const relationshipAnalysisScopeSchema = z.object({
377
396
  if (scope.replaceExistingRelationships && !scope.characterIds?.length) {
378
397
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["replaceExistingRelationships"], message: "覆盖已有关系前必须选择被分析角色" });
379
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
+ }
380
411
  });
381
412
  const analysisTaskSchema = z.union([
382
413
  z.object({ taskType: z.literal("relationship-analysis"), scope: relationshipAnalysisScopeSchema.optional(), modelId: identifier.optional() }).strict(),
@@ -387,6 +418,15 @@ const analysisTaskSchema = z.union([
387
418
  if (input.scope?.additionalPrompt !== undefined) {
388
419
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "additionalPrompt"], message: "额外分析提示仅支持人物关系分析" });
389
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
+ }
390
430
  if (input.scope?.characterIds !== undefined || input.scope?.replaceExistingRelationships !== undefined) {
391
431
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["scope", "characterIds"], message: "被分析角色仅支持人物关系分析" });
392
432
  }
@@ -491,6 +531,11 @@ function redactTaskCharacterNames(record, permissions) {
491
531
  return redactedRelationship;
492
532
  });
493
533
  }
534
+ const changePreview = recordValue(taskResult.relationshipChangePreview);
535
+ if (changePreview) {
536
+ const { operations: _operations, ...redactedChangePreview } = changePreview;
537
+ redactedTaskResult.relationshipChangePreview = redactedChangePreview;
538
+ }
494
539
  const analysisTarget = recordValue(taskResult.analysisTarget);
495
540
  if (analysisTarget) {
496
541
  const { characterNames: _characterNames, ...redactedAnalysisTarget } = analysisTarget;
@@ -499,6 +544,14 @@ function redactTaskCharacterNames(record, permissions) {
499
544
  result.result = redactedTaskResult;
500
545
  }
501
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
+ }
502
555
  const resultSummary = recordValue(result.resultSummary);
503
556
  const characterSensitiveTask = ["character-extraction", "character-summary", "character-identity-audit", "relationship-analysis"]
504
557
  .includes(String(result.taskType));
@@ -548,14 +601,14 @@ export function createRuntime(options) {
548
601
  mkdirSync(attachmentStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
549
602
  const auth = new UserAuthService(database);
550
603
  const collaborationPresence = new CollaborationPresence();
551
- const publishCollaborativeChange = (workId, pageKey, label = pageLabelForKey(pageKey)) => {
604
+ const publishRelationshipChange = (workId, relationshipId) => {
552
605
  const actor = currentRequestActor();
553
- if (!actor || !workId || !pageKey)
606
+ if (!actor || !workId || !relationshipId)
554
607
  return;
555
- collaborationPresence.publishChange(workId, pageKey, {
608
+ collaborationPresence.publishChange(workId, entityEditorPageKey("relationship", relationshipId), {
556
609
  userId: actor.userId,
557
610
  displayName: actor.displayName
558
- }, label);
611
+ });
559
612
  };
560
613
  const getDevelopmentUser = () => options.devAuthBypass
561
614
  ? auth.listUsers().find((user) => user.status === "active") ?? null
@@ -618,6 +671,7 @@ export function createRuntime(options) {
618
671
  status: "ok",
619
672
  version: APP_VERSION,
620
673
  protocol: "openai-chat-completions",
674
+ protocols: ["openai-chat-completions", "anthropic-messages"],
621
675
  development: options.developmentServer === true
622
676
  });
623
677
  });
@@ -944,14 +998,11 @@ export function createRuntime(options) {
944
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);
945
999
  const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
946
1000
  const chapter = store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo);
947
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
948
1001
  data(response, chapter);
949
1002
  });
950
1003
  app.delete("/api/chapters/:chapterId", (request, response) => {
951
1004
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
952
- const chapter = store.getChapter(request.params.chapterId);
953
1005
  store.deleteChapter(request.params.chapterId, input.expectedVersionNo);
954
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
955
1006
  noContent(response);
956
1007
  });
957
1008
  app.get("/api/chapters/:chapterId/versions", (request, response) => {
@@ -965,7 +1016,6 @@ export function createRuntime(options) {
965
1016
  app.post("/api/chapters/:chapterId/restore", (request, response) => {
966
1017
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
967
1018
  const chapter = store.restoreChapter(request.params.chapterId, input.versionNo, input.expectedVersionNo);
968
- publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
969
1019
  data(response, chapter);
970
1020
  });
971
1021
  app.post("/api/chapters/:chapterId/move", (request, response) => {
@@ -981,14 +1031,11 @@ export function createRuntime(options) {
981
1031
  app.put("/api/chapters/:chapterId/outline", (request, response) => {
982
1032
  const { changeNote, expectedVersionNo, ...input } = parse(chapterOutlineSchema.extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
983
1033
  const outline = store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote, expectedVersionNo);
984
- publishCollaborativeChange(String(outline.workId), modulePageKey("outlines"));
985
1034
  data(response, outline);
986
1035
  });
987
1036
  app.delete("/api/chapters/:chapterId/outline", (request, response) => {
988
1037
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
989
- const chapter = store.getChapter(request.params.chapterId);
990
1038
  store.deleteChapterOutline(request.params.chapterId, input.expectedVersionNo);
991
- publishCollaborativeChange(String(chapter.workId), modulePageKey("outlines"));
992
1039
  noContent(response);
993
1040
  });
994
1041
  app.get("/api/works/:workId/foreshadows", (request, response) => {
@@ -1003,42 +1050,34 @@ export function createRuntime(options) {
1003
1050
  });
1004
1051
  app.post("/api/works/:workId/foreshadows", (request, response) => {
1005
1052
  const foreshadow = store.createForeshadow(request.params.workId, parse(foreshadowSchema, request.body));
1006
- publishCollaborativeChange(request.params.workId, modulePageKey("outlines"));
1007
1053
  data(response, foreshadow, 201);
1008
1054
  });
1009
1055
  app.get("/api/foreshadows/:foreshadowId", (request, response) => data(response, store.getForeshadow(request.params.foreshadowId)));
1010
1056
  app.patch("/api/foreshadows/:foreshadowId", (request, response) => {
1011
1057
  const { changeNote, expectedVersionNo, ...input } = parse(foreshadowSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1012
1058
  const foreshadow = store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote, expectedVersionNo);
1013
- publishCollaborativeChange(String(foreshadow.workId), modulePageKey("outlines"));
1014
1059
  data(response, foreshadow);
1015
1060
  });
1016
1061
  app.delete("/api/foreshadows/:foreshadowId", (request, response) => {
1017
1062
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1018
- const foreshadow = store.getForeshadow(request.params.foreshadowId);
1019
1063
  store.deleteForeshadow(request.params.foreshadowId, input.expectedVersionNo);
1020
- publishCollaborativeChange(String(foreshadow.workId), modulePageKey("outlines"));
1021
1064
  noContent(response);
1022
1065
  });
1023
1066
  app.post("/api/foreshadows/:foreshadowId/occurrences", (request, response) => {
1024
1067
  const input = parse(foreshadowOccurrenceSchema.extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1025
1068
  const { expectedVersionNo, ...occurrenceInput } = input;
1026
1069
  const occurrence = store.createForeshadowOccurrence(request.params.foreshadowId, occurrenceInput, expectedVersionNo);
1027
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1028
1070
  data(response, occurrence, 201);
1029
1071
  });
1030
1072
  app.patch("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
1031
1073
  const input = parse(foreshadowOccurrenceSchema.partial().extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1032
1074
  const { expectedVersionNo, ...occurrenceInput } = input;
1033
1075
  const occurrence = store.updateForeshadowOccurrence(request.params.occurrenceId, occurrenceInput, expectedVersionNo);
1034
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1035
1076
  data(response, occurrence);
1036
1077
  });
1037
1078
  app.delete("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
1038
1079
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1039
- const occurrence = store.getForeshadowOccurrence(request.params.occurrenceId);
1040
1080
  store.deleteForeshadowOccurrence(request.params.occurrenceId, input.expectedVersionNo);
1041
- publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
1042
1081
  noContent(response);
1043
1082
  });
1044
1083
  app.get("/api/works/:workId/settings", (request, response) => {
@@ -1048,7 +1087,6 @@ export function createRuntime(options) {
1048
1087
  });
1049
1088
  app.post("/api/works/:workId/settings", (request, response) => {
1050
1089
  const setting = store.createSetting(request.params.workId, parse(settingSchema, request.body));
1051
- publishCollaborativeChange(request.params.workId, modulePageKey("settings"));
1052
1090
  data(response, setting, 201);
1053
1091
  });
1054
1092
  app.get("/api/works/:workId/settings/context", (request, response) => data(response, store.listSettings(request.params.workId, true)));
@@ -1056,14 +1094,11 @@ export function createRuntime(options) {
1056
1094
  app.patch("/api/settings/:settingId", (request, response) => {
1057
1095
  const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1058
1096
  const setting = store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo);
1059
- publishCollaborativeChange(String(setting.workId), entityEditorPageKey("setting", String(setting.id)));
1060
1097
  data(response, setting);
1061
1098
  });
1062
1099
  app.delete("/api/settings/:settingId", (request, response) => {
1063
1100
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1064
- const setting = store.getSetting(request.params.settingId);
1065
1101
  store.deleteSetting(request.params.settingId, input.expectedVersionNo);
1066
- publishCollaborativeChange(String(setting.workId), entityEditorPageKey("setting", String(setting.id)));
1067
1102
  noContent(response);
1068
1103
  });
1069
1104
  app.get("/api/works/:workId/characters", (request, response) => {
@@ -1081,7 +1116,6 @@ export function createRuntime(options) {
1081
1116
  });
1082
1117
  app.post("/api/works/:workId/characters", (request, response) => {
1083
1118
  const character = store.createCharacter(request.params.workId, parse(characterSchema, request.body));
1084
- publishCollaborativeChange(request.params.workId, modulePageKey("characters"));
1085
1119
  data(response, redactCharacterLinks(character, requestPermissions(request, request.params.workId)), 201);
1086
1120
  });
1087
1121
  app.get("/api/characters/:characterId", (request, response) => {
@@ -1090,7 +1124,6 @@ export function createRuntime(options) {
1090
1124
  app.patch("/api/characters/:characterId", (request, response) => {
1091
1125
  const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
1092
1126
  const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
1093
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1094
1127
  data(response, redactCharacterLinks(character, requestPermissions(request)));
1095
1128
  });
1096
1129
  app.get("/api/characters/:characterId/versions", (request, response) => {
@@ -1102,14 +1135,11 @@ export function createRuntime(options) {
1102
1135
  app.post("/api/characters/:characterId/restore", (request, response) => {
1103
1136
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1104
1137
  const character = store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo);
1105
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1106
1138
  data(response, redactCharacterLinks(character, requestPermissions(request)));
1107
1139
  });
1108
1140
  app.delete("/api/characters/:characterId", (request, response) => {
1109
1141
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1110
- const character = store.getCharacter(request.params.characterId);
1111
1142
  store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
1112
- publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
1113
1143
  noContent(response);
1114
1144
  });
1115
1145
  app.post("/api/characters/:characterId/merge", (request, response) => {
@@ -1134,7 +1164,6 @@ export function createRuntime(options) {
1134
1164
  });
1135
1165
  app.post("/api/characters/:characterId/sections", (request, response) => {
1136
1166
  const section = store.createCharacterProfileSection(request.params.characterId, parse(characterProfileSectionSchema, request.body));
1137
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1138
1167
  data(response, section, 201);
1139
1168
  });
1140
1169
  app.get("/api/character-sections/:sectionId", (request, response) => {
@@ -1143,14 +1172,11 @@ export function createRuntime(options) {
1143
1172
  app.patch("/api/character-sections/:sectionId", (request, response) => {
1144
1173
  const { changeNote, expectedVersionNo, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1145
1174
  const section = store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo);
1146
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1147
1175
  data(response, section);
1148
1176
  });
1149
1177
  app.delete("/api/character-sections/:sectionId", (request, response) => {
1150
1178
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1151
- const section = store.getCharacterProfileSection(request.params.sectionId);
1152
1179
  store.deleteCharacterProfileSection(request.params.sectionId, input.expectedVersionNo);
1153
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1154
1180
  noContent(response);
1155
1181
  });
1156
1182
  app.get("/api/character-sections/:sectionId/versions", (request, response) => {
@@ -1162,7 +1188,6 @@ export function createRuntime(options) {
1162
1188
  app.post("/api/character-sections/:sectionId/restore", (request, response) => {
1163
1189
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1164
1190
  const section = store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo, input.expectedVersionNo);
1165
- publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1166
1191
  data(response, section);
1167
1192
  });
1168
1193
  app.get("/api/works/:workId/attachments", (request, response) => {
@@ -1219,7 +1244,6 @@ export function createRuntime(options) {
1219
1244
  });
1220
1245
  app.post("/api/works/:workId/races", (request, response) => {
1221
1246
  const race = store.createRace(request.params.workId, parse(raceSchema, request.body));
1222
- publishCollaborativeChange(request.params.workId, modulePageKey("races"));
1223
1247
  data(response, redactRaceMembers(race, requestPermissions(request, request.params.workId)), 201);
1224
1248
  });
1225
1249
  app.get("/api/races/:raceId", (request, response) => {
@@ -1228,14 +1252,11 @@ export function createRuntime(options) {
1228
1252
  app.patch("/api/races/:raceId", (request, response) => {
1229
1253
  const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1230
1254
  const race = store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo);
1231
- publishCollaborativeChange(String(race.workId), entityEditorPageKey("race", String(race.id)));
1232
1255
  data(response, redactRaceMembers(race, requestPermissions(request)));
1233
1256
  });
1234
1257
  app.delete("/api/races/:raceId", (request, response) => {
1235
1258
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1236
- const race = store.getRace(request.params.raceId);
1237
1259
  store.deleteRace(request.params.raceId, input.expectedVersionNo);
1238
- publishCollaborativeChange(String(race.workId), entityEditorPageKey("race", String(race.id)));
1239
1260
  noContent(response);
1240
1261
  });
1241
1262
  app.post("/api/races/:raceId/merge", (request, response) => {
@@ -1253,7 +1274,6 @@ export function createRuntime(options) {
1253
1274
  });
1254
1275
  app.post("/api/works/:workId/organizations", (request, response) => {
1255
1276
  const organization = store.createOrganization(request.params.workId, parse(organizationSchema, request.body));
1256
- publishCollaborativeChange(request.params.workId, modulePageKey("organizations"));
1257
1277
  data(response, redactOrganizationMembers(organization, requestPermissions(request, request.params.workId)), 201);
1258
1278
  });
1259
1279
  app.get("/api/organizations/:organizationId", (request, response) => {
@@ -1262,14 +1282,11 @@ export function createRuntime(options) {
1262
1282
  app.patch("/api/organizations/:organizationId", (request, response) => {
1263
1283
  const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1264
1284
  const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
1265
- publishCollaborativeChange(String(organization.workId), entityEditorPageKey("organization", String(organization.id)));
1266
1285
  data(response, redactOrganizationMembers(organization, requestPermissions(request)));
1267
1286
  });
1268
1287
  app.delete("/api/organizations/:organizationId", (request, response) => {
1269
1288
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1270
- const organization = store.getOrganization(request.params.organizationId);
1271
1289
  store.deleteOrganization(request.params.organizationId, input.expectedVersionNo);
1272
- publishCollaborativeChange(String(organization.workId), entityEditorPageKey("organization", String(organization.id)));
1273
1290
  noContent(response);
1274
1291
  });
1275
1292
  app.post("/api/organizations/:organizationId/merge", (request, response) => {
@@ -1284,21 +1301,17 @@ export function createRuntime(options) {
1284
1301
  });
1285
1302
  app.post("/api/works/:workId/timeline-tracks", (request, response) => {
1286
1303
  const track = store.createTimelineTrack(request.params.workId, parse(timelineTrackSchema, request.body));
1287
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1288
1304
  data(response, track, 201);
1289
1305
  });
1290
1306
  app.get("/api/timeline-tracks/:trackId", (request, response) => data(response, store.getTimelineTrack(request.params.trackId)));
1291
1307
  app.patch("/api/timeline-tracks/:trackId", (request, response) => {
1292
1308
  const { changeNote, expectedVersionNo, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1293
1309
  const track = store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo);
1294
- publishCollaborativeChange(String(track.workId), modulePageKey("timeline"));
1295
1310
  data(response, track);
1296
1311
  });
1297
1312
  app.delete("/api/timeline-tracks/:trackId", (request, response) => {
1298
1313
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1299
- const track = store.getTimelineTrack(request.params.trackId);
1300
1314
  store.deleteTimelineTrack(request.params.trackId, input.expectedVersionNo);
1301
- publishCollaborativeChange(String(track.workId), modulePageKey("timeline"));
1302
1315
  noContent(response);
1303
1316
  });
1304
1317
  app.get("/api/works/:workId/timeline", (request, response) => {
@@ -1307,7 +1320,6 @@ export function createRuntime(options) {
1307
1320
  });
1308
1321
  app.post("/api/works/:workId/timeline", (request, response) => {
1309
1322
  const event = store.createTimelineEvent(request.params.workId, parse(timelineSchema, request.body));
1310
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1311
1323
  data(response, event, 201);
1312
1324
  });
1313
1325
  app.post("/api/works/:workId/timeline/merge", (request, response) => {
@@ -1321,14 +1333,12 @@ export function createRuntime(options) {
1321
1333
  }).strict(), request.body);
1322
1334
  const { expectedVersionNos, ...mergeInput } = input;
1323
1335
  const merged = store.mergeTimelineEvents(request.params.workId, input.eventIds, mergeInput, expectedVersionNos);
1324
- publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1325
1336
  data(response, merged, 201);
1326
1337
  });
1327
1338
  app.get("/api/timeline/:eventId", (request, response) => data(response, store.getTimelineEvent(request.params.eventId)));
1328
1339
  app.patch("/api/timeline/:eventId", (request, response) => {
1329
1340
  const { changeNote, expectedVersionNo, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1330
1341
  const event = store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo);
1331
- publishCollaborativeChange(String(event.workId), modulePageKey("timeline"));
1332
1342
  data(response, event);
1333
1343
  });
1334
1344
  app.post("/api/timeline/:eventId/split", (request, response) => {
@@ -1342,16 +1352,11 @@ export function createRuntime(options) {
1342
1352
  expectedVersionNo: expectedVersionNoSchema
1343
1353
  }).strict(), request.body);
1344
1354
  const split = store.splitTimelineEvent(request.params.eventId, input.parts, input.expectedVersionNo);
1345
- const first = split[0];
1346
- if (first)
1347
- publishCollaborativeChange(String(first.workId), modulePageKey("timeline"));
1348
1355
  data(response, split, 201);
1349
1356
  });
1350
1357
  app.delete("/api/timeline/:eventId", (request, response) => {
1351
1358
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1352
- const event = store.getTimelineEvent(request.params.eventId);
1353
1359
  store.deleteTimelineEvent(request.params.eventId, input.expectedVersionNo);
1354
- publishCollaborativeChange(String(event.workId), modulePageKey("timeline"));
1355
1360
  noContent(response);
1356
1361
  });
1357
1362
  app.get("/api/works/:workId/relationships", (request, response) => {
@@ -1365,21 +1370,20 @@ export function createRuntime(options) {
1365
1370
  });
1366
1371
  app.post("/api/works/:workId/relationships", (request, response) => {
1367
1372
  const relationship = store.createRelationship(request.params.workId, parse(relationshipSchema, request.body));
1368
- publishCollaborativeChange(request.params.workId, modulePageKey("relationships"));
1369
1373
  data(response, relationship, 201);
1370
1374
  });
1371
1375
  app.get("/api/relationships/:relationshipId", (request, response) => data(response, store.getRelationship(request.params.relationshipId)));
1372
1376
  app.patch("/api/relationships/:relationshipId", (request, response) => {
1373
1377
  const { changeNote, expectedVersionNo, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1374
1378
  const relationship = store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo);
1375
- publishCollaborativeChange(String(relationship.workId), modulePageKey("relationships"));
1379
+ publishRelationshipChange(String(relationship.workId), String(relationship.id));
1376
1380
  data(response, relationship);
1377
1381
  });
1378
1382
  app.delete("/api/relationships/:relationshipId", (request, response) => {
1379
1383
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1380
1384
  const relationship = store.getRelationship(request.params.relationshipId);
1381
1385
  store.deleteRelationship(request.params.relationshipId, input.expectedVersionNo);
1382
- publishCollaborativeChange(String(relationship.workId), modulePageKey("relationships"));
1386
+ publishRelationshipChange(String(relationship.workId), String(relationship.id));
1383
1387
  noContent(response);
1384
1388
  });
1385
1389
  app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
@@ -1432,6 +1436,20 @@ export function createRuntime(options) {
1432
1436
  const permissions = requestPermissions(request, request.params.workId);
1433
1437
  data(response, mapRecords(store.listTaskSummariesPage(request.params.workId, parsePagination(request.query) ?? { page: 1, limit: 30, offset: 0 }), (task) => redactTaskCharacterNames(task, permissions)));
1434
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
+ });
1435
1453
  app.post("/api/works/:workId/tasks", (request, response) => {
1436
1454
  const input = parse(analysisTaskSchema, request.body);
1437
1455
  data(response, redactTaskCharacterNames(ai.createTask(request.params.workId, input), requestPermissions(request, request.params.workId)), 201);
@@ -1483,7 +1501,30 @@ export function createRuntime(options) {
1483
1501
  allowAdminAccess: request.authMethod !== "api-key"
1484
1502
  } : undefined), requestPermissions(request)));
1485
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
+ });
1486
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
+ });
1487
1528
  app.get("/api/platform/ai/providers", (request, response) => {
1488
1529
  const pagination = parsePagination(request.query);
1489
1530
  data(response, pagination ? ai.listProvidersPage(pagination) : ai.listProviders());
@@ -1495,12 +1536,39 @@ export function createRuntime(options) {
1495
1536
  });
1496
1537
  app.get("/api/platform/ai/settings", (_request, response) => data(response, store.getPlatformAiSettings()));
1497
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
+ });
1498
1543
  app.get("/api/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
1499
1544
  app.get("/api/platform/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
1500
1545
  app.patch("/api/platform/ui-settings", (request, response) => {
1501
1546
  data(response, store.updatePlatformUiSettings(parse(platformUiSettingsSchema, request.body)));
1502
1547
  });
1503
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
+ });
1504
1572
  app.patch("/api/works/:workId/ai-settings", (request, response) => {
1505
1573
  const workId = request.params.workId;
1506
1574
  const before = store.getWorkAiSettings(workId);