From 7ddbc88315c6f504091feb21ebf6c34af56bea32 Mon Sep 17 00:00:00 2001 From: u-00a0 Date: Tue, 10 Mar 2026 01:50:29 +0800 Subject: [PATCH 1/5] feat: add Tauri v2 architecture and backend abstraction - Initialize Tauri v2 project structure under web/src-tauri. - Add Rust backend commands for bootstrap and solve IPC. - Introduce Backend interface to abstract solver calls. - Implement tauri-backend for desktop and wasm-backend for browser. - Update Home.svelte to dynamically resolve the appropriate backend runtime. - Conditionally skip the WASM builder in Vite when running in Tauri. - Add web/src-tauri to the root Cargo workspace. - Update package.json with Tauri dependencies and dev/build scripts. - Update AGENTS.md with instructions for Tauri development. --- AGENTS.md | 3 + Cargo.toml | 1 + web/package-lock.json | 240 ++++++++++++++++++++++-- web/package.json | 4 + web/src-tauri/.cargo/config.toml | 2 + web/src-tauri/Cargo.toml | 19 ++ web/src-tauri/build.rs | 3 + web/src-tauri/capabilities/default.json | 7 + web/src-tauri/icons/.gitkeep | 2 + web/src-tauri/src/main.rs | 32 ++++ web/src-tauri/tauri.conf.json | 28 +++ web/src/lib/backend.ts | 31 +++ web/src/lib/tauri-backend.ts | 37 ++++ web/src/lib/wasm-backend.ts | 23 +++ web/src/routes/Home.svelte | 18 +- web/vite.config.ts | 14 +- 16 files changed, 447 insertions(+), 17 deletions(-) create mode 100644 web/src-tauri/.cargo/config.toml create mode 100644 web/src-tauri/Cargo.toml create mode 100644 web/src-tauri/build.rs create mode 100644 web/src-tauri/capabilities/default.json create mode 100644 web/src-tauri/icons/.gitkeep create mode 100644 web/src-tauri/src/main.rs create mode 100644 web/src-tauri/tauri.conf.json create mode 100644 web/src/lib/backend.ts create mode 100644 web/src/lib/tauri-backend.ts create mode 100644 web/src/lib/wasm-backend.ts diff --git a/AGENTS.md b/AGENTS.md index f54bb1d..8b721ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,7 @@ 修改rust代码后跑,没改rust代码不用跑:`cargo make done`, `scripts/build_web_wasm.sh`。 修改前端代码后在`web`目录下跑:`npm run check` 检查类型和 `npm run test:e2e` 运行e2e测试。 +Tauri 桌面端开发:在`web`目录下跑 `npm run dev:tauri` 启动 Tauri 开发模式,`npm run build:tauri` 构建桌面端安装包。 +Tauri 后端代码在 `web/src-tauri/src/main.rs`,依赖 `end-web` crate 的纯 Rust API(不经过 WASM)。 + 开发这个项目需使用 rust-dev skill。 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f16e942..60c0831 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/end_report", "crates/end_cli", "crates/end_web", + "web/src-tauri", ] default-members = ["crates/end_cli"] resolver = "2" diff --git a/web/package-lock.json b/web/package-lock.json index c4aec83..077206e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,6 +9,7 @@ "version": "0.2.0", "dependencies": { "@dagrejs/dagre": "^2.0.4", + "@tauri-apps/api": "^2.0.0", "@xyflow/svelte": "^1.5.0", "dom-to-svg": "^0.12.0", "material-symbols": "^0.40.2", @@ -19,6 +20,7 @@ "@eslint/js": "^9.34.0", "@playwright/test": "^1.51.1", "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tauri-apps/cli": "^2.0.0", "@tsconfig/svelte": "^5.0.4", "@types/node": "^24.12.0", "@typescript-eslint/eslint-plugin": "^8.41.0", @@ -1207,7 +1209,6 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -1242,6 +1243,233 @@ "vite": "^6.0.0" } }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/svelte": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", @@ -1379,7 +1607,6 @@ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1432,7 +1659,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -1784,7 +2010,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2143,7 +2368,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -2363,7 +2587,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4020,7 +4243,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4079,7 +4301,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4539,7 +4760,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz", "integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -4728,7 +4948,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4935,7 +5154,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/web/package.json b/web/package.json index 684cfa7..20d8f08 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,9 @@ "gen:model-v1": "node ./scripts/gen-model-v1.mjs", "precheck": "npm run gen:model-v1", "dev": "vite", + "dev:tauri": "tauri dev", "build": "vite build", + "build:tauri": "tauri build", "preview": "vite preview", "preview:e2e": "npm run build:wasm && npm run build -- --logLevel warn && npm run preview -- --host 127.0.0.1 --strictPort", "check": "svelte-check --tsconfig ./tsconfig.json", @@ -20,6 +22,7 @@ }, "dependencies": { "@dagrejs/dagre": "^2.0.4", + "@tauri-apps/api": "^2.0.0", "@xyflow/svelte": "^1.5.0", "dom-to-svg": "^0.12.0", "material-symbols": "^0.40.2", @@ -30,6 +33,7 @@ "@eslint/js": "^9.34.0", "@playwright/test": "^1.51.1", "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tauri-apps/cli": "^2.0.0", "@tsconfig/svelte": "^5.0.4", "@types/node": "^24.12.0", "@typescript-eslint/eslint-plugin": "^8.41.0", diff --git a/web/src-tauri/.cargo/config.toml b/web/src-tauri/.cargo/config.toml new file mode 100644 index 0000000..51573b8 --- /dev/null +++ b/web/src-tauri/.cargo/config.toml @@ -0,0 +1,2 @@ +[default] +runner = "cargo" diff --git a/web/src-tauri/Cargo.toml b/web/src-tauri/Cargo.toml new file mode 100644 index 0000000..30ddbf3 --- /dev/null +++ b/web/src-tauri/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "end-tauri" +version = "0.2.0" +edition = "2024" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +end-web = { path = "../../crates/end_web" } +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { version = "2", features = [] } + +[lints.clippy] +indexing_slicing = "warn" +unwrap_used = "warn" +expect_used = "warn" +panic = "warn" diff --git a/web/src-tauri/build.rs b/web/src-tauri/build.rs new file mode 100644 index 0000000..261851f --- /dev/null +++ b/web/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/web/src-tauri/capabilities/default.json b/web/src-tauri/capabilities/default.json new file mode 100644 index 0000000..c2ca36e --- /dev/null +++ b/web/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/web/src-tauri/icons/.gitkeep b/web/src-tauri/icons/.gitkeep new file mode 100644 index 0000000..ed8192a --- /dev/null +++ b/web/src-tauri/icons/.gitkeep @@ -0,0 +1,2 @@ +// Workaround: include icons dir for Tauri build. +// Tauri expects this directory to exist even with default config. diff --git a/web/src-tauri/src/main.rs b/web/src-tauri/src/main.rs new file mode 100644 index 0000000..44bf20b --- /dev/null +++ b/web/src-tauri/src/main.rs @@ -0,0 +1,32 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use end_web::{Lang, bootstrap, solve_from_aic_toml}; + +fn parse_lang(tag: &str) -> Result { + match tag.trim().to_ascii_lowercase().as_str() { + "zh" => Ok(Lang::Zh), + "en" => Ok(Lang::En), + other => Err(format!("Unknown lang `{other}` (expected `zh` or `en`)")), + } +} + +#[tauri::command] +fn cmd_bootstrap(lang: String) -> Result { + let lang = parse_lang(&lang)?; + let payload = bootstrap(lang).map_err(|e| e.to_string())?; + serde_json::to_value(&payload).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn cmd_solve(lang: String, aic_toml: String) -> Result { + let lang = parse_lang(&lang)?; + let payload = solve_from_aic_toml(lang, &aic_toml).map_err(|e| e.to_string())?; + serde_json::to_value(&payload).map_err(|e| e.to_string()) +} + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![cmd_bootstrap, cmd_solve]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/web/src-tauri/tauri.conf.json b/web/src-tauri/tauri.conf.json new file mode 100644 index 0000000..97dfdff --- /dev/null +++ b/web/src-tauri/tauri.conf.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://raw.githubusercontent.com/nickelpack/nsis-tauri-utils/heads/main/schema/config.schema.json", + "productName": "源石计划", + "version": "0.2.0", + "identifier": "com.endfield.planner", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:5173", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "title": "源石计划 - 终末地产线规划", + "windows": [ + { + "label": "main", + "title": "源石计划 - 终末地产线规划", + "width": 1400, + "height": 900, + "minWidth": 800, + "minHeight": 600 + } + ], + "security": { + "csp": null + } + } +} diff --git a/web/src/lib/backend.ts b/web/src/lib/backend.ts new file mode 100644 index 0000000..d7a7f19 --- /dev/null +++ b/web/src/lib/backend.ts @@ -0,0 +1,31 @@ +import type { BootstrapPayload, LangTag, SolvePayload } from './types'; + +/** + * Backend abstraction — unified interface for both WASM (web) and Tauri (desktop) modes. + */ +export interface Backend { + loadBootstrap(lang: LangTag): Promise; + solveScenario(lang: LangTag, aicToml: string): Promise; + warmup(): Promise; +} + +/** + * Detect whether the app is running inside a Tauri webview. + */ +function isTauri(): boolean { + return '__TAURI_INTERNALS__' in window; +} + +/** + * Create the appropriate backend for the current runtime environment. + * - In Tauri desktop mode: uses IPC `invoke()` to call native Rust commands. + * - In browser mode: uses Emscripten WASM running in a Web Worker. + */ +export async function createBackend(): Promise { + if (isTauri()) { + const { createTauriBackend } = await import('./tauri-backend'); + return createTauriBackend(); + } + const { createWasmBackend } = await import('./wasm-backend'); + return createWasmBackend(); +} diff --git a/web/src/lib/tauri-backend.ts b/web/src/lib/tauri-backend.ts new file mode 100644 index 0000000..7787e4c --- /dev/null +++ b/web/src/lib/tauri-backend.ts @@ -0,0 +1,37 @@ +import type { Backend } from './backend'; +import type { BootstrapPayload, LangTag, SolvePayload } from './types'; + +/** + * Tauri backend — calls native Rust commands via Tauri IPC. + * The Rust side runs `end_web::bootstrap` and `end_web::solve_from_aic_toml` directly + * (native-compiled, no WASM), which is faster and avoids the Emscripten toolchain. + */ +export function createTauriBackend(): Backend { + // Lazy-import so this module is only pulled in when running inside Tauri. + let invokePromise: Promise | null = null; + + async function getInvoke() { + if (!invokePromise) { + invokePromise = import('@tauri-apps/api/core').then((m) => m.invoke); + } + return invokePromise; + } + + return { + async loadBootstrap(lang: LangTag): Promise { + const invoke = await getInvoke(); + return invoke('cmd_bootstrap', { lang }); + }, + + async solveScenario(lang: LangTag, aicToml: string): Promise { + const invoke = await getInvoke(); + return invoke('cmd_solve', { lang, aicToml }); + }, + + async warmup(): Promise { + // In Tauri mode there is no WASM module to warm up. + // Optionally we could fire a no-op bootstrap call here to warm the Rust side, + // but it's fast enough natively that it's not needed. + }, + }; +} diff --git a/web/src/lib/wasm-backend.ts b/web/src/lib/wasm-backend.ts new file mode 100644 index 0000000..39cdee5 --- /dev/null +++ b/web/src/lib/wasm-backend.ts @@ -0,0 +1,23 @@ +import type { Backend } from './backend'; +import type { BootstrapPayload, LangTag, SolvePayload } from './types'; +import { warmupWasmWorker, loadBootstrap, solveScenario } from './wasm'; + +/** + * WASM backend — delegates to the existing Web Worker + Emscripten WASM pipeline. + * This is the default backend for browser deployments (no Tauri). + */ +export function createWasmBackend(): Backend { + return { + loadBootstrap(lang: LangTag): Promise { + return loadBootstrap(lang); + }, + + solveScenario(lang: LangTag, aicToml: string): Promise { + return solveScenario(lang, aicToml); + }, + + warmup(): Promise { + return warmupWasmWorker(); + }, + }; +} diff --git a/web/src/routes/Home.svelte b/web/src/routes/Home.svelte index 933c5f2..e711170 100644 --- a/web/src/routes/Home.svelte +++ b/web/src/routes/Home.svelte @@ -53,9 +53,15 @@ import type { AicDraft, CatalogItemDto, LangTag } from "../lib/types"; import { EMPTY_DRAFT } from "../lib/types"; import { createHydrationPersistGate as createHydrationGate } from "../lib/hydration-persist-gate.svelte"; - import { loadBootstrap, solveScenario, warmupWasmWorker } from "../lib/wasm"; + import { createBackend, type Backend } from "../lib/backend"; import bundledDefaultAicToml from "../../../crates/end_io/src/aic.toml?raw"; + let backend: Backend | null = null; + const backendReady: Promise = createBackend().then((b) => { + backend = b; + return b; + }); + const NARROW_LAYOUT_QUERY = "(max-width: 760px)"; const MIN_EDITOR_WIDTH_PX = 300; const MIN_RIGHT_WIDTH_PX = 420; @@ -91,7 +97,10 @@ const solverController: SolverController = createSolverController({ debounceMs: AUTO_SOLVE_DEBOUNCE_MS, toToml: buildAicToml, - solve: (solveLang, toml) => solveScenario(solveLang, toml), + solve: async (solveLang, toml) => { + const b = backend ?? await backendReady; + return b.solveScenario(solveLang, toml); + }, onStateChange: (next) => { solveState = next; }, @@ -202,7 +211,8 @@ isBootstrapping = true; try { - const payload = await loadBootstrap(lang); + const b = backend ?? await backendReady; + const payload = await b.loadBootstrap(lang); catalogItems = payload.catalog.items; } catch (error) { showErrorToast(error instanceof Error ? error.message : String(error)); @@ -352,7 +362,7 @@ }; void (async () => { - void warmupWasmWorker().catch(() => undefined); + void backendReady.then((b) => b.warmup()).catch(() => undefined); const restored = restoreLocalState(STORAGE_CONFIG); const shareParam = new URLSearchParams(window.location.search).get("s"); diff --git a/web/vite.config.ts b/web/vite.config.ts index 5c8522e..03582b8 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -282,9 +282,17 @@ function modelV1BuildPlugin(): PluginOption { export default defineConfig(({ mode }) => { const env = loadEnv(mode, '.', ''); const basePath = env.VITE_BASE_PATH || process.env.VITE_BASE_PATH; + const isTauri = !!process.env.TAURI_ENV_PLATFORM; + + const plugins: PluginOption[] = [svelte(), modelV1DevPlugin(), modelV1BuildPlugin()]; + + // Only run the WASM rebuild plugin in plain web mode (not inside Tauri dev). + if (!isTauri) { + plugins.push(rustWasmDevPlugin()); + } return { - plugins: [svelte(), modelV1DevPlugin(), modelV1BuildPlugin(), rustWasmDevPlugin()], + plugins, base: normalizeBasePath(basePath), define: { global: 'globalThis' @@ -299,6 +307,8 @@ export default defineConfig(({ mode }) => { server: { host: '0.0.0.0', port: 5173, - } + }, + // Clear this env variable so `@tauri-apps/api` can detect Tauri environment. + envPrefix: ['VITE_', 'TAURI_ENV_'], }; }); From 16eb01347370c5eb21b3436fc6c30b6c3980c5be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:03:05 +0000 Subject: [PATCH 2/5] chore: remove fork-only files not in upstream --- Cargo.toml | 1 - web/src-tauri/.cargo/config.toml | 2 -- web/src-tauri/icons/.gitkeep | 2 -- 3 files changed, 5 deletions(-) delete mode 100644 web/src-tauri/.cargo/config.toml delete mode 100644 web/src-tauri/icons/.gitkeep diff --git a/Cargo.toml b/Cargo.toml index 60c0831..f16e942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "crates/end_report", "crates/end_cli", "crates/end_web", - "web/src-tauri", ] default-members = ["crates/end_cli"] resolver = "2" diff --git a/web/src-tauri/.cargo/config.toml b/web/src-tauri/.cargo/config.toml deleted file mode 100644 index 51573b8..0000000 --- a/web/src-tauri/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[default] -runner = "cargo" diff --git a/web/src-tauri/icons/.gitkeep b/web/src-tauri/icons/.gitkeep deleted file mode 100644 index ed8192a..0000000 --- a/web/src-tauri/icons/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ -// Workaround: include icons dir for Tauri build. -// Tauri expects this directory to exist even with default config. From f8b80b5d1cc168fb4e810e30ac1e8afb30e2004e Mon Sep 17 00:00:00 2001 From: null Date: Wed, 25 Mar 2026 21:49:52 +0800 Subject: [PATCH 3/5] docs: documents optimization --- LICENSE | 1 + README.md | 43 ++++++++++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ed0712 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +点击输入文本 \ No newline at end of file diff --git a/README.md b/README.md index 822757d..e3aea2b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,19 @@ -# 源石图标 源石计划 - 终末地产线规划 + -[![CI](https://github.com/sssxks/end-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/sssxks/end-cli/actions/workflows/ci.yml) +
+ +源石图标
+# 源石计划 + + + +[![CI](https://github.com/sssxks/end-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/sssxks/end-cli/actions/workflows/ci.yml)
+终末地产线规划 使用 Rust/WebAssembly 实现的终末地生产线规划工具,支持 CLI 和 Web 版本。基于 HiGHS 求解器实现 MILP 模型求解。 🔗 网页链接: [end-8jk.pages.dev](https://end-8jk.pages.dev/), [sssxks.github.io/end-cli/](https://sssxks.github.io/end-cli/) +
## 截图展示 @@ -39,31 +48,31 @@ cargo install --git https://github.com/sssxks/end-cli end-cli 1. 生成配置模板,这是程序的输入数据文件: -```bash -end-cli init -``` + ```bash + end-cli init + ``` 2. 编辑当前目录下的 `aic.toml`(外部供给、外部消耗、据点价格、据点上限、外部耗电)。 3. 运行求解: -```bash -end-cli solve -``` + ```bash + end-cli solve + ``` -默认输出中文报告。英文报告可用: + 默认输出中文报告。英文报告可用: -```bash -end-cli solve --lang en -``` + ```bash + end-cli solve --lang en + ``` -如果你看到下面这条报错: + 如果你看到下面这条报错: -```text -Error: aic.toml not found; run `end-cli init --aic aic.toml` to create it -``` + ```text + Error: aic.toml not found; run `end-cli init --aic aic.toml` to create it + ``` -它表示当前目录没有对应配置文件,`solve` 会直接拒绝执行。先运行 `end-cli init` 生成模板并按需修改后再求解。 + 它表示当前目录没有对应配置文件,`solve` 会直接拒绝执行。先运行 `end-cli init` 生成模板并按需修改后再求解。 ## 常用命令 From 0573924bb48d28f557ce7be379b713bdbd1c3284 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 25 Mar 2026 21:55:27 +0800 Subject: [PATCH 4/5] chore: updated `flatted` version and resolved npm audit warning --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index b6c4e3e..9069831 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2968,9 +2968,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, From 34e4e57d8427bb93fd13ccd4d8d5cae0c8488b5c Mon Sep 17 00:00:00 2001 From: null Date: Wed, 25 Mar 2026 22:17:27 +0800 Subject: [PATCH 5/5] docs: documents optimization --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3aea2b..f2696bf 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,16 @@
源石图标
-# 源石计划 -[![CI](https://github.com/sssxks/end-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/sssxks/end-cli/actions/workflows/ci.yml)
+# 源石计划 + +[![CI](https://github.com/sssxks/end-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/sssxks/end-cli/actions/workflows/ci.yml) + 终末地产线规划 -使用 Rust/WebAssembly 实现的终末地生产线规划工具,支持 CLI 和 Web 版本。基于 HiGHS 求解器实现 MILP 模型求解。 +使用 Rust / WebAssembly 实现的终末地生产线规划工具,支持 CLI 和 Web 版本。基于 HiGHS 求解器实现 MILP 模型求解。 🔗 网页链接: [end-8jk.pages.dev](https://end-8jk.pages.dev/), [sssxks.github.io/end-cli/](https://sssxks.github.io/end-cli/)
@@ -109,3 +111,6 @@ end-cli solve --help - 配方吞吐受机器数量约束 - 总发电功率 >= 总用电功率 +## 贡献者 + +[![Contributors](https://contrib.rocks/image?repo=sssxks/end-cli)](https://github.com/sssxks/end-cli/graphs/contributors)