初始提交:识流 AI 助手项目
微信自动回复机器人,基于截图+OCR识别消息,支持关键词规则和 AI(OpenAI/DeepSeek/Dify)自动回复。 技术栈:PySide6 + Flask + Vue3 + RapidOCR + SQLite 注:OCR大模型文件(.onnx / .pdiparams)不纳入版本控制,需单独下载。 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
356
frontend/src-tauri/src/lib.rs
Normal file
356
frontend/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::TcpStream,
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, Command, Stdio},
|
||||
sync::Mutex,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use tauri::{AppHandle, Manager, RunEvent};
|
||||
|
||||
struct AppState {
|
||||
backend_child: Mutex<Option<Child>>,
|
||||
backend_bootstrap_error: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
struct RuntimePaths {
|
||||
app_data_dir: PathBuf,
|
||||
log_root_dir: PathBuf,
|
||||
runtime_log_dir: PathBuf,
|
||||
bot_log_file: PathBuf,
|
||||
ocr_log_file: PathBuf,
|
||||
ocr_save_dir: PathBuf,
|
||||
blocked_row_cache_file: PathBuf,
|
||||
}
|
||||
|
||||
struct BackendLaunchPlan {
|
||||
program: PathBuf,
|
||||
args: Vec<PathBuf>,
|
||||
working_dir: PathBuf,
|
||||
envs: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn project_root() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.to_path_buf())
|
||||
.expect("project root not found")
|
||||
}
|
||||
|
||||
fn backend_python() -> PathBuf {
|
||||
project_root().join(".venv").join("Scripts").join("python.exe")
|
||||
}
|
||||
|
||||
fn backend_entry() -> PathBuf {
|
||||
project_root().join("backend_main.py")
|
||||
}
|
||||
|
||||
fn normalize_mode(value: &str) -> String {
|
||||
let mode = value.trim().to_ascii_lowercase();
|
||||
match mode.as_str() {
|
||||
"release" | "prod" | "production" | "bundle" | "packaged" => "release".to_string(),
|
||||
_ => "dev".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn backend_mode() -> String {
|
||||
std::env::var("OPENCLAW_BACKEND_MODE")
|
||||
.map(|value| normalize_mode(&value))
|
||||
.unwrap_or_else(|_| {
|
||||
if cfg!(debug_assertions) {
|
||||
"dev".to_string()
|
||||
} else {
|
||||
"release".to_string()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn backend_addr() -> String {
|
||||
std::env::var("APP_HOST")
|
||||
.map(|host| {
|
||||
let port = std::env::var("APP_PORT").unwrap_or_else(|_| "5000".to_string());
|
||||
format!("{}:{}", host, port)
|
||||
})
|
||||
.unwrap_or_else(|_| "127.0.0.1:5000".to_string())
|
||||
}
|
||||
|
||||
fn is_backend_alive() -> bool {
|
||||
TcpStream::connect(backend_addr()).is_ok()
|
||||
}
|
||||
|
||||
fn wait_for_backend(child: &mut Child, timeout: Duration) -> Result<(), String> {
|
||||
let started = Instant::now();
|
||||
while started.elapsed() < timeout {
|
||||
if is_backend_alive() {
|
||||
return Ok(());
|
||||
}
|
||||
if let Ok(Some(status)) = child.try_wait() {
|
||||
return Err(format!("独立后台进程已退出,退出码: {}", status));
|
||||
}
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
Err(format!("后台启动超时,未监听 {}", backend_addr()))
|
||||
}
|
||||
|
||||
fn ensure_dir(path: &Path) -> Result<(), String> {
|
||||
std::fs::create_dir_all(path).map_err(|err| format!("创建目录失败 {}: {}", path.display(), err))
|
||||
}
|
||||
|
||||
fn resolve_runtime_paths(app: &AppHandle) -> Result<RuntimePaths, String> {
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_local_data_dir()
|
||||
.map_err(|err| format!("获取应用数据目录失败: {}", err))?;
|
||||
let log_root_dir = app_data_dir.join("logs");
|
||||
let runtime_log_dir = log_root_dir.join("runtime");
|
||||
let bot_log_file = runtime_log_dir.join("bot").join("wechat_multi_chat_bot.log");
|
||||
let ocr_log_file = runtime_log_dir.join("ocr").join("wechat_ocr.log");
|
||||
let ocr_save_dir = log_root_dir.join("ocr_debug_images");
|
||||
let blocked_row_cache_file = log_root_dir.join("state").join("blocked_rows.json");
|
||||
|
||||
ensure_dir(&app_data_dir)?;
|
||||
ensure_dir(&runtime_log_dir.join("bot"))?;
|
||||
ensure_dir(&runtime_log_dir.join("ocr"))?;
|
||||
ensure_dir(&ocr_save_dir)?;
|
||||
ensure_dir(
|
||||
blocked_row_cache_file
|
||||
.parent()
|
||||
.ok_or_else(|| "状态缓存目录无效".to_string())?,
|
||||
)?;
|
||||
|
||||
Ok(RuntimePaths {
|
||||
app_data_dir,
|
||||
log_root_dir,
|
||||
runtime_log_dir,
|
||||
bot_log_file,
|
||||
ocr_log_file,
|
||||
ocr_save_dir,
|
||||
blocked_row_cache_file,
|
||||
})
|
||||
}
|
||||
|
||||
fn runtime_envs(app: &AppHandle, mode: &str) -> Result<HashMap<String, String>, String> {
|
||||
let paths = resolve_runtime_paths(app)?;
|
||||
let mut envs = HashMap::new();
|
||||
envs.insert("APP_NAME".to_string(), "AiShiliu".to_string());
|
||||
envs.insert("APP_DATA_DIR".to_string(), paths.app_data_dir.display().to_string());
|
||||
envs.insert("LOG_ROOT_DIR".to_string(), paths.log_root_dir.display().to_string());
|
||||
envs.insert(
|
||||
"RUNTIME_LOG_DIR".to_string(),
|
||||
paths.runtime_log_dir.display().to_string(),
|
||||
);
|
||||
envs.insert("BOT_LOG_FILE".to_string(), paths.bot_log_file.display().to_string());
|
||||
envs.insert("OCR_LOG_FILE".to_string(), paths.ocr_log_file.display().to_string());
|
||||
envs.insert("OCR_SAVE_DIR".to_string(), paths.ocr_save_dir.display().to_string());
|
||||
envs.insert(
|
||||
"BLOCKED_ROW_CACHE_FILE".to_string(),
|
||||
paths.blocked_row_cache_file.display().to_string(),
|
||||
);
|
||||
|
||||
envs.insert("OPENCLAW_BACKEND_MODE".to_string(), mode.to_string());
|
||||
envs.insert("OPENCLAW_RUNTIME_MODE".to_string(), mode.to_string());
|
||||
Ok(envs)
|
||||
}
|
||||
|
||||
fn release_backend_candidates(app: &AppHandle) -> Result<Vec<PathBuf>, String> {
|
||||
let resource_dir = app
|
||||
.path()
|
||||
.resource_dir()
|
||||
.map_err(|err| format!("获取资源目录失败: {}", err))?;
|
||||
let exe_dir = std::env::current_exe()
|
||||
.map_err(|err| format!("获取主程序路径失败: {}", err))?
|
||||
.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.ok_or_else(|| "无法解析主程序目录".to_string())?;
|
||||
|
||||
Ok(vec![
|
||||
resource_dir.join("backend").join("backend.exe"),
|
||||
resource_dir.join("backend.exe"),
|
||||
exe_dir.join("resources").join("backend").join("backend.exe"),
|
||||
exe_dir.join("resources").join("backend.exe"),
|
||||
exe_dir.join("backend").join("backend.exe"),
|
||||
exe_dir.join("backend.exe"),
|
||||
])
|
||||
}
|
||||
|
||||
fn release_working_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let resource_dir = app
|
||||
.path()
|
||||
.resource_dir()
|
||||
.map_err(|err| format!("获取资源目录失败: {}", err))?;
|
||||
Ok(resource_dir)
|
||||
}
|
||||
|
||||
fn resolve_backend_launch_plan(app: &AppHandle) -> Result<BackendLaunchPlan, String> {
|
||||
let mode = backend_mode();
|
||||
let envs = runtime_envs(app, &mode)?;
|
||||
|
||||
if mode == "release" {
|
||||
let candidates = release_backend_candidates(app)?;
|
||||
let program = candidates
|
||||
.into_iter()
|
||||
.find(|candidate| candidate.exists())
|
||||
.ok_or_else(|| "未找到发布态后端程序 backend.exe".to_string())?;
|
||||
let working_dir = release_working_dir(app)?;
|
||||
return Ok(BackendLaunchPlan {
|
||||
program,
|
||||
args: Vec::new(),
|
||||
working_dir,
|
||||
envs,
|
||||
});
|
||||
}
|
||||
|
||||
let python = backend_python();
|
||||
if !python.exists() {
|
||||
return Err(format!("未找到后端解释器: {}", python.display()));
|
||||
}
|
||||
let entry = backend_entry();
|
||||
if !entry.exists() {
|
||||
return Err(format!("未找到后端入口: {}", entry.display()));
|
||||
}
|
||||
Ok(BackendLaunchPlan {
|
||||
program: python,
|
||||
args: vec![entry],
|
||||
working_dir: project_root(),
|
||||
envs,
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_backend(app: &AppHandle) -> Result<(), String> {
|
||||
if is_backend_alive() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let plan = resolve_backend_launch_plan(app)?;
|
||||
let mut command = Command::new(&plan.program);
|
||||
command.current_dir(&plan.working_dir);
|
||||
for arg in &plan.args {
|
||||
command.arg(arg);
|
||||
}
|
||||
for (key, value) in &plan.envs {
|
||||
command.env(key, value);
|
||||
}
|
||||
command
|
||||
.env("OPENCLAW_PROJECT_VENV_REEXEC", "1")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|err| format!("启动后台失败: {}", err))?;
|
||||
if let Err(err) = wait_for_backend(&mut child, Duration::from_secs(20)) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(err);
|
||||
}
|
||||
let state = app.state::<AppState>();
|
||||
let mut guard = state
|
||||
.backend_child
|
||||
.lock()
|
||||
.map_err(|_| "后台状态锁定失败".to_string())?;
|
||||
*guard = Some(child);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ps_single_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "''"))
|
||||
}
|
||||
|
||||
fn cleanup_orphan_backend_processes(app: &AppHandle, mode: &str) {
|
||||
let script = if mode == "release" {
|
||||
let targets = release_backend_candidates(app)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|path| ps_single_quote(&path.display().to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
if targets.is_empty() {
|
||||
return;
|
||||
}
|
||||
format!("$targets=@({}); Get-CimInstance Win32_Process | Where-Object {{ $_.ExecutablePath -and ($targets -contains $_.ExecutablePath) }} | ForEach-Object {{ Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }}", targets)
|
||||
} else {
|
||||
"Get-CimInstance Win32_Process | Where-Object { $_.Name -ieq 'python.exe' -and $_.CommandLine -match 'backend_main.py' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }".to_string()
|
||||
};
|
||||
let _ = Command::new("powershell")
|
||||
.arg("-NoProfile")
|
||||
.arg("-ExecutionPolicy")
|
||||
.arg("Bypass")
|
||||
.arg("-Command")
|
||||
.arg(script)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
|
||||
fn terminate_backend(app: &AppHandle) {
|
||||
if let Some(state) = app.try_state::<AppState>() {
|
||||
if let Ok(mut guard) = state.backend_child.lock() {
|
||||
if let Some(mut child) = guard.take() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup_orphan_backend_processes(app, &backend_mode());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_devtools(app: AppHandle) -> Result<(), String> {
|
||||
let webview = app
|
||||
.get_webview_window("main")
|
||||
.ok_or_else(|| "未找到主窗口".to_string())?;
|
||||
webview.open_devtools();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_backend_bootstrap_error(app: AppHandle) -> Result<Option<String>, String> {
|
||||
let state = app.state::<AppState>();
|
||||
let guard = state
|
||||
.backend_bootstrap_error
|
||||
.lock()
|
||||
.map_err(|_| "后台启动错误状态锁定失败".to_string())?;
|
||||
Ok(guard.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn restart_backend(app: AppHandle) -> Result<(), String> {
|
||||
terminate_backend(&app);
|
||||
spawn_backend(&app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.manage(AppState {
|
||||
backend_child: Mutex::new(None),
|
||||
backend_bootstrap_error: Mutex::new(None),
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![open_devtools, get_backend_bootstrap_error, restart_backend])
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
if let Err(err) = spawn_backend(app.handle()) {
|
||||
if let Ok(mut guard) = app.state::<AppState>().backend_bootstrap_error.lock() {
|
||||
*guard = Some(err.clone());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app, event| {
|
||||
if matches!(event, RunEvent::Exit | RunEvent::ExitRequested { .. }) {
|
||||
terminate_backend(app);
|
||||
}
|
||||
});
|
||||
}
|
||||
6
frontend/src-tauri/src/main.rs
Normal file
6
frontend/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
||||
Reference in New Issue
Block a user