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/gen/
agent/agent
agent/agent-*
.DS_Store
*.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",
"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",

View File

@ -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",

View File

@ -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-*

206
src-tauri/Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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"
]
}

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)]
pub fn run() {
tauri::Builder::default()
.manage(AgentProcess(Mutex::new(None)))
.plugin(tauri_plugin_shell::init())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
@ -11,6 +166,11 @@ pub fn run() {
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
open_popup_window,
start_agent,
stop_agent
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -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",

View File

@ -24,7 +24,7 @@ function App() {
const isWindowRoute = route.startsWith("/window/");
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 ? (
<SecondaryWindow
route={route}

View File

@ -16,7 +16,7 @@ export default function MainShell(props) {
}[props.activeTab];
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} />
<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} />

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 = {
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 (
<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">
<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 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.map((line, index) => {
<div className="space-y-2">
{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 <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>

View File

@ -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 (
<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">
<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 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="btn-secondary h-9 px-3" onClick={() => go("/")}>关闭</button>
<button className="btn-secondary h-9 px-3" onClick={closeWindow}>关闭</button>
)}
</div>
</header>

View File

@ -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 {

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 { 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 (
<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">
<button
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 ? "停用引擎" : "启动引擎"}
</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>
<Terminal title="运行日志" lines={logs} />
<Terminal title="运行日志" lines={agentLogs} onClear={() => setAgentLogs([])} />
</section>
);
}

View File

@ -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() {
<p className="mt-3 text-[13px] text-muted">导入聊天记录话术和案例沉淀可授权调用的员工能力</p>
<div className="mt-5 w-full flex flex-wrap gap-8">
<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 className="panel rounded-md p-3">
@ -30,7 +30,7 @@ export default function DistillPage() {
</div>
<div className="space-y-3">
{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">
<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">

View File

@ -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 }) {
<Segmented items={types} active={active} onChange={setActive} />
<div className="space-y-3">
{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="flex items-center gap-2">
<h3 className="truncate text-[14px] font-extrabold">{item.title}</h3>
@ -27,7 +27,7 @@ export default function KnowledgePage({ active, setActive }) {
</article>
))}
</div>
<DraggableFab onClick={() => go("/window/knowledge-create")} ariaLabel="新增知识库" />
<DraggableFab onClick={() => openWindow("/window/knowledge-create")} ariaLabel="新增知识库" />
</section>
);
}

View File

@ -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 }) {
</article>
))}
</div>
<DraggableFab onClick={() => go("/window/skill-create")} ariaLabel="新增 skill" />
<DraggableFab onClick={() => openWindow("/window/skill-create")} ariaLabel="新增 skill" />
</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) {
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]);
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">
<TitleBar title={title} subtitle="750 x 950 Web 窗口预览" theme={theme} setTheme={setTheme} compact />
<div className="app-window flex h-dvh w-full flex-col overflow-hidden rounded-[12px]">
<TitleBar title={title} theme={theme} setTheme={setTheme} compact />
<main className="content-canvas flex min-h-0 flex-1 flex-col overflow-y-auto">
{route.includes("settings") ? (
<SettingsWindow active={settingSection} setActive={setSettingSection} />