@musnows/scriverse 0.5.7 → 0.5.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/store.js CHANGED
@@ -660,6 +660,12 @@ export class Store {
660
660
  autoRunEnabled: Number(row?.auto_run_enabled ?? 0) === 1,
661
661
  autoRunConcurrency: Math.min(8, Math.max(1, Number(row?.auto_run_concurrency ?? 2) || 2)),
662
662
  autoRunBatchLimit: Math.min(200, Math.max(1, Number(row?.auto_run_batch_limit ?? 20) || 20)),
663
+ autoRunDailyTaskLimit: Math.min(10_000, Math.max(0, Number(row?.auto_run_daily_task_limit ?? 0) || 0)),
664
+ autoRunFailureThreshold: Math.min(10, Math.max(1, Number(row?.auto_run_failure_threshold ?? 3) || 3)),
665
+ autoRunPaused: Number(row?.auto_run_paused ?? 0) === 1,
666
+ autoRunPauseReason: String(row?.auto_run_pause_reason ?? ""),
667
+ autoRunResumeAt: row?.auto_run_resume_at === null || row?.auto_run_resume_at === undefined ? null : String(row.auto_run_resume_at),
668
+ autoRunConsecutiveFailures: Math.max(0, Number(row?.auto_run_consecutive_failures ?? 0) || 0),
663
669
  bookSummaryContextPercent: Math.min(90, Math.max(1, Number(row?.book_summary_context_percent ?? 50) || 50)),
664
670
  contextCompactThreshold: Math.min(90, Math.max(50, Number(row?.context_compact_threshold ?? 85) || 85)),
665
671
  agentTools: json(String(row?.agent_tools_json ?? '["story_index","read_chapters","search_story_entities","grep","read_character_sections"]'), ["story_index", "read_chapters", "search_story_entities", "grep", "read_character_sections"])
@@ -676,32 +682,92 @@ export class Store {
676
682
  const nextEnabled = input.autoRunEnabled ?? Boolean(current.autoRunEnabled);
677
683
  const nextConcurrency = input.autoRunConcurrency ?? Number(current.autoRunConcurrency);
678
684
  const nextBatchLimit = input.autoRunBatchLimit ?? Number(current.autoRunBatchLimit);
685
+ const nextDailyTaskLimit = input.autoRunDailyTaskLimit ?? Number(current.autoRunDailyTaskLimit);
686
+ const nextFailureThreshold = input.autoRunFailureThreshold ?? Number(current.autoRunFailureThreshold);
679
687
  const nextBookSummaryContextPercent = input.bookSummaryContextPercent ?? Number(current.bookSummaryContextPercent);
680
688
  const nextContextCompactThreshold = input.contextCompactThreshold ?? Number(current.contextCompactThreshold);
681
689
  const nextAgentTools = input.agentTools ?? current.agentTools;
682
690
  this.db.run(`INSERT INTO work_ai_settings (
683
- work_id, system_prompt, auto_run_enabled, auto_run_concurrency, auto_run_batch_limit, book_summary_context_percent, context_compact_threshold, agent_tools_json, updated_at
684
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
691
+ work_id, system_prompt, auto_run_enabled, auto_run_concurrency, auto_run_batch_limit,
692
+ auto_run_daily_task_limit, auto_run_failure_threshold, auto_run_paused, auto_run_pause_reason,
693
+ auto_run_resume_at, auto_run_consecutive_failures, book_summary_context_percent,
694
+ context_compact_threshold, agent_tools_json, updated_at
695
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
685
696
  ON CONFLICT(work_id) DO UPDATE SET
686
697
  system_prompt = excluded.system_prompt,
687
698
  auto_run_enabled = excluded.auto_run_enabled,
688
699
  auto_run_concurrency = excluded.auto_run_concurrency,
689
700
  auto_run_batch_limit = excluded.auto_run_batch_limit,
701
+ auto_run_daily_task_limit = excluded.auto_run_daily_task_limit,
702
+ auto_run_failure_threshold = excluded.auto_run_failure_threshold,
703
+ auto_run_paused = excluded.auto_run_paused,
704
+ auto_run_pause_reason = excluded.auto_run_pause_reason,
705
+ auto_run_resume_at = excluded.auto_run_resume_at,
706
+ auto_run_consecutive_failures = excluded.auto_run_consecutive_failures,
690
707
  book_summary_context_percent = excluded.book_summary_context_percent,
691
708
  context_compact_threshold = excluded.context_compact_threshold,
692
709
  agent_tools_json = excluded.agent_tools_json,
693
- updated_at = excluded.updated_at`, workId, nextPrompt, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), JSON.stringify(nextAgentTools), timestamp);
710
+ updated_at = excluded.updated_at`, workId, nextPrompt, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(10_000, Math.max(0, nextDailyTaskLimit)), Math.min(10, Math.max(1, nextFailureThreshold)), current.autoRunPaused ? 1 : 0, String(current.autoRunPauseReason ?? ""), current.autoRunResumeAt === null ? null : String(current.autoRunResumeAt), Math.max(0, Number(current.autoRunConsecutiveFailures) || 0), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), JSON.stringify(nextAgentTools), timestamp);
694
711
  this.audit(workId, "work.ai-settings.updated", "work-ai-settings", workId, {
695
712
  systemPromptChanged: input.systemPrompt !== undefined,
696
713
  autoRunEnabled: nextEnabled,
697
714
  autoRunConcurrency: Math.min(8, Math.max(1, nextConcurrency)),
698
715
  autoRunBatchLimit: Math.min(200, Math.max(1, nextBatchLimit)),
716
+ autoRunDailyTaskLimit: Math.min(10_000, Math.max(0, nextDailyTaskLimit)),
717
+ autoRunFailureThreshold: Math.min(10, Math.max(1, nextFailureThreshold)),
699
718
  bookSummaryContextPercent: Math.min(90, Math.max(1, nextBookSummaryContextPercent)),
700
719
  contextCompactThreshold: Math.min(90, Math.max(50, nextContextCompactThreshold)),
701
720
  agentTools: nextAgentTools
702
721
  });
703
722
  return this.getWorkAiSettings(workId);
704
723
  }
724
+ clearAutoRunPause(workId) {
725
+ this.getWork(workId);
726
+ const current = this.getWorkAiSettings(workId);
727
+ this.db.run(`UPDATE work_ai_settings
728
+ SET auto_run_paused = 0, auto_run_pause_reason = '', auto_run_resume_at = NULL,
729
+ auto_run_consecutive_failures = 0, updated_at = ?
730
+ WHERE work_id = ?`, now(), workId);
731
+ if (current.autoRunPaused) {
732
+ this.audit(workId, "task.auto-run.resumed", "work-ai-settings", workId, {
733
+ previousReason: current.autoRunPauseReason
734
+ });
735
+ }
736
+ return this.getWorkAiSettings(workId);
737
+ }
738
+ pauseAutoRun(workId, reason, resumeAt = null) {
739
+ this.getWork(workId);
740
+ this.db.run(`UPDATE work_ai_settings
741
+ SET auto_run_paused = 1, auto_run_pause_reason = ?, auto_run_resume_at = ?, updated_at = ?
742
+ WHERE work_id = ?`, reason.slice(0, 500), resumeAt, now(), workId);
743
+ this.audit(workId, "task.auto-run.paused", "work-ai-settings", workId, { reason, resumeAt });
744
+ return this.getWorkAiSettings(workId);
745
+ }
746
+ recordAutoRunSuccess(workId) {
747
+ this.getWork(workId);
748
+ this.db.run("UPDATE work_ai_settings SET auto_run_consecutive_failures = 0, updated_at = ? WHERE work_id = ?", now(), workId);
749
+ return this.getWorkAiSettings(workId);
750
+ }
751
+ recordAutoRunFailure(workId, message, pauseImmediately = false) {
752
+ return this.db.transaction(() => {
753
+ const current = this.getWorkAiSettings(workId);
754
+ const consecutiveFailures = Number(current.autoRunConsecutiveFailures) + 1;
755
+ const shouldPause = pauseImmediately || consecutiveFailures >= Number(current.autoRunFailureThreshold);
756
+ this.db.run(`UPDATE work_ai_settings
757
+ SET auto_run_consecutive_failures = ?, auto_run_paused = ?,
758
+ auto_run_pause_reason = CASE WHEN ? = 1 THEN ? ELSE auto_run_pause_reason END,
759
+ auto_run_resume_at = CASE WHEN ? = 1 THEN NULL ELSE auto_run_resume_at END,
760
+ updated_at = ? WHERE work_id = ?`, consecutiveFailures, shouldPause ? 1 : 0, shouldPause ? 1 : 0, `连续任务失败,自动执行已暂停:${message}`.slice(0, 500), shouldPause ? 1 : 0, now(), workId);
761
+ if (shouldPause) {
762
+ this.audit(workId, "task.auto-run.paused", "work-ai-settings", workId, {
763
+ reason: "consecutive-failures",
764
+ consecutiveFailures,
765
+ message
766
+ });
767
+ }
768
+ return this.getWorkAiSettings(workId);
769
+ });
770
+ }
705
771
  updateWork(workId, input, expectedVersionNo, source = "manual", sourceRef = null, changeNote = "") {
706
772
  this.db.transaction(() => {
707
773
  const current = this.getWork(workId);
@@ -1027,7 +1093,7 @@ export class Store {
1027
1093
  throw new AppError(409, "VOLUME_NOT_EMPTY", "卷内仍有章节,需先移动或删除章节");
1028
1094
  }
1029
1095
  if (numberValue(counts ?? {}, "deleted_count") > 0) {
1030
- throw new AppError(409, "VOLUME_HAS_DELETED_CHAPTERS", "分卷回收站中仍有章节,请先恢复并移动这些章节后再删除分卷");
1096
+ throw new AppError(409, "VOLUME_HAS_DELETED_CHAPTERS", "分卷回收站中仍有章节,请先彻底删除或恢复并移动这些章节");
1031
1097
  }
1032
1098
  this.recordEntityVersion("volume", volumeId, "delete", null, "删除分卷");
1033
1099
  this.db.run("DELETE FROM volumes WHERE id = ?", volumeId);
@@ -1620,6 +1686,35 @@ export class Store {
1620
1686
  this.audit(String(chapter.workId), "chapter.deleted", "chapter", chapterId, { versionNo });
1621
1687
  });
1622
1688
  }
1689
+ permanentlyDeleteChapter(chapterId, expectedVersionNo) {
1690
+ const chapter = this.db.get("SELECT * FROM chapters WHERE id = ?", chapterId);
1691
+ if (!chapter)
1692
+ throw notFound("章节");
1693
+ if (!chapter.deleted_at)
1694
+ throw new AppError(409, "CHAPTER_NOT_IN_RECYCLE_BIN", "仅回收站中的章节可以彻底删除");
1695
+ this.assertExpectedRevision("chapter", chapterId, expectedVersionNo, "章节", numberValue(chapter, "version_no"));
1696
+ const workId = requiredString(chapter, "work_id");
1697
+ const timestamp = now();
1698
+ this.db.transaction(() => {
1699
+ const locked = this.db.get("SELECT * FROM chapters WHERE id = ?", chapterId);
1700
+ if (!locked)
1701
+ throw notFound("章节");
1702
+ if (!locked.deleted_at)
1703
+ throw new AppError(409, "CHAPTER_NOT_IN_RECYCLE_BIN", "仅回收站中的章节可以彻底删除");
1704
+ this.assertExpectedRevision("chapter", chapterId, expectedVersionNo, "章节", numberValue(locked, "version_no"));
1705
+ this.db.run("DELETE FROM chapter_versions WHERE chapter_id = ?", chapterId);
1706
+ this.db.run("DELETE FROM entity_versions WHERE entity_type = 'chapter-outline' AND entity_id = ?", chapterId);
1707
+ this.db.run("DELETE FROM attachment_references WHERE entity_type = 'chapter' AND entity_id = ?", chapterId);
1708
+ this.db.run("DELETE FROM chapters WHERE id = ?", chapterId);
1709
+ this.db.run("UPDATE works SET updated_at = ? WHERE id = ?", timestamp, workId);
1710
+ this.audit(workId, "chapter.purged", "chapter", chapterId, {
1711
+ title: requiredString(locked, "title"),
1712
+ volumeId: requiredString(locked, "volume_id"),
1713
+ versionNo: numberValue(locked, "version_no"),
1714
+ recoverable: false
1715
+ });
1716
+ });
1717
+ }
1623
1718
  insertChapter(workId, volumeId, title, content, sortOrder, source, sourceRef, chapterType = "正文") {
1624
1719
  const chapterId = id("chapter");
1625
1720
  const timestamp = now();
@@ -2179,6 +2274,19 @@ export class Store {
2179
2274
  this.getWork(workId);
2180
2275
  return this.db.all("SELECT * FROM races WHERE work_id = ? ORDER BY name", workId).map((row) => this.mapRace(row, includeMarkdown));
2181
2276
  }
2277
+ listRacesByHierarchyScope(workId, scope, includeMarkdown = true) {
2278
+ this.getWork(workId);
2279
+ const hierarchyCondition = scope === "roots" ? "IS NULL" : "IS NOT NULL";
2280
+ return this.db.all(`SELECT race.*,
2281
+ (SELECT COUNT(*) FROM races child WHERE child.parent_race_id = race.id) AS child_count
2282
+ FROM races race
2283
+ WHERE race.work_id = ? AND race.parent_race_id ${hierarchyCondition}
2284
+ ORDER BY race.name`, workId).map((row) => this.mapRace(row, includeMarkdown));
2285
+ }
2286
+ countRaces(workId) {
2287
+ this.getWork(workId);
2288
+ return numberValue(this.db.get("SELECT COUNT(*) AS count FROM races WHERE work_id = ?", workId) ?? {}, "count");
2289
+ }
2182
2290
  listRacesPage(workId, pagination, includeMarkdown = true) {
2183
2291
  this.getWork(workId);
2184
2292
  const page = paginationSql(pagination);
@@ -2310,6 +2418,7 @@ export class Store {
2310
2418
  parentRaceId: optionalString(row, "parent_race_id"),
2311
2419
  name: requiredString(row, "name"),
2312
2420
  description: requiredString(row, "description"),
2421
+ ...(row.child_count === undefined ? {} : { childCount: numberValue(row, "child_count") }),
2313
2422
  ...(includeMarkdown
2314
2423
  ? { settings, settingsMarkdown: settingsMarkdownFromList(settings), settingsSections }
2315
2424
  : { settings: [], settingsCount: settingsSections.length }),
@@ -4100,7 +4209,8 @@ export class Store {
4100
4209
  FROM analysis_tasks WHERE work_id = ?`, workId) ?? {};
4101
4210
  const total = numberValue(statsRow, "total");
4102
4211
  const page = paginationSql(pagination);
4103
- const rows = this.db.all(`SELECT task.id, task.model_id, task.task_type, task.scope_json, task.status, task.progress, task.created_at, task.updated_at,
4212
+ const rows = this.db.all(`SELECT task.id, task.model_id, task.task_type, task.scope_json, task.status, task.progress,
4213
+ task.attempt_count, task.next_attempt_at, task.last_attempt_at, task.created_at, task.updated_at,
4104
4214
  model.display_name AS model_display_name, model.model_id AS model_api_id
4105
4215
  FROM analysis_tasks task
4106
4216
  LEFT JOIN models model ON model.id = task.model_id
@@ -4136,11 +4246,56 @@ export class Store {
4136
4246
  const row = this.db.get("SELECT COUNT(*) AS value FROM analysis_tasks WHERE work_id = ? AND status = 'running'", workId);
4137
4247
  return numberValue(row ?? {}, "value");
4138
4248
  }
4249
+ listAutoRunWorkIds() {
4250
+ return this.db.all("SELECT work_id FROM work_ai_settings WHERE auto_run_enabled = 1 ORDER BY work_id").map((row) => requiredString(row, "work_id"));
4251
+ }
4252
+ claimPendingTask(taskId, runningLimit) {
4253
+ return this.db.transaction(() => {
4254
+ const current = this.getTask(taskId);
4255
+ if (current.status !== "pending")
4256
+ return null;
4257
+ if (current.nextAttemptAt && String(current.nextAttemptAt) > now())
4258
+ return null;
4259
+ if (runningLimit !== undefined && this.countRunningTasks(String(current.workId)) >= runningLimit)
4260
+ return null;
4261
+ const timestamp = now();
4262
+ const claimed = this.db.run(`UPDATE analysis_tasks
4263
+ SET status = 'running', progress = 5, attempt_count = attempt_count + 1,
4264
+ next_attempt_at = NULL, last_attempt_at = ?, updated_at = ?
4265
+ WHERE id = ? AND status = 'pending' AND (next_attempt_at IS NULL OR next_attempt_at <= ?)`, timestamp, timestamp, taskId, timestamp);
4266
+ return claimed.changes === 1 ? this.getTask(taskId) : null;
4267
+ });
4268
+ }
4139
4269
  listOldestPendingTaskIds(workId, limit) {
4140
4270
  if (limit <= 0)
4141
4271
  return [];
4142
4272
  return this.db.all(`SELECT id FROM analysis_tasks WHERE work_id = ? AND status = 'pending'
4143
- ORDER BY created_at ASC, id ASC LIMIT ?`, workId, limit).map((row) => requiredString(row, "id"));
4273
+ AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
4274
+ ORDER BY created_at ASC, id ASC LIMIT ?`, workId, now(), limit).map((row) => requiredString(row, "id"));
4275
+ }
4276
+ nextPendingTaskAttemptAt(workId) {
4277
+ const row = this.db.get(`SELECT MIN(next_attempt_at) AS value FROM analysis_tasks
4278
+ WHERE work_id = ? AND status = 'pending' AND next_attempt_at IS NOT NULL`, workId);
4279
+ return row?.value === null || row?.value === undefined ? null : String(row.value);
4280
+ }
4281
+ countAutoRunAttemptsToday(workId) {
4282
+ const row = this.db.get(`SELECT COUNT(*) AS value FROM analysis_tasks
4283
+ WHERE work_id = ? AND last_attempt_at >= strftime('%Y-%m-%dT00:00:00.000Z', 'now')`, workId);
4284
+ return numberValue(row ?? {}, "value");
4285
+ }
4286
+ rescheduleTask(taskId, failure, nextAttemptAt) {
4287
+ const current = this.getTask(taskId);
4288
+ if (current.status !== "running")
4289
+ return current;
4290
+ const failures = Array.isArray(current.failures) ? [...current.failures, failure].slice(-10) : [failure];
4291
+ this.db.run(`UPDATE analysis_tasks
4292
+ SET status = 'pending', progress = 0, failure_json = ?, next_attempt_at = ?, updated_at = ?
4293
+ WHERE id = ? AND status = 'running'`, JSON.stringify(failures), nextAttemptAt, now(), taskId);
4294
+ this.audit(String(current.workId), "task.retry-scheduled", "analysis-task", taskId, {
4295
+ attemptCount: current.attemptCount,
4296
+ nextAttemptAt
4297
+ });
4298
+ return this.getTask(taskId);
4144
4299
  }
4145
4300
  countPendingTasks(workId) {
4146
4301
  const row = this.db.get("SELECT COUNT(*) AS value FROM analysis_tasks WHERE work_id = ? AND status = 'pending'", workId);
@@ -5030,6 +5185,9 @@ export class Store {
5030
5185
  result: taskResult,
5031
5186
  failures: json(requiredString(row, "failure_json"), []),
5032
5187
  sourceVersions: json(requiredString(row, "source_versions_json"), {}),
5188
+ attemptCount: numberValue(row, "attempt_count"),
5189
+ nextAttemptAt: row.next_attempt_at === null || row.next_attempt_at === undefined ? null : String(row.next_attempt_at),
5190
+ lastAttemptAt: row.last_attempt_at === null || row.last_attempt_at === undefined ? null : String(row.last_attempt_at),
5033
5191
  createdAt: requiredString(row, "created_at"),
5034
5192
  updatedAt: requiredString(row, "updated_at")
5035
5193
  };
@@ -5044,6 +5202,9 @@ export class Store {
5044
5202
  scopeSummaryWithoutCharacterNames: this.taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, new Map(), false),
5045
5203
  status: requiredString(row, "status"),
5046
5204
  progress: numberValue(row, "progress"),
5205
+ attemptCount: numberValue(row, "attempt_count"),
5206
+ nextAttemptAt: row.next_attempt_at === null || row.next_attempt_at === undefined ? null : String(row.next_attempt_at),
5207
+ lastAttemptAt: row.last_attempt_at === null || row.last_attempt_at === undefined ? null : String(row.last_attempt_at),
5047
5208
  createdAt: requiredString(row, "created_at"),
5048
5209
  updatedAt: requiredString(row, "updated_at")
5049
5210
  };