Skip to content

feat(docker): Headless 可选部署的 Dev-Tools 镜像与 Compose 配置 - #410

Open
thirsty5034 wants to merge 16 commits into
Stack-Cairn:mainfrom
thirsty5034:feat/headless-docker
Open

feat(docker): Headless 可选部署的 Dev-Tools 镜像与 Compose 配置#410
thirsty5034 wants to merge 16 commits into
Stack-Cairn:mainfrom
thirsty5034:feat/headless-docker

Conversation

@thirsty5034

@thirsty5034 thirsty5034 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

关联 issue

Resolves #380(Headless 运行时主体见 #409,本 PR 在其之上增加可选部署层)

概述

为 Headless 运行时提供可选的部署层:基于 mise 构建分层 Dev-Tools 镜像(minimal / core / full),并提供 headless 与 gateway 的 docker-compose 配置。本 PR 叠加在运行时 PR(#409)之上,增量仅为 Docker/Compose 工具链(11 个文件)

改动范围

  • 模块:Dockerfile.headless-toolsdocker/.github/workflows/liveagent-headless-tools.yml
  • 关键路径:
    • Dockerfile.headless-tools — 分层构建(minimal → core → full),apt/npm 镜像加速(mirrors.tuna.tsinghua.edu.cn、npmmirror),便于国内网络快速构建
    • docker/docker-compose.headless-{minimal,core,full}.yml — headless 服务器配置
    • docker/docker-compose.gateway.yml — gateway 部署配置
    • docker/mise.{core,full}.toml — 锁定工具链版本
    • .github/workflows/liveagent-headless-tools.yml — 在 main 分支 push/tag 时构建并推送镜像到 ghcr.nju.edu.cn(支持手动触发)

主要亮点

  • 完全可选:仅 main push/tag 或手动触发时构建镜像,不增加功能分支的 CI 负担
  • 高校镜像加速:清华 apt 源 + npmmirror 源,使国内网络环境也能顺利构建镜像
  • 已实际部署验证:GitHub Actions 构建镜像,在测试主机拉取运行(liveagent-full + liveagent-minimal),会话数据完好

截图 / 预览

基于构建镜像运行的 Headless WebUI(运行时见 #409):

headless WebUI

验证情况

  • 镜像已构建并推送至 ghcr.nju.edu.cn/thirsty5034/liveagent-*(手动触发,feat-headless-pr-gmain 标签均存在)
  • 已在远程 Docker 主机验证:docker compose up 启动 full 镜像,WebUI 可访问,既有会话数据保留

提交前检查

@StackCairn
StackCairn marked this pull request as draft August 9, 2026 10:26
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

PR governance checks passed. Awaiting human review.

@thirsty5034
thirsty5034 marked this pull request as ready for review August 9, 2026 10:27
@thirsty5034 thirsty5034 changed the title feat(docker): optional dev-tools images and compose profiles for headless feat(docker): Headless 可选部署的 Dev-Tools 镜像与 Compose 配置 Aug 9, 2026
@thirsty5034

Copy link
Copy Markdown
Contributor Author

已同步一个构建修复(de1c3c66):mise 安装 python@3.12 时默认做 GitHub artifact attestations 验证,在 GitHub Actions 上会因未认证 API 调用触发 rate limit(403)导致 full/core 镜像构建失败。现已在 Dockerfile 与 docker/mise.{core,full}.toml 中关闭该验证(MISE_PYTHON_GITHUB_ATTESTATIONS=false)。fork 侧 main 上用此修复构建的三个镜像(minimal/core/full,tag main)已全部成功并在容器环境验证通过。另外 #410 的 CI 首次运行需要维护者批准,烦请 Approve 一下 workflow 运行。

Replace direct AppHandle/State usage in the command layer with a plain
AppContext assembly plus a typed event emitter, so the same business
commands can run under both the desktop Tauri runtime and a headless
server. Desktop-only modules are feature-gated behind the default
desktop feature. Tests updated to assert the compat abstraction instead
of the raw tauri runtime paths.
A --no-default-features build strips the Tauri desktop runtime and runs
the same command surface over an axum HTTP/WebSocket server: GET /health,
GET /api/status, POST /api/invoke, GET /ws event broadcast, GET /* WebUI
SPA fallback and /proxy/{provider}/ BFF routes. Adds a same-origin
security model (CORS gate, optional Bearer token auth, rate limiting by
real peer IP) and a runtime-fallback mode for browsers without the Tauri
bridge.
Add a tauriBridge module exposing the same invoke interface to the
existing GUI code so the WebUI runs unchanged on the headless server
(runtime-fallback over fetch/WebSocket). Adds HeadlessFolderPicker and
headless-aware file upload. All GUI imports now go through the bridge
instead of @tauri-apps/api directly.
scripts/manifest/commands.json is the committed source of truth for the
234 Tauri commands; build_type_map.py derives the Rust type map,
gen_adapters.py regenerates commands/adapters.rs and verify_headless.py
asserts headless.rs dispatch arms match the manifest both ways. The
gen-verify CI job fails on any drift.
Add headless-rust (cargo check/test/build --no-default-features) and
gen-verify (generator drift check) jobs. Document the headless security
model and the command-registry workflow in the README.
…owser mode

agent-gui (headless WebUI) previously re-exported @tauri-apps/api modules
(tauriCore/tauriEvent/tauriOpener) which are native-Tauri-only and throw in
the browser, breaking all agent-ui fs/memory/proxy/settings paths
(workspaces all red, cannot add/activate workspace). Re-export from
lib/tauriBridge instead so invoke/listen/openUrl/revealItemInDir fall back
to the WebSocket/fetch bridge in headless mode.

Verified: tsc + vite build pass; new bundle's fsBackend invoke now imports
from tauriBridge chunk (fetch /api/invoke) instead of @tauri-apps/api/core.
The headless WebUI (agent-gui) handled system_pick_folder by simply
returning the initial_workdir, so clicking '打开本地文件夹' closed the
clone modal with no visible result (no directory picker). Intercept
system_pick_folder in lib/tauriBridge for non-Tauri runtimes and show a
browser prompt asking for the absolute server path, mirroring the
agent-gateway web shim. The headless server still validates the path.
…save)

Upstream added the model-failover settings command; the business fn was
already merged into commands/config/settings/commands.rs but headless.rs
had no dispatch arm and the manifest was stale, so saving model failover
settings from the browser UI failed with 'unknown command'.

- add dispatch arm in headless.rs (mirrors settings_save_memory)
- register settings_save_model_failover in scripts/manifest/commands.json
- regenerate commands/adapters.rs (235 adapters)
- verify_headless.py: manifest 235 == dispatch 235
…load

The upstream merge dropped the wired-up versions of two headless WebUI
features; the components/endpoints survived but the glue was lost:

1. system_pick_folder fell back to a bare absolute-path text prompt
   (HeadlessFolderPicker.tsx existed but was never wired). Route the
   headless intercept through openFolderPicker() instead, so the user can
   browse the server's filesystem (breadcrumbs + fs_list_dirs listing +
   fs_roots quick locations) rather than typing a path by hand. Dynamic
   import breaks the static cycle HeadlessFolderPicker -> fsBackend ->
   shims/tauriCore -> tauriBridge.

2. 'pick files' in headless mode called system_pick_readable_files, which
   errors ('file picker is unavailable in headless mode') because there is
   no native dialog. Restore the browser <input type=file> fallback
   (pickBrowserFiles) and route headless uploads (pick + drag&drop + paste)
   through the multipart endpoint POST /api/files/import via
   importReadableFilesViaMultipart (no base64 +33% expansion, no 413 on
   larger files). Tauri desktop paths unchanged.

Verified: tsc, biome (changed files), vite build (HeadlessFolderPicker
emitted as its own lazy chunk), verify_headless.py 235==235, 1509 frontend
tests pass.
The proxy baseUrl was hardcoded to a loopback (http://127.0.0.1:17890).
When the WebUI is reached through a public domain / reverse proxy
(https://liveagent.code.pp00.top), the frontend built cross-origin
proxy URLs pointing at 127.0.0.1, which the browser blocked via CORS
(no Access-Control-Allow-Origin, plus HTTPS->HTTP mixed content),
so model listing showed 'Failed to fetch' on the domain entry while
the loopback entry worked.

Now /api/invoke reads the browser Origin header and returns it as the
proxy baseUrl, so proxy requests stay same-origin on the accessing
entrypoint. Falls back to the startup loopback baseUrl for desktop/curl
callers without an Origin.
- Route GUI imports through @liveagent/ui after agent-ui extract
- Drop residual tauri:: usage from chat_history_set_cwd / HistorySetCwd
- Align append_segment with ChatHistoryAppendSegmentInput across
  manifest, adapters, and headless dispatch
@thirsty5034
thirsty5034 force-pushed the feat/headless-docker branch from f4f6ed4 to ca5d119 Compare August 13, 2026 04:12
@thirsty5034
thirsty5034 force-pushed the feat/headless-docker branch from ca5d119 to fa2a358 Compare August 13, 2026 04:14
appUpdates now invokes through tauriBridge's dynamic desktop import.
Mock the resolved bridge module so deferred install/check assertions
stay in sync under the CommonJS test loader.
Single Dockerfile.headless-tools with TARGET_PROFILE=minimal|core|full,
multi-arch amd64/arm64, shared base layers. Runtimes are managed by mise
(go/node/pnpm/bun/python; full adds Temurin 17 + Maven) and kept lean;
missing versions auto-install on first boot against a named volume.
Adds the liveagent-headless-tools GH Actions workflow to build and push
the three images to GHCR (main push/tag/workflow_dispatch).
docker-compose.headless-{minimal,core,full}.yml and
docker-compose.gateway.yml with data + mise volumes, per-profile
environment, restart policy and network mode; entrypoint.sh wires mise
lazy install with a bounded timeout. README documents the quick-start,
runtime version switching via MISE_*_VERSION and volume requirements.
@thirsty5034
thirsty5034 force-pushed the feat/headless-docker branch from fa2a358 to 4f2f93e Compare August 13, 2026 04:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(headless): headless server runtime for LiveAgent

1 participant