修复: 消息溯源P2切读确认完成 + 走查报告高优项

- P2 切读方案文档更新(确认批次B已上线)
- query LIKE 通配符转义(project/idea/task/knowledge 4 repo)
- dag.rs deep_merge null 覆盖全局配置
- dag.rs 通配 match 展开显式变体
- INDEX.md 补漏 10 个文档索引
- ARCHITECTURE.md 删除与新文档逐字重复
- coordinator.rs 加 #[deprecated] 编译守卫
- AiChat.vue 空值传播加 console.warn
This commit is contained in:
2026-06-28 05:05:30 +08:00
parent fee856129c
commit c4b02b5370
9 changed files with 55 additions and 179 deletions

View File

@@ -5,6 +5,7 @@
/// Agent 协调器
///
/// TODO: 实现多 Agent 协作逻辑
#[deprecated(note = "B 路线占位,勿用于生产")]
pub struct AgentCoordinator;
impl AgentCoordinator {

View File

@@ -301,10 +301,11 @@ impl IdeaRepo {
params_vec.push(Box::new(s.clone()));
}
if let Some(kw) = &keyword {
let pat = format!("%{kw}%");
let escaped = kw.replace('%', "\\%").replace('_', "\\_");
let pat = format!("%{escaped}%");
let p1 = params_vec.len() + 1;
let p2 = p1 + 1;
where_clauses.push(format!("(title LIKE ?{p1} OR description LIKE ?{p2})"));
where_clauses.push(format!("(title LIKE ?{p1} OR description LIKE ?{p2}) ESCAPE '\\'"));
params_vec.push(Box::new(pat.clone()));
params_vec.push(Box::new(pat));
}
@@ -545,7 +546,8 @@ impl KnowledgeRepo {
/// 克制检索: top-N≤3(由调用方 limit 控制),精确匹配优先(语义模糊后做)。
pub async fn search(&self, query: &str, kind: Option<&str>, limit: usize) -> Result<Vec<KnowledgeRecord>> {
let conn = self.conn.clone();
let pattern = format!("%{}%", query);
let escaped = query.replace('%', "\\%").replace('_', "\\_");
let pattern = format!("%{escaped}%");
let kind = kind.map(|s| s.to_owned());
let limit_i = limit as i64;
tokio::task::spawn_blocking(move || {
@@ -553,7 +555,7 @@ impl KnowledgeRepo {
let mut results = Vec::new();
if let Some(k) = &kind {
let mut stmt = guard
.prepare(&format!("SELECT {KNOWLEDGE_COLS} FROM knowledges WHERE status = 'published' AND (title LIKE ?1 OR content LIKE ?2) AND kind = ?3 ORDER BY reuse_count DESC LIMIT ?4"))
.prepare(&format!("SELECT {KNOWLEDGE_COLS} FROM knowledges WHERE status = 'published' AND (title LIKE ?1 ESCAPE '\\' OR content LIKE ?2 ESCAPE '\\') AND kind = ?3 ORDER BY reuse_count DESC LIMIT ?4"))
.map_err(storage_err)?;
let rows = stmt
.query_map(params![pattern, pattern, k, limit_i], |row| knowledge_from_row(row))
@@ -563,7 +565,7 @@ impl KnowledgeRepo {
}
} else {
let mut stmt = guard
.prepare(&format!("SELECT {KNOWLEDGE_COLS} FROM knowledges WHERE status = 'published' AND (title LIKE ?1 OR content LIKE ?2) ORDER BY reuse_count DESC LIMIT ?3"))
.prepare(&format!("SELECT {KNOWLEDGE_COLS} FROM knowledges WHERE status = 'published' AND (title LIKE ?1 ESCAPE '\\' OR content LIKE ?2 ESCAPE '\\') ORDER BY reuse_count DESC LIMIT ?3"))
.map_err(storage_err)?;
let rows = stmt
.query_map(params![pattern, pattern, limit_i], |row| knowledge_from_row(row))

View File

@@ -242,8 +242,9 @@ impl ProjectRepo {
if let Some(kw) = &q.keyword {
let trimmed = kw.trim();
if !trimmed.is_empty() {
sql.push_str(" AND (name LIKE ? OR description LIKE ?)");
let pattern = format!("%{trimmed}%");
let escaped = trimmed.replace('%', "\\%").replace('_', "\\_");
sql.push_str(" AND (name LIKE ? OR description LIKE ?) ESCAPE '\\'");
let pattern = format!("%{escaped}%");
params_vec.push(Box::new(pattern.clone()));
params_vec.push(Box::new(pattern));
}

View File

@@ -378,10 +378,11 @@ impl TaskRepo {
}
// keyword: title/description LIKE %kw%(P2,对齐知识库 search 的 LIKE 模式)
if let Some(kw) = &keyword {
let pat = format!("%{kw}%");
let escaped = kw.replace('%', "\\%").replace('_', "\\_");
let pat = format!("%{escaped}%");
let p1 = params_vec.len() + 1;
let p2 = p1 + 1;
where_clauses.push(format!("(title LIKE ?{p1} OR description LIKE ?{p2})"));
where_clauses.push(format!("(title LIKE ?{p1} OR description LIKE ?{p2}) ESCAPE '\\'"));
params_vec.push(Box::new(pat.clone()));
params_vec.push(Box::new(pat));
}

View File

@@ -165,9 +165,10 @@ impl Default for Dag {
///
/// 规则:
/// - 两端均为 Object递归合并同 key 时节点级覆盖全局级,新增 key 各自保留)。
/// - 非两端 Object节点级直接覆盖全局级(包括 `Null`,符合 JSON 显式空语义)
/// - 节点级为 `Null` 的语义是「显式置空」,按非 Object 规则覆盖(直接返回 Null
/// 若调用方希望「跳过节点配置」,应在构造 Dag 时不写入该 keynode_configs 缺失即用全局)
/// - 非 Object/Null 两端:节点级直接覆盖全局级。
/// - 节点级为 `Null` 时跳过(保留全局配置),对应语义「节点未指定该配置」。
/// 若调用方希望「显式置空」,应在构造 Dag 时不写入该 keynode_configs 缺失即用全局)
/// 而非传 JSON null。
///
/// 用途DagExecutor 构造 NodeContext 时 `deep_merge(initial_config, node_config)`
/// 让节点定义里写的 NodeDef.config 优先于 run_workflow 传入的全局 config。
@@ -187,8 +188,15 @@ pub fn deep_merge(global: &serde_json::Value, node: &serde_json::Value) -> serde
}
Value::Object(merged)
}
// 非两端 Object节点级覆盖全局级含 Null、标量、数组
_ => node.clone(),
// 节点级为 Null → 保留全局配置(不覆盖
(_, Value::Null) => global.clone(),
// 显式覆盖剩余变体,防止新变体被 _ 吞没
(Value::Number(_), _)
| (Value::String(_), _)
| (Value::Array(_), _)
| (Value::Bool(_), _)
| (Value::Null, _)
| (Value::Object(_), _) => node.clone(),
}
}
@@ -226,11 +234,10 @@ mod deep_merge_tests {
}
#[test]
fn missing_node_config_treated_as_noop() {
// DagExecutor 对缺失 key 的节点不调 deep_merge(直接用 global此处覆盖语义对照
// 若强行把缺失视作 Null应覆盖显式空语义与「缺失」不同缺失走 if let Some 分支)。
fn null_node_config_keeps_global() {
// 节点级为 Null → 保留全局(不覆盖),区别于「缺失 key 不调 deep_merge」。
let global = json!({"a": 1});
assert_eq!(deep_merge(&global, &json!(null)), json!(null));
assert_eq!(deep_merge(&global, &json!(null)), global);
}
#[test]