15 KiB
3. 现状分析:角色层级关系
setup-ai-workspace-all-in-one.sh(位于 console 仓库)在目标主机引导后,运行 setup-ai-workspace-all-in-one.yml(位于 playbooks 仓库)。其按导入顺序的角色层级:
setup-ai-workspace-all-in-one.sh [repo: xworkspace-console/scripts]
└─ ansible-playbook setup-ai-workspace-all-in-one.yml [repo: playbooks]
├─1 setup-nodejs.yml → role roles/vhosts/nodejs NodeJS(22.x)+yarn
├─2 setup-xworkspace-console.yaml WORKSPACE PORTAL/CONSOLE(内联 task,无 role)
│ apt: caddy,xfce4,python3,golang-go,google-chrome-stable,ttyd
│ git clone console → npm build;systemd --user: console(:17000)/api(:8788)/ttyd(:7681)/status.timer
│ Caddy 公网站点 workspace.svc.plus ⚠ 标准模式下也公开
├─3 setup-ai-agent-skills.yml → role roles/ai_agent_runtime AI WORKSPACE RUNTIME 核心
│ NodeJS(24.x)+Playwright;Agent CLI: opencode/gemini/codex/claude;Python/browser/docs/fonts
│ └─ role agent_skills → 注入 xworkspace-core-skills 市场技能
├─4 deploy_gateway_openclaw.yml → role roles/vhosts/gateway_openclaw OpenClaw(2026.5.28)
├─5 deploy_xworkmate_bridge_vhosts.yml BRIDGE + ACP 集群
│ ├─ import setup-xworkspace-console.yaml(带 bridge 变量再跑一次)
│ └─ roles: acp_server_codex / acp_server_opencode / acp_server_gemini /
│ acp_server_hermes / xworkmate_bridge(:8787 本地,公网 Caddy)
│ 域名默认 xworkmate-bridge.svc.plus → acp-bridge.onwalk.net
├─6 setup-vault.yaml → role roles/vhosts/vault Vault(1.20.4) :8200
├─7 setup-postgres-standalone.yaml → role roles/vhosts/postgres(dep: common) 原生 apt PG17 :5432
├─8 setup-litellm.yaml → role roles/vhosts/litellm pip 安装 :4000
├─9 deploy_QMD.yml → role roles/vhosts/qmd bun qmd, MCP :8181
├─10 deploy_agent_hermes.yml → role roles/vhosts/acp_server_hermes ⚠ Hermes 重复部署(与步骤5重叠)
└─11 setup-xfce-xrdp.yaml [可选] → role roles/vhosts/xfce_xrdp_minimal
→ 拆分为 xfce_desktop_minimal_runtime + remote_desktop_xrdp_server
3.1 关键发现
- 公开面冲突:步骤 2/5 在
ai_workspace_security_level != strict时为workspace.svc.plus部署公网 Caddy 站点,导致 Portal 也对外暴露,与“Bridge 唯一公开”冲突。 - Hermes 重复部署:步骤 5(ACP 集群内)与步骤 10(独立)各部署一次,冗余。
- 版本固定点分散:OpenClaw、Vault 已有固定变量;NodeJS 有但偏宽松(
22.x/24.x);Hermes、QMD、LiteLLM 缺少显式版本/源固定。
4. 关键设计决策
4.1 对外公开面:Bridge Only
- Bridge 是默认唯一公开的服务:
XWORKMATE_BRIDGE_PUBLIC_ACCESS默认true,公网域名由XWORKMATE_BRIDGE_DOMAIN自定义传入(目标主机acp-bridge.onwalk.net)。如需关闭可显式设false。 xworkspace_console_public_access默认false(仅XWORKSPACE_CONSOLE_PUBLIC_ACCESS=true时公开)。GATEWAY_OPENCLAW_PUBLIC_ACCESS/VAULT_PUBLIC_ACCESS默认false;其余(QMD / Hermes / PG / LiteLLM)维持本地监听(127.0.0.1),不部署公网 Caddy 站点。- 实现方式:最小改动——仅调整默认值/开关并对齐 env 名称(§2.1),不删除既有 public_access 能力(保留可手动放开)。
4.2 Hermes 去重
setup-ai-workspace-all-in-one.yml中移除步骤 10 的独立deploy_agent_hermes.yml导入(步骤 5 的 ACP 集群已含 hermes)。- 保留
deploy_agent_hermes.yml文件本身,供单独部署场景使用,仅从 all-in-one 聚合链里去重。
4.3 运行模式矩阵(docker / k3s / systemd)
引入一个校验型变量 ai_workspace_runtime_modes(列表),在 all-in-one 顶部加一段 assert 守卫,不重写各组件部署逻辑:
| 约束 | 规则 |
|---|---|
| 互斥 | docker 与 k3s 不可同时出现 |
| 可组合 | docker + systemd 允许;systemd 可单独 |
| 默认 | ['docker','systemd'](多数 Agent 服务 systemd,PostgreSQL 走 docker compose) |
组件与模式映射(复用现有能力,不新增重型实现):
| 组件 | systemd | docker | k3s |
|---|---|---|---|
| Console / API / ttyd / Bridge / ACP / OpenClaw / QMD / LiteLLM | ✅ 默认 | — | — |
| PostgreSQL | 可选 | ✅ 默认 docker compose | 可选 |
| Vault | vault_deploy_mode=systemd |
— | vault_deploy_mode=kubernetes(k3s) |
守卫伪代码(放入 all-in-one 顶层 play):
- name: Validate runtime mode combination
hosts: all
gather_facts: false
tasks:
- assert:
that:
- not ('docker' in ai_workspace_runtime_modes and 'k3s' in ai_workspace_runtime_modes)
- ai_workspace_runtime_modes | length > 0
fail_msg: "docker 与 k3s 互斥;请选择 docker/k3s/systemd 的合法组合。"
4.4 PostgreSQL 默认 docker compose
- 新增开关
postgresql_deploy_mode,默认compose。 compose模式:在roles/vhosts/postgres增加一条 compose 部署路径(镜像版本固定,端口/口令复用现有变量),与现有原生 apt 路径并存、互斥择一。- 不删除原生 apt 路径(设
postgresql_deploy_mode=native可回退)。
4.5 QMD / LiteLLM 源仓库与版本固定
- QMD:安装源指向
https://github.com/ai-workspace-services/qmd.git,新增qmd_source_repo/qmd_version变量固定。 - LiteLLM:安装源指向
https://github.com/ai-workspace-services/litellm.git,新增litellm_source_repo/litellm_version变量固定。
10. 并发优化设计(深入分析 + 定制策略)
目标:在不丢 tasks、不破坏现有 role 结构、不牺牲稳定性的前提下,提升单机部署速度。 总策略:三相执行——Phase 1 串行(系统全局/抢锁)→ Phase 2 并发(互不依赖 I/O)→ Phase 3 串行(确定性收口)。不要把多个 role 直接改并发;只把“耗时、互不依赖、不写同一文件、不抢同一锁”的任务做
async,最后async_status收口。
10.1 三相模型(权威定义)
Phase 1 — 必须串行(抢锁 / 修改系统全局状态):
apt update、apt install、dpkg 相关、添加 apt repo / keyring、用户/用户组创建、基础目录创建、基础权限设置、Docker 安装、Caddy 安装、systemd 基础准备、防火墙基础规则、全局 pip / 全局 npm(-g) 安装。
Phase 2 — 可以并发(互不依赖、不写同一文件、不操作同一锁):
docker pull 多镜像、下载多个二进制、git clone 多仓库、go build、不同目录的 npm/pnpm install、不同目录的前端 build、拉取插件、拉取静态资源、生成互不冲突的服务配置、初始化各服务独立工作目录、各服务独立 prepare 脚本。
Phase 3 — 必须串行(收口确定性):
渲染最终配置、systemd daemon-reload、enable service、按依赖顺序 start/restart、health check、输出部署结果、清理临时文件。
10.2 关键定制结论(针对本 Playbook 的深入分析)
- 所有
npm -g共享同一 prefix → 必须 Phase 1 串行。roles/vhosts/nodejs设npm_config_prefix=/usr/local/lib/npm;Agent CLI(opencode-ai / @google/gemini-cli / @openai/codex / @anthropic-ai/claude-code)、yarn、openclaw@ver全部npm -g到该 prefix。并发会争用同一node_modules/.staging与 npm cache 锁 → 不可并发。 - LiteLLM 已改为独立 Python 3.13 venv,但依赖安装仍应串行收口。它不再写系统 site-packages,但
pip install litellm[proxy]依赖树大、网络失败率高,默认方向应是优先消费离线 wheelhouse,在线 venv 安装仅作 fallback。 - 真正安全的 Phase 2 候选是“外部 I/O 预取”:git clone、二进制下载、docker pull、独立目录的前端 build、runtime release 下载。它们不碰 dpkg/npm-prefix/pip 全局锁,且写入各自独立路径。
- 跨 sub-playbook 的并发收益最大处在 Shell 预取层:11 个步骤由 ansible 顺序导入,play 间难并发;把可并行的 I/O 上提到 bootstrap 的 Phase 2 fork 池(§10.5)预取,ansible 仅消费已就位产物,是收益/风险比最高的定制。
- 离线包优先(呼应 TODO):已有离线安装包/已导入镜像时,Phase 2 预取应短路跳过,直接复用缓存。
10.3 现状任务 → 三相映射
| 步骤 / role | Phase 1(串行) | Phase 2(可并发预取) | Phase 3(串行收口) |
|---|---|---|---|
| 1 nodejs | nodesource keyring/repo、apt install nodejs、npm -g yarn |
— | — |
| 2 console | apt(caddy/xfce4/python3/golang-go/chrome)+chrome repo/key、用户/目录/权限 | get_url ttyd 二进制、git clone console、dashboard npm install && build(独立目录) |
渲染 systemd unit/env/portal-services.json、daemon-reload/enable/restart、Caddy 写入+reload |
| 3 ai_agent_runtime | npm -g Agent CLI、全局 pip(python deps)、apt(browser/docs/fonts)、Playwright(-g) |
agent_skills 拉取 core-skills 市场(独立目录) |
校验/health、register 输出 |
| 4 gateway_openclaw | npm -g openclaw@ver+插件 |
(插件若独立目录拉取可并发) | 配置渲染、systemd、版本 assert、health |
| 5 bridge + ACP | 同步 console;acp_server_* 的全局安装部分 | xworkmate-go-core 二进制下载/放置、acp 各自独立工作目录 prepare |
配置渲染、按依赖 requires acp-*.service 顺序启动、validation |
| 6 vault | (systemd 基础准备) | get_url vault zip 下载、解压放置 |
配置渲染、systemd/init、health |
| 7 postgres | Docker 安装、common 基础 | docker pull PG 镜像、初始化独立 data 目录 |
compose 渲染、compose up、health |
| 8 litellm | apt/Homebrew Python 准备、Python 3.13 venv 创建、离线 wheelhouse 或 fallback pip 安装 | 下载 litellm-runtime-<distro>-<version>-<arch>.tar.gz、校验 SHA256、准备 packages/pip/metadata/runtime.env |
配置渲染、Prisma client generate、systemd/launchd、health(:4000/health) |
| 9 qmd | (bun 运行时安装,全局) | 条件并发:qmd 拉取/bun install(隔离于 ~/.bun,不碰 dpkg) |
qmd.env/index.yml 渲染、systemd --user、health(:8181) |
| 11 xfce(可选) | apt 桌面包/xrdp/chrome、npm -g/Playwright |
— | xrdp 服务 enable/start、会话配置 |
说明:标“条件并发”的(如 qmd
bun)仅当确认其只写入服务自身用户目录、且不与同时段其它全局安装争锁时才纳入 Phase 2,否则归 Phase 1。
10.4 Ansible 层 async 模式(保留全部属性)
在单个 play 内对 Phase 2 任务用 poll:0 发起、集中 async_status 收口。register/when/notify/tags/become/failed_when 一律保留:
- name: Download ttyd binary (async)
ansible.builtin.get_url: { url: "...", dest: "{{ ttyd_path }}", mode: "0755" }
async: 1800
poll: 0
register: ttyd_job
- name: Clone xworkspace-console (async)
ansible.builtin.git: { repo: "...", dest: "{{ repo_dir }}", version: main, depth: 1 }
become_user: "{{ xworkspace_console_user }}"
async: 1800
poll: 0
register: console_clone_job
# …其它独立 Phase 2 任务一并 poll:0 发起…
- name: Collect async Phase-2 jobs
ansible.builtin.async_status: { jid: "{{ item }}" }
register: p2
until: p2.finished
retries: 120
delay: 5
loop:
- "{{ ttyd_job.ansible_job_id }}"
- "{{ console_clone_job.ansible_job_id }}"
- 收口铁律:任一 Phase 2 产物在被 Phase 3 消费前必须
finished。 - dpkg/全局 npm/全局 pip 绝不
async;LiteLLM venv 安装虽然不再是全局 pip,也应在 wheelhouse 准备完成后串行执行,便于失败定位与重试(§10.2)。
10.5 Shell 层动态 fork 并发(≤ CPU 核心数 × 2,预取层)
bootstrap 把可并行的外部 I/O 收敛到一个负载自适应的有界 fork 池,在 ansible 前(Phase 2 预取)与摘要阶段使用。硬上限为目标主机在线 CPU 核心数的 2 倍;AI_WORKSPACE_MAX_PARALLEL_JOBS 可设更低人工上限,默认 auto。每次启动子任务前读取 1 分钟 load average,按 min(人工上限, 2 × CPU - ceil(load1)) 动态收缩,最低保留 1 路:
CPU_COUNT="$(getconf _NPROCESSORS_ONLN)"
HARD_LIMIT=$((CPU_COUNT * 2))
LOAD_CEILING="$(awk -v load="$(cut -d' ' -f1 /proc/loadavg)" 'BEGIN { n=int(load); print load > n ? n + 1 : n }')"
DYNAMIC_LIMIT=$((HARD_LIMIT - LOAD_CEILING))
[ "$DYNAMIC_LIMIT" -ge 1 ] || DYNAMIC_LIMIT=1
run_bounded() {
while [ "$(jobs -rp | wc -l)" -ge "$DYNAMIC_LIMIT" ]; do wait -n; done
"$@" &
}
# Phase 2 预取:5 仓库 pull + 二进制下载 + 镜像 pull(离线包存在则短路跳过)
for r in playbooks console core-skills qmd litellm; do run_bounded fetch_repo "$r"; done
for b in ttyd vault xworkmate-go-core; do run_bounded fetch_binary "$b"; done
for img in "${PG_IMAGES[@]}"; do run_bounded docker_pull "$img"; done
for p in "${pids[@]}"; do wait "$p" || rc=1; done
[ "$rc" -eq 0 ] || { echo "[phase2] 存在失败子任务"; exit 1; }
- 健康探测 fan-out(摘要前):对 Portal/Bridge/OpenClaw/QMD/Hermes/PG/Vault/LiteLLM 的
systemctl is-active+curl使用同一动态上限,统一按固定顺序汇总。 - 每子进程带日志前缀(
[repo:qmd]/[bin:vault]),失败非零退出、不静默。 - 串行保留:
ansible-playbook主执行(Phase 1/Phase 3 由其内部保证)、一次性 token/摘要打印。
10.6 不允许丢失的内容(硬约束)
逐一保留现有所有 tasks 及属性:apt/package、用户/目录/权限、env 文件、systemd unit 渲染、Caddy/Nginx、Docker/compose、服务启动、health check、debug、失败处理、handlers、tags、become、when、notify、register。不得因并发删除/合并/跳过任何已有任务;仅改变“何时等待”(poll:0+async_status),不改变“做什么”。
10.7 安全的全局提速(与 async 互补,不改 task 语义)
ansible.cfg(已存在)可叠加低风险项:
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
[ssh_connection]
pipelining = true
并补足 TODO 关注点:APT/部署锁需安全等待(重试而非强删锁),保证二次幂等执行成功。strategy: free 单机收益有限、改变执行观感,默认不启用。
10.8 验收(等价性回归)
- 优化前后
ansible-playbook --list-tasks任务集合一致(无丢失/合并)。 - 每个
async任务都有对应async_status收口,无悬挂 job。 - Phase 1(apt/全局 npm/全局 pip/dpkg、LiteLLM venv 安装)与 Phase 3(daemon-reload/enable/start/health/摘要/清理)仍严格串行。
- Phase 2 任务互不写同一文件、不抢同一锁;离线包存在时短路跳过。
- 连续两次执行均成功、
changed=0幂等行为不变;Shell fork 池失败子任务非零退出且日志可见。