@musnows/scriverse 0.3.7 → 0.3.9

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
@@ -16,6 +16,7 @@ import { TASK_TYPES } from "./domain.js";
16
16
  import { AppError } from "./errors.js";
17
17
  import { applyImportFileHints, parseNovelText } from "./parser.js";
18
18
  import { Store, versionedEntityTypes } from "./store.js";
19
+ import { parsePagination } from "./pagination.js";
19
20
  import { normalizeUploadFileName } from "./utils.js";
20
21
  import { assertSafeAiEndpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware } from "./security.js";
21
22
  import { ImageCaptchaService } from "./image-captcha.js";
@@ -54,11 +55,12 @@ const loginSchema = z.object({
54
55
  ...captchaFields
55
56
  }).strict();
56
57
  const userUpdateSchema = z.object({ role: z.enum(["admin", "user"]).optional(), status: z.enum(["active", "disabled"]).optional() }).strict();
57
- const memberSchema = z.object({ userId: identifier, role: z.enum(["editor", "viewer"]) }).strict();
58
- const memberRoleSchema = z.object({ role: z.enum(["editor", "viewer"]) }).strict();
58
+ const memberSchema = z.object({ userId: identifier, role: z.enum(["editor", "settings-editor", "viewer"]) }).strict();
59
+ const memberRoleSchema = z.object({ role: z.enum(["editor", "settings-editor", "viewer"]) }).strict();
59
60
  const profileSchema = z.object({ displayName: z.string().trim().min(1).max(80) }).strict();
60
61
  const passwordChangeSchema = z.object({ currentPassword: z.string().max(200), newPassword: passwordSchema }).strict();
61
62
  const changeNoteSchema = z.string().trim().max(500).optional();
63
+ const expectedVersionNoSchema = z.coerce.number().int().positive().optional();
62
64
  function validateImportedText(text) {
63
65
  if (text.length > maximumImportedTextLength)
64
66
  throw new AppError(413, "IMPORT_TEXT_TOO_LARGE", "导入文件解压后的文本超过 2000 万字符限制");
@@ -173,7 +175,7 @@ const organizationSchema = z.object({
173
175
  description: z.string().max(100_000).optional(),
174
176
  settings: z.array(z.string().trim().min(1).max(20_000)).max(200).optional(),
175
177
  memberIds: z.array(identifier).max(1000).optional()
176
- });
178
+ }).strict();
177
179
  const raceSchema = z.object({
178
180
  name: nonEmpty.max(200),
179
181
  parentRaceId: identifier.nullable().optional(),
@@ -474,8 +476,15 @@ export function createRuntime(options) {
474
476
  });
475
477
  data(response, result);
476
478
  });
477
- app.get("/api/users", (_request, response) => data(response, auth.listUsers()));
478
- app.get("/api/users/directory", (request, response) => data(response, auth.directory(String(request.query.q ?? ""))));
479
+ app.get("/api/users", (request, response) => {
480
+ const pagination = parsePagination(request.query);
481
+ data(response, pagination ? auth.listUsersPage(pagination) : auth.listUsers());
482
+ });
483
+ app.get("/api/users/directory", (request, response) => {
484
+ const pagination = parsePagination(request.query);
485
+ const query = String(request.query.q ?? "");
486
+ data(response, pagination ? auth.directoryPage(query, pagination) : auth.directory(query));
487
+ });
479
488
  app.get("/api/user-avatars/:userId", (request, response) => {
480
489
  const avatar = auth.getAvatar(request.params.userId);
481
490
  response.setHeader("Content-Type", avatar.mimeType);
@@ -491,7 +500,10 @@ export function createRuntime(options) {
491
500
  store.audit(null, "user.updated", "user", updated.userId, { role: updated.role, status: updated.status });
492
501
  data(response, updated);
493
502
  });
494
- app.get("/api/works", (_request, response) => data(response, store.listWorks()));
503
+ app.get("/api/works", (request, response) => {
504
+ const pagination = parsePagination(request.query);
505
+ data(response, pagination ? store.listWorksPage(pagination) : store.listWorks());
506
+ });
495
507
  app.post("/api/works", (request, response) => data(response, store.createWork(parse(workSchema, request.body)), 201));
496
508
  app.post("/api/works/import", upload.single("file"), async (request, response) => {
497
509
  if (!request.file)
@@ -512,8 +524,14 @@ export function createRuntime(options) {
512
524
  });
513
525
  data(response, store.createImportedWork(input, originalFileName, extension.slice(1), parsedNovel), 201);
514
526
  });
515
- app.get("/api/works/:workId", (request, response) => data(response, store.getWorkDirectory(request.params.workId)));
516
- app.get("/api/works/:workId/members", (request, response) => data(response, auth.listMembers(request.params.workId)));
527
+ app.get("/api/works/:workId", (request, response) => {
528
+ const pagination = parsePagination(request.query);
529
+ data(response, pagination ? store.getWorkDirectoryPage(request.params.workId, pagination) : store.getWorkDirectory(request.params.workId));
530
+ });
531
+ app.get("/api/works/:workId/members", (request, response) => {
532
+ const pagination = parsePagination(request.query);
533
+ data(response, pagination ? auth.listMembersPage(request.params.workId, pagination) : auth.listMembers(request.params.workId));
534
+ });
517
535
  app.post("/api/works/:workId/members", (request, response) => {
518
536
  if (!request.authUser)
519
537
  throw new AppError(401, "AUTH_REQUIRED", "请先登录");
@@ -533,9 +551,13 @@ export function createRuntime(options) {
533
551
  store.audit(request.params.workId, "work.member-removed", "user", request.params.userId);
534
552
  data(response, members);
535
553
  });
536
- app.patch("/api/works/:workId", (request, response) => data(response, store.updateWork(request.params.workId, parse(workSchema.partial(), request.body))));
554
+ app.patch("/api/works/:workId", (request, response) => {
555
+ const { expectedVersionNo, changeNote, ...input } = parse(workSchema.partial().extend({ expectedVersionNo: expectedVersionNoSchema, changeNote: changeNoteSchema }).strict(), request.body);
556
+ data(response, store.updateWork(request.params.workId, input, expectedVersionNo, "manual", null, changeNote));
557
+ });
537
558
  app.delete("/api/works/:workId", async (request, response) => {
538
- const removableStorageKeys = store.deleteWork(request.params.workId);
559
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
560
+ const removableStorageKeys = store.deleteWork(request.params.workId, input.expectedVersionNo);
539
561
  await Promise.all(removableStorageKeys.map((storageKey) => attachmentStorage.remove(storageKey)));
540
562
  noContent(response);
541
563
  });
@@ -557,15 +579,21 @@ export function createRuntime(options) {
557
579
  const mimeType = isPng ? "image/png" : isJpeg ? "image/jpeg" : isWebp ? "image/webp" : null;
558
580
  if (!mimeType)
559
581
  throw new AppError(415, "INVALID_COVER", "封面文件内容不是有效的 PNG、JPEG 或 WebP 图片");
560
- data(response, store.setWorkCover(String(request.params.workId), mimeType, bytes));
582
+ const expectedVersionNo = parse(expectedVersionNoSchema, request.body.expectedVersionNo);
583
+ data(response, store.setWorkCover(String(request.params.workId), mimeType, bytes, expectedVersionNo));
561
584
  });
562
585
  app.delete("/api/works/:workId/cover", (request, response) => {
563
- store.deleteWorkCover(request.params.workId);
586
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
587
+ store.deleteWorkCover(request.params.workId, input.expectedVersionNo);
564
588
  noContent(response);
565
589
  });
566
- app.get("/api/works/:workId/file-versions", (request, response) => data(response, store.listFileVersions(request.params.workId)));
590
+ app.get("/api/works/:workId/file-versions", (request, response) => {
591
+ const pagination = parsePagination(request.query);
592
+ data(response, pagination ? store.listFileVersionsPage(request.params.workId, pagination) : store.listFileVersions(request.params.workId));
593
+ });
567
594
  app.post("/api/works/:workId/file-versions/:fileVersionId/restore", (request, response) => {
568
- data(response, store.restoreFileVersion(request.params.workId, request.params.fileVersionId));
595
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
596
+ data(response, store.restoreFileVersion(request.params.workId, request.params.fileVersionId, input.expectedVersionNo));
569
597
  });
570
598
  app.post("/api/works/:workId/import", upload.single("file"), async (request, response) => {
571
599
  if (!request.file)
@@ -579,19 +607,23 @@ export function createRuntime(options) {
579
607
  ? await extractDocxText(request.file.buffer)
580
608
  : decodeUtf8ImportedText(request.file.buffer));
581
609
  const parsed = applyImportFileHints(parseNovelText(text), originalFileName);
582
- data(response, store.importNovel(String(request.params.workId), originalFileName, extension.slice(1), parsed), 201);
610
+ const mode = parse(z.enum(["append", "overwrite"]), request.body.mode ?? "overwrite");
611
+ const expectedVersionNo = parse(expectedVersionNoSchema, request.body.expectedVersionNo);
612
+ data(response, store.importNovel(String(request.params.workId), originalFileName, extension.slice(1), parsed, mode, expectedVersionNo), 201);
583
613
  });
584
614
  app.post("/api/works/:workId/volumes", (request, response) => {
585
615
  const input = parse(z.object({ title: nonEmpty.max(200), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional() }), request.body);
586
616
  data(response, store.createVolume(request.params.workId, input), 201);
587
617
  });
588
618
  app.patch("/api/volumes/:volumeId", (request, response) => {
589
- const input = parse(z.object({ title: nonEmpty.max(200).optional(), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional(), sortOrder: z.number().int().min(0).optional() }), request.body);
590
- data(response, store.updateVolume(request.params.volumeId, input));
619
+ const input = parse(z.object({ title: nonEmpty.max(200).optional(), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional(), sortOrder: z.number().int().min(0).optional(), expectedVersionNo: expectedVersionNoSchema, changeNote: changeNoteSchema }).strict(), request.body);
620
+ const { expectedVersionNo, changeNote, ...volumeInput } = input;
621
+ data(response, store.updateVolume(request.params.volumeId, volumeInput, expectedVersionNo, "manual", null, changeNote));
591
622
  });
592
623
  app.get("/api/volumes/:volumeId", (request, response) => data(response, store.getVolume(request.params.volumeId)));
593
624
  app.delete("/api/volumes/:volumeId", (request, response) => {
594
- store.deleteVolume(request.params.volumeId);
625
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
626
+ store.deleteVolume(request.params.volumeId, input.expectedVersionNo);
595
627
  noContent(response);
596
628
  });
597
629
  app.post("/api/works/:workId/chapters", (request, response) => {
@@ -600,32 +632,44 @@ export function createRuntime(options) {
600
632
  });
601
633
  app.get("/api/chapters/:chapterId", (request, response) => data(response, store.getChapter(request.params.chapterId)));
602
634
  app.patch("/api/chapters/:chapterId", (request, response) => {
603
- 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 }).strict(), request.body);
604
- const { source, changeNote, ...chapterInput } = input;
605
- data(response, store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote));
635
+ 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);
636
+ const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
637
+ data(response, store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo));
606
638
  });
607
639
  app.delete("/api/chapters/:chapterId", (request, response) => {
608
- store.deleteChapter(request.params.chapterId);
640
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
641
+ store.deleteChapter(request.params.chapterId, input.expectedVersionNo);
609
642
  noContent(response);
610
643
  });
611
- app.get("/api/chapters/:chapterId/versions", (request, response) => data(response, store.listChapterVersions(request.params.chapterId)));
612
- app.get("/api/chapters/:chapterId/insights", (request, response) => data(response, store.listChapterInsights(request.params.chapterId)));
644
+ app.get("/api/chapters/:chapterId/versions", (request, response) => {
645
+ const pagination = parsePagination(request.query);
646
+ data(response, pagination ? store.listChapterVersionsPage(request.params.chapterId, pagination) : store.listChapterVersions(request.params.chapterId));
647
+ });
648
+ app.get("/api/chapters/:chapterId/insights", (request, response) => {
649
+ const pagination = parsePagination(request.query);
650
+ data(response, pagination ? store.listChapterInsightsPage(request.params.chapterId, pagination) : store.listChapterInsights(request.params.chapterId));
651
+ });
613
652
  app.post("/api/chapters/:chapterId/restore", (request, response) => {
614
- const input = parse(z.object({ versionNo: z.number().int().positive() }), request.body);
615
- data(response, store.restoreChapter(request.params.chapterId, input.versionNo));
653
+ const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
654
+ data(response, store.restoreChapter(request.params.chapterId, input.versionNo, input.expectedVersionNo));
616
655
  });
617
656
  app.post("/api/chapters/:chapterId/move", (request, response) => {
618
- const input = parse(z.object({ volumeId: identifier, sortOrder: z.number().int().min(0) }), request.body);
619
- data(response, store.moveChapter(request.params.chapterId, input));
657
+ const input = parse(z.object({ volumeId: identifier, sortOrder: z.number().int().min(0), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
658
+ const { expectedVersionNo, ...moveInput } = input;
659
+ data(response, store.moveChapter(request.params.chapterId, moveInput, expectedVersionNo));
660
+ });
661
+ app.get("/api/works/:workId/outlines", (request, response) => {
662
+ const pagination = parsePagination(request.query);
663
+ data(response, pagination ? store.listChapterOutlinesPage(request.params.workId, pagination) : store.listChapterOutlines(request.params.workId));
620
664
  });
621
- app.get("/api/works/:workId/outlines", (request, response) => data(response, store.listChapterOutlines(request.params.workId)));
622
665
  app.get("/api/chapters/:chapterId/outline", (request, response) => data(response, store.getChapterOutline(request.params.chapterId)));
623
666
  app.put("/api/chapters/:chapterId/outline", (request, response) => {
624
- const { changeNote, ...input } = parse(chapterOutlineSchema.extend({ changeNote: changeNoteSchema }), request.body);
625
- data(response, store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote));
667
+ const { changeNote, expectedVersionNo, ...input } = parse(chapterOutlineSchema.extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
668
+ data(response, store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote, expectedVersionNo));
626
669
  });
627
670
  app.delete("/api/chapters/:chapterId/outline", (request, response) => {
628
- store.deleteChapterOutline(request.params.chapterId);
671
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
672
+ store.deleteChapterOutline(request.params.chapterId, input.expectedVersionNo);
629
673
  noContent(response);
630
674
  });
631
675
  app.get("/api/works/:workId/foreshadows", (request, response) => {
@@ -633,39 +677,52 @@ export function createRuntime(options) {
633
677
  status: z.enum(["all", "unresolved", "resolved"]).default("all"),
634
678
  currentChapterId: identifier.optional()
635
679
  }), request.query);
636
- data(response, store.listForeshadows(request.params.workId, query.status, query.currentChapterId));
680
+ const pagination = parsePagination(request.query);
681
+ data(response, pagination
682
+ ? store.listForeshadowsPage(request.params.workId, pagination, query.status, query.currentChapterId)
683
+ : store.listForeshadows(request.params.workId, query.status, query.currentChapterId));
637
684
  });
638
685
  app.post("/api/works/:workId/foreshadows", (request, response) => {
639
686
  data(response, store.createForeshadow(request.params.workId, parse(foreshadowSchema, request.body)), 201);
640
687
  });
641
688
  app.get("/api/foreshadows/:foreshadowId", (request, response) => data(response, store.getForeshadow(request.params.foreshadowId)));
642
689
  app.patch("/api/foreshadows/:foreshadowId", (request, response) => {
643
- const { changeNote, ...input } = parse(foreshadowSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
644
- data(response, store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote));
690
+ const { changeNote, expectedVersionNo, ...input } = parse(foreshadowSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
691
+ data(response, store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote, expectedVersionNo));
645
692
  });
646
693
  app.delete("/api/foreshadows/:foreshadowId", (request, response) => {
647
- store.deleteForeshadow(request.params.foreshadowId);
694
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
695
+ store.deleteForeshadow(request.params.foreshadowId, input.expectedVersionNo);
648
696
  noContent(response);
649
697
  });
650
698
  app.post("/api/foreshadows/:foreshadowId/occurrences", (request, response) => {
651
- data(response, store.createForeshadowOccurrence(request.params.foreshadowId, parse(foreshadowOccurrenceSchema, request.body)), 201);
699
+ const input = parse(foreshadowOccurrenceSchema.extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
700
+ const { expectedVersionNo, ...occurrenceInput } = input;
701
+ data(response, store.createForeshadowOccurrence(request.params.foreshadowId, occurrenceInput, expectedVersionNo), 201);
652
702
  });
653
703
  app.patch("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
654
- data(response, store.updateForeshadowOccurrence(request.params.occurrenceId, parse(foreshadowOccurrenceSchema.partial(), request.body)));
704
+ const input = parse(foreshadowOccurrenceSchema.partial().extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
705
+ const { expectedVersionNo, ...occurrenceInput } = input;
706
+ data(response, store.updateForeshadowOccurrence(request.params.occurrenceId, occurrenceInput, expectedVersionNo));
655
707
  });
656
708
  app.delete("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
657
- store.deleteForeshadowOccurrence(request.params.occurrenceId);
709
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
710
+ store.deleteForeshadowOccurrence(request.params.occurrenceId, input.expectedVersionNo);
658
711
  noContent(response);
659
712
  });
660
- app.get("/api/works/:workId/settings", (request, response) => data(response, store.listSettings(request.params.workId)));
713
+ app.get("/api/works/:workId/settings", (request, response) => {
714
+ const pagination = parsePagination(request.query);
715
+ data(response, pagination ? store.listSettingsPage(request.params.workId, pagination) : store.listSettings(request.params.workId));
716
+ });
661
717
  app.post("/api/works/:workId/settings", (request, response) => data(response, store.createSetting(request.params.workId, parse(settingSchema, request.body)), 201));
662
718
  app.get("/api/settings/:settingId", (request, response) => data(response, store.getSetting(request.params.settingId)));
663
719
  app.patch("/api/settings/:settingId", (request, response) => {
664
- const { changeNote, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
665
- data(response, store.updateSetting(request.params.settingId, input, "manual", null, changeNote));
720
+ const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
721
+ data(response, store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo));
666
722
  });
667
723
  app.delete("/api/settings/:settingId", (request, response) => {
668
- store.deleteSetting(request.params.settingId);
724
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
725
+ store.deleteSetting(request.params.settingId, input.expectedVersionNo);
669
726
  noContent(response);
670
727
  });
671
728
  app.get("/api/works/:workId/characters", (request, response) => {
@@ -673,25 +730,47 @@ export function createRuntime(options) {
673
730
  includeSections: z.enum(["true", "false"]).default("false"),
674
731
  includeMerged: z.enum(["0", "1"]).default("0")
675
732
  }), request.query);
676
- data(response, store.listCharacters(request.params.workId, includeSections === "true", includeMerged === "1"));
733
+ const pagination = parsePagination(request.query);
734
+ data(response, pagination
735
+ ? store.listCharactersPage(request.params.workId, pagination, includeSections === "true", includeMerged === "1")
736
+ : store.listCharacters(request.params.workId, includeSections === "true", includeMerged === "1"));
677
737
  });
678
738
  app.post("/api/works/:workId/characters", (request, response) => data(response, store.createCharacter(request.params.workId, parse(characterSchema, request.body)), 201));
679
739
  app.get("/api/characters/:characterId", (request, response) => data(response, store.getCharacter(request.params.characterId)));
680
740
  app.patch("/api/characters/:characterId", (request, response) => {
681
- const { changeNote, ...input } = parse(characterUpdateSchema, request.body);
682
- data(response, store.updateCharacter(request.params.characterId, input, "manual", null, changeNote));
741
+ const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
742
+ data(response, store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo));
743
+ });
744
+ app.get("/api/characters/:characterId/versions", (request, response) => {
745
+ const pagination = parsePagination(request.query);
746
+ data(response, pagination ? store.listCharacterVersionsPage(request.params.characterId, pagination) : store.listCharacterVersions(request.params.characterId));
683
747
  });
684
- app.get("/api/characters/:characterId/versions", (request, response) => data(response, store.listCharacterVersions(request.params.characterId)));
685
748
  app.post("/api/characters/:characterId/restore", (request, response) => {
686
- const input = parse(z.object({ versionNo: z.number().int().positive() }), request.body);
687
- data(response, store.restoreCharacter(request.params.characterId, input.versionNo));
749
+ const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
750
+ data(response, store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo));
688
751
  });
689
752
  app.delete("/api/characters/:characterId", (request, response) => {
690
- store.deleteCharacter(request.params.characterId);
753
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
754
+ store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
691
755
  noContent(response);
692
756
  });
757
+ app.post("/api/characters/:characterId/merge", (request, response) => {
758
+ const input = parse(z.object({
759
+ targetCharacterId: identifier,
760
+ expectedTargetVersionNo: z.number().int().positive(),
761
+ expectedSourceVersionNo: z.number().int().positive()
762
+ }).strict(), request.body);
763
+ data(response, store.mergeCharacters({
764
+ reviewId: null,
765
+ sourceCharacterId: request.params.characterId,
766
+ ...input
767
+ }));
768
+ });
693
769
  app.get("/api/characters/:characterId/sections", (request, response) => {
694
- data(response, store.listCharacterProfileSections(request.params.characterId));
770
+ const pagination = parsePagination(request.query);
771
+ data(response, pagination
772
+ ? store.listCharacterProfileSectionsPage(request.params.characterId, pagination)
773
+ : store.listCharacterProfileSections(request.params.characterId));
695
774
  });
696
775
  app.post("/api/characters/:characterId/sections", (request, response) => {
697
776
  data(response, store.createCharacterProfileSection(request.params.characterId, parse(characterProfileSectionSchema, request.body)), 201);
@@ -700,22 +779,27 @@ export function createRuntime(options) {
700
779
  data(response, store.getCharacterProfileSection(request.params.sectionId));
701
780
  });
702
781
  app.patch("/api/character-sections/:sectionId", (request, response) => {
703
- const { changeNote, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema }).strict(), request.body);
704
- data(response, store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote));
782
+ const { changeNote, expectedVersionNo, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
783
+ data(response, store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo));
705
784
  });
706
785
  app.delete("/api/character-sections/:sectionId", (request, response) => {
707
- store.deleteCharacterProfileSection(request.params.sectionId);
786
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
787
+ store.deleteCharacterProfileSection(request.params.sectionId, input.expectedVersionNo);
708
788
  noContent(response);
709
789
  });
710
790
  app.get("/api/character-sections/:sectionId/versions", (request, response) => {
711
- data(response, store.listCharacterProfileSectionVersions(request.params.sectionId));
791
+ const pagination = parsePagination(request.query);
792
+ data(response, pagination
793
+ ? store.listCharacterProfileSectionVersionsPage(request.params.sectionId, pagination)
794
+ : store.listCharacterProfileSectionVersions(request.params.sectionId));
712
795
  });
713
796
  app.post("/api/character-sections/:sectionId/restore", (request, response) => {
714
- const input = parse(z.object({ versionNo: z.number().int().positive() }).strict(), request.body);
715
- data(response, store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo));
797
+ const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
798
+ data(response, store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo, input.expectedVersionNo));
716
799
  });
717
800
  app.get("/api/works/:workId/attachments", (request, response) => {
718
- data(response, store.listAttachments(request.params.workId));
801
+ const pagination = parsePagination(request.query);
802
+ data(response, pagination ? store.listAttachmentsPage(request.params.workId, pagination) : store.listAttachments(request.params.workId));
719
803
  });
720
804
  app.post("/api/works/:workId/attachments", attachmentUpload.single("file"), async (request, response) => {
721
805
  if (!request.file)
@@ -758,44 +842,67 @@ export function createRuntime(options) {
758
842
  await attachmentStorage.remove(deleted.storageKey);
759
843
  noContent(response);
760
844
  });
761
- app.get("/api/works/:workId/races", (request, response) => data(response, store.listRaces(request.params.workId)));
845
+ app.get("/api/works/:workId/races", (request, response) => {
846
+ const pagination = parsePagination(request.query);
847
+ data(response, pagination ? store.listRacesPage(request.params.workId, pagination) : store.listRaces(request.params.workId));
848
+ });
762
849
  app.post("/api/works/:workId/races", (request, response) => {
763
850
  data(response, store.createRace(request.params.workId, parse(raceSchema, request.body)), 201);
764
851
  });
765
852
  app.get("/api/races/:raceId", (request, response) => data(response, store.getRace(request.params.raceId)));
766
853
  app.patch("/api/races/:raceId", (request, response) => {
767
- const { changeNote, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
768
- data(response, store.updateRace(request.params.raceId, input, "manual", null, changeNote));
854
+ const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
855
+ data(response, store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo));
769
856
  });
770
857
  app.delete("/api/races/:raceId", (request, response) => {
771
- store.deleteRace(request.params.raceId);
858
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
859
+ store.deleteRace(request.params.raceId, input.expectedVersionNo);
772
860
  noContent(response);
773
861
  });
774
- app.get("/api/works/:workId/organizations", (request, response) => data(response, store.listOrganizations(request.params.workId)));
862
+ app.post("/api/races/:raceId/merge", (request, response) => {
863
+ const input = parse(z.object({ targetRaceId: identifier }).strict(), request.body);
864
+ data(response, store.mergeRaces(request.params.raceId, input.targetRaceId));
865
+ });
866
+ app.get("/api/works/:workId/organizations", (request, response) => {
867
+ const pagination = parsePagination(request.query);
868
+ data(response, pagination ? store.listOrganizationsPage(request.params.workId, pagination) : store.listOrganizations(request.params.workId));
869
+ });
775
870
  app.post("/api/works/:workId/organizations", (request, response) => {
776
871
  data(response, store.createOrganization(request.params.workId, parse(organizationSchema, request.body)), 201);
777
872
  });
778
873
  app.get("/api/organizations/:organizationId", (request, response) => data(response, store.getOrganization(request.params.organizationId)));
779
874
  app.patch("/api/organizations/:organizationId", (request, response) => {
780
- const { changeNote, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
781
- data(response, store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote));
875
+ const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
876
+ data(response, store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo));
782
877
  });
783
878
  app.delete("/api/organizations/:organizationId", (request, response) => {
784
- store.deleteOrganization(request.params.organizationId);
879
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
880
+ store.deleteOrganization(request.params.organizationId, input.expectedVersionNo);
785
881
  noContent(response);
786
882
  });
787
- app.get("/api/works/:workId/timeline-tracks", (request, response) => data(response, store.listTimelineTracks(request.params.workId)));
883
+ app.post("/api/organizations/:organizationId/merge", (request, response) => {
884
+ const input = parse(z.object({ targetOrganizationId: identifier }).strict(), request.body);
885
+ data(response, store.mergeOrganizations(request.params.organizationId, input.targetOrganizationId));
886
+ });
887
+ app.get("/api/works/:workId/timeline-tracks", (request, response) => {
888
+ const pagination = parsePagination(request.query);
889
+ data(response, pagination ? store.listTimelineTracksPage(request.params.workId, pagination) : store.listTimelineTracks(request.params.workId));
890
+ });
788
891
  app.post("/api/works/:workId/timeline-tracks", (request, response) => data(response, store.createTimelineTrack(request.params.workId, parse(timelineTrackSchema, request.body)), 201));
789
892
  app.get("/api/timeline-tracks/:trackId", (request, response) => data(response, store.getTimelineTrack(request.params.trackId)));
790
893
  app.patch("/api/timeline-tracks/:trackId", (request, response) => {
791
- const { changeNote, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
792
- data(response, store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote));
894
+ const { changeNote, expectedVersionNo, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
895
+ data(response, store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo));
793
896
  });
794
897
  app.delete("/api/timeline-tracks/:trackId", (request, response) => {
795
- store.deleteTimelineTrack(request.params.trackId);
898
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
899
+ store.deleteTimelineTrack(request.params.trackId, input.expectedVersionNo);
796
900
  noContent(response);
797
901
  });
798
- app.get("/api/works/:workId/timeline", (request, response) => data(response, store.listTimelineEvents(request.params.workId)));
902
+ app.get("/api/works/:workId/timeline", (request, response) => {
903
+ const pagination = parsePagination(request.query);
904
+ data(response, pagination ? store.listTimelineEventsPage(request.params.workId, pagination) : store.listTimelineEvents(request.params.workId));
905
+ });
799
906
  app.post("/api/works/:workId/timeline", (request, response) => data(response, store.createTimelineEvent(request.params.workId, parse(timelineSchema, request.body)), 201));
800
907
  app.post("/api/works/:workId/timeline/merge", (request, response) => {
801
908
  const input = parse(z.object({
@@ -803,14 +910,16 @@ export function createRuntime(options) {
803
910
  name: nonEmpty.max(300),
804
911
  description: z.string().max(100_000).optional(),
805
912
  timeLabel: z.string().max(300).optional(),
806
- timeSort: z.number().finite().nullable().optional()
807
- }), request.body);
808
- data(response, store.mergeTimelineEvents(request.params.workId, input.eventIds, input), 201);
913
+ timeSort: z.number().finite().nullable().optional(),
914
+ expectedVersionNos: z.record(identifier, z.number().int().positive()).optional()
915
+ }).strict(), request.body);
916
+ const { expectedVersionNos, ...mergeInput } = input;
917
+ data(response, store.mergeTimelineEvents(request.params.workId, input.eventIds, mergeInput, expectedVersionNos), 201);
809
918
  });
810
919
  app.get("/api/timeline/:eventId", (request, response) => data(response, store.getTimelineEvent(request.params.eventId)));
811
920
  app.patch("/api/timeline/:eventId", (request, response) => {
812
- const { changeNote, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
813
- data(response, store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote));
921
+ const { changeNote, expectedVersionNo, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
922
+ data(response, store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo));
814
923
  });
815
924
  app.post("/api/timeline/:eventId/split", (request, response) => {
816
925
  const input = parse(z.object({
@@ -819,42 +928,52 @@ export function createRuntime(options) {
819
928
  description: z.string().max(100_000).optional(),
820
929
  timeLabel: z.string().max(300).optional(),
821
930
  timeSort: z.number().finite().nullable().optional()
822
- })).min(2)
823
- }), request.body);
824
- data(response, store.splitTimelineEvent(request.params.eventId, input.parts), 201);
931
+ })).min(2),
932
+ expectedVersionNo: expectedVersionNoSchema
933
+ }).strict(), request.body);
934
+ data(response, store.splitTimelineEvent(request.params.eventId, input.parts, input.expectedVersionNo), 201);
825
935
  });
826
936
  app.delete("/api/timeline/:eventId", (request, response) => {
827
- store.deleteTimelineEvent(request.params.eventId);
937
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
938
+ store.deleteTimelineEvent(request.params.eventId, input.expectedVersionNo);
828
939
  noContent(response);
829
940
  });
830
941
  app.get("/api/works/:workId/relationships", (request, response) => {
831
942
  const confidence = request.query.minimumConfidence ? Number(request.query.minimumConfidence) : 0;
832
943
  if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1)
833
944
  throw new AppError(400, "INVALID_CONFIDENCE", "置信度必须在 0 到 1 之间");
834
- data(response, store.listRelationships(request.params.workId, confidence));
945
+ const pagination = parsePagination(request.query);
946
+ data(response, pagination
947
+ ? store.listRelationshipsPage(request.params.workId, pagination, confidence)
948
+ : store.listRelationships(request.params.workId, confidence));
835
949
  });
836
950
  app.post("/api/works/:workId/relationships", (request, response) => data(response, store.createRelationship(request.params.workId, parse(relationshipSchema, request.body)), 201));
837
951
  app.get("/api/relationships/:relationshipId", (request, response) => data(response, store.getRelationship(request.params.relationshipId)));
838
952
  app.patch("/api/relationships/:relationshipId", (request, response) => {
839
- const { changeNote, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
840
- data(response, store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote));
953
+ const { changeNote, expectedVersionNo, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
954
+ data(response, store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo));
841
955
  });
842
956
  app.delete("/api/relationships/:relationshipId", (request, response) => {
843
- store.deleteRelationship(request.params.relationshipId);
957
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
958
+ store.deleteRelationship(request.params.relationshipId, input.expectedVersionNo);
844
959
  noContent(response);
845
960
  });
846
961
  app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
847
962
  const input = parse(z.object({ entityType: versionedEntityTypeSchema, entityId: identifier }), request.params);
848
- data(response, store.listEntityVersions(input.entityType, input.entityId));
963
+ const pagination = parsePagination(request.query);
964
+ data(response, pagination
965
+ ? store.listEntityVersionsPage(input.entityType, input.entityId, pagination)
966
+ : store.listEntityVersions(input.entityType, input.entityId));
849
967
  });
850
968
  app.post("/api/entity-versions/:entityType/:entityId/restore", (request, response) => {
851
969
  const params = parse(z.object({ entityType: versionedEntityTypeSchema, entityId: identifier }), request.params);
852
- const input = parse(z.object({ versionNo: z.number().int().positive() }), request.body);
853
- data(response, store.restoreEntityVersion(params.entityType, params.entityId, input.versionNo));
970
+ const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
971
+ data(response, store.restoreEntityVersion(params.entityType, params.entityId, input.versionNo, input.expectedVersionNo));
854
972
  });
855
973
  app.get("/api/works/:workId/reviews", (request, response) => {
856
974
  const status = typeof request.query.status === "string" ? request.query.status : undefined;
857
- data(response, store.listReviewItems(request.params.workId, status));
975
+ const pagination = parsePagination(request.query);
976
+ data(response, pagination ? store.listReviewItemsPage(request.params.workId, pagination, status) : store.listReviewItems(request.params.workId, status));
858
977
  });
859
978
  app.post("/api/works/:workId/reviews", (request, response) => data(response, store.createReviewItem(request.params.workId, parse(reviewSchema, request.body)), 201));
860
979
  app.patch("/api/reviews/:reviewId", (request, response) => data(response, store.updateReviewItem(request.params.reviewId, parse(reviewSchema.partial(), request.body))));
@@ -875,7 +994,10 @@ export function createRuntime(options) {
875
994
  }
876
995
  data(response, store.mergeCharacters({ reviewId: request.params.reviewId, ...input }));
877
996
  });
878
- app.get("/api/works/:workId/tasks", (request, response) => data(response, store.listTasks(request.params.workId)));
997
+ app.get("/api/works/:workId/tasks", (request, response) => {
998
+ const pagination = parsePagination(request.query);
999
+ data(response, pagination ? store.listTasksPage(request.params.workId, pagination) : store.listTasks(request.params.workId));
1000
+ });
879
1001
  app.post("/api/works/:workId/tasks", (request, response) => {
880
1002
  const input = parse(z.object({ taskType: z.enum(["structure", "chapter-analysis", "character-extraction", "character-summary", "character-identity-audit", "timeline-analysis", "relationship-analysis", "worldview-analysis", "setting-extraction", "consistency-check", "report-update", "book-analysis"]), scope: jsonObject.optional() }), request.body);
881
1003
  data(response, store.createTask(request.params.workId, input), 201);
@@ -889,9 +1011,15 @@ export function createRuntime(options) {
889
1011
  data(response, await ai.runTask(request.params.taskId, input.modelId));
890
1012
  });
891
1013
  app.post("/api/tasks/:taskId/cancel", (request, response) => data(response, ai.cancelTask(request.params.taskId)));
892
- app.get("/api/platform/ai/providers", (_request, response) => data(response, ai.listProviders()));
1014
+ app.get("/api/platform/ai/providers", (request, response) => {
1015
+ const pagination = parsePagination(request.query);
1016
+ data(response, pagination ? ai.listProvidersPage(pagination) : ai.listProviders());
1017
+ });
893
1018
  app.post("/api/platform/ai/providers", (request, response) => data(response, ai.createProvider(parse(providerSchema, request.body)), 201));
894
- app.get("/api/platform/ai/models", (_request, response) => data(response, ai.listPlatformModels()));
1019
+ app.get("/api/platform/ai/models", (request, response) => {
1020
+ const pagination = parsePagination(request.query);
1021
+ data(response, pagination ? ai.listPlatformModelsPage(pagination) : ai.listPlatformModels());
1022
+ });
895
1023
  app.get("/api/platform/ai/settings", (_request, response) => data(response, store.getPlatformAiSettings()));
896
1024
  app.patch("/api/platform/ai/settings", (request, response) => data(response, store.updatePlatformAiSettings(parse(aiPromptSchema, request.body))));
897
1025
  app.get("/api/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
@@ -911,12 +1039,18 @@ export function createRuntime(options) {
911
1039
  }
912
1040
  data(response, updated);
913
1041
  });
914
- app.get("/api/works/:workId/ai-conversations", (request, response) => data(response, store.listAiConversations(request.params.workId)));
1042
+ app.get("/api/works/:workId/ai-conversations", (request, response) => {
1043
+ const pagination = parsePagination(request.query);
1044
+ data(response, pagination ? store.listAiConversationsPage(request.params.workId, pagination) : store.listAiConversations(request.params.workId));
1045
+ });
915
1046
  app.post("/api/works/:workId/ai-conversations", (request, response) => {
916
1047
  const input = parse(z.object({ title: z.string().max(200).optional() }), request.body ?? {});
917
1048
  data(response, store.createAiConversation(request.params.workId, input.title), 201);
918
1049
  });
919
- app.get("/api/ai-conversations/:conversationId", (request, response) => data(response, store.getAiConversation(request.params.conversationId)));
1050
+ app.get("/api/ai-conversations/:conversationId", (request, response) => {
1051
+ const pagination = parsePagination(request.query);
1052
+ data(response, pagination ? store.getAiConversationPage(request.params.conversationId, pagination) : store.getAiConversation(request.params.conversationId));
1053
+ });
920
1054
  app.post("/api/ai-conversations/:conversationId/fork", (request, response) => {
921
1055
  const input = parse(z.object({ messageId: identifier, title: z.string().max(200).optional() }), request.body);
922
1056
  data(response, store.forkAiConversation(request.params.conversationId, input.messageId, input.title), 201);
@@ -997,7 +1131,10 @@ export function createRuntime(options) {
997
1131
  noContent(response);
998
1132
  });
999
1133
  app.post("/api/providers/:providerId/test", async (request, response) => data(response, await ai.testProvider(request.params.providerId)));
1000
- app.get("/api/providers/:providerId/models", (request, response) => data(response, ai.listModels(request.params.providerId)));
1134
+ app.get("/api/providers/:providerId/models", (request, response) => {
1135
+ const pagination = parsePagination(request.query);
1136
+ data(response, pagination ? ai.listModelsPage(request.params.providerId, pagination) : ai.listModels(request.params.providerId));
1137
+ });
1001
1138
  app.post("/api/providers/:providerId/models", (request, response) => data(response, ai.createModel(request.params.providerId, parse(modelSchema, request.body)), 201));
1002
1139
  app.get("/api/models/:modelId", (request, response) => data(response, ai.getModel(request.params.modelId)));
1003
1140
  app.patch("/api/models/:modelId", (request, response) => data(response, ai.updateModel(request.params.modelId, parse(modelSchema.partial(), request.body))));
@@ -1005,8 +1142,14 @@ export function createRuntime(options) {
1005
1142
  ai.deleteModel(request.params.modelId);
1006
1143
  noContent(response);
1007
1144
  });
1008
- app.get("/api/works/:workId/models", (request, response) => data(response, ai.listWorkModels(request.params.workId)));
1009
- app.get("/api/works/:workId/task-defaults", (request, response) => data(response, ai.listTaskDefaults(request.params.workId)));
1145
+ app.get("/api/works/:workId/models", (request, response) => {
1146
+ const pagination = parsePagination(request.query);
1147
+ data(response, pagination ? ai.listWorkModelsPage(request.params.workId, pagination) : ai.listWorkModels(request.params.workId));
1148
+ });
1149
+ app.get("/api/works/:workId/task-defaults", (request, response) => {
1150
+ const pagination = parsePagination(request.query);
1151
+ data(response, pagination ? ai.listTaskDefaultsPage(request.params.workId, pagination) : ai.listTaskDefaults(request.params.workId));
1152
+ });
1010
1153
  app.put("/api/works/:workId/task-defaults/:taskType", (request, response) => {
1011
1154
  const taskType = parse(z.enum(TASK_TYPES), request.params.taskType);
1012
1155
  const input = parse(z.object({ modelId: identifier }), request.body);
@@ -1014,7 +1157,8 @@ export function createRuntime(options) {
1014
1157
  });
1015
1158
  app.get("/api/works/:workId/suggestions", (request, response) => {
1016
1159
  const status = typeof request.query.status === "string" ? request.query.status : undefined;
1017
- data(response, ai.listSuggestions(request.params.workId, status));
1160
+ const pagination = parsePagination(request.query);
1161
+ data(response, pagination ? ai.listSuggestionsPage(request.params.workId, pagination, status) : ai.listSuggestions(request.params.workId, status));
1018
1162
  });
1019
1163
  app.post("/api/works/:workId/suggestions", async (request, response) => {
1020
1164
  const input = parse(z.object({
@@ -1109,7 +1253,10 @@ export function createRuntime(options) {
1109
1253
  });
1110
1254
  app.get("/api/suggestions/:suggestionId", (request, response) => data(response, ai.getSuggestion(request.params.suggestionId)));
1111
1255
  app.get("/api/suggestions/:suggestionId/guards", (request, response) => {
1112
- data(response, store.listContinuationGuards(request.params.suggestionId));
1256
+ const pagination = parsePagination(request.query);
1257
+ data(response, pagination
1258
+ ? store.listContinuationGuardsPage(request.params.suggestionId, pagination)
1259
+ : store.listContinuationGuards(request.params.suggestionId));
1113
1260
  });
1114
1261
  app.post("/api/suggestions/:suggestionId/guard", async (request, response) => {
1115
1262
  const input = parse(z.object({ content: z.string().max(2_000_000).optional() }), request.body ?? {});
@@ -1120,7 +1267,10 @@ export function createRuntime(options) {
1120
1267
  data(response, ai.acceptSuggestion(request.params.suggestionId, input.content));
1121
1268
  });
1122
1269
  app.post("/api/suggestions/:suggestionId/reject", (request, response) => data(response, ai.rejectSuggestion(request.params.suggestionId)));
1123
- app.get("/api/works/:workId/ai-calls", (request, response) => data(response, ai.listCalls(request.params.workId)));
1270
+ app.get("/api/works/:workId/ai-calls", (request, response) => {
1271
+ const pagination = parsePagination(request.query);
1272
+ data(response, pagination ? ai.listCallsPage(request.params.workId, pagination) : ai.listCalls(request.params.workId));
1273
+ });
1124
1274
  app.get("/api/works/:workId/search", (request, response) => {
1125
1275
  const query = parse(z.string().trim().min(1).max(500), request.query.q);
1126
1276
  data(response, store.search(request.params.workId, query));
@@ -1136,7 +1286,10 @@ export function createRuntime(options) {
1136
1286
  response.setHeader("Content-Disposition", `attachment; filename=novel-${request.params.workId}.${format === "markdown" ? "md" : "txt"}`);
1137
1287
  response.send(store.exportText(request.params.workId, format));
1138
1288
  });
1139
- app.get("/api/works/:workId/audit-logs", (request, response) => data(response, store.listAuditLogs(request.params.workId)));
1289
+ app.get("/api/works/:workId/audit-logs", (request, response) => {
1290
+ const pagination = parsePagination(request.query);
1291
+ data(response, pagination ? store.listAuditLogsPage(request.params.workId, pagination) : store.listAuditLogs(request.params.workId));
1292
+ });
1140
1293
  if (options.serveUi ?? true) {
1141
1294
  const publicPath = options.publicPath ?? join(process.cwd(), "src", "public");
1142
1295
  // index.html 按登录态动态下发:未登录时注入 login-route 类,首帧直接渲染登录页;