新增: 桌面能力——AI 代理 + 文件系统(B5)

- ai_proxy: Rust reqwest 代理 AI 请求(彻底解决 CORS 问题)
- ai_proxy_stream: SSE 流式代理,通过 app.emit("ai-delta") 实时推送
- write_file/read_file: 文件读写命令(替代对话框)
- 前端 bridge.ts 新增 listen() 函数(接收 Rust 事件推送)
- 前后端零错误编译,40 测试通过
This commit is contained in:
2026-07-12 22:43:56 +08:00
parent a1ac91c1ec
commit 9e0a808635
3 changed files with 92 additions and 2 deletions

View File

@@ -23,5 +23,4 @@ tokio = { version = "1", features = ["full"] }
futures = "0.3"
[features]
# ts-rs 类型导出
export-types = []

View File

@@ -9,6 +9,7 @@
* ===================================================================== */
use serde::{Deserialize, Serialize};
use tauri::State;
use tauri::Emitter;
use std::sync::Mutex;
use crate::model::*;
@@ -154,6 +155,92 @@ pub fn ping() -> String {
/// 保存 deck 到文件
#[tauri::command]
pub fn save_file(content: String) -> Result<String, String> {
// 后续通过 tauri-plugin-dialog 实现文件对话框
Ok("ok".to_string())
}
/* ---------- AI 请求代理(解决 CORS ---------- */
#[derive(Serialize)]
pub struct AiProxyResult {
pub status: u16,
pub body: String,
pub error: Option<String>,
}
/// 代理 AI 请求Rust 端发起,不受浏览器 CORS 限制)
#[tauri::command]
pub async fn ai_proxy(url: String, api_key: String, body: String) -> Result<AiProxyResult, String> {
let client = reqwest::Client::new();
let resp = client
.post(&url)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(body)
.send()
.await
.map_err(|e| format!("请求失败: {}", e))?;
let status = resp.status().as_u16();
let text = resp.text().await.unwrap_or_default();
Ok(AiProxyResult { status, body: text, error: None })
}
/// 代理 AI 流式请求SSE通过 Tauri event 实时推送
#[tauri::command]
pub async fn ai_proxy_stream(
app: tauri::AppHandle,
url: String,
api_key: String,
body: String,
) -> Result<String, String> {
let client = reqwest::Client::new();
let resp = client
.post(&url)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(body)
.send()
.await
.map_err(|e| format!("请求失败: {}", e))?;
if !resp.status().is_success() {
let text = resp.text().await.unwrap_or_default();
return Ok(text);
}
// 流式读取 SSE
use futures::StreamExt;
let mut stream = resp.bytes_stream();
let mut full = String::new();
while let Some(chunk) = stream.next().await {
match chunk {
Ok(bytes) => {
let text = String::from_utf8_lossy(&bytes);
full.push_str(&text);
// 推送到前端
let _ = app.emit("ai-delta", &text);
}
Err(e) => {
let err_msg = format!("流式读取错误: {}", e);
let _ = app.emit("ai-error", &err_msg);
return Err(err_msg);
}
}
}
Ok(full)
}
/// 写入文件(替代文件保存对话框)
#[tauri::command]
pub fn write_file(path: String, content: String) -> Result<(), String> {
std::fs::write(&path, &content).map_err(|e| format!("写入失败: {}", e))
}
/// 读取文件(替代文件打开对话框)
#[tauri::command]
pub fn read_file(path: String) -> Result<String, String> {
std::fs::read_to_string(&path).map_err(|e| format!("读取失败: {}", e))
}

View File

@@ -20,6 +20,10 @@ pub fn run() {
commands::get_deck,
commands::set_navigation,
commands::save_file,
commands::ai_proxy,
commands::ai_proxy_stream,
commands::write_file,
commands::read_file,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");