新增: Phase2 阶段收尾(Sprint 1-20)
重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
@@ -4,7 +4,19 @@ use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
|
||||
/// 当前迁移版本
|
||||
const MIGRATION_VERSION: i32 = 2;
|
||||
const MIGRATION_VERSION: i32 = 12;
|
||||
|
||||
/// 检测表是否存在指定列(幂等迁移通用辅助)
|
||||
fn column_exists(conn: &Connection, table: &str, col: &str) -> bool {
|
||||
let Ok(mut stmt) = conn.prepare(&format!("PRAGMA table_info({table})")) else { return false };
|
||||
let Ok(rows) = stmt.query_map([], |r| r.get::<_, String>(1)) else { return false };
|
||||
for r in rows {
|
||||
if let Ok(name) = r {
|
||||
if name == col { return true; }
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// 执行所有迁移
|
||||
pub fn run(conn: &Connection) -> Result<()> {
|
||||
@@ -31,8 +43,49 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
migrate_v2(conn)?;
|
||||
}
|
||||
|
||||
// 未来迁移在此扩展:
|
||||
// if current_version < 3 { migrate_v3(conn)?; }
|
||||
if current_version < 3 {
|
||||
migrate_v3(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 4 {
|
||||
migrate_v4(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 5 {
|
||||
migrate_v5(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 6 {
|
||||
migrate_v6(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 7 {
|
||||
migrate_v7(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 8 {
|
||||
migrate_v8(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 9 {
|
||||
migrate_v9(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 10 {
|
||||
migrate_v10(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 11 {
|
||||
migrate_v11(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 12 {
|
||||
migrate_v12(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 13 {
|
||||
migrate_v13(conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -53,6 +106,170 @@ fn migrate_v2(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V3: AI 对话表补建(新库) + 归档标记列(新老库统一)
|
||||
fn migrate_v3(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(V3_SQL)?;
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [3])?;
|
||||
tracing::info!("迁移 v3 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V4: 幂等补 ai_conversations.archived 列
|
||||
///
|
||||
/// 修复历史缺陷:早期 v3 迁移仅写入版本号 3,ALTER ADD COLUMN archived 未实际生效,
|
||||
/// 导致 schema_version=3 但 ai_conversations 缺列,from_row 读 archived 报错,
|
||||
/// list_all 失败 → 前端历史会话不显示 + 新对话 insert 失败。
|
||||
/// 因 run() 按 `current_version < 3` 跳过 v3,该列无法靠 v3 自补。
|
||||
/// 此处用 PRAGMA 探测列存在性,缺失才 ALTER,对新库/老库/坏库均安全。
|
||||
fn migrate_v4(conn: &Connection) -> Result<()> {
|
||||
let has_archived = column_exists(conn, "ai_conversations", "archived");
|
||||
if !has_archived {
|
||||
conn.execute(
|
||||
"ALTER TABLE ai_conversations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
)?;
|
||||
tracing::info!("v4: 补建 ai_conversations.archived 列");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [4])?;
|
||||
tracing::info!("迁移 v4 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V5: 幂等补 ai_conversations.prompt_tokens / completion_tokens 列
|
||||
///
|
||||
/// 流式 token 用量记录:对话级累计 input/output token(由 save_conversation 写入)。
|
||||
/// 用 PRAGMA 探测列存在性,缺失才 ALTER,对新库/老库/坏库均安全(同 v4 模式)。
|
||||
fn migrate_v5(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "ai_conversations", "prompt_tokens") {
|
||||
conn.execute("ALTER TABLE ai_conversations ADD COLUMN prompt_tokens INTEGER", [])?;
|
||||
tracing::info!("v5: 补建 ai_conversations.prompt_tokens 列");
|
||||
}
|
||||
if !column_exists(conn, "ai_conversations", "completion_tokens") {
|
||||
conn.execute("ALTER TABLE ai_conversations ADD COLUMN completion_tokens INTEGER", [])?;
|
||||
tracing::info!("v5: 补建 ai_conversations.completion_tokens 列");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [5])?;
|
||||
tracing::info!("迁移 v5 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V6: 幂等补 ai_conversations.models 列
|
||||
///
|
||||
/// 对话级多 model 记录:JSON 数组字符串(去重存对话用过的所有 model)。
|
||||
/// 用 PRAGMA 探测列存在性,缺失才 ALTER(同 v4/v5 模式)。
|
||||
fn migrate_v6(conn: &Connection) -> Result<()> {
|
||||
let has_models = column_exists(conn, "ai_conversations", "models");
|
||||
if !has_models {
|
||||
conn.execute("ALTER TABLE ai_conversations ADD COLUMN models TEXT", [])?;
|
||||
tracing::info!("v6: 补建 ai_conversations.models 列");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [6])?;
|
||||
tracing::info!("迁移 v6 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V7: 知识库表 — 经验沉淀的基本单元(共享记忆层)
|
||||
///
|
||||
/// 状态机: candidate → pending_review → published → archived
|
||||
/// AI 只产 candidate,人工门控发布;reuse_count 是唯一客观排序信号。
|
||||
/// effectiveness 列不建(决策撤销人工评分)。时间字段用毫秒字符串(同既有 model 约定)。
|
||||
fn migrate_v7(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(V7_SQL)?;
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [7])?;
|
||||
tracing::info!("迁移 v7 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V8: 幂等补 knowledges.embedding 列(向量检索)
|
||||
///
|
||||
/// 存 Vec<f32> 的小端字节序列化 BLOB。NULL = 未嵌入(走 LIKE 降级)。
|
||||
/// 用 PRAGMA 探测列存在性,缺失才 ALTER(同 v4/v5/v6 模式)。
|
||||
fn migrate_v8(conn: &Connection) -> Result<()> {
|
||||
let has_embedding = column_exists(conn, "knowledges", "embedding");
|
||||
if !has_embedding {
|
||||
conn.execute("ALTER TABLE knowledges ADD COLUMN embedding BLOB", [])?;
|
||||
tracing::info!("v8: 补建 knowledges.embedding 列");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [8])?;
|
||||
tracing::info!("迁移 v8 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V9: 幂等补建 ai_providers + ai_tool_executions 表
|
||||
///
|
||||
/// 历史遗漏:这两张表从未写入迁移文件(V1-V8 均未包含),
|
||||
/// 旧库可能通过其他方式已建,新库缺失导致 save_provider 等操作报 SQL 错误。
|
||||
/// 用 CREATE TABLE IF NOT EXISTS 幂等,已有表不受影响。
|
||||
fn migrate_v9(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(V9_SQL)?;
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [9])?;
|
||||
tracing::info!("迁移 v9 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V10: 知识生命线 — 补 knowledges.reasoning 列 + 新建 knowledge_events 事件表
|
||||
///
|
||||
/// - reasoning: AI 提炼时给出的"为何值得沉淀"判断依据(此前 prompt 要求但写库丢弃,
|
||||
/// 此处补列修复;老库行默认 NULL,前端降级展示"手动录入/无依据")。幂等(PRAGMA 探测)。
|
||||
/// - knowledge_events: 追加型审计表,记录产生/审核/引用/归档四类事件,支撑生命线视图。
|
||||
/// 独立表(非 JSON 嵌主表): 一条知识可被引用数百次,JSON 嵌入致行膨胀+更新竞争。
|
||||
fn migrate_v10(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "knowledges", "reasoning") {
|
||||
conn.execute("ALTER TABLE knowledges ADD COLUMN reasoning TEXT", [])?;
|
||||
tracing::info!("v10: 补建 knowledges.reasoning 列");
|
||||
}
|
||||
conn.execute_batch(V10_SQL)?;
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [10])?;
|
||||
tracing::info!("迁移 v10 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V11: 幂等补 projects.deleted_at 列(软删回收站)
|
||||
///
|
||||
/// 删除项目改为软删:deleted_at NULL=正常,非空=已进回收站(可恢复)。
|
||||
/// ProjectRecord 不带该字段,纯靠 SQL WHERE deleted_at IS NULL 过滤;
|
||||
/// 子表(tasks/releases/branches)不动,FK 仍满足,项目数据完整保留待恢复。
|
||||
/// 用 PRAGMA 探测列存在性,缺失才 ALTER(同 v4/v5/v6/v8/v10 模式)。
|
||||
fn migrate_v11(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "projects", "deleted_at") {
|
||||
conn.execute("ALTER TABLE projects ADD COLUMN deleted_at TEXT", [])?;
|
||||
tracing::info!("v11: 补建 projects.deleted_at 列(软删回收站)");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [11])?;
|
||||
tracing::info!("迁移 v11 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V12: 幂等补 projects.path / projects.stack 列(项目绑定真实代码目录)
|
||||
///
|
||||
/// 项目与磁盘代码库脱钩是项目管理核心缺失:此版补 path(绑定目录绝对路径) +
|
||||
/// stack(技术栈 JSON 数组字符串),为「绑定目录 + 探测技术栈」打地基,
|
||||
/// 第二步「导入历史项目」直接复用。两列均 nullable,老项目 path/stack=NULL 天然兼容。
|
||||
/// 用 PRAGMA 探测列存在性,缺失才 ALTER(同 v4/v5/v6/v8/v10/v11 模式)。
|
||||
fn migrate_v12(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "projects", "path") {
|
||||
conn.execute("ALTER TABLE projects ADD COLUMN path TEXT", [])?;
|
||||
tracing::info!("v12: 补建 projects.path 列(绑定代码目录)");
|
||||
}
|
||||
if !column_exists(conn, "projects", "stack") {
|
||||
conn.execute("ALTER TABLE projects ADD COLUMN stack TEXT", [])?;
|
||||
tracing::info!("v12: 补建 projects.stack 列(技术栈)");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [12])?;
|
||||
tracing::info!("迁移 v12 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V13: 通用应用设置 KV 表(前端 localStorage 迁移目标)
|
||||
///
|
||||
/// 存主题/语言/AI 偏好/连接配置等,`value` 为 JSON 字符串。CREATE TABLE IF NOT EXISTS 幂等。
|
||||
fn migrate_v13(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(V13_SQL)?;
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [13])?;
|
||||
tracing::info!("迁移 v13 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V1 建表 SQL
|
||||
const V1_SQL: &str = "
|
||||
-- 想法表
|
||||
@@ -172,3 +389,116 @@ CREATE TABLE IF NOT EXISTS branches (
|
||||
CREATE INDEX IF NOT EXISTS idx_branches_project_id ON branches(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_branches_task_id ON branches(task_id);
|
||||
";
|
||||
|
||||
/// V3 迁移 SQL — AI 对话表补建(新库首次创建;老库 IF NOT EXISTS 跳过)
|
||||
///
|
||||
/// 注:archived 列不在此处 ALTER —— 由 v4 迁移幂等补建。
|
||||
/// (历史 v3 曾写入版本号但 ALTER 未生效,统一交 v4 用 PRAGMA 探测修复)
|
||||
const V3_SQL: &str = "
|
||||
CREATE TABLE IF NOT EXISTS ai_conversations (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
messages TEXT NOT NULL DEFAULT '[]',
|
||||
provider_id TEXT,
|
||||
model TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
";
|
||||
|
||||
/// V7 建表 SQL — 知识库表
|
||||
///
|
||||
/// kind: 7 种 KnowledgeKind snake_case(review_rule/prompt_template/pitfall/
|
||||
/// architecture_pattern/diagnosis/deployment_note/workflow_optimization)
|
||||
/// status: candidate|pending_review|published|archived
|
||||
/// confidence: high|medium|low(AI 提炼自评,可空)
|
||||
/// verified: 发布审核时一次性人工标(INTEGER 0/1)
|
||||
/// reuse_count: 检索命中自动 +1(唯一客观排序信号)
|
||||
/// source_project/source_ref: 来源溯源(不过滤,仅展示)
|
||||
const V7_SQL: &str = "
|
||||
CREATE TABLE IF NOT EXISTS knowledges (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL DEFAULT 'pitfall',
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
tags TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'candidate',
|
||||
confidence TEXT,
|
||||
reuse_count INTEGER NOT NULL DEFAULT 0,
|
||||
verified INTEGER NOT NULL DEFAULT 0,
|
||||
source_project TEXT,
|
||||
source_ref TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_status ON knowledges(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_kind ON knowledges(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_reuse_count ON knowledges(reuse_count DESC);
|
||||
";
|
||||
|
||||
/// V9 建表 SQL — AI Provider 配置 + 工具执行审计
|
||||
///
|
||||
/// 历史遗漏补建:ai_providers(AI 提供商配置) + ai_tool_executions(工具调用审计记录)。
|
||||
/// CREATE TABLE IF NOT EXISTS 保证老库(已有表)和新库(缺表)均安全。
|
||||
const V9_SQL: &str = "
|
||||
CREATE TABLE IF NOT EXISTS ai_providers (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
provider_type TEXT NOT NULL DEFAULT 'openai_compat',
|
||||
api_key TEXT NOT NULL,
|
||||
base_url TEXT NOT NULL,
|
||||
default_model TEXT NOT NULL,
|
||||
models TEXT,
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
config TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_tool_executions (
|
||||
id TEXT PRIMARY KEY,
|
||||
conversation_id TEXT,
|
||||
tool_call_id TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
arguments TEXT NOT NULL,
|
||||
result TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
risk_level TEXT NOT NULL DEFAULT 'medium',
|
||||
requested_at TEXT NOT NULL,
|
||||
executed_at TEXT,
|
||||
decided_by TEXT
|
||||
);
|
||||
";
|
||||
|
||||
/// V10 建表 SQL — 知识生命线事件表
|
||||
///
|
||||
/// 追加型审计表(只增不改),记录知识产生/审核/引用/归档四类事件,支撑生命线视图。
|
||||
/// event_type: created | extracted | status_changed | referenced | archived
|
||||
/// context_json: 因 event_type 而异的上下文(如引用事件的 conv_id+query)。
|
||||
const V10_SQL: &str = "
|
||||
CREATE TABLE IF NOT EXISTS knowledge_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
knowledge_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
source_ref TEXT,
|
||||
context_json TEXT,
|
||||
timestamp TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_events_kid ON knowledge_events(knowledge_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_events_type ON knowledge_events(event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_events_kid_type ON knowledge_events(knowledge_id, event_type);
|
||||
";
|
||||
|
||||
/// V13 建表 SQL — 通用应用设置 KV 表
|
||||
///
|
||||
/// 前端 localStorage 迁移目标:key/value(JSON 字符串)+ updated_at。
|
||||
/// CREATE TABLE IF NOT EXISTS 幂等(新库建、老库已有则跳过)。
|
||||
const V13_SQL: &str = "
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
";
|
||||
|
||||
Reference in New Issue
Block a user