feat: 完成 OpenNexus 第三阶段核心功能与生产化基础 #45

Merged
Kronecker merged 151 commits from feat/phase3-completion into main 2026-09-10 01:16:31 +08:00
10 changed files with 423 additions and 15 deletions
Showing only changes of commit fd6b091c48 - Show all commits
+3 -2
View File
@@ -8,7 +8,8 @@
| 命令 | 参数 / 返回 | | 命令 | 参数 / 返回 |
| --- | --- | | --- | --- |
| `host_capabilities` | protocol=1workspace=truecore/sync/credentials/extensions=false | | `host_capabilities` | protocol=1workspace/core=truesync/credentials/extensions=false |
| `core_request` | 仅代理固定的 `http://127.0.0.1:8000/health``/api/*` JSON 请求;路径、方法和超时由 Host 限制 |
| `workspace_choose` | 原生选择,取消返回 null;成功返回 vault_id/path/name | | `workspace_choose` | 原生选择,取消返回 null;成功返回 vault_id/path/name |
| `workspace_open` | 只允许重开应用数据目录中已持久化授权的规范化路径 | | `workspace_open` | 只允许重开应用数据目录中已持久化授权的规范化路径 |
| `workspace_recent` | 最近 20 个用户主动选择的 Vault;只保存身份、名称和路径 | | `workspace_recent` | 最近 20 个用户主动选择的 Vault;只保存身份、名称和路径 |
@@ -34,6 +35,6 @@ OS 文件锁配合 Rust Mutex 维持单实例 Vault 写入。Web `serialized_vau
## 前端与发布 ## 前端与发布
Web 模式沿用现有服务。Tauri Workspace Service 调用原生命令;AI Core 未接通时明确返回 CORE_UNAVAILABLE。桌面窗口关闭系统装饰,统一由应用标题栏提供拖动、最小化、最大化/还原及关闭;窗口命令只授予本地 `main` WebView。关闭请求先尝试保存,冲突或保存失败时保持窗口。原生菜单事件和标题栏可见的“段落”菜单复用 `editor.import-note-properties`;仅在源码模式、已打开笔记且无冲突时启用,转换作为单次编辑器历史事务执行。 Web 模式沿用现有服务。Tauri Workspace Service 调用原生命令;当前预览版通过 Host 连接已在 `127.0.0.1:8000` 启动的 AI Core,连接失败时返回 CORE_UNAVAILABLE。随机端口、临时令牌和自动管理进程仍属于后续 Sidecar 交付。桌面窗口关闭系统装饰,统一由应用标题栏提供拖动、最小化、最大化/还原及关闭;窗口命令只授予本地 `main` WebView。关闭请求先尝试保存,冲突或保存失败时保持窗口。原生菜单事件和标题栏可见的“段落”菜单复用 `editor.import-note-properties`;仅在源码模式、已打开笔记且无冲突时启用,转换作为单次编辑器历史事务执行。
`bundle.active=false`,无自动更新、无签名安装包。开发命令为前端 `pnpm dev``cargo run --features desktop`;生产 EXE 必须在 `frontend` 目录执行 `pnpm desktop:build`,由 Tauri CLI 先构建并嵌入前端资源。不要用普通 `cargo build --release` 代替该命令,否则 Rust Host 会更新但 WebView 仍可能携带旧资源。需要对应平台 C++ 工具链和 WebView;本次 Windows 已使用系统 `x86_64-pc-windows-gnu` 工具链构建,仍不替代 MSVC 或其他平台验证。 `bundle.active=false`,无自动更新、无签名安装包。开发命令为前端 `pnpm dev``cargo run --features desktop`;生产 EXE 必须在 `frontend` 目录执行 `pnpm desktop:build`,由 Tauri CLI 先构建并嵌入前端资源。不要用普通 `cargo build --release` 代替该命令,否则 Rust Host 会更新但 WebView 仍可能携带旧资源。需要对应平台 C++ 工具链和 WebView;本次 Windows 已使用系统 `x86_64-pc-windows-gnu` 工具链构建,仍不替代 MSVC 或其他平台验证。
+302 -5
View File
@@ -70,7 +70,7 @@ dependencies = [
"enumflags2", "enumflags2",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"rand", "rand 0.9.5",
"raw-window-handle", "raw-window-handle",
"serde", "serde",
"serde_repr", "serde_repr",
@@ -515,6 +515,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures 0.3.1",
"rand_core 0.10.1",
]
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.45" version = "0.4.45"
@@ -605,6 +622,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.1" version = "1.5.1"
@@ -1348,8 +1374,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1371,8 +1399,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi 6.0.0", "r-efi 6.0.0",
"rand_core 0.10.1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1646,6 +1677,22 @@ dependencies = [
"want", "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",
"webpki-roots",
]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.20" version = "0.1.20"
@@ -2127,6 +2174,12 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.38.0" version = "0.38.0"
@@ -2246,6 +2299,7 @@ name = "notesagent-desktop"
version = "0.3.0-alpha.1" version = "0.3.0-alpha.1"
dependencies = [ dependencies = [
"fs2", "fs2",
"reqwest 0.12.28",
"rfd", "rfd",
"rusqlite", "rusqlite",
"serde", "serde",
@@ -2831,6 +2885,62 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quinn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.20",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
dependencies = [
"bytes",
"getrandom 0.4.3",
"lru-slab",
"rand 0.10.2",
"rand_pcg",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.20",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
@@ -2859,7 +2969,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [ dependencies = [
"rand_chacha", "rand_chacha",
"rand_core", "rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.3",
"rand_core 0.10.1",
] ]
[[package]] [[package]]
@@ -2869,7 +2990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.9.5",
] ]
[[package]] [[package]]
@@ -2881,6 +3002,21 @@ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
] ]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]] [[package]]
name = "raw-window-handle" name = "raw-window-handle"
version = "0.6.2" version = "0.6.2"
@@ -2956,6 +3092,44 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" 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",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.4" version = "0.13.4"
@@ -3014,6 +3188,20 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[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]] [[package]]
name = "rusqlite" name = "rusqlite"
version = "0.32.1" version = "0.32.1"
@@ -3056,12 +3244,53 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "rustls"
version = "0.23.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.23" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -3258,6 +3487,18 @@ dependencies = [
"serde_core", "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]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.22.0" version = "3.22.0"
@@ -3329,7 +3570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures 0.2.17",
"digest", "digest",
] ]
@@ -3467,6 +3708,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "swift-rs" name = "swift-rs"
version = "1.0.8" version = "1.0.8"
@@ -3630,7 +3877,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"plist", "plist",
"raw-window-handle", "raw-window-handle",
"reqwest", "reqwest 0.13.4",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@@ -3944,6 +4191,16 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
dependencies = [
"rustls",
"tokio",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.19" version = "0.7.19"
@@ -4257,6 +4514,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@@ -4516,6 +4779,16 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "web_atoms" name = "web_atoms"
version = "0.2.6" version = "0.2.6"
@@ -4572,6 +4845,15 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.38.2" version = "0.38.2"
@@ -4802,6 +5084,15 @@ dependencies = [
"windows-targets 0.42.2", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@@ -5204,6 +5495,12 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.5" version = "0.2.5"
+3 -2
View File
@@ -14,7 +14,7 @@ required-features = ["desktop"]
[features] [features]
default = [] default = []
desktop = ["dep:tauri", "dep:tauri-build", "dep:rfd"] desktop = ["dep:tauri", "dep:tauri-build", "dep:rfd", "dep:reqwest"]
[dependencies] [dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@@ -26,6 +26,7 @@ tempfile = "3"
fs2 = "0.4" fs2 = "0.4"
tauri = { version = "2", optional = true, features = ["tray-icon"] } tauri = { version = "2", optional = true, features = ["tray-icon"] }
rfd = { version = "0.15", optional = true } rfd = { version = "0.15", optional = true }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"], optional = true }
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", optional = true } tauri-build = { version = "2", optional = true , features = [] }
+1
View File
@@ -3,6 +3,7 @@ fn main() {
tauri_build::try_build(tauri_build::Attributes::new().app_manifest( tauri_build::try_build(tauri_build::Attributes::new().app_manifest(
tauri_build::AppManifest::new().commands(&[ tauri_build::AppManifest::new().commands(&[
"host_capabilities", "host_capabilities",
"core_request",
"editor_capabilities", "editor_capabilities",
"workspace_choose", "workspace_choose",
"workspace_open", "workspace_open",
@@ -8,6 +8,7 @@
"permissions": [ "permissions": [
"core:default", "core:default",
"allow-host-capabilities", "allow-host-capabilities",
"allow-core-request",
"allow-workspace-choose", "allow-workspace-choose",
"allow-workspace-open", "allow-workspace-open",
"allow-workspace-tree", "allow-workspace-tree",
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-core-request"
description = "Enables the core_request command without any pre-configured scope."
commands.allow = ["core_request"]
[[permission]]
identifier = "deny-core-request"
description = "Denies the core_request command without any pre-configured scope."
commands.deny = ["core_request"]
+80 -1
View File
@@ -6,6 +6,7 @@ use notesagent_host::recent::{RecentVault, RecentVaultStore};
use notesagent_host::workspace::{Document, Entry, Workspace}; use notesagent_host::workspace::{Document, Entry, Workspace};
use std::path::Path; use std::path::Path;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::Duration;
use tauri::{Emitter, Manager, State}; use tauri::{Emitter, Manager, State};
#[derive(Default)] #[derive(Default)]
@@ -38,7 +39,84 @@ fn with_workspace<T>(
#[tauri::command] #[tauri::command]
fn host_capabilities() -> serde_json::Value { fn host_capabilities() -> serde_json::Value {
serde_json::json!({"protocol":1,"workspace":true,"core":false,"sync":false,"credentials":false,"extensions":false,"release":"preview"}) serde_json::json!({"protocol":1,"workspace":true,"core":true,"sync":false,"credentials":false,"extensions":false,"release":"preview"})
}
#[derive(serde::Serialize)]
struct CoreResponse {
status: u16,
content_type: String,
body: String,
}
fn core_url(path: &str) -> Result<String, String> {
if (!path.starts_with("/api/") && path != "/api" && path != "/health")
|| path.contains("..")
|| path.contains(['\r', '\n'])
{
return Err("CORE_PATH_DENIED".into());
}
Ok(format!("http://127.0.0.1:8000{path}"))
}
#[cfg(test)]
mod core_proxy_tests {
use super::core_url;
#[test]
fn only_allows_expected_loopback_paths() {
assert_eq!(core_url("/health").unwrap(), "http://127.0.0.1:8000/health");
assert!(core_url("/api/status?verbose=true").is_ok());
assert!(core_url("https://example.com/api/status").is_err());
assert!(core_url("/api/../secret").is_err());
}
}
/// 预览版只代理固定回环地址,避免 WebView CORS 与任意地址转发。
#[tauri::command]
async fn core_request(
method: String,
path: String,
body: Option<serde_json::Value>,
authorization: Option<String>,
) -> Result<CoreResponse, String> {
let method =
reqwest::Method::from_bytes(method.as_bytes()).map_err(|_| "CORE_METHOD_DENIED")?;
if !matches!(
method,
reqwest::Method::GET
| reqwest::Method::POST
| reqwest::Method::PUT
| reqwest::Method::PATCH
| reqwest::Method::DELETE
) {
return Err("CORE_METHOD_DENIED".into());
}
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|_| "CORE_CLIENT_ERROR")?;
let mut request = client.request(method, core_url(&path)?);
if let Some(value) = body {
request = request.json(&value);
}
if let Some(value) = authorization {
request = request.header(reqwest::header::AUTHORIZATION, value);
}
let response = request.send().await.map_err(|_| "CORE_UNAVAILABLE")?;
let status = response.status().as_u16();
let content_type = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("")
.to_owned();
let body = response.text().await.map_err(|_| "CORE_RESPONSE_ERROR")?;
Ok(CoreResponse {
status,
content_type,
body,
})
} }
#[tauri::command] #[tauri::command]
@@ -197,6 +275,7 @@ fn main() {
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
host_capabilities, host_capabilities,
core_request,
editor_capabilities, editor_capabilities,
workspace_choose, workspace_choose,
workspace_open, workspace_open,
+1 -1
View File
@@ -12,7 +12,7 @@
"app": { "app": {
"windows": [{"label": "main", "title": "NotesAgent Preview", "width": 1200, "height": 800, "minWidth": 640, "minHeight": 480, "decorations": false}], "windows": [{"label": "main", "title": "NotesAgent Preview", "width": 1200, "height": 800, "minWidth": 640, "minHeight": 480, "decorations": false}],
"security": { "security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src ipc: http://ipc.localhost; frame-src 'self' blob:; object-src 'none'; base-uri 'self'", "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:8000; font-src 'self' data:; connect-src ipc: http://ipc.localhost http://127.0.0.1:8000; frame-src 'self' blob:; object-src 'none'; base-uri 'self'",
"capabilities": ["main"] "capabilities": ["main"]
} }
}, },
+1 -1
View File
@@ -72,7 +72,7 @@ async function openFolderPicker() {
<div v-if="openError" class="error-banner" role="alert">{{ openError }} <button class="btn" @click="initializeVault" :disabled="isLoading">{{ t('重试', 'Retry') }}</button></div> <div v-if="openError" class="error-banner" role="alert">{{ openError }} <button class="btn" @click="initializeVault" :disabled="isLoading">{{ t('重试', 'Retry') }}</button></div>
<p v-if="isLoading" role="status">{{ t('正在打开知识库', 'Opening knowledge base') }}</p> <p v-if="isLoading" role="status">{{ t('正在打开知识库', 'Opening knowledge base') }}</p>
<h2 class="card-title">{{ t('选择知识库', 'Select Knowledge Base') }}</h2> <h2 class="card-title">{{ t('选择知识库', 'Select Knowledge Base') }}</h2>
<p class="card-desc">{{ isDesktop() ? t('桌面预览:选择本地目录;AI 与同步尚未接通。', 'Desktop preview: choose a local folder. AI and sync are not connected yet.') : t('Web 联调模式连接 AI Core 当前配置的 Vault', 'Web development mode connects to the Vault configured in AI Core') }}</p> <p class="card-desc">{{ isDesktop() ? t('桌面预览:选择本地目录;自动连接本机 AI Core。', 'Desktop preview: choose a local folder and connect to the local AI Core.') : t('Web 联调模式连接 AI Core 当前配置的 Vault', 'Web development mode connects to the Vault configured in AI Core') }}</p>
<div v-if="workspaceStore.recentVaults.length" class="recent-vaults"> <div v-if="workspaceStore.recentVaults.length" class="recent-vaults">
<div class="section-label">{{ t('最近打开', 'Recently opened') }}</div> <div class="section-label">{{ t('最近打开', 'Recently opened') }}</div>
+20 -3
View File
@@ -1,8 +1,10 @@
import type { ApiError, ErrorResponse } from '@/contracts' import type { ApiError, ErrorResponse } from '@/contracts'
import { isDesktop } from './platform/desktop' import { hostInvoke, isDesktop } from './platform/desktop'
// 所有 HTTP 请求都经过此边界,以统一地址、请求追踪和错误契约。 // 所有 HTTP 请求都经过此边界,以统一地址、请求追踪和错误契约。
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? import.meta.env.VITE_API_BASE ?? '' const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? import.meta.env.VITE_API_BASE ?? (isDesktop() ? 'http://127.0.0.1:8000' : '')
interface DesktopCoreResponse { status: number; content_type: string; body: string }
export function resolveApiUrl(path: string): string { export function resolveApiUrl(path: string): string {
if (/^https?:\/\//i.test(path)) return path if (/^https?:\/\//i.test(path)) return path
@@ -28,7 +30,6 @@ export class ApiErrorClass extends Error {
} }
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> { async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
if (isDesktop()) throw new ApiErrorClass('CORE_UNAVAILABLE', '桌面 AI Core 尚未接通;本地编辑可继续。')
const { params, token, headers, timeoutMs, ...rest } = options const { params, token, headers, timeoutMs, ...rest } = options
const controller = timeoutMs ? new AbortController() : null const controller = timeoutMs ? new AbortController() : null
let timedOut = false let timedOut = false
@@ -61,6 +62,22 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
reqHeaders['X-Request-Id'] = reqId reqHeaders['X-Request-Id'] = reqId
try { try {
if (isDesktop()) {
const parsed = new URL(url)
const response = await hostInvoke<DesktopCoreResponse>('core_request', {
method: rest.method ?? 'GET',
path: `${parsed.pathname}${parsed.search}`,
body: typeof rest.body === 'string' ? JSON.parse(rest.body) : undefined,
authorization: token ? `Bearer ${token}` : undefined,
})
if (response.status >= 200 && response.status < 300) {
if (response.status === 204) return undefined as T
return (response.content_type.includes('application/json') ? JSON.parse(response.body) : response.body) as T
}
let error: ErrorResponse | null = null
try { error = JSON.parse(response.body) as ErrorResponse } catch { /* 非 JSON 错误 */ }
throw new ApiErrorClass(error?.error?.code ?? `HTTP_${response.status}`, error?.error?.message ?? `Request failed with status ${response.status}`, error?.error?.details)
}
const resp = await fetch(url, { const resp = await fetch(url, {
...rest, ...rest,
signal: controller?.signal ?? rest.signal, signal: controller?.signal ?? rest.signal,