feat: 完整日志系统 + 设置增强 + 子窗口交互优化
This commit is contained in:
+7
-7
@@ -63,8 +63,7 @@ pub async fn call_ai_api(
|
||||
{"type": "image_url", "image_url": {"url": format!("data:image/png;base64,{}", image_base64)}}
|
||||
]}
|
||||
],
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.3
|
||||
"max_tokens": 4096
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
@@ -76,8 +75,7 @@ pub async fn call_ai_api(
|
||||
{"type": "image_url", "image_url": {"url": format!("data:image/png;base64,{}", image_base64)}}
|
||||
]}
|
||||
],
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.3
|
||||
"max_tokens": 4096
|
||||
})
|
||||
};
|
||||
|
||||
@@ -103,8 +101,10 @@ pub async fn call_ai_api(
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
if error_text.contains("does not support image") || error_text.contains("does not support") && error_text.contains("image") {
|
||||
if error_text.contains("does not support image") || error_text.contains("does not support") && error_text.contains("image") || error_text.contains("Cannot read") || error_text.contains("image.png") {
|
||||
last_error = "当前模型不支持图片输入,请在设置中更换为支持视觉的模型(如 Qwen/Qwen2.5-32B-Instruct、gpt-4o 等)".to_string();
|
||||
} else if status.as_u16() == 401 || error_text.contains("invalid") || error_text.contains("Invalid") || error_text.contains("unauthorized") || error_text.contains("Unauthorized") || error_text.contains("Token") || error_text.contains("API key") {
|
||||
last_error = "API 认证失败,请检查 API Key 是否正确".to_string();
|
||||
} else {
|
||||
last_error = format!("API 返回错误 ({}): {}", status, &error_text);
|
||||
}
|
||||
@@ -117,14 +117,14 @@ pub async fn call_ai_api(
|
||||
}
|
||||
|
||||
let body = response.text().await.map_err(|e| e.to_string())?;
|
||||
if body.contains("does not support image") || body.contains("does not support") && body.contains("image") {
|
||||
if body.contains("does not support image") || body.contains("does not support") && body.contains("image") || body.contains("Cannot read") || body.contains("image.png") {
|
||||
last_error = "当前模型不支持图片输入,请在设置中更换为支持视觉的模型(如 Qwen/Qwen2.5-32B-Instruct、gpt-4o 等)".to_string();
|
||||
log::warn!("AI API body error: {}", last_error);
|
||||
return Err(last_error);
|
||||
}
|
||||
let json: Value = serde_json::from_str(&body).map_err(|e| format!("解析返回JSON失败: {}", e))?;
|
||||
if let Some(err_msg) = json["error"]["message"].as_str() {
|
||||
if err_msg.contains("does not support image") || err_msg.contains("image input") {
|
||||
if err_msg.contains("does not support image") || err_msg.contains("image input") || err_msg.contains("Cannot read") || err_msg.contains("image.png") {
|
||||
last_error = "当前模型不支持图片输入,请在设置中更换为支持视觉的模型(如 Qwen/Qwen2.5-32B-Instruct、gpt-4o 等)".to_string();
|
||||
return Err(last_error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::AppState;
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn log_write(state: State<AppState>, level: String, message: String, source: String) -> Result<(), String> {
|
||||
let db = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db.write_log(&level, &message, &source);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn log_list(state: State<AppState>, limit: Option<i64>) -> Result<Vec<serde_json::Value>, String> {
|
||||
let db = state.db.lock().map_err(|e| e.to_string())?;
|
||||
Ok(db.list_logs(limit.unwrap_or(200)))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn log_clear(state: State<AppState>) -> Result<(), String> {
|
||||
let db = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db.clear_logs();
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod history;
|
||||
pub mod grading;
|
||||
pub mod rubric;
|
||||
pub mod template;
|
||||
pub mod log;
|
||||
|
||||
use crate::ai;
|
||||
use crate::create_dialog_window;
|
||||
@@ -22,12 +23,13 @@ pub struct ImageResult {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn dialog_open(app: AppHandle, dialog_type: String) -> Result<(), String> {
|
||||
let sizes: [(&str, f64, f64); 5] = [
|
||||
let sizes: [(&str, f64, f64); 6] = [
|
||||
("settings", 520.0, 540.0),
|
||||
("history", 860.0, 660.0),
|
||||
("templates", 720.0, 540.0),
|
||||
("rubric", 650.0, 700.0),
|
||||
("result", 620.0, 680.0),
|
||||
("logs", 860.0, 660.0),
|
||||
];
|
||||
let (_, w, h) = sizes.iter().find(|(t, _, _)| *t == dialog_type).unwrap_or(&("result", 600.0, 780.0));
|
||||
create_dialog_window(&app, &dialog_type, *w, *h).map_err(|e| e.to_string())
|
||||
@@ -87,3 +89,50 @@ pub async fn ai_grade(app: AppHandle, request: GradeRequest) -> Result<ai::Grade
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FetchModelsRequest {
|
||||
pub api_key: String,
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ai_fetch_models(request: FetchModelsRequest) -> Result<Vec<String>, String> {
|
||||
let url = format!("{}/models", request.base_url.trim_end_matches('/'));
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()
|
||||
.map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?;
|
||||
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", request.api_key))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("请求失败: {}", e))?;
|
||||
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("API 返回错误 ({}): {}", status, text));
|
||||
}
|
||||
|
||||
let json: serde_json::Value = resp.json().await
|
||||
.map_err(|e| format!("解析返回数据失败: {}", e))?;
|
||||
|
||||
let models = json["data"]
|
||||
.as_array()
|
||||
.ok_or_else(|| "返回数据格式异常:缺少 data 字段".to_string())?;
|
||||
|
||||
let ids: Vec<String> = models
|
||||
.iter()
|
||||
.filter_map(|m| m["id"].as_str().map(|s| s.to_string()))
|
||||
.collect();
|
||||
|
||||
if ids.is_empty() {
|
||||
return Err("未获取到任何模型".to_string());
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use tauri_plugin_store::StoreExt;
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppSettings {
|
||||
pub api_key: String,
|
||||
pub api_key_presets: std::collections::HashMap<String, String>,
|
||||
pub base_url: String,
|
||||
pub model: String,
|
||||
pub shortcut: String,
|
||||
@@ -20,6 +21,7 @@ impl Default for AppSettings {
|
||||
fn default() -> Self {
|
||||
AppSettings {
|
||||
api_key: String::new(),
|
||||
api_key_presets: std::collections::HashMap::new(),
|
||||
base_url: "https://api.siliconflow.cn/v1".to_string(),
|
||||
model: String::new(),
|
||||
shortcut: "Alt+Q".to_string(),
|
||||
@@ -37,6 +39,7 @@ pub async fn settings_get(app: AppHandle) -> Result<AppSettings, String> {
|
||||
let store = app.store("settings.json").map_err(|e| e.to_string())?;
|
||||
Ok(AppSettings {
|
||||
api_key: store.get("apiKey").and_then(|v| v.as_str().map(|s| s.to_string())).unwrap_or_default(),
|
||||
api_key_presets: store.get("apiKeyPresets").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default(),
|
||||
base_url: store.get("baseUrl").and_then(|v| v.as_str().map(|s| s.to_string())).unwrap_or_else(|| "https://api.siliconflow.cn/v1".to_string()),
|
||||
model: store.get("model").and_then(|v| v.as_str().map(|s| s.to_string())).unwrap_or_default(),
|
||||
shortcut: store.get("shortcut").and_then(|v| v.as_str().map(|s| s.to_string())).unwrap_or_else(|| "Alt+Q".to_string()),
|
||||
@@ -55,6 +58,7 @@ pub async fn settings_get(app: AppHandle) -> Result<AppSettings, String> {
|
||||
pub async fn settings_set(app: AppHandle, settings: AppSettings) -> Result<bool, String> {
|
||||
let store = app.store("settings.json").map_err(|e| e.to_string())?;
|
||||
store.set("apiKey", serde_json::Value::String(settings.api_key));
|
||||
store.set("apiKeyPresets", serde_json::to_value(&settings.api_key_presets).unwrap_or(serde_json::Value::Object(Default::default())));
|
||||
store.set("baseUrl", serde_json::Value::String(settings.base_url));
|
||||
store.set("model", serde_json::Value::String(settings.model));
|
||||
store.set("shortcut", serde_json::Value::String(settings.shortcut));
|
||||
|
||||
+36
-1
@@ -48,6 +48,13 @@ impl Database {
|
||||
rubric TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
reference_answer_image TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
level TEXT NOT NULL DEFAULT 'INFO',
|
||||
message TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);"
|
||||
).expect("Failed to create tables");
|
||||
Database { conn }
|
||||
@@ -159,7 +166,7 @@ impl Database {
|
||||
let q_max: f64 = row.get(2)?;
|
||||
let q_ref: String = row.get(3)?;
|
||||
let q_rubric: String = row.get(4)?;
|
||||
let q_img: String = row.get(5)?;
|
||||
let q_img: String = row.get(6)?;
|
||||
let ref_img = if q_img.is_empty() || q_img.starts_with("data:") { q_img } else { format!("data:image/png;base64,{}", q_img) };
|
||||
Ok(serde_json::json!({
|
||||
"questionTitle": q_title,
|
||||
@@ -237,4 +244,32 @@ impl Database {
|
||||
pub fn delete_template(&self, id: i64) -> bool {
|
||||
self.conn.execute("DELETE FROM exam WHERE id = ?", params![id]).is_ok()
|
||||
}
|
||||
|
||||
// Log methods
|
||||
pub fn write_log(&self, level: &str, message: &str, source: &str) {
|
||||
let _ = self.conn.execute(
|
||||
"INSERT INTO logs (level, message, source) VALUES (?1, ?2, ?3)",
|
||||
params![level, message, source],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn list_logs(&self, limit: i64) -> Vec<serde_json::Value> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT * FROM logs ORDER BY id DESC LIMIT ?"
|
||||
).unwrap();
|
||||
let rows = stmt.query_map(params![limit], |row| {
|
||||
Ok(serde_json::json!({
|
||||
"id": row.get::<_, i64>(0)?,
|
||||
"level": row.get::<_, String>(1)?,
|
||||
"message": row.get::<_, String>(2)?,
|
||||
"source": row.get::<_, String>(3)?,
|
||||
"createdAt": row.get::<_, String>(4)?
|
||||
}))
|
||||
}).unwrap();
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
}
|
||||
|
||||
pub fn clear_logs(&self) {
|
||||
let _ = self.conn.execute("DELETE FROM logs", []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ pub fn run() {
|
||||
commands::screenshot::screenshot_capture,
|
||||
commands::screenshot::screenshot_cancel,
|
||||
commands::ai_grade,
|
||||
commands::ai_fetch_models,
|
||||
commands::history::history_list,
|
||||
commands::history::history_delete,
|
||||
commands::history::history_get_by_id,
|
||||
@@ -117,6 +118,9 @@ pub fn run() {
|
||||
commands::template::template_import_json,
|
||||
commands::dialog_open,
|
||||
commands::dialog_open_image,
|
||||
commands::log::log_write,
|
||||
commands::log::log_list,
|
||||
commands::log::log_clear,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
@@ -165,14 +169,16 @@ pub fn create_dialog_window(
|
||||
"settings" => "设置",
|
||||
"history" => "历史记录",
|
||||
"templates" => "模板管理",
|
||||
"logs" => "日志",
|
||||
"rubric" => "评分标准",
|
||||
"result" => "评分结果",
|
||||
_ => "AI 阅卷助手",
|
||||
})
|
||||
.inner_size(width, height)
|
||||
.min_inner_size(400.0, 300.0)
|
||||
.position(dx, dy)
|
||||
.decorations(false)
|
||||
.resizable(false)
|
||||
.resizable(true)
|
||||
.build()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user