@musnows/scriverse 0.4.11 → 0.5.0

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/store.js CHANGED
@@ -6,6 +6,27 @@ import { paginated, paginationSql } from "./pagination.js";
6
6
  import { currentRequestActor } from "./request-context.js";
7
7
  import { classifyWorkModulePermissions, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions } from "./work-permissions.js";
8
8
  import { countWords, documentShortSearchTerms, id, json, normalizeDocumentSearchText, normalizeParagraphSpacing, now, splitDocumentParagraphs } from "./utils.js";
9
+ const defaultPlatformPageSizes = {
10
+ characters: 30,
11
+ analysisTasks: 30,
12
+ fileVersions: 30
13
+ };
14
+ function platformPageSizes(value) {
15
+ const stored = typeof value === "string"
16
+ ? json(value, {})
17
+ : value && typeof value === "object" && !Array.isArray(value) ? value : {};
18
+ const pageSize = (key) => {
19
+ const candidate = Number(stored[key]);
20
+ return Number.isInteger(candidate) && candidate >= 10 && candidate <= 100
21
+ ? candidate
22
+ : defaultPlatformPageSizes[key];
23
+ };
24
+ return {
25
+ characters: pageSize("characters"),
26
+ analysisTasks: pageSize("analysisTasks"),
27
+ fileVersions: pageSize("fileVersions")
28
+ };
29
+ }
9
30
  function settingsFromInput(settingsMarkdown, settings, fallback = []) {
10
31
  if (settingsMarkdown !== undefined)
11
32
  return settingsMarkdown.trim() ? [settingsMarkdown] : [];
@@ -571,16 +592,23 @@ export class Store {
571
592
  const row = this.db.get("SELECT * FROM platform_ui_settings WHERE id = 1");
572
593
  return {
573
594
  toastPosition: String(row?.toast_position) === "top-right" ? "top-right" : "bottom-right",
595
+ pageSizes: platformPageSizes(row?.page_sizes_json),
574
596
  updatedAt: String(row?.updated_at ?? "")
575
597
  };
576
598
  }
577
599
  updatePlatformUiSettings(input) {
578
600
  const timestamp = now();
601
+ const current = this.getPlatformUiSettings();
602
+ const currentPageSizes = platformPageSizes(current.pageSizes);
603
+ const pageSizes = platformPageSizes(JSON.stringify({ ...currentPageSizes, ...input.pageSizes }));
604
+ const toastPosition = input.toastPosition ?? (current.toastPosition === "top-right" ? "top-right" : "bottom-right");
579
605
  this.db.transaction(() => {
580
- this.db.run(`INSERT INTO platform_ui_settings (id, toast_position, updated_at) VALUES (1, ?, ?)
581
- ON CONFLICT(id) DO UPDATE SET toast_position = excluded.toast_position, updated_at = excluded.updated_at`, input.toastPosition, timestamp);
606
+ this.db.run(`INSERT INTO platform_ui_settings (id, toast_position, page_sizes_json, updated_at) VALUES (1, ?, ?, ?)
607
+ ON CONFLICT(id) DO UPDATE SET toast_position = excluded.toast_position,
608
+ page_sizes_json = excluded.page_sizes_json, updated_at = excluded.updated_at`, toastPosition, JSON.stringify(pageSizes), timestamp);
582
609
  this.audit(PLATFORM_AI_WORK_ID, "platform.ui-settings.updated", "platform-ui-settings", "platform-ui-settings", {
583
- toastPosition: input.toastPosition
610
+ toastPosition,
611
+ pageSizes
584
612
  });
585
613
  });
586
614
  return this.getPlatformUiSettings();
@@ -3605,8 +3633,21 @@ export class Store {
3605
3633
  this.getWork(workId);
3606
3634
  const taskId = id("task");
3607
3635
  const timestamp = now();
3608
- const scope = input.scope ?? { type: "book" };
3636
+ const scope = { ...(input.scope ?? { type: "book" }) };
3609
3637
  const sourceVersions = {};
3638
+ const targetCharacters = [];
3639
+ if (Array.isArray(scope.characterIds)) {
3640
+ for (const characterId of scope.characterIds) {
3641
+ if (typeof characterId !== "string")
3642
+ throw new AppError(400, "CHARACTER_REQUIRED", "被分析角色标识无效");
3643
+ const character = this.getCharacter(characterId);
3644
+ if (character.workId !== workId)
3645
+ throw new AppError(400, "CHARACTER_WORK_MISMATCH", "被分析角色不属于当前作品");
3646
+ targetCharacters.push({ id: characterId, name: String(character.name) });
3647
+ }
3648
+ if (targetCharacters.length > 0)
3649
+ scope.targetCharacters = targetCharacters;
3650
+ }
3610
3651
  if (typeof scope.chapterId === "string") {
3611
3652
  const chapter = this.getChapter(scope.chapterId);
3612
3653
  if (chapter.workId !== workId)
@@ -3631,20 +3672,16 @@ export class Store {
3631
3672
  this.notifyAnalysisTaskQueued(workId);
3632
3673
  return this.getTask(taskId);
3633
3674
  }
3634
- listTasks(workId) {
3635
- this.getWork(workId);
3636
- return this.db.all("SELECT * FROM analysis_tasks WHERE work_id = ? ORDER BY created_at DESC, id DESC", workId).map((row) => this.mapTask(row));
3637
- }
3638
- listTasksPage(workId, pagination) {
3639
- this.getWork(workId);
3640
- const page = paginationSql(pagination);
3641
- const rows = this.db.all(`SELECT * FROM analysis_tasks WHERE work_id = ? ORDER BY created_at DESC, id DESC${page.sql}`, workId, ...page.params);
3642
- return paginated(rows.map((row) => this.mapTask(row)), pagination);
3643
- }
3644
3675
  listTaskSummariesPage(workId, pagination) {
3645
3676
  this.getWork(workId);
3677
+ const statsRow = this.db.get(`SELECT COUNT(*) AS total,
3678
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_count,
3679
+ SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running_count,
3680
+ AVG(CASE WHEN status = 'running' THEN progress ELSE NULL END) AS running_progress
3681
+ FROM analysis_tasks WHERE work_id = ?`, workId) ?? {};
3682
+ const total = numberValue(statsRow, "total");
3646
3683
  const page = paginationSql(pagination);
3647
- const rows = this.db.all(`SELECT id, work_id, task_type, scope_json, status, progress, created_at, updated_at
3684
+ const rows = this.db.all(`SELECT id, task_type, scope_json, status, progress, created_at, updated_at
3648
3685
  FROM analysis_tasks WHERE work_id = ? ORDER BY created_at DESC, id DESC${page.sql}`, workId, ...page.params);
3649
3686
  const chapterSummaries = new Map(this.db.all(`SELECT chapter.id, chapter.title, volume.title AS volume_title
3650
3687
  FROM chapters chapter JOIN volumes volume ON volume.id = chapter.volume_id
@@ -3653,7 +3690,16 @@ export class Store {
3653
3690
  `${requiredString(row, "volume_title")} · ${requiredString(row, "title")}`
3654
3691
  ]));
3655
3692
  const volumeTitles = new Map(this.db.all("SELECT id, title FROM volumes WHERE work_id = ?", workId).map((row) => [requiredString(row, "id"), requiredString(row, "title")]));
3656
- return paginated(rows.map((row) => this.mapTaskSummary(row, chapterSummaries, volumeTitles)), pagination);
3693
+ const characterNames = this.taskCharacterNames(workId, rows.map((row) => json(requiredString(row, "scope_json"), {})));
3694
+ return {
3695
+ ...paginated(rows.map((row) => this.mapTaskSummary(row, chapterSummaries, volumeTitles, characterNames)), pagination, total),
3696
+ stats: {
3697
+ total,
3698
+ pendingCount: numberValue(statsRow, "pending_count"),
3699
+ runningCount: numberValue(statsRow, "running_count"),
3700
+ runningProgress: numberValue(statsRow, "running_progress")
3701
+ }
3702
+ };
3657
3703
  }
3658
3704
  getTask(taskId) {
3659
3705
  const row = this.db.get("SELECT * FROM analysis_tasks WHERE id = ?", taskId);
@@ -3728,12 +3774,14 @@ export class Store {
3728
3774
  mapTask(row) {
3729
3775
  const workId = requiredString(row, "work_id");
3730
3776
  const scope = json(requiredString(row, "scope_json"), {});
3777
+ const characterNames = this.taskCharacterNames(workId, [scope]);
3731
3778
  return {
3732
3779
  id: requiredString(row, "id"),
3733
3780
  workId,
3734
3781
  taskType: requiredString(row, "task_type"),
3735
3782
  scope,
3736
- scopeSummary: this.taskScopeSummary(workId, scope),
3783
+ scopeSummary: this.taskScopeSummary(workId, scope, characterNames),
3784
+ scopeSummaryWithoutCharacterNames: this.taskScopeSummary(workId, scope, new Map(), false),
3737
3785
  scopeDetails: this.taskScopeDetails(workId, scope),
3738
3786
  status: requiredString(row, "status"),
3739
3787
  progress: numberValue(row, "progress"),
@@ -3744,51 +3792,96 @@ export class Store {
3744
3792
  updatedAt: requiredString(row, "updated_at")
3745
3793
  };
3746
3794
  }
3747
- mapTaskSummary(row, chapterSummaries, volumeTitles) {
3795
+ mapTaskSummary(row, chapterSummaries, volumeTitles, characterNames) {
3748
3796
  const scope = json(requiredString(row, "scope_json"), {});
3749
3797
  return {
3750
3798
  id: requiredString(row, "id"),
3751
- workId: requiredString(row, "work_id"),
3752
3799
  taskType: requiredString(row, "task_type"),
3753
- scope,
3754
- scopeSummary: this.taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles),
3800
+ scopeSummary: this.taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, characterNames),
3801
+ scopeSummaryWithoutCharacterNames: this.taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, new Map(), false),
3755
3802
  status: requiredString(row, "status"),
3756
3803
  progress: numberValue(row, "progress"),
3757
3804
  createdAt: requiredString(row, "created_at"),
3758
3805
  updatedAt: requiredString(row, "updated_at")
3759
3806
  };
3760
3807
  }
3761
- taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles) {
3808
+ taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, characterNames, includeCharacterNames = true) {
3809
+ const targetedSuffix = this.taskTargetedSuffix(scope, characterNames, includeCharacterNames);
3762
3810
  if (typeof scope.chapterId === "string")
3763
- return chapterSummaries.get(scope.chapterId) ?? "章节已删除";
3811
+ return `${chapterSummaries.get(scope.chapterId) ?? "章节已删除"}${targetedSuffix}`;
3764
3812
  if (scope.type === "volume" && typeof scope.volumeId === "string") {
3765
3813
  const title = volumeTitles.get(scope.volumeId);
3766
- return title ? `分卷 · ${title}` : "分卷已删除";
3814
+ return `${title ? `分卷 · ${title}` : "分卷已删除"}${targetedSuffix}`;
3767
3815
  }
3768
3816
  if (scope.type === "book" || Object.keys(scope).length === 0)
3769
- return "全书";
3817
+ return `${scope.includeAllSettings === true ? "全书 + 所有设定" : "全书"}${targetedSuffix}`;
3770
3818
  return "未指定范围";
3771
3819
  }
3772
- taskScopeSummary(workId, scope) {
3820
+ taskScopeSummary(workId, scope, characterNames, includeCharacterNames = true) {
3821
+ const targetedSuffix = this.taskTargetedSuffix(scope, characterNames, includeCharacterNames);
3773
3822
  if (typeof scope.chapterId === "string") {
3774
3823
  const chapter = this.db.get(`SELECT chapter.title AS title, volume.title AS volume_title
3775
3824
  FROM chapters chapter
3776
3825
  JOIN volumes volume ON volume.id = chapter.volume_id
3777
3826
  WHERE chapter.id = ? AND chapter.work_id = ?`, scope.chapterId, workId);
3778
3827
  if (!chapter)
3779
- return "章节已删除";
3828
+ return `章节已删除${targetedSuffix}`;
3780
3829
  const title = requiredString(chapter, "title");
3781
3830
  const volumeTitle = requiredString(chapter, "volume_title");
3782
- return `${volumeTitle} · ${title}`;
3831
+ return `${volumeTitle} · ${title}${targetedSuffix}`;
3783
3832
  }
3784
3833
  if (scope.type === "volume" && typeof scope.volumeId === "string") {
3785
3834
  const volume = this.db.get("SELECT title FROM volumes WHERE id = ? AND work_id = ?", scope.volumeId, workId);
3786
- return volume ? `分卷 · ${requiredString(volume, "title")}` : "分卷已删除";
3835
+ return `${volume ? `分卷 · ${requiredString(volume, "title")}` : "分卷已删除"}${targetedSuffix}`;
3787
3836
  }
3788
3837
  if (scope.type === "book" || Object.keys(scope).length === 0)
3789
- return "全书";
3838
+ return `${scope.includeAllSettings === true ? "全书 + 所有设定" : "全书"}${targetedSuffix}`;
3790
3839
  return "未指定范围";
3791
3840
  }
3841
+ taskCharacterNames(workId, scopes) {
3842
+ const characterIds = [...new Set(scopes.flatMap((scope) => {
3843
+ const snapshotNames = this.taskCharacterSnapshotNames(scope);
3844
+ return Array.isArray(scope.characterIds)
3845
+ ? scope.characterIds.filter((characterId) => typeof characterId === "string" && !snapshotNames.has(characterId))
3846
+ : [];
3847
+ }))];
3848
+ const characterNames = new Map();
3849
+ for (let offset = 0; offset < characterIds.length; offset += 400) {
3850
+ const batch = characterIds.slice(offset, offset + 400);
3851
+ const placeholders = batch.map(() => "?").join(", ");
3852
+ for (const row of this.db.all(`SELECT id, name FROM characters WHERE work_id = ? AND id IN (${placeholders})`, workId, ...batch)) {
3853
+ characterNames.set(requiredString(row, "id"), requiredString(row, "name"));
3854
+ }
3855
+ }
3856
+ return characterNames;
3857
+ }
3858
+ taskTargetedSuffix(scope, characterNames, includeCharacterNames = true) {
3859
+ const characterIds = Array.isArray(scope.characterIds)
3860
+ ? scope.characterIds.filter((characterId) => typeof characterId === "string")
3861
+ : [];
3862
+ if (characterIds.length === 0)
3863
+ return "";
3864
+ const overwriteSuffix = scope.replaceExistingRelationships === true ? " · 覆盖已有关系" : "";
3865
+ if (!includeCharacterNames)
3866
+ return ` · 定向 ${characterIds.length} 人${overwriteSuffix}`;
3867
+ const snapshotNames = this.taskCharacterSnapshotNames(scope);
3868
+ const names = characterIds.map((characterId) => snapshotNames.get(characterId) ?? characterNames.get(characterId) ?? "已删除角色");
3869
+ return ` · 定向 ${characterIds.length} 人:${names.join("、")}${overwriteSuffix}`;
3870
+ }
3871
+ taskCharacterSnapshotNames(scope) {
3872
+ const snapshotNames = new Map();
3873
+ if (Array.isArray(scope.targetCharacters)) {
3874
+ for (const item of scope.targetCharacters) {
3875
+ if (!item || typeof item !== "object" || Array.isArray(item))
3876
+ continue;
3877
+ const target = item;
3878
+ if (typeof target.id === "string" && typeof target.name === "string" && target.name.trim()) {
3879
+ snapshotNames.set(target.id, target.name);
3880
+ }
3881
+ }
3882
+ }
3883
+ return snapshotNames;
3884
+ }
3792
3885
  taskScopeDetails(workId, scope) {
3793
3886
  if (typeof scope.chapterId === "string") {
3794
3887
  const chapter = this.db.get(`SELECT chapter.id AS id, chapter.title AS title, chapter.version_no AS version_no,