新增: secret下沉与provider注入方案设计(方案B纯密钥下沉df-storage避循环依赖)
This commit is contained in:
306
docs/02-架构设计/secret下沉与provider注入方案-2026-06-16.md
Normal file
306
docs/02-架构设计/secret下沉与provider注入方案-2026-06-16.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
# secret 下沉与 provider 注入方案
|
||||||
|
|
||||||
|
> 背景:AiNode 自审闭环②③④⑤已实施(commit c10adaf + 741b0b9),provider 配置注入阻塞⑥端到端联调。
|
||||||
|
> 根因:api_key 明文经 `run_workflow config` 注入 `NodeContext.config` 违背 FR-S1 mask 设计(前端 / LLM tool_call schema 可拿明文 key)。
|
||||||
|
> secret 解析(`ensure_resolved_key` / `resolve_provider_secret`)位于 `src-tauri/src/commands/ai/secret.rs`(app crate),df-nodes 不可依赖 src-tauri(单向 app→df-nodes)。
|
||||||
|
> 范围:**仅设计**,不改任何 .rs/.ts/.vue 代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、现状核验(file:line 证据)
|
||||||
|
|
||||||
|
### 1.1 secret.rs 函数清单(src-tauri/src/commands/ai/secret.rs)
|
||||||
|
|
||||||
|
| 函数 | 位置 | 签名 | 核心逻辑 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_provider_secret` | `secret.rs:88` | `fn(id: &str) -> Option<String>` | keyring(`devflow-ai-provider`/<id>)读密钥,空/无→None(`secret.rs:88-94`) |
|
||||||
|
| `resolve_provider_secret` | `secret.rs:97` | `fn(record: &AiProviderRecord) -> String` | **DB.api_key 非空→直接返回**(兼容未迁移);否则 keyring 取,空→`unwrap_or_default()` 空串(`secret.rs:97-102`) |
|
||||||
|
| `ensure_resolved_key` | `secret.rs:179` | `fn(provider_name: &str, resolved: &str) -> Result<(), String>` | 空串/纯空白→Err「未读取到密钥」;非空 Ok(`secret.rs:179-188`) |
|
||||||
|
| `set_provider_secret` | `secret.rs:105` | `fn(id, key) -> anyhow::Result<()>` | keyring 写(`secret.rs:105-108`) |
|
||||||
|
| `delete_provider_secret` | `secret.rs:111` | `fn(id) -> anyhow::Result<()>` | keyring 删(`secret.rs:111-114`) |
|
||||||
|
| `migrate_secrets_to_keyring` | `secret.rs:117` | `async fn(repo: &AiProviderRepo) -> Result<usize>` | 启动一次性:DB 明文→keyring→DB 置空;失败累计 `failcount` 达 3 升级 warn(`secret.rs:117-156`) |
|
||||||
|
| `build_provider_for` | `secret.rs:164` | `fn(record) -> Result<Box<dyn LlmProvider>, String>` | 三步打包 resolve→ensure→`df_ai::build_provider`(`secret.rs:164-175`) |
|
||||||
|
| keyring entry | `secret.rs:83-85` | `entry_for(id)` | `Entry::new("devflow-ai-provider", id)`,服务名常量 `KEYRING_SERVICE`(`secret.rs:18`) |
|
||||||
|
| failcount sidecar | `secret.rs:26-81` | read/write/record/clear | `<cwd>/.devflow-keyring-failcount`,`provider_id=count` 跨启动持久化告警计数 |
|
||||||
|
|
||||||
|
依赖:`use df_storage::crud::AiProviderRepo; use df_storage::models::AiProviderRecord; use keyring::Entry;`(`secret.rs:14-16`)。
|
||||||
|
|
||||||
|
### 1.2 调用点清单(下沉影响面)
|
||||||
|
|
||||||
|
| 调用点 | 位置 | 调的函数 |
|
||||||
|
|---|---|---|
|
||||||
|
| `lib.rs` 启动迁移 | `src-tauri/src/lib.rs:28` | `migrate_secrets_to_keyring(&app_state.ai_providers)` |
|
||||||
|
| `commands/ai/commands.rs` ai_list_providers | `commands.rs:683` | `get_provider_secret`(list mask) |
|
||||||
|
| `commands/ai/commands.rs` ai_save_provider | `commands.rs:724, 737, 740` | `set_provider_secret`(新/改写)、`get_provider_secret`(未迁移检测)、`set_provider_secret`(即时迁移) |
|
||||||
|
| `commands/ai/commands.rs` ai_delete_provider | `commands.rs:818` | `delete_provider_secret` |
|
||||||
|
| `commands/ai/agentic.rs` 主对话循环 | `agentic.rs:126, 128` | `resolve_provider_secret` + `ensure_resolved_key`(内联三步,B-17 复用 resolved key) |
|
||||||
|
| `commands/ai/title.rs` 标题生成 | `title.rs:62` | `build_provider_for` |
|
||||||
|
| `commands/ai/knowledge_inject.rs` 知识提炼 | `knowledge_inject.rs:35, 327` | `build_provider_for`(两处:embed + 提炼) |
|
||||||
|
| `commands/idea.rs` 灵感评分 | `idea.rs:272` | `build_provider_for` |
|
||||||
|
| `commands/project.rs` 项目导入 | `project.rs:438, 598` | `build_provider_for`(两处) |
|
||||||
|
|
||||||
|
**影响面**:9 文件、~12 调用点,全在 `src-tauri/commands/`。下沉后这些调用点改 import 路径(`crate::commands::ai::secret::` → `df_storage::secret::`)即可,函数签名不变,行为零变化。
|
||||||
|
|
||||||
|
### 1.3 ai_providers 表 schema(df-storage)
|
||||||
|
|
||||||
|
**models.rs**(`crates/df-storage/src/models.rs:147-159`):
|
||||||
|
```
|
||||||
|
AiProviderRecord { id, name, provider_type, api_key, base_url,
|
||||||
|
default_model, models: Option<String>, is_default,
|
||||||
|
config: Option<String>, created_at, updated_at }
|
||||||
|
```
|
||||||
|
|
||||||
|
**建表 SQL**(`crates/df-storage/src/migrations.rs:488-500`,V9):
|
||||||
|
```
|
||||||
|
api_key TEXT NOT NULL, -- 列定义无 default、无加密、无 env 引用语法
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRUD**(`crates/df-storage/src/crud.rs:1113-1140`):`impl_repo!` 宏生成 AiProviderRepo,insert/update/`from_row`(`crud.rs:1034-1048`),`list_all`/`get_by_id`(宏内置)。
|
||||||
|
|
||||||
|
**api_key 存储形态结论**:
|
||||||
|
- **不是 mask**(列定义无 `sk-****` 语法);**不是 env 引用**(无 `$ENV_VAR` 解析);**不是加密**(明文 TEXT)。
|
||||||
|
- 真相:FR-S1 后 **DB api_key 列恒空**(迁移后/新建均空,`secret.rs:1-8` 模块文档明确),真实密钥唯一源 = OS keyring(service=`devflow-ai-provider`, username=provider_id)。
|
||||||
|
- 消费读路径:`resolve_provider_secret` 先看 DB(兼容未迁移老库明文)→空则 keyring(`secret.rs:97-102`)。
|
||||||
|
- 前端只拿到 mask:`ai_list_providers`(`commands.rs:679-686`) 用 `mask_api_key`(`commands.rs:663-671`,首尾各 4 字符 + 中间 `••••`)返回,前端 `Settings.vue:458` 编辑不回填 apiKey。
|
||||||
|
|
||||||
|
### 1.4 依赖链(df-nodes / df-storage Cargo.toml)
|
||||||
|
|
||||||
|
**df-nodes**(`crates/df-nodes/Cargo.toml:6-17`)已依赖:
|
||||||
|
- `df-core`, `df-execute`, `df-workflow`, `df-ai`, `df-storage`, serde, tokio, async-trait, anyhow, tracing。
|
||||||
|
- **关键**:df-nodes **已依赖 df-storage + df-ai**,可直接调 `AiProviderRepo` + `df_ai::build_provider`,无需新增 crate 依赖。
|
||||||
|
|
||||||
|
**df-storage**(`crates/df-storage/Cargo.toml:6-13`)依赖:df-core, serde, serde_json, anyhow, tokio, rusqlite, tracing。**不含 keyring**。
|
||||||
|
|
||||||
|
**keyring crate**:声明在 workspace 根 `Cargo.toml:42`(features=`windows-native, apple-native`),仅 src-tauri 引用。下沉到 df-storage 需在 df-storage 的 `Cargo.toml` 加 `keyring = { workspace = true }`(workspace 已定义,path 无需重声明)。
|
||||||
|
|
||||||
|
### 1.5 AiNode 现状(ai_node.rs)
|
||||||
|
|
||||||
|
- `AiNode` 持 `Arc<Database>`(`ai_node.rs:119-128`),注册时注入(`state.rs:260-263`)。
|
||||||
|
- `parse_params`(`ai_node.rs:38-112`)从 `ctx.config` 读 `base_url` / `api_key`(**明文,P0 风险点**),空 key 早失败(`ai_node.rs:54-56`,对齐 `ensure_resolved_key`)。
|
||||||
|
- `execute`(`ai_node.rs:135-138`)直接 `df_ai::build_provider(protocol, base_url, api_key, default_model)`。
|
||||||
|
- schema `required: ["base_url","api_key"]`(`ai_node.rs:214`),`api_key` 字段对 LLM tool_call 可见。
|
||||||
|
- AiSelfReviewNode 同构(`ai_node.rs:330-358`)。
|
||||||
|
|
||||||
|
### 1.6 run_workflow config 注入链
|
||||||
|
|
||||||
|
- IPC `run_workflow(name, dag, config, task_id, target_status)`(`workflow.rs:67-76`),`config: serde_json::Value` 由前端构造传入。
|
||||||
|
- `executor.run(&runtime_dag, config)`(`workflow.rs:245`)→ 透传到 `NodeContext.config`(`node.rs:13-26`),节点 execute 直接读。
|
||||||
|
- **前端目前不构造 ai/ai_self_review 节点 config**:模板由后端 `template_for`(`workflow.rs:84-92`,`task_workflow_templates.rs`)提供,节点 config 来自模板定义 + 前端 run_workflow 传入的 config 合并/覆盖。
|
||||||
|
- config 经 IPC(明文) + NodeContext.config(明文) 两层暴露,前端 Vue devtools / LLM tool_call schema 均可拿到明文 api_key。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、下沉方案对比(A/B/C)
|
||||||
|
|
||||||
|
### 方案 A:完整下沉 secret.rs → df-storage(新建 `crates/df-storage/src/secret.rs`)
|
||||||
|
|
||||||
|
**做什么**:把 `secret.rs` 全部逻辑(get/set/delete/resolve/ensure/migrate/build_provider_for/failcount)整体 move 到 `crates/df-storage/src/secret.rs`,df-storage 加 `keyring` 依赖,src-tauri 改 `use df_storage::secret::*;` 转发。
|
||||||
|
|
||||||
|
| 维度 | 评估 |
|
||||||
|
|---|---|
|
||||||
|
| 影响面 | 9 文件 12 调用点改 import 路径;df-storage Cargo.toml +1 依赖;df-nodes Cargo.toml 已有 df-storage 依赖(零改动);src-tauri/Cargo.toml keyring 可保留(workspace 引用)或移除(改由 df-storage 传递) |
|
||||||
|
| 工作量 | 中:move 文件 + 改 12 调用点 import + df-storage 加 keyring feat + cargo check。约 1-2h |
|
||||||
|
| 风险 | **keyring feature 传递**:df-storage 加 `keyring = { workspace = true }` 后,feat=`windows-native, apple-native` 随 workspace 注入,Linux 构建(df-storage 是否跑测)需补 `linux-native`。df-storage 单测若启 keyring 会触发 OS keyring 副作用(单测应 cfg-gate)。`build_provider_for` 依赖 `df_ai::build_provider`——df-storage **不依赖 df-ai**,下沉 `build_provider_for` 会打破单向依赖(df-storage→df-ai 反向,df-ai 不依赖 df-storage 但 df-storage 反引 df-ai 形成潜在循环)。需把 `build_provider_for` **留在 src-tauri**(不下沉),只下沉 resolve/ensure/get/set/migrate |
|
||||||
|
| 收益 | df-nodes 可直接 `df_storage::secret::resolve_provider_secret`,最干净 |
|
||||||
|
|
||||||
|
**致命点**:`build_provider_for`(`secret.rs:164`)调 `df_ai::build_provider`,下沉 df-storage 会引 df-storage→df-ai。需拆分下沉边界(见方案 B 的边界划分)。
|
||||||
|
|
||||||
|
### 方案 B:df-storage 加薄 secret 查询方法(推荐) ★
|
||||||
|
|
||||||
|
**做什么**:
|
||||||
|
1. **下沉**纯密钥解析逻辑到 `crates/df-storage/src/secret.rs`:`get/set/delete_provider_secret`、`resolve_provider_secret`、`ensure_resolved_key`、`migrate_secrets_to_keyring`、failcount helper。**不含** `build_provider_for`(因依赖 df-ai)。
|
||||||
|
2. df-storage 加 `keyring = { workspace = true }` 依赖。
|
||||||
|
3. src-tauri `secret.rs` 保留 `build_provider_for` 作为转发壳:内部调 `df_storage::secret::resolve_provider_secret + ensure_resolved_key + df_ai::build_provider`(src-tauri 已依赖 df-ai,无循环)。
|
||||||
|
4. df-nodes AiNode/AiSelfReviewNode 调 `df_storage::secret::resolve_provider_secret(&record)` + `ensure_resolved_key` 自己拼 `df_ai::build_provider`(df-nodes 已依赖 df-ai + df-storage)。
|
||||||
|
|
||||||
|
| 维度 | 评估 |
|
||||||
|
|---|---|
|
||||||
|
| 影响面 | df-storage 新增 1 文件 + Cargo.toml +1 依赖;src-tauri secret.rs 瘦身为转发壳(保留 build_provider_for);12 调用点 import 改为 `df_storage::secret::`(build_provider_for 仍从 src-tauri 调,6 调用点不变);df-nodes AiNode 改读 provider 配置(见第三章) |
|
||||||
|
| 工作量 | 中:move 纯密钥逻辑(约 100 行)+ df-storage Cargo.toml + 改 import + AiNode 注入链。约 2-3h |
|
||||||
|
| 风险 | keyring feature 同 A;df-storage 单测需 mock keyring 或 cfg-gate。**无循环依赖**:df-storage 不引 df-ai,build_provider_for 留 src-tauri。最稳 |
|
||||||
|
| 收益 | 边界清晰:密钥解析下沉、provider 构造留 app 层;df-nodes 获密钥解析能力;FR-S1 mask 链路后端闭环 |
|
||||||
|
|
||||||
|
### 方案 C:provider_id 注入 config + df-nodes 直查 ai_providers.api_key(df-nodes 自己解析)
|
||||||
|
|
||||||
|
**做什么**:config 注 `provider_id`,AiNode 用 `AiProviderRepo.get_by_id` 查 record,自己实现 keyring 解析(在 df-nodes 里复制一份 resolve 逻辑)。
|
||||||
|
|
||||||
|
| 维度 | 评估 |
|
||||||
|
|---|---|
|
||||||
|
| 影响面 | df-nodes 复制一份 keyring 解析(逻辑重复,违反 DRY);或 df-nodes 也下沉 secret(等同方案 B 但把解析放 df-nodes 而非 df-storage) |
|
||||||
|
| 工作量 | 中,但**重复实现** |
|
||||||
|
| 风险 | **DRY 破坏**:两份 keyring 解析逻辑(secret.rs 一份 + df-nodes 一份),未来迁移逻辑/常量(service name `devflow-ai-provider`)变更需同步两处,易漏。FAIL_THRESHOLD/failcount 等告警逻辑更难复制 |
|
||||||
|
| 收益 | 无(被 B 完全覆盖) |
|
||||||
|
|
||||||
|
### 推荐:**方案 B**
|
||||||
|
|
||||||
|
理由:
|
||||||
|
1. **无循环依赖**:纯密钥解析(无 df_ai 依赖)下沉 df-storage,`build_provider_for` 留 src-tauri(app 层既依赖 df-storage 又依赖 df-ai,天然适合拼装)。
|
||||||
|
2. **DRY**:keyring 解析逻辑唯一源在 df-storage,src-tauri 与 df-nodes 共用。
|
||||||
|
3. **df-nodes 零新依赖**:df-nodes Cargo.toml 已含 df-storage + df-ai,方案 B 下沉后直接可调,不改 crate 依赖。
|
||||||
|
4. **FR-S1 闭环**:config 只注 `provider_id`,明文 api_key 不进 IPC / NodeContext.config,前端 / LLM tool_call schema 拿不到明文,与现有 `ai_list_providers` mask 设计一致。
|
||||||
|
5. **最小破坏**:12 调用点中 6 个调 `build_provider_for`(src-tauri 保留,import 不变),6 个调 resolve/ensure/get/set/import 改路径,行为零变化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、run_workflow → AiNode provider 注入链设计(方案 B)
|
||||||
|
|
||||||
|
### 3.1 config 注入什么
|
||||||
|
|
||||||
|
**注入(明文安全)**:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"provider_id": "<uuid>", // 必填,AiNode 据此查 ai_providers 表
|
||||||
|
"prompt": "...", // 必填(可上游覆盖)
|
||||||
|
"system_prompt": "...", // 可选
|
||||||
|
"model": "glm-4-flash", // 可选,留空用 record.default_model
|
||||||
|
"temperature": 0.3, // 可选
|
||||||
|
"max_tokens": 1024, // 可选
|
||||||
|
"task_id": "..." // 可选(自审闭环用)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**不注入(api_key/base_url/protocol 经下沉层解析,不进 config)**:
|
||||||
|
- `api_key` ❌ — 明文,违背 FR-S1
|
||||||
|
- `base_url` ❌ — 从 record 读(record.base_url),config 注入是冗余 + 泄漏
|
||||||
|
- `protocol` ❌ — 从 record.provider_type 映射(openai_compat / anthropic),config 注入冗余
|
||||||
|
|
||||||
|
### 3.2 api_key 如何经 df-storage 层解析不进 config
|
||||||
|
|
||||||
|
AiNode.execute 内部链路(伪代码):
|
||||||
|
```
|
||||||
|
let provider_id = ctx.config["provider_id"]?; // 仅 id 进 config
|
||||||
|
let repo = AiProviderRepo::new(&self.db);
|
||||||
|
let record = repo.get_by_id(provider_id).await?; // 查 ai_providers 行
|
||||||
|
let api_key = df_storage::secret::resolve_provider_secret(&record); // DB 优先→keyring
|
||||||
|
df_storage::secret::ensure_resolved_key(&record.name, &api_key)?; // 空 key 早失败
|
||||||
|
let provider = df_ai::build_provider(
|
||||||
|
&record.provider_type, &record.base_url, &api_key, &model_or_default);
|
||||||
|
```
|
||||||
|
|
||||||
|
**api_key 全程在 AiNode 进程内存,不出 IPC / 不进 NodeContext.config**:
|
||||||
|
- IPC `run_workflow` 收到的 config 只有 `provider_id`(非密)。
|
||||||
|
- NodeContext.config 只含 provider_id,LLM tool_call schema 看不到 api_key。
|
||||||
|
- api_key 经 `df_storage::secret::resolve_provider_secret` 在 AiNode 内存解析,直达 `df_ai::build_provider`。
|
||||||
|
|
||||||
|
### 3.3 run_workflow 如何拿到 provider 配置注入 config
|
||||||
|
|
||||||
|
**前端 → run_workflow config 注入路径(三选一)**:
|
||||||
|
|
||||||
|
**路径 1(推荐):前端只传 `provider_id`,其余从 DB 默认 provider**
|
||||||
|
- 前端 `ai_list_providers` 拿到 provider 列表(mask,无明文)。
|
||||||
|
- 触发 run_workflow 时 config 注 `{provider_id: <default or chosen id>}`(或留空,AiNode 内部取 `is_default=true` 首条)。
|
||||||
|
- base_url/protocol/model 全由 AiNode 从 record 读,前端不构造。
|
||||||
|
- **最安全**:前端全程不接触明文,config 最小化。
|
||||||
|
|
||||||
|
**路径 2:run_workflow IPC 加 provider_id 顶层参数**
|
||||||
|
- `run_workflow(name, dag, config, task_id, target_status, provider_id: Option<String>)`。
|
||||||
|
- 后端 `workflow.rs:245` 前把 provider_id 注入 config 注入到 ai/ai_self_review 节点 config。
|
||||||
|
- 显式更安全,但改 IPC 签名(向后兼容,Option 缺省 None)。
|
||||||
|
|
||||||
|
**路径 3:模板内嵌 provider_id 占位**
|
||||||
|
- `task_workflow_templates.rs` 的 ai 节点 config 写 `{"provider_id": "{{default}}"}`,executor 注入时替换为 DB 默认 provider id。
|
||||||
|
- 需 executor 支持 placeholder 替换,改动大。
|
||||||
|
|
||||||
|
**推荐路径 1**:最小改动,前端 config 注 provider_id(或留空走默认),AiNode 内部解析。run_workflow 签名不变(向后兼容)。
|
||||||
|
|
||||||
|
### 3.4 兼容老 config(base_url/api_key 明文)迁移
|
||||||
|
|
||||||
|
- AiNode `parse_params` 兼容窗口:**有 provider_id → 走下沉解析路径;无 provider_id → 走老 base_url/api_key 明文路径**(对齐现有 ai_node.rs:42-56,兼容期保留)。
|
||||||
|
- 老路径打 deprecation warn,引导 config 迁移到 provider_id。
|
||||||
|
- 模板(`task_workflow_templates.rs`)切到 provider_id 后,老路径仅 demo dag(ProjectDetail.vue demoDag)用,逐步淘汰。
|
||||||
|
- schema `required` 从 `["base_url","api_key"]` 改为 `["provider_id"]`(明文路径保留但不再 required)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、影响面 + 风险 + 实施步骤
|
||||||
|
|
||||||
|
### 4.1 影响面
|
||||||
|
|
||||||
|
| 模块 | 改动 |
|
||||||
|
|---|---|
|
||||||
|
| `crates/df-storage/src/secret.rs` | **新建**:move 纯密钥逻辑(get/set/delete/resolve/ensure/migrate/failcount) |
|
||||||
|
| `crates/df-storage/Cargo.toml` | +`keyring = { workspace = true }` |
|
||||||
|
| `crates/df-storage/src/lib.rs` | `pub mod secret;` |
|
||||||
|
| `src-tauri/src/commands/ai/secret.rs` | 瘦身为转发壳:`pub use df_storage::secret::*;` + 保留 `build_provider_for`(调 df_ai) |
|
||||||
|
| `crates/df-nodes/src/ai_node.rs` | `parse_params` 改读 provider_id;execute 加 repo.get_by_id + resolve + ensure + build_provider;schema required 改 |
|
||||||
|
| `src-tauri/src/lib.rs:28` | `df_storage::secret::migrate_secrets_to_keyring`(改 import) |
|
||||||
|
| `commands/ai/commands.rs`(3 处) | import 改 `df_storage::secret::`(行为不变) |
|
||||||
|
| `commands/ai/agentic.rs`(2 处) | import 改(build_provider_for 仍 src-tauri,resolve/ensure 改 df-storage) |
|
||||||
|
| `commands/ai/title.rs` / `knowledge_inject.rs`(3 处) / `idea.rs`(1 处) / `project.rs`(2 处) | build_provider_for 调用点不变(src-tauri 保留) |
|
||||||
|
| `crates/df-nodes/src/task_workflow_templates.rs` | ai/ai_self_review 节点 config 改注 `provider_id`(替代 base_url/api_key) |
|
||||||
|
| 前端 `api/workflow.ts`(触发 run_workflow 处) | config 注 provider_id(替代 base_url/api_key 明文) |
|
||||||
|
|
||||||
|
### 4.2 风险
|
||||||
|
|
||||||
|
| 风险 | 等级 | 缓解 |
|
||||||
|
|---|---|---|
|
||||||
|
| keyring feature 传递:df-storage 加 keyring 后 Linux 无 native feat | 中 | workspace `Cargo.toml:42` 已含 `windows-native, apple-native`;CI 若有 Linux 跑 df-storage 单测需补 `linux-native` 或 cfg-gate keyring 单测 |
|
||||||
|
| df-storage 单测触发 OS keyring 副作用 | 中 | keyring 相关单测加 `#[cfg(not(test))]` 或 mock trait 抽象;migrate 测试用 in_memory db + mock keyring |
|
||||||
|
| 老配置兼容期明文路径残留 | 低 | parse_params 双路径兼容(provider_id 优先),deprecation warn 引导迁移 |
|
||||||
|
| build_provider_for 下沉边界划错引循环依赖 | 中 | 严格:df-storage 只下沉纯密钥逻辑,build_provider_for 留 src-tauri(依赖 df-ai),方案 B 明确划界 |
|
||||||
|
| run_workflow config 注入 provider_id 后,模板节点缺 provider_id 致 AiNode 报错 | 中 | AiNode 内部 provider_id 留空时取 DB `is_default=true` 首条(对齐 idea.rs:265-279 build_default_provider 模式) |
|
||||||
|
| 前端 demoDag(ProjectDetail.vue)仍注明文 base_url/api_key | 低 | 老路径兼容,逐步切模板 provider_id |
|
||||||
|
|
||||||
|
### 4.3 实施步骤(可并发标注)
|
||||||
|
|
||||||
|
**阶段 1:下沉(串行,基础)**
|
||||||
|
- S1:df-storage 加 keyring 依赖 + 新建 `crates/df-storage/src/secret.rs`(move 纯密钥逻辑,删除 build_provider_for)
|
||||||
|
- S2:src-tauri `secret.rs` 改转发壳(`pub use df_storage::secret::*;` + 保留 build_provider_for 调 df_ai)
|
||||||
|
- S3:`cargo check -p df-storage && -p src-tauri` 验证下沉零行为变化
|
||||||
|
|
||||||
|
**阶段 2:调用点 import 切换(可并发,S3 完成后)**
|
||||||
|
- 并行 P1:`lib.rs:28` / `commands.rs`(3 处) / `agentic.rs`(2 处) 改 import(纯路径替换,行为零变)
|
||||||
|
- 并行 P2:build_provider_for 调用点(title/knowledge_inject/idea/project,6 处)无需改(src-tauri 保留)
|
||||||
|
- 合并:`cargo check` 全绿
|
||||||
|
|
||||||
|
**阶段 3:AiNode 注入链改造(串行,阶段 2 完成后)**
|
||||||
|
- S4:`ai_node.rs parse_params` 改 provider_id 路径(双路径兼容)
|
||||||
|
- S5:`ai_node.rs execute` 加 `AiProviderRepo::get_by_id + resolve + ensure + build_provider`
|
||||||
|
- S6:`AiSelfReviewNode` 同构改造
|
||||||
|
- S7:schema required 改 `["provider_id"]`
|
||||||
|
|
||||||
|
**阶段 4:模板 + 前端(可并发,阶段 3 完成后)**
|
||||||
|
- 并行 P3:`task_workflow_templates.rs` ai/ai_self_review 节点 config 改 provider_id
|
||||||
|
- 并行 P4:前端 `api/workflow.ts` / `TaskDetail.vue` 触发 run_workflow 处 config 注 provider_id
|
||||||
|
|
||||||
|
**阶段 5:验证(串行)**
|
||||||
|
- S8:`cargo test -p df-storage`(keyring 单测 cfg-gate) / `-p df-nodes`(parse_params 双路径) / `-p src-tauri`
|
||||||
|
- S9:端到端联调 AiNode 自审闭环⑥(模板触发 → AiNode 查 provider → LLM 调用 → 产出落 task.output_json)
|
||||||
|
|
||||||
|
**并发点**:阶段 2(P1+P2)、阶段 4(P3+P4) 可分别并发;阶段 1/3/5 串行(前后依赖)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、关键 file:line 索引
|
||||||
|
|
||||||
|
| 关注点 | 位置 |
|
||||||
|
|---|---|
|
||||||
|
| secret.rs 全貌 | `src-tauri/src/commands/ai/secret.rs:1-228` |
|
||||||
|
| resolve_provider_secret | `secret.rs:97-102` |
|
||||||
|
| ensure_resolved_key | `secret.rs:179-188` |
|
||||||
|
| build_provider_for(留 src-tauri) | `secret.rs:164-175` |
|
||||||
|
| keyring 服务名常量 | `secret.rs:18`(`devflow-ai-provider`) |
|
||||||
|
| AiProviderRecord schema | `crates/df-storage/src/models.rs:147-159` |
|
||||||
|
| ai_providers 建表 SQL | `crates/df-storage/src/migrations.rs:488-500`(V9) |
|
||||||
|
| AiProviderRepo CRUD | `crates/df-storage/src/crud.rs:1113-1140` |
|
||||||
|
| df-nodes Cargo.toml(已含 df-storage+df-ai) | `crates/df-nodes/Cargo.toml:6-17` |
|
||||||
|
| df-storage Cargo.toml(缺 keyring) | `crates/df-storage/Cargo.toml:6-13` |
|
||||||
|
| workspace keyring 声明 | `Cargo.toml:42` |
|
||||||
|
| AiNode parse_params(明文 api_key 风险点) | `crates/df-nodes/src/ai_node.rs:38-112` |
|
||||||
|
| AiNode execute build_provider | `ai_node.rs:135-138` |
|
||||||
|
| AiSelfReviewNode execute | `ai_node.rs:330-358` |
|
||||||
|
| run_workflow config 注入 | `src-tauri/src/commands/workflow.rs:67-76,245` |
|
||||||
|
| NodeContext.config | `crates/df-workflow/src/node.rs:13-26` |
|
||||||
|
| build_registry AiNode 工厂 | `src-tauri/src/state.rs:260-263` |
|
||||||
|
| ai_list_providers mask | `src-tauri/src/commands/ai/commands.rs:675-688` |
|
||||||
|
| 启动迁移 | `src-tauri/src/lib.rs:27-31` |
|
||||||
|
| 12 调用点 | 见 1.2 表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、决策记录(规格)
|
||||||
|
|
||||||
|
- **方案选型**:方案 B(df-storage 加薄 secret 查询方法)。理由:无循环依赖(build_provider_for 留 src-tauri)、DRY(keyring 解析唯一源)、df-nodes 零新依赖、FR-S1 闭环。
|
||||||
|
- **config 注入**:仅 `provider_id`(非密);base_url/protocol/model 经 AiNode 从 record 读;api_key 经 `df_storage::secret::resolve_provider_secret` 解析,全程不进 IPC / NodeContext.config。
|
||||||
|
- **run_workflow config 来源**:路径 1(前端只传 provider_id,AiNode 内部取默认/查表),run_workflow 签名不变(向后兼容)。
|
||||||
|
- **兼容窗口**:parse_params 双路径(provider_id 优先,老 base_url/api_key 明文路径保留 + deprecation warn)。
|
||||||
Reference in New Issue
Block a user