feat: 完整日志系统 + 设置增强 + 子窗口交互优化

This commit is contained in:
2026-07-20 13:17:28 +08:00
parent 1bc2da9c20
commit f89391c791
22 changed files with 559 additions and 112 deletions
+7 -7
View File
@@ -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);
}
+22
View File
@@ -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(())
}
+50 -1
View File
@@ -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)
}
+4
View File
@@ -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
View File
@@ -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", []);
}
}
+7 -1
View File
@@ -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(())
}