@musnows/scriverse 0.6.9 → 0.6.11

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/database.js CHANGED
@@ -1,10 +1,10 @@
1
- import { chmodSync, existsSync, mkdirSync } from "node:fs";
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { logger, sanitizeError } from "./logger.js";
5
5
  import { documentShortSearchTerms, normalizeDocumentSearchText, splitDocumentParagraphs } from "./utils.js";
6
6
  export const PLATFORM_AI_WORK_ID = "__scriverse_platform_ai__";
7
- export const DATABASE_SCHEMA_VERSION = 74;
7
+ export const DATABASE_SCHEMA_VERSION = 76;
8
8
  export function readDatabaseSchemaVersion(filename) {
9
9
  if (!existsSync(filename))
10
10
  return null;
@@ -21,8 +21,10 @@ export function readDatabaseSchemaVersion(filename) {
21
21
  }
22
22
  }
23
23
  export class Database {
24
+ filename;
24
25
  raw;
25
26
  constructor(filename) {
27
+ this.filename = filename;
26
28
  logger.info("database.opening", { databasePath: filename, inMemory: filename === ":memory:" });
27
29
  try {
28
30
  if (filename !== ":memory:")
@@ -53,6 +55,18 @@ export class Database {
53
55
  this.raw.close();
54
56
  logger.info("database.closed");
55
57
  }
58
+ createSnapshotBuffer() {
59
+ if (this.filename === ":memory:")
60
+ throw new Error("内存数据库不能创建文件快照");
61
+ if (this.raw.isTransaction)
62
+ throw new Error("事务执行期间不能创建数据库快照");
63
+ // 项目只使用当前这一条同步连接;截断 WAL 后立即同步读取主库,期间不会穿插其他写入。
64
+ const checkpoint = this.get("PRAGMA wal_checkpoint(TRUNCATE)");
65
+ if (Number(checkpoint?.busy ?? 0) !== 0 || Number(checkpoint?.log ?? 0) !== Number(checkpoint?.checkpointed ?? 0)) {
66
+ throw new Error("数据库 WAL 尚未完整合并,无法创建一致性快照");
67
+ }
68
+ return readFileSync(this.filename);
69
+ }
56
70
  run(sql, ...params) {
57
71
  const result = this.raw.prepare(sql).run(...params);
58
72
  return { changes: Number(result.changes), lastInsertRowid: result.lastInsertRowid };
@@ -454,7 +468,7 @@ export class Database {
454
468
  auto_run_consecutive_failures INTEGER NOT NULL DEFAULT 0,
455
469
  book_summary_context_percent INTEGER NOT NULL DEFAULT 50 CHECK(book_summary_context_percent BETWEEN 1 AND 90),
456
470
  context_compact_threshold INTEGER NOT NULL DEFAULT 85 CHECK(context_compact_threshold BETWEEN 50 AND 90),
457
- agent_tool_call_limit INTEGER NOT NULL DEFAULT 12 CHECK(agent_tool_call_limit BETWEEN 5 AND 48),
471
+ agent_tool_call_limit INTEGER NOT NULL DEFAULT 12 CHECK(agent_tool_call_limit BETWEEN 5 AND 1000),
458
472
  agent_tool_call_global_multiplier INTEGER NOT NULL DEFAULT 3 CHECK(agent_tool_call_global_multiplier BETWEEN 1 AND 6),
459
473
  agent_tools_json TEXT NOT NULL DEFAULT '["story_index","read_chapters","search_story_entities","grep","read_character_sections","search_drafts","image"]',
460
474
  title_generation_model_id TEXT REFERENCES models(id) ON DELETE SET NULL,
@@ -2733,8 +2747,44 @@ export class Database {
2733
2747
  if (foreignKeys.length > 0)
2734
2748
  throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
2735
2749
  }
2736
- if (!applied.has(73)) {
2750
+ const s3TargetsPresent = this.all("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 's3_backup_targets'").length > 0;
2751
+ const modelColumnsAt73 = new Set(this.all("PRAGMA table_info(models)").map((row) => String(row.name)));
2752
+ const platformAiColumnsAt73 = new Set(this.all("PRAGMA table_info(platform_ai_settings)").map((row) => String(row.name)));
2753
+ const workAiColumnsAt73 = new Set(this.all("PRAGMA table_info(work_ai_settings)").map((row) => String(row.name)));
2754
+ const multimodalMigrationPresent = modelColumnsAt73.has("multimodal_enabled")
2755
+ && platformAiColumnsAt73.has("image_tool_model_id")
2756
+ && workAiColumnsAt73.has("image_tool_model_id");
2757
+ if (!applied.has(73) || !s3TargetsPresent || !multimodalMigrationPresent) {
2737
2758
  this.transaction(() => {
2759
+ this.run(`CREATE TABLE IF NOT EXISTS s3_backup_targets (
2760
+ id TEXT PRIMARY KEY,
2761
+ name TEXT NOT NULL,
2762
+ endpoint TEXT NOT NULL,
2763
+ region TEXT NOT NULL DEFAULT 'us-east-1',
2764
+ bucket TEXT NOT NULL,
2765
+ base_path TEXT NOT NULL DEFAULT '',
2766
+ access_key_encrypted TEXT NOT NULL,
2767
+ access_key_iv TEXT NOT NULL,
2768
+ access_key_tag TEXT NOT NULL,
2769
+ secret_key_encrypted TEXT NOT NULL,
2770
+ secret_key_iv TEXT NOT NULL,
2771
+ secret_key_tag TEXT NOT NULL,
2772
+ force_path_style INTEGER NOT NULL DEFAULT 1 CHECK(force_path_style IN (0, 1)),
2773
+ enabled INTEGER NOT NULL DEFAULT 0 CHECK(enabled IN (0, 1)),
2774
+ backup_images INTEGER NOT NULL DEFAULT 1 CHECK(backup_images IN (0, 1)),
2775
+ schedule_time TEXT NOT NULL DEFAULT '03:00' CHECK(
2776
+ schedule_time GLOB '[0-2][0-9]:[0-5][0-9]'
2777
+ AND substr(schedule_time, 1, 2) <= '23'
2778
+ ),
2779
+ retention_count INTEGER NOT NULL DEFAULT 7 CHECK(retention_count BETWEEN 1 AND 365),
2780
+ last_started_at TEXT,
2781
+ last_success_at TEXT,
2782
+ last_failure_at TEXT,
2783
+ last_error TEXT,
2784
+ created_at TEXT NOT NULL,
2785
+ updated_at TEXT NOT NULL
2786
+ )`);
2787
+ this.run("CREATE INDEX IF NOT EXISTS idx_s3_backup_targets_schedule ON s3_backup_targets(enabled, schedule_time, created_at)");
2738
2788
  const modelColumns = new Set(this.all("PRAGMA table_info(models)").map((row) => String(row.name)));
2739
2789
  if (!modelColumns.has("multimodal_enabled")) {
2740
2790
  this.run("ALTER TABLE models ADD COLUMN multimodal_enabled INTEGER NOT NULL DEFAULT 0 CHECK(multimodal_enabled IN (0, 1))");
@@ -2761,7 +2811,7 @@ export class Database {
2761
2811
  tools.push("image");
2762
2812
  this.run("UPDATE work_ai_settings SET agent_tools_json = ? WHERE work_id = ?", JSON.stringify(tools), String(row.work_id));
2763
2813
  }
2764
- this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (73, ?)", new Date().toISOString());
2814
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (73, ?)", new Date().toISOString());
2765
2815
  });
2766
2816
  const integrity = this.all("PRAGMA integrity_check");
2767
2817
  if (integrity.some((row) => row.integrity_check !== "ok")) {
@@ -2771,8 +2821,27 @@ export class Database {
2771
2821
  if (foreignKeys.length > 0)
2772
2822
  throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
2773
2823
  }
2774
- if (!applied.has(74)) {
2824
+ const s3RunsPresent = this.all("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 's3_backup_runs'").length > 0;
2825
+ const aiHistorySearchPresent = this.all("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'ai_history_search'").length > 0;
2826
+ if (!applied.has(74) || !s3RunsPresent || !aiHistorySearchPresent) {
2775
2827
  this.transaction(() => {
2828
+ this.run(`CREATE TABLE IF NOT EXISTS s3_backup_runs (
2829
+ id TEXT PRIMARY KEY,
2830
+ target_id TEXT REFERENCES s3_backup_targets(id) ON DELETE SET NULL,
2831
+ target_name TEXT NOT NULL,
2832
+ trigger TEXT NOT NULL CHECK(trigger IN ('manual', 'scheduled')),
2833
+ status TEXT NOT NULL CHECK(status IN ('running', 'succeeded', 'failed')),
2834
+ database_key TEXT,
2835
+ images_uploaded INTEGER NOT NULL DEFAULT 0,
2836
+ images_skipped INTEGER NOT NULL DEFAULT 0,
2837
+ databases_deleted INTEGER NOT NULL DEFAULT 0,
2838
+ error_message TEXT,
2839
+ server_response_json TEXT,
2840
+ started_at TEXT NOT NULL,
2841
+ finished_at TEXT
2842
+ )`);
2843
+ this.run("CREATE INDEX IF NOT EXISTS idx_s3_backup_runs_started ON s3_backup_runs(started_at DESC, id)");
2844
+ this.run("CREATE INDEX IF NOT EXISTS idx_s3_backup_runs_target ON s3_backup_runs(target_id, started_at DESC)");
2776
2845
  this.raw.exec(`
2777
2846
  CREATE TABLE IF NOT EXISTS ai_history_search (
2778
2847
  id INTEGER PRIMARY KEY,
@@ -2887,12 +2956,38 @@ export class Database {
2887
2956
  this.run("UPDATE ai_history_search SET search_content = ? WHERE id = ?", searchContent, row.id);
2888
2957
  }
2889
2958
  this.run("INSERT INTO ai_history_search_fts(ai_history_search_fts) VALUES ('rebuild')");
2959
+ this.run("DELETE FROM ai_history_search_short_terms");
2890
2960
  const insertTerm = this.raw.prepare("INSERT INTO ai_history_search_short_terms (search_id, term) VALUES (?, ?)");
2891
2961
  for (const row of this.all("SELECT id, search_content FROM ai_history_search")) {
2892
2962
  for (const term of documentShortSearchTerms(String(row.search_content)))
2893
2963
  insertTerm.run(row.id, term);
2894
2964
  }
2895
- this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (74, ?)", new Date().toISOString());
2965
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (74, ?)", new Date().toISOString());
2966
+ });
2967
+ const integrity = this.all("PRAGMA integrity_check");
2968
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
2969
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
2970
+ }
2971
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
2972
+ if (foreignKeys.length > 0)
2973
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
2974
+ }
2975
+ const s3TargetsTablePresentForOrder = this.all("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 's3_backup_targets'").length > 0;
2976
+ const s3SortOrderPresent = s3TargetsTablePresentForOrder
2977
+ && new Set(this.all("PRAGMA table_info(s3_backup_targets)").map((row) => String(row.name))).has("sort_order");
2978
+ if (!applied.has(75) || !s3SortOrderPresent) {
2979
+ this.transaction(() => {
2980
+ const columns = new Set(this.all("PRAGMA table_info(s3_backup_targets)").map((row) => String(row.name)));
2981
+ if (!columns.has("sort_order")) {
2982
+ this.run("ALTER TABLE s3_backup_targets ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0");
2983
+ }
2984
+ this.run(`UPDATE s3_backup_targets AS target SET sort_order = (
2985
+ SELECT COUNT(*) FROM s3_backup_targets AS previous
2986
+ WHERE previous.created_at < target.created_at
2987
+ OR (previous.created_at = target.created_at AND previous.id < target.id)
2988
+ )`);
2989
+ this.run("CREATE INDEX IF NOT EXISTS idx_s3_backup_targets_order ON s3_backup_targets(sort_order, created_at, id)");
2990
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (75, ?)", new Date().toISOString());
2896
2991
  });
2897
2992
  const integrity = this.all("PRAGMA integrity_check");
2898
2993
  if (integrity.some((row) => row.integrity_check !== "ok")) {
@@ -2902,6 +2997,69 @@ export class Database {
2902
2997
  if (foreignKeys.length > 0)
2903
2998
  throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
2904
2999
  }
3000
+ if (!applied.has(76)) {
3001
+ this.raw.exec("PRAGMA foreign_keys = OFF");
3002
+ try {
3003
+ this.transaction(() => {
3004
+ this.run(`CREATE TABLE work_ai_settings_v76 (
3005
+ work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
3006
+ system_prompt TEXT NOT NULL DEFAULT '',
3007
+ daily_token_quota INTEGER CHECK(daily_token_quota IS NULL OR daily_token_quota >= 10000),
3008
+ auto_run_enabled INTEGER NOT NULL DEFAULT 0,
3009
+ auto_run_concurrency INTEGER NOT NULL DEFAULT 2,
3010
+ auto_run_batch_limit INTEGER NOT NULL DEFAULT 20,
3011
+ auto_run_daily_task_limit INTEGER NOT NULL DEFAULT 0 CHECK(auto_run_daily_task_limit BETWEEN 0 AND 10000),
3012
+ auto_run_failure_threshold INTEGER NOT NULL DEFAULT 3 CHECK(auto_run_failure_threshold BETWEEN 1 AND 10),
3013
+ auto_run_paused INTEGER NOT NULL DEFAULT 0 CHECK(auto_run_paused IN (0, 1)),
3014
+ auto_run_pause_reason TEXT NOT NULL DEFAULT '',
3015
+ auto_run_resume_at TEXT,
3016
+ auto_run_consecutive_failures INTEGER NOT NULL DEFAULT 0,
3017
+ book_summary_context_percent INTEGER NOT NULL DEFAULT 50 CHECK(book_summary_context_percent BETWEEN 1 AND 90),
3018
+ context_compact_threshold INTEGER NOT NULL DEFAULT 85 CHECK(context_compact_threshold BETWEEN 50 AND 90),
3019
+ agent_tool_call_limit INTEGER NOT NULL DEFAULT 12 CHECK(agent_tool_call_limit BETWEEN 5 AND 1000),
3020
+ agent_tool_call_global_multiplier INTEGER NOT NULL DEFAULT 3 CHECK(agent_tool_call_global_multiplier BETWEEN 1 AND 6),
3021
+ agent_tools_json TEXT NOT NULL DEFAULT '["story_index","read_chapters","search_story_entities","grep","read_character_sections","search_drafts","image"]',
3022
+ title_generation_model_id TEXT REFERENCES models(id) ON DELETE SET NULL,
3023
+ image_tool_model_id TEXT REFERENCES models(id) ON DELETE SET NULL,
3024
+ always_include_setting_info INTEGER NOT NULL DEFAULT 0 CHECK(always_include_setting_info IN (0, 1)),
3025
+ updated_at TEXT NOT NULL
3026
+ )`);
3027
+ this.run(`INSERT INTO work_ai_settings_v76 (
3028
+ work_id, system_prompt, daily_token_quota, auto_run_enabled, auto_run_concurrency, auto_run_batch_limit,
3029
+ auto_run_daily_task_limit, auto_run_failure_threshold, auto_run_paused, auto_run_pause_reason,
3030
+ auto_run_resume_at, auto_run_consecutive_failures, book_summary_context_percent,
3031
+ context_compact_threshold, agent_tool_call_limit, agent_tool_call_global_multiplier,
3032
+ agent_tools_json, title_generation_model_id, image_tool_model_id, always_include_setting_info, updated_at
3033
+ )
3034
+ SELECT
3035
+ work_id, system_prompt, daily_token_quota, auto_run_enabled, auto_run_concurrency, auto_run_batch_limit,
3036
+ auto_run_daily_task_limit, auto_run_failure_threshold, auto_run_paused, auto_run_pause_reason,
3037
+ auto_run_resume_at, auto_run_consecutive_failures, book_summary_context_percent,
3038
+ context_compact_threshold,
3039
+ CASE
3040
+ WHEN agent_tool_call_limit < 5 THEN 5
3041
+ WHEN agent_tool_call_limit > 1000 THEN 1000
3042
+ ELSE agent_tool_call_limit
3043
+ END,
3044
+ agent_tool_call_global_multiplier,
3045
+ agent_tools_json, title_generation_model_id, image_tool_model_id, always_include_setting_info, updated_at
3046
+ FROM work_ai_settings`);
3047
+ this.run("DROP TABLE work_ai_settings");
3048
+ this.run("ALTER TABLE work_ai_settings_v76 RENAME TO work_ai_settings");
3049
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (76, ?)", new Date().toISOString());
3050
+ });
3051
+ }
3052
+ finally {
3053
+ this.raw.exec("PRAGMA foreign_keys = ON");
3054
+ }
3055
+ const integrity = this.all("PRAGMA integrity_check");
3056
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3057
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3058
+ }
3059
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3060
+ if (foreignKeys.length > 0)
3061
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3062
+ }
2905
3063
  }
2906
3064
  normalizeCharacterName(value) {
2907
3065
  return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLocaleLowerCase("zh-CN");
@@ -2921,6 +3079,8 @@ export class Database {
2921
3079
  WHERE status = 'running'`, timestamp);
2922
3080
  this.run(`UPDATE analysis_tasks SET status = 'partial', failure_json = ?, updated_at = ?
2923
3081
  WHERE status = 'running'`, JSON.stringify([{ message: "服务重启导致任务中断" }]), timestamp);
3082
+ this.run(`UPDATE s3_backup_runs SET status = 'failed', error_message = COALESCE(error_message, '服务重启导致备份中断'), finished_at = ?
3083
+ WHERE status = 'running'`, timestamp);
2924
3084
  }
2925
3085
  }
2926
3086
  //# sourceMappingURL=database.js.map