@musnows/scriverse 0.9.4 → 0.9.6
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/ai-protocol.js +6 -0
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai-skills.js +134 -0
- package/dist/ai-skills.js.map +1 -0
- package/dist/ai-write-plans.js +2279 -0
- package/dist/ai-write-plans.js.map +1 -0
- package/dist/ai.js +2781 -154
- package/dist/ai.js.map +1 -1
- package/dist/app.js +328 -17
- package/dist/app.js.map +1 -1
- package/dist/chapter-annotation-anchor.js +327 -0
- package/dist/chapter-annotation-anchor.js.map +1 -0
- package/dist/chapter-title-numbering.js +56 -0
- package/dist/chapter-title-numbering.js.map +1 -0
- package/dist/database.js +478 -3
- package/dist/database.js.map +1 -1
- package/dist/public/ai-context-meter.js +3 -3
- package/dist/public/ai-interactive.js +483 -0
- package/dist/public/app.js +1991 -268
- package/dist/public/chapter-editor-behavior.js +40 -0
- package/dist/public/chapter-line-id-tracker.d.ts +25 -0
- package/dist/public/chapter-line-id-tracker.js +151 -0
- package/dist/public/index.html +79 -19
- package/dist/public/model-config.d.ts +1 -0
- package/dist/public/model-config.js +9 -4
- package/dist/public/styles.css +337 -57
- package/dist/public/theme-init.js +25 -1
- package/dist/remote-mcp.js +496 -0
- package/dist/remote-mcp.js.map +1 -0
- package/dist/roleplay-memory.js +53 -0
- package/dist/roleplay-memory.js.map +1 -0
- package/dist/security.js +4 -0
- package/dist/security.js.map +1 -1
- package/dist/semantic-search.js +225 -0
- package/dist/semantic-search.js.map +1 -0
- package/dist/skills/continue-writing/SKILL.md +23 -0
- package/dist/skills/polish-writing/SKILL.md +23 -0
- package/dist/store.js +695 -51
- package/dist/store.js.map +1 -1
- package/dist/ui-module-preload.js +27 -0
- package/dist/ui-module-preload.js.map +1 -0
- package/dist/user-auth.js +33 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
package/dist/database.js
CHANGED
|
@@ -4,12 +4,13 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
4
4
|
import { DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS, MAX_AI_ANALYSIS_TIMEOUT_SECONDS, MIN_AI_ANALYSIS_TIMEOUT_SECONDS } from "./ai-analysis-timeout.js";
|
|
5
5
|
import { documentParagraphLineRanges } from "./hybrid-search.js";
|
|
6
6
|
import { logger, sanitizeError } from "./logger.js";
|
|
7
|
-
import { documentShortSearchTerms, normalizeDocumentSearchText, splitDocumentParagraphs } from "./utils.js";
|
|
7
|
+
import { documentShortSearchTerms, id, normalizeDocumentSearchText, splitDocumentParagraphs } from "./utils.js";
|
|
8
|
+
import { createChapterLineIds, parseChapterAnnotationLineHashes, parseChapterLineIds } from "./chapter-annotation-anchor.js";
|
|
8
9
|
export const PLATFORM_AI_WORK_ID = "__scriverse_platform_ai__";
|
|
9
10
|
export const SYSTEM_USER_ID = "__scriverse_system_user__";
|
|
10
|
-
// 版本 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 创建;版本 120 将书籍资料收藏按用户隔离并增加书籍级共享置顶;版本 121 强制作品 Owner
|
|
11
|
+
// 版本 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 创建;版本 120 将书籍资料收藏按用户隔离并增加书籍级共享置顶;版本 121 强制作品 Owner 非空并建立用户外键约束;版本 122 创建 AI 写入审批与持久化提问表,并增加按角色归属的共享角色扮演记忆及作品级正文编辑偏好;版本 123 为正文评论持久化逐行哈希锚点;版本 124 为正文行和评论锚点持久化稳定行身份;版本 125 增加显式语义检索配置、分片索引、模型类型与上下文快照;版本 126 为作品增加加密的远程 MCP 配置与工具目录。
|
|
11
12
|
export const ENTITY_VERSION_BASELINE_MIGRATION_VERSION = 82;
|
|
12
|
-
export const DATABASE_SCHEMA_VERSION =
|
|
13
|
+
export const DATABASE_SCHEMA_VERSION = 126;
|
|
13
14
|
export const SQLITE_IOERR_SHMSIZE = 4874;
|
|
14
15
|
export function isSqliteDiskIoError(error) {
|
|
15
16
|
if (!(error instanceof Error))
|
|
@@ -188,6 +189,8 @@ export class Database {
|
|
|
188
189
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
189
190
|
is_internal INTEGER NOT NULL DEFAULT 0,
|
|
190
191
|
offline_access_enabled INTEGER NOT NULL DEFAULT 0 CHECK(offline_access_enabled IN (0, 1)),
|
|
192
|
+
editor_auto_indent_enabled INTEGER NOT NULL DEFAULT 0 CHECK(editor_auto_indent_enabled IN (0, 1)),
|
|
193
|
+
editor_typewriter_mode_enabled INTEGER NOT NULL DEFAULT 0 CHECK(editor_typewriter_mode_enabled IN (0, 1)),
|
|
191
194
|
version_no INTEGER NOT NULL DEFAULT 1,
|
|
192
195
|
deleted_at TEXT,
|
|
193
196
|
created_at TEXT NOT NULL,
|
|
@@ -565,6 +568,15 @@ export class Database {
|
|
|
565
568
|
updated_at TEXT NOT NULL
|
|
566
569
|
);
|
|
567
570
|
|
|
571
|
+
CREATE TABLE IF NOT EXISTS work_mcp_settings (
|
|
572
|
+
work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
|
|
573
|
+
config_encrypted TEXT NOT NULL,
|
|
574
|
+
config_iv TEXT NOT NULL,
|
|
575
|
+
config_tag TEXT NOT NULL,
|
|
576
|
+
tool_catalog_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(tool_catalog_json) AND json_type(tool_catalog_json) = 'array'),
|
|
577
|
+
updated_at TEXT NOT NULL
|
|
578
|
+
);
|
|
579
|
+
|
|
568
580
|
CREATE TABLE IF NOT EXISTS ai_calls (
|
|
569
581
|
id TEXT PRIMARY KEY,
|
|
570
582
|
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
@@ -636,6 +648,61 @@ export class Database {
|
|
|
636
648
|
created_at TEXT NOT NULL
|
|
637
649
|
);
|
|
638
650
|
|
|
651
|
+
CREATE TABLE IF NOT EXISTS roleplay_memories (
|
|
652
|
+
id TEXT PRIMARY KEY,
|
|
653
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
654
|
+
character_id TEXT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
|
655
|
+
category TEXT NOT NULL CHECK(category IN ('event', 'state', 'relationship', 'commitment', 'knowledge', 'scene')),
|
|
656
|
+
content TEXT NOT NULL CHECK(length(content) BETWEEN 1 AND 2000),
|
|
657
|
+
content_hash TEXT NOT NULL CHECK(length(content_hash) = 64),
|
|
658
|
+
importance TEXT NOT NULL DEFAULT 'medium' CHECK(importance IN ('low', 'medium', 'high')),
|
|
659
|
+
certainty TEXT NOT NULL DEFAULT 'experienced' CHECK(certainty IN ('experienced', 'observed', 'heard', 'believed')),
|
|
660
|
+
origin TEXT NOT NULL DEFAULT 'roleplay' CHECK(origin = 'roleplay'),
|
|
661
|
+
canonical INTEGER NOT NULL DEFAULT 0 CHECK(canonical = 0),
|
|
662
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'superseded', 'archived')),
|
|
663
|
+
is_pinned INTEGER NOT NULL DEFAULT 0 CHECK(is_pinned IN (0, 1)),
|
|
664
|
+
version_no INTEGER NOT NULL DEFAULT 1 CHECK(version_no > 0),
|
|
665
|
+
superseded_by_memory_id TEXT REFERENCES roleplay_memories(id) ON DELETE SET NULL,
|
|
666
|
+
source_type TEXT NOT NULL CHECK(source_type IN ('ai', 'manual')),
|
|
667
|
+
source_assistant_message_id TEXT REFERENCES ai_conversation_messages(id) ON DELETE SET NULL,
|
|
668
|
+
idempotency_key TEXT,
|
|
669
|
+
created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
670
|
+
updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
671
|
+
created_at TEXT NOT NULL,
|
|
672
|
+
updated_at TEXT NOT NULL,
|
|
673
|
+
UNIQUE(character_id, content_hash),
|
|
674
|
+
UNIQUE(character_id, idempotency_key)
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
CREATE TABLE IF NOT EXISTS roleplay_memory_sources (
|
|
678
|
+
id TEXT PRIMARY KEY,
|
|
679
|
+
memory_id TEXT NOT NULL REFERENCES roleplay_memories(id) ON DELETE CASCADE,
|
|
680
|
+
conversation_id TEXT REFERENCES ai_conversations(id) ON DELETE SET NULL,
|
|
681
|
+
message_id TEXT REFERENCES ai_conversation_messages(id) ON DELETE SET NULL,
|
|
682
|
+
message_role TEXT NOT NULL CHECK(message_role IN ('user', 'assistant')),
|
|
683
|
+
source_created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
684
|
+
source_created_at TEXT NOT NULL,
|
|
685
|
+
evidence_snapshot TEXT NOT NULL CHECK(length(evidence_snapshot) <= 2000),
|
|
686
|
+
created_at TEXT NOT NULL,
|
|
687
|
+
UNIQUE(memory_id, message_id)
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
CREATE TABLE IF NOT EXISTS roleplay_memory_versions (
|
|
691
|
+
id TEXT PRIMARY KEY,
|
|
692
|
+
memory_id TEXT NOT NULL REFERENCES roleplay_memories(id) ON DELETE CASCADE,
|
|
693
|
+
version_no INTEGER NOT NULL CHECK(version_no > 0),
|
|
694
|
+
category TEXT NOT NULL CHECK(category IN ('event', 'state', 'relationship', 'commitment', 'knowledge', 'scene')),
|
|
695
|
+
content TEXT NOT NULL CHECK(length(content) BETWEEN 1 AND 2000),
|
|
696
|
+
importance TEXT NOT NULL CHECK(importance IN ('low', 'medium', 'high')),
|
|
697
|
+
certainty TEXT NOT NULL CHECK(certainty IN ('experienced', 'observed', 'heard', 'believed')),
|
|
698
|
+
status TEXT NOT NULL CHECK(status IN ('active', 'superseded', 'archived')),
|
|
699
|
+
is_pinned INTEGER NOT NULL CHECK(is_pinned IN (0, 1)),
|
|
700
|
+
action TEXT NOT NULL CHECK(action IN ('created', 'edited', 'pinned', 'archived', 'restored', 'superseded', 'merged')),
|
|
701
|
+
actor_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
702
|
+
created_at TEXT NOT NULL,
|
|
703
|
+
UNIQUE(memory_id, version_no)
|
|
704
|
+
);
|
|
705
|
+
|
|
639
706
|
CREATE TABLE IF NOT EXISTS ai_conversation_stream_requests (
|
|
640
707
|
id TEXT PRIMARY KEY,
|
|
641
708
|
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
@@ -817,6 +884,12 @@ export class Database {
|
|
|
817
884
|
CREATE INDEX IF NOT EXISTS idx_ai_suggestions_work ON ai_suggestions(work_id, status, created_at DESC);
|
|
818
885
|
CREATE INDEX IF NOT EXISTS idx_ai_conversations_work ON ai_conversations(work_id, updated_at DESC);
|
|
819
886
|
CREATE INDEX IF NOT EXISTS idx_ai_conversation_messages ON ai_conversation_messages(conversation_id, created_at);
|
|
887
|
+
CREATE INDEX IF NOT EXISTS idx_roleplay_memories_character
|
|
888
|
+
ON roleplay_memories(work_id, character_id, status, is_pinned DESC, importance DESC, updated_at DESC);
|
|
889
|
+
CREATE INDEX IF NOT EXISTS idx_roleplay_memory_sources_memory
|
|
890
|
+
ON roleplay_memory_sources(memory_id, created_at, id);
|
|
891
|
+
CREATE INDEX IF NOT EXISTS idx_roleplay_memory_versions_memory
|
|
892
|
+
ON roleplay_memory_versions(memory_id, version_no DESC);
|
|
820
893
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_ai_conversation_stream_requests_active
|
|
821
894
|
ON ai_conversation_stream_requests(conversation_id) WHERE status = 'in_progress';
|
|
822
895
|
CREATE INDEX IF NOT EXISTS idx_ai_conversation_stream_requests_lease
|
|
@@ -4494,6 +4567,408 @@ export class Database {
|
|
|
4494
4567
|
finally {
|
|
4495
4568
|
this.raw.exec("PRAGMA foreign_keys = ON");
|
|
4496
4569
|
}
|
|
4570
|
+
}
|
|
4571
|
+
const aiWritePlanTablesPresent = [
|
|
4572
|
+
"work_ai_tool_settings",
|
|
4573
|
+
"ai_write_plans",
|
|
4574
|
+
"ai_write_plan_operations",
|
|
4575
|
+
"ai_user_questions"
|
|
4576
|
+
].every((table) => this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?", table) !== undefined);
|
|
4577
|
+
if (!applied.has(122) || !aiWritePlanTablesPresent) {
|
|
4578
|
+
this.transaction(() => {
|
|
4579
|
+
const conversationColumns = new Set(this.all("PRAGMA table_info(ai_conversations)").map((row) => String(row.name)));
|
|
4580
|
+
if (!conversationColumns.has("ai_write_tools_json")) {
|
|
4581
|
+
this.run("ALTER TABLE ai_conversations ADD COLUMN ai_write_tools_json TEXT");
|
|
4582
|
+
}
|
|
4583
|
+
// 作品级 AI 可写工具开关:默认全部关闭,仅拥有 AI 设置权限的用户可修改。
|
|
4584
|
+
this.run(`
|
|
4585
|
+
CREATE TABLE IF NOT EXISTS work_ai_tool_settings (
|
|
4586
|
+
work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
|
|
4587
|
+
tools_json TEXT NOT NULL DEFAULT '{}',
|
|
4588
|
+
updated_at TEXT NOT NULL,
|
|
4589
|
+
updated_by_user_id TEXT
|
|
4590
|
+
)
|
|
4591
|
+
`);
|
|
4592
|
+
// AI 修改计划(审批):AI 只能提交计划,确认接口只接收审批 ID。
|
|
4593
|
+
this.run(`
|
|
4594
|
+
CREATE TABLE IF NOT EXISTS ai_write_plans (
|
|
4595
|
+
id TEXT PRIMARY KEY,
|
|
4596
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
4597
|
+
conversation_id TEXT,
|
|
4598
|
+
plan_kind TEXT NOT NULL DEFAULT 'write' CHECK(plan_kind IN ('write', 'undo')),
|
|
4599
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected', 'expired', 'invalidated', 'executing', 'executed', 'failed')),
|
|
4600
|
+
ai_summary TEXT NOT NULL DEFAULT '',
|
|
4601
|
+
max_operations INTEGER NOT NULL,
|
|
4602
|
+
invalid_reason TEXT NOT NULL DEFAULT '',
|
|
4603
|
+
failure_message TEXT,
|
|
4604
|
+
initiator_user_id TEXT,
|
|
4605
|
+
conversation_owner_user_id TEXT,
|
|
4606
|
+
source_plan_id TEXT,
|
|
4607
|
+
created_at TEXT NOT NULL,
|
|
4608
|
+
decided_at TEXT,
|
|
4609
|
+
executed_at TEXT,
|
|
4610
|
+
executed_by_user_id TEXT
|
|
4611
|
+
)
|
|
4612
|
+
`);
|
|
4613
|
+
// 计划操作明细:创建时由系统根据当前数据库内容生成不可变 diff,执行成功后补充结果列。
|
|
4614
|
+
this.run(`
|
|
4615
|
+
CREATE TABLE IF NOT EXISTS ai_write_plan_operations (
|
|
4616
|
+
id TEXT PRIMARY KEY,
|
|
4617
|
+
plan_id TEXT NOT NULL REFERENCES ai_write_plans(id) ON DELETE CASCADE,
|
|
4618
|
+
seq INTEGER NOT NULL,
|
|
4619
|
+
op_type TEXT NOT NULL CHECK(op_type IN ('create_entry', 'update_entry', 'create_annotation', 'create_task')),
|
|
4620
|
+
module TEXT NOT NULL,
|
|
4621
|
+
entity_type TEXT NOT NULL DEFAULT '',
|
|
4622
|
+
entity_id TEXT,
|
|
4623
|
+
target_version_no INTEGER,
|
|
4624
|
+
title TEXT NOT NULL DEFAULT '',
|
|
4625
|
+
operation_input_json TEXT NOT NULL DEFAULT '{}',
|
|
4626
|
+
detail_json TEXT NOT NULL DEFAULT '{}',
|
|
4627
|
+
required_modules_json TEXT NOT NULL DEFAULT '[]',
|
|
4628
|
+
result_entity_id TEXT,
|
|
4629
|
+
result_version_no INTEGER,
|
|
4630
|
+
result_summary TEXT NOT NULL DEFAULT ''
|
|
4631
|
+
)
|
|
4632
|
+
`);
|
|
4633
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_ai_write_plans_work ON ai_write_plans(work_id, created_at)");
|
|
4634
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_ai_write_plans_initiator ON ai_write_plans(initiator_user_id)");
|
|
4635
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_ai_write_plans_owner ON ai_write_plans(conversation_owner_user_id)");
|
|
4636
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_ai_write_plan_operations_plan ON ai_write_plan_operations(plan_id, seq)");
|
|
4637
|
+
// AskUserQuestions 提问记录:一次一个问题,持久化保存,支持刷新后继续查看。
|
|
4638
|
+
this.run(`
|
|
4639
|
+
CREATE TABLE IF NOT EXISTS ai_user_questions (
|
|
4640
|
+
id TEXT PRIMARY KEY,
|
|
4641
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
4642
|
+
conversation_id TEXT,
|
|
4643
|
+
initiator_user_id TEXT,
|
|
4644
|
+
recipient_user_id TEXT,
|
|
4645
|
+
question TEXT NOT NULL,
|
|
4646
|
+
options_json TEXT NOT NULL DEFAULT '[]',
|
|
4647
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'answered', 'rejected', 'expired')),
|
|
4648
|
+
selected_option INTEGER,
|
|
4649
|
+
answer_text TEXT NOT NULL DEFAULT '',
|
|
4650
|
+
is_custom_answer INTEGER NOT NULL DEFAULT 0,
|
|
4651
|
+
tool_call_id TEXT,
|
|
4652
|
+
continuation_json TEXT NOT NULL DEFAULT '{}',
|
|
4653
|
+
resume_state TEXT NOT NULL DEFAULT 'pending' CHECK(resume_state IN ('pending', 'claimed', 'completed', 'failed')),
|
|
4654
|
+
resume_result_json TEXT NOT NULL DEFAULT '{}',
|
|
4655
|
+
resumed_at TEXT,
|
|
4656
|
+
created_at TEXT NOT NULL,
|
|
4657
|
+
expires_at TEXT NOT NULL,
|
|
4658
|
+
decided_at TEXT
|
|
4659
|
+
)
|
|
4660
|
+
`);
|
|
4661
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_ai_user_questions_conversation ON ai_user_questions(conversation_id, status)");
|
|
4662
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_ai_user_questions_work ON ai_user_questions(work_id, created_at)");
|
|
4663
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (122, ?)", new Date().toISOString());
|
|
4664
|
+
});
|
|
4665
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
4666
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4667
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
4668
|
+
}
|
|
4669
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
4670
|
+
if (foreignKeys.length > 0)
|
|
4671
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4672
|
+
}
|
|
4673
|
+
const roleplayMemoryTablesPresent = [
|
|
4674
|
+
"roleplay_memories",
|
|
4675
|
+
"roleplay_memory_sources",
|
|
4676
|
+
"roleplay_memory_versions",
|
|
4677
|
+
"roleplay_memory_fts"
|
|
4678
|
+
].every((name) => this.get("SELECT 1 AS present FROM sqlite_master WHERE name = ?", name) !== undefined);
|
|
4679
|
+
const roleplayMemoryColumns = new Set(this.all("PRAGMA table_info(roleplay_memories)").map((column) => String(column.name)));
|
|
4680
|
+
if (!applied.has(122) || !roleplayMemoryTablesPresent || !roleplayMemoryColumns.has("character_id") || !roleplayMemoryColumns.has("content_hash")) {
|
|
4681
|
+
this.transaction(() => {
|
|
4682
|
+
this.run(`CREATE TABLE IF NOT EXISTS roleplay_memories (
|
|
4683
|
+
id TEXT PRIMARY KEY,
|
|
4684
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
4685
|
+
character_id TEXT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
|
4686
|
+
category TEXT NOT NULL CHECK(category IN ('event', 'state', 'relationship', 'commitment', 'knowledge', 'scene')),
|
|
4687
|
+
content TEXT NOT NULL CHECK(length(content) BETWEEN 1 AND 2000),
|
|
4688
|
+
content_hash TEXT NOT NULL CHECK(length(content_hash) = 64),
|
|
4689
|
+
importance TEXT NOT NULL DEFAULT 'medium' CHECK(importance IN ('low', 'medium', 'high')),
|
|
4690
|
+
certainty TEXT NOT NULL DEFAULT 'experienced' CHECK(certainty IN ('experienced', 'observed', 'heard', 'believed')),
|
|
4691
|
+
origin TEXT NOT NULL DEFAULT 'roleplay' CHECK(origin = 'roleplay'),
|
|
4692
|
+
canonical INTEGER NOT NULL DEFAULT 0 CHECK(canonical = 0),
|
|
4693
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'superseded', 'archived')),
|
|
4694
|
+
is_pinned INTEGER NOT NULL DEFAULT 0 CHECK(is_pinned IN (0, 1)),
|
|
4695
|
+
version_no INTEGER NOT NULL DEFAULT 1 CHECK(version_no > 0),
|
|
4696
|
+
superseded_by_memory_id TEXT REFERENCES roleplay_memories(id) ON DELETE SET NULL,
|
|
4697
|
+
source_type TEXT NOT NULL CHECK(source_type IN ('ai', 'manual')),
|
|
4698
|
+
source_assistant_message_id TEXT REFERENCES ai_conversation_messages(id) ON DELETE SET NULL,
|
|
4699
|
+
idempotency_key TEXT,
|
|
4700
|
+
created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
4701
|
+
updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
4702
|
+
created_at TEXT NOT NULL,
|
|
4703
|
+
updated_at TEXT NOT NULL,
|
|
4704
|
+
UNIQUE(character_id, content_hash),
|
|
4705
|
+
UNIQUE(character_id, idempotency_key)
|
|
4706
|
+
)`);
|
|
4707
|
+
this.run(`CREATE TABLE IF NOT EXISTS roleplay_memory_sources (
|
|
4708
|
+
id TEXT PRIMARY KEY,
|
|
4709
|
+
memory_id TEXT NOT NULL REFERENCES roleplay_memories(id) ON DELETE CASCADE,
|
|
4710
|
+
conversation_id TEXT REFERENCES ai_conversations(id) ON DELETE SET NULL,
|
|
4711
|
+
message_id TEXT REFERENCES ai_conversation_messages(id) ON DELETE SET NULL,
|
|
4712
|
+
message_role TEXT NOT NULL CHECK(message_role IN ('user', 'assistant')),
|
|
4713
|
+
source_created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
4714
|
+
source_created_at TEXT NOT NULL,
|
|
4715
|
+
evidence_snapshot TEXT NOT NULL CHECK(length(evidence_snapshot) <= 2000),
|
|
4716
|
+
created_at TEXT NOT NULL,
|
|
4717
|
+
UNIQUE(memory_id, message_id)
|
|
4718
|
+
)`);
|
|
4719
|
+
this.run(`CREATE TABLE IF NOT EXISTS roleplay_memory_versions (
|
|
4720
|
+
id TEXT PRIMARY KEY,
|
|
4721
|
+
memory_id TEXT NOT NULL REFERENCES roleplay_memories(id) ON DELETE CASCADE,
|
|
4722
|
+
version_no INTEGER NOT NULL CHECK(version_no > 0),
|
|
4723
|
+
category TEXT NOT NULL CHECK(category IN ('event', 'state', 'relationship', 'commitment', 'knowledge', 'scene')),
|
|
4724
|
+
content TEXT NOT NULL CHECK(length(content) BETWEEN 1 AND 2000),
|
|
4725
|
+
importance TEXT NOT NULL CHECK(importance IN ('low', 'medium', 'high')),
|
|
4726
|
+
certainty TEXT NOT NULL CHECK(certainty IN ('experienced', 'observed', 'heard', 'believed')),
|
|
4727
|
+
status TEXT NOT NULL CHECK(status IN ('active', 'superseded', 'archived')),
|
|
4728
|
+
is_pinned INTEGER NOT NULL CHECK(is_pinned IN (0, 1)),
|
|
4729
|
+
action TEXT NOT NULL CHECK(action IN ('created', 'edited', 'pinned', 'archived', 'restored', 'superseded', 'merged')),
|
|
4730
|
+
actor_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
4731
|
+
created_at TEXT NOT NULL,
|
|
4732
|
+
UNIQUE(memory_id, version_no)
|
|
4733
|
+
)`);
|
|
4734
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_roleplay_memories_character ON roleplay_memories(work_id, character_id, status, is_pinned DESC, importance DESC, updated_at DESC)");
|
|
4735
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_roleplay_memory_sources_memory ON roleplay_memory_sources(memory_id, created_at, id)");
|
|
4736
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_roleplay_memory_versions_memory ON roleplay_memory_versions(memory_id, version_no DESC)");
|
|
4737
|
+
this.raw.exec(`
|
|
4738
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS roleplay_memory_fts USING fts5(
|
|
4739
|
+
memory_id UNINDEXED,
|
|
4740
|
+
character_id UNINDEXED,
|
|
4741
|
+
category UNINDEXED,
|
|
4742
|
+
status UNINDEXED,
|
|
4743
|
+
content,
|
|
4744
|
+
tokenize='trigram'
|
|
4745
|
+
);
|
|
4746
|
+
CREATE TRIGGER IF NOT EXISTS roleplay_memory_fts_ai AFTER INSERT ON roleplay_memories BEGIN
|
|
4747
|
+
INSERT INTO roleplay_memory_fts(memory_id, character_id, category, status, content)
|
|
4748
|
+
VALUES (new.id, new.character_id, new.category, new.status, lower(new.content));
|
|
4749
|
+
END;
|
|
4750
|
+
CREATE TRIGGER IF NOT EXISTS roleplay_memory_fts_ad AFTER DELETE ON roleplay_memories BEGIN
|
|
4751
|
+
DELETE FROM roleplay_memory_fts WHERE memory_id = old.id;
|
|
4752
|
+
END;
|
|
4753
|
+
CREATE TRIGGER IF NOT EXISTS roleplay_memory_fts_au AFTER UPDATE OF character_id, category, status, content ON roleplay_memories BEGIN
|
|
4754
|
+
DELETE FROM roleplay_memory_fts WHERE memory_id = old.id;
|
|
4755
|
+
INSERT INTO roleplay_memory_fts(memory_id, character_id, category, status, content)
|
|
4756
|
+
VALUES (new.id, new.character_id, new.category, new.status, lower(new.content));
|
|
4757
|
+
END;
|
|
4758
|
+
`);
|
|
4759
|
+
this.run("DELETE FROM roleplay_memory_fts");
|
|
4760
|
+
this.run(`INSERT INTO roleplay_memory_fts(memory_id, character_id, category, status, content)
|
|
4761
|
+
SELECT id, character_id, category, status, lower(content) FROM roleplay_memories`);
|
|
4762
|
+
const timestamp = new Date().toISOString();
|
|
4763
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (122, ?)", timestamp);
|
|
4764
|
+
});
|
|
4765
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
4766
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4767
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
4768
|
+
}
|
|
4769
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
4770
|
+
if (foreignKeys.length > 0)
|
|
4771
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4772
|
+
}
|
|
4773
|
+
const workPreferenceColumns = new Set(this.all("PRAGMA table_info(works)").map((column) => column.name));
|
|
4774
|
+
if (!applied.has(122) || !workPreferenceColumns.has("editor_auto_indent_enabled") || !workPreferenceColumns.has("editor_typewriter_mode_enabled")) {
|
|
4775
|
+
this.transaction(() => {
|
|
4776
|
+
if (!workPreferenceColumns.has("editor_auto_indent_enabled")) {
|
|
4777
|
+
this.run("ALTER TABLE works ADD COLUMN editor_auto_indent_enabled INTEGER NOT NULL DEFAULT 0 CHECK(editor_auto_indent_enabled IN (0, 1))");
|
|
4778
|
+
}
|
|
4779
|
+
if (!workPreferenceColumns.has("editor_typewriter_mode_enabled")) {
|
|
4780
|
+
this.run("ALTER TABLE works ADD COLUMN editor_typewriter_mode_enabled INTEGER NOT NULL DEFAULT 0 CHECK(editor_typewriter_mode_enabled IN (0, 1))");
|
|
4781
|
+
}
|
|
4782
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (122, ?)", new Date().toISOString());
|
|
4783
|
+
});
|
|
4784
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
4785
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4786
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
4787
|
+
}
|
|
4788
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
4789
|
+
if (foreignKeys.length > 0)
|
|
4790
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4791
|
+
}
|
|
4792
|
+
const chapterAnnotationLineHashesPresent = this.all("PRAGMA table_info(chapter_annotations)")
|
|
4793
|
+
.some((column) => column.name === "line_hashes_json");
|
|
4794
|
+
if (!applied.has(123) || !chapterAnnotationLineHashesPresent) {
|
|
4795
|
+
this.transaction(() => {
|
|
4796
|
+
if (!chapterAnnotationLineHashesPresent) {
|
|
4797
|
+
this.run("ALTER TABLE chapter_annotations ADD COLUMN line_hashes_json TEXT NOT NULL DEFAULT '[]'");
|
|
4798
|
+
}
|
|
4799
|
+
const annotations = this.all("SELECT id, quote, line_hashes_json FROM chapter_annotations");
|
|
4800
|
+
for (const annotation of annotations) {
|
|
4801
|
+
this.run("UPDATE chapter_annotations SET line_hashes_json = ? WHERE id = ?", JSON.stringify(parseChapterAnnotationLineHashes(annotation.line_hashes_json, annotation.quote)), annotation.id);
|
|
4802
|
+
}
|
|
4803
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (123, ?)", new Date().toISOString());
|
|
4804
|
+
});
|
|
4805
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
4806
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4807
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
4808
|
+
}
|
|
4809
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
4810
|
+
if (foreignKeys.length > 0)
|
|
4811
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4812
|
+
}
|
|
4813
|
+
const chapterLineIdsPresent = this.all("PRAGMA table_info(chapters)")
|
|
4814
|
+
.some((column) => column.name === "line_ids_json");
|
|
4815
|
+
const chapterAnnotationLineIdsPresent = this.all("PRAGMA table_info(chapter_annotations)")
|
|
4816
|
+
.some((column) => column.name === "anchor_line_ids_json");
|
|
4817
|
+
if (!applied.has(124) || !chapterLineIdsPresent || !chapterAnnotationLineIdsPresent) {
|
|
4818
|
+
this.transaction(() => {
|
|
4819
|
+
if (!chapterLineIdsPresent) {
|
|
4820
|
+
this.run("ALTER TABLE chapters ADD COLUMN line_ids_json TEXT NOT NULL DEFAULT '[]'");
|
|
4821
|
+
}
|
|
4822
|
+
if (!chapterAnnotationLineIdsPresent) {
|
|
4823
|
+
this.run("ALTER TABLE chapter_annotations ADD COLUMN anchor_line_ids_json TEXT NOT NULL DEFAULT '[]'");
|
|
4824
|
+
}
|
|
4825
|
+
const chapters = this.all("SELECT id, content, line_ids_json FROM chapters");
|
|
4826
|
+
for (const chapter of chapters) {
|
|
4827
|
+
const existingLineIds = parseChapterLineIds(chapter.line_ids_json, chapter.content);
|
|
4828
|
+
const lineIds = existingLineIds.length > 0
|
|
4829
|
+
? existingLineIds
|
|
4830
|
+
: createChapterLineIds(chapter.content, () => id("chapterLine"));
|
|
4831
|
+
this.run("UPDATE chapters SET line_ids_json = ? WHERE id = ?", JSON.stringify(lineIds), chapter.id);
|
|
4832
|
+
const annotations = this.all("SELECT id, start_line, end_line FROM chapter_annotations WHERE chapter_id = ?", chapter.id);
|
|
4833
|
+
for (const annotation of annotations) {
|
|
4834
|
+
const startIndex = Math.max(0, Math.min(lineIds.length - 1, Number(annotation.start_line) - 1));
|
|
4835
|
+
const endIndex = Math.max(startIndex, Math.min(lineIds.length - 1, Number(annotation.end_line) - 1));
|
|
4836
|
+
this.run("UPDATE chapter_annotations SET anchor_line_ids_json = ? WHERE id = ?", JSON.stringify(lineIds.slice(startIndex, endIndex + 1)), annotation.id);
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (124, ?)", new Date().toISOString());
|
|
4840
|
+
});
|
|
4841
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
4842
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4843
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
4844
|
+
}
|
|
4845
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
4846
|
+
if (foreignKeys.length > 0)
|
|
4847
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4848
|
+
}
|
|
4849
|
+
const modelKindPresent = this.all("PRAGMA table_info(models)")
|
|
4850
|
+
.some((column) => column.name === "model_kind");
|
|
4851
|
+
const semanticSettingsColumns = new Set(this.all("PRAGMA table_info(work_ai_settings)").map((column) => column.name));
|
|
4852
|
+
const semanticIndexPresent = Boolean(this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'semantic_index_entries'"));
|
|
4853
|
+
if (!applied.has(125) || !modelKindPresent || !semanticIndexPresent) {
|
|
4854
|
+
this.transaction(() => {
|
|
4855
|
+
if (!modelKindPresent) {
|
|
4856
|
+
this.run("ALTER TABLE models ADD COLUMN model_kind TEXT NOT NULL DEFAULT 'chat' CHECK(model_kind IN ('chat', 'embedding', 'rerank'))");
|
|
4857
|
+
}
|
|
4858
|
+
const semanticSettings = [
|
|
4859
|
+
["semantic_search_enabled", "INTEGER NOT NULL DEFAULT 0 CHECK(semantic_search_enabled IN (0, 1))"],
|
|
4860
|
+
["semantic_embedding_model_id", "TEXT REFERENCES models(id) ON DELETE SET NULL"],
|
|
4861
|
+
["semantic_rerank_model_id", "TEXT REFERENCES models(id) ON DELETE SET NULL"],
|
|
4862
|
+
["semantic_vector_dimension", "INTEGER NOT NULL DEFAULT 1024 CHECK(semantic_vector_dimension BETWEEN 1 AND 65536)"],
|
|
4863
|
+
["semantic_recall_limit", "INTEGER NOT NULL DEFAULT 20 CHECK(semantic_recall_limit BETWEEN 1 AND 200)"],
|
|
4864
|
+
["semantic_result_limit", "INTEGER NOT NULL DEFAULT 12 CHECK(semantic_result_limit BETWEEN 1 AND 100)"],
|
|
4865
|
+
["semantic_budget_tokens", "INTEGER NOT NULL DEFAULT 4000 CHECK(semantic_budget_tokens BETWEEN 256 AND 100000)"],
|
|
4866
|
+
["semantic_channel_weight", "REAL NOT NULL DEFAULT 1 CHECK(semantic_channel_weight BETWEEN 0.1 AND 5)"]
|
|
4867
|
+
];
|
|
4868
|
+
for (const [column, definition] of semanticSettings) {
|
|
4869
|
+
if (!semanticSettingsColumns.has(column))
|
|
4870
|
+
this.run(`ALTER TABLE work_ai_settings ADD COLUMN ${column} ${definition}`);
|
|
4871
|
+
}
|
|
4872
|
+
this.raw.exec(`
|
|
4873
|
+
CREATE TABLE IF NOT EXISTS semantic_index_entries (
|
|
4874
|
+
id TEXT PRIMARY KEY,
|
|
4875
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
4876
|
+
source_type TEXT NOT NULL CHECK(source_type IN (
|
|
4877
|
+
'chapter', 'setting', 'character', 'race', 'organization', 'timeline-track',
|
|
4878
|
+
'timeline-event', 'relationship', 'chapter-outline', 'foreshadow'
|
|
4879
|
+
)),
|
|
4880
|
+
source_id TEXT NOT NULL,
|
|
4881
|
+
section_id TEXT NOT NULL DEFAULT '',
|
|
4882
|
+
source_version TEXT NOT NULL,
|
|
4883
|
+
source_title TEXT NOT NULL,
|
|
4884
|
+
chunk_order INTEGER NOT NULL CHECK(chunk_order >= 0),
|
|
4885
|
+
start_line INTEGER NOT NULL CHECK(start_line >= 1),
|
|
4886
|
+
end_line INTEGER NOT NULL CHECK(end_line >= start_line),
|
|
4887
|
+
start_offset INTEGER NOT NULL CHECK(start_offset >= 0),
|
|
4888
|
+
end_offset INTEGER NOT NULL CHECK(end_offset >= start_offset),
|
|
4889
|
+
content TEXT NOT NULL,
|
|
4890
|
+
content_hash TEXT NOT NULL,
|
|
4891
|
+
vector_json TEXT NOT NULL CHECK(json_valid(vector_json) AND json_type(vector_json) = 'array'),
|
|
4892
|
+
vector_dimension INTEGER NOT NULL CHECK(vector_dimension BETWEEN 1 AND 65536),
|
|
4893
|
+
embedding_model_id TEXT NOT NULL,
|
|
4894
|
+
config_fingerprint TEXT NOT NULL CHECK(length(config_fingerprint) = 64),
|
|
4895
|
+
chunk_rule_version INTEGER NOT NULL DEFAULT 1 CHECK(chunk_rule_version >= 1),
|
|
4896
|
+
created_at TEXT NOT NULL,
|
|
4897
|
+
UNIQUE(work_id, source_type, source_id, section_id, source_version, chunk_order, config_fingerprint)
|
|
4898
|
+
);
|
|
4899
|
+
CREATE INDEX IF NOT EXISTS idx_semantic_index_entries_recall
|
|
4900
|
+
ON semantic_index_entries(work_id, config_fingerprint, source_type, source_id);
|
|
4901
|
+
CREATE INDEX IF NOT EXISTS idx_semantic_index_entries_source
|
|
4902
|
+
ON semantic_index_entries(work_id, source_type, source_id, section_id, source_version);
|
|
4903
|
+
|
|
4904
|
+
CREATE TABLE IF NOT EXISTS semantic_index_state (
|
|
4905
|
+
work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
|
|
4906
|
+
status TEXT NOT NULL DEFAULT 'idle' CHECK(status IN ('disabled', 'idle', 'building', 'ready', 'failed', 'paused')),
|
|
4907
|
+
config_fingerprint TEXT NOT NULL DEFAULT '',
|
|
4908
|
+
total_sources INTEGER NOT NULL DEFAULT 0 CHECK(total_sources >= 0),
|
|
4909
|
+
processed_sources INTEGER NOT NULL DEFAULT 0 CHECK(processed_sources >= 0),
|
|
4910
|
+
failed_sources INTEGER NOT NULL DEFAULT 0 CHECK(failed_sources >= 0),
|
|
4911
|
+
consecutive_failures INTEGER NOT NULL DEFAULT 0 CHECK(consecutive_failures >= 0),
|
|
4912
|
+
error TEXT NOT NULL DEFAULT '',
|
|
4913
|
+
updated_at TEXT NOT NULL
|
|
4914
|
+
) WITHOUT ROWID;
|
|
4915
|
+
|
|
4916
|
+
CREATE TABLE IF NOT EXISTS semantic_context_snapshots (
|
|
4917
|
+
id TEXT PRIMARY KEY,
|
|
4918
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
4919
|
+
conversation_id TEXT REFERENCES ai_conversations(id) ON DELETE SET NULL,
|
|
4920
|
+
query TEXT NOT NULL,
|
|
4921
|
+
scope_json TEXT NOT NULL CHECK(json_valid(scope_json) AND json_type(scope_json) = 'object'),
|
|
4922
|
+
config_fingerprint TEXT NOT NULL CHECK(length(config_fingerprint) = 64),
|
|
4923
|
+
created_by_user_id TEXT,
|
|
4924
|
+
created_at TEXT NOT NULL
|
|
4925
|
+
);
|
|
4926
|
+
CREATE INDEX IF NOT EXISTS idx_semantic_context_snapshots_work
|
|
4927
|
+
ON semantic_context_snapshots(work_id, created_at DESC);
|
|
4928
|
+
|
|
4929
|
+
CREATE TABLE IF NOT EXISTS semantic_context_snapshot_items (
|
|
4930
|
+
snapshot_id TEXT NOT NULL REFERENCES semantic_context_snapshots(id) ON DELETE CASCADE,
|
|
4931
|
+
position INTEGER NOT NULL CHECK(position >= 0),
|
|
4932
|
+
entry_id TEXT NOT NULL,
|
|
4933
|
+
source_type TEXT NOT NULL,
|
|
4934
|
+
source_id TEXT NOT NULL,
|
|
4935
|
+
section_id TEXT NOT NULL DEFAULT '',
|
|
4936
|
+
source_version TEXT NOT NULL,
|
|
4937
|
+
source_title TEXT NOT NULL,
|
|
4938
|
+
start_line INTEGER NOT NULL CHECK(start_line >= 1),
|
|
4939
|
+
end_line INTEGER NOT NULL CHECK(end_line >= start_line),
|
|
4940
|
+
content TEXT NOT NULL,
|
|
4941
|
+
estimated_tokens INTEGER NOT NULL CHECK(estimated_tokens >= 0),
|
|
4942
|
+
semantic_score REAL NOT NULL,
|
|
4943
|
+
rerank_score REAL,
|
|
4944
|
+
match_kinds_json TEXT NOT NULL CHECK(json_valid(match_kinds_json) AND json_type(match_kinds_json) = 'array'),
|
|
4945
|
+
PRIMARY KEY(snapshot_id, position),
|
|
4946
|
+
UNIQUE(snapshot_id, entry_id)
|
|
4947
|
+
) WITHOUT ROWID;
|
|
4948
|
+
`);
|
|
4949
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (125, ?)", new Date().toISOString());
|
|
4950
|
+
});
|
|
4951
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
4952
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4953
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
4954
|
+
}
|
|
4955
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
4956
|
+
if (foreignKeys.length > 0)
|
|
4957
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4958
|
+
}
|
|
4959
|
+
const workMcpSettingsPresent = this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'work_mcp_settings'") !== undefined;
|
|
4960
|
+
if (!applied.has(126) || !workMcpSettingsPresent) {
|
|
4961
|
+
this.transaction(() => {
|
|
4962
|
+
this.run(`CREATE TABLE IF NOT EXISTS work_mcp_settings (
|
|
4963
|
+
work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
|
|
4964
|
+
config_encrypted TEXT NOT NULL,
|
|
4965
|
+
config_iv TEXT NOT NULL,
|
|
4966
|
+
config_tag TEXT NOT NULL,
|
|
4967
|
+
tool_catalog_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(tool_catalog_json) AND json_type(tool_catalog_json) = 'array'),
|
|
4968
|
+
updated_at TEXT NOT NULL
|
|
4969
|
+
)`);
|
|
4970
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (126, ?)", new Date().toISOString());
|
|
4971
|
+
});
|
|
4497
4972
|
const integrity = this.all("PRAGMA integrity_check");
|
|
4498
4973
|
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4499
4974
|
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|