新增: 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:
2026-06-14 14:08:20 +08:00
parent 98393b4908
commit cf017f81e2
167 changed files with 19549 additions and 6886 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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
);
";

View File

@@ -36,6 +36,10 @@ pub struct ProjectRecord {
pub description: String,
pub status: String,
pub idea_id: Option<String>,
/// 绑定的本地代码目录(绝对路径,可空=未绑定,第二步导入历史项目时复用)
pub path: Option<String>,
/// 技术栈 JSON 数组字符串(如 ["rust","vue","tauri"],由探测填充,可空)
pub stack: Option<String>,
pub created_at: String,
pub updated_at: String,
}
@@ -149,6 +153,10 @@ pub struct AiConversationRecord {
pub messages: String, // JSON array of ChatMessage
pub provider_id: Option<String>,
pub model: Option<String>,
pub models: Option<String>, // 用过的所有 model(JSON 数组字符串,去重)
pub archived: bool, // 是否归档(侧栏折叠展示)
pub prompt_tokens: Option<i64>, // 输入 token 累计(流式 usage 落库)
pub completion_tokens: Option<i64>, // 输出 token 累计(流式 usage 落库)
pub created_at: String,
pub updated_at: String,
}
@@ -168,3 +176,37 @@ pub struct AiToolExecutionRecord {
pub executed_at: Option<String>,
pub decided_by: Option<String>, // human/auto
}
// ============================================================
// 知识库模型 (V7)
// ============================================================
/// 知识条目记录(经验沉淀基本单元,共享记忆层)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeRecord {
pub id: String,
pub kind: String, // KnowledgeKind snake_case(7 种)
pub title: String,
pub content: String,
pub tags: Option<String>, // JSON 数组字符串
pub status: String, // candidate|pending_review|published|archived
pub confidence: Option<String>, // high|medium|low(AI 提炼自评,可空)
pub reuse_count: i32, // 唯一客观排序信号
pub verified: bool, // 发布审核时一次性人工标
pub source_project: Option<String>, // 来源项目(仅溯源不过滤)
pub source_ref: Option<String>, // 来源实体引用(如 conv:{id})
pub reasoning: Option<String>, // AI 提炼判断依据("为何值得沉淀"),手动录入为 None
pub created_at: String,
pub updated_at: String,
}
/// 知识生命线事件记录(追加型审计:产生/审核/引用/归档)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeEventRecord {
pub id: String,
pub knowledge_id: String,
pub event_type: String, // created | extracted | status_changed | referenced | archived
pub source_ref: Option<String>, // 触发来源: conv:{id} / manual / system
pub context_json: Option<String>, // JSON: 因 event_type 而异
pub timestamp: String,
}