修复: workspace_root 编译期常量消除+data_dir 运行期+首次授权引导
- 删除 production 中全部 env!("CARGO_MANIFEST_DIR")引用
- workspace_root() 替换为运行期 data_dir(Tauri app_data_dir)
- 相对路径锚定改为 AllowedDirs.first_persistent_dir(),无授权时引导绑定项目
- .trash 迁到 data_dir/.trash,含自动迁移
- authz_debug → temp_dir,workspace_drive → current_dir
- run_command 默认 working_dir 改为空串
- G1 目标刷新改为每次消息覆盖(含 ai_chat_edit 路径)
- #6 评分关键词拆到独立文件 scoring_keywords.rs
- #7 promote 补偿删除 purge_with_descendants → soft_delete
- #8 关联双向同步: 后端事务 sync_related_ids + IPC + 前端全链路
- 设置面板新增数据目录显示
This commit is contained in:
@@ -73,6 +73,17 @@ fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
|
||||
dot / (norm_a * norm_b + 1e-8)
|
||||
}
|
||||
|
||||
/// 解析 JSON 数组字符串为 Vec<String>。NULL / 空 / 非法 → 空 Vec。
|
||||
fn parse_json_id_array(raw: &Option<String>) -> Vec<String> {
|
||||
match raw {
|
||||
Some(s) if !s.is_empty() => match serde_json::from_str::<Vec<String>>(s) {
|
||||
Ok(v) => v,
|
||||
Err(_) => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// from_row 辅助函数
|
||||
// ============================================================
|
||||
@@ -385,6 +396,120 @@ impl IdeaRepo {
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 双向同步关联关系:原子地更新主体灵感及其所有关联目标的 `related_ids`。
|
||||
///
|
||||
/// `subject_id` 的 `related_ids` 被设为 `new_target_ids`(全量替换);
|
||||
/// 新增的关联目标追加 `subject_id` 到其 `related_ids`;
|
||||
/// 移除的关联目标从中删除 `subject_id`。
|
||||
/// 全部操作在同一 SQLite 事务中完成,保证原子性。
|
||||
pub async fn sync_related_ids(
|
||||
&self,
|
||||
subject_id: &str,
|
||||
new_target_ids: &[String],
|
||||
) -> Result<()> {
|
||||
use rusqlite::Transaction;
|
||||
let conn = self.conn.clone();
|
||||
let subject_id = subject_id.to_owned();
|
||||
let new_target_ids = new_target_ids.to_vec();
|
||||
let now = now_millis_str();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut guard = conn.blocking_lock();
|
||||
let tx: Transaction = guard
|
||||
.transaction()
|
||||
.map_err(storage_err)?;
|
||||
|
||||
// 1. 读主体当前 related_ids
|
||||
let old_raw: Option<String> = tx
|
||||
.query_row(
|
||||
"SELECT related_ids FROM ideas WHERE id = ?1",
|
||||
params![subject_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(storage_err)?
|
||||
.flatten();
|
||||
|
||||
// 2. 解析新旧集合
|
||||
let old_set: std::collections::HashSet<String> =
|
||||
parse_json_id_array(&old_raw).into_iter().collect();
|
||||
let new_set: std::collections::HashSet<String> =
|
||||
new_target_ids.iter().cloned().collect();
|
||||
|
||||
let added: Vec<&str> = new_set
|
||||
.difference(&old_set)
|
||||
.map(|s| s.as_str())
|
||||
.filter(|id| *id != subject_id) // 不自关联
|
||||
.collect();
|
||||
let removed: Vec<&str> = old_set
|
||||
.difference(&new_set)
|
||||
.map(|s| s.as_str())
|
||||
.filter(|id| *id != subject_id)
|
||||
.collect();
|
||||
|
||||
// 3. 更新 added 目标:追加 subject_id
|
||||
for target_id in &added {
|
||||
let cur: Option<String> = tx
|
||||
.query_row(
|
||||
"SELECT related_ids FROM ideas WHERE id = ?1",
|
||||
params![target_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(storage_err)?
|
||||
.flatten();
|
||||
let mut ids: Vec<String> = parse_json_id_array(&cur);
|
||||
if !ids.iter().any(|i| i == &subject_id) {
|
||||
ids.push(subject_id.clone());
|
||||
}
|
||||
let json = serde_json::to_string(&ids).map_err(|e| {
|
||||
storage_err::<df_types::error::Error>(e.into())
|
||||
})?;
|
||||
tx.execute(
|
||||
"UPDATE ideas SET related_ids = ?1, updated_at = ?2 WHERE id = ?3",
|
||||
params![json, &now, target_id],
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
}
|
||||
|
||||
// 4. 更新 removed 目标:移除 subject_id
|
||||
for target_id in &removed {
|
||||
let cur: Option<String> = tx
|
||||
.query_row(
|
||||
"SELECT related_ids FROM ideas WHERE id = ?1",
|
||||
params![target_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(storage_err)?
|
||||
.flatten();
|
||||
let mut ids: Vec<String> = parse_json_id_array(&cur);
|
||||
ids.retain(|i| i != &subject_id);
|
||||
let json = serde_json::to_string(&ids).map_err(|e| {
|
||||
storage_err::<df_types::error::Error>(e.into())
|
||||
})?;
|
||||
tx.execute(
|
||||
"UPDATE ideas SET related_ids = ?1, updated_at = ?2 WHERE id = ?3",
|
||||
params![json, &now, target_id],
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
}
|
||||
|
||||
// 5. 更新主体
|
||||
let new_json = serde_json::to_string(&new_target_ids)
|
||||
.map_err(|e| storage_err::<df_types::error::Error>(e.into()))?;
|
||||
tx.execute(
|
||||
"UPDATE ideas SET related_ids = ?1, updated_at = ?2 WHERE id = ?3",
|
||||
params![new_json, &now, &subject_id],
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
|
||||
tx.commit().map_err(storage_err)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 列出回收站(deleted_at IS NOT NULL),按更新时间(≈删除时间)降序。
|
||||
/// 对标 TaskRepo::list_deleted / ProjectRepo::list_deleted。
|
||||
pub async fn list_deleted(&self) -> Result<Vec<IdeaRecord>> {
|
||||
|
||||
Reference in New Issue
Block a user