修复: 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:
2026-06-27 21:32:12 +08:00
parent a6d692270f
commit 9c3f27f4ca
26 changed files with 743 additions and 104 deletions

View File

@@ -5,3 +5,4 @@ mod adversarial_helpers;
pub mod capture;
pub mod promotion;
pub mod scoring;
pub mod scoring_keywords;

View File

@@ -11,29 +11,14 @@ pub use crate::capture::IdeaScores;
use df_types::types::Priority;
// ─── 启发式关键词(DEC-03 c:同文件 const 拆出,接 LLM 语义评分后升 a:JSON 文件配置)───
// ─── 启发式关键词(从 `scoring_keywords` 模块导入)───
//
// 现阶段关键词集合固定且与维度强相关,抽 const 仅为可读性/可维护性(零行为变化)
// TODO(DEC-03 a):接入 LLM 语义评分后,这些关键词退居「冷启动兜底」,
// 集合迁移到 JSON 配置文件,支持热更新与项目级覆盖。
// 注:以下集合与原方法内字面量逐一对应,集合/顺序/否定前缀/权重均不变(纯结构提取)。
/// 可行性正向信号词(描述充实度外的技术/资源信号),命中每个 +0.5
pub const FEASIBILITY_POSITIVE_KEYWORDS: &[&str] = &[
"复用", "已有", "简单", "集成", "支持", "成熟", "基于", "现成", "脚手架", "模板",
];
/// 可行性负向信号词(复杂度上升信号),命中每个 -0.6
pub const FEASIBILITY_NEGATIVE_KEYWORDS: &[&str] = &[
"重构", "迁移", "大规模", "分布式", "重写", "从零", "全新架构", "高并发", "底层",
];
/// 影响力价值信号词(用户/增长/收入...),命中每个 +0.5
pub const IMPACT_KEYWORDS: &[&str] = &[
"用户", "增长", "收入", "效率", "体验", "核心", "关键", "痛点", "竞品", "留存",
];
/// 紧急度时效信号词(立即/马上/紧急...),命中每个 +0.5
pub const URGENCY_KEYWORDS: &[&str] = &[
"立即", "马上", "紧急", "尽快", "本周", "上线", "deadline", "截止", "先行", "阻塞",
];
// 关键词常量已拆至独立文件 `scoring_keywords.rs`
// 启发式是过渡方案,对抗式评估上线后按需升级为 JSON 配置。
pub use crate::scoring_keywords::{
FEASIBILITY_POSITIVE_KEYWORDS, FEASIBILITY_NEGATIVE_KEYWORDS,
IMPACT_KEYWORDS, URGENCY_KEYWORDS,
};
/// 评分权重配置
#[derive(Debug, Clone)]

View File

@@ -0,0 +1,24 @@
//! 评分引擎关键词 — 从 `scoring.rs` 拆出的独立常量文件。
//!
//! 启发式评分是过渡方案(对抗式评估上线后按需升级为 JSON 配置),
//! 此处仅做最小改动:将硬编码关键词集中于此,保持 `scoring.rs` 逻辑清晰。
/// 可行性正向信号词(描述充实度外的技术/资源信号),命中每个 +0.5
pub const FEASIBILITY_POSITIVE_KEYWORDS: &[&str] = &[
"复用", "已有", "简单", "集成", "支持", "成熟", "基于", "现成", "脚手架", "模板",
];
/// 可行性负向信号词(复杂度上升信号),命中每个 -0.6
pub const FEASIBILITY_NEGATIVE_KEYWORDS: &[&str] = &[
"重构", "迁移", "大规模", "分布式", "重写", "从零", "全新架构", "高并发", "底层",
];
/// 影响力价值信号词(用户/增长/收入...),命中每个 +0.5
pub const IMPACT_KEYWORDS: &[&str] = &[
"用户", "增长", "收入", "效率", "体验", "核心", "关键", "痛点", "竞品", "留存",
];
/// 紧急度时效信号词(立即/马上/紧急...),命中每个 +0.5
pub const URGENCY_KEYWORDS: &[&str] = &[
"立即", "马上", "紧急", "尽快", "本周", "上线", "deadline", "截止", "先行", "阻塞",
];

View File

@@ -0,0 +1,30 @@
# df-relay 跨端 WS 中继反向代理(miniapp ↔ 桌面端)
# wss 在 nginx 终止(复用 *.1216.top 通配符证书),明文 ws 转 df-relay :9180
# HTTP → HTTPS 重定向
server {
listen 80;
server_name u-work.1216.top;
return 301 https://$host$request_uri;
}
# HTTPS wss 反代
server {
listen 443 ssl;
server_name u-work.1216.top;
include /etc/nginx/snippets/ssl-1216.conf;
# df-relay 两个 WS 端点原样转发(/ws/device + /ws/miniapp)
# proxy_pass 不带路径后缀 → 完整 URI 透传给 df-relay
location /ws/ {
proxy_pass http://127.0.0.1:9180;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
}

View File

@@ -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>> {