diff --git a/docs/contracts/Host-v1契约.md b/docs/contracts/Host-v1契约.md index d8d27fd..8f457d1 100644 --- a/docs/contracts/Host-v1契约.md +++ b/docs/contracts/Host-v1契约.md @@ -8,7 +8,8 @@ | 命令 | 参数 / 返回 | | --- | --- | -| `host_capabilities` | protocol=1,workspace=true;core/sync/credentials/extensions=false | +| `host_capabilities` | protocol=1,workspace/core=true;sync/credentials/extensions=false | +| `core_request` | 仅代理固定的 `http://127.0.0.1:8000/health` 与 `/api/*` JSON 请求;路径、方法和超时由 Host 限制 | | `workspace_choose` | 原生选择,取消返回 null;成功返回 vault_id/path/name | | `workspace_open` | 只允许重开应用数据目录中已持久化授权的规范化路径 | | `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 或其他平台验证。 diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 3e14754..0ce041d 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -70,7 +70,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand", + "rand 0.9.5", "raw-window-handle", "serde", "serde_repr", @@ -515,6 +515,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "chrono" version = "0.4.45" @@ -605,6 +622,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.1" @@ -1348,8 +1374,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1371,8 +1399,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -1646,6 +1677,22 @@ 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", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -2127,6 +2174,12 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "markup5ever" version = "0.38.0" @@ -2246,6 +2299,7 @@ name = "notesagent-desktop" version = "0.3.0-alpha.1" dependencies = [ "fs2", + "reqwest 0.12.28", "rfd", "rusqlite", "serde", @@ -2831,6 +2885,62 @@ dependencies = [ "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]] name = "quote" version = "1.0.47" @@ -2859,7 +2969,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "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]] @@ -2869,7 +2990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -2881,6 +3002,21 @@ dependencies = [ "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]] name = "raw-window-handle" version = "0.6.2" @@ -2956,6 +3092,44 @@ 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", + "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]] name = "reqwest" version = "0.13.4" @@ -3014,6 +3188,20 @@ dependencies = [ "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]] name = "rusqlite" version = "0.32.1" @@ -3056,12 +3244,53 @@ dependencies = [ "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]] 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" @@ -3258,6 +3487,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.22.0" @@ -3329,7 +3570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3467,6 +3708,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.8" @@ -3630,7 +3877,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -3944,6 +4191,16 @@ dependencies = [ "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]] name = "tokio-util" version = "0.7.19" @@ -4257,6 +4514,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" @@ -4516,6 +4779,16 @@ dependencies = [ "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]] name = "web_atoms" version = "0.2.6" @@ -4572,6 +4845,15 @@ dependencies = [ "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]] name = "webview2-com" version = "0.38.2" @@ -4802,6 +5084,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" @@ -5204,6 +5495,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.5" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 11cdf86..3975553 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -14,7 +14,7 @@ required-features = ["desktop"] [features] default = [] -desktop = ["dep:tauri", "dep:tauri-build", "dep:rfd"] +desktop = ["dep:tauri", "dep:tauri-build", "dep:rfd", "dep:reqwest"] [dependencies] serde = { version = "1", features = ["derive"] } @@ -26,6 +26,7 @@ tempfile = "3" fs2 = "0.4" tauri = { version = "2", optional = true, features = ["tray-icon"] } rfd = { version = "0.15", optional = true } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"], optional = true } [build-dependencies] -tauri-build = { version = "2", optional = true } +tauri-build = { version = "2", optional = true , features = [] } diff --git a/frontend/src-tauri/build.rs b/frontend/src-tauri/build.rs index d822f38..6758303 100644 --- a/frontend/src-tauri/build.rs +++ b/frontend/src-tauri/build.rs @@ -3,6 +3,7 @@ fn main() { tauri_build::try_build(tauri_build::Attributes::new().app_manifest( tauri_build::AppManifest::new().commands(&[ "host_capabilities", + "core_request", "editor_capabilities", "workspace_choose", "workspace_open", diff --git a/frontend/src-tauri/capabilities/main.json b/frontend/src-tauri/capabilities/main.json index ba003a3..c50da85 100644 --- a/frontend/src-tauri/capabilities/main.json +++ b/frontend/src-tauri/capabilities/main.json @@ -8,6 +8,7 @@ "permissions": [ "core:default", "allow-host-capabilities", + "allow-core-request", "allow-workspace-choose", "allow-workspace-open", "allow-workspace-tree", diff --git a/frontend/src-tauri/permissions/autogenerated/core_request.toml b/frontend/src-tauri/permissions/autogenerated/core_request.toml new file mode 100644 index 0000000..5140356 --- /dev/null +++ b/frontend/src-tauri/permissions/autogenerated/core_request.toml @@ -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"] diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index d6bd922..ea6ef21 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -6,6 +6,7 @@ use notesagent_host::recent::{RecentVault, RecentVaultStore}; use notesagent_host::workspace::{Document, Entry, Workspace}; use std::path::Path; use std::sync::Mutex; +use std::time::Duration; use tauri::{Emitter, Manager, State}; #[derive(Default)] @@ -38,7 +39,84 @@ fn with_workspace( #[tauri::command] 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 { + 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, + authorization: Option, +) -> Result { + 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] @@ -197,6 +275,7 @@ fn main() { }) .invoke_handler(tauri::generate_handler![ host_capabilities, + core_request, editor_capabilities, workspace_choose, workspace_open, diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index d5d2d23..bf4cdd1 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -12,7 +12,7 @@ "app": { "windows": [{"label": "main", "title": "NotesAgent Preview", "width": 1200, "height": 800, "minWidth": 640, "minHeight": 480, "decorations": false}], "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"] } }, diff --git a/frontend/src/features/vault/VaultEntry.vue b/frontend/src/features/vault/VaultEntry.vue index bcb5d15..fd862c5 100644 --- a/frontend/src/features/vault/VaultEntry.vue +++ b/frontend/src/features/vault/VaultEntry.vue @@ -72,7 +72,7 @@ async function openFolderPicker() {

{{ t('正在打开知识库…', 'Opening knowledge base…') }}

{{ t('选择知识库', 'Select Knowledge Base') }}

-

{{ 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') }}

+

{{ 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') }}

diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts index 19b85bc..7d97f61 100644 --- a/frontend/src/services/apiClient.ts +++ b/frontend/src/services/apiClient.ts @@ -1,8 +1,10 @@ import type { ApiError, ErrorResponse } from '@/contracts' -import { isDesktop } from './platform/desktop' +import { hostInvoke, isDesktop } from './platform/desktop' // 所有 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 { if (/^https?:\/\//i.test(path)) return path @@ -28,7 +30,6 @@ export class ApiErrorClass extends Error { } async function request(path: string, options: RequestOptions = {}): Promise { - if (isDesktop()) throw new ApiErrorClass('CORE_UNAVAILABLE', '桌面 AI Core 尚未接通;本地编辑可继续。') const { params, token, headers, timeoutMs, ...rest } = options const controller = timeoutMs ? new AbortController() : null let timedOut = false @@ -61,6 +62,22 @@ async function request(path: string, options: RequestOptions = {}): Promise('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, { ...rest, signal: controller?.signal ?? rest.signal,