lib.rs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. mod commands;
  2. mod panic_guard;
  3. mod proxy;
  4. mod types;
  5. /// Apply a proxy configuration to the process env immediately, so the
  6. /// next outbound HTTP request picks it up without needing the user to
  7. /// restart the app. tauri-plugin-http builds a fresh
  8. /// `reqwest::ClientBuilder` per fetch and reqwest's `auto_sys_proxy`
  9. /// re-reads HTTP_PROXY / HTTPS_PROXY / NO_PROXY each time, so updating
  10. /// these env vars is sufficient to flip the proxy on/off live.
  11. ///
  12. /// Returns the same human-readable summary `apply_proxy_env` produces
  13. /// for logging.
  14. #[tauri::command]
  15. fn set_proxy_env(config: proxy::ProxyConfig) -> String {
  16. let summary = proxy::apply_proxy_env(&config);
  17. eprintln!("[proxy] live update: {summary}");
  18. summary
  19. }
  20. #[cfg_attr(mobile, tauri::mobile_entry_point)]
  21. pub fn run() {
  22. tauri::Builder::default()
  23. .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
  24. use tauri::Manager;
  25. if let Some(window) = app.get_webview_window("main") {
  26. let _ = window.unminimize();
  27. let _ = window.show();
  28. let _ = window.set_focus();
  29. }
  30. }))
  31. .plugin(tauri_plugin_opener::init())
  32. .plugin(tauri_plugin_dialog::init())
  33. .plugin(tauri_plugin_store::Builder::default().build())
  34. // Rust-backed fetch so third-party LLM APIs that reject
  35. // browser-origin headers via CORS preflight (MiniMax, Volcengine
  36. // Ark's api/coding/v3, etc.) still work. Requests leave the app
  37. // from Rust, never the webview.
  38. .plugin(tauri_plugin_http::init())
  39. .plugin(tauri_plugin_updater::Builder::new().build())
  40. .setup(|app| {
  41. // Let the PDF extractor find the bundled pdfium dynamic
  42. // library via Tauri's platform-correct resource path.
  43. use tauri::Manager;
  44. if let Ok(dir) = app.path().resource_dir() {
  45. commands::fs::set_resource_dir_hint(dir);
  46. }
  47. // Apply user-configured global HTTP proxy by setting
  48. // HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars BEFORE
  49. // any HTTP request is made. tauri-plugin-http's reqwest
  50. // client reads these on first construction. Lives next
  51. // to the resource-dir hint so the proxy applies to
  52. // everything: LLM, embedding, update check, deep
  53. // research, captioning. See src-tauri/src/proxy.rs.
  54. if let Ok(dir) = app.path().app_data_dir() {
  55. let store_path = dir.join("app-state.json");
  56. eprintln!("[proxy] reading from {}", store_path.display());
  57. let summary = proxy::apply_proxy_env_from_store(&store_path);
  58. eprintln!("[proxy] {summary}");
  59. } else {
  60. eprintln!("[proxy] could not resolve app_data_dir");
  61. }
  62. // Registry of running `claude` subprocesses, keyed by the
  63. // frontend-generated stream id. Populated by claude_cli_spawn,
  64. // drained on process exit or by claude_cli_kill.
  65. app.manage(commands::claude_cli::ClaudeCliState::default());
  66. app.manage(commands::codex_cli::CodexCliState::default());
  67. app.manage(commands::file_sync::FileSyncState::default());
  68. Ok(())
  69. })
  70. .invoke_handler(tauri::generate_handler![
  71. commands::fs::read_file,
  72. commands::fs::write_file,
  73. commands::fs::write_file_atomic,
  74. commands::fs::list_directory,
  75. commands::fs::copy_file,
  76. commands::fs::copy_directory,
  77. commands::fs::preprocess_file,
  78. commands::fs::delete_file,
  79. commands::fs::find_related_wiki_pages,
  80. commands::fs::create_directory,
  81. commands::fs::file_exists,
  82. commands::fs::get_file_modified_time,
  83. commands::fs::get_file_size,
  84. commands::fs::get_file_md5,
  85. commands::fs::read_file_as_base64,
  86. commands::fs::get_executable_dir,
  87. commands::fs::get_resource_dir,
  88. commands::project::create_project,
  89. commands::project::open_project,
  90. commands::project::open_project_folder,
  91. commands::project::open_file_location,
  92. commands::vectorstore::vector_upsert,
  93. commands::vectorstore::vector_search,
  94. commands::vectorstore::vector_delete,
  95. commands::vectorstore::vector_count,
  96. commands::vectorstore::vector_upsert_chunks,
  97. commands::vectorstore::vector_search_chunks,
  98. commands::vectorstore::vector_delete_page,
  99. commands::vectorstore::vector_count_chunks,
  100. commands::vectorstore::vector_legacy_row_count,
  101. commands::vectorstore::vector_drop_legacy,
  102. commands::claude_cli::claude_cli_detect,
  103. commands::claude_cli::claude_cli_spawn,
  104. commands::claude_cli::claude_cli_kill,
  105. commands::codex_cli::codex_cli_detect,
  106. commands::codex_cli::codex_cli_spawn,
  107. commands::codex_cli::codex_cli_kill,
  108. commands::extract_images::extract_pdf_images_cmd,
  109. commands::extract_images::extract_office_images_cmd,
  110. commands::extract_images::extract_and_save_pdf_images_cmd,
  111. commands::extract_images::extract_and_save_office_images_cmd,
  112. commands::file_sync::start_project_file_watcher,
  113. commands::file_sync::stop_project_file_watcher,
  114. commands::file_sync::rescan_project_files,
  115. commands::file_sync::get_file_change_queue,
  116. commands::file_sync::retry_file_change_task,
  117. commands::file_sync::ignore_file_change_task,
  118. commands::backup::export_backup,
  119. commands::backup::import_backup,
  120. set_proxy_env,
  121. ])
  122. .on_window_event(|window, event| {
  123. if let tauri::WindowEvent::CloseRequested { api, .. } = event {
  124. #[cfg(target_os = "macos")]
  125. {
  126. let _ = window.hide();
  127. api.prevent_close();
  128. }
  129. #[cfg(not(target_os = "macos"))]
  130. {
  131. use tauri::Manager;
  132. api.prevent_close();
  133. let win = window.clone();
  134. let app = window.app_handle().clone();
  135. tauri::async_runtime::spawn(async move {
  136. use tauri_plugin_dialog::DialogExt;
  137. let confirmed = app
  138. .dialog()
  139. .message("确定要退出小说写作助手吗?")
  140. .title("确认退出")
  141. .kind(tauri_plugin_dialog::MessageDialogKind::Warning)
  142. .blocking_show();
  143. if confirmed {
  144. let _ = win.destroy();
  145. }
  146. });
  147. }
  148. }
  149. })
  150. .build(tauri::generate_context!())
  151. .unwrap_or_else(|e| {
  152. // 启动失败时显示友好的错误信息,而非裸 panic
  153. let msg = format!("应用程序启动失败: {e}");
  154. eprintln!("{msg}");
  155. // 尝试弹出 Windows 消息框(不依赖 Tauri 运行时)
  156. #[cfg(windows)]
  157. {
  158. use std::ffi::OsStr;
  159. use std::os::windows::ffi::OsStrExt;
  160. extern "system" {
  161. fn MessageBoxW(
  162. hwnd: *mut std::ffi::c_void,
  163. lp_text: *const u16,
  164. lp_caption: *const u16,
  165. u_type: u32,
  166. ) -> i32;
  167. }
  168. fn to_wide(s: &str) -> Vec<u16> {
  169. OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
  170. }
  171. let text = to_wide(&msg);
  172. let caption = to_wide("启动错误");
  173. unsafe {
  174. MessageBoxW(
  175. std::ptr::null_mut(),
  176. text.as_ptr(),
  177. caption.as_ptr(),
  178. 0x10, // MB_ICONERROR
  179. );
  180. }
  181. }
  182. std::process::exit(1);
  183. })
  184. .run(|app, event| {
  185. #[cfg(target_os = "macos")]
  186. if let tauri::RunEvent::Reopen { has_visible_windows, .. } = event {
  187. if !has_visible_windows {
  188. use tauri::Manager;
  189. if let Some(window) = app.get_webview_window("main") {
  190. let _ = window.show();
  191. let _ = window.set_focus();
  192. }
  193. }
  194. }
  195. let _ = (app, event); // suppress unused warnings on non-macOS
  196. });
  197. }