422c8931d6
实现: 在rust内存里维护错误事件队列, webview调用tauri command传入, 后台任务agent工具等直接插入 把rust , webview console的日志统一写到AppData文件夹下的(滚动保存的)日志文件. rust对传入的错误进行筛选,脱敏, 防抖,后通知前端提醒用户. 用户提醒是一个不阻塞的小UI, 展开后可以选择错误上报, 可以附加文字描述 上传时附带最近日志, 错误堆栈等信息 元数据存在数据库, 考虑到字符串信息很难查询, 所以在api server打包成zip存在OSS. 管理页面新增错误报告的查看页面     --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/240 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
101 lines
3.9 KiB
Rust
101 lines
3.9 KiB
Rust
//! 调试落盘:保存 LLM 原始输出 / 失败输入,便于排查截断、空返回等问题。
|
||
//!
|
||
//! 本模块用 `#[cfg(all(debug_assertions, not(test)))]` 声明——仅在开发(debug)
|
||
//! 且非测试构建中编入二进制;生产 release 与 cargo test 下整个模块与其调用处一并被剔除,
|
||
//! 不会往仓库写任何文件。
|
||
|
||
use crate::unix_millis;
|
||
use std::fs;
|
||
use std::path::PathBuf;
|
||
|
||
// 找到仓库根(包含 apps/ai-game-creator-shell/src-tauri/Cargo.toml 的目录),
|
||
// 以便把草案落到仓库内而非 tmp 项目目录。
|
||
fn repo_root() -> Option<PathBuf> {
|
||
let mut roots = Vec::new();
|
||
if let Ok(cwd) = std::env::current_dir() {
|
||
roots.push(cwd);
|
||
}
|
||
if let Ok(exe) = std::env::current_exe() {
|
||
if let Some(parent) = exe.parent() {
|
||
roots.push(parent.to_path_buf());
|
||
}
|
||
}
|
||
for root in roots {
|
||
for directory in root.ancestors().take(8) {
|
||
let marker = directory
|
||
.join("apps")
|
||
.join("ai-game-creator-shell")
|
||
.join("src-tauri")
|
||
.join("Cargo.toml");
|
||
if marker.is_file() {
|
||
return Some(directory.to_path_buf());
|
||
}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
// 把生成器原始返回(含被截断、解析失败的内容)保存到仓库 .llm-drafts/,
|
||
// 便于排查 “EOF while parsing a string” 这类输出截断问题。尽力而为,不阻断主流程。
|
||
pub(crate) fn persist_snapshot(raw_content: &str) {
|
||
let Some(repo_root) = repo_root() else {
|
||
app_log!("llm.draft.snapshot.skip: 未能定位仓库根目录");
|
||
return;
|
||
};
|
||
let dir = repo_root
|
||
.join("apps")
|
||
.join("ai-game-creator-shell")
|
||
.join(".llm-drafts");
|
||
if let Err(error) = fs::create_dir_all(&dir) {
|
||
app_log!("llm.draft.snapshot.dir.failed: {}: {error}", dir.display());
|
||
return;
|
||
}
|
||
let path = dir.join(format!("draft-{}.txt", unix_millis()));
|
||
if let Err(error) = fs::write(&path, raw_content) {
|
||
app_log!(
|
||
"llm.draft.snapshot.write.failed: {}: {error}",
|
||
path.display()
|
||
);
|
||
return;
|
||
}
|
||
// 同步更新 latest.txt,方便直接打开最近一次草案。
|
||
let latest = dir.join("latest.txt");
|
||
let _ = fs::write(&latest, raw_content);
|
||
app_log!("llm.draft.snapshot.saved: {}", path.display());
|
||
}
|
||
|
||
// LLM 调用失败(例如返回内容为空)时,把本次发送给模型的输入(system + 完整 user prompt)
|
||
// 连同错误信息一起落盘到 .llm-drafts/,便于按同样输入复现与定位。尽力而为,不阻断主流程。
|
||
pub(crate) fn persist_error_input(system_prompt: &str, user_prompt: &str, error: &str) {
|
||
let Some(repo_root) = repo_root() else {
|
||
app_log!("llm.draft.error-input.skip: 未能定位仓库根目录");
|
||
return;
|
||
};
|
||
let dir = repo_root
|
||
.join("apps")
|
||
.join("ai-game-creator-shell")
|
||
.join(".llm-drafts");
|
||
if let Err(io_error) = fs::create_dir_all(&dir) {
|
||
app_log!(
|
||
"llm.draft.error-input.dir.failed: {}: {io_error}",
|
||
dir.display()
|
||
);
|
||
return;
|
||
}
|
||
let body = format!(
|
||
"# LLM 生成失败输入快照\n\n## 错误\n{error}\n\n## System Prompt\n{system_prompt}\n\n## User Prompt(含用户需求/短长期记忆/spec/agenda/组 brief/findings)\n{user_prompt}\n",
|
||
);
|
||
let path = dir.join(format!("error-input-{}.txt", unix_millis()));
|
||
if let Err(io_error) = fs::write(&path, &body) {
|
||
app_log!(
|
||
"llm.draft.error-input.write.failed: {}: {io_error}",
|
||
path.display()
|
||
);
|
||
return;
|
||
}
|
||
// 同步更新 latest-error-input.txt,方便直接打开最近一次失败输入。
|
||
let latest = dir.join("latest-error-input.txt");
|
||
let _ = fs::write(&latest, &body);
|
||
app_log!("llm.draft.error-input.saved: {}", path.display());
|
||
}
|