From 7ddbc88315c6f504091feb21ebf6c34af56bea32 Mon Sep 17 00:00:00 2001 From: u-00a0 Date: Tue, 10 Mar 2026 01:50:29 +0800 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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) From 545cfdd831456a3938f6f27920f3ca7e9f57297c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=B9=E7=A7=91=E6=B7=B1=20Xiang=20Keshen?= <30371455+sssxks@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:21:17 +0800 Subject: [PATCH 6/6] Add Apache License 2.0 Added Apache License 2.0 to the project. --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.