- docs/02 架构设计: 新增 aichat审查/异步审批构想/流式渲染调研/generating状态机/密钥迁移健壮性/工作流脚本执行边界/条件表达式引擎/F-07 trait下沉/Agent架构说明/任务推进构想/功能创意池;更新功能决策记录+归档/对抗论证/文档记录规范/经验记录
- docs/03 模块文档: 新增 AI对话引擎/DAG引擎详解;更新 df-knowledge/df-nodes/df-storage/df-workflow/df-ai
- docs/05 代码审查: 新增 全栈审查/全局review/架构审查/近期改动审查/工作区多角度走查/自研memo流式渲染审查
- docs/09 问题排查: 新增 aichat-apikey-401
- docs/INDEX+README 索引同步;docs/todo 待办看板(2026-06-15 汇总)
- PROGRESS.md Sprint 22-25;URGENT.md 加急清单快照(5 项 P0 已全修)
- scripts/cleanup_orphan_tasks.{py,sh} 孤儿任务清理工具
- .gitignore 补 *.broken.bak + tmp/ 噪音排除
13 KiB
AI 生成状态机加固(generating 生命周期)
创建: 2026-06-15 | 来源: /review generating 状态机专项审查 | 关联 bug: 用户报障"创建不了新对话"
1. 背景
用户报障:AI 面板"创建不了新对话了",前端报 生成中无法新建对话,请先停止或等待完成,但前端实际无内容生成。
排查定位:后端 AiSession.generating 标志卡在 true,与实际无生成状态不符。ai_conversation_create(commands.rs:451)硬拦 generating=true → 死锁,用户永远新建不了对话,只能重启 app。
/review 对 generating 状态机(commands.rs 对话/审批/stop + agentic.rs loop/try_continue + stream_recv.rs stream_llm + mod.rs AiSession)做专项审查,产出本文档。
2. 现状:三标志组合表达隐式状态机
2.1 状态标志
| 标志 | 类型 | 位置 | 作用 |
|---|---|---|---|
generating |
bool |
Mutex<AiSession> 内 |
是否有 agentic loop 活跃 |
pending_approvals |
HashMap |
Mutex<AiSession> 内 |
待审批工具调用 |
stop_flag |
Arc<AtomicBool> |
跨锁共享 | ai_chat_stop 置位,loop/stream 读取 |
2.2 隐式表达的 4 态
| 状态 | 标志组合 | 语义 |
|---|---|---|
| Idle | generating=false |
空闲,可接新请求 |
| Streaming | generating=true && pending=空 |
agentic loop 流式生成中 |
| AwaitingApproval | generating=true && pending非空 |
loop 暂停等用户审批 |
| Stopping | stop_flag=true(瞬态) |
用户点了停止,loop 检查点退出中 |
2.3 两个散布问题(根因)
写侧散布(复位):generating=false 手动散落在 6 个 return 点(agentic.rs:53/94/144/190/264/318)。任一新增 return 漏写、或 spawn task panic/abort → 所有复位点跳过 → generating 永久 true。
读侧散布(判别):状态判别靠标志组合反推:
try_continue(agentic.rs:283):should_continue = generating && !pendingai_chat_stop(commands.rs:250):pending非空分流流式/审批态ai_conversation_create(commands.rs:451):generating硬拦
判别逻辑散落三处,无单一真相源,新增分支易漏。
3. 状态机决策:轻量状态机,不引入框架
3.1 决策结论
不引入独立状态机框架(enum 字段替换 bool + 转换守卫 + codegen 库)。采用轻量状态机:
- 写侧收敛 = RAII guard(Drop 兜底复位),对应审查项 ①
- 读侧收敛 =
session.state()enum 视图方法(只读,不改字段),对应审查项 ⑤ - stop_flag 保留
Arc<AtomicBool>(跨锁信号,状态机管不了)
3.2 多角度论证
| 角度 | 独立状态机框架 | 轻量方案(guard+视图) | 判 |
|---|---|---|---|
| 状态复杂度 | 4 态 6 边转换,简单 | 同 | 不足以 justify 框架 |
| stop_flag 约束 | enum 不能跨锁,stop_flag 仍须 AtomicBool 独存 | stop_flag 保留,enum 只读视图 | 框架无法替代跨锁信号 |
| 根因对症 | 复位散布=写侧 / 判别散布=读侧 | guard 治写 + 视图治读,精准对症 | 轻量直达根因 |
| 风格契合 | enum 字段+转换函数+非法检测=过度封装 | 增量加 guard+方法,做减法 | 轻量契合"可读性>抽象性" |
| 迁移成本 | 改 AiSession 字段+所有读写点+测试,大改 | 增量,①⑤已在审查清单 | 轻量零额外工作 |
3.3 关键洞察
① RAII guard(写收敛)+ ⑤ enum 视图(读收敛)= 状态机读写两端都收敛,功能上等价于状态机,但不引入 enum 字段/转换守卫/框架。这是务实路线,符合作减法风格。
stop_flag 是跨锁信号(ai_chat_stop 在 IPC 线程置位,loop/stream 在 agentic task 读),必须保持 Arc<AtomicBool> 独立——这是状态机 enum 字段(锁内)无法替代的并发要求。
4. 改造设计
4.1 ① RAII guard — 写侧收敛(P0 根治)
新增 guard struct,run_agentic_loop 入口创建,函数退出(含 panic/abort/正常 return)时 Drop 兜底复位。
struct GeneratingGuard {
session: Arc<Mutex<AiSession>>,
app: AppHandle,
conv_id: String,
}
impl Drop for GeneratingGuard {
fn drop(&mut self) {
let app = self.app.clone();
let conv_id = self.conv_id.clone();
let session = self.session.clone();
tauri::async_runtime::spawn(async move {
let mut s = session.lock().await;
if s.generating { // 仅在仍 true 时复位(避免重复 emit)
s.generating = false;
drop(s);
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0,
conversation_id: Some(conv_id),
});
}
});
}
}
run_agentic_loop 入口:let _guard = GeneratingGuard { ... };,函数内所有手动 session.generating = false 删除。
注意:Drop 是同步的不能再 .await,故 spawn 异步复位。存在极小窗口 generating 仍 true,由 ② 软复位兜底。
4.2 ⑤ session.state() 视图 — 读侧收敛(P1)
只读视图方法,不改 AiSession 字段,收敛三标志组合判别:
enum SessionState { Idle, Streaming, AwaitingApproval }
impl AiSession {
fn state(&self) -> SessionState {
if !self.generating { return SessionState::Idle; }
if self.pending_approvals.is_empty() { SessionState::Streaming }
else { SessionState::AwaitingApproval }
}
}
改造判别点:
try_continue:should_continue = matches!(session.state(), SessionState::Streaming)ai_chat_stop:分流用matches!(session.state(), SessionState::AwaitingApproval)ai_conversation_create:见 4.3
4.3 ② newConversation 软复位 — 死锁解除(P0)
ai_conversation_create(commands.rs:451)硬拦改软复位。generating=true 时强制复位(等同 ai_chat_stop 审批态分支 commands.rs:252)再新建:
if session.generating {
session.generating = false;
session.pending_approvals.clear();
session.stop_flag.store(true, Ordering::SeqCst);
let conv_id = session.active_conversation_id.clone();
drop(session);
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted { ... conversation_id: conv_id });
session = state.ai_session.lock().await;
}
session.active_conversation_id = Some(id.clone());
// ...
含 ④ 策略统一:软复位后 newConversation 与 switchConversation:518(readonly 放行)形成一致心智——「新建=放弃当前生成 / 切换=只读接管」。
需加 app: AppHandle 参数(tauri 注入,前端无感)。
4.4 ③ loop 对话一致性校验 — 竞态防护(P0)
② 软复位后旧 loop 退出时 session.messages.push(旧 assistant)(agentic.rs:171/173)污染新对话。loop 每次 push 前校验:
let mut session = session_arc.lock().await;
if session.active_conversation_id.as_deref() != Some(&conv_id) {
return; // 对话已切走,丢弃本轮,不 push(guard ① 兜底复位)
}
if has_tool_calls { /* push */ }
同样校验建议加在 save_conversation 调用前(agentic.rs:90/186/212/254),防旧 loop 写库污染。
4.5 ⑥ stop 流式态兜底 — 治 loop 已死(P1)
ai_chat_stop(commands.rs:261)流式态分支仅置 stop_flag,依赖 loop 自复位。若 loop 已 panic/abort,stop_flag 无人读,generating 永不复位。复用 ② 抽出的复位 helper 兜底。
4.6 ⑦ stop_notify 即时打断(P1,可选)
stop_flag(AtomicBool)无 async 通知能力,靠 30s 心跳 tick 间接唤醒 select!。换 tokio::sync::Notify 即时打断。stop_flag 保留作循环顶快检(冗余双保险)。
5. 状态转换图
ai_chat_send
│
▼
┌─────────────────┐
│ Streaming │◄────────────────┐
│ generating=true │ │
│ pending=空 │ │
└────────┬────────┘ │
│ │
┌──────────┼──────────┐ │
│ │ │ │
有待审批 无工具调用 stop_flag ai_approve
│ (收敛) (用户停) (处理完)
▼ │ │ │
┌──────────────┐ │ ▼ │
│AwaitingApproval│ │ ┌─────────┐ │
│ generating=true │ │ │ Stopping│ │
│ pending非空 │ │ │stop=true│ │
└──────┬───────┘ │ └────┬────┘ │
│ │ │ │
ai_approve │ loop 检查点 │
(处理完) │ 退出+复位 │
│ │ │ │
└───────────┼──────────┼───────────────┘
▼ ▼
┌────────────────────┐
│ Idle │
│ generating=false │
└────────────────────┘
复位保障:所有退出路径(实线)由 ① guard 的 Drop 兜底;异常路径(panic/abort,虚线)同样由 Drop 覆盖。② 软复位作为 newConversation 入口的二级防线。
6. 实施计划
P0(用户 bug 根治组合,必须同批)
| # | 项 | 文件 | 依赖 |
|---|---|---|---|
| B-260615-09 | ① RAII guard 收尾 | agentic.rs | 无(上游) |
| B-260615-10 | ② newConversation 软复位(含④策略统一) | commands.rs:451 | 必须配 B-11 |
| B-260615-11 | ③ loop 对话一致性校验 | agentic.rs:158 | B-10 配套 |
依赖关系:
- ②③ 强耦合(②放开 newConversation 触发③的竞态,单独做②会引入数据污染)→ 捆绑实施
- ① 是②③的上游根治(①修了仍需②③,因 panic 兜底不可能 100% 覆盖 OS 级 abort)→ 三者同批
P1
| # | 项 | 文件 |
|---|---|---|
| B-260615-12 | ⑤ session.state() enum 视图(轻量状态机读侧) | mod.rs:97 |
| B-260615-13 | ⑥ stop 流式态兜底(复用②复位 helper) | commands.rs:261 |
| B-260615-14 | ⑦ stop_flag 换 Notify 即时打断 | stream_recv.rs:148 |
P2
| # | 项 | 文件 |
|---|---|---|
| B-260615-15 | ⑧ heartbeat interval 提到 loop 外 | stream_recv.rs:138 |
| B-260615-16 | ⑨ MAX_AGENT_ITERATIONS 配置化 | agentic.rs:28 |
| B-260615-17 | ⑩ key_len 复用 build_provider_for 结果(FR-S1 相关) | agentic.rs:67 |
| B-260615-18 | ⑪ pending_approvals 单例+conv_id 路由加注释 | mod.rs:107 |
7. 验证
P0 验证(用户 bug 复现 + 根治)
- 卡死复现:构造 panic 场景(如临时在 run_agentic_loop 内 panic)→ 验证 generating 卡 true → newConversation 被拦
- ① guard 兜底:同上 panic 场景 → guard Drop 复位 generating → newConversation 正常
- ② 软复位:手动置 generating=true(模拟卡死)→ newConversation 强制复位成功新建
- ③ 一致性:② 软复位后旧 loop 退出 → 验证不 push 到新对话 messages
回归
- 正常收敛(无工具调用)→ break → guard 复位 ✅
- 审批等待 → pending 非空 → guard 不复位(设计)→ ai_approve → try_continue 续 ✅
- stop 流式态 → stop_flag → loop 退出 → guard 复位 ✅
- stop 审批态 → 直接清 pending + 复位 ✅
8. 取舍 / 未做
| 项 | 决策 | 理由 |
|---|---|---|
| 独立状态机框架 | 不做 | 过度封装,轻量方案(guard+视图)功能等价 |
| enum 字段替换 bool | 不做 | 改字段+所有读写点,大改;视图方法够用 |
| stop_flag 转 enum | 不做 | 跨锁信号必须 AtomicBool,enum 锁内无法替代 |
| panic 100% 兜底 | 不可能 | OS 级 abort(OOM kill)guard 抓不到,②软复位兜底 |
| 命令黑名单/资源限制 | 不做(run_command 安全边界) | 属 B/C/D 方案领域,A 方案靠人审 |