From 689d65f9ae6704cdad22b9a93f20009227ab9635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E6=A3=AE?= Date: Fri, 12 Jun 2026 15:45:07 +0800 Subject: [PATCH] feat: add tauri sidecar agent integration --- .gitignore | 3 + agent/REAMD.md | 19 +++ agent/go.mod | 3 + agent/main.go | 58 ++++++++ package-lock.json | 11 ++ package.json | 1 + src-tauri/.gitignore | 4 + src-tauri/Cargo.lock | 206 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 3 +- src-tauri/build.rs | 2 +- src-tauri/capabilities/default.json | 7 +- src-tauri/src/lib.rs | 186 +++++++++++++++++++++++-- src-tauri/src/main.rs | 2 +- src-tauri/tauri.conf.json | 12 +- src/App.jsx | 2 +- src/components/MainShell.jsx | 2 +- src/components/Terminal.jsx | 49 +++++-- src/components/TitleBar.jsx | 14 +- src/index.css | 46 ++++--- src/pages/ClonePage.jsx | 59 +++++++- src/pages/DistillPage.jsx | 6 +- src/pages/KnowledgePage.jsx | 6 +- src/pages/SkillsPage.jsx | 4 +- src/utils/navigation.js | 32 +++++ src/windows/SecondaryWindow.jsx | 4 +- 25 files changed, 673 insertions(+), 68 deletions(-) create mode 100644 agent/REAMD.md create mode 100644 agent/go.mod create mode 100644 agent/main.go diff --git a/.gitignore b/.gitignore index 6f2b9f3..3327715 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,8 @@ dist/ src-tauri/target/ src-tauri/gen/ +agent/agent +agent/agent-* + .DS_Store *.log diff --git a/agent/REAMD.md b/agent/REAMD.md new file mode 100644 index 0000000..2970883 --- /dev/null +++ b/agent/REAMD.md @@ -0,0 +1,19 @@ +## golang 智能体 + +### 需要读取制定文件的md提示词、知识库、skill技能、员工蒸馏文件、历史回话记录 + +### 接入兼容OpenAI协议的模型 + +### 接入tts模型,支持声音克隆与用户语音通话 + +### 接入asr模型。支持实时语音转文字 + +### 接入多模态模型,支持识别用户发来的视频、图片、文档等信息 + +### 支持多agent写作,主agent调度 +- 对话agent +- 任务agent +- 记忆总结agent +- 多模态agent +- 语音agent +- 视频agent \ No newline at end of file diff --git a/agent/go.mod b/agent/go.mod new file mode 100644 index 0000000..edaf2d3 --- /dev/null +++ b/agent/go.mod @@ -0,0 +1,3 @@ +module agent + +go 1.26.1 diff --git a/agent/main.go b/agent/main.go new file mode 100644 index 0000000..bffde15 --- /dev/null +++ b/agent/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/json" + "fmt" + "sync" + "time" +) + +var waitGroup sync.WaitGroup + +type logEntry struct { + Level string `json:"level"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` +} + +// 创建一个函数,启动一个协程,每隔600ms输出一次日志信息到命令行界面 +func logToConsole() { + defer waitGroup.Done() + for { + time.Sleep(600 * time.Millisecond) + fmt.Println(randomLogMsg()) + } +} + +// 生成随机日志信息,日志信息从以下列表中随机选择 日志格式为json格式,格式{"level": "info", "message": "正在处理用户请求..."},日志级别有info、debug、error,warning, think五种,日志信息有正在处理用户请求...、正在连接服务器...、正在加载数据...、正在分析用户输入...、正在生成回复...、正在优化模型...、正在更新知识库...等 +func randomLogMsg() string { + logMsgs := []logEntry{ + {Level: "info", Message: "正在处理用户请求..."}, + {Level: "debug", Message: "正在连接服务器..."}, + {Level: "error", Message: "正在加载数据..."}, + {Level: "warning", Message: "正在分析用户输入..."}, + {Level: "think", Message: "正在生成回复..."}, + {Level: "info", Message: "正在优化模型..."}, + {Level: "info", Message: "正在更新知识库..."}, + } + logMsg := logMsgs[time.Now().UnixNano()%int64(len(logMsgs))] + logMsg.Timestamp = time.Now().Format("15:04:05") + + data, err := json.Marshal(logMsg) + if err != nil { + return fmt.Sprintf(`{"level":"error","message":"%s","timestamp":"%s"}`, err.Error(), time.Now().Format("15:04:05")) + } + + return string(data) +} + +func main() { + waitGroup = sync.WaitGroup{} + // 启动日志协程 + waitGroup.Add(1) + go logToConsole() + // 等待协程main执行完成 + time.Sleep(50 * time.Second) + waitGroup.Wait() + +} diff --git a/package-lock.json b/package-lock.json index 1c87178..a046789 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "wechat-ai-ui", "version": "0.1.0", "dependencies": { + "@tauri-apps/api": "^2.11.0", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.20", "lucide-react": "^1.17.0", @@ -1123,6 +1124,16 @@ "win32" ] }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, "node_modules/@tauri-apps/cli": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", diff --git a/package.json b/package.json index 7b4d754..676ef16 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview --host 127.0.0.1" }, "dependencies": { + "@tauri-apps/api": "^2.11.0", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.20", "lucide-react": "^1.17.0", diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index 502406b..75ca48a 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -2,3 +2,7 @@ # will have compiled files and executables /target/ /gen/schemas + +# Go sidecar build outputs copied under src-tauri during local packaging +/agent +/agent-* diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b5d8623..7d9e77c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -85,6 +85,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-log", + "tauri-plugin-shell", ] [[package]] @@ -824,6 +825,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 = "env_filter" version = "0.1.4" @@ -851,6 +861,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" @@ -1646,6 +1666,25 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2202,12 +2241,34 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[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" @@ -2256,6 +2317,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3009,12 +3076,54 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[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" @@ -3419,6 +3528,27 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "tauri-runtime" version = "2.11.2" @@ -4480,6 +4610,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4513,13 +4652,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -4550,6 +4706,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4562,6 +4724,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4574,12 +4742,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4592,6 +4772,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4604,6 +4790,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4616,6 +4808,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4628,6 +4826,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 904d09e..1f23f7a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,5 +21,6 @@ tauri-build = { version = "2.6.2", features = [] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" -tauri = { version = "2.11.2", features = [] } +tauri = { version = "2.11.2", features = ["macos-private-api"] } tauri-plugin-log = "2" +tauri-plugin-shell = "2.3.5" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 795b9b7..d860e1e 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c135d7f..1f51888 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -3,9 +3,12 @@ "identifier": "default", "description": "enables the default permissions", "windows": [ - "main" + "main", + "popup-*" ], "permissions": [ - "core:default" + "core:default", + "core:window:allow-close", + "core:window:allow-start-dragging" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9c3118c..8265087 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,16 +1,176 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; +use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; +use tauri_plugin_shell::{ + process::{CommandChild, CommandEvent}, + ShellExt, +}; + +struct AgentProcess(Mutex>); + +#[derive(Clone, Deserialize, Serialize)] +struct AgentLog { + level: String, + message: String, + timestamp: Option, +} + +fn emit_agent_log(app: &tauri::AppHandle, level: &str, message: impl Into) { + let _ = app.emit( + "agent-log", + AgentLog { + level: level.to_string(), + message: message.into(), + timestamp: None, + }, + ); +} + +fn emit_agent_line(app: &tauri::AppHandle, fallback_level: &str, line: String) { + if let Ok(log) = serde_json::from_str::(&line) { + let _ = app.emit("agent-log", log); + return; + } + + emit_agent_log(app, fallback_level, line); +} + +#[tauri::command] +fn start_agent(app: tauri::AppHandle, process: tauri::State) -> Result<(), String> { + let mut process_guard = process.0.lock().map_err(|error| error.to_string())?; + + if process_guard.is_some() { + emit_agent_log(&app, "warning", "agent already running"); + return Ok(()); + } + + let (mut rx, child) = app + .shell() + .sidecar("agent") + .map_err(|error| error.to_string())? + .spawn() + .map_err(|error| error.to_string())?; + + *process_guard = Some(child); + emit_agent_log(&app, "info", "agent started"); + + let app_handle = app.clone(); + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => { + emit_agent_line( + &app_handle, + "info", + String::from_utf8_lossy(&line).trim().to_string(), + ); + } + CommandEvent::Stderr(line) => { + emit_agent_line( + &app_handle, + "error", + String::from_utf8_lossy(&line).trim().to_string(), + ); + } + CommandEvent::Error(error) => { + emit_agent_log(&app_handle, "error", error); + } + CommandEvent::Terminated(payload) => { + emit_agent_log( + &app_handle, + "warning", + format!("agent exited with code {:?}", payload.code), + ); + + if let Ok(mut process_guard) = app_handle.state::().0.lock() { + *process_guard = None; + } + + break; + } + _ => {} + } + } + }); + + Ok(()) +} + +#[tauri::command] +fn stop_agent(app: tauri::AppHandle, process: tauri::State) -> Result<(), String> { + let mut process_guard = process.0.lock().map_err(|error| error.to_string())?; + + if let Some(child) = process_guard.take() { + child.kill().map_err(|error| error.to_string())?; + emit_agent_log(&app, "warning", "agent stopped"); + } else { + emit_agent_log(&app, "warning", "agent is not running"); + } + + Ok(()) +} + +#[tauri::command] +fn open_popup_window(app: tauri::AppHandle, route: String) -> Result<(), String> { + let route = if route.starts_with('/') { + route + } else { + format!("/{route}") + }; + let label = format!("popup-{}", route.trim_start_matches('/').replace('/', "-")); + + if let Some(window) = app.get_webview_window(&label) { + window.set_focus().map_err(|error| error.to_string())?; + return Ok(()); + } + + let title = match route.as_str() { + "/window/settings" => "设置", + "/window/engine-logs" => "分身引擎日志", + "/window/knowledge-create" => "新增知识库", + "/window/knowledge-detail" => "知识库详情", + "/window/skill-create" => "skill技能详情", + "/window/employee-detail" => "员工蒸馏详情", + "/window/distill-start" => "开始蒸馏", + _ => "二级窗口", + }; + + WebviewWindowBuilder::new( + &app, + label, + WebviewUrl::App(format!("index.html#{route}").into()), + ) + .title(title) + .inner_size(1080.0, 800.0) + .decorations(false) + .transparent(true) + .resizable(true) + .build() + .map_err(|error| error.to_string())?; + + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() - .setup(|app| { - if cfg!(debug_assertions) { - app.handle().plugin( - tauri_plugin_log::Builder::default() - .level(log::LevelFilter::Info) - .build(), - )?; - } - Ok(()) - }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + tauri::Builder::default() + .manage(AgentProcess(Mutex::new(None))) + .plugin(tauri_plugin_shell::init()) + .setup(|app| { + if cfg!(debug_assertions) { + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + open_popup_window, + start_agent, + stop_agent + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ad5fe83..69c3a72 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - app_lib::run(); + app_lib::run(); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c54e460..4fb46b5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -10,11 +10,16 @@ "beforeBuildCommand": "npm run build" }, "app": { + "macOSPrivateApi": true, "windows": [ { "title": "微信AI助手", - "width": 800, - "height": 600, + "width": 500, + "height": 900, + "minWidth": 500, + "minHeight": 900, + "decorations": false, + "transparent": true, "resizable": true, "fullscreen": false } @@ -26,6 +31,9 @@ "bundle": { "active": true, "targets": "all", + "externalBin": [ + "../agent/agent" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/App.jsx b/src/App.jsx index f4fdcb0..1b137c7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,7 +24,7 @@ function App() { const isWindowRoute = route.startsWith("/window/"); return ( -
+
{isWindowRoute ? ( +
{content}
diff --git a/src/components/Terminal.jsx b/src/components/Terminal.jsx index 7082135..8a883e7 100644 --- a/src/components/Terminal.jsx +++ b/src/components/Terminal.jsx @@ -1,27 +1,58 @@ -export default function Terminal({ title, lines, tall = false }) { +import { useEffect, useRef } from "react"; + +export default function Terminal({ title, lines, tall = false, onClear }) { + const scrollRef = useRef(null); + + useEffect(() => { + if (!scrollRef.current) return; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + }, [lines.length]); + const levelClass = { + debug: "terminal-line-debug", info: "terminal-line-info", error: "terminal-line-error", + think: "terminal-line-think", warning: "terminal-line-warning", waring: "terminal-line-warning", }; + const levelLabel = { + debug: "调试", + error: "错误", + info: "日志", + think: "思考", + warning: "警告", + waring: "警告", + }; + return (

{title}

- 清理日志 +
-
+
{lines.length ? ( - lines.map((line, index) => { - const level = typeof line === "string" ? "info" : line.level; - const message = typeof line === "string" ? line : line.message; +
+ {lines.map((line, index) => { + const level = typeof line === "string" ? "info" : line.level; + const message = typeof line === "string" ? line : line.message; + const timestamp = typeof line === "string" ? null : line.timestamp; - return

{message}

; - }) + return ( +

+ {timestamp ? {timestamp} : null} + [{levelLabel[level] ?? level}] + {message} +

+ ); + })} +
) : ( -

引擎未启动

+
+

引擎未启动

+
)}
diff --git a/src/components/TitleBar.jsx b/src/components/TitleBar.jsx index 1014033..0d593ae 100644 --- a/src/components/TitleBar.jsx +++ b/src/components/TitleBar.jsx @@ -1,13 +1,19 @@ import { LogOut, RotateCcw } from "lucide-react"; import ThemeSwitch from "./ThemeSwitch"; -import { go } from "../utils/navigation"; +import { closeWindow, startWindowDrag } from "../utils/navigation"; export default function TitleBar({ title, theme, setTheme, compact = false }) { return (
-
+
{ + if (event.button === 0) startWindowDrag(); + }} + >
-

{title}

+

{title}

@@ -18,7 +24,7 @@ export default function TitleBar({ title, theme, setTheme, compact = false }) { ) : ( - + )}
diff --git a/src/index.css b/src/index.css index 473838e..42ea1f4 100644 --- a/src/index.css +++ b/src/index.css @@ -52,14 +52,17 @@ html, body, #root { min-height: 100%; + background: transparent; + overflow: hidden; + overscroll-behavior: none; } body { margin: 0; - background: - radial-gradient(circle at 12% 10%, rgba(7, 193, 96, 0.11), transparent 30%), - radial-gradient(circle at 86% 4%, rgba(45, 140, 255, 0.1), transparent 24%), - var(--bg); + background: transparent; + position: fixed; + width: 100%; + height: 100%; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; letter-spacing: 0; } @@ -74,23 +77,27 @@ button { cursor: pointer; } +.scrollbar-hidden { + scrollbar-width: none; + -ms-overflow-style: none; +} + +.scrollbar-hidden::-webkit-scrollbar { + display: none; +} + @layer components { .app-window { position: relative; - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.54), - inset 0 -1px 0 rgba(128, 128, 128, 0.08), - var(--shadow); background: - radial-gradient(circle at 16% 4%, rgba(7, 193, 96, 0.12), transparent 28%), - radial-gradient(circle at 86% 2%, rgba(45, 140, 255, 0.11), transparent 24%), - radial-gradient(circle at 50% 36%, rgba(7, 193, 96, 0.08), transparent 30%), - linear-gradient(180deg, color-mix(in srgb, var(--page) 68%, transparent), color-mix(in srgb, var(--page) 82%, transparent) 72%); + radial-gradient(circle at 16% 4%, rgba(7, 193, 96, 0.18), transparent 30%), + radial-gradient(circle at 86% 2%, rgba(45, 140, 255, 0.16), transparent 26%), + radial-gradient(circle at 50% 36%, rgba(7, 193, 96, 0.13), transparent 32%), + linear-gradient(180deg, color-mix(in srgb, var(--page) 58%, transparent), color-mix(in srgb, var(--page) 76%, transparent) 72%); } html[data-theme="dark"] .app-window { box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 -1px 0 rgba(255, 255, 255, 0.04), var(--shadow); } @@ -101,7 +108,6 @@ button { backdrop-filter: blur(10px) saturate(1.35); -webkit-backdrop-filter: blur(28px) saturate(1.35); box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.36), 0 10px 28px rgba(20, 36, 27, 0.06); } @@ -194,7 +200,7 @@ button { } .status { - @apply inline-flex h-6 items-center rounded-full px-2.5 text-[11px] font-black; + @apply inline-flex h-6 items-center rounded-full px-2.5 text-[11px] font-medium; } .status-success { @@ -224,6 +230,14 @@ button { color: #8ff0b3; } + .terminal-line-debug { + color: #82c7ff; + } + + .terminal-line-think { + color: #c9a7ff; + } + .terminal-line-warning { color: #ffd166; } @@ -285,7 +299,7 @@ button { } .fab { - @apply fixed bottom-24 right-[max(24px,calc((100vw-750px)/2+24px))] grid h-14 w-14 place-items-center rounded-full bg-primary text-[30px] font-light leading-none text-white shadow-[0_18px_36px_rgba(7,193,96,.28)] transition hover:bg-[#05964d]; + @apply fixed bottom-24 right-6 grid h-14 w-14 place-items-center rounded-full bg-primary text-[30px] font-light leading-none text-white shadow-[0_18px_36px_rgba(7,193,96,.28)] transition hover:bg-[#05964d]; } .input-like { diff --git a/src/pages/ClonePage.jsx b/src/pages/ClonePage.jsx index cbba027..79c0b2b 100644 --- a/src/pages/ClonePage.jsx +++ b/src/pages/ClonePage.jsx @@ -1,12 +1,59 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; import { Power, PowerOff, Settings } from "lucide-react"; -import { logs } from "../data/mockData"; import StatusPill from "../components/StatusPill"; import Terminal from "../components/Terminal"; -import { go } from "../utils/navigation"; +import { openWindow } from "../utils/navigation"; export default function ClonePage() { const [engineEnabled, setEngineEnabled] = useState(false); + const [agentLogs, setAgentLogs] = useState([]); + + useEffect(() => { + if (!window.__TAURI_INTERNALS__) return undefined; + + let unlisten; + let cancelled = false; + + listen("agent-log", (event) => { + setAgentLogs((currentLogs) => [...currentLogs.slice(-300), event.payload]); + }).then((cleanup) => { + if (cancelled) { + cleanup(); + return; + } + + unlisten = cleanup; + }); + + return () => { + cancelled = true; + if (unlisten) unlisten(); + }; + }, []); + + async function toggleAgent() { + if (!window.__TAURI_INTERNALS__) { + setEngineEnabled((enabled) => !enabled); + return; + } + + try { + if (engineEnabled) { + await invoke("stop_agent"); + setEngineEnabled(false); + } else { + await invoke("start_agent"); + setEngineEnabled(true); + } + } catch (error) { + setAgentLogs((currentLogs) => [ + ...currentLogs, + { level: "error", message: String(error) }, + ]); + } + } return (
@@ -22,15 +69,15 @@ export default function ClonePage() {
- +
- + setAgentLogs([])} /> ); } diff --git a/src/pages/DistillPage.jsx b/src/pages/DistillPage.jsx index 45cb234..47fa372 100644 --- a/src/pages/DistillPage.jsx +++ b/src/pages/DistillPage.jsx @@ -1,5 +1,5 @@ import { employeeItems } from "../data/mockData"; -import { go } from "../utils/navigation"; +import { openWindow } from "../utils/navigation"; export default function DistillPage() { return ( @@ -9,7 +9,7 @@ export default function DistillPage() {

导入聊天记录、话术和案例,沉淀可授权调用的员工能力。

- +
@@ -30,7 +30,7 @@ export default function DistillPage() {
{employeeItems.map((item) => ( -
go("/window/employee-detail")}> +
openWindow("/window/employee-detail")}>

{item.name}

diff --git a/src/pages/KnowledgePage.jsx b/src/pages/KnowledgePage.jsx index 84b2d05..6d84e46 100644 --- a/src/pages/KnowledgePage.jsx +++ b/src/pages/KnowledgePage.jsx @@ -2,7 +2,7 @@ import { knowledgeItems } from "../data/mockData"; import DraggableFab from "../components/DraggableFab"; import Segmented from "../components/Segmented"; import StatusPill from "../components/StatusPill"; -import { go } from "../utils/navigation"; +import { openWindow } from "../utils/navigation"; export default function KnowledgePage({ active, setActive }) { const types = ["全部", "视频", "图片", "文件", "案例", "其他"]; @@ -13,7 +13,7 @@ export default function KnowledgePage({ active, setActive }) {
{filtered.map((item) => ( -
go("/window/knowledge-detail")}> +
openWindow("/window/knowledge-detail")}>

{item.title}

@@ -27,7 +27,7 @@ export default function KnowledgePage({ active, setActive }) {
))}
- go("/window/knowledge-create")} ariaLabel="新增知识库" /> + openWindow("/window/knowledge-create")} ariaLabel="新增知识库" /> ); } diff --git a/src/pages/SkillsPage.jsx b/src/pages/SkillsPage.jsx index 22c0f16..f9e5c48 100644 --- a/src/pages/SkillsPage.jsx +++ b/src/pages/SkillsPage.jsx @@ -1,7 +1,7 @@ import { skillItems, tagToneClasses } from "../data/mockData"; import DraggableFab from "../components/DraggableFab"; import Segmented from "../components/Segmented"; -import { go } from "../utils/navigation"; +import { openWindow } from "../utils/navigation"; export default function SkillsPage({ active, setActive }) { const types = ["全部", "内置", "自定义"]; @@ -31,7 +31,7 @@ export default function SkillsPage({ active, setActive }) {
))}
- go("/window/skill-create")} ariaLabel="新增 skill" /> + openWindow("/window/skill-create")} ariaLabel="新增 skill" /> ); } diff --git a/src/utils/navigation.js b/src/utils/navigation.js index 05841b8..b3bfb55 100644 --- a/src/utils/navigation.js +++ b/src/utils/navigation.js @@ -1,3 +1,35 @@ +import { invoke } from "@tauri-apps/api/core"; +import { getCurrentWindow } from "@tauri-apps/api/window"; + export function go(path) { window.location.hash = path === "/" ? "" : path; } + +export async function openWindow(path) { + if (!window.__TAURI_INTERNALS__) { + go(path); + return; + } + + try { + await invoke("open_popup_window", { route: path }); + } catch (error) { + console.error("Failed to open popup window", error); + go(path); + } +} + +export async function closeWindow() { + if (!window.__TAURI_INTERNALS__) { + go("/"); + return; + } + + await getCurrentWindow().close(); +} + +export async function startWindowDrag() { + if (!window.__TAURI_INTERNALS__) return; + + await getCurrentWindow().startDragging(); +} diff --git a/src/windows/SecondaryWindow.jsx b/src/windows/SecondaryWindow.jsx index 8e28548..ab5f40b 100644 --- a/src/windows/SecondaryWindow.jsx +++ b/src/windows/SecondaryWindow.jsx @@ -18,8 +18,8 @@ export default function SecondaryWindow({ route, theme, setTheme, settingSection }, [route]); return ( -
- +
+
{route.includes("settings") ? (