重构: 巨函数拆分 + 清理历史标记注释 + custom_prompt/停止按钮/tunnel 改进
This commit is contained in:
@@ -53,7 +53,7 @@ fn ai_provider_from_row(row: &Row<'_>) -> std::result::Result<AiProviderRecord,
|
||||
config: row.get("config")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
// F-260614-04: enabled/weight 列老库经 v19 迁移补建,DEFAULT 1 / DEFAULT 50。
|
||||
// enabled/weight 列老库经 v19 迁移补建,DEFAULT 1 / DEFAULT 50。
|
||||
// from_row 按 i32 取列值兼容(SQLite 无真 BOOLEAN),0→false/非0→true。
|
||||
enabled: row.get::<_, i32>("enabled").unwrap_or(1) != 0,
|
||||
// weight 读侧 clamp [0,100]:与 insert/update_full 落库的 `.min(100)` 对齐,
|
||||
@@ -95,7 +95,7 @@ fn ai_tool_execution_from_row(row: &Row<'_>) -> std::result::Result<AiToolExecut
|
||||
Ok(AiToolExecutionRecord {
|
||||
id: row.get("id")?,
|
||||
conversation_id: row.get("conversation_id")?,
|
||||
// F-260619-04:message_id 列老库经 v21 迁移补建。unwrap_or(None) 兜底:
|
||||
// message_id 列老库经 v21 迁移补建。unwrap_or(None) 兜底:
|
||||
// 新库空表直接有列;老库行 ALTER 后 NULL;极端情况(迁移未跑/手工删列)防御。
|
||||
message_id: row.get("message_id").unwrap_or(None),
|
||||
tool_call_id: row.get("tool_call_id")?,
|
||||
@@ -124,7 +124,7 @@ impl_repo!(
|
||||
let is_default = if rec.is_default { 1i32 } else { 0i32 };
|
||||
// model_configs:Vec<ModelConfig> → JSON 字符串落 TEXT 列
|
||||
let model_configs_json = serde_json::to_string(&rec.model_configs).unwrap_or_else(|_| "[]".into());
|
||||
// F-260614-04: enabled/weight 落库(SQLite 无 BOOLEAN,i32 承载)。
|
||||
// enabled/weight 落库(SQLite 无 BOOLEAN,i32 承载)。
|
||||
let enabled_i = if rec.enabled { 1i32 } else { 0i32 };
|
||||
let weight_i = rec.weight.min(100) as i32;
|
||||
conn.execute(
|
||||
@@ -420,7 +420,7 @@ mod tests {
|
||||
use crate::models::AiProviderRecord;
|
||||
use df_ai_core::model::{Capability, IntelligenceTier, Modality, ModelConfig};
|
||||
|
||||
/// model_configs DB roundtrip + 老库空兼容(F-01 阶段1)
|
||||
/// model_configs DB roundtrip + 老库空兼容
|
||||
#[tokio::test]
|
||||
async fn ai_provider_model_configs_roundtrip_and_old_db_compat() {
|
||||
let db = Database::open_in_memory().await.expect("open_in_memory");
|
||||
|
||||
@@ -20,7 +20,7 @@ use super::{now_millis_str, storage_err, validate_column_name};
|
||||
|
||||
/// `knowledges` 表对应 `KnowledgeRecord` 15 个字段的列名(顺序与结构体一致)。
|
||||
///
|
||||
/// 多处 `search`/`search_vector` 内联 COLS 串的 DRY 收口(CR-260615-03):集中一处定义,
|
||||
/// 多处 `search`/`search_vector` 内联 COLS 串的 DRY 收口:集中一处定义,
|
||||
/// 配合下方 `KNOWLEDGE_COL_COUNT` 断言,任一处加列漏改会被测试 `test_knowledge_cols_matches_record`
|
||||
/// 立即捕获(`knowledge_from_row` 按 name 取列,SELECT 漏列会运行时 rusqlite 报错,故提前断言)。
|
||||
///
|
||||
@@ -38,7 +38,7 @@ const KNOWLEDGE_COLS_WITH_EMBEDDING: &str = concat!(
|
||||
|
||||
/// `ideas` 表对应 `IdeaRecord` 14 个字段的列名(顺序与结构体一致)。
|
||||
///
|
||||
/// 同 KNOWLEDGE_COLS 的列漂移防护(CR-260615-03):idea 表 INSERT/UPDATE/from_row 三处
|
||||
/// 同 KNOWLEDGE_COLS 的列漂移防护:idea 表 INSERT/UPDATE/from_row 三处
|
||||
/// 各写一份列名串,加列须三处同步(如 V24 加 related_ids 即三处齐改),漏一处
|
||||
/// 只在运行时 rusqlite 报错(INSERT 列数与参数数不匹配 / from_row 取不到列)。集中一处
|
||||
/// 定义 + 配合 `IDEA_COL_COUNT` 断言 + 测试 `test_idea_cols_matches_record`,加列漏改即捕获。
|
||||
@@ -147,7 +147,7 @@ fn knowledge_event_from_row(row: &Row<'_>) -> std::result::Result<KnowledgeEvent
|
||||
// ============================================================
|
||||
|
||||
// ============================================================
|
||||
// IdeaQuery — 多条件查询入参(F-260621-02 status 下沉 + 关键词 + 排序 + 分页)
|
||||
// IdeaQuery — 多条件查询入参(status 下沉 + 关键词 + 排序 + 分页)
|
||||
// ============================================================
|
||||
|
||||
/// 灵感多条件查询入参。
|
||||
@@ -256,7 +256,7 @@ impl_repo!(
|
||||
// KnowledgeRepo 的整体更新已由 impl_repo! 宏统一生成的 update_full 提供。
|
||||
|
||||
impl IdeaRepo {
|
||||
/// 多条件查询:动态 WHERE 拼接(status / keyword) + 白名单排序 + 分页(F-260621-02)。
|
||||
/// 多条件查询:动态 WHERE 拼接(status / keyword) + 白名单排序 + 分页。
|
||||
///
|
||||
/// 复用 `KnowledgeRepo::search` 的动态 WHERE 模式:if-let 分支按可选条件拼 SQL 片段,
|
||||
/// 各分支化参数绑定到 `?N` 占位符。`order_by` 经 `validate_idea_order_by` 白名单校验后
|
||||
@@ -925,7 +925,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::db::Database;
|
||||
|
||||
// ---------- COLS 漂移防护(CR-260615-03) ----------
|
||||
// ---------- COLS 漂移防护 ----------
|
||||
|
||||
/// KNOWLEDGE_COLS 列数须等于 KNOWLEDGE_COL_COUNT(任一处漂移:加列漏改 / 串错位 → 立即失败)。
|
||||
/// `knowledge_from_row` 按 name 取列,SELECT 漏列会在运行时被 rusqlite 报错;此断言提前到测试期捕获。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! AI 消息 Repo — ai_messages 表(F-260619-03 消息拆分存储)
|
||||
//! AI 消息 Repo — ai_messages 表(消息拆分存储)
|
||||
//!
|
||||
//! 每条 ChatMessage 一行的独立表,替代 `ai_conversations.messages` 整对话 JSON 列存。
|
||||
//! 全专用方法(insert_batch / list_by_conversation / delete_range / update_status /
|
||||
@@ -239,7 +239,6 @@ impl AiMessageRepo {
|
||||
|
||||
/// 全量重写对话的消息(单事务 DELETE + INSERT OR IGNORE,原子)。
|
||||
///
|
||||
/// F-260619-03 批次 B(save_conversation 写路径切 ai_messages)的核心方法:
|
||||
/// 全量重写语义——以入参 records 为该对话的**唯一真相**,先删该 conv 全部旧行再批量插。
|
||||
/// 单事务保证「删 + 插」原子,无中间空窗(reload 不会读到半删半插的中间态)。
|
||||
///
|
||||
@@ -472,7 +471,7 @@ mod tests {
|
||||
assert_eq!(got[0].content, "替换后的结果", "其他消息不应被改");
|
||||
}
|
||||
|
||||
// ---------- replace_conversation(F-260619-03 批次 B)----------
|
||||
// ---------- replace_conversation ----------
|
||||
|
||||
/// replace_conversation 全量重写:删旧 + 插新原子,list 一致
|
||||
#[tokio::test]
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! - [`mod@conversation_repo`]:AiProviderRepo/AiConversationRepo/AiToolExecutionRepo
|
||||
//! - [`mod@idea_repo`]:IdeaRepo/KnowledgeRepo/KnowledgeEventsRepo + 向量工具
|
||||
//! - [`mod@idea_eval_repo`]:IdeaEvalRepo(灵感评估历史追加型审计表 idea_evaluations,V22)
|
||||
//! - [`mod@message_repo`]:AiMessageRepo(F-260619-03 消息拆分存储,全专用方法不走宏)
|
||||
//! - [`mod@message_repo`]:AiMessageRepo(消息拆分存储,全专用方法不走宏)
|
||||
//!
|
||||
//! re-export(`pub use ...::*`)保持 `df_storage::crud::XxxRepo` /
|
||||
//! `df_storage::crud::is_allowed_column` 路径不变,**调用方零改动**。
|
||||
|
||||
@@ -17,7 +17,7 @@ use super::impl_repo;
|
||||
use super::{normalize_stored_path, now_millis_str, storage_err, validate_column_name};
|
||||
|
||||
// ============================================================
|
||||
// 项目查询入参(F-260621-02 P2/P3 查询维度补全)
|
||||
// 项目查询入参(P2/P3 查询维度补全)
|
||||
// ============================================================
|
||||
|
||||
/// 项目列表查询条件(可选字段,全 None = 当前 list_active 全量行为)。
|
||||
@@ -204,7 +204,7 @@ impl ProjectRepo {
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 按条件查询未删除项目(F-260621-02 P2/P3:关键词搜索 + 排序 + 分页)。
|
||||
/// 按条件查询未删除项目(P2/P3:关键词搜索 + 排序 + 分页)。
|
||||
///
|
||||
/// 复用 `KnowledgeRepo::search` 动态 WHERE 拼接模式:按可选字段 if-let 拼 SQL 子句 +
|
||||
/// 分支化参数绑定。始终排除 `deleted_at IS NOT NULL`(软删),与 `list_active` 行为对齐。
|
||||
|
||||
@@ -119,8 +119,8 @@ impl SettingsRepo {
|
||||
pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> {
|
||||
Some(match table {
|
||||
"ideas" => &[
|
||||
// IDEA-FIX-02: id\created_at 不列入 — 主键与创建时间不可通过通用 update_field 改写
|
||||
// (对标 tasks 白名单 B-260616-16 同款防护,防篡改主键/伪造创建时间)
|
||||
// id\created_at 不列入 — 主键与创建时间不可通过通用 update_field 改写
|
||||
// (对标 tasks 白名单同款防护,防篡改主键/伪造创建时间)
|
||||
"title", "description", "status", "priority", "score", "tags", "source",
|
||||
"promoted_to", "ai_analysis", "scores", "related_ids", "updated_at",
|
||||
],
|
||||
@@ -141,15 +141,15 @@ pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> {
|
||||
// 否则破坏「review_rounds 唯一写入路径」收口、引入旁路写导致计数错乱。
|
||||
"project_id", "title", "description", "priority", "branch_name",
|
||||
"assignee",
|
||||
// workflow_def_id / base_branch: 预留字段,阶段4 Git/workflow_defs 集成前无写入路径。
|
||||
// workflow_def_id / base_branch: 预留字段,Git/workflow_defs 集成前无写入路径。
|
||||
// 当前推进链用硬编码三模板(task_workflow_templates.rs,不建 workflow_defs 表,
|
||||
// tasks.workflow_def_id 留 None),无任何代码写这两列。白名单列入仅为阶段4 预留 +
|
||||
// tasks.workflow_def_id 留 None),无任何代码写这两列。白名单列入仅为预留 +
|
||||
// 允许手动/未来填充,勿判死代码删除。base_branch 同理(code kind 闸门接 git 前预留)。
|
||||
"workflow_def_id", "base_branch",
|
||||
// output_json:ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象
|
||||
// (决策 a:task 中心,产出跟 task 走)。非状态机收口字段,合法可写。
|
||||
"output_json",
|
||||
// idea_id(F-260619-01 任务关联灵感,1对1 单向):任务可关联/解关联一条灵感,
|
||||
// idea_id(任务关联灵感,1对1 单向):任务可关联/解关联一条灵感,
|
||||
// 非状态机收口字段,合法可写(idea_id 存在性由外键约束 + 上层校验兜底)。
|
||||
"idea_id",
|
||||
// 知识图谱 Phase 1 V29 三列(对标设计 §2.1):非状态机收口字段,合法可写。
|
||||
@@ -160,7 +160,7 @@ pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> {
|
||||
// 走专用方法 set_status_for_aggregation,不经通用 update_field。
|
||||
"queue", "parent_id", "content_json",
|
||||
"updated_at",
|
||||
// TODO(B-260616-16): project_id 跨表存在性校验待 commands/task.rs 层补。
|
||||
// TODO: project_id 跨表存在性校验待 commands/task.rs 层补。
|
||||
// 通用 CRUD 层(db repo)只懂表/列语义,不持有跨表业务约束(查 projects 表存在性)。
|
||||
// project_id 当前可在白名单内改写,合法目标存在性由上层命令层校验。
|
||||
],
|
||||
@@ -218,7 +218,7 @@ pub(crate) fn validate_column_name(field: &str, table: &str) -> Result<()> {
|
||||
match allowed_columns_for(table) {
|
||||
Some(cols) if cols.contains(&field) => Ok(()),
|
||||
Some(_) => Err(Error::Storage(format!("表 {} 不允许的字段名: {}", table, field))),
|
||||
// 未登记表保守拒绝(FR-S6: 原放行 Ok,若未来未登记表走通用查询路径,列名直进字符串拼接即 SQL 注入;与 is_allowed_column 的 None=>false 对齐)
|
||||
// 未登记表保守拒绝:原放行 Ok,若未来未登记表走通用查询路径,列名直进字符串拼接即 SQL 注入;与 is_allowed_column 的 None=>false 对齐
|
||||
None => Err(Error::Storage(format!("表 {} 未登记列白名单,拒绝防注入", table))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ fn task_from_row(row: &Row<'_>) -> std::result::Result<TaskRecord, rusqlite::Err
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 任务列表查询入参(F-260621-02 查询维度补全)
|
||||
// 任务列表查询入参(查询维度补全)
|
||||
// ============================================================
|
||||
|
||||
/// 任务列表动态查询入参。全可选,空 query = 等价当前全量行为(向后兼容)。
|
||||
@@ -213,7 +213,7 @@ impl TaskRepo {
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 原子推进任务状态(任务推进链 F-260616-02 唯一 status 写入路径)
|
||||
/// 原子推进任务状态(任务推进链唯一 status 写入路径)
|
||||
///
|
||||
/// 下沉 SQL `WHERE id=? AND status=?expected` 做 CAS(Compare-And-Swap)防 TOCTOU:
|
||||
/// 并发推进/旁路修改若已改 status,affected_rows==0,本方法返回 None,调用方
|
||||
@@ -296,7 +296,7 @@ impl TaskRepo {
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 动态条件列出未删除任务(F-260621-02 查询维度补全)。
|
||||
/// 动态条件列出未删除任务(查询维度补全)。
|
||||
///
|
||||
/// 复用 KnowledgeRepo::search 的「动态 WHERE + 参数绑定」模式,但用累积式条件收集
|
||||
/// (Vec<String> WHERE 子句 + Vec<rusqlite::Value> 参数)替代 if-let 二分支——
|
||||
@@ -528,7 +528,7 @@ impl TaskRepo {
|
||||
///
|
||||
/// 本方法返回 `Vec<(status, count)>`(SQL GROUP BY 一次查询,数据量小 ~50 无压力),
|
||||
/// 聚合规则的具体判定由调用方(commands 层)实现 —— 本层只提供原始计数,不持有
|
||||
/// 业务聚合逻辑(CRUD 层只懂表/列语义,对标 B-260616-16 跨表校验下沉思路)。
|
||||
/// 业务聚合逻辑(CRUD 层只懂表/列语义,对标跨表校验下沉思路)。
|
||||
pub async fn count_children_by_status(
|
||||
&self,
|
||||
parent_id: &str,
|
||||
|
||||
@@ -23,12 +23,12 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
|
||||
// 迁移步骤链: 顺序执行,跳过已应用的版本(current_version < N 才跑)。
|
||||
// 新增版本时,在此数组追加一项 (N, migrate_vN) 即可,无需改逻辑。
|
||||
// V20 = F-260619-01(任务关联灵感 idea_id);V21 = 消息拆分存储 + audit message_id;
|
||||
// V20 = 任务关联灵感 idea_id;V21 = 消息拆分存储 + audit message_id;
|
||||
// V22 = 灵感评估历史持久化(idea_evaluations 追加型审计表);
|
||||
// V23 = knowledges.embedding_status 列(嵌入失败可补偿重试);
|
||||
// V24 = ideas.related_ids 列(灵感间关联关系持久化打底);
|
||||
// V25 = idea_evaluations (idea_id, version) 唯一约束(评估版本并发重复兜底);
|
||||
// V26 = F-260621-02 任务索引缺口补全(priority/assignee,对齐 idx_tasks_status 同类索引)。
|
||||
// V26 = 任务索引缺口补全(priority/assignee,对齐 idx_tasks_status 同类索引)。
|
||||
// V27 = TD-260621-05 审批状态统一(ai_tool_executions.status executed→completed,
|
||||
// 对齐 DTO 契约 audit/mod.rs:53 只列 completed + 前端 i18n auditLog.status 无 executed 键 +
|
||||
// 治 AuditLog executed 记录显示错位蓝pending标签+raw"executed")。
|
||||
@@ -290,7 +290,7 @@ fn migrate_v14(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V15: 幂等补 tasks.review_rounds 列(review 退回累计轮数,F-260616-04)
|
||||
/// V15: 幂等补 tasks.review_rounds 列(review 退回累计轮数)
|
||||
///
|
||||
/// 任务推进链状态机退回时累加:in_review→in_progress / testing→in_review 各 +1,
|
||||
/// 由 advance_task(df-nodes::task_advance_node)原子写入。默认 0(从未退回过的任务)。
|
||||
@@ -343,7 +343,7 @@ fn migrate_v17(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V18: 幂等补 ai_providers.model_configs 列(模型能力配置,F-01 阶段1)
|
||||
/// V18: 幂等补 ai_providers.model_configs 列(模型能力配置)
|
||||
///
|
||||
/// 模型 4 维度(模态/能力/价格/智力)+ 路由控制配置 JSON 字符串。TEXT NULL 向后兼容:
|
||||
/// 老库行默认 NULL,from_row 经 deserialize_model_configs 解析为空 Vec(配合 default_model 过渡)。
|
||||
@@ -358,7 +358,7 @@ fn migrate_v18(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V19: 幂等补 ai_providers.enabled + ai_providers.weight 列(F-260614-04 多 Provider 负载均衡池)
|
||||
/// V19: 幂等补 ai_providers.enabled + ai_providers.weight 列(多 Provider 负载均衡池)
|
||||
///
|
||||
/// - `enabled INTEGER NOT NULL DEFAULT 1`:provider 是否进入负载均衡池。
|
||||
/// 老库行迁移后默认 1(所有现存 provider 默认启用,单 provider 路径零变化)。
|
||||
@@ -374,21 +374,21 @@ fn migrate_v19(conn: &Connection) -> Result<()> {
|
||||
"ALTER TABLE ai_providers ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1",
|
||||
[],
|
||||
)?;
|
||||
tracing::info!("v19: 补建 ai_providers.enabled 列(多 Provider 负载均衡池,F-260614-04)");
|
||||
tracing::info!("v19: 补建 ai_providers.enabled 列(多 Provider 负载均衡池)");
|
||||
}
|
||||
if !column_exists(conn, "ai_providers", "weight") {
|
||||
conn.execute(
|
||||
"ALTER TABLE ai_providers ADD COLUMN weight INTEGER NOT NULL DEFAULT 50",
|
||||
[],
|
||||
)?;
|
||||
tracing::info!("v19: 补建 ai_providers.weight 列(多 Provider 负载均衡池,F-260614-04)");
|
||||
tracing::info!("v19: 补建 ai_providers.weight 列(多 Provider 负载均衡池)");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [19])?;
|
||||
tracing::info!("迁移 v19 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V20:幂等补 tasks.idea_id 列(F-260619-01 任务关联灵感,1对1 单向)
|
||||
/// V20:幂等补 tasks.idea_id 列(任务关联灵感,1对1 单向)
|
||||
///
|
||||
/// 任务可关联到一条灵感(任务→灵感单向),复用 projects.idea_id 模式
|
||||
/// (REFERENCES ideas(id) 外键)。TEXT NULL 向后兼容:老库行默认 NULL,TaskRecord
|
||||
@@ -401,7 +401,7 @@ fn migrate_v20(conn: &Connection) -> Result<()> {
|
||||
"ALTER TABLE tasks ADD COLUMN idea_id TEXT REFERENCES ideas(id)",
|
||||
[],
|
||||
)?;
|
||||
tracing::info!("v20: 补建 tasks.idea_id 列(任务关联灵感,F-260619-01)");
|
||||
tracing::info!("v20: 补建 tasks.idea_id 列(任务关联灵感)");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [20])?;
|
||||
tracing::info!("迁移 v20 完成");
|
||||
@@ -433,7 +433,7 @@ fn migrate_v21(conn: &Connection) -> Result<()> {
|
||||
// 1. 建 ai_messages 表(IF NOT EXISTS 幂等)
|
||||
conn.execute_batch(V21_SQL)?;
|
||||
|
||||
// 2. 幂等补 ai_tool_executions.message_id 列(消息级溯源 audit,F-260619-04)
|
||||
// 2. 幂等补 ai_tool_executions.message_id 列(消息级溯源 audit)
|
||||
// 表存在性兜底:run() 正常流程下 V9 已先建该表,但测试/手动调用可能跳过 V9。
|
||||
// 表不存在时跳过 ALTER(新库会由 V9_SQL 建表带 message_id 列;此处只补老库已有表)。
|
||||
let tool_exec_table_exists: bool = conn
|
||||
@@ -564,7 +564,7 @@ fn migrate_v21(conn: &Connection) -> Result<()> {
|
||||
///
|
||||
/// 不登记通用列白名单(allowed_columns_for):本表走专用 list_by_idea,
|
||||
/// 宏生成的 query/update_field 未登记表会被 validate_column_name 保守拒绝
|
||||
/// (FR-S6),与追加型审计语义一致(历史不改),不开放通用写路径。
|
||||
/// 与追加型审计语义一致(历史不改),不开放通用写路径。
|
||||
fn migrate_v22(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(V22_SQL)?;
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [22])?;
|
||||
@@ -661,7 +661,7 @@ fn migrate_v25(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V26:补建 tasks 表 priority/assignee 索引(F-260621-02 索引缺口)
|
||||
/// V26:补建 tasks 表 priority/assignee 索引(索引缺口)
|
||||
///
|
||||
/// list_by_query 已支持 priority/assignee 过滤下推(TaskQuery.priority/assignee),
|
||||
/// 但缺索引 → 数据量增长后全表扫描。补建索引对齐已有的 idx_tasks_status /
|
||||
@@ -1219,7 +1219,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
priority INTEGER NOT NULL DEFAULT 2,
|
||||
branch_name TEXT,
|
||||
assignee TEXT,
|
||||
-- F-260619-01 任务关联灵感(1对1 单向,复用 projects.idea_id 模式)。老库由 V20 迁移补列。
|
||||
-- 任务关联灵感(1对1 单向,复用 projects.idea_id 模式)。老库由 V20 迁移补列。
|
||||
idea_id TEXT REFERENCES ideas(id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
@@ -1265,7 +1265,7 @@ CREATE TABLE IF NOT EXISTS node_executions (
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
||||
-- V26 补建(F-260621-02 索引缺口):priority/assignee 过滤下推索引,新库一次性建;
|
||||
-- V26 补建(索引缺口):priority/assignee 过滤下推索引,新库一次性建;
|
||||
-- 老库由 migrate_v26 CREATE INDEX IF NOT EXISTS 补建,两边索引定义须一致。
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee);
|
||||
@@ -1395,7 +1395,7 @@ CREATE TABLE IF NOT EXISTS ai_tool_executions (
|
||||
decided_by TEXT
|
||||
);
|
||||
|
||||
-- F-260619-03 消息拆分存储:每条 ChatMessage 一行的独立表。
|
||||
-- 消息拆分存储:每条 ChatMessage 一行的独立表。
|
||||
-- 与 V21 迁移建表 SQL 镜像(V21 用于老库 ALTER,此处给新库直接建最终态)。
|
||||
-- 改动须两边同步(V21_SQL 见下方)。
|
||||
CREATE TABLE IF NOT EXISTS ai_messages (
|
||||
@@ -1451,7 +1451,7 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
||||
";
|
||||
|
||||
// ============================================================
|
||||
// 单元测试 — V21 迁移幂等安全(新库/老库/坏数据三态,F-260619-03)
|
||||
// 单元测试 — V21 迁移幂等安全(新库/老库/坏数据三态)
|
||||
// ============================================================
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1673,7 +1673,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// V20 迁移幂等安全(F-260619-01 任务关联灵感)
|
||||
// V20 迁移幂等安全(任务关联灵感)
|
||||
// ============================================================
|
||||
|
||||
/// 构造最小老库 schema:tasks 表(无 idea_id 列,模拟 V1 建表老形态)+ schema_version。
|
||||
|
||||
@@ -94,7 +94,7 @@ pub struct TaskRecord {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub output_json: Option<String>,
|
||||
/// 关联灵感 ID(F-260619-01,1对1 单向,任务→灵感)。
|
||||
/// 关联灵感 ID(1对1 单向,任务→灵感)。
|
||||
/// 复用 projects.idea_id 模式(REFERENCES ideas(id) 外键),可空(任务可不关联灵感)。
|
||||
/// 老任务无 idea_id → None。#[serde(default)] 兼容旧前端 JSON(无该字段时为 None)。
|
||||
#[serde(default)]
|
||||
@@ -291,7 +291,7 @@ pub struct AiProviderRecord {
|
||||
pub base_url: String,
|
||||
pub default_model: String,
|
||||
pub models: Option<String>, // JSON array of model names
|
||||
/// 模型能力配置数组(F-01 阶段1,4 维度 + 路由控制)。
|
||||
/// 模型能力配置数组(4 维度 + 路由控制)。
|
||||
/// DB TEXT 列存 JSON 字符串,from_row 经 deserialize_model_configs 解析。
|
||||
/// 向后兼容:老库 NULL/空/老字符串数组 → 空 Vec 或转默认 ModelConfig。default_model 保留过渡。
|
||||
#[serde(default, deserialize_with = "deserialize_model_configs")]
|
||||
@@ -300,11 +300,11 @@ pub struct AiProviderRecord {
|
||||
pub config: Option<String>, // JSON extra config
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
/// provider 是否进入负载均衡池(F-260614-04)。false = 仅作为配置存在,不参与主链路由/选池。
|
||||
/// provider 是否进入负载均衡池。false = 仅作为配置存在,不参与主链路由/选池。
|
||||
/// 老库迁移默认 1(单 provider 路径零变化)。
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
/// provider 在负载均衡池中的选择权重(0-100,F-260614-04)。高权重优先被选为主;
|
||||
/// provider 在负载均衡池中的选择权重(0-100)。高权重优先被选为主;
|
||||
/// 同权重时退化近似轮询。老库迁移默认 50。
|
||||
#[serde(default = "default_weight")]
|
||||
pub weight: u32,
|
||||
@@ -371,9 +371,9 @@ pub struct AiConversationRecord {
|
||||
pub struct AiToolExecutionRecord {
|
||||
pub id: String,
|
||||
pub conversation_id: Option<String>,
|
||||
/// 消息级溯源:工具调用所属的 ChatMessage.id(F-260619-04)。
|
||||
/// 消息级溯源:工具调用所属的 ChatMessage.id。
|
||||
/// NULL = 老库行 / 消息级溯源未启用期的记录 / 无法关联的调用。
|
||||
/// 升级后,audit 写入从 ContextManager 取当前 assistant 消息 id 填入(P1 接入,P0 只建列)。
|
||||
/// 升级后,audit 写入从 ContextManager 取当前 assistant 消息 id 填入。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub message_id: Option<String>,
|
||||
pub tool_call_id: String,
|
||||
@@ -387,7 +387,7 @@ pub struct AiToolExecutionRecord {
|
||||
pub decided_by: Option<String>, // human/auto
|
||||
}
|
||||
|
||||
/// 消息记录(ai_messages 表,F-260619-03 消息拆分存储)。
|
||||
/// 消息记录(ai_messages 表,消息拆分存储)。
|
||||
///
|
||||
/// 每条 ChatMessage 一行,替代 `ai_conversations.messages` 的整对话 JSON 列存。
|
||||
/// 主键 `id` = ChatMessage.id(构造时 ULID 风格生成),(conversation_id, seq) UNIQUE
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! FR-S1 api_key 密钥管理 — 真实密钥存 OS keyring,DB `api_key` 列迁移后存空串。
|
||||
//! api_key 密钥管理 — 真实密钥存 OS keyring,DB `api_key` 列迁移后存空串。
|
||||
//!
|
||||
//! **下沉层(方案 B,2026-06-16)**:原位于 `src-tauri/src/commands/ai/secret.rs`,
|
||||
//! 下沉纯密钥逻辑(get/set/delete/resolve/ensure/migrate + failcount sidecar)到 df-storage,
|
||||
@@ -67,7 +67,7 @@ fn write_failcounts(map: &HashMap<String, u32>) {
|
||||
text.push('\n');
|
||||
}
|
||||
if let Err(e) = fs::write(failcount_path(), text) {
|
||||
tracing::debug!("[FR-S1] 迁移失败计数文件写入失败(忽略): {}", e);
|
||||
tracing::debug!("[密钥迁移] 迁移失败计数文件写入失败(忽略): {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,14 +172,14 @@ pub async fn migrate_secrets_to_keyring(repo: &AiProviderRepo) -> anyhow::Result
|
||||
let n = record_migration_fail(&p.id);
|
||||
if n >= MIGRATION_FAIL_THRESHOLD {
|
||||
tracing::warn!(
|
||||
"[FR-S1] provider {} keyring 迁移已连续失败 {} 次,明文 api_key 长期滞留 SQLite 文件(无加密)。\
|
||||
"[密钥迁移] provider {} keyring 迁移已连续失败 {} 次,明文 api_key 长期滞留 SQLite 文件(无加密)。\
|
||||
建议:1) 确认 OS 钥匙串可用(Win Credential Manager / macOS Keychain);\
|
||||
2) keyring 后端异常时排查对应平台后端;3) 必要时手动在设置中重新保存密钥触发写入",
|
||||
p.id, n
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"[FR-S1] keyring 迁移失败 {} (累计 {}/{},保留明文下次重试): {}",
|
||||
"[密钥迁移] keyring 迁移失败 {} (累计 {}/{},保留明文下次重试): {}",
|
||||
p.id, n, MIGRATION_FAIL_THRESHOLD, e
|
||||
);
|
||||
}
|
||||
@@ -188,14 +188,14 @@ pub async fn migrate_secrets_to_keyring(repo: &AiProviderRepo) -> anyhow::Result
|
||||
let pid = p.id.clone();
|
||||
p.api_key.clear();
|
||||
if let Err(e) = repo.insert(p).await {
|
||||
tracing::warn!("[FR-S1] 迁移后清空 DB api_key 失败 {}: {}", pid, e);
|
||||
tracing::warn!("[密钥迁移] 迁移后清空 DB api_key 失败 {}: {}", pid, e);
|
||||
}
|
||||
// 迁移成功 → 清零该 provider 的失败计数(下次若再出现失败从 1 重新累计)
|
||||
clear_migration_failcount(&pid);
|
||||
migrated += 1;
|
||||
}
|
||||
if migrated > 0 {
|
||||
tracing::info!("[FR-S1] {} 条 provider 密钥迁移至 OS keyring", migrated);
|
||||
tracing::info!("[密钥迁移] {} 条 provider 密钥迁移至 OS keyring", migrated);
|
||||
}
|
||||
Ok(migrated)
|
||||
}
|
||||
@@ -241,7 +241,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn resolve_prefers_db_when_non_empty() {
|
||||
// DB api_key 非空 → 直接返回 DB 值,不触发 keyring(FR-S1 兼容未迁移老库)
|
||||
// DB api_key 非空 → 直接返回 DB 值,不触发 keyring(兼容未迁移老库)
|
||||
// 纯逻辑路径,不碰 OS keyring,CI 任意 OS 安全。
|
||||
let rec = AiProviderRecord {
|
||||
id: "t1".into(), name: "t".into(), provider_type: "openai_compat".into(),
|
||||
|
||||
Reference in New Issue
Block a user