@musnows/scriverse 0.4.12 → 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,9 @@ 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 = [];
3610
3639
  if (Array.isArray(scope.characterIds)) {
3611
3640
  for (const characterId of scope.characterIds) {
3612
3641
  if (typeof characterId !== "string")
@@ -3614,7 +3643,10 @@ export class Store {
3614
3643
  const character = this.getCharacter(characterId);
3615
3644
  if (character.workId !== workId)
3616
3645
  throw new AppError(400, "CHARACTER_WORK_MISMATCH", "被分析角色不属于当前作品");
3646
+ targetCharacters.push({ id: characterId, name: String(character.name) });
3617
3647
  }
3648
+ if (targetCharacters.length > 0)
3649
+ scope.targetCharacters = targetCharacters;
3618
3650
  }
3619
3651
  if (typeof scope.chapterId === "string") {
3620
3652
  const chapter = this.getChapter(scope.chapterId);
@@ -3640,20 +3672,16 @@ export class Store {
3640
3672
  this.notifyAnalysisTaskQueued(workId);
3641
3673
  return this.getTask(taskId);
3642
3674
  }
3643
- listTasks(workId) {
3644
- this.getWork(workId);
3645
- return this.db.all("SELECT * FROM analysis_tasks WHERE work_id = ? ORDER BY created_at DESC, id DESC", workId).map((row) => this.mapTask(row));
3646
- }
3647
- listTasksPage(workId, pagination) {
3648
- this.getWork(workId);
3649
- const page = paginationSql(pagination);
3650
- const rows = this.db.all(`SELECT * FROM analysis_tasks WHERE work_id = ? ORDER BY created_at DESC, id DESC${page.sql}`, workId, ...page.params);
3651
- return paginated(rows.map((row) => this.mapTask(row)), pagination);
3652
- }
3653
3675
  listTaskSummariesPage(workId, pagination) {
3654
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");
3655
3683
  const page = paginationSql(pagination);
3656
- 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
3657
3685
  FROM analysis_tasks WHERE work_id = ? ORDER BY created_at DESC, id DESC${page.sql}`, workId, ...page.params);
3658
3686
  const chapterSummaries = new Map(this.db.all(`SELECT chapter.id, chapter.title, volume.title AS volume_title
3659
3687
  FROM chapters chapter JOIN volumes volume ON volume.id = chapter.volume_id
@@ -3662,7 +3690,16 @@ export class Store {
3662
3690
  `${requiredString(row, "volume_title")} · ${requiredString(row, "title")}`
3663
3691
  ]));
3664
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")]));
3665
- 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
+ };
3666
3703
  }
3667
3704
  getTask(taskId) {
3668
3705
  const row = this.db.get("SELECT * FROM analysis_tasks WHERE id = ?", taskId);
@@ -3737,12 +3774,14 @@ export class Store {
3737
3774
  mapTask(row) {
3738
3775
  const workId = requiredString(row, "work_id");
3739
3776
  const scope = json(requiredString(row, "scope_json"), {});
3777
+ const characterNames = this.taskCharacterNames(workId, [scope]);
3740
3778
  return {
3741
3779
  id: requiredString(row, "id"),
3742
3780
  workId,
3743
3781
  taskType: requiredString(row, "task_type"),
3744
3782
  scope,
3745
- scopeSummary: this.taskScopeSummary(workId, scope),
3783
+ scopeSummary: this.taskScopeSummary(workId, scope, characterNames),
3784
+ scopeSummaryWithoutCharacterNames: this.taskScopeSummary(workId, scope, new Map(), false),
3746
3785
  scopeDetails: this.taskScopeDetails(workId, scope),
3747
3786
  status: requiredString(row, "status"),
3748
3787
  progress: numberValue(row, "progress"),
@@ -3753,57 +3792,96 @@ export class Store {
3753
3792
  updatedAt: requiredString(row, "updated_at")
3754
3793
  };
3755
3794
  }
3756
- mapTaskSummary(row, chapterSummaries, volumeTitles) {
3795
+ mapTaskSummary(row, chapterSummaries, volumeTitles, characterNames) {
3757
3796
  const scope = json(requiredString(row, "scope_json"), {});
3758
3797
  return {
3759
3798
  id: requiredString(row, "id"),
3760
- workId: requiredString(row, "work_id"),
3761
3799
  taskType: requiredString(row, "task_type"),
3762
- scope,
3763
- scopeSummary: this.taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles),
3800
+ scopeSummary: this.taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, characterNames),
3801
+ scopeSummaryWithoutCharacterNames: this.taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, new Map(), false),
3764
3802
  status: requiredString(row, "status"),
3765
3803
  progress: numberValue(row, "progress"),
3766
3804
  createdAt: requiredString(row, "created_at"),
3767
3805
  updatedAt: requiredString(row, "updated_at")
3768
3806
  };
3769
3807
  }
3770
- taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles) {
3771
- const targetedSuffix = Array.isArray(scope.characterIds) && scope.characterIds.length
3772
- ? ` · 定向 ${scope.characterIds.length} 人${scope.replaceExistingRelationships === true ? " · 覆盖已有关系" : ""}`
3773
- : "";
3808
+ taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, characterNames, includeCharacterNames = true) {
3809
+ const targetedSuffix = this.taskTargetedSuffix(scope, characterNames, includeCharacterNames);
3774
3810
  if (typeof scope.chapterId === "string")
3775
3811
  return `${chapterSummaries.get(scope.chapterId) ?? "章节已删除"}${targetedSuffix}`;
3776
3812
  if (scope.type === "volume" && typeof scope.volumeId === "string") {
3777
3813
  const title = volumeTitles.get(scope.volumeId);
3778
- return title ? `分卷 · ${title}${targetedSuffix}` : "分卷已删除";
3814
+ return `${title ? `分卷 · ${title}` : "分卷已删除"}${targetedSuffix}`;
3779
3815
  }
3780
3816
  if (scope.type === "book" || Object.keys(scope).length === 0)
3781
3817
  return `${scope.includeAllSettings === true ? "全书 + 所有设定" : "全书"}${targetedSuffix}`;
3782
3818
  return "未指定范围";
3783
3819
  }
3784
- taskScopeSummary(workId, scope) {
3785
- const targetedSuffix = Array.isArray(scope.characterIds) && scope.characterIds.length
3786
- ? ` · 定向 ${scope.characterIds.length} 人${scope.replaceExistingRelationships === true ? " · 覆盖已有关系" : ""}`
3787
- : "";
3820
+ taskScopeSummary(workId, scope, characterNames, includeCharacterNames = true) {
3821
+ const targetedSuffix = this.taskTargetedSuffix(scope, characterNames, includeCharacterNames);
3788
3822
  if (typeof scope.chapterId === "string") {
3789
3823
  const chapter = this.db.get(`SELECT chapter.title AS title, volume.title AS volume_title
3790
3824
  FROM chapters chapter
3791
3825
  JOIN volumes volume ON volume.id = chapter.volume_id
3792
3826
  WHERE chapter.id = ? AND chapter.work_id = ?`, scope.chapterId, workId);
3793
3827
  if (!chapter)
3794
- return "章节已删除";
3828
+ return `章节已删除${targetedSuffix}`;
3795
3829
  const title = requiredString(chapter, "title");
3796
3830
  const volumeTitle = requiredString(chapter, "volume_title");
3797
3831
  return `${volumeTitle} · ${title}${targetedSuffix}`;
3798
3832
  }
3799
3833
  if (scope.type === "volume" && typeof scope.volumeId === "string") {
3800
3834
  const volume = this.db.get("SELECT title FROM volumes WHERE id = ? AND work_id = ?", scope.volumeId, workId);
3801
- return volume ? `分卷 · ${requiredString(volume, "title")}${targetedSuffix}` : "分卷已删除";
3835
+ return `${volume ? `分卷 · ${requiredString(volume, "title")}` : "分卷已删除"}${targetedSuffix}`;
3802
3836
  }
3803
3837
  if (scope.type === "book" || Object.keys(scope).length === 0)
3804
3838
  return `${scope.includeAllSettings === true ? "全书 + 所有设定" : "全书"}${targetedSuffix}`;
3805
3839
  return "未指定范围";
3806
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
+ }
3807
3885
  taskScopeDetails(workId, scope) {
3808
3886
  if (typeof scope.chapterId === "string") {
3809
3887
  const chapter = this.db.get(`SELECT chapter.id AS id, chapter.title AS title, chapter.version_no AS version_no,