新增: 桌面能力——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:
@@ -23,5 +23,4 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# ts-rs 类型导出
|
|
||||||
export-types = []
|
export-types = []
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
* ===================================================================== */
|
* ===================================================================== */
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
use tauri::Emitter;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::model::*;
|
use crate::model::*;
|
||||||
@@ -154,6 +155,92 @@ pub fn ping() -> String {
|
|||||||
/// 保存 deck 到文件
|
/// 保存 deck 到文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn save_file(content: String) -> Result<String, String> {
|
pub fn save_file(content: String) -> Result<String, String> {
|
||||||
// 后续通过 tauri-plugin-dialog 实现文件对话框
|
|
||||||
Ok("ok".to_string())
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ pub fn run() {
|
|||||||
commands::get_deck,
|
commands::get_deck,
|
||||||
commands::set_navigation,
|
commands::set_navigation,
|
||||||
commands::save_file,
|
commands::save_file,
|
||||||
|
commands::ai_proxy,
|
||||||
|
commands::ai_proxy_stream,
|
||||||
|
commands::write_file,
|
||||||
|
commands::read_file,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
Reference in New Issue
Block a user