diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 45adb581..9d548665 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -9,6 +9,7 @@ const capabilityPath = new URL( const capability = JSON.parse(fs.readFileSync(capabilityPath, 'utf8')); const buildScriptPath = new URL('../src-tauri/build.rs', import.meta.url); const buildScript = fs.readFileSync(buildScriptPath, 'utf8'); +const iconPath = new URL('../src-tauri/icons/icon.png', import.meta.url); if (config.build?.frontendDist !== '../../../dist') { throw new Error('desktop shell must package the root H5 dist'); @@ -23,13 +24,17 @@ if (!mainWindow || mainWindow.create !== false) { throw new Error('desktop shell must create the main window from Rust setup'); } +if (!config.bundle?.icon?.includes('icons/icon.png')) { + throw new Error('desktop shell must use the real brand icon asset'); +} + const requiredUrlParts = [ 'clientRuntime=native_app', 'clientType=native_app', 'hostShell=tauri_desktop', 'hostPlatform=unknown', 'bridgeVersion=1', - 'hostCapabilities=host.getRuntime,app.openExternalUrl', + 'hostCapabilities=host.getRuntime,app.openExternalUrl,clipboard.writeText', ]; for (const part of requiredUrlParts) { @@ -46,6 +51,10 @@ const requiredPermissions = [ 'allow-host-bridge-request', ]; const requiredBuildCommands = ['host_bridge_request']; +const requiredMainSnippets = [ + 'tauri_plugin_clipboard_manager::init()', + '"clipboard.writeText"', +]; for (const permission of requiredPermissions) { if (!capability.permissions?.includes(permission)) { @@ -62,3 +71,17 @@ for (const command of requiredBuildCommands) { if (buildScript.includes('resolve_desktop_shell_runtime')) { throw new Error('desktop shell build manifest exposes an unused runtime command'); } + +const icon = fs.readFileSync(iconPath); +if (icon.length < 10000) { + throw new Error('desktop shell icon must use a real brand asset'); +} + +const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url); +const main = fs.readFileSync(mainPath, 'utf8'); + +for (const snippet of requiredMainSnippets) { + if (!main.includes(snippet)) { + throw new Error(`desktop shell Rust host bridge missing ${snippet}`); + } +} diff --git a/apps/desktop-shell/src-tauri/Cargo.lock b/apps/desktop-shell/src-tauri/Cargo.lock index 42996dce..e694380c 100644 --- a/apps/desktop-shell/src-tauri/Cargo.lock +++ b/apps/desktop-shell/src-tauri/Cargo.lock @@ -47,6 +47,27 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.59.0", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -334,6 +355,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -465,6 +492,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "combine" version = "4.6.7" @@ -567,6 +603,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -783,6 +825,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -908,6 +956,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "5.4.1" @@ -935,6 +989,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -960,6 +1020,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -1215,6 +1281,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-clipboard-manager", "tauri-plugin-opener", ] @@ -1228,6 +1295,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1412,6 +1489,17 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1698,6 +1786,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2016,6 +2118,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.19.3" @@ -2067,6 +2179,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2124,6 +2245,7 @@ dependencies = [ "block2", "objc2", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation", ] @@ -2333,6 +2455,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "pango" version = "0.18.3" @@ -2399,6 +2531,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", +] + [[package]] name = "phf" version = "0.13.1" @@ -2621,6 +2764,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.39.4" @@ -3443,6 +3598,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.4" @@ -3628,6 +3798,20 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.49" @@ -3928,6 +4112,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -4242,6 +4437,76 @@ dependencies = [ "semver", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.13.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.102" @@ -4344,6 +4609,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -4823,6 +5094,24 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.3" @@ -4894,6 +5183,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "yoke" version = "0.8.3" @@ -4978,6 +5284,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -5038,6 +5364,21 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.12.0" diff --git a/apps/desktop-shell/src-tauri/Cargo.toml b/apps/desktop-shell/src-tauri/Cargo.toml index eb9e145d..79c83770 100644 --- a/apps/desktop-shell/src-tauri/Cargo.toml +++ b/apps/desktop-shell/src-tauri/Cargo.toml @@ -11,4 +11,5 @@ tauri-build = { version = "2.6.2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" tauri = { version = "2.11.2", features = [] } +tauri-plugin-clipboard-manager = "2.3.2" tauri-plugin-opener = "2.5.4" diff --git a/apps/desktop-shell/src-tauri/icons/icon.png b/apps/desktop-shell/src-tauri/icons/icon.png new file mode 100644 index 00000000..4aa1f7d8 Binary files /dev/null and b/apps/desktop-shell/src-tauri/icons/icon.png differ diff --git a/apps/desktop-shell/src-tauri/src/main.rs b/apps/desktop-shell/src-tauri/src/main.rs index b6688698..ef243499 100644 --- a/apps/desktop-shell/src-tauri/src/main.rs +++ b/apps/desktop-shell/src-tauri/src/main.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use tauri_plugin_clipboard_manager::ClipboardExt; use tauri_plugin_opener::OpenerExt; const HOST_BRIDGE_PROTOCOL: &str = "GenarrativeHostBridge"; @@ -57,7 +58,11 @@ fn desktop_platform() -> &'static str { } fn capabilities() -> Vec<&'static str> { - vec!["host.getRuntime", "app.openExternalUrl"] + vec![ + "host.getRuntime", + "app.openExternalUrl", + "clipboard.writeText", + ] } fn ok(id: String, result: Value) -> HostBridgeResponse { @@ -97,6 +102,24 @@ fn validate_request(request: &HostBridgeRequest) -> Option { None } +fn required_string_payload<'a>( + request: &'a HostBridgeRequest, + field: &'static str, +) -> Result<&'a str, HostBridgeResponse> { + request + .payload + .as_ref() + .and_then(|value| value.get(field)) + .and_then(Value::as_str) + .ok_or_else(|| { + failed( + request.id.clone(), + "invalid_request", + format!("{} is required", field), + ) + }) +} + fn resolve_host_bridge_request(request: HostBridgeRequest) -> HostBridgeResponse { if let Some(response) = validate_request(&request) { return response; @@ -132,13 +155,9 @@ async fn host_bridge_request( match request.method.as_str() { "app.openExternalUrl" => { - let Some(url) = request - .payload - .as_ref() - .and_then(|value| value.get("url")) - .and_then(Value::as_str) - else { - return failed(request.id, "invalid_request", "url is required"); + let url = match required_string_payload(&request, "url") { + Ok(url) => url, + Err(response) => return response, }; match app.opener().open_url(url, None::<&str>) { @@ -146,12 +165,24 @@ async fn host_bridge_request( Err(error) => failed(request.id, "host_error", error.to_string()), } } + "clipboard.writeText" => { + let text = match required_string_payload(&request, "text") { + Ok(text) => text, + Err(response) => return response, + }; + + match app.clipboard().write_text(text) { + Ok(()) => ok(request.id, json!(true)), + Err(error) => failed(request.id, "host_error", error.to_string()), + } + } _ => resolve_host_bridge_request(request), } } fn main() { tauri::Builder::default() + .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { let window_config = app.config().app.windows.get(0).cloned(); @@ -210,4 +241,17 @@ mod tests { assert!(!response.ok); assert_eq!(response.error.expect("error").code, "invalid_request"); } + + #[test] + fn invalid_string_payload_is_rejected() { + let mut invalid = request("clipboard.writeText"); + invalid.payload = Some(json!({ "text": 123 })); + + let response = required_string_payload(&invalid, "text").expect_err("invalid payload"); + + assert!(!response.ok); + let error = response.error.expect("error"); + assert_eq!(error.code, "invalid_request"); + assert_eq!(error.message, "text is required"); + } } diff --git a/apps/desktop-shell/src-tauri/tauri.conf.json b/apps/desktop-shell/src-tauri/tauri.conf.json index e6671a8c..953f2ee1 100644 --- a/apps/desktop-shell/src-tauri/tauri.conf.json +++ b/apps/desktop-shell/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "build": { "beforeDevCommand": "npm --prefix ../.. run dev:web", "beforeBuildCommand": "npm --prefix ../.. run build:raw && npm run typecheck", - "devUrl": "http://127.0.0.1:3000/?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,app.openExternalUrl", + "devUrl": "http://127.0.0.1:3000/?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,app.openExternalUrl,clipboard.writeText", "frontendDist": "../../../dist" }, "app": { @@ -14,7 +14,7 @@ { "create": false, "label": "main", - "url": "index.html?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,app.openExternalUrl", + "url": "index.html?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,app.openExternalUrl,clipboard.writeText", "title": "Genarrative", "width": 1280, "height": 820, @@ -29,6 +29,6 @@ "bundle": { "active": true, "targets": "all", - "icon": [] + "icon": ["icons/icon.png"] } } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3816f569..ccf36b2d 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -20,7 +20,7 @@ - 背景:后续需要移动端 App 和桌面端 App,但现有主站、固定玩法 runtime、小程序壳和未来 AI H5 sandbox 已经以 H5 为主线;如果移动端重写 React Native UI、桌面端重写 Rust/Tauri UI,会形成玩法、登录、支付、分享和运行态的多套实现。 - 决策:移动端原生壳采用 `Expo + React Native`,桌面端壳采用 `Tauri`。两者都只作为 `native_app` 宿主壳和 HostBridge adapter,不重写现有 React H5 主站,不把固定内置玩法迁到 React Native / Rust UI,也不让 AI 生成 H5 游戏直接访问完整 HostBridge。Expo 壳通过 `react-native-webview` 承接 H5 与 native 通信,Tauri 壳通过受控 command 和 capabilities 承接桌面能力;新增能力必须先进入 HostBridge 契约和测试。 -- 2026-06-17 首轮落地:新增 `packages/shared/src/contracts/hostBridge.ts`、`src/services/host-bridge/nativeAppHostBridge.ts`、`apps/mobile-shell/` 和 `apps/desktop-shell/`。首轮壳只声明并实现真实可用能力;登录、支付、桌面剪贴板等未接入真实 SDK / 插件前必须返回 unsupported 并让 H5 fallback,生产代码禁止 mock 成功。 +- 2026-06-17 首轮落地:新增 `packages/shared/src/contracts/hostBridge.ts`、`src/services/host-bridge/nativeAppHostBridge.ts`、`apps/mobile-shell/` 和 `apps/desktop-shell/`。壳只声明并实现真实可用能力;桌面壳已通过 Tauri clipboard-manager 接入 `clipboard.writeText`,登录、支付、桌面分享等未接入真实 SDK / 插件前必须返回 unsupported 并让 H5 fallback,生产代码禁止 mock 成功。 - 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。 - 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。 - 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 3cd24040..065e6c36 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -249,7 +249,7 @@ GameBridge 禁止: - 实现 runtime、openExternalUrl、clipboard、share fallback、窗口标题同步。 - 验证 macOS / Windows / Linux 至少一条本地 smoke。 -当前状态:已新增 `apps/desktop-shell/`,Tauri dev 直接加载本地主站 Vite,release 打包根 `dist` 主站资产。Rust 侧只把 `host_bridge_request` command 授给主窗口,`app.openExternalUrl` 由 Rust 内部通过 opener 插件执行,不把 opener 插件直接暴露给前端;首轮真实能力为 `host.getRuntime` 和 `app.openExternalUrl`。剪贴板、分享面板、登录和支付未接入真实插件 / 渠道前不声明支持,不返回 mock 成功。 +当前状态:已新增 `apps/desktop-shell/`,Tauri dev 直接加载本地主站 Vite,release 打包根 `dist` 主站资产。Rust 侧只把 `host_bridge_request` command 授给主窗口,`app.openExternalUrl` 由 Rust 内部通过 opener 插件执行,`clipboard.writeText` 由 Rust 内部通过 clipboard-manager 插件写入系统剪贴板;不把 opener 或 clipboard 插件命令直接暴露给前端。当前真实能力为 `host.getRuntime`、`app.openExternalUrl` 和 `clipboard.writeText`。分享面板、登录和支付未接入真实插件 / 渠道前不声明支持,不返回 mock 成功。 ### Phase 4:宿主能力扩展