@musnows/scriverse 0.8.7 → 0.9.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/database.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, statfsSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
+ import { DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS, MAX_AI_ANALYSIS_TIMEOUT_SECONDS, MIN_AI_ANALYSIS_TIMEOUT_SECONDS } from "./ai-analysis-timeout.js";
4
5
  import { documentParagraphLineRanges } from "./hybrid-search.js";
5
6
  import { logger, sanitizeError } from "./logger.js";
6
7
  import { documentShortSearchTerms, normalizeDocumentSearchText, splitDocumentParagraphs } from "./utils.js";
7
8
  export const PLATFORM_AI_WORK_ID = "__scriverse_platform_ai__";
8
- // 版本 81 用于列表查询索引;版本 82 由 Store 写入实体版本基线标记;版本 83 创建协作状态表;版本 84 创建备份加密表;版本 85 持久化协作变更动作;版本 86 扩展直接图片上传格式;版本 87 增加作品与分卷回收站;版本 88 持久化 AI 对话分支幂等键;版本 89 持久化 AI 对话流请求锁与幂等状态;版本 90 持久化 AI 连通性测试冷却状态;版本 91 建立章节段落行号索引;版本 92 增加 AI 对话收藏状态;版本 93 优化伏笔计划回收章节查询;版本 94 增加模型思考强度;版本 95 增加供应商最大输出参数选择;版本 96 扩展模型思考强度档位;版本 97 增加人物性别字段;版本 98 增加正文稳定等待配置;版本 99 持久化关系扮演中的用户角色;版本 100 增加平台 AI 流事件空闲超时配置;版本 101 将平台 AI 流事件空闲超时上限提升至 600 秒;版本 102 创建角色头像元数据表;版本 103 回填历史 AI 对话归属并建立用户列表索引;版本 104 扩大供应商协议约束以支持 OpenAI Responses;版本 105 增加独立分卷剧情顺序;版本 106 增加供应商思考类型配置;版本 107 增加 AI Cache Write 输入 Token 统计;版本 108 增加作品 AI 每月 Token 额度;版本 109 增加供应商日、月 Token 额度;版本 110 将日、月 Token 额度下限调整为大于 0;版本 111 扩展模型思考强度为 auto
9
+ // 版本 81 用于列表查询索引;版本 82 由 Store 写入实体版本基线标记;版本 83 创建协作状态表;版本 84 创建备份加密表;版本 85 持久化协作变更动作;版本 86 扩展直接图片上传格式;版本 87 增加作品与分卷回收站;版本 88 持久化 AI 对话分支幂等键;版本 89 持久化 AI 对话流请求锁与幂等状态;版本 90 持久化 AI 连通性测试冷却状态;版本 91 建立章节段落行号索引;版本 92 增加 AI 对话收藏状态;版本 93 优化伏笔计划回收章节查询;版本 94 增加模型思考强度;版本 95 增加供应商最大输出参数选择;版本 96 扩展模型思考强度档位;版本 97 增加人物性别字段;版本 98 增加正文稳定等待配置;版本 99 持久化关系扮演中的用户角色;版本 100 增加平台 AI 流事件空闲超时配置;版本 101 将平台 AI 流事件空闲超时上限提升至 600 秒;版本 102 创建角色头像元数据表;版本 103 回填历史 AI 对话归属并建立用户列表索引;版本 104 扩大供应商协议约束以支持 OpenAI Responses;版本 105 增加独立分卷剧情顺序;版本 106 增加供应商思考类型配置;版本 107 增加 AI Cache Write 输入 Token 统计;版本 108 增加作品 AI 每月 Token 额度;版本 109 增加供应商日、月 Token 额度;版本 110 将日、月 Token 额度下限调整为大于 0;版本 111 扩展模型思考强度为 auto;版本 112 为 CLI API Key 增加可复制的加密密文;版本 113 增加角色收藏状态与列表索引;版本 114 增加组织、设定档案与想法收藏状态及列表索引;版本 115 增加 AI 对话会话级场景钉;版本 116 增加可持久化且可撤销的 Desktop Bearer 会话;版本 117 增加作品离线授权、同步变更游标与幂等变更结果;版本 118 增加供应商分析请求超时配置;版本 119 记录分析任务是否由 API Key 创建。
9
10
  export const ENTITY_VERSION_BASELINE_MIGRATION_VERSION = 82;
10
- export const DATABASE_SCHEMA_VERSION = 111;
11
+ export const DATABASE_SCHEMA_VERSION = 119;
11
12
  export const SQLITE_IOERR_SHMSIZE = 4874;
12
13
  export function isSqliteDiskIoError(error) {
13
14
  if (!(error instanceof Error))
@@ -185,6 +186,7 @@ export class Database {
185
186
  cover_url TEXT,
186
187
  tags_json TEXT NOT NULL DEFAULT '[]',
187
188
  is_internal INTEGER NOT NULL DEFAULT 0,
189
+ offline_access_enabled INTEGER NOT NULL DEFAULT 0 CHECK(offline_access_enabled IN (0, 1)),
188
190
  version_no INTEGER NOT NULL DEFAULT 1,
189
191
  deleted_at TEXT,
190
192
  created_at TEXT NOT NULL,
@@ -278,6 +280,7 @@ export class Database {
278
280
  tags_json TEXT NOT NULL DEFAULT '[]',
279
281
  status TEXT NOT NULL DEFAULT 'draft',
280
282
  locked INTEGER NOT NULL DEFAULT 0,
283
+ is_favorite INTEGER NOT NULL DEFAULT 0 CHECK(is_favorite IN (0, 1)),
281
284
  evidence_json TEXT NOT NULL DEFAULT '[]',
282
285
  scope_json TEXT NOT NULL DEFAULT '{}',
283
286
  author_note TEXT NOT NULL DEFAULT '',
@@ -293,6 +296,7 @@ export class Database {
293
296
  setting_module TEXT,
294
297
  title TEXT NOT NULL,
295
298
  content TEXT NOT NULL DEFAULT '',
299
+ is_favorite INTEGER NOT NULL DEFAULT 0 CHECK(is_favorite IN (0, 1)),
296
300
  created_at TEXT NOT NULL,
297
301
  updated_at TEXT NOT NULL,
298
302
  CHECK(setting_module IS NULL OR setting_module IN ('settings', 'characters', 'races', 'organizations', 'timeline', 'relationships', 'outlines')),
@@ -326,6 +330,7 @@ export class Database {
326
330
  profile_json TEXT NOT NULL DEFAULT '{}',
327
331
  current_state_json TEXT NOT NULL DEFAULT '{}',
328
332
  is_dead INTEGER NOT NULL DEFAULT 0 CHECK(is_dead IN (0, 1)),
333
+ is_favorite INTEGER NOT NULL DEFAULT 0 CHECK(is_favorite IN (0, 1)),
329
334
  locked_fields_json TEXT NOT NULL DEFAULT '[]',
330
335
  first_chapter_id TEXT REFERENCES chapters(id) ON DELETE SET NULL,
331
336
  merged_into_character_id TEXT,
@@ -457,6 +462,7 @@ export class Database {
457
462
  connection_status TEXT NOT NULL DEFAULT 'unchecked',
458
463
  concurrency_limit INTEGER NOT NULL DEFAULT 10 CHECK(concurrency_limit BETWEEN 1 AND 100),
459
464
  rpm_limit INTEGER NOT NULL DEFAULT 10 CHECK(rpm_limit BETWEEN 1 AND 10000),
465
+ analysis_timeout_seconds INTEGER NOT NULL DEFAULT ${DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS} CHECK(analysis_timeout_seconds BETWEEN ${MIN_AI_ANALYSIS_TIMEOUT_SECONDS} AND ${MAX_AI_ANALYSIS_TIMEOUT_SECONDS}),
460
466
  daily_token_quota INTEGER CHECK(daily_token_quota IS NULL OR daily_token_quota >= 1),
461
467
  monthly_token_quota INTEGER CHECK(monthly_token_quota IS NULL OR monthly_token_quota >= 1),
462
468
  max_tokens INTEGER NOT NULL DEFAULT 32000 CHECK(max_tokens BETWEEN 1 AND 32768),
@@ -613,6 +619,7 @@ export class Database {
613
619
  agent_tools_json TEXT,
614
620
  injected_entities_json TEXT NOT NULL DEFAULT '{"characters":[],"races":[],"organizations":[]}',
615
621
  system_clock_text TEXT NOT NULL DEFAULT '',
622
+ scene_pin_json TEXT NOT NULL DEFAULT '{}',
616
623
  created_at TEXT NOT NULL,
617
624
  updated_at TEXT NOT NULL
618
625
  );
@@ -673,6 +680,7 @@ export class Database {
673
680
  attempt_count INTEGER NOT NULL DEFAULT 0,
674
681
  next_attempt_at TEXT,
675
682
  last_attempt_at TEXT,
683
+ created_via_api_key INTEGER NOT NULL DEFAULT 0 CHECK(created_via_api_key IN (0, 1)),
676
684
  created_at TEXT NOT NULL,
677
685
  updated_at TEXT NOT NULL
678
686
  );
@@ -724,6 +732,7 @@ export class Database {
724
732
  normalized_name TEXT NOT NULL,
725
733
  description TEXT NOT NULL DEFAULT '',
726
734
  is_dissolved INTEGER NOT NULL DEFAULT 0 CHECK(is_dissolved IN (0, 1)),
735
+ is_favorite INTEGER NOT NULL DEFAULT 0 CHECK(is_favorite IN (0, 1)),
727
736
  settings_json TEXT NOT NULL DEFAULT '[]',
728
737
  created_at TEXT NOT NULL,
729
738
  updated_at TEXT NOT NULL,
@@ -4127,6 +4136,215 @@ export class Database {
4127
4136
  if (foreignKeys.length > 0)
4128
4137
  throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4129
4138
  }
4139
+ if (!applied.has(112)) {
4140
+ this.transaction(() => {
4141
+ const columns = new Set(this.all("PRAGMA table_info(user_api_keys)").map((row) => String(row.name)));
4142
+ if (!columns.has("key_encrypted")) {
4143
+ this.run("ALTER TABLE user_api_keys ADD COLUMN key_encrypted TEXT");
4144
+ }
4145
+ if (!columns.has("key_iv")) {
4146
+ this.run("ALTER TABLE user_api_keys ADD COLUMN key_iv TEXT");
4147
+ }
4148
+ if (!columns.has("key_tag")) {
4149
+ this.run("ALTER TABLE user_api_keys ADD COLUMN key_tag TEXT");
4150
+ }
4151
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (112, ?)", new Date().toISOString());
4152
+ });
4153
+ const integrity = this.all("PRAGMA integrity_check");
4154
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
4155
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
4156
+ }
4157
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
4158
+ if (foreignKeys.length > 0)
4159
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4160
+ }
4161
+ const characterFavoritePresent = this.all("PRAGMA table_info(characters)").some((row) => String(row.name) === "is_favorite");
4162
+ const characterFavoriteIndexPresent = this.all("PRAGMA index_list(characters)").some((row) => String(row.name) === "idx_characters_favorite");
4163
+ if (!applied.has(113) || !characterFavoritePresent || !characterFavoriteIndexPresent) {
4164
+ this.transaction(() => {
4165
+ const columns = new Set(this.all("PRAGMA table_info(characters)").map((row) => String(row.name)));
4166
+ if (!columns.has("is_favorite")) {
4167
+ this.run("ALTER TABLE characters ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0 CHECK(is_favorite IN (0, 1))");
4168
+ }
4169
+ this.run("CREATE INDEX IF NOT EXISTS idx_characters_favorite ON characters(work_id, is_favorite DESC, name)");
4170
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (113, ?)", new Date().toISOString());
4171
+ });
4172
+ const integrity = this.all("PRAGMA integrity_check");
4173
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
4174
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
4175
+ }
4176
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
4177
+ if (foreignKeys.length > 0)
4178
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4179
+ }
4180
+ const favoriteRecordTables = ["drafts", "settings", "organizations"];
4181
+ const favoriteRecordColumnsPresent = favoriteRecordTables.every((table) => (this.all(`PRAGMA table_info(${table})`).some((row) => String(row.name) === "is_favorite")));
4182
+ const favoriteRecordIndexes = [
4183
+ ["drafts", "idx_drafts_favorite"],
4184
+ ["settings", "idx_settings_favorite"],
4185
+ ["organizations", "idx_organizations_favorite"]
4186
+ ];
4187
+ const favoriteRecordIndexesPresent = favoriteRecordIndexes.every(([table, index]) => (this.all(`PRAGMA index_list(${table})`).some((row) => String(row.name) === index)));
4188
+ if (!applied.has(114) || !favoriteRecordColumnsPresent || !favoriteRecordIndexesPresent) {
4189
+ this.transaction(() => {
4190
+ for (const table of favoriteRecordTables) {
4191
+ const columns = new Set(this.all(`PRAGMA table_info(${table})`).map((row) => String(row.name)));
4192
+ if (!columns.has("is_favorite")) {
4193
+ this.run(`ALTER TABLE ${table} ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0 CHECK(is_favorite IN (0, 1))`);
4194
+ }
4195
+ }
4196
+ this.run("CREATE INDEX IF NOT EXISTS idx_drafts_favorite ON drafts(work_id, is_favorite DESC, updated_at DESC, title)");
4197
+ this.run("CREATE INDEX IF NOT EXISTS idx_settings_favorite ON settings(work_id, is_favorite DESC, locked DESC, category, title)");
4198
+ this.run("CREATE INDEX IF NOT EXISTS idx_organizations_favorite ON organizations(work_id, is_favorite DESC, name)");
4199
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (114, ?)", new Date().toISOString());
4200
+ });
4201
+ const integrity = this.all("PRAGMA integrity_check");
4202
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
4203
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
4204
+ }
4205
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
4206
+ if (foreignKeys.length > 0)
4207
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4208
+ }
4209
+ const scenePinPresent = this.all("PRAGMA table_info(ai_conversations)").some((row) => String(row.name) === "scene_pin_json");
4210
+ if (!applied.has(115) || !scenePinPresent) {
4211
+ this.transaction(() => {
4212
+ const columns = new Set(this.all("PRAGMA table_info(ai_conversations)").map((row) => String(row.name)));
4213
+ if (!columns.has("scene_pin_json")) {
4214
+ this.run("ALTER TABLE ai_conversations ADD COLUMN scene_pin_json TEXT NOT NULL DEFAULT '{}'");
4215
+ }
4216
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (115, ?)", new Date().toISOString());
4217
+ });
4218
+ const integrity = this.all("PRAGMA integrity_check");
4219
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
4220
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
4221
+ }
4222
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
4223
+ if (foreignKeys.length > 0)
4224
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4225
+ }
4226
+ const desktopSessionTablePresent = this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'user_desktop_sessions'") !== undefined;
4227
+ const desktopSessionIndexesPresent = [
4228
+ "idx_user_desktop_sessions_token",
4229
+ "idx_user_desktop_sessions_user",
4230
+ "idx_user_desktop_sessions_active_profile"
4231
+ ].every((index) => this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'index' AND name = ?", index) !== undefined);
4232
+ if (!applied.has(116) || !desktopSessionTablePresent || !desktopSessionIndexesPresent) {
4233
+ this.transaction(() => {
4234
+ this.run(`CREATE TABLE IF NOT EXISTS user_desktop_sessions (
4235
+ id TEXT PRIMARY KEY,
4236
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
4237
+ token_hash TEXT NOT NULL UNIQUE,
4238
+ desktop_id TEXT NOT NULL,
4239
+ profile_id TEXT NOT NULL,
4240
+ client_version TEXT NOT NULL,
4241
+ created_at TEXT NOT NULL,
4242
+ expires_at TEXT NOT NULL,
4243
+ last_seen_at TEXT NOT NULL,
4244
+ revoked_at TEXT
4245
+ )`);
4246
+ this.run("CREATE INDEX IF NOT EXISTS idx_user_desktop_sessions_token ON user_desktop_sessions(token_hash)");
4247
+ this.run("CREATE INDEX IF NOT EXISTS idx_user_desktop_sessions_user ON user_desktop_sessions(user_id, expires_at)");
4248
+ this.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_desktop_sessions_active_profile
4249
+ ON user_desktop_sessions(desktop_id, profile_id) WHERE revoked_at IS NULL`);
4250
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (116, ?)", new Date().toISOString());
4251
+ });
4252
+ const integrity = this.all("PRAGMA integrity_check");
4253
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
4254
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
4255
+ }
4256
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
4257
+ if (foreignKeys.length > 0)
4258
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4259
+ }
4260
+ const workOfflineAccessPresent = this.all("PRAGMA table_info(works)").some((row) => String(row.name) === "offline_access_enabled");
4261
+ const syncChangesTablePresent = this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'sync_changes'") !== undefined;
4262
+ const syncMutationResultsTablePresent = this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'sync_mutation_results'") !== undefined;
4263
+ const syncIndexesPresent = [
4264
+ "idx_sync_changes_work_cursor",
4265
+ "idx_sync_changes_entity",
4266
+ "idx_sync_mutation_results_client"
4267
+ ].every((index) => this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'index' AND name = ?", index) !== undefined);
4268
+ if (!applied.has(117) || !workOfflineAccessPresent || !syncChangesTablePresent || !syncMutationResultsTablePresent || !syncIndexesPresent) {
4269
+ this.transaction(() => {
4270
+ const workColumns = new Set(this.all("PRAGMA table_info(works)").map((row) => String(row.name)));
4271
+ if (!workColumns.has("offline_access_enabled")) {
4272
+ this.run("ALTER TABLE works ADD COLUMN offline_access_enabled INTEGER NOT NULL DEFAULT 0 CHECK(offline_access_enabled IN (0, 1))");
4273
+ }
4274
+ this.run(`CREATE TABLE IF NOT EXISTS sync_changes (
4275
+ cursor INTEGER PRIMARY KEY AUTOINCREMENT,
4276
+ work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
4277
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('chapter', 'setting')),
4278
+ entity_id TEXT NOT NULL,
4279
+ operation TEXT NOT NULL CHECK(operation IN ('upsert', 'delete')),
4280
+ version_no INTEGER NOT NULL CHECK(version_no > 0),
4281
+ changed_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
4282
+ changed_at TEXT NOT NULL
4283
+ )`);
4284
+ this.run(`CREATE TABLE IF NOT EXISTS sync_mutation_results (
4285
+ mutation_id TEXT PRIMARY KEY,
4286
+ client_id TEXT NOT NULL,
4287
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
4288
+ work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
4289
+ request_hash TEXT NOT NULL,
4290
+ status TEXT NOT NULL CHECK(status IN ('applied', 'conflict', 'rejected')),
4291
+ applied_version_no INTEGER,
4292
+ conflict_version_no INTEGER,
4293
+ error_code TEXT,
4294
+ result_json TEXT NOT NULL,
4295
+ created_at TEXT NOT NULL,
4296
+ updated_at TEXT NOT NULL
4297
+ )`);
4298
+ this.run("CREATE INDEX IF NOT EXISTS idx_sync_changes_work_cursor ON sync_changes(work_id, cursor)");
4299
+ this.run("CREATE INDEX IF NOT EXISTS idx_sync_changes_entity ON sync_changes(work_id, entity_type, entity_id, cursor)");
4300
+ this.run("CREATE INDEX IF NOT EXISTS idx_sync_mutation_results_client ON sync_mutation_results(client_id, user_id, work_id, created_at)");
4301
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (117, ?)", new Date().toISOString());
4302
+ });
4303
+ const integrity = this.all("PRAGMA integrity_check");
4304
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
4305
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
4306
+ }
4307
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
4308
+ if (foreignKeys.length > 0)
4309
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4310
+ }
4311
+ const providerAnalysisTimeoutPresent = this.all("PRAGMA table_info(providers)").some((row) => String(row.name) === "analysis_timeout_seconds");
4312
+ if (!applied.has(118) || !providerAnalysisTimeoutPresent) {
4313
+ this.transaction(() => {
4314
+ const columns = new Set(this.all("PRAGMA table_info(providers)").map((row) => String(row.name)));
4315
+ if (!columns.has("analysis_timeout_seconds")) {
4316
+ this.run(`ALTER TABLE providers ADD COLUMN analysis_timeout_seconds INTEGER NOT NULL DEFAULT ${DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS}
4317
+ CHECK(analysis_timeout_seconds BETWEEN ${MIN_AI_ANALYSIS_TIMEOUT_SECONDS} AND ${MAX_AI_ANALYSIS_TIMEOUT_SECONDS})`);
4318
+ }
4319
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (118, ?)", new Date().toISOString());
4320
+ });
4321
+ const integrity = this.all("PRAGMA integrity_check");
4322
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
4323
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
4324
+ }
4325
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
4326
+ if (foreignKeys.length > 0)
4327
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4328
+ }
4329
+ const taskApiKeySourcePresent = this.all("PRAGMA table_info(analysis_tasks)")
4330
+ .some((row) => String(row.name) === "created_via_api_key");
4331
+ if (!applied.has(119) || !taskApiKeySourcePresent) {
4332
+ this.transaction(() => {
4333
+ const columns = new Set(this.all("PRAGMA table_info(analysis_tasks)").map((row) => String(row.name)));
4334
+ if (!columns.has("created_via_api_key")) {
4335
+ this.run(`ALTER TABLE analysis_tasks ADD COLUMN created_via_api_key INTEGER NOT NULL DEFAULT 0
4336
+ CHECK(created_via_api_key IN (0, 1))`);
4337
+ }
4338
+ this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (119, ?)", new Date().toISOString());
4339
+ });
4340
+ const integrity = this.all("PRAGMA integrity_check");
4341
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
4342
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
4343
+ }
4344
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
4345
+ if (foreignKeys.length > 0)
4346
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
4347
+ }
4130
4348
  }
4131
4349
  normalizeCharacterName(value) {
4132
4350
  return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLocaleLowerCase("zh-CN");