squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
24 KiB
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 代码。
✅ 实施状态(2026-06-18 核对:方案 B 全量落地)
推荐方案 B(df-storage 加薄 secret 查询方法)已全量实施,行为与设计一致。
下沉层:
crates/df-storage/src/secret.rs(新建,纯密钥逻辑唯一源)——KEYRING_SERVICE常量(:26)、failcount sidecar(:34-56)、get/set/delete/resolve/ensure/migrate 全套函数均下沉至此。crates/df-storage/src/lib.rspub mod secret暴露。df-storage 依赖:
crates/df-storage/Cargo.toml:16-18已加keyring = { workspace = true }(注释标注 FR-S1 密钥解析下沉)。src-tauri 转发壳:
src-tauri/src/commands/ai/secret.rs:20pub use df_storage::secret::*;(12 调用点路径不变),:32-43保留build_provider_for(依赖df_ai::build_provider不下沉,对齐设计「留 app 层避免 df-storage→df-ai 循环」)。AiNode 注入链(阶段 3):
crates/df-nodes/src/ai_node.rs:21importresolve_provider_secret/ensure_resolved_key;:60-64/101-139三路径解析(provider_id 优先 → 老明文兼容 → 空兜底取 is_default 首条);:166-178从 record 解析 provider 构造要素(resolve→ensure→base_url/api_key)。AiSelfReviewNode 同构(:476起)。schema:
ai_node.rs:344-351schema 描述 provider_id/base_url/api_key(base_url/api_key 标注「已废弃过渡」),:351required=[](SW-260618-15:prompt/provider_id 均「留空走兜底」与 required 矛盾,改 required=[] 对齐运行时)。设计偏离:无功能性偏离。仅 schema required 值与设计 §3.4「
["provider_id"]」略不同(实际required=[],对齐「留空走兜底」运行时语义,SW-260618-15 决策)。本文档 §1.2/§五的 file:line 索引:原指向
src-tauri/src/commands/ai/secret.rs的函数行号(resolve:97-102 等)下沉后已迁移至crates/df-storage/src/secret.rs,src-tauri 文件已瘦身为转发壳(原 228 行 → 现 43 行)。查阅实际函数请走crates/df-storage/src/secret.rs。
一、现状核验(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/)读密钥,空/无→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 查询方法(推荐) ★
做什么:
- 下沉纯密钥解析逻辑到
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)。 - df-storage 加
keyring = { workspace = true }依赖。 - src-tauri
secret.rs保留build_provider_for作为转发壳:内部调df_storage::secret::resolve_provider_secret + ensure_resolved_key + df_ai::build_provider(src-tauri 已依赖 df-ai,无循环)。 - 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
理由:
- 无循环依赖:纯密钥解析(无 df_ai 依赖)下沉 df-storage,
build_provider_for留 src-tauri(app 层既依赖 df-storage 又依赖 df-ai,天然适合拼装)。 - DRY:keyring 解析逻辑唯一源在 df-storage,src-tauri 与 df-nodes 共用。
- df-nodes 零新依赖:df-nodes Cargo.toml 已含 df-storage + df-ai,方案 B 下沉后直接可调,不改 crate 依赖。
- FR-S1 闭环:config 只注
provider_id,明文 api_key 不进 IPC / NodeContext.config,前端 / LLM tool_call schema 拿不到明文,与现有ai_list_providersmask 设计一致。 - 最小破坏: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-S1base_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.rsai/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)。