feat(desktop): ship full and mini clients

This commit is contained in:
2026-07-22 18:52:38 +08:00
parent a23a110758
commit aeb5276a73
10 changed files with 92 additions and 15 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ coverage/
*.log *.log
test-results/ test-results/
playwright-report/ playwright-report/
apps/desktop/release/

20
apps/desktop/README.md Normal file
View File

@@ -0,0 +1,20 @@
# 森林AI 桌面客户端
桌面端提供两个独立可执行文件:
- `senlinai-mini.exe`:默认启动入口。窄屏、置顶,启动后按当前显示器高度停靠在屏幕右侧,也可以切换到左侧。
- `senlinai-full.exe`:完整工作台客户端。
```bash
# 默认启动 Mini
npm run dev
# 分别启动
npm run mini:dev
npm run full:dev
# 一次构建两个 EXE
npm run build
```
两个构建产物会汇总到 `apps/desktop/release`

View File

@@ -1,12 +1,17 @@
{ {
"name": "desktop", "name": "desktop",
"private": true, "private": true,
"version": "0.1.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"tauri": "tauri", "tauri": "tauri",
"dev": "tauri dev", "start": "npm run mini:dev",
"build": "tauri build --no-bundle", "dev": "npm run mini:dev",
"mini:dev": "npm --prefix ../mini run desktop:dev",
"full:dev": "tauri dev",
"mini:build": "npm --prefix ../mini run desktop:build",
"full:build": "tauri build --no-bundle",
"build": "node scripts/build-all.mjs",
"bundle": "tauri build --ci --no-sign" "bundle": "tauri build --ci --no-sign"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,44 @@
import { copyFileSync, mkdirSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
const desktopDir = join(dirname(fileURLToPath(import.meta.url)), '..')
const repoDir = join(desktopDir, '..', '..')
const isWindows = process.platform === 'win32'
const npmCommand = isWindows ? process.env.ComSpec || 'cmd.exe' : 'npm'
function run(script) {
const args = isWindows ? ['/d', '/s', '/c', `npm run ${script}`] : ['run', script]
const result = spawnSync(npmCommand, args, {
cwd: desktopDir,
stdio: 'inherit',
})
if (result.error) throw result.error
if (result.status !== 0) {
process.exit(result.status ?? 1)
}
}
run('full:build')
run('mini:build')
const releaseDir = join(desktopDir, 'release')
mkdirSync(releaseDir, { recursive: true })
const artifacts = [
{
source: join(desktopDir, 'src-tauri', 'target', 'release', 'senlinai-full.exe'),
target: join(releaseDir, 'senlinai-full.exe'),
},
{
source: join(repoDir, 'apps', 'mini', 'src-tauri', 'target', 'release', 'senlinai-mini.exe'),
target: join(releaseDir, 'senlinai-mini.exe'),
},
]
for (const artifact of artifacts) {
copyFileSync(artifact.source, artifact.target)
console.log(`Created ${artifact.target}`)
}

View File

@@ -2833,8 +2833,8 @@ dependencies = [
] ]
[[package]] [[package]]
name = "senlinai_workbench" name = "senlinai-full"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "senlinai_workbench" name = "senlinai-full"
version = "0.1.0" version = "1.0.0"
description = "森林AI 项目工作台桌面端" description = "森林AI 完整版桌面客户端"
authors = ["森林AI"] authors = ["森林AI"]
edition = "2021" edition = "2021"

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "森林AI", "productName": "森林AI Full",
"version": "0.1.0", "version": "1.0.0",
"identifier": "ai.senlin.workbench", "identifier": "ai.senlin.workbench",
"build": { "build": {
"frontendDist": "../../web_v1/dist", "frontendDist": "../../web_v1/dist",
@@ -12,7 +12,7 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "森林AI", "title": "森林AI Full",
"width": 1280, "width": 1280,
"height": 820 "height": 820
} }

View File

@@ -9,5 +9,5 @@ npm run desktop:dev
``` ```
- Web 开发端口:`5180` - Web 开发端口:`5180`
- 桌面窗口默认尺寸`420 × 820` - 桌面窗口默认宽度`420`;启动后高度自动适配当前屏幕全高,并停靠在屏幕右侧
- 左右停靠状态保存在本地;在 Tauri 桌面环境中会移动实际窗口。 - 左右停靠状态保存在本地;在 Tauri 桌面环境中会移动实际窗口。

View File

@@ -1,4 +1,4 @@
use tauri::{PhysicalPosition, Window}; use tauri::{PhysicalPosition, PhysicalSize, Window};
#[tauri::command] #[tauri::command]
fn dock_window(window: Window, side: String) -> Result<(), String> { fn dock_window(window: Window, side: String) -> Result<(), String> {
@@ -9,12 +9,16 @@ fn dock_window(window: Window, side: String) -> Result<(), String> {
let window_size = window.outer_size().map_err(|error| error.to_string())?; let window_size = window.outer_size().map_err(|error| error.to_string())?;
let monitor_position = monitor.position(); let monitor_position = monitor.position();
let monitor_size = monitor.size(); let monitor_size = monitor.size();
let dock_size = PhysicalSize::new(window_size.width, monitor_size.height);
let x = if side == "left" { let x = if side == "left" {
monitor_position.x monitor_position.x
} else { } else {
monitor_position.x + monitor_size.width as i32 - window_size.width as i32 monitor_position.x + monitor_size.width as i32 - dock_size.width as i32
}; };
window
.set_size(dock_size)
.map_err(|error| error.to_string())?;
window window
.set_position(PhysicalPosition::new(x, monitor_position.y)) .set_position(PhysicalPosition::new(x, monitor_position.y))
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;

View File

@@ -31,7 +31,10 @@ export function App() {
const [experts, setExperts] = useState<Expert[]>([]) const [experts, setExperts] = useState<Expert[]>([])
const [aiSessions, setAISessions] = useState<AISession[]>([]) const [aiSessions, setAISessions] = useState<AISession[]>([])
const [view, setView] = useState<View>('home') const [view, setView] = useState<View>('home')
const [dockSide, setDockSide] = useState<DockSide>(() => localStorage.getItem('senlin-mini-dock') === 'left' ? 'left' : 'right') const [dockSide, setDockSide] = useState<DockSide>(() => {
if ('__TAURI_INTERNALS__' in window) return 'right'
return localStorage.getItem('senlin-mini-dock') === 'left' ? 'left' : 'right'
})
const [loading, setLoading] = useState(Boolean(session)) const [loading, setLoading] = useState(Boolean(session))
const [error, setError] = useState('') const [error, setError] = useState('')
const [action, setAction] = useState<Action>(null) const [action, setAction] = useState<Action>(null)