feat(client): add Tauri 2.x desktop client with sidecar process management
- Tauri 2.x project scaffold with dual-window (splash + main) - Rust sidecar management: spawn/kill Python backend, port discovery via stdout - CancellationToken for graceful task cancellation on exit - System tray with show/quit, close-to-tray behavior - Frontend: dynamic baseURL, SplashScreen, TitleBar, Tauri IPC adapter - PyInstaller build scripts for cross-platform sidecar packaging - GitHub Actions CI for Win/Mac/Linux release builds - CSP security policy, proper capabilities configuration
This commit is contained in:
parent
321c2333d6
commit
bc43b962c7
|
|
@ -0,0 +1,110 @@
|
|||
name: Release Desktop Client
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- platform: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install -e ".[server]" pyinstaller
|
||||
|
||||
- name: Build Python sidecar
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p src-tauri/binaries
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
python -m PyInstaller --onefile \
|
||||
--name "agentkit-server-${{ matrix.target }}.exe" \
|
||||
--hidden-import agentkit.server \
|
||||
--hidden-import agentkit.server.app \
|
||||
--hidden-import agentkit.server.routes \
|
||||
--hidden-import agentkit.server.config \
|
||||
--hidden-import agentkit.cli.main \
|
||||
--hidden-import uvicorn.logging \
|
||||
--hidden-import uvicorn.lifespan.on \
|
||||
--hidden-import uvicorn.lifespan.off \
|
||||
--hidden-import uvicorn.protocols.websockets.auto \
|
||||
--hidden-import uvicorn.protocols.http.auto \
|
||||
--hidden-import sse_starlette \
|
||||
--distpath src-tauri/binaries \
|
||||
--workpath build/pyinstaller-work \
|
||||
--specpath build \
|
||||
src/agentkit/__main__.py
|
||||
else
|
||||
python -m PyInstaller --onefile \
|
||||
--name "agentkit-server-${{ matrix.target }}" \
|
||||
--hidden-import agentkit.server \
|
||||
--hidden-import agentkit.server.app \
|
||||
--hidden-import agentkit.server.routes \
|
||||
--hidden-import agentkit.server.config \
|
||||
--hidden-import agentkit.cli.main \
|
||||
--hidden-import uvicorn.logging \
|
||||
--hidden-import uvicorn.lifespan.on \
|
||||
--hidden-import uvicorn.lifespan.off \
|
||||
--hidden-import uvicorn.protocols.websockets.auto \
|
||||
--hidden-import uvicorn.protocols.http.auto \
|
||||
--hidden-import sse_starlette \
|
||||
--distpath src-tauri/binaries \
|
||||
--workpath build/pyinstaller-work \
|
||||
--specpath build \
|
||||
src/agentkit/__main__.py
|
||||
fi
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: src/agentkit/server/frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Install Rust dependencies
|
||||
working-directory: src-tauri
|
||||
run: cargo fetch
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: 'Fischer AgentKit ${{ github.ref_name }}'
|
||||
releaseBody: 'See the assets below to download and install.'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
|
|
@ -23,3 +23,18 @@ htmlcov/
|
|||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tauri
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
src-tauri/binaries/agentkit-server-*
|
||||
|
||||
# PyInstaller
|
||||
build/pyinstaller-work/
|
||||
*.spec
|
||||
|
||||
# Frontend build artifacts
|
||||
src/agentkit/server/static/
|
||||
|
||||
# Env
|
||||
.env
|
||||
|
|
|
|||
|
|
@ -0,0 +1,405 @@
|
|||
---
|
||||
date: "2026-06-13"
|
||||
status: active
|
||||
depth: standard
|
||||
origin: null
|
||||
---
|
||||
|
||||
# feat: Tauri 2.x Desktop Client
|
||||
|
||||
## Summary
|
||||
|
||||
创建 Tauri 2.x 桌面客户端外壳,将现有 Vue 3 GUI 包装为跨平台桌面应用(Windows/macOS/Linux)。Tauri 管理窗口和 Python 后端进程生命周期,前端代码仅做最小适配(动态 API baseURL + Splash 启动流程),不修改任何 GUI 组件、布局、样式或业务逻辑。
|
||||
|
||||
## Problem Frame
|
||||
|
||||
当前 AgentKit GUI 只能通过浏览器访问(`agentkit gui` 启动后自动打开浏览器),用户需要:
|
||||
1. 手动启动后端进程
|
||||
2. 在浏览器中操作,缺乏原生体验(无系统托盘、无桌面集成、无自动更新)
|
||||
3. 无法像桌面应用一样安装、启动、关闭
|
||||
|
||||
竞品(Cursor、Devin)均提供原生桌面客户端。AgentKit 需要对齐这一体验标准。
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
**KTD-1: Tauri 2.x 作为桌面壳。** Tauri 使用系统 WebView(不打包 Chromium),安装包 ~5-10MB + PyInstaller 产物 ~30-80MB,总计远小于 Electron 方案(~200MB+)。启动快(窗口 <500ms),内存低(~110-200MB vs Electron ~300-500MB)。Tauri 2.x 插件化架构更灵活,capabilities 权限模型更安全,且是长期维护版本。
|
||||
|
||||
**KTD-2: PyInstaller 打包 Python 后端为 sidecar。** PyInstaller 将 Python 后端打包为独立单文件二进制,Tauri 通过 `tauri-plugin-shell` sidecar API 管理其生命周期。比 Nuitka 兼容性更好,社区更成熟。
|
||||
|
||||
**KTD-3: 端口 0 + stdout 解析实现端口发现。** Python 后端以 `--port 0` 启动,OS 分配随机可用端口,uvicorn 启动后通过 stdout 输出 `AGENTKIT_PORT=XXXXX`,Rust 侧解析后通过 Tauri IPC 传递给前端。避免端口冲突,无需用户配置。
|
||||
|
||||
**KTD-4: 前端最小适配策略。** 仅修改 `api/base.ts`(动态 baseURL)和 `App.vue`(Splash 启动流程),新增 `api/tauri.ts`(Tauri invoke 封装)和 `TitleBar.vue`(自定义标题栏)。所有现有 GUI 组件、布局、样式、业务逻辑不做任何修改。
|
||||
|
||||
**KTD-5: 双窗口 Splash 方案。** Tauri 配置两个窗口:splash(小窗口,加载动画)和 main(主窗口,初始隐藏)。后端就绪后 Rust 侧关闭 splash、显示 main。比前端内 Splash 更干净,不侵入现有 App.vue 的布局逻辑。
|
||||
|
||||
## High-Level Technical Design
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Tauri Application (Rust) │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
|
||||
│ │ Splash │ │ Main Window │ │ System Tray │ │
|
||||
│ │ (loading) │ │ (Vue 3 SPA) │ │ (minimize) │ │
|
||||
│ └─────────────┘ └──────┬───────┘ └─────────────┘ │
|
||||
│ │ HTTP/WS │
|
||||
│ ┌───────────────────────▼──────────────────────────┐│
|
||||
│ │ Python Backend (sidecar process) ││
|
||||
│ │ FastAPI + uvicorn on random port ││
|
||||
│ └──────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ Rust Modules: │
|
||||
│ - sidecar.rs: start/stop/health Python process │
|
||||
│ - tray.rs: system tray menu │
|
||||
│ - lib.rs: Tauri Builder + plugin registration │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**启动序列:**
|
||||
|
||||
```
|
||||
1. 用户双击桌面图标 → Tauri 启动
|
||||
2. Rust setup() → 创建系统托盘
|
||||
3. Splash 窗口显示(加载动画)
|
||||
4. 前端 invoke('start_backend')
|
||||
5. Rust spawn sidecar: agentkit-server --port 0
|
||||
6. Python 绑定随机端口 → stdout: AGENTKIT_PORT=XXXXX
|
||||
7. Rust 解析端口 → 存入 BackendState → emit('backend-ready')
|
||||
8. 前端设置 apiBaseURL → 健康检查通过
|
||||
9. Rust 关闭 splash → 显示 main 窗口
|
||||
```
|
||||
|
||||
**关闭序列:**
|
||||
|
||||
```
|
||||
1. 用户点击关闭按钮 → 窗口隐藏到托盘(不退出)
|
||||
2. 托盘菜单"退出" → Rust 发送 SIGTERM 给 Python 进程
|
||||
3. Python 优雅关闭 → Tauri app.exit(0)
|
||||
```
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
**在范围内:**
|
||||
- Tauri 2.x 项目脚手架和配置
|
||||
- Rust 侧 sidecar 进程管理(启动/停止/健康检查/端口发现)
|
||||
- 前端最小适配(api/base.ts 动态 baseURL、api/tauri.ts 封装、App.vue Splash 流程、TitleBar.vue)
|
||||
- 系统托盘(显示/隐藏窗口、退出)
|
||||
- 单实例锁
|
||||
- 自定义标题栏(无边框窗口)
|
||||
- PyInstaller 打包脚本
|
||||
- 跨平台 CI 构建(GitHub Actions)
|
||||
|
||||
**延迟到后续迭代:**
|
||||
- 自动更新器(tauri-plugin-updater 集成 + 签名密钥 + 发布服务器)
|
||||
- 应用签名和公证(macOS code signing / Windows Authenticode)
|
||||
- 多语言支持
|
||||
- 离线模式
|
||||
- 深度链接(deep link / URL scheme 注册)
|
||||
- 文件关联
|
||||
|
||||
**不在范围内:**
|
||||
- 修改现有 GUI 组件、布局、样式
|
||||
- 修改后端 API 或业务逻辑
|
||||
- 移动端适配(Tauri 2.x 支持 iOS/Android,但不在本次范围)
|
||||
- 多用户协作
|
||||
|
||||
## Implementation Units
|
||||
|
||||
### U1. Tauri 2.x 项目脚手架
|
||||
|
||||
**Goal:** 创建 Tauri 2.x 项目结构,配置构建管线,验证 dev 模式可运行。
|
||||
|
||||
**Requirements:** KTD-1, KTD-4
|
||||
|
||||
**Dependencies:** 无
|
||||
|
||||
**Files:**
|
||||
- `src-tauri/Cargo.toml` — Rust 依赖(tauri 2, tauri-plugin-shell, tauri-plugin-single-instance, serde, tokio, reqwest)
|
||||
- `src-tauri/tauri.conf.json` — Tauri 核心配置(窗口、sidecar 声明、bundle 设置)
|
||||
- `src-tauri/capabilities/default.json` — 权限声明
|
||||
- `src-tauri/src/main.rs` — Rust 入口
|
||||
- `src-tauri/src/lib.rs` — Tauri Builder(占位,后续单元填充)
|
||||
- `src-tauri/build.rs` — 构建脚本
|
||||
- `src-tauri/icons/` — 应用图标(占位)
|
||||
- `package.json` — 添加 Tauri CLI 和 API 依赖
|
||||
|
||||
**Approach:**
|
||||
1. 在项目根目录初始化 Tauri 2.x:`npm create tauri-app@latest` 或手动创建 `src-tauri/` 目录
|
||||
2. `tauri.conf.json` 配置双窗口(splash + main),main 初始 `visible: false`
|
||||
3. `build.beforeDevCommand` 指向前端 dev 脚本,`build.beforeBuildCommand` 指向前端 build 脚本
|
||||
4. `build.frontendDist` 指向前端构建输出目录
|
||||
5. `bundle.externalBin` 声明 sidecar 二进制名
|
||||
6. 安装前端依赖:`@tauri-apps/api@^2`, `@tauri-apps/plugin-shell@^2`
|
||||
7. 验证 `npm run tauri dev` 能启动窗口
|
||||
|
||||
**Patterns to follow:** Tauri 2.x 官方项目结构
|
||||
|
||||
**Test scenarios:**
|
||||
- `npm run tauri dev` 启动后显示 Tauri 窗口(前端内容可能为空,因为后端未启动)
|
||||
- `npm run tauri build` 能生成安装包(可能因缺少 sidecar 二进制而失败,此为预期)
|
||||
|
||||
**Verification:** `npm run tauri dev` 能启动 Tauri 窗口并加载前端页面
|
||||
|
||||
---
|
||||
|
||||
### U2. Rust Sidecar 进程管理
|
||||
|
||||
**Goal:** 实现 Python 后端进程的启动、停止、健康检查和端口发现。
|
||||
|
||||
**Requirements:** KTD-2, KTD-3
|
||||
|
||||
**Dependencies:** U1
|
||||
|
||||
**Files:**
|
||||
- `src-tauri/src/sidecar.rs` — sidecar 管理模块(新建)
|
||||
- `src-tauri/src/lib.rs` — 注册 invoke handler 和 BackendState
|
||||
- `src-tauri/Cargo.toml` — 添加 `regex` 依赖
|
||||
|
||||
**Approach:**
|
||||
1. 定义 `BackendState` 结构体:`port: Mutex<Option<u16>>`, `pid: Mutex<Option<u32>>`
|
||||
2. 实现 `start_backend` 命令:
|
||||
- 通过 `app.shell().sidecar("agentkit-server")` 创建 sidecar 命令
|
||||
- 传入 `--port 0` 和 `AGENTKIT_GUI_MODE=1` 环境变量
|
||||
- spawn 后监听 stdout/stderr,用正则解析 `AGENTKIT_PORT=(\d+)`
|
||||
- 端口就绪后 emit `backend-ready` 事件
|
||||
- 带超时等待(30s)
|
||||
3. 实现 `get_backend_port` 命令:返回当前端口
|
||||
4. 实现 `stop_backend` 命令:发送 SIGTERM(Unix)或 taskkill(Windows)
|
||||
5. 实现 `check_backend_health` 命令:HTTP GET `http://127.0.0.1:{port}/api/v1/health`
|
||||
6. 在 `lib.rs` 中注册所有命令和 `BackendState` managed state
|
||||
|
||||
**Technical design:**
|
||||
|
||||
```
|
||||
start_backend() flow:
|
||||
sidecar("agentkit-server").args(["--port", "0"]).env("AGENTKIT_GUI_MODE", "1")
|
||||
→ spawn → tokio::spawn listen stdout/stderr
|
||||
→ parse AGENTKIT_PORT=XXXXX → store in BackendState.port
|
||||
→ emit("backend-ready", { port }) → return port
|
||||
|
||||
stop_backend() flow:
|
||||
BackendState.pid → SIGTERM/taskkill → clear port & pid
|
||||
```
|
||||
|
||||
**Patterns to follow:** `tauri-plugin-shell` sidecar API 文档
|
||||
|
||||
**Test scenarios:**
|
||||
- 启动 sidecar 后 `get_backend_port` 返回有效端口号
|
||||
- `check_backend_health` 在后端就绪后返回 true
|
||||
- `stop_backend` 能成功终止 Python 进程
|
||||
- 后端启动超时(30s)时返回错误
|
||||
- 后端意外崩溃时 Rust 侧能检测到并清理状态
|
||||
|
||||
**Verification:** 手动测试 `invoke('start_backend')` → `invoke('get_backend_port')` → `invoke('check_backend_health')` → `invoke('stop_backend')` 全流程
|
||||
|
||||
---
|
||||
|
||||
### U3. 前端适配 — 动态 API baseURL + Splash 启动流程
|
||||
|
||||
**Goal:** 前端支持 Tauri 环境,动态设置 API baseURL,添加 Splash 启动流程。
|
||||
|
||||
**Requirements:** KTD-3, KTD-4, KTD-5
|
||||
|
||||
**Dependencies:** U2
|
||||
|
||||
**Files:**
|
||||
- `src/agentkit/server/frontend/src/api/base.ts` — 修改:支持 Tauri 环境动态 baseURL
|
||||
- `src/agentkit/server/frontend/src/api/tauri.ts` — 新增:Tauri invoke 封装
|
||||
- `src/agentkit/server/frontend/src/App.vue` — 修改:添加 Splash 启动流程
|
||||
- `src/agentkit/server/frontend/src/components/layout/SplashScreen.vue` — 新增:Splash 加载组件
|
||||
- `src/agentkit/server/frontend/src/components/layout/TitleBar.vue` — 新增:自定义标题栏
|
||||
- `src/agentkit/server/frontend/src/views/AgentLayout.vue` — 修改:集成 TitleBar
|
||||
- `src/agentkit/server/frontend/vite.config.ts` — 修改:Tauri 环境适配
|
||||
|
||||
**Approach:**
|
||||
|
||||
1. **`api/tauri.ts`**:封装 Tauri invoke 调用
|
||||
- `isTauri()` 检测是否在 Tauri 环境中(`'__TAURI_INTERNALS__' in window`)
|
||||
- `startBackend()` → `invoke('start_backend')`
|
||||
- `getBackendPort()` → `invoke('get_backend_port')`
|
||||
- `stopBackend()` → `invoke('stop_backend')`
|
||||
- `checkBackendHealth()` → `invoke('check_backend_health')`
|
||||
|
||||
2. **`api/base.ts`**:动态 baseURL
|
||||
- 新增 `initApiBaseURL()` 函数
|
||||
- Tauri 环境:从 `getBackendPort()` 获取端口,设置 `apiBaseURL = http://127.0.0.1:{port}`
|
||||
- 浏览器环境:`apiBaseURL = ''`(保持现有相对路径行为)
|
||||
- `BaseApiClient` 的 `request()` 和 `createWebSocket()` 方法使用动态 baseURL
|
||||
|
||||
3. **`App.vue`**:Splash 启动流程
|
||||
- 新增 `loading` 和 `loadingProgress` 响应式状态
|
||||
- `onMounted` 时:如果 `isTauri()`,调用 `startBackend()` → `initApiBaseURL()` → `checkBackendHealth()`
|
||||
- 加载完成前显示 `SplashScreen`,完成后显示 `RouterView`
|
||||
- 浏览器环境保持现有行为不变
|
||||
|
||||
4. **`SplashScreen.vue`**:加载动画组件
|
||||
- 品牌 Logo + 加载进度条 + 状态文字("正在启动后端..."、"正在连接...")
|
||||
- 使用现有 Design Token 变量
|
||||
|
||||
5. **`TitleBar.vue`**:自定义标题栏
|
||||
- `data-tauri-drag-region` 使标题栏可拖拽
|
||||
- 最小化/最大化/关闭按钮(关闭时隐藏到托盘)
|
||||
- 仅在 Tauri 环境中显示
|
||||
|
||||
6. **`vite.config.ts`**:Tauri 适配
|
||||
- 读取 `TAURI_DEV_HOST` 环境变量
|
||||
- `envPrefix` 添加 `TAURI_`
|
||||
- `server.watch.ignored` 排除 `src-tauri/`
|
||||
|
||||
**Patterns to follow:** 现有 `api/base.ts` 的 `BaseApiClient` 模式;现有 Design Token 体系
|
||||
|
||||
**Test scenarios:**
|
||||
- 浏览器模式:所有 API 调用正常工作(相对路径不变)
|
||||
- Tauri 模式:`initApiBaseURL()` 设置正确的 `http://127.0.0.1:{port}`
|
||||
- Tauri 模式:WebSocket 连接到正确的 `ws://127.0.0.1:{port}/api/v1/...`
|
||||
- Splash 在后端就绪前显示,就绪后消失
|
||||
- TitleBar 拖拽移动窗口,按钮功能正确
|
||||
- 后端启动失败时显示错误信息
|
||||
|
||||
**Verification:** `npm run tauri dev` 启动后:Splash 显示 → 后端启动 → 主界面加载 → API 调用正常
|
||||
|
||||
---
|
||||
|
||||
### U4. 系统托盘 + 单实例 + 窗口管理
|
||||
|
||||
**Goal:** 实现系统托盘、单实例锁和窗口关闭行为(最小化到托盘)。
|
||||
|
||||
**Requirements:** KTD-1
|
||||
|
||||
**Dependencies:** U2
|
||||
|
||||
**Files:**
|
||||
- `src-tauri/src/tray.rs` — 新增:系统托盘模块
|
||||
- `src-tauri/src/lib.rs` — 修改:注册托盘、单实例插件、窗口事件
|
||||
- `src-tauri/Cargo.toml` — 添加 `tauri-plugin-single-instance`
|
||||
|
||||
**Approach:**
|
||||
1. **系统托盘**(`tray.rs`):
|
||||
- 菜单项:显示主窗口 / 分隔线 / 退出
|
||||
- 左键点击托盘图标:显示并聚焦主窗口
|
||||
- 退出菜单项:先 `stop_backend()`,再 `app.exit(0)`
|
||||
|
||||
2. **单实例锁**:
|
||||
- 注册 `tauri-plugin-single-instance`
|
||||
- 第二个实例启动时:聚焦已有主窗口
|
||||
|
||||
3. **窗口关闭行为**:
|
||||
- `CloseRequested` 事件中 `api.prevent_close()` + `window.hide()`
|
||||
- 窗口隐藏到托盘而非退出
|
||||
|
||||
4. **Splash → Main 切换**:
|
||||
- `start_backend` 成功后,Rust 侧关闭 splash 窗口、显示 main 窗口
|
||||
|
||||
**Patterns to follow:** Tauri 2.x tray API 文档
|
||||
|
||||
**Test scenarios:**
|
||||
- 点击关闭按钮后窗口隐藏,托盘图标仍在
|
||||
- 托盘"显示"菜单恢复窗口
|
||||
- 托盘"退出"菜单停止后端并退出应用
|
||||
- 左键点击托盘图标恢复窗口
|
||||
- 启动第二个实例时聚焦已有窗口
|
||||
|
||||
**Verification:** 完整的窗口生命周期测试:启动 → 关闭到托盘 → 从托盘恢复 → 托盘退出
|
||||
|
||||
---
|
||||
|
||||
### U5. PyInstaller 打包脚本 + Python 侧适配
|
||||
|
||||
**Goal:** 创建 PyInstaller 打包脚本,Python 后端支持 `--port 0` 并输出端口信息。
|
||||
|
||||
**Requirements:** KTD-2, KTD-3
|
||||
|
||||
**Dependencies:** U2
|
||||
|
||||
**Files:**
|
||||
- `scripts/build-backend.sh` — 新增:PyInstaller 打包脚本
|
||||
- `scripts/build-backend.ps1` — 新增:Windows 版打包脚本
|
||||
- `src/agentkit/cli/main.py` — 修改:`gui` 命令支持 `--port 0`
|
||||
- `src/agentkit/server/runner.py` — 修改:uvicorn 启动后输出 `AGENTKIT_PORT=XXXXX`
|
||||
|
||||
**Approach:**
|
||||
1. **Python 侧适配**:
|
||||
- `gui` 命令的 `--port` 参数支持 `0`(让 OS 分配随机端口)
|
||||
- uvicorn 启动后,在 lifespan startup 中检测实际绑定端口
|
||||
- 通过 stdout 输出 `AGENTKIT_PORT={port}`(flush=True),供 Rust 解析
|
||||
- 当 `--port 0` 时,host 默认改为 `127.0.0.1`(仅本地访问,安全)
|
||||
|
||||
2. **PyInstaller 打包脚本**:
|
||||
- `--onefile` 模式打包
|
||||
- `--name agentkit-server` 命名
|
||||
- 包含必要的 `--hidden-import`(uvicorn.logging, agentkit.server 等)
|
||||
- `--add-data` 包含 agentkit.yaml 和 configs 目录
|
||||
- 输出到 `src-tauri/binaries/` 并添加平台后缀
|
||||
|
||||
3. **平台后缀命名**:
|
||||
- macOS ARM: `agentkit-server-aarch64-apple-darwin`
|
||||
- macOS Intel: `agentkit-server-x86_64-apple-darwin`
|
||||
- Windows: `agentkit-server-x86_64-pc-windows-msvc.exe`
|
||||
- Linux: `agentkit-server-x86_64-unknown-linux-gnu`
|
||||
|
||||
**Patterns to follow:** 现有 `agentkit gui` 命令的启动流程
|
||||
|
||||
**Test scenarios:**
|
||||
- `python -m agentkit gui --port 0` 启动后 stdout 输出 `AGENTKIT_PORT=XXXXX`
|
||||
- PyInstaller 打包的二进制能独立运行
|
||||
- 打包的二进制支持 `--port 0` 和 `AGENTKIT_GUI_MODE=1`
|
||||
- 打包的二进制在无 Python 环境的机器上能运行
|
||||
|
||||
**Verification:** 在当前平台执行 `scripts/build-backend.sh`,产物放入 `src-tauri/binaries/`,`npm run tauri dev` 能通过 sidecar 启动后端
|
||||
|
||||
---
|
||||
|
||||
### U6. 跨平台 CI 构建
|
||||
|
||||
**Goal:** 配置 GitHub Actions 自动构建 Windows/macOS/Linux 安装包。
|
||||
|
||||
**Requirements:** KTD-1
|
||||
|
||||
**Dependencies:** U1, U5
|
||||
|
||||
**Files:**
|
||||
- `.github/workflows/release-desktop.yml` — 新增:桌面客户端发布工作流
|
||||
|
||||
**Approach:**
|
||||
1. 触发条件:推送 `v*` tag
|
||||
2. 构建矩阵:macOS (aarch64 + x86_64)、Windows (x86_64)、Linux (x86_64)
|
||||
3. 每个平台步骤:
|
||||
- Setup Node.js 20 + Rust stable + Python 3.11
|
||||
- 安装 Linux 系统依赖(libwebkit2gtk-4.1-dev 等)
|
||||
- `pip install pyinstaller` + 执行 `scripts/build-backend.sh`
|
||||
- `npm ci` + `npm run tauri build`
|
||||
4. 使用 `tauri-apps/tauri-action@v0` 构建和发布
|
||||
5. 产物上传到 GitHub Release(draft)
|
||||
|
||||
**Patterns to follow:** Tauri 官方 GitHub Actions 模板
|
||||
|
||||
**Test scenarios:**
|
||||
- 推送 tag 后工作流在所有 4 个目标上成功运行
|
||||
- 产物正确上传到 GitHub Release
|
||||
|
||||
**Verification:** 手动触发工作流或推送 tag,检查 GitHub Release 页面
|
||||
|
||||
---
|
||||
|
||||
## Risks & Dependencies
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
|------|------|------|
|
||||
| PyInstaller 打包后部分依赖缺失 | 后端无法启动 | 逐步测试,添加 `--hidden-import` |
|
||||
| Linux WebView 兼容性差异 | GUI 渲染异常 | 测试主流发行版(Ubuntu 22.04+),文档说明依赖 |
|
||||
| Python sidecar 启动慢(3-5s) | 用户等待时间长 | Splash 加载画面 + 进度反馈 |
|
||||
| macOS 签名/公证缺失 | 用户安装时被 Gatekeeper 阻止 | 延迟到后续迭代,文档说明绕过方法 |
|
||||
| Tauri 2.x API 变更 | 代码需调整 | 锁定 Tauri 2.x 稳定版 |
|
||||
|
||||
## Assumptions
|
||||
|
||||
- 用户机器上有 WebView2(Windows 10+ 自带)、WebKit(macOS 自带)、WebKitGTK(Linux 需安装)
|
||||
- Python 后端不需要 GPU 加速或其他特殊硬件
|
||||
- 单用户本地使用场景,不需要多用户认证
|
||||
- 首个版本不需要应用签名和公证
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 自动更新服务器的部署方案(S3 / GitHub Releases / 自建)— 延迟到后续迭代
|
||||
- 是否需要支持 Linux ARM64 — 视用户需求决定
|
||||
- PyInstaller 产物体积优化(是否排除不必要的依赖)— 实现时评估
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Build Python backend as standalone binary using PyInstaller (Windows)
|
||||
# Usage: .\scripts\build-backend.ps1 [target-triple]
|
||||
|
||||
param(
|
||||
[string]$TargetTriple = "x86_64-pc-windows-msvc"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProjectRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
|
||||
Write-Host "Building agentkit-server for $TargetTriple..."
|
||||
|
||||
Set-Location $ProjectRoot
|
||||
|
||||
# Install PyInstaller if not present
|
||||
pip install pyinstaller 2>$null
|
||||
|
||||
# Build the binary
|
||||
pyinstaller --onefile `
|
||||
--name "agentkit-server.exe" `
|
||||
--hidden-import agentkit.server `
|
||||
--hidden-import agentkit.server.app `
|
||||
--hidden-import agentkit.server.routes `
|
||||
--hidden-import agentkit.server.config `
|
||||
--hidden-import agentkit.cli.main `
|
||||
--hidden-import uvicorn.logging `
|
||||
--hidden-import uvicorn.lifespan.on `
|
||||
--hidden-import uvicorn.lifespan.off `
|
||||
--hidden-import uvicorn.protocols.websockets.auto `
|
||||
--hidden-import uvicorn.protocols.http.auto `
|
||||
--hidden-import uvicorn.protocols.websockets.wsproto_impl `
|
||||
--hidden-import uvicorn.protocols.websockets.websockets_impl `
|
||||
--hidden-import uvicorn.protocols.websockets.impl_11 `
|
||||
--hidden-import uvicorn.lifespan `
|
||||
--hidden-import sse_starlette `
|
||||
--distpath "$ProjectRoot\src-tauri\binaries" `
|
||||
--workpath "$ProjectRoot\build\pyinstaller-work" `
|
||||
--specpath "$ProjectRoot\build" `
|
||||
src\agentkit\__main__.py
|
||||
|
||||
# Rename with target triple suffix
|
||||
Rename-Item "$ProjectRoot\src-tauri\binaries\agentkit-server.exe" "agentkit-server-$TargetTriple.exe"
|
||||
|
||||
Write-Host "Built: src-tauri\binaries\agentkit-server-$TargetTriple.exe"
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build Python backend as standalone binary using PyInstaller
|
||||
# Usage: ./scripts/build-backend.sh [target-triple]
|
||||
# Example: ./scripts/build-backend.sh aarch64-apple-darwin
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
TARGET_TRIPLE="${1:-}"
|
||||
|
||||
# Detect platform triple if not provided
|
||||
if [ -z "$TARGET_TRIPLE" ]; then
|
||||
OS="$(uname -s)"
|
||||
ARCH="$(uname -m)"
|
||||
case "$OS" in
|
||||
Darwin)
|
||||
if [ "$ARCH" = "arm64" ]; then
|
||||
TARGET_TRIPLE="aarch64-apple-darwin"
|
||||
else
|
||||
TARGET_TRIPLE="x86_64-apple-darwin"
|
||||
fi
|
||||
;;
|
||||
Linux)
|
||||
TARGET_TRIPLE="x86_64-unknown-linux-gnu"
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
TARGET_TRIPLE="x86_64-pc-windows-msvc"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported OS: $OS"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "Building agentkit-server for $TARGET_TRIPLE..."
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Install PyInstaller if not present
|
||||
pip install pyinstaller 2>/dev/null || pip3 install pyinstaller 2>/dev/null
|
||||
|
||||
# Build the binary
|
||||
BINARY_NAME="agentkit-server"
|
||||
if [[ "$TARGET_TRIPLE" == *"-windows-"* ]]; then
|
||||
BINARY_NAME="agentkit-server.exe"
|
||||
fi
|
||||
|
||||
pyinstaller --onefile \
|
||||
--name "$BINARY_NAME" \
|
||||
--hidden-import agentkit.server \
|
||||
--hidden-import agentkit.server.app \
|
||||
--hidden-import agentkit.server.routes \
|
||||
--hidden-import agentkit.server.config \
|
||||
--hidden-import agentkit.cli.main \
|
||||
--hidden-import uvicorn.logging \
|
||||
--hidden-import uvicorn.lifespan.on \
|
||||
--hidden-import uvicorn.lifespan.off \
|
||||
--hidden-import uvicorn.protocols.websockets.auto \
|
||||
--hidden-import uvicorn.protocols.http.auto \
|
||||
--hidden-import uvicorn.protocols.websockets.wsproto_impl \
|
||||
--hidden-import uvicorn.protocols.websockets.websockets_impl \
|
||||
--hidden-import uvicorn.protocols.websockets.impl_11 \
|
||||
--hidden-import uvicorn.lifespan \
|
||||
--hidden-import sse_starlette \
|
||||
--distpath "$PROJECT_ROOT/src-tauri/binaries" \
|
||||
--workpath "$PROJECT_ROOT/build/pyinstaller-work" \
|
||||
--specpath "$PROJECT_ROOT/build" \
|
||||
src/agentkit/__main__.py
|
||||
|
||||
# Rename with target triple suffix (Tauri sidecar convention)
|
||||
if [[ "$TARGET_TRIPLE" != *"-windows-"* ]]; then
|
||||
mv "$PROJECT_ROOT/src-tauri/binaries/$BINARY_NAME" \
|
||||
"$PROJECT_ROOT/src-tauri/binaries/agentkit-server-$TARGET_TRIPLE"
|
||||
else
|
||||
mv "$PROJECT_ROOT/src-tauri/binaries/$BINARY_NAME" \
|
||||
"$PROJECT_ROOT/src-tauri/binaries/agentkit-server-$TARGET_TRIPLE.exe"
|
||||
fi
|
||||
|
||||
echo "Built: src-tauri/binaries/agentkit-server-$TARGET_TRIPLE"
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "fischer-agentkit"
|
||||
version = "0.1.0"
|
||||
description = "Fischer AgentKit Desktop Client"
|
||||
authors = ["Fischer Team"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "fischer_agentkit_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-process = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
regex = "1"
|
||||
log = "0.4"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-utils/schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for Fischer AgentKit",
|
||||
"windows": ["main", "splash"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:default",
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-kill",
|
||||
"shell:allow-stdin-write",
|
||||
"process:default",
|
||||
"process:allow-restart",
|
||||
"process:allow-exit"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 360 B |
Binary file not shown.
|
After Width: | Height: | Size: 857 B |
Binary file not shown.
|
After Width: | Height: | Size: 104 B |
|
|
@ -0,0 +1,52 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
mod sidecar;
|
||||
mod tray;
|
||||
|
||||
pub struct BackendState {
|
||||
pub port: Arc<Mutex<Option<u16>>>,
|
||||
pub child: Arc<Mutex<Option<CommandChild>>>,
|
||||
pub cancel_token: Arc<Mutex<Option<CancellationToken>>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.show();
|
||||
let _ = w.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.manage(BackendState {
|
||||
port: Arc::new(Mutex::new(None)),
|
||||
child: Arc::new(Mutex::new(None)),
|
||||
cancel_token: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
sidecar::start_backend,
|
||||
sidecar::get_backend_port,
|
||||
sidecar::stop_backend,
|
||||
sidecar::check_backend_health,
|
||||
])
|
||||
.setup(|app| {
|
||||
tray::create_tray(app)?;
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
// Close-to-tray: hide main window instead of closing
|
||||
if window.label() == "main" {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
fischer_agentkit_lib::run()
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
use std::sync::OnceLock;
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tauri_plugin_shell::process::CommandEvent;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
static AGENTKIT_PORT_RE: OnceLock<regex::Regex> = OnceLock::new();
|
||||
static UVICORN_PORT_RE: OnceLock<regex::Regex> = OnceLock::new();
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_backend(
|
||||
app: AppHandle,
|
||||
state: State<'_, crate::BackendState>,
|
||||
) -> Result<u16, String> {
|
||||
// If already started, return existing port
|
||||
if let Some(port) = *state.port.lock().unwrap() {
|
||||
return Ok(port);
|
||||
}
|
||||
|
||||
// Spawn sidecar with --port 0 for random port assignment
|
||||
let sidecar_command = app
|
||||
.shell()
|
||||
.sidecar("agentkit-server")
|
||||
.map_err(|e| format!("Failed to create sidecar command: {}", e))?
|
||||
.args(["--port", "0", "--host", "127.0.0.1"])
|
||||
.env("AGENTKIT_GUI_MODE", "1");
|
||||
|
||||
let (mut rx, child) = sidecar_command
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn sidecar: {}", e))?;
|
||||
|
||||
// Store the CommandChild for proper cleanup later
|
||||
*state.child.lock().unwrap() = Some(child);
|
||||
|
||||
let port_clone = state.port.clone();
|
||||
let cancel_token = CancellationToken::new();
|
||||
*state.cancel_token.lock().unwrap() = Some(cancel_token.clone());
|
||||
let app_clone = app.clone();
|
||||
|
||||
// Listen for stdout/stderr to detect port
|
||||
let cancel = cancel_token.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => {
|
||||
log::info!("Backend listener task cancelled");
|
||||
}
|
||||
result = async {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
let output = String::from_utf8_lossy(&line);
|
||||
log::info!("[backend:stdout] {}", output.trim());
|
||||
if let Some(port) = extract_port(&output) {
|
||||
*port_clone.lock().unwrap() = Some(port);
|
||||
let _ = app_clone.emit("backend-ready", serde_json::json!({ "port": port }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
CommandEvent::Stderr(line) => {
|
||||
let output = String::from_utf8_lossy(&line);
|
||||
log::info!("[backend:stderr] {}", output.trim());
|
||||
if let Some(port) = extract_port(&output) {
|
||||
*port_clone.lock().unwrap() = Some(port);
|
||||
let _ = app_clone.emit("backend-ready", serde_json::json!({ "port": port }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
CommandEvent::Terminated(payload) => {
|
||||
log::error!("Backend process terminated: {:?}", payload);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} => { result }
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for port to be ready (with timeout)
|
||||
let max_wait = std::time::Duration::from_secs(30);
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < max_wait {
|
||||
{
|
||||
let guard = state.port.lock().unwrap();
|
||||
if let Some(port) = *guard {
|
||||
log::info!("Backend started on port {}", port);
|
||||
|
||||
// Close splash, show main window
|
||||
if let Some(splash) = app.get_webview_window("splash") {
|
||||
let _ = splash.close();
|
||||
}
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
let _ = main.show();
|
||||
let _ = main.set_focus();
|
||||
}
|
||||
|
||||
return Ok(port);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
|
||||
// Timeout: clean up the started backend process
|
||||
log::warn!("Backend startup timed out, cleaning up...");
|
||||
cleanup_backend(&state);
|
||||
Err("Backend startup timed out after 30 seconds".into())
|
||||
}
|
||||
|
||||
fn extract_port(output: &str) -> Option<u16> {
|
||||
let re = AGENTKIT_PORT_RE.get_or_init(|| regex::Regex::new(r"AGENTKIT_PORT=(\d{1,5})").unwrap());
|
||||
if let Some(caps) = re.captures(output) {
|
||||
return caps[1].parse().ok();
|
||||
}
|
||||
let re2 = UVICORN_PORT_RE.get_or_init(|| regex::Regex::new(r"Uvicorn running on https?://[\d.]+:(\d{1,5})").unwrap());
|
||||
if let Some(caps) = re2.captures(output) {
|
||||
return caps[1].parse().ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Clean up backend process: cancel listener task, kill child process, reset state.
|
||||
fn cleanup_backend(state: &State<'_, crate::BackendState>) {
|
||||
// Cancel the listener task
|
||||
if let Some(token) = state.cancel_token.lock().unwrap().take() {
|
||||
token.cancel();
|
||||
}
|
||||
// Kill the child process
|
||||
if let Some(child) = state.child.lock().unwrap().take() {
|
||||
if let Err(e) = child.kill() {
|
||||
log::warn!("Failed to kill backend child process: {}", e);
|
||||
}
|
||||
}
|
||||
// Reset state
|
||||
*state.port.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_backend_port(state: State<'_, crate::BackendState>) -> Result<u16, String> {
|
||||
state
|
||||
.port
|
||||
.lock()
|
||||
.unwrap()
|
||||
.ok_or_else(|| "Backend not started".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_backend(state: State<'_, crate::BackendState>) -> Result<(), String> {
|
||||
cleanup_backend(&state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_backend_health(
|
||||
state: State<'_, crate::BackendState>,
|
||||
) -> Result<bool, String> {
|
||||
let port = state.port.lock().unwrap().ok_or("Backend not started")?;
|
||||
let url = format!("http://127.0.0.1:{}/api/v1/health", port);
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.timeout(std::time::Duration::from_secs(3))
|
||||
.send()
|
||||
.await;
|
||||
Ok(resp.map(|r| r.status().is_success()).unwrap_or(false))
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
use tauri::{
|
||||
Manager,
|
||||
menu::{MenuBuilder, MenuItemBuilder},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
};
|
||||
|
||||
pub fn create_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let show = MenuItemBuilder::with_id("show", "显示主窗口").build(app)?;
|
||||
let quit = MenuItemBuilder::with_id("quit", "退出").build(app)?;
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&show)
|
||||
.separator()
|
||||
.item(&quit)
|
||||
.build()?;
|
||||
|
||||
let mut tray_builder = TrayIconBuilder::new()
|
||||
.menu(&menu)
|
||||
.tooltip("Fischer AgentKit");
|
||||
|
||||
if let Some(icon) = app.default_window_icon() {
|
||||
tray_builder = tray_builder.icon(icon.clone());
|
||||
}
|
||||
|
||||
let _tray = tray_builder
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"show" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
// Stop backend before exiting
|
||||
let state = app.state::<crate::BackendState>();
|
||||
// Cancel listener task
|
||||
if let Some(token) = state.cancel_token.lock().unwrap().take() {
|
||||
token.cancel();
|
||||
}
|
||||
// Kill the child process
|
||||
if let Some(child) = state.child.lock().unwrap().take() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
*state.port.lock().unwrap() = None;
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let app = tray.app_handle();
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
|
||||
"productName": "Fischer AgentKit",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.fischer.agentkit",
|
||||
"build": {
|
||||
"beforeDevCommand": "cd src/agentkit/server/frontend && npm run dev:frontend",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeBuildCommand": "cd src/agentkit/server/frontend && npm run build:frontend",
|
||||
"frontendDist": "../src/agentkit/server/static"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "splash",
|
||||
"title": "Fischer AgentKit",
|
||||
"width": 400,
|
||||
"height": 280,
|
||||
"resizable": false,
|
||||
"decorations": false,
|
||||
"center": true,
|
||||
"visible": true,
|
||||
"url": "/splash.html"
|
||||
},
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Fischer AgentKit",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
"center": true,
|
||||
"visible": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; script-src 'self' 'unsafe-eval'; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: https://asset.localhost"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/agentkit-server"
|
||||
],
|
||||
"resources": []
|
||||
}
|
||||
}
|
||||
|
|
@ -33,18 +33,23 @@ app.command(name="chat")(chat)
|
|||
@app.command()
|
||||
def gui(
|
||||
host: str = typer.Option("0.0.0.0", "--host", help="Server bind host"),
|
||||
port: int = typer.Option(8002, "--port", help="Server port"),
|
||||
port: int = typer.Option(8002, "--port", help="Server port (0 for random)"),
|
||||
config: Optional[str] = typer.Option(None, "--config", help="Path to agentkit.yaml"),
|
||||
no_open: bool = typer.Option(False, "--no-open", help="Do not open browser automatically"),
|
||||
):
|
||||
"""Start AgentKit with a web UI for chatting with your Agent"""
|
||||
import os
|
||||
import sys
|
||||
import webbrowser
|
||||
import uvicorn
|
||||
|
||||
from agentkit.server.config import ServerConfig, find_config_path
|
||||
from agentkit.cli.onboarding import run_onboarding
|
||||
|
||||
# When port=0, default to localhost for security (desktop client mode)
|
||||
if port == 0 and host == "0.0.0.0":
|
||||
host = "127.0.0.1"
|
||||
|
||||
# Load config
|
||||
config_path = find_config_path(config)
|
||||
|
||||
|
|
@ -58,6 +63,7 @@ def gui(
|
|||
else:
|
||||
rprint("[dim]Using default configuration (no LLM providers).[/dim]")
|
||||
|
||||
server_config = None
|
||||
if config_path:
|
||||
rprint(f"[green]Loading config from {config_path}[/green]")
|
||||
server_config = ServerConfig.from_yaml(config_path)
|
||||
|
|
@ -89,10 +95,11 @@ def gui(
|
|||
os.environ["AGENTKIT_GUI_MODE"] = "1"
|
||||
|
||||
# Browser always opens localhost, server binds to configured host
|
||||
browser_url = f"http://localhost:{port}"
|
||||
browser_url = f"http://localhost:{port}" if port != 0 else None
|
||||
if browser_url:
|
||||
rprint(f"[green]Starting AgentKit GUI — open {browser_url} in your browser[/green]")
|
||||
|
||||
if not no_open:
|
||||
if not no_open and browser_url:
|
||||
import threading
|
||||
def _open_browser():
|
||||
import time
|
||||
|
|
@ -105,8 +112,34 @@ def gui(
|
|||
from agentkit.server.app import create_app
|
||||
app = create_app(server_config=server_config)
|
||||
|
||||
if port == 0:
|
||||
# Desktop client mode: use Server + Config for port discovery
|
||||
config = uvicorn.Config(
|
||||
app,
|
||||
host=host,
|
||||
port=0,
|
||||
log_level="info",
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
# Hook into startup to detect the actual port
|
||||
original_on_startup = config.on_startup
|
||||
|
||||
async def on_startup_with_port():
|
||||
if original_on_startup:
|
||||
await original_on_startup()
|
||||
# Output the actual port for sidecar process to parse
|
||||
for server_state in server.servers:
|
||||
for sock in server_state.sockets:
|
||||
actual_port = sock.getsockname()[1]
|
||||
print(f"AGENTKIT_PORT={actual_port}", flush=True)
|
||||
rprint(f"[green]Backend started on port {actual_port}[/green]")
|
||||
|
||||
config.on_startup = on_startup_with_port
|
||||
server.run()
|
||||
else:
|
||||
uvicorn.run(
|
||||
app, # Direct app instance, not factory string
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,17 +9,21 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"@vue-flow/background": "^1.3.0",
|
||||
"@vue-flow/controls": "^1.1.0",
|
||||
"@vue-flow/core": "^1.41.0",
|
||||
"ant-design-vue": "^4.2.0",
|
||||
"dompurify": "^3.4.10",
|
||||
"highlight.js": "^11.11.1",
|
||||
"markdown-it": "^14.2.0",
|
||||
"pinia": "^2.2.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11.2",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
|
|
@ -935,6 +939,242 @@
|
|||
"nanopop": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@tauri-apps/cli/-/cli-2.11.2.tgz",
|
||||
"integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.11.2",
|
||||
"@tauri-apps/cli-darwin-x64": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz",
|
||||
"integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz",
|
||||
"integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz",
|
||||
"integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-shell": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
||||
"integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dompurify": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
|
|
@ -1674,6 +1914,15 @@
|
|||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -5,22 +5,29 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:frontend": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
"build:frontend": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"@vue-flow/background": "^1.3.0",
|
||||
"@vue-flow/controls": "^1.1.0",
|
||||
"@vue-flow/core": "^1.41.0",
|
||||
"ant-design-vue": "^4.2.0",
|
||||
"dompurify": "^3.4.10",
|
||||
"highlight.js": "^11.11.1",
|
||||
"markdown-it": "^14.2.0",
|
||||
"pinia": "^2.2.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11.2",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fischer AgentKit</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
width: 100vw; height: 100vh; overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
background: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
.logo { font-size: 28px; font-weight: 700; color: #1a1a1a; margin-bottom: 8px; }
|
||||
.subtitle { font-size: 13px; color: #888; margin-bottom: 32px; }
|
||||
.progress-bar {
|
||||
width: 200px; height: 3px; background: #eee; border-radius: 2px; overflow: hidden;
|
||||
}
|
||||
.progress-bar-inner {
|
||||
width: 40%; height: 100%; background: #6366f1; border-radius: 2px;
|
||||
animation: loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(350%); }
|
||||
}
|
||||
.status { margin-top: 16px; font-size: 12px; color: #aaa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="logo">Fischer AgentKit</div>
|
||||
<div class="subtitle">AI Agent Framework</div>
|
||||
<div class="progress-bar"><div class="progress-bar-inner"></div></div>
|
||||
<div class="status">正在启动后端服务...</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,13 +1,44 @@
|
|||
<template>
|
||||
<a-config-provider :locale="zhCN" :theme="themeConfig">
|
||||
<router-view />
|
||||
<SplashScreen v-if="loading" :status="loadingStatus" :error="loadError" />
|
||||
<router-view v-else />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ConfigProvider as AConfigProvider } from 'ant-design-vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { themeConfig } from './styles'
|
||||
import { isTauri, startBackend, checkBackendHealth } from './api/tauri'
|
||||
import { initApiBaseURL } from './api/base'
|
||||
import SplashScreen from './components/layout/SplashScreen.vue'
|
||||
|
||||
const loading = ref(isTauri())
|
||||
const loadingStatus = ref('正在初始化...')
|
||||
const loadError = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
loadingStatus.value = '正在启动后端...'
|
||||
await startBackend()
|
||||
loadingStatus.value = '正在配置连接...'
|
||||
await initApiBaseURL()
|
||||
loadingStatus.value = '正在检查服务...'
|
||||
const healthy = await checkBackendHealth()
|
||||
if (healthy) {
|
||||
loading.value = false
|
||||
} else {
|
||||
loadError.value = '后端服务健康检查失败'
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
loadError.value = err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
} else {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
@ -29,5 +60,56 @@ body {
|
|||
'Noto Color Emoji';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
background: var(--bg-secondary, #fbfbfa);
|
||||
}
|
||||
|
||||
/* Notion-style scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-300, #dfdfde);
|
||||
border-radius: var(--radius-full, 9999px);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-gray-400, #cececd);
|
||||
}
|
||||
|
||||
/* Ant Design overrides for Notion-style feel */
|
||||
.ant-btn {
|
||||
font-weight: var(--font-weight-medium, 500);
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-color: var(--border-color, #ededec) !important;
|
||||
}
|
||||
|
||||
.ant-card-hoverable:hover {
|
||||
border-color: var(--color-primary-light, #eef2ff) !important;
|
||||
box-shadow: var(--shadow-md, 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04)) !important;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
transition: color var(--transition-fast, 150ms ease) !important;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
border-radius: var(--radius-sm, 4px) !important;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: var(--radius-xl, 14px) !important;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: var(--radius-md, 6px) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,28 @@
|
|||
/** Shared API base client */
|
||||
|
||||
import { isTauri, getBackendPort } from './tauri'
|
||||
|
||||
export interface IApiError {
|
||||
status: number
|
||||
message: string
|
||||
detail?: string
|
||||
}
|
||||
|
||||
let _dynamicBaseURL = ''
|
||||
|
||||
/** Initialize the dynamic base URL for Tauri (sidecar backend). */
|
||||
export async function initApiBaseURL(): Promise<void> {
|
||||
if (isTauri()) {
|
||||
const port = await getBackendPort()
|
||||
_dynamicBaseURL = `http://127.0.0.1:${port}`
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the current dynamic base URL (empty string when not in Tauri). */
|
||||
export function getDynamicBaseURL(): string {
|
||||
return _dynamicBaseURL
|
||||
}
|
||||
|
||||
export class BaseApiClient {
|
||||
protected baseUrl: string
|
||||
|
||||
|
|
@ -14,8 +31,9 @@ export class BaseApiClient {
|
|||
}
|
||||
|
||||
protected async request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
// If path starts with /api/, it's an absolute path — don't prepend baseUrl
|
||||
const url = path.startsWith('/api/') ? path : `${this.baseUrl}${path}`
|
||||
const effectiveUrl = _dynamicBaseURL
|
||||
? (path.startsWith('/api/') ? `${_dynamicBaseURL}${path}` : `${_dynamicBaseURL}${this.baseUrl}${path}`)
|
||||
: (path.startsWith('/api/') ? path : `${this.baseUrl}${path}`)
|
||||
const headers: Record<string, string> = {
|
||||
...options.headers as Record<string, string>,
|
||||
}
|
||||
|
|
@ -24,7 +42,7 @@ export class BaseApiClient {
|
|||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...options, headers })
|
||||
const response = await fetch(effectiveUrl, { ...options, headers })
|
||||
|
||||
if (!response.ok) {
|
||||
const error: IApiError = {
|
||||
|
|
@ -45,6 +63,11 @@ export class BaseApiClient {
|
|||
}
|
||||
|
||||
protected createWebSocket(path: string): WebSocket {
|
||||
if (_dynamicBaseURL) {
|
||||
const url = new URL(_dynamicBaseURL)
|
||||
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
return new WebSocket(`${protocol}//${url.host}${this.baseUrl}${path}`)
|
||||
}
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
return new WebSocket(`${protocol}//${host}${this.baseUrl}${path}`)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Tauri IPC wrapper — provides typed access to Rust backend commands.
|
||||
* Falls back gracefully when not running inside Tauri (browser mode).
|
||||
*/
|
||||
|
||||
// Detect Tauri environment
|
||||
export function isTauri(): boolean {
|
||||
return '__TAURI_INTERNALS__' in window
|
||||
}
|
||||
|
||||
// Lazy-loaded invoke — only import when in Tauri environment
|
||||
async function invoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
|
||||
if (!isTauri()) {
|
||||
throw new Error(`invoke('${cmd}') called outside Tauri environment`)
|
||||
}
|
||||
const { invoke: tauriInvoke } = await import('@tauri-apps/api/core')
|
||||
return tauriInvoke<T>(cmd, args)
|
||||
}
|
||||
|
||||
/** Start the Python backend sidecar. Returns the port it bound to. */
|
||||
export async function startBackend(): Promise<number> {
|
||||
return invoke<number>('start_backend')
|
||||
}
|
||||
|
||||
/** Get the currently bound backend port. Throws if backend not started. */
|
||||
export async function getBackendPort(): Promise<number> {
|
||||
return invoke<number>('get_backend_port')
|
||||
}
|
||||
|
||||
/** Stop the Python backend sidecar. */
|
||||
export async function stopBackend(): Promise<void> {
|
||||
return invoke<void>('stop_backend')
|
||||
}
|
||||
|
||||
/** Check if the backend is healthy (responds to /api/v1/health). */
|
||||
export async function checkBackendHealth(): Promise<boolean> {
|
||||
return invoke<boolean>('check_backend_health')
|
||||
}
|
||||
|
||||
/** Listen for a Tauri event. Returns an unlisten function. */
|
||||
export async function listen<T>(event: string, handler: (payload: T) => void): Promise<() => void> {
|
||||
if (!isTauri()) {
|
||||
return () => {}
|
||||
}
|
||||
const { listen: tauriListen } = await import('@tauri-apps/api/event')
|
||||
const unlisten = await tauriListen<T>(event, (e) => handler(e.payload))
|
||||
return unlisten
|
||||
}
|
||||
|
|
@ -1,44 +1,20 @@
|
|||
<template>
|
||||
<div class="agent-layout">
|
||||
<TopNav />
|
||||
<TitleBar />
|
||||
<TopNav :icon-nav-collapsed="iconNavCollapsed" @toggle-icon-nav="iconNavCollapsed = !iconNavCollapsed" />
|
||||
<div class="agent-layout__body">
|
||||
<IconNav
|
||||
:collapsed="iconNavCollapsed"
|
||||
@navigate="handleIconNav"
|
||||
/>
|
||||
<SplitPane
|
||||
direction="horizontal"
|
||||
:default-ratio="0.5"
|
||||
:default-ratio="0.55"
|
||||
storage-key="agent-h-split"
|
||||
>
|
||||
<template #first>
|
||||
<SplitPane
|
||||
direction="vertical"
|
||||
:default-ratio="0.6"
|
||||
storage-key="agent-left-v-split"
|
||||
>
|
||||
<template #first>
|
||||
<QuadrantPanel
|
||||
ref="tlPanel"
|
||||
:tabs="topLeftTabs"
|
||||
default-tab="chat"
|
||||
storage-key="quadrant-tl-tab"
|
||||
>
|
||||
<template #chat>
|
||||
<ChatView />
|
||||
</template>
|
||||
</QuadrantPanel>
|
||||
</template>
|
||||
<template #second>
|
||||
<QuadrantPanel
|
||||
ref="blPanel"
|
||||
:tabs="bottomLeftTabs"
|
||||
default-tab="terminal"
|
||||
storage-key="quadrant-bl-tab"
|
||||
>
|
||||
<template #terminal>
|
||||
<TerminalView />
|
||||
</template>
|
||||
</QuadrantPanel>
|
||||
</template>
|
||||
</SplitPane>
|
||||
</template>
|
||||
<template #second>
|
||||
<SplitPane
|
||||
direction="vertical"
|
||||
|
|
@ -91,7 +67,7 @@
|
|||
<div class="agent-layout__small-screen">
|
||||
<DesktopOutlined style="font-size: 48px; color: var(--text-placeholder)" />
|
||||
<h2>请使用更大的屏幕</h2>
|
||||
<p>Fischer AgentKit 四象限布局需要至少 1280px 宽度的屏幕才能正常使用。</p>
|
||||
<p>Fischer AgentKit 布局需要至少 1024px 宽度的屏幕才能正常使用。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -100,8 +76,6 @@
|
|||
import { ref, watch, defineAsyncComponent, type Component } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
MessageOutlined,
|
||||
CodeOutlined,
|
||||
FileTextOutlined,
|
||||
ApartmentOutlined,
|
||||
BookOutlined,
|
||||
|
|
@ -111,13 +85,14 @@ import {
|
|||
DesktopOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import TopNav from './TopNav.vue'
|
||||
import TitleBar from './TitleBar.vue'
|
||||
import SplitPane from './SplitPane.vue'
|
||||
import QuadrantPanel from './QuadrantPanel.vue'
|
||||
import IconNav from './IconNav.vue'
|
||||
import type { QuadrantTab } from './QuadrantPanel.vue'
|
||||
|
||||
// Lazy-load views to avoid bundling all into initial chunk
|
||||
const ChatView = defineAsyncComponent(() => import('@/views/ChatView.vue'))
|
||||
const TerminalView = defineAsyncComponent(() => import('@/views/TerminalView.vue'))
|
||||
const WorkflowView = defineAsyncComponent(() => import('@/views/WorkflowView.vue'))
|
||||
const KnowledgeBaseView = defineAsyncComponent(() => import('@/views/KnowledgeBaseView.vue'))
|
||||
const EvolutionView = defineAsyncComponent(() => import('@/views/EvolutionView.vue'))
|
||||
|
|
@ -126,13 +101,19 @@ const SettingsView = defineAsyncComponent(() => import('@/views/SettingsView.vue
|
|||
|
||||
const route = useRoute()
|
||||
|
||||
const topLeftTabs: QuadrantTab[] = [
|
||||
{ key: 'chat', label: '对话', icon: MessageOutlined as Component },
|
||||
]
|
||||
// IconNav state
|
||||
const iconNavCollapsed = ref(localStorage.getItem('icon-nav-collapsed') === 'true')
|
||||
watch(iconNavCollapsed, (val) => {
|
||||
localStorage.setItem('icon-nav-collapsed', String(val))
|
||||
})
|
||||
|
||||
const bottomLeftTabs: QuadrantTab[] = [
|
||||
{ key: 'terminal', label: '终端', icon: CodeOutlined as Component },
|
||||
]
|
||||
function handleIconNav(panel: string, tab?: string) {
|
||||
if (panel === 'chat') return // ChatView is always visible
|
||||
const panelRef = panel === 'tr' ? trPanel : brPanel
|
||||
if (tab && panelRef.value) {
|
||||
panelRef.value.setActiveTab(tab)
|
||||
}
|
||||
}
|
||||
|
||||
const topRightTabs: QuadrantTab[] = [
|
||||
{ key: 'code', label: '代码', icon: FileTextOutlined as Component },
|
||||
|
|
@ -147,25 +128,21 @@ const bottomRightTabs: QuadrantTab[] = [
|
|||
]
|
||||
|
||||
// Quadrant refs for route-driven tab switching
|
||||
const tlPanel = ref<InstanceType<typeof QuadrantPanel> | null>(null)
|
||||
const blPanel = ref<InstanceType<typeof QuadrantPanel> | null>(null)
|
||||
const trPanel = ref<InstanceType<typeof QuadrantPanel> | null>(null)
|
||||
const brPanel = ref<InstanceType<typeof QuadrantPanel> | null>(null)
|
||||
|
||||
// Watch route changes to sync quadrant tabs with URL
|
||||
watch(() => route.meta, (meta) => {
|
||||
const quadrant = meta.quadrant as string | undefined
|
||||
const panel = meta.panel as string | undefined
|
||||
const tab = meta.tab as string | undefined
|
||||
if (quadrant && tab) {
|
||||
const panelMap: Record<string, typeof tlPanel> = {
|
||||
tl: tlPanel,
|
||||
bl: blPanel,
|
||||
if (panel && tab) {
|
||||
const panelMap: Record<string, typeof trPanel> = {
|
||||
tr: trPanel,
|
||||
br: brPanel,
|
||||
}
|
||||
const panel = panelMap[quadrant]
|
||||
if (panel?.value) {
|
||||
panel.value.setActiveTab(tab)
|
||||
const panelRef = panelMap[panel]
|
||||
if (panelRef?.value) {
|
||||
panelRef.value.setActiveTab(tab)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
|
@ -178,14 +155,15 @@ watch(() => route.meta, (meta) => {
|
|||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.agent-layout__body {
|
||||
flex: 1;
|
||||
padding: var(--space-2);
|
||||
gap: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.agent-layout__placeholder {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div class="splash-screen">
|
||||
<div class="splash-content">
|
||||
<div class="splash-logo">Fischer AgentKit</div>
|
||||
<div class="splash-subtitle">AI Agent Framework</div>
|
||||
<div class="splash-progress">
|
||||
<div class="splash-progress-bar">
|
||||
<div class="splash-progress-inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="splash-status">{{ status }}</div>
|
||||
<div v-if="error" class="splash-error">{{ error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
status: string
|
||||
error?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.splash-screen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-secondary, #fbfbfa);
|
||||
}
|
||||
.splash-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.splash-logo {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.splash-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary, #888);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.splash-progress {
|
||||
width: 200px;
|
||||
}
|
||||
.splash-progress-bar {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: var(--border-color, #ededec);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.splash-progress-inner {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
background: var(--color-primary, #6366f1);
|
||||
border-radius: 2px;
|
||||
animation: splash-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes splash-loading {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(350%); }
|
||||
}
|
||||
.splash-status {
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-quaternary, #aaa);
|
||||
}
|
||||
.splash-error {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--color-error, #ff4d4f);
|
||||
max-width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div v-if="isTauri()" class="titlebar" data-tauri-drag-region>
|
||||
<div class="titlebar-title" data-tauri-drag-region>Fischer AgentKit</div>
|
||||
<div class="titlebar-controls">
|
||||
<button class="titlebar-btn" @click="minimize" title="最小化">
|
||||
<MinusOutlined />
|
||||
</button>
|
||||
<button class="titlebar-btn" @click="toggleMaximize" title="最大化">
|
||||
<BorderOutlined />
|
||||
</button>
|
||||
<button class="titlebar-btn close-btn" @click="close" title="关闭">
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MinusOutlined, BorderOutlined, CloseOutlined } from '@ant-design/icons-vue'
|
||||
import { isTauri } from '../../api/tauri'
|
||||
|
||||
async function minimize() {
|
||||
if (!isTauri()) return
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||
await getCurrentWindow().minimize()
|
||||
}
|
||||
|
||||
async function toggleMaximize() {
|
||||
if (!isTauri()) return
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||
await getCurrentWindow().toggleMaximize()
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (!isTauri()) return
|
||||
// Hide to tray instead of closing
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||
await getCurrentWindow().hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.titlebar {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-container, #ffffff);
|
||||
border-bottom: 1px solid var(--border-color, #ededec);
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.titlebar-title {
|
||||
padding-left: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #555);
|
||||
}
|
||||
.titlebar-controls {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.titlebar-btn {
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary, #555);
|
||||
font-size: 12px;
|
||||
}
|
||||
.titlebar-btn:hover {
|
||||
background: var(--fill-secondary, #f5f5f5);
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,6 +4,8 @@ import { resolve } from 'path'
|
|||
import Components from 'unplugin-vue-components/vite'
|
||||
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
|
|
@ -24,7 +26,16 @@ export default defineConfig({
|
|||
outDir: '../static',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
// Tauri dev server configuration
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host ? { protocol: 'ws', host, port: 5174 } : undefined,
|
||||
watch: {
|
||||
ignored: ['**/src-tauri/**'],
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
|
|
@ -33,4 +44,5 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
envPrefix: ['VITE_', 'TAURI_'],
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue