@musnows/scriverse 0.7.2 → 0.7.4

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.
Files changed (82) hide show
  1. package/README.en.md +3 -0
  2. package/README.md +8 -0
  3. package/dist/ai-connectivity-test.js +109 -0
  4. package/dist/ai-connectivity-test.js.map +1 -0
  5. package/dist/ai-conversation-export.js +70 -0
  6. package/dist/ai-conversation-export.js.map +1 -0
  7. package/dist/ai-stream-timeout.js +18 -0
  8. package/dist/ai-stream-timeout.js.map +1 -0
  9. package/dist/ai.js +926 -354
  10. package/dist/ai.js.map +1 -1
  11. package/dist/app.js +498 -80
  12. package/dist/app.js.map +1 -1
  13. package/dist/attachment-storage.js +28 -12
  14. package/dist/attachment-storage.js.map +1 -1
  15. package/dist/backup-encryption.js +129 -0
  16. package/dist/backup-encryption.js.map +1 -0
  17. package/dist/character-extraction.js +133 -0
  18. package/dist/character-extraction.js.map +1 -0
  19. package/dist/cli-core.js +7 -6
  20. package/dist/cli-core.js.map +1 -1
  21. package/dist/collaboration-presence.js +200 -18
  22. package/dist/collaboration-presence.js.map +1 -1
  23. package/dist/database.js +335 -3
  24. package/dist/database.js.map +1 -1
  25. package/dist/docx-export.js +1 -1
  26. package/dist/docx-export.js.map +1 -1
  27. package/dist/domain.js +18 -0
  28. package/dist/domain.js.map +1 -1
  29. package/dist/epub-export.js +319 -0
  30. package/dist/epub-export.js.map +1 -0
  31. package/dist/hybrid-search.js +8 -0
  32. package/dist/hybrid-search.js.map +1 -1
  33. package/dist/image-metadata.js +17 -2
  34. package/dist/image-metadata.js.map +1 -1
  35. package/dist/presence-store.js +160 -0
  36. package/dist/presence-store.js.map +1 -0
  37. package/dist/public/ai-connectivity-test.d.ts +7 -0
  38. package/dist/public/ai-connectivity-test.js +82 -0
  39. package/dist/public/ai-context-meter.js +27 -0
  40. package/dist/public/ai-mentions.js +14 -0
  41. package/dist/public/ai-request-manager.js +99 -0
  42. package/dist/public/ai-stream-protocol.js +51 -0
  43. package/dist/public/app.js +3587 -559
  44. package/dist/public/background-task-center.js +1 -1
  45. package/dist/public/chapter-editor-virtualization.js +52 -0
  46. package/dist/public/chapter-version-diff.d.ts +20 -0
  47. package/dist/public/chapter-version-diff.js +116 -0
  48. package/dist/public/foreshadow-reminder.d.ts +32 -0
  49. package/dist/public/foreshadow-reminder.js +73 -0
  50. package/dist/public/global-replace-refresh.js +60 -0
  51. package/dist/public/index.html +161 -31
  52. package/dist/public/outline-board.d.ts +61 -0
  53. package/dist/public/outline-board.js +137 -0
  54. package/dist/public/page-route.d.ts +1 -0
  55. package/dist/public/page-route.js +8 -0
  56. package/dist/public/presence-client-id.d.ts +10 -0
  57. package/dist/public/presence-client-id.js +31 -0
  58. package/dist/public/reading-preview.d.ts +32 -0
  59. package/dist/public/reading-preview.js +136 -0
  60. package/dist/public/s3-backup-ui.d.ts +10 -0
  61. package/dist/public/s3-backup-ui.js +29 -0
  62. package/dist/public/setting-filters.d.ts +16 -0
  63. package/dist/public/setting-filters.js +19 -0
  64. package/dist/public/styles.css +522 -11
  65. package/dist/public/upload-progress.d.ts +2 -0
  66. package/dist/public/upload-progress.js +10 -0
  67. package/dist/s3-backup.js +197 -12
  68. package/dist/s3-backup.js.map +1 -1
  69. package/dist/security.js +86 -21
  70. package/dist/security.js.map +1 -1
  71. package/dist/server-runtime.js +38 -18
  72. package/dist/server-runtime.js.map +1 -1
  73. package/dist/store.js +899 -104
  74. package/dist/store.js.map +1 -1
  75. package/dist/upload-limits.js +35 -0
  76. package/dist/upload-limits.js.map +1 -0
  77. package/dist/user-auth.js +56 -10
  78. package/dist/user-auth.js.map +1 -1
  79. package/dist/utils.js +3 -0
  80. package/dist/utils.js.map +1 -1
  81. package/dist/version.js +1 -1
  82. package/package.json +4 -2
package/dist/database.js CHANGED
@@ -4,7 +4,9 @@ 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 = 79;
7
+ // 版本 81 用于列表查询索引;版本 82 由 Store 写入实体版本基线标记;版本 83 创建协作状态表;版本 84 创建备份加密表;版本 85 持久化协作变更动作;版本 86 扩展直接图片上传格式;版本 87 增加作品与分卷回收站;版本 88 持久化 AI 对话分支幂等键;版本 89 持久化 AI 对话流请求锁与幂等状态;版本 90 持久化 AI 连通性测试冷却状态。
8
+ export const ENTITY_VERSION_BASELINE_MIGRATION_VERSION = 82;
9
+ export const DATABASE_SCHEMA_VERSION = 90;
8
10
  export const SQLITE_IOERR_SHMSIZE = 4874;
9
11
  export function isSqliteDiskIoError(error) {
10
12
  if (!(error instanceof Error))
@@ -183,6 +185,7 @@ export class Database {
183
185
  tags_json TEXT NOT NULL DEFAULT '[]',
184
186
  is_internal INTEGER NOT NULL DEFAULT 0,
185
187
  version_no INTEGER NOT NULL DEFAULT 1,
188
+ deleted_at TEXT,
186
189
  created_at TEXT NOT NULL,
187
190
  updated_at TEXT NOT NULL
188
191
  );
@@ -209,6 +212,7 @@ export class Database {
209
212
  keywords_json TEXT NOT NULL DEFAULT '[]',
210
213
  sort_order INTEGER NOT NULL,
211
214
  version_no INTEGER NOT NULL DEFAULT 1,
215
+ deleted_at TEXT,
212
216
  created_at TEXT NOT NULL,
213
217
  updated_at TEXT NOT NULL
214
218
  );
@@ -226,6 +230,7 @@ export class Database {
226
230
  analysis_status TEXT NOT NULL DEFAULT 'pending',
227
231
  excluded_from_analysis INTEGER NOT NULL DEFAULT 0,
228
232
  deleted_at TEXT,
233
+ deleted_via_volume_id TEXT,
229
234
  created_at TEXT NOT NULL,
230
235
  updated_at TEXT NOT NULL
231
236
  );
@@ -477,6 +482,26 @@ export class Database {
477
482
  UNIQUE(provider_id, model_id)
478
483
  );
479
484
 
485
+ CREATE TABLE IF NOT EXISTS ai_connectivity_test_states (
486
+ object_type TEXT NOT NULL CHECK(object_type IN ('provider', 'model')),
487
+ object_id TEXT NOT NULL,
488
+ config_fingerprint TEXT NOT NULL CHECK(length(config_fingerprint) = 64),
489
+ state TEXT NOT NULL CHECK(state IN ('in_progress', 'success', 'failure')),
490
+ attempt_id TEXT NOT NULL,
491
+ retry_at_ms INTEGER NOT NULL CHECK(retry_at_ms >= 0),
492
+ updated_at TEXT NOT NULL,
493
+ PRIMARY KEY(object_type, object_id)
494
+ );
495
+ CREATE INDEX IF NOT EXISTS idx_ai_connectivity_test_states_retry_at ON ai_connectivity_test_states(retry_at_ms);
496
+ CREATE TRIGGER IF NOT EXISTS ai_connectivity_test_states_provider_delete
497
+ AFTER DELETE ON providers BEGIN
498
+ DELETE FROM ai_connectivity_test_states WHERE object_type = 'provider' AND object_id = OLD.id;
499
+ END;
500
+ CREATE TRIGGER IF NOT EXISTS ai_connectivity_test_states_model_delete
501
+ AFTER DELETE ON models BEGIN
502
+ DELETE FROM ai_connectivity_test_states WHERE object_type = 'model' AND object_id = OLD.id;
503
+ END;
504
+
480
505
  CREATE TABLE IF NOT EXISTS task_defaults (
481
506
  work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
482
507
  task_type TEXT NOT NULL,
@@ -590,6 +615,37 @@ export class Database {
590
615
  created_at TEXT NOT NULL
591
616
  );
592
617
 
618
+ CREATE TABLE IF NOT EXISTS ai_conversation_stream_requests (
619
+ id TEXT PRIMARY KEY,
620
+ work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
621
+ conversation_id TEXT NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE,
622
+ actor_scope TEXT NOT NULL CHECK(length(actor_scope) BETWEEN 1 AND 200),
623
+ idempotency_key TEXT NOT NULL CHECK(length(idempotency_key) BETWEEN 16 AND 128),
624
+ request_hash TEXT NOT NULL CHECK(length(request_hash) = 64),
625
+ status TEXT NOT NULL CHECK(status IN ('in_progress', 'completed', 'cancelled', 'failed', 'timed_out', 'abandoned')),
626
+ terminal_reason TEXT,
627
+ user_message_id TEXT REFERENCES ai_conversation_messages(id) ON DELETE SET NULL,
628
+ assistant_message_id TEXT REFERENCES ai_conversation_messages(id) ON DELETE SET NULL,
629
+ lease_expires_at TEXT,
630
+ created_at TEXT NOT NULL,
631
+ updated_at TEXT NOT NULL,
632
+ completed_at TEXT,
633
+ UNIQUE(actor_scope, work_id, idempotency_key),
634
+ CHECK(
635
+ (status = 'in_progress' AND lease_expires_at IS NOT NULL AND completed_at IS NULL)
636
+ OR (status <> 'in_progress' AND lease_expires_at IS NULL AND completed_at IS NOT NULL)
637
+ )
638
+ );
639
+
640
+ CREATE TABLE IF NOT EXISTS ai_conversation_forks (
641
+ source_conversation_id TEXT NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE,
642
+ source_message_id TEXT NOT NULL REFERENCES ai_conversation_messages(id) ON DELETE CASCADE,
643
+ request_id TEXT NOT NULL CHECK(length(request_id) BETWEEN 1 AND 200),
644
+ conversation_id TEXT NOT NULL UNIQUE REFERENCES ai_conversations(id) ON DELETE CASCADE,
645
+ created_at TEXT NOT NULL,
646
+ PRIMARY KEY(source_conversation_id, request_id)
647
+ );
648
+
593
649
  CREATE TABLE IF NOT EXISTS analysis_tasks (
594
650
  id TEXT PRIMARY KEY,
595
651
  work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
@@ -631,7 +687,7 @@ export class Database {
631
687
 
632
688
  CREATE TABLE IF NOT EXISTS work_covers (
633
689
  work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
634
- mime_type TEXT NOT NULL CHECK(mime_type IN ('image/jpeg', 'image/png', 'image/webp')),
690
+ mime_type TEXT NOT NULL CHECK(mime_type IN ('image/jpeg', 'image/png', 'image/webp', 'image/gif')),
635
691
  content BLOB NOT NULL,
636
692
  byte_length INTEGER NOT NULL,
637
693
  sha256 TEXT NOT NULL,
@@ -723,6 +779,8 @@ export class Database {
723
779
  CREATE INDEX IF NOT EXISTS idx_volumes_work ON volumes(work_id, sort_order);
724
780
  CREATE INDEX IF NOT EXISTS idx_chapters_work ON chapters(work_id, volume_id, sort_order);
725
781
  CREATE INDEX IF NOT EXISTS idx_versions_chapter ON chapter_versions(chapter_id, version_no DESC);
782
+ CREATE INDEX IF NOT EXISTS idx_file_versions_work ON file_versions(work_id, created_at DESC, id DESC);
783
+ CREATE INDEX IF NOT EXISTS idx_chapter_insights_chapter ON chapter_insights(chapter_id, chapter_version DESC, created_at DESC);
726
784
  CREATE INDEX IF NOT EXISTS idx_settings_work ON settings(work_id, category);
727
785
  CREATE INDEX IF NOT EXISTS idx_drafts_work ON drafts(work_id, draft_type, updated_at DESC);
728
786
  CREATE INDEX IF NOT EXISTS idx_characters_work ON characters(work_id, name);
@@ -733,8 +791,15 @@ export class Database {
733
791
  CREATE INDEX IF NOT EXISTS idx_character_merges_work ON character_merges(work_id, created_at DESC);
734
792
  CREATE INDEX IF NOT EXISTS idx_tasks_work ON analysis_tasks(work_id, status);
735
793
  CREATE INDEX IF NOT EXISTS idx_calls_work ON ai_calls(work_id, created_at DESC);
794
+ CREATE INDEX IF NOT EXISTS idx_ai_suggestions_work ON ai_suggestions(work_id, status, created_at DESC);
736
795
  CREATE INDEX IF NOT EXISTS idx_ai_conversations_work ON ai_conversations(work_id, updated_at DESC);
737
796
  CREATE INDEX IF NOT EXISTS idx_ai_conversation_messages ON ai_conversation_messages(conversation_id, created_at);
797
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ai_conversation_stream_requests_active
798
+ ON ai_conversation_stream_requests(conversation_id) WHERE status = 'in_progress';
799
+ CREATE INDEX IF NOT EXISTS idx_ai_conversation_stream_requests_lease
800
+ ON ai_conversation_stream_requests(status, lease_expires_at);
801
+ CREATE INDEX IF NOT EXISTS idx_ai_conversation_forks_message ON ai_conversation_forks(source_message_id);
802
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_work_created ON audit_logs(work_id, created_at DESC);
738
803
  CREATE UNIQUE INDEX IF NOT EXISTS idx_character_names_primary ON character_names(character_id) WHERE kind = 'primary';
739
804
  CREATE INDEX IF NOT EXISTS idx_character_names_character ON character_names(character_id, sort_order);
740
805
  CREATE INDEX IF NOT EXISTS idx_character_versions_character ON character_versions(character_id, version_no DESC);
@@ -1269,7 +1334,7 @@ export class Database {
1269
1334
  }
1270
1335
  this.run(`CREATE TABLE IF NOT EXISTS user_avatars (
1271
1336
  user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
1272
- mime_type TEXT NOT NULL CHECK(mime_type IN ('image/png', 'image/jpeg', 'image/webp')),
1337
+ mime_type TEXT NOT NULL CHECK(mime_type IN ('image/png', 'image/jpeg', 'image/webp', 'image/gif')),
1273
1338
  content BLOB NOT NULL,
1274
1339
  byte_length INTEGER NOT NULL,
1275
1340
  sha256 TEXT NOT NULL,
@@ -3175,6 +3240,273 @@ export class Database {
3175
3240
  if (foreignKeys.length > 0)
3176
3241
  throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3177
3242
  }
3243
+ if (!applied.has(80)) {
3244
+ this.transaction(() => {
3245
+ this.run("CREATE INDEX IF NOT EXISTS idx_audit_logs_work_created ON audit_logs(work_id, created_at DESC)");
3246
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (80, ?)", new Date().toISOString());
3247
+ });
3248
+ const integrity = this.all("PRAGMA integrity_check");
3249
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3250
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3251
+ }
3252
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3253
+ if (foreignKeys.length > 0)
3254
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3255
+ }
3256
+ if (!applied.has(81)) {
3257
+ this.transaction(() => {
3258
+ this.run("CREATE INDEX IF NOT EXISTS idx_ai_suggestions_work ON ai_suggestions(work_id, status, created_at DESC)");
3259
+ this.run("CREATE INDEX IF NOT EXISTS idx_file_versions_work ON file_versions(work_id, created_at DESC, id DESC)");
3260
+ this.run("CREATE INDEX IF NOT EXISTS idx_chapter_insights_chapter ON chapter_insights(chapter_id, chapter_version DESC, created_at DESC)");
3261
+ const integrity = this.all("PRAGMA integrity_check");
3262
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3263
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3264
+ }
3265
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3266
+ if (foreignKeys.length > 0)
3267
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3268
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (81, ?)", new Date().toISOString());
3269
+ });
3270
+ }
3271
+ if (!applied.has(83)) {
3272
+ this.transaction(() => {
3273
+ this.run(`CREATE TABLE IF NOT EXISTS presence_entries (
3274
+ work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
3275
+ client_id TEXT NOT NULL,
3276
+ user_id TEXT NOT NULL,
3277
+ username TEXT NOT NULL,
3278
+ display_name TEXT NOT NULL,
3279
+ avatar_url TEXT,
3280
+ page_kind TEXT NOT NULL,
3281
+ page_module TEXT,
3282
+ page_resource_id TEXT,
3283
+ last_seen_at TEXT NOT NULL,
3284
+ PRIMARY KEY(work_id, client_id)
3285
+ )`);
3286
+ this.run("CREATE INDEX IF NOT EXISTS idx_presence_entries_work ON presence_entries(work_id, last_seen_at)");
3287
+ this.run("CREATE INDEX IF NOT EXISTS idx_presence_entries_last_seen ON presence_entries(last_seen_at)");
3288
+ this.run(`CREATE TABLE IF NOT EXISTS presence_changes (
3289
+ id TEXT PRIMARY KEY,
3290
+ work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
3291
+ page_key TEXT NOT NULL,
3292
+ label TEXT NOT NULL,
3293
+ actor_user_id TEXT NOT NULL,
3294
+ actor_display_name TEXT NOT NULL,
3295
+ saved_at TEXT NOT NULL,
3296
+ recipient_client_ids_json TEXT NOT NULL DEFAULT '[]'
3297
+ CHECK(json_valid(recipient_client_ids_json) AND json_type(recipient_client_ids_json) = 'array')
3298
+ )`);
3299
+ this.run("CREATE INDEX IF NOT EXISTS idx_presence_changes_work ON presence_changes(work_id, page_key, saved_at DESC)");
3300
+ this.run("CREATE INDEX IF NOT EXISTS idx_presence_changes_saved_at ON presence_changes(saved_at)");
3301
+ const integrity = this.all("PRAGMA integrity_check");
3302
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3303
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3304
+ }
3305
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3306
+ if (foreignKeys.length > 0)
3307
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3308
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (83, ?)", new Date().toISOString());
3309
+ });
3310
+ }
3311
+ if (!applied.has(84)) {
3312
+ this.transaction(() => {
3313
+ // 兼容曾使用更早迁移号创建备份加密表的开发版本,并确保列表索引完整。
3314
+ this.run("CREATE INDEX IF NOT EXISTS idx_ai_suggestions_work ON ai_suggestions(work_id, status, created_at DESC)");
3315
+ this.run("CREATE INDEX IF NOT EXISTS idx_file_versions_work ON file_versions(work_id, created_at DESC, id DESC)");
3316
+ this.run("CREATE INDEX IF NOT EXISTS idx_chapter_insights_chapter ON chapter_insights(chapter_id, chapter_version DESC, created_at DESC)");
3317
+ this.run(`CREATE TABLE IF NOT EXISTS s3_backup_encryption (
3318
+ id INTEGER PRIMARY KEY CHECK(id = 1),
3319
+ enabled INTEGER NOT NULL DEFAULT 0 CHECK(enabled IN (0, 1)),
3320
+ kek_encrypted TEXT,
3321
+ kek_iv TEXT,
3322
+ kek_tag TEXT,
3323
+ created_at TEXT NOT NULL,
3324
+ updated_at TEXT NOT NULL,
3325
+ CHECK(
3326
+ (kek_encrypted IS NULL AND kek_iv IS NULL AND kek_tag IS NULL)
3327
+ OR (kek_encrypted IS NOT NULL AND kek_iv IS NOT NULL AND kek_tag IS NOT NULL)
3328
+ ),
3329
+ CHECK(enabled = 0 OR kek_encrypted IS NOT NULL)
3330
+ )`);
3331
+ const integrity = this.all("PRAGMA integrity_check");
3332
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3333
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3334
+ }
3335
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3336
+ if (foreignKeys.length > 0)
3337
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3338
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (84, ?)", new Date().toISOString());
3339
+ });
3340
+ }
3341
+ if (!applied.has(85)) {
3342
+ this.transaction(() => {
3343
+ const columns = new Set(this.all("PRAGMA table_info(presence_changes)").map((row) => String(row.name)));
3344
+ if (!columns.has("action")) {
3345
+ this.run("ALTER TABLE presence_changes ADD COLUMN action TEXT NOT NULL DEFAULT 'save' CHECK(action IN ('save', 'delete'))");
3346
+ }
3347
+ if (!columns.has("page_deleted")) {
3348
+ this.run("ALTER TABLE presence_changes ADD COLUMN page_deleted INTEGER NOT NULL DEFAULT 0 CHECK(page_deleted IN (0, 1))");
3349
+ }
3350
+ const integrity = this.all("PRAGMA integrity_check");
3351
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3352
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3353
+ }
3354
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3355
+ if (foreignKeys.length > 0)
3356
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3357
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (85, ?)", new Date().toISOString());
3358
+ });
3359
+ }
3360
+ if (!applied.has(86)) {
3361
+ this.transaction(() => {
3362
+ this.run(`CREATE TABLE work_covers_v86 (
3363
+ work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
3364
+ mime_type TEXT NOT NULL CHECK(mime_type IN ('image/jpeg', 'image/png', 'image/webp', 'image/gif')),
3365
+ content BLOB NOT NULL,
3366
+ byte_length INTEGER NOT NULL,
3367
+ sha256 TEXT NOT NULL,
3368
+ updated_at TEXT NOT NULL
3369
+ )`);
3370
+ this.run("INSERT INTO work_covers_v86 SELECT work_id, mime_type, content, byte_length, sha256, updated_at FROM work_covers");
3371
+ this.run("DROP TABLE work_covers");
3372
+ this.run("ALTER TABLE work_covers_v86 RENAME TO work_covers");
3373
+ this.run(`CREATE TABLE user_avatars_v86 (
3374
+ user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
3375
+ mime_type TEXT NOT NULL CHECK(mime_type IN ('image/png', 'image/jpeg', 'image/webp', 'image/gif')),
3376
+ content BLOB NOT NULL,
3377
+ byte_length INTEGER NOT NULL,
3378
+ sha256 TEXT NOT NULL,
3379
+ width INTEGER NOT NULL,
3380
+ height INTEGER NOT NULL,
3381
+ updated_at TEXT NOT NULL
3382
+ )`);
3383
+ this.run("INSERT INTO user_avatars_v86 SELECT user_id, mime_type, content, byte_length, sha256, width, height, updated_at FROM user_avatars");
3384
+ this.run("DROP TABLE user_avatars");
3385
+ this.run("ALTER TABLE user_avatars_v86 RENAME TO user_avatars");
3386
+ const integrity = this.all("PRAGMA integrity_check");
3387
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3388
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3389
+ }
3390
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3391
+ if (foreignKeys.length > 0)
3392
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3393
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (86, ?)", new Date().toISOString());
3394
+ });
3395
+ }
3396
+ if (!applied.has(87)) {
3397
+ this.transaction(() => {
3398
+ const workColumns = new Set(this.all("PRAGMA table_info(works)").map((row) => String(row.name)));
3399
+ if (!workColumns.has("deleted_at"))
3400
+ this.run("ALTER TABLE works ADD COLUMN deleted_at TEXT");
3401
+ const volumeColumns = new Set(this.all("PRAGMA table_info(volumes)").map((row) => String(row.name)));
3402
+ if (!volumeColumns.has("deleted_at"))
3403
+ this.run("ALTER TABLE volumes ADD COLUMN deleted_at TEXT");
3404
+ const chapterColumns = new Set(this.all("PRAGMA table_info(chapters)").map((row) => String(row.name)));
3405
+ if (!chapterColumns.has("deleted_via_volume_id"))
3406
+ this.run("ALTER TABLE chapters ADD COLUMN deleted_via_volume_id TEXT");
3407
+ this.run("CREATE INDEX IF NOT EXISTS idx_works_recycle_bin ON works(deleted_at, owner_user_id)");
3408
+ this.run("CREATE INDEX IF NOT EXISTS idx_volumes_active_work ON volumes(work_id, deleted_at, sort_order)");
3409
+ this.run("CREATE INDEX IF NOT EXISTS idx_volumes_recycle_bin ON volumes(work_id, deleted_at DESC)");
3410
+ this.run("CREATE INDEX IF NOT EXISTS idx_chapters_deleted_via_volume ON chapters(deleted_via_volume_id, deleted_at)");
3411
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (87, ?)", new Date().toISOString());
3412
+ });
3413
+ const integrity = this.all("PRAGMA integrity_check");
3414
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3415
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3416
+ }
3417
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3418
+ if (foreignKeys.length > 0)
3419
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3420
+ }
3421
+ if (!applied.has(88)) {
3422
+ this.transaction(() => {
3423
+ this.run(`CREATE TABLE IF NOT EXISTS ai_conversation_forks (
3424
+ source_conversation_id TEXT NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE,
3425
+ source_message_id TEXT NOT NULL REFERENCES ai_conversation_messages(id) ON DELETE CASCADE,
3426
+ request_id TEXT NOT NULL CHECK(length(request_id) BETWEEN 1 AND 200),
3427
+ conversation_id TEXT NOT NULL UNIQUE REFERENCES ai_conversations(id) ON DELETE CASCADE,
3428
+ created_at TEXT NOT NULL,
3429
+ PRIMARY KEY(source_conversation_id, request_id)
3430
+ )`);
3431
+ this.run("CREATE INDEX IF NOT EXISTS idx_ai_conversation_forks_message ON ai_conversation_forks(source_message_id)");
3432
+ const integrity = this.all("PRAGMA integrity_check");
3433
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3434
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3435
+ }
3436
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3437
+ if (foreignKeys.length > 0)
3438
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3439
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (88, ?)", new Date().toISOString());
3440
+ });
3441
+ }
3442
+ if (!applied.has(89)) {
3443
+ this.transaction(() => {
3444
+ this.run(`CREATE TABLE IF NOT EXISTS ai_conversation_stream_requests (
3445
+ id TEXT PRIMARY KEY,
3446
+ work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
3447
+ conversation_id TEXT NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE,
3448
+ actor_scope TEXT NOT NULL CHECK(length(actor_scope) BETWEEN 1 AND 200),
3449
+ idempotency_key TEXT NOT NULL CHECK(length(idempotency_key) BETWEEN 16 AND 128),
3450
+ request_hash TEXT NOT NULL CHECK(length(request_hash) = 64),
3451
+ status TEXT NOT NULL CHECK(status IN ('in_progress', 'completed', 'cancelled', 'failed', 'timed_out', 'abandoned')),
3452
+ terminal_reason TEXT,
3453
+ user_message_id TEXT REFERENCES ai_conversation_messages(id) ON DELETE SET NULL,
3454
+ assistant_message_id TEXT REFERENCES ai_conversation_messages(id) ON DELETE SET NULL,
3455
+ lease_expires_at TEXT,
3456
+ created_at TEXT NOT NULL,
3457
+ updated_at TEXT NOT NULL,
3458
+ completed_at TEXT,
3459
+ UNIQUE(actor_scope, work_id, idempotency_key),
3460
+ CHECK(
3461
+ (status = 'in_progress' AND lease_expires_at IS NOT NULL AND completed_at IS NULL)
3462
+ OR (status <> 'in_progress' AND lease_expires_at IS NULL AND completed_at IS NOT NULL)
3463
+ )
3464
+ )`);
3465
+ this.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ai_conversation_stream_requests_active
3466
+ ON ai_conversation_stream_requests(conversation_id) WHERE status = 'in_progress'`);
3467
+ this.run(`CREATE INDEX IF NOT EXISTS idx_ai_conversation_stream_requests_lease
3468
+ ON ai_conversation_stream_requests(status, lease_expires_at)`);
3469
+ const integrity = this.all("PRAGMA integrity_check");
3470
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3471
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3472
+ }
3473
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3474
+ if (foreignKeys.length > 0)
3475
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3476
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (89, ?)", new Date().toISOString());
3477
+ });
3478
+ }
3479
+ if (!applied.has(90)) {
3480
+ this.transaction(() => {
3481
+ this.run(`CREATE TABLE IF NOT EXISTS ai_connectivity_test_states (
3482
+ object_type TEXT NOT NULL CHECK(object_type IN ('provider', 'model')),
3483
+ object_id TEXT NOT NULL,
3484
+ config_fingerprint TEXT NOT NULL CHECK(length(config_fingerprint) = 64),
3485
+ state TEXT NOT NULL CHECK(state IN ('in_progress', 'success', 'failure')),
3486
+ attempt_id TEXT NOT NULL,
3487
+ retry_at_ms INTEGER NOT NULL CHECK(retry_at_ms >= 0),
3488
+ updated_at TEXT NOT NULL,
3489
+ PRIMARY KEY(object_type, object_id)
3490
+ )`);
3491
+ this.run("CREATE INDEX IF NOT EXISTS idx_ai_connectivity_test_states_retry_at ON ai_connectivity_test_states(retry_at_ms)");
3492
+ this.run(`CREATE TRIGGER IF NOT EXISTS ai_connectivity_test_states_provider_delete
3493
+ AFTER DELETE ON providers BEGIN
3494
+ DELETE FROM ai_connectivity_test_states WHERE object_type = 'provider' AND object_id = OLD.id;
3495
+ END`);
3496
+ this.run(`CREATE TRIGGER IF NOT EXISTS ai_connectivity_test_states_model_delete
3497
+ AFTER DELETE ON models BEGIN
3498
+ DELETE FROM ai_connectivity_test_states WHERE object_type = 'model' AND object_id = OLD.id;
3499
+ END`);
3500
+ const integrity = this.all("PRAGMA integrity_check");
3501
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
3502
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
3503
+ }
3504
+ const foreignKeys = this.all("PRAGMA foreign_key_check");
3505
+ if (foreignKeys.length > 0)
3506
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
3507
+ this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (90, ?)", new Date().toISOString());
3508
+ });
3509
+ }
3178
3510
  }
3179
3511
  normalizeCharacterName(value) {
3180
3512
  return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLocaleLowerCase("zh-CN");