优化: run_command 实时流式 + 审批浮窗修复 + 文档探索闭环(fetch_url+obscura引导+model_fetch兼容+prompt策略+厂商预设)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
//! run_command 实时流式输出 — task-local sink 机制。
|
||||
//!
|
||||
//! 治「run_command 执行中黑盒」:execute() 等 exit 才返回整块 stdout/stderr,
|
||||
//! 长命令(cargo/npm 构建)期间前端只看 Started→Completed,中间进度不可见。
|
||||
//!
|
||||
//! 架构约束:工具 handler 注册为 `Box<dyn Fn(Value) -> Future>`(ai_tools.rs:48),
|
||||
//! 签名只收 args 不收 AppHandle/tool_call_id,无法直接 emit 事件。改 schema 把 id
|
||||
//! 塞进 args 会泄漏给 LLM,不可取。
|
||||
//!
|
||||
//! 本模块用 [`tokio::task_local!`] 解耦:调用方(execute_with_heartbeat / ai_approve)
|
||||
//! 持有 AppHandle + tool_call_id + conv_id,调 `tools.execute` 前用 [`scope`] 把一个
|
||||
//! [`CommandSink`] 注入当前 task 上下文;run_command handler 在 tools/file.rs 内
|
||||
//! 读 task-local(同一 task,因 tools.execute 不 spawn 直接 await handler),
|
||||
//! 命中则改走 shell `execute_streaming`,每行回调 [`emit_output`] → AiCommandOutput。
|
||||
//!
|
||||
//! 未注入 sink(非 run_command / 调用方未配 scope)时 [`emit_output`] 静默 noop,
|
||||
//! 兜底不报错不阻断。
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
use super::AiChatEvent;
|
||||
use df_execute::shell::StreamKind;
|
||||
|
||||
/// 一次 run_command 调用的输出下沉目标(emit 事件所需上下文)。
|
||||
#[derive(Clone)]
|
||||
pub struct CommandSink {
|
||||
app: AppHandle,
|
||||
tool_call_id: String,
|
||||
conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
impl CommandSink {
|
||||
pub fn new(app: AppHandle, tool_call_id: String, conversation_id: Option<String>) -> Self {
|
||||
Self { app, tool_call_id, conversation_id }
|
||||
}
|
||||
|
||||
/// emit 一行 stdout/stderr(AiCommandOutput,双写 app.emit + ai_event_bus)。
|
||||
/// emit 失败静默吞(前端未 listen / 总线无订阅不阻断命令执行)。
|
||||
fn emit(&self, kind: StreamKind, line: &str) {
|
||||
let ev = AiChatEvent::AiCommandOutput {
|
||||
id: self.tool_call_id.clone(),
|
||||
stream: kind.as_str().to_string(),
|
||||
line: line.to_string(),
|
||||
conversation_id: self.conversation_id.clone(),
|
||||
};
|
||||
let _ = self.app.emit("ai-chat-event", ev.clone());
|
||||
let _ = self.app.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
}
|
||||
}
|
||||
|
||||
// task-local 槽:tools.execute 调用期间(同 task)handler 可读。
|
||||
// 嵌套 Option:外层是 task-local 是否 set,内层是是否注入 sink(None = 已 scope 但无 sink)。
|
||||
tokio::task_local! {
|
||||
static SINK: Option<CommandSink>;
|
||||
}
|
||||
|
||||
/// 在 sink 作用域内执行 future。future 完成后自动清理(无残留)。
|
||||
///
|
||||
/// 调用方:execute_with_heartbeat / ai_approve 在调 `tools.execute("run_command", args)`
|
||||
/// 前包一层 `command_stream::scope(Some(sink), async { tools.execute(...).await })`。
|
||||
pub async fn scope<F, R>(sink: Option<CommandSink>, fut: F) -> R
|
||||
where
|
||||
F: std::future::Future<Output = R>,
|
||||
{
|
||||
SINK.scope(sink, fut).await
|
||||
}
|
||||
|
||||
/// run_command handler 读取:回调每行输出 → emit AiCommandOutput。
|
||||
///
|
||||
/// task-local 未 set(非经 scope 调用,如直接单测调 handler)或内层 None → 静默 noop。
|
||||
/// 返回值忽略(emit 失败不阻断命令)。
|
||||
pub fn emit_output(kind: StreamKind, line: &str) {
|
||||
// LocalKey::with 在 task-local 未 scope 时返 AccessError,静默吞(noop 兜底)。
|
||||
let _ = SINK.with(|maybe_sink: &Option<CommandSink>| {
|
||||
if let Some(sink) = maybe_sink {
|
||||
sink.emit(kind, line);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user