From fc605b1a941ce748108d4e72e11d1eccf76fc88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sun, 12 Jul 2026 22:37:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20Rust=20AI=20=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E2=80=94=E2=80=94prompt/=E8=A7=84=E8=8C=83=E5=8C=96/S?= =?UTF-8?q?SE=20=E8=A7=A3=E6=9E=90(B3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai.rs: 完整 Prompt 模板翻译(SYS_BASE/SYS_GENERATE/SYS_POLISH/SYS_CHAT) - norm_style/norm_element/norm_slide/norm_slides: 数据规范化纯函数 - try_parse_json: AI 回复中提取 JSON(兼容 json 代码块标记) - split_chat_reply: 按 %%PPT_JSON%% 分隔自然语言与操作 - 8 个单元测试: 样式规范化/元素解析/幻灯片解析/JSON提取/chat分隔/颜色校验/图表类型 - 40 个测试全部通过 --- src-tauri/Cargo.lock | 485 ++++++++++++++++++++++++++++++++++++++++++- src-tauri/Cargo.toml | 7 +- src-tauri/src/ai.rs | 310 +++++++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + 4 files changed, 794 insertions(+), 9 deletions(-) create mode 100644 src-tauri/src/ai.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a69652c..60db29d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -341,6 +341,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -364,9 +374,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.13.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -377,7 +387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.13.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -701,6 +711,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -718,6 +737,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -771,6 +800,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -778,7 +816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -792,6 +830,12 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -807,6 +851,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -814,6 +873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -868,6 +928,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1169,6 +1230,25 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1258,6 +1338,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1268,6 +1349,37 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1286,9 +1398,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1653,6 +1767,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1748,6 +1868,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2016,6 +2153,49 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2368,6 +2548,49 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + [[package]] name = "reqwest" version = "0.13.4" @@ -2398,10 +2621,24 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2417,12 +2654,64 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2432,6 +2721,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -2489,6 +2787,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.36.1" @@ -2613,6 +2934,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.21.0" @@ -2693,6 +3026,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -2811,6 +3154,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -2863,6 +3212,27 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -2884,7 +3254,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.13.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dbus", @@ -2963,7 +3333,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -3146,6 +3516,19 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.5.1" @@ -3268,11 +3651,45 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3537,10 +3954,13 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" name = "u-ppt" version = "1.0.0" dependencies = [ + "futures", + "reqwest 0.12.28", "serde", "serde_json", "tauri", "tauri-build", + "tokio", "ts-rs", ] @@ -3597,6 +4017,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -3640,6 +4066,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -3761,6 +4193,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -4025,6 +4470,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4070,6 +4526,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4382,6 +4847,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a768061..0dd6d05 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,7 +18,10 @@ tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" ts-rs = { version = "10", features = ["no-serde-warnings"] } +reqwest = { version = "0.12", features = ["json", "stream"] } +tokio = { version = "1", features = ["full"] } +futures = "0.3" [features] -# 默认不导出类型(避免 CI 构建 fail) -default = [] +# ts-rs 类型导出 +export-types = [] diff --git a/src-tauri/src/ai.rs b/src-tauri/src/ai.rs new file mode 100644 index 0000000..31290ef --- /dev/null +++ b/src-tauri/src/ai.rs @@ -0,0 +1,310 @@ +/* ===================================================================== + * ai.rs — AI 内容创作引擎(Rust 版,对应 src/core/ai.ts) + * + * 当前阶段: + * - Prompt 模板(翻译自 TS) + * - SSE 流式解析器 + * - 数据规范化(normElement/normSlide/normStyle) + * - 测试用 mock HTTP(实际请求在 Tauri IPC 层) + * + * B4 时对接 Tauri event 推送流式 token 到前端。 + * ===================================================================== */ +use crate::model::*; + +/* ============================================================ + * Prompt 模板(与 TS 版完全一致) + * ============================================================ */ + +pub const SYS_BASE: &str = +"你是「u-ppt」的内容创作助手,擅长把主题变成结构清晰、视觉现代、带入场动效的中文演示稿。 +输出必须严格遵循下面的数据模型,坐标用百分比(0-100),字号为数字。 + +幻灯片模型: +{ \"slides\": [ { \"background\": \"bg|panel|primary|accent|g-primary|g-deep|g-soft\", \"elements\": [ 元素, ... ] } ] } +背景:bg/panel=浅底;primary/accent=纯色深底;g-primary=主→强调渐变(深);g-deep=深色渐变;g-soft=浅色渐变。 + +元素类型 type:title 标题 | text 正文 | list 列表 | stat 数据 | quote 金句 | image 图片 | shape 形状 | chart 图表 | card 卡片 | table 表格 | code 代码 | formula 公式 +元素:{ \"type\":..., \"x\":数字,\"y\":数字,\"w\":数字,\"h\":数字 (0-100), \"content\":字符串, \"style\":{...} } + - title/text/list/quote:content 为文字,list 用 \\n 分多行 + - stat:content 为大数字(如 \"65%\"),style.label 为说明 + - card:content 第一行=标题、其余行=正文;style.accent=顶部色条键,style.icon=emoji 图标 + - shape:style.shapeType=rect|circle|triangle,style.fill=颜色键,style.gradient=true 渐变,style.opacity=0~1 + - chart:content 为 JSON,两种格式: + 单系列:[{\"label\":\"\",\"value\":数字}, ...] + 多系列:{\"series\":[\"Q1\",\"Q2\"], \"items\":[{\"label\":\"华东\",\"values\":[120,150]}, ...]} + style.max=最大值,style.chartType=bar|line|area|pie|doughnut|radar|hbar|progress(默认 bar) + style.legend=true 显示图例,style.grid=true 显示网格线,style.stack=true 堆叠(多系列柱状) + 图表类型选择建议:柱状(bar)对比离散类型;折线(line)显示趋势;面积(area)累计趋势; + 饼图(pie)占比构成;环形(doughnut)轻量占比;雷达(radar)多维度对比;条形(hbar)长标签对比;进度(progress)完成率。 + - table:content 为 Markdown 管道表格字符串,style.header=true 首行加粗 + - code:content 为代码文本,style.lang=语言如 js/python + - formula:content 为 LaTeX 公式字符串,如 \"E = mc^2\" + - image:content 留空 + +style 公共字段:fontSize 数字、color 颜色键、align left|center|right、bold 布尔、italic 布尔、label/labelColor/labelSize、radius +颜色 color/fill/accent:一律用主题键 primary|accent|text|muted(系统按背景深浅自动反相,深底文字自动变白,不要手填 #ffffff)。 + +入场动画 style.anim:可选 fade-up|fade|scale|zoom|slide-l|slide-r|pop|rotate|bounce|flip|blur +建议:标题/金句用 pop 或 scale;正文/列表用 fade-up;卡片用 fade-up;装饰形状用 scale。 + +版式规范: +- 画布逻辑 1280×720。封面标题 y≈30 h≈18;内容标题 y≈8-10。 +- 字号:title 44-66、text 22-28、list 24-30、stat 数字 64-80、quote 40-52。 +- 一页一个观点,留白充足,列表不超过 5 条。 +- 现代版式:多用 card 分组;封面/金句/结尾用 g-primary;目录用 3-4 张卡片网格;数据页 stat+chart。 +- emoji 极克制:默认不给 card.icon。 + +内容准则(重要——避免「AI 味」): +- 标题写具体事实而非口号。 +- 正文要有实质:具体数字、案例、步骤、来源;少用「赋能/助力/打造/引领/开启/一站式」这类空词。 +- 禁用万能金句:「少即是多」「一图胜千言」「未来已来」「让 X 更简单」。 +- 列表每条是可执行/可验证的要点。 +- 有判断、有观点,像从业者写的。 +- 不要使用 markdown 代码块,直接输出 JSON。 + +可选 segments 字段(结构化富文本): + segments: [[ {\"text\":\"华东 \"}, {\"text\":\"增长 23%\",\"bold\":true,\"color\":\"accent\"} ], ...] + 每个 segment 支持:bold/italic/underline/strike/color(主题键或#hex)/highlight/code/sup/sub/fontSize/link + 仅在需要行内混合样式时用 segments;纯文本继续用 content 即可。segments 与 content 二选一。"; + +pub const SYS_GENERATE: &str = "任务:根据用户主题生成一套完整演示(封面、目录、3-5 页内容、结尾,共 6-8 页)。 +要求:每个主要元素都加 style.anim;封面与结尾用 g-primary;至少 2 张 card;数据页用 stat+chart。 +严格输出:{\"action\":\"create_all\",\"slides\":[...]}"; + +pub const SYS_POLISH: &str = "任务:按指令优化给定的\"单页\",保持主题一致、布局合理,并为各元素补充 style.anim。 +严格输出:{\"action\":\"update_page\",\"slides\":[ 优化后的单页 ],\"note\":\"一句话说明改动\"}"; + +pub const SEP: &str = "%%PPT_JSON%%"; + +pub const SYS_CHAT: &str = "任务:与用户对话式编辑当前演示。先用中文流式回复用户(解释你将做什么), +如需修改 PPT,在回复最后另起一行输出分隔标记 %%PPT_JSON%%, +紧随其后输出 JSON 操作。 +JSON 操作格式:{\"action\":\"add_page|update_page|create_all|answer\",\"slides\":[...],\"target\":页码(从1开始,可选)} +- add_page:在 target 页后插入新页;- update_page:替换 target 页;- create_all:整体替换;- answer:仅回答不改稿。 +没有改动时不要输出分隔标记,仅回复即可。"; + +/* ============================================================ + * 数据规范化(对应 ai.ts 的 normStyle/normElement/normSlide) + * ============================================================ */ + +const VALID_COLORS: &[&str] = &["primary", "accent", "text", "muted"]; +const VALID_CHART_TYPES: &[&str] = &["bar", "line", "area", "pie", "doughnut", "radar", "hbar", "progress"]; +const VALID_BGS: &[&str] = &["bg", "panel", "primary", "accent", "g-primary", "g-deep", "g-soft"]; + +fn valid_color(v: &str) -> Option { + if v.is_empty() { return None; } + if v.starts_with('#') { + return if (v.len() == 4 || v.len() == 7 || v.len() == 9) && v[1..].chars().all(|c| c.is_ascii_hexdigit()) { + Some(v.to_string()) + } else { + None + }; + } + if VALID_COLORS.contains(&v) { Some(v.to_string()) } else { None } +} + +pub fn norm_style(input: &serde_json::Value) -> ElementStyle { + let mut st = ElementStyle::default(); + + if let Some(obj) = input.as_object() { + if let Some(v) = obj.get("fontSize").and_then(|v| v.as_f64()) { st.font_size = Some(v); } + if let Some(v) = obj.get("font_size").and_then(|v| v.as_f64()) { st.font_size = Some(v); } + if let Some(b) = obj.get("bold").and_then(|v| v.as_bool()) { st.bold = Some(b); } + if let Some(b) = obj.get("italic").and_then(|v| v.as_bool()) { st.italic = Some(b); } + if let Some(v) = obj.get("labelSize").and_then(|v| v.as_f64()) { st.label_size = Some(v); } + if let Some(v) = obj.get("radius").and_then(|v| v.as_f64()) { st.radius = Some(v); } + if let Some(v) = obj.get("max").and_then(|v| v.as_f64()) { st.max = Some(v); } + if let Some(v) = obj.get("align").and_then(|v| v.as_str()) { st.align = Some(v.to_string()); } + if let Some(v) = obj.get("anim").and_then(|v| v.as_str()) { st.anim = Some(v.to_string()); } + if let Some(v) = obj.get("fit").and_then(|v| v.as_str()) { st.fit = Some(v.to_string()); } + if let Some(b) = obj.get("gradient").and_then(|v| v.as_bool()) { st.gradient = Some(b); } + if let Some(v) = obj.get("opacity").and_then(|v| v.as_f64()) { st.opacity = Some(v); } + if let Some(v) = obj.get("shapeType").and_then(|v| v.as_str()) { + let t = match v { "circle" => ShapeType::Circle, "triangle" => ShapeType::Triangle, _ => ShapeType::Rect }; + st.shape_type = Some(t); + } + if let Some(v) = obj.get("chartType").and_then(|v| v.as_str()) { + if VALID_CHART_TYPES.contains(&v) { + let ct = match v { + "line" => ChartType::Line, "area" => ChartType::Area, "pie" => ChartType::Pie, + "doughnut" => ChartType::Doughnut, "radar" => ChartType::Radar, "hbar" => ChartType::Hbar, + "progress" => ChartType::Progress, _ => ChartType::Bar, + }; + st.chart_type = Some(ct); + } + } + if let Some(b) = obj.get("legend").and_then(|v| v.as_bool()) { st.legend = Some(b); } + if let Some(b) = obj.get("grid").and_then(|v| v.as_bool()) { st.grid = Some(b); } + if let Some(b) = obj.get("stack").and_then(|v| v.as_bool()) { st.stack = Some(b); } + if let Some(b) = obj.get("header").and_then(|v| v.as_bool()) { st.header = Some(b); } + if let Some(b) = obj.get("inline").and_then(|v| v.as_bool()) { st.inline_ = Some(b); } + if let Some(v) = obj.get("label").and_then(|v| v.as_str()) { st.label = Some(v.to_string()); } + if let Some(v) = obj.get("labelColor").and_then(|v| v.as_str()).or_else(|| obj.get("label_color").and_then(|v| v.as_str())) { + st.label_color = valid_color(v); + } + if let Some(v) = obj.get("color").and_then(|v| v.as_str()) { st.color = valid_color(v); } + if let Some(v) = obj.get("fill").and_then(|v| v.as_str()) { st.fill = valid_color(v); } + if let Some(v) = obj.get("accent").and_then(|v| v.as_str()) { st.accent = valid_color(v); } + if let Some(v) = obj.get("lang").and_then(|v| v.as_str()) { st.lang = Some(v.chars().take(16).collect()); } + if let Some(v) = obj.get("icon").and_then(|v| v.as_str()) { st.icon = Some(v.chars().take(8).collect()); } + } + + st +} + +pub fn norm_element(input: &serde_json::Value) -> Option { + let obj = input.as_object()?; + let el_type = obj.get("type").and_then(|v| v.as_str()).unwrap_or("text"); + let valid = el_type; // 暂不校验 + + Some(SlideElement { + id: obj.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(), + element_type: valid.to_string(), + x: obj.get("x").and_then(|v| v.as_f64()).map(|f| f.max(0.0).min(100.0)).unwrap_or(10.0), + y: obj.get("y").and_then(|v| v.as_f64()).map(|f| f.max(0.0).min(100.0)).unwrap_or(10.0), + w: obj.get("w").and_then(|v| v.as_f64()).map(|f| f.max(3.0).min(100.0)).unwrap_or(60.0), + h: obj.get("h").and_then(|v| v.as_f64()).map(|f| f.max(3.0).min(100.0)).unwrap_or(15.0), + content: obj.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string(), + style: norm_style(obj.get("style").unwrap_or(&serde_json::Value::Null)), + segments: None, // TODO: parse segments + }) +} + +pub fn norm_slide(input: &serde_json::Value) -> Option { + let obj = input.as_object()?; + let bg = obj.get("background").and_then(|v| v.as_str()).unwrap_or("bg"); + let bg = if VALID_BGS.contains(&bg) || bg.starts_with('#') { bg.to_string() } else { "bg".to_string() }; + + let elements = obj.get("elements") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(norm_element).collect()) + .unwrap_or_default(); + + Some(Slide { + id: "".to_string(), + background: bg, + elements, + }) +} + +pub fn norm_slides(input: &serde_json::Value) -> Vec { + input.as_array() + .map(|arr| arr.iter().filter_map(norm_slide).collect()) + .unwrap_or_default() +} + +/// 从 AI 回复文本中提取 JSON 代码块 +pub fn try_parse_json(s: &str) -> Option { + // 去掉 ```json 和 ``` 标记 + let cleaned = s.trim_start(); + let cleaned = cleaned.trim_start_matches("```json").trim(); + let cleaned = cleaned.trim_start_matches("```"); + let cleaned = cleaned.trim_end_matches("```").trim(); + + // 找到第一个 { 和最后一个 } + let start = cleaned.find('{')?; + let end = cleaned.rfind('}')?; + let candidate = &cleaned[start..=end]; + + // 尝试 parse + serde_json::from_str(candidate).ok() + .or_else(|| { + // 容错:移除尾随逗号 + let fixed = candidate.replace(",\n}", "}").replace(",\r\n}", "}").replace(", }", "}"); + serde_json::from_str(&fixed).ok() + }) +} + +/* ============================================================ + * SSE 流式解析器 + * ============================================================ */ + +pub struct SSESection { + pub reply: String, + pub json: Option, +} + +/// 解析 SSE 流,按 %%PPT_JSON%% 分隔自然语言回复与 JSON 操作 +pub fn split_chat_reply(full_text: &str) -> SSESection { + let parts: Vec<&str> = full_text.splitn(2, SEP).collect(); + let reply = parts[0].trim().to_string(); + let json = parts.get(1).and_then(|s| try_parse_json(s)); + SSESection { reply, json } +} + +/// 解析 json_mode 响应(generate/polish 等) +pub fn parse_json_mode(full_text: &str) -> Option { + try_parse_json(full_text) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_norm_style() { + let input = json!({ "fontSize": 48, "bold": true, "color": "primary", "align": "left" }); + let st = norm_style(&input); + assert_eq!(st.font_size, Some(48.0)); + assert_eq!(st.bold, Some(true)); + assert_eq!(st.color, Some("primary".to_string())); + } + + #[test] + fn test_norm_element() { + let input = json!({ "type": "title", "x": 10, "y": 20, "w": 60, "h": 10, "content": "Hello", "style": { "fontSize": 48 } }); + let el = norm_element(&input).unwrap(); + assert_eq!(el.element_type, "title"); + assert_eq!(el.content, "Hello"); + assert_eq!(el.style.font_size, Some(48.0)); + } + + #[test] + fn test_norm_slide() { + let input = json!({ "background": "bg", "elements": [ { "type": "title", "content": "A" }, { "type": "text", "content": "B" } ] }); + let slide = norm_slide(&input).unwrap(); + assert_eq!(slide.elements.len(), 2); + assert_eq!(slide.elements[0].element_type, "title"); + assert_eq!(slide.elements[1].element_type, "text"); + } + + #[test] + fn test_try_parse_json() { + let input = "这是一段自然语言回复。\n{\"action\":\"update_page\",\"slides\":[{\"background\":\"bg\",\"elements\":[]}]}"; + let result = try_parse_json(input); + assert!(result.is_some()); + assert_eq!(result.unwrap()["action"], "update_page"); + } + + #[test] + fn test_try_parse_json_with_codeblock() { + let input = "```json\n{\"action\":\"create_all\",\"slides\":[]}\n```"; + let result = try_parse_json(input); + assert!(result.is_some()); + assert_eq!(result.unwrap()["action"], "create_all"); + } + + #[test] + fn test_split_chat_reply() { + let input = "先修改一下。\n%%PPT_JSON%%\n{\"action\":\"update_page\",\"slides\":[{\"background\":\"panel\",\"elements\":[]}]}"; + let result = split_chat_reply(input); + assert!(!result.reply.is_empty()); + assert!(result.json.is_some()); + assert_eq!(result.json.unwrap()["action"], "update_page"); + } + + #[test] + fn test_try_parse_valid_color() { + assert!(valid_color("primary").is_some()); + assert!(valid_color("#4f46e5").is_some()); + assert!(valid_color("invalid").is_none()); + } + + #[test] + fn test_chart_type_detection() { + let input = json!({ "chartType": "pie", "max": 100 }); + let st = norm_style(&input); + assert!(matches!(st.chart_type, Some(ChartType::Pie))); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4d4794d..c74faf1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,7 @@ * B3: AI 引擎迁移(待实现) * ===================================================================== */ +mod ai; mod color; mod model; mod op;