feat: add tauri sidecar agent integration

This commit is contained in:
大森 2026-06-12 15:45:07 +08:00
parent 606fa585bf
commit 689d65f9ae
25 changed files with 673 additions and 68 deletions

3
.gitignore vendored
View File

@ -5,5 +5,8 @@ dist/
src-tauri/target/ src-tauri/target/
src-tauri/gen/ src-tauri/gen/
agent/agent
agent/agent-*
.DS_Store .DS_Store
*.log *.log

19
agent/REAMD.md Normal file
View File

@ -0,0 +1,19 @@
## golang 智能体
### 需要读取制定文件的md提示词、知识库、skill技能、员工蒸馏文件、历史回话记录
### 接入兼容OpenAI协议的模型
### 接入tts模型支持声音克隆与用户语音通话
### 接入asr模型。支持实时语音转文字
### 接入多模态模型,支持识别用户发来的视频、图片、文档等信息
### 支持多agent写作主agent调度
- 对话agent
- 任务agent
- 记忆总结agent
- 多模态agent
- 语音agent
- 视频agent

3
agent/go.mod Normal file
View File

@ -0,0 +1,3 @@
module agent
go 1.26.1

58
agent/main.go Normal file
View File

@ -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()
}

11
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "wechat-ai-ui", "name": "wechat-ai-ui",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.0",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"lucide-react": "^1.17.0", "lucide-react": "^1.17.0",
@ -1123,6 +1124,16 @@
"win32" "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": { "node_modules/@tauri-apps/cli": {
"version": "2.11.2", "version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",

View File

@ -10,6 +10,7 @@
"preview": "vite preview --host 127.0.0.1" "preview": "vite preview --host 127.0.0.1"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.0",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"lucide-react": "^1.17.0", "lucide-react": "^1.17.0",

View File

@ -2,3 +2,7 @@
# will have compiled files and executables # will have compiled files and executables
/target/ /target/
/gen/schemas /gen/schemas
# Go sidecar build outputs copied under src-tauri during local packaging
/agent
/agent-*

206
src-tauri/Cargo.lock generated
View File

@ -85,6 +85,7 @@ dependencies = [
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-log", "tauri-plugin-log",
"tauri-plugin-shell",
] ]
[[package]] [[package]]
@ -824,6 +825,15 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" 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]] [[package]]
name = "env_filter" name = "env_filter"
version = "0.1.4" version = "0.1.4"
@ -851,6 +861,16 @@ dependencies = [
"typeid", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.4.1" version = "2.4.1"
@ -1646,6 +1666,25 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" 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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
@ -2202,12 +2241,34 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" 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]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 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]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@ -2256,6 +2317,12 @@ dependencies = [
"windows-link 0.2.1", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -3009,12 +3076,54 @@ dependencies = [
"digest", "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]] [[package]]
name = "shlex" name = "shlex"
version = "2.0.1" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" 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]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.9" version = "0.3.9"
@ -3419,6 +3528,27 @@ dependencies = [
"time", "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]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.11.2" version = "2.11.2"
@ -4480,6 +4610,15 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@ -4513,13 +4652,30 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6", "windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6", "windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 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_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6", "windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6", "windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 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]] [[package]]
name = "windows-threading" name = "windows-threading"
version = "0.1.0" version = "0.1.0"
@ -4550,6 +4706,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.42.2" version = "0.42.2"
@ -4562,6 +4724,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.42.2" version = "0.42.2"
@ -4574,12 +4742,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]] [[package]]
name = "windows_i686_gnullvm" name = "windows_i686_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.42.2" version = "0.42.2"
@ -4592,6 +4772,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.42.2" version = "0.42.2"
@ -4604,6 +4790,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 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]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.42.2" version = "0.42.2"
@ -4616,6 +4808,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 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]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.42.2" version = "0.42.2"
@ -4628,6 +4826,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 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]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.40" version = "0.5.40"

View File

@ -21,5 +21,6 @@ tauri-build = { version = "2.6.2", features = [] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
log = "0.4" log = "0.4"
tauri = { version = "2.11.2", features = [] } tauri = { version = "2.11.2", features = ["macos-private-api"] }
tauri-plugin-log = "2" tauri-plugin-log = "2"
tauri-plugin-shell = "2.3.5"

View File

@ -3,9 +3,12 @@
"identifier": "default", "identifier": "default",
"description": "enables the default permissions", "description": "enables the default permissions",
"windows": [ "windows": [
"main" "main",
"popup-*"
], ],
"permissions": [ "permissions": [
"core:default" "core:default",
"core:window:allow-close",
"core:window:allow-start-dragging"
] ]
} }

View File

@ -1,6 +1,161 @@
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<Option<CommandChild>>);
#[derive(Clone, Deserialize, Serialize)]
struct AgentLog {
level: String,
message: String,
timestamp: Option<String>,
}
fn emit_agent_log(app: &tauri::AppHandle, level: &str, message: impl Into<String>) {
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::<AgentLog>(&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<AgentProcess>) -> 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::<AgentProcess>().0.lock() {
*process_guard = None;
}
break;
}
_ => {}
}
}
});
Ok(())
}
#[tauri::command]
fn stop_agent(app: tauri::AppHandle, process: tauri::State<AgentProcess>) -> 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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.manage(AgentProcess(Mutex::new(None)))
.plugin(tauri_plugin_shell::init())
.setup(|app| { .setup(|app| {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
app.handle().plugin( app.handle().plugin(
@ -11,6 +166,11 @@ pub fn run() {
} }
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![
open_popup_window,
start_agent,
stop_agent
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -10,11 +10,16 @@
"beforeBuildCommand": "npm run build" "beforeBuildCommand": "npm run build"
}, },
"app": { "app": {
"macOSPrivateApi": true,
"windows": [ "windows": [
{ {
"title": "微信AI助手", "title": "微信AI助手",
"width": 800, "width": 500,
"height": 600, "height": 900,
"minWidth": 500,
"minHeight": 900,
"decorations": false,
"transparent": true,
"resizable": true, "resizable": true,
"fullscreen": false "fullscreen": false
} }
@ -26,6 +31,9 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"externalBin": [
"../agent/agent"
],
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",

View File

@ -24,7 +24,7 @@ function App() {
const isWindowRoute = route.startsWith("/window/"); const isWindowRoute = route.startsWith("/window/");
return ( return (
<div className="min-h-dvh px-4 py-4 text-text sm:px-6"> <div className="min-h-dvh overflow-hidden rounded-[12px] bg-page text-text">
{isWindowRoute ? ( {isWindowRoute ? (
<SecondaryWindow <SecondaryWindow
route={route} route={route}

View File

@ -16,7 +16,7 @@ export default function MainShell(props) {
}[props.activeTab]; }[props.activeTab];
return ( return (
<div className="app-window mx-auto flex min-h-[calc(100dvh-32px)] w-full max-w-[750px] flex-col overflow-hidden rounded-[10px] border border-line shadow-panel"> <div className="app-window flex h-dvh w-full flex-col overflow-hidden rounded-[12px]">
<TitleBar title={activeTitle} theme={props.theme} setTheme={props.setTheme} /> <TitleBar title={activeTitle} theme={props.theme} setTheme={props.setTheme} />
<main className="relative flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-5">{content}</main> <main className="relative flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-5">{content}</main>
<BottomTabs activeTab={props.activeTab} setActiveTab={props.setActiveTab} /> <BottomTabs activeTab={props.activeTab} setActiveTab={props.setActiveTab} />

View File

@ -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 = { const levelClass = {
debug: "terminal-line-debug",
info: "terminal-line-info", info: "terminal-line-info",
error: "terminal-line-error", error: "terminal-line-error",
think: "terminal-line-think",
warning: "terminal-line-warning", warning: "terminal-line-warning",
waring: "terminal-line-warning", waring: "terminal-line-warning",
}; };
const levelLabel = {
debug: "调试",
error: "错误",
info: "日志",
think: "思考",
warning: "警告",
waring: "警告",
};
return ( return (
<div className={`terminal ${tall ? "basis-[650px]" : "basis-[250px]"} flex min-h-0 flex-1 flex-col`}> <div className={`terminal ${tall ? "basis-[650px]" : "basis-[250px]"} flex min-h-0 flex-1 flex-col`}>
<div className="mb-3 flex shrink-0 items-center justify-between"> <div className="mb-3 flex shrink-0 items-center justify-between">
<h3 className="text-[13px] font-black text-[#d7ffe4]">{title}</h3> <h3 className="text-[13px] font-black text-[#d7ffe4]">{title}</h3>
<span className="cursor-pointer text-[11px] font-bold text-[#7ea58b]">清理日志</span> <button className="cursor-pointer border-0 bg-transparent p-0 text-[11px] font-bold text-[#7ea58b]" onClick={onClear}>清理日志</button>
</div> </div>
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto font-mono text-[12px] leading-5"> <div ref={scrollRef} className="scrollbar-hidden relative min-h-0 flex-1 overflow-y-auto font-mono text-[12px] leading-5">
{lines.length ? ( {lines.length ? (
lines.map((line, index) => { <div className="space-y-2">
{lines.map((line, index) => {
const level = typeof line === "string" ? "info" : line.level; const level = typeof line === "string" ? "info" : line.level;
const message = typeof line === "string" ? line : line.message; const message = typeof line === "string" ? line : line.message;
const timestamp = typeof line === "string" ? null : line.timestamp;
return <p key={`${message}-${index}`} className={levelClass[level] ?? levelClass.info}>{message}</p>; return (
}) <p key={`${timestamp ?? ""}-${message}-${index}`} className="text-[#d7ffe4]">
{timestamp ? <span className="text-[#6f8978]">{timestamp} </span> : null}
<span className={levelClass[level] ?? levelClass.info}>[{levelLabel[level] ?? level}] </span>
{message}
</p>
);
})}
</div>
) : ( ) : (
<p className="terminal-line-info">引擎未启动</p> <div className="absolute inset-0 grid place-items-center">
<p className="text-[16px] font-medium tracking-[0.18em] text-[#d7ffe4]">引擎未启动</p>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -1,13 +1,19 @@
import { LogOut, RotateCcw } from "lucide-react"; import { LogOut, RotateCcw } from "lucide-react";
import ThemeSwitch from "./ThemeSwitch"; import ThemeSwitch from "./ThemeSwitch";
import { go } from "../utils/navigation"; import { closeWindow, startWindowDrag } from "../utils/navigation";
export default function TitleBar({ title, theme, setTheme, compact = false }) { export default function TitleBar({ title, theme, setTheme, compact = false }) {
return ( return (
<header className="glass-bar relative z-10 flex items-center justify-between gap-4 border-b border-line px-5 py-2"> <header className="glass-bar relative z-10 flex items-center justify-between gap-4 border-b border-line px-5 py-2">
<div className="flex min-w-0 items-center gap-3"> <div
data-tauri-drag-region
className="flex min-w-0 flex-1 cursor-pointer select-none items-center gap-3 self-stretch"
onPointerDown={(event) => {
if (event.button === 0) startWindowDrag();
}}
>
<div className="min-w-0"> <div className="min-w-0">
<h1 className="truncate text-[18px] font-extrabold leading-tight">{title}</h1> <h1 data-tauri-drag-region className="truncate text-[18px] font-medium leading-tight">{title}</h1>
</div> </div>
</div> </div>
<div className="flex shrink-0 items-center gap-5"> <div className="flex shrink-0 items-center gap-5">
@ -18,7 +24,7 @@ export default function TitleBar({ title, theme, setTheme, compact = false }) {
<button className="icon-btn" aria-label="退出"><LogOut size={16} strokeWidth={2.5} /></button> <button className="icon-btn" aria-label="退出"><LogOut size={16} strokeWidth={2.5} /></button>
</> </>
) : ( ) : (
<button className="btn-secondary h-9 px-3" onClick={() => go("/")}>关闭</button> <button className="btn-secondary h-9 px-3" onClick={closeWindow}>关闭</button>
)} )}
</div> </div>
</header> </header>

View File

@ -52,14 +52,17 @@ html,
body, body,
#root { #root {
min-height: 100%; min-height: 100%;
background: transparent;
overflow: hidden;
overscroll-behavior: none;
} }
body { body {
margin: 0; margin: 0;
background: background: transparent;
radial-gradient(circle at 12% 10%, rgba(7, 193, 96, 0.11), transparent 30%), position: fixed;
radial-gradient(circle at 86% 4%, rgba(45, 140, 255, 0.1), transparent 24%), width: 100%;
var(--bg); height: 100%;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
letter-spacing: 0; letter-spacing: 0;
} }
@ -74,23 +77,27 @@ button {
cursor: pointer; cursor: pointer;
} }
.scrollbar-hidden {
scrollbar-width: none;
-ms-overflow-style: none;
}
.scrollbar-hidden::-webkit-scrollbar {
display: none;
}
@layer components { @layer components {
.app-window { .app-window {
position: relative; 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: background:
radial-gradient(circle at 16% 4%, rgba(7, 193, 96, 0.12), transparent 28%), 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.11), transparent 24%), 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.08), transparent 30%), radial-gradient(circle at 50% 36%, rgba(7, 193, 96, 0.13), transparent 32%),
linear-gradient(180deg, color-mix(in srgb, var(--page) 68%, transparent), color-mix(in srgb, var(--page) 82%, transparent) 72%); 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 { html[data-theme="dark"] .app-window {
box-shadow: box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 -1px 0 rgba(255, 255, 255, 0.04), inset 0 -1px 0 rgba(255, 255, 255, 0.04),
var(--shadow); var(--shadow);
} }
@ -101,7 +108,6 @@ button {
backdrop-filter: blur(10px) saturate(1.35); backdrop-filter: blur(10px) saturate(1.35);
-webkit-backdrop-filter: blur(28px) saturate(1.35); -webkit-backdrop-filter: blur(28px) saturate(1.35);
box-shadow: box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.36),
0 10px 28px rgba(20, 36, 27, 0.06); 0 10px 28px rgba(20, 36, 27, 0.06);
} }
@ -194,7 +200,7 @@ button {
} }
.status { .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 { .status-success {
@ -224,6 +230,14 @@ button {
color: #8ff0b3; color: #8ff0b3;
} }
.terminal-line-debug {
color: #82c7ff;
}
.terminal-line-think {
color: #c9a7ff;
}
.terminal-line-warning { .terminal-line-warning {
color: #ffd166; color: #ffd166;
} }
@ -285,7 +299,7 @@ button {
} }
.fab { .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 { .input-like {

View File

@ -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 { Power, PowerOff, Settings } from "lucide-react";
import { logs } from "../data/mockData";
import StatusPill from "../components/StatusPill"; import StatusPill from "../components/StatusPill";
import Terminal from "../components/Terminal"; import Terminal from "../components/Terminal";
import { go } from "../utils/navigation"; import { openWindow } from "../utils/navigation";
export default function ClonePage() { export default function ClonePage() {
const [engineEnabled, setEngineEnabled] = useState(false); 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 ( return (
<section className="flex min-h-full flex-1 flex-col gap-3"> <section className="flex min-h-full flex-1 flex-col gap-3">
@ -22,15 +69,15 @@ export default function ClonePage() {
<div className="flex w-full items-center justify-center gap-4"> <div className="flex w-full items-center justify-center gap-4">
<button <button
className={`${engineEnabled ? "btn-danger" : "btn-primary"} w-full gap-2`} className={`${engineEnabled ? "btn-danger" : "btn-primary"} w-full gap-2`}
onClick={() => setEngineEnabled((enabled) => !enabled)} onClick={toggleAgent}
> >
{engineEnabled ? <PowerOff size={16} strokeWidth={2.6} /> : <Power size={16} strokeWidth={2.6} />} {engineEnabled ? <PowerOff size={16} strokeWidth={2.6} /> : <Power size={16} strokeWidth={2.6} />}
{engineEnabled ? "停用引擎" : "启动引擎"} {engineEnabled ? "停用引擎" : "启动引擎"}
</button> </button>
<button className="icon-action shrink-0" onClick={() => go("/window/settings")} aria-label="设置"><Settings size={18} strokeWidth={2.5} /></button> <button className="icon-action shrink-0" onClick={() => openWindow("/window/settings")} aria-label="设置"><Settings size={18} strokeWidth={2.5} /></button>
</div> </div>
</div> </div>
<Terminal title="运行日志" lines={logs} /> <Terminal title="运行日志" lines={agentLogs} onClear={() => setAgentLogs([])} />
</section> </section>
); );
} }

View File

@ -1,5 +1,5 @@
import { employeeItems } from "../data/mockData"; import { employeeItems } from "../data/mockData";
import { go } from "../utils/navigation"; import { openWindow } from "../utils/navigation";
export default function DistillPage() { export default function DistillPage() {
return ( return (
@ -9,7 +9,7 @@ export default function DistillPage() {
<p className="mt-3 text-[13px] text-muted">导入聊天记录话术和案例沉淀可授权调用的员工能力</p> <p className="mt-3 text-[13px] text-muted">导入聊天记录话术和案例沉淀可授权调用的员工能力</p>
<div className="mt-5 w-full flex flex-wrap gap-8"> <div className="mt-5 w-full flex flex-wrap gap-8">
<button className="btn-secondary flex-1">蒸馏教程</button> <button className="btn-secondary flex-1">蒸馏教程</button>
<button className="btn-primary flex-1" onClick={() => go("/window/distill-start")}>开始蒸馏</button> <button className="btn-primary flex-1" onClick={() => openWindow("/window/distill-start")}>开始蒸馏</button>
</div> </div>
</div> </div>
<div className="panel rounded-md p-3"> <div className="panel rounded-md p-3">
@ -30,7 +30,7 @@ export default function DistillPage() {
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{employeeItems.map((item) => ( {employeeItems.map((item) => (
<article key={item.name} className="panel rounded-md p-3" onClick={() => go("/window/employee-detail")}> <article key={item.name} className="panel rounded-md p-3" onClick={() => openWindow("/window/employee-detail")}>
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<h3 className="min-w-0 truncate text-[15px] font-extrabold">{item.name}</h3> <h3 className="min-w-0 truncate text-[15px] font-extrabold">{item.name}</h3>
<div className="flex shrink-0 items-center gap-1.5 text-[12px] font-semibold text-muted"> <div className="flex shrink-0 items-center gap-1.5 text-[12px] font-semibold text-muted">

View File

@ -2,7 +2,7 @@ import { knowledgeItems } from "../data/mockData";
import DraggableFab from "../components/DraggableFab"; import DraggableFab from "../components/DraggableFab";
import Segmented from "../components/Segmented"; import Segmented from "../components/Segmented";
import StatusPill from "../components/StatusPill"; import StatusPill from "../components/StatusPill";
import { go } from "../utils/navigation"; import { openWindow } from "../utils/navigation";
export default function KnowledgePage({ active, setActive }) { export default function KnowledgePage({ active, setActive }) {
const types = ["全部", "视频", "图片", "文件", "案例", "其他"]; const types = ["全部", "视频", "图片", "文件", "案例", "其他"];
@ -13,7 +13,7 @@ export default function KnowledgePage({ active, setActive }) {
<Segmented items={types} active={active} onChange={setActive} /> <Segmented items={types} active={active} onChange={setActive} />
<div className="space-y-3"> <div className="space-y-3">
{filtered.map((item) => ( {filtered.map((item) => (
<article key={item.title} className="list-card" onClick={() => go("/window/knowledge-detail")}> <article key={item.title} className="list-card" onClick={() => openWindow("/window/knowledge-detail")}>
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="truncate text-[14px] font-extrabold">{item.title}</h3> <h3 className="truncate text-[14px] font-extrabold">{item.title}</h3>
@ -27,7 +27,7 @@ export default function KnowledgePage({ active, setActive }) {
</article> </article>
))} ))}
</div> </div>
<DraggableFab onClick={() => go("/window/knowledge-create")} ariaLabel="新增知识库" /> <DraggableFab onClick={() => openWindow("/window/knowledge-create")} ariaLabel="新增知识库" />
</section> </section>
); );
} }

View File

@ -1,7 +1,7 @@
import { skillItems, tagToneClasses } from "../data/mockData"; import { skillItems, tagToneClasses } from "../data/mockData";
import DraggableFab from "../components/DraggableFab"; import DraggableFab from "../components/DraggableFab";
import Segmented from "../components/Segmented"; import Segmented from "../components/Segmented";
import { go } from "../utils/navigation"; import { openWindow } from "../utils/navigation";
export default function SkillsPage({ active, setActive }) { export default function SkillsPage({ active, setActive }) {
const types = ["全部", "内置", "自定义"]; const types = ["全部", "内置", "自定义"];
@ -31,7 +31,7 @@ export default function SkillsPage({ active, setActive }) {
</article> </article>
))} ))}
</div> </div>
<DraggableFab onClick={() => go("/window/skill-create")} ariaLabel="新增 skill" /> <DraggableFab onClick={() => openWindow("/window/skill-create")} ariaLabel="新增 skill" />
</section> </section>
); );
} }

View File

@ -1,3 +1,35 @@
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
export function go(path) { export function go(path) {
window.location.hash = path === "/" ? "" : 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();
}

View File

@ -18,8 +18,8 @@ export default function SecondaryWindow({ route, theme, setTheme, settingSection
}, [route]); }, [route]);
return ( return (
<div className="app-window mx-auto flex min-h-[calc(100dvh-32px)] w-full max-w-[750px] flex-col overflow-hidden rounded-[10px] border border-line shadow-panel"> <div className="app-window flex h-dvh w-full flex-col overflow-hidden rounded-[12px]">
<TitleBar title={title} subtitle="750 x 950 Web 窗口预览" theme={theme} setTheme={setTheme} compact /> <TitleBar title={title} theme={theme} setTheme={setTheme} compact />
<main className="content-canvas flex min-h-0 flex-1 flex-col overflow-y-auto"> <main className="content-canvas flex min-h-0 flex-1 flex-col overflow-y-auto">
{route.includes("settings") ? ( {route.includes("settings") ? (
<SettingsWindow active={settingSection} setActive={setSettingSection} /> <SettingsWindow active={settingSection} setActive={setSettingSection} />