playbooks/setup-xworkspace-console.yaml
Haitao Pan 2ef144d572 fix(console): serve dashboard/dist via local python http.server (not npm/caddy)
Prebuilt runtime ships only dashboard/dist (no package.json) so npm run
preview ENOENT-crash-loops (254). console is a local-only static backend on
127.0.0.1:17000 (dashboard is a routerless SPA); serve it with python3
-m http.server on both Linux (console.service) and macOS (console.plist) —
no second caddy (avoids clashing with the system caddy on :80; console is
local-only and not proxied by default). Gate the apt caddy install on
caddy_enabled (true on public-IP Linux VPS for the bridge ingress; macOS
installs no caddy).

Verified: debian13 + ubuntu26.04 console.service active serving 17000=200;
macOS python3 serves the same dist locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:44:01 +08:00

839 lines
37 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
- name: Setup AI Agentic Workspace runtime
hosts: "{{ xworkspace_console_hosts | default('all') }}"
become: true
gather_facts: true
module_defaults:
ansible.builtin.apt:
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
vars:
# 跟随连接用户,与 xworkspace_console_home(ansible_env.HOME) 保持一致:
# 以 root 连接时 user=root/home=/root避免 become_user=ubuntu 去 link /root
# 下的 unit 文件而报 "src does not exist"root 家目录 700ubuntu 无法进入)。
xworkspace_console_user: "{{ ansible_env.USER | default('ubuntu') }}"
xworkspace_console_public_access: false
xworkspace_console_domain: workspace.svc.plus
xworkspace_console_home: "{{ ansible_env.HOME | default('/home/ubuntu') }}"
xworkspace_console_root: "{{ xworkspace_console_home }}/.local/state/ai-workspace"
xworkspace_console_repo_dir: "{{ xworkspace_console_home }}/xworkspace-console"
xworkspace_console_runtime_archive: "{{ lookup('ansible.builtin.env', 'XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE') | default('', true) }}"
ai_workspace_prebuilt_components_required: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_PREBUILT_COMPONENTS_REQUIRED') | default('false', true) | bool }}"
xworkspace_console_dashboard_dir: "{{ xworkspace_console_repo_dir }}/dashboard"
# 预编译 runtime tar 的 manifest.json 记 apiBinary: bin/xworkspace-api
# 二进制落在 bin/(非源码布局的 api/)。对齐之,否则服务 203/EXEC 崩溃重启。
xworkspace_console_api_dir: "{{ xworkspace_console_repo_dir }}/bin"
xworkspace_console_api_binary: "{{ xworkspace_console_api_dir }}/xworkspace-api"
xworkspace_console_runtime_marker: "{{ xworkspace_console_repo_dir }}/.runtime-archive-sha256"
xworkspace_console_api_working_dir: "{{ xworkspace_console_repo_dir }}"
xworkspace_console_api_exec: "{{ xworkspace_console_api_binary }}"
xworkspace_console_scripts_dir: "{{ xworkspace_console_root }}/scripts"
xworkspace_console_config_dir: "{{ xworkspace_console_home }}/.config/xworkspace"
xworkspace_console_url: http://127.0.0.1:17000
xworkspace_console_port: 17000
xworkspace_console_api_port: 8788
xworkspace_console_ttyd_port: 7681
xworkspace_console_enable_ttyd: true
xworkspace_console_install_chrome: true
xworkspace_console_browser_package: >-
{{ 'google-chrome-stable' if ansible_architecture in ['x86_64', 'amd64'] else '' }}
xworkspace_console_browser_binary: >-
{{ '/usr/bin/google-chrome' if ansible_architecture in ['x86_64', 'amd64'] else '/usr/local/bin/chromium' }}
ai_workspace_offline_active: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_OFFLINE_ACTIVE') | default('false', true) | bool }}"
xworkspace_console_autostart_enabled: true
xworkspace_console_ttyd_binary_path: /usr/local/bin/ttyd
ai_workspace_auth_token: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_AUTH_TOKEN') | default('', true) }}"
xworkspace_console_auth_token: >-
{{ lookup('ansible.builtin.env', 'XWORKSPACE_CONSOLE_AUTH_TOKEN')
| default(lookup('ansible.builtin.env', 'AI_WORKSPACE_AUTH_TOKEN'), true)
| default(lookup('ansible.builtin.env', 'BRIDGE_AUTH_TOKEN'), true)
| default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_AUTH_TOKEN'), true)
| default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN'), true) }}
xworkspace_console_review_auth_token: "{{ lookup('ansible.builtin.env', 'BRIDGE_REVIEW_AUTH_TOKEN') | default('', true) }}"
xworkspace_console_portal_services:
- key: litellm
name: LiteLLM Admin UI
url: http://localhost:4000/ui
openMode: iframe
healthUrl: http://127.0.0.1:4000/ui
description: Model routing and provider administration.
icon: chart
match:
- litellm
- lite
port: 4000
role: model-router
- key: openclaw
name: OpenClaw
url: http://127.0.0.1:18789/channels
openMode: external
healthUrl: http://127.0.0.1:18789/channels
description: Gateway dashboard.
icon: claw
match:
- openclaw
- gateway
port: 18789
role: gateway
- key: vault
name: Vault Server
url: http://127.0.0.1:8200/ui
openMode: external
healthUrl: http://127.0.0.1:8200/ui
description: Vault UI.
icon: shield
match:
- vault
port: 8200
- key: terminal
name: Terminal
url: http://127.0.0.1:7681
openMode: iframe
healthUrl: http://127.0.0.1:7681
description: Local ttyd terminal.
icon: terminal
match:
- ttyd
- terminal
port: 7681
tasks:
- name: Install Google Chrome apt repository prerequisites
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
- xdg-utils
state: present
install_recommends: false
update_cache: true
when: ansible_os_family != 'Darwin'
- name: Ensure Google Chrome apt keyring directory exists
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
owner: root
group: root
mode: "0755"
when: ansible_os_family != 'Darwin'
- name: Install Google Linux signing key
ansible.builtin.shell: |
set -euo pipefail
tmp="$(mktemp)"
curl -fsSL "https://dl.google.com/linux/linux_signing_key.pub" -o "$tmp"
gpg --dearmor -o /etc/apt/keyrings/google-linux-signing-key.gpg "$tmp"
rm -f "$tmp"
chmod 0644 /etc/apt/keyrings/google-linux-signing-key.gpg
args:
executable: /bin/bash
creates: /etc/apt/keyrings/google-linux-signing-key.gpg
when:
- ansible_architecture in ['x86_64', 'amd64']
- not ai_workspace_offline_active
- ansible_os_family != 'Darwin'
- name: Configure Google Chrome apt repository
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/google-chrome.list
owner: root
group: root
mode: "0644"
content: "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-linux-signing-key.gpg] https://dl.google.com/linux/chrome/deb/ stable main\n"
when:
- ansible_architecture in ['x86_64', 'amd64']
- not ai_workspace_offline_active
- ansible_os_family != 'Darwin'
- name: Refresh apt cache after Google Chrome repository changes
ansible.builtin.apt:
update_cache: true
when:
- ansible_architecture in ['x86_64', 'amd64']
- not ai_workspace_offline_active
- ansible_os_family != 'Darwin'
- name: Install AI Agentic Workspace runtime packages
ansible.builtin.apt:
update_cache: true
name: >-
{{
['xfce4', 'python3', 'golang-go']
+ (['caddy'] if caddy_enabled | default(true) | bool else [])
+ ([xworkspace_console_browser_package] if xworkspace_console_browser_package | length > 0 else [])
}}
state: present
# xfce4 元包会拉入整套桌面,安装期间偶发重置网络/拖长,导致前台 SSH 会话
# 掉线 → ansible 误判 UNREACHABLE实际包已在主机装完。改异步执行 + 轮询,
# 让安装在主机后台跑、ansible 重连轮询,掉线也不影响。
async: "{{ ai_workspace_runtime_apt_async | default(1800) | int }}"
poll: 15
when: ansible_os_family != 'Darwin'
- name: Ensure ttyd binary target directory exists
ansible.builtin.file:
path: "{{ xworkspace_console_ttyd_binary_path | dirname }}"
state: directory
owner: root
group: root
mode: "0755"
when: ansible_os_family != 'Darwin'
- name: Set ttyd binary download metadata
ansible.builtin.set_fact:
xworkspace_console_ttyd_arch: >-
{{ 'x86_64' if ansible_architecture in ['x86_64', 'amd64'] else
'aarch64' if ansible_architecture in ['aarch64', 'arm64'] else
'' }}
- name: Fail when ttyd binary architecture is unsupported
ansible.builtin.fail:
msg: "Unsupported architecture for ttyd binary: {{ ansible_architecture }}"
when: xworkspace_console_ttyd_arch == ''
- name: Download ttyd release binary
ansible.builtin.get_url:
url: "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.{{ xworkspace_console_ttyd_arch }}"
dest: "{{ xworkspace_console_ttyd_binary_path }}"
mode: "0755"
owner: root
group: root
force: false
when:
- not ai_workspace_offline_active
- ansible_os_family != 'Darwin'
- name: Verify ttyd binary
ansible.builtin.command: "{{ xworkspace_console_ttyd_binary_path }} --version"
changed_when: false
register: ttyd_version_check
failed_when: ttyd_version_check.rc != 0
when: ansible_os_family != 'Darwin'
- name: Find ttyd path on macOS
ansible.builtin.command: which ttyd
register: ttyd_macos_path
changed_when: false
when: ansible_os_family == 'Darwin'
- name: Set ttyd binary path for macOS
ansible.builtin.set_fact:
xworkspace_console_ttyd_binary_path: "{{ ttyd_macos_path.stdout }}"
when: ansible_os_family == 'Darwin'
- name: Show ttyd binary version
ansible.builtin.debug:
msg: "{{ ttyd_version_check.stdout | default('ttyd path: ' + ttyd_macos_path.stdout | default('Unknown')) }}"
- name: Ensure AI Agentic Workspace user exists
ansible.builtin.user:
name: "{{ xworkspace_console_user }}"
state: present
create_home: true
shell: /bin/bash
when:
- xworkspace_console_user != 'root'
- ansible_os_family != 'Darwin'
- name: Ensure AI Agentic Workspace directories exist
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0755"
loop: "{{ _directories | reject('search', 'systemd') | list if ansible_os_family == 'Darwin' else _directories }}"
vars:
_directories:
- "{{ xworkspace_console_root }}"
- "{{ xworkspace_console_scripts_dir }}"
- "{{ xworkspace_console_repo_dir }}"
- "{{ xworkspace_console_home }}/.config"
- "{{ xworkspace_console_config_dir }}"
- "{{ xworkspace_console_home }}/.config/autostart"
- "{{ xworkspace_console_home }}/.config/systemd"
- "{{ xworkspace_console_home }}/.config/systemd/user"
- "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants"
- "{{ xworkspace_console_home }}/.config/systemd/user/timers.target.wants"
- name: Deploy AI Agentic Workspace status generator
ansible.builtin.copy:
dest: "{{ xworkspace_console_scripts_dir }}/generate-status.py"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0755"
content: |
#!/usr/bin/env python3
from __future__ import annotations
import datetime as dt
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from urllib import request, error
HOME = Path("{{ xworkspace_console_home }}")
ROOT = Path("{{ xworkspace_console_root }}")
CONSOLE_DIR = Path("{{ xworkspace_console_dashboard_dir }}")
LITELLM_CONFIG = HOME / ".local/share/xworkspace/litellm-config.yaml"
OUTPUT = CONSOLE_DIR / "public/status.json"
def run(*cmd: str) -> str:
try:
return subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL).strip()
except Exception:
return ""
def systemd_state(unit: str, scope: str = "--user") -> str:
cmd = ["systemctl"]
if scope == "--user":
cmd.append("--user")
cmd += ["is-active", unit]
return run(*cmd) or "unknown"
def health(url: str) -> tuple[str, str]:
try:
with request.urlopen(url, timeout=2) as response:
payload = response.read().decode("utf-8", "replace").strip()
return ("active" if response.status < 400 else "degraded", payload[:160] or "ok")
except error.HTTPError as exc:
return ("degraded", f"HTTP {exc.code}")
except Exception as exc:
return ("inactive", str(exc).splitlines()[0][:160])
def probe_unit(name: str, label: str, group: str, endpoint: str | None = None, candidates: list[str] | None = None, scope: str = "--user") -> dict:
candidates = candidates or [name]
state = "unknown"
found = None
for candidate in candidates:
state = systemd_state(candidate, scope=scope)
if state != "unknown":
found = candidate
break
details = f"{label} {state}"
if endpoint:
endpoint_state, endpoint_detail = health(endpoint)
if endpoint_state == "active" and state in {"active", "unknown"}:
state = "active"
elif endpoint_state == "degraded" and state == "active":
state = "degraded"
details = endpoint_detail
return {
"id": found or name,
"name": found or name,
"label": label,
"state": state,
"details": details,
"endpoint": endpoint,
"group": group,
}
def parse_models(path: Path) -> list[dict]:
if not path.exists():
return []
text = path.read_text(encoding="utf-8", errors="replace")
models = []
current = None
for line in text.splitlines():
match = re.search(r"^\s*model_name:\s*(.+?)\s*$", line)
if match:
current = match.group(1).strip().strip('"\'')
models.append({
"name": current,
"state": "active",
"details": "Detected from litellm-config.yaml",
})
return models
services = [
probe_unit("xworkspace-console.service", "console", "core", "{{ xworkspace_console_url }}", ["xworkspace-console.service"], "--user"),
probe_unit("xworkspace-litellm.service", "litellm", "core", "http://127.0.0.1:4000/health/liveliness", ["xworkspace-litellm.service", "litellm.service"], "--user"),
probe_unit("xworkmate-bridge.service", "xworkmate-bridge", "workspace", None, ["xworkmate-bridge.service"], "--user"),
probe_unit("openclaw-gateway.service", "openclaw", "workspace", None, ["openclaw-gateway.service", "openclaw.service"], "--user"),
probe_unit("hermes-gateway.service", "hermes", "workspace", None, ["hermes-gateway.service", "hermes.service"], "--user"),
probe_unit("qmd.service", "QMD", "workspace", None, ["qmd.service", "QMD.service", "xworkspace-qmd.service"], "--user"),
probe_unit("xworkspace-ttyd.service", "ttyd", "workspace", "http://127.0.0.1:7681", ["xworkspace-ttyd.service"], "--user"),
probe_unit("ttyd.service", "ttyd(system)", "workspace", "http://127.0.0.1:7681", ["ttyd.service"], "system"),
]
litellm = next((item for item in services if item["label"] == "litellm"), None)
terminal = next((item for item in services if item["label"].startswith("ttyd")), None)
console = next((item for item in services if item["label"] == "console"), None)
output = {
"generatedAt": dt.datetime.now(dt.timezone.utc).isoformat(),
"summary": "Workspace healthy" if all(item["state"] == "active" for item in services if item["label"] != "QMD") else "Workspace degraded",
"services": services,
"models": parse_models(LITELLM_CONFIG),
"terminalTranscript": "ubuntu@workspace:~$ openclaw status\nstatus snapshot generated from live probes",
"latencySummary": "live probe",
"latencySource": litellm["details"] if litellm else "no litellm probe",
"rpmSummary": "live probe",
"rpmSource": "xworkspace-litellm.service",
"costSummary": "live probe",
"costSource": "litellm-config.yaml",
"consoleState": console["state"] if console else "unknown",
"consoleDetails": console["details"] if console else "No console probe",
"terminalState": terminal["state"] if terminal else "unknown",
"terminalDetails": terminal["details"] if terminal else "No ttyd probe",
}
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
tmp = OUTPUT.with_suffix(".json.tmp")
tmp.write_text(json.dumps(output, indent=2, sort_keys=True), encoding="utf-8")
os.replace(tmp, OUTPUT)
print(f"Wrote {OUTPUT}")
- name: Deploy AI Agentic Workspace status service
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-status.service"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0644"
content: |
[Unit]
Description=AI Agentic Workspace status snapshot generator
After=xworkspace-console.service xworkspace-litellm.service
Wants=xworkspace-console.service xworkspace-litellm.service
[Service]
Type=oneshot
ExecStart={{ xworkspace_console_scripts_dir }}/generate-status.py
when: ansible_os_family != 'Darwin'
- name: Deploy AI Agentic Workspace status timer
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-status.timer"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0644"
content: |
[Unit]
Description=Refresh AI Agentic Workspace status snapshot periodically
[Timer]
OnBootSec=5
OnUnitActiveSec=8
Unit=xworkspace-status.service
Persistent=true
[Install]
WantedBy=timers.target
when: ansible_os_family != 'Darwin'
- name: Deploy Xinit entrypoint for AI Agentic Workspace
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.xinitrc"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0755"
content: |
#!/usr/bin/env bash
set -euo pipefail
export XDG_CURRENT_DESKTOP=XFCE
export DESKTOP_SESSION=xfce
export BROWSER={{ xworkspace_console_scripts_dir }}/chrome-app.sh
mkdir -p "$HOME/.config/xworkspace"
if [ -z "${XAUTHORITY:-}" ]; then
export XAUTHORITY="$HOME/.Xauthority"
fi
cat > "$HOME/.config/xworkspace/session.env" <<EOF
DISPLAY=${DISPLAY:-}
XAUTHORITY=${XAUTHORITY:-}
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-}
DBUS_SESSION_BUS_ADDRESS=${DBUS_SESSION_BUS_ADDRESS:-}
EOF
systemctl --user import-environment DISPLAY XAUTHORITY XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS >/dev/null 2>&1 || true
dbus-update-activation-environment --systemd DISPLAY XAUTHORITY XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS >/dev/null 2>&1 || true
systemctl --user daemon-reload >/dev/null 2>&1 || true
systemctl --user start xworkspace-console.service >/dev/null 2>&1 || true
exec dbus-launch --exit-with-session startxfce4
when: ansible_os_family != 'Darwin'
- name: Point Xsession to Xinit entrypoint
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.xsession"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0755"
content: |
#!/usr/bin/env bash
exec "$HOME/.xinitrc"
when: ansible_os_family != 'Darwin'
- name: Deploy AI Agentic Workspace console launcher script
ansible.builtin.copy:
dest: "{{ xworkspace_console_scripts_dir }}/start.sh"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0755"
content: |
#!/usr/bin/env bash
set -euo pipefail
exit 0
- name: Deploy AI Agentic Workspace Chrome launcher wrapper
ansible.builtin.copy:
dest: "{{ xworkspace_console_scripts_dir }}/chrome-app.sh"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0755"
content: |
#!/usr/bin/env bash
set -euo pipefail
mkdir -p "$HOME/.config/xworkspace-chrome"
exec {{ xworkspace_console_browser_binary }} \
--app=http://localhost:17000 \
--user-data-dir="$HOME/.config/xworkspace-chrome" \
--profile-directory=Default \
--no-first-run \
--disable-session-crashed-bubble \
--disable-sync \
--new-window
- name: Download XWorkspace Console runtime release
ansible.builtin.get_url:
url: "https://github.com/ai-workspace-lab/xworkspace-console/releases/download/latest-runtime/xworkspace-console-runtime-{{ ansible_system | lower }}-{{ 'amd64' if ansible_architecture in ['x86_64', 'amd64'] else 'arm64' }}.tar.gz"
dest: "/tmp/xworkspace-console-runtime.tar.gz"
mode: "0644"
force: true
register: xworkspace_console_runtime_download
until: xworkspace_console_runtime_download is succeeded
retries: 3
delay: 5
when: xworkspace_console_runtime_archive | length == 0
- name: Set runtime archive path
ansible.builtin.set_fact:
xworkspace_console_runtime_archive_resolved: "{{ xworkspace_console_runtime_archive if (xworkspace_console_runtime_archive | length > 0) else '/tmp/xworkspace-console-runtime.tar.gz' }}"
- name: Validate packaged XWorkspace Console runtime
ansible.builtin.stat:
path: "{{ xworkspace_console_runtime_archive_resolved }}"
register: xworkspace_console_runtime_archive_stat
- name: Require packaged XWorkspace Console runtime
ansible.builtin.assert:
that:
- xworkspace_console_runtime_archive_stat.stat.exists | default(false)
fail_msg: "A valid XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE is required or download failed."
- name: Inspect installed XWorkspace Console runtime marker
ansible.builtin.slurp:
path: "{{ xworkspace_console_runtime_marker }}"
register: xworkspace_console_runtime_marker_content
failed_when: false
- name: Stop xworkspace-console services before extracting
ansible.builtin.shell: |
uid="$(id -u {{ xworkspace_console_user }})"
systemctl start "user@${uid}.service" || true
for svc in xworkspace-status.timer xworkspace-status.service xworkspace-console.service; do
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user stop "$svc" || true
done
become: true
changed_when: false
when: ansible_os_family != 'Darwin'
- name: Install packaged XWorkspace Console runtime
ansible.builtin.unarchive:
src: "{{ xworkspace_console_runtime_archive_resolved }}"
dest: "{{ xworkspace_console_repo_dir | dirname }}"
remote_src: true
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
when:
- xworkspace_console_runtime_archive_stat.stat.exists | default(false)
- >-
(xworkspace_console_runtime_marker_content.content | default('') | b64decode | trim)
!= (xworkspace_console_runtime_archive_stat.stat.checksum | default(''))
or not (xworkspace_console_api_binary is file)
or not ((xworkspace_console_dashboard_dir ~ '/dist/index.html') is file)
- name: Record installed XWorkspace Console runtime checksum
ansible.builtin.copy:
dest: "{{ xworkspace_console_runtime_marker }}"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0644"
content: "{{ xworkspace_console_runtime_archive_stat.stat.checksum }}\n"
when:
- xworkspace_console_runtime_archive_stat.stat.exists | default(false)
- name: Deploy AI Workspace portal service configuration
ansible.builtin.copy:
dest: "{{ xworkspace_console_config_dir }}/portal-services.json"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0644"
content: "{{ {'services': xworkspace_console_portal_services} | to_nice_json }}\n"
- name: Deploy AI Workspace portal token file
ansible.builtin.copy:
dest: "{{ xworkspace_console_config_dir }}/auth-token"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0600"
content: "{{ xworkspace_console_auth_token }}\n"
no_log: true
- name: Deploy AI Workspace shared auth token file
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.ai_workspace_auth_token"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0600"
content: "{{ xworkspace_console_auth_token }}\n"
no_log: true
- name: Deploy XWorkspace API environment
ansible.builtin.copy:
dest: "{{ xworkspace_console_config_dir }}/portal.env"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0600"
content: |
AI_WORKSPACE_AUTH_TOKEN={{ xworkspace_console_auth_token }}
XWORKSPACE_CONSOLE_AUTH_TOKEN={{ xworkspace_console_auth_token }}
BRIDGE_AUTH_TOKEN={{ xworkspace_console_auth_token }}
BRIDGE_REVIEW_AUTH_TOKEN={{ xworkspace_console_review_auth_token }}
XWORKMATE_BRIDGE_AUTH_TOKEN={{ xworkspace_console_auth_token }}
INTERNAL_SERVICE_TOKEN={{ xworkspace_console_auth_token }}
XWORKSPACE_PORTAL_SERVICES_FILE={{ xworkspace_console_config_dir }}/portal-services.json
no_log: true
- name: Deploy XWorkspace Console service
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-console.service"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0644"
content: |
[Unit]
Description=XWorkspace Console dashboard
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory={{ xworkspace_console_dashboard_dir }}/dist
# console 只是 17000 上的静态后端dashboard 为无路由单页 dist由系统
# caddy 经 /etc/caddy/conf.d/ 反代对外。用 python3 静态伺服即可,跨 Linux/
# macOS 统一、不再起第二个 caddy避免与系统 caddy 抢 :80
ExecStart=/usr/bin/env python3 -m http.server {{ xworkspace_console_port }} --bind 127.0.0.1 --directory {{ xworkspace_console_dashboard_dir }}/dist
Restart=always
RestartSec=2
[Install]
WantedBy=default.target
when: ansible_os_family != 'Darwin'
- name: Deploy XWorkspace API service
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-api.service"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0644"
content: |
[Unit]
Description=XWorkspace status API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory={{ xworkspace_console_api_working_dir }}
EnvironmentFile={{ xworkspace_console_config_dir }}/portal.env
ExecStart={{ xworkspace_console_api_exec }}
Restart=always
RestartSec=2
[Install]
WantedBy=default.target
when: ansible_os_family != 'Darwin'
- name: Deploy AI Agentic Workspace ttyd service
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-ttyd.service"
owner: "{{ xworkspace_console_user }}"
group: "{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}"
mode: "0644"
content: |
[Unit]
Description=AI Agentic Workspace ttyd
After=graphical-session.target
Wants=graphical-session.target
[Service]
Type=simple
ExecStart={{ xworkspace_console_ttyd_binary_path }} -i lo -p {{ xworkspace_console_ttyd_port }} -O login bash
Restart=always
RestartSec=2
[Install]
WantedBy=default.target
when: ansible_os_family != 'Darwin'
- name: Enable XWorkspace Console service
ansible.builtin.file:
src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-console.service"
dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-console.service"
state: link
become_user: "{{ xworkspace_console_user }}"
when: ansible_os_family != 'Darwin'
- name: Enable XWorkspace API service
ansible.builtin.file:
src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-api.service"
dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-api.service"
state: link
become_user: "{{ xworkspace_console_user }}"
when: ansible_os_family != 'Darwin'
- name: Enable AI Agentic Workspace ttyd service
ansible.builtin.file:
src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-ttyd.service"
dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-ttyd.service"
state: link
become_user: "{{ xworkspace_console_user }}"
when: ansible_os_family != 'Darwin'
- name: Enable AI Agentic Workspace LiteLLM service
ansible.builtin.file:
src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-litellm.service"
dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-litellm.service"
state: link
force: true
become_user: "{{ xworkspace_console_user }}"
when: ansible_os_family != 'Darwin'
- name: Enable AI Agentic Workspace status timer
ansible.builtin.file:
src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-status.timer"
dest: "{{ xworkspace_console_home }}/.config/systemd/user/timers.target.wants/xworkspace-status.timer"
state: link
become_user: "{{ xworkspace_console_user }}"
when: ansible_os_family != 'Darwin'
- name: Kill legacy python http.server on port 7000
ansible.builtin.shell: |
pid=$(lsof -ti:7000 2>/dev/null || true)
if [ -n "$pid" ]; then
kill "$pid" 2>/dev/null || true
sleep 1
fi
become_user: "{{ xworkspace_console_user }}"
- name: Remove legacy portal chrome and target units
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-portal.service"
- "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-chrome.service"
- "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-console.target"
- "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-portal.service"
- "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-chrome.service"
- "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-console.target"
when: ansible_os_family != 'Darwin'
- name: Remove legacy portal directory
ansible.builtin.file:
path: "{{ xworkspace_console_root }}/portal"
state: absent
- name: Reload systemd user daemon
ansible.builtin.shell: |
uid="$(id -u {{ xworkspace_console_user }})"
loginctl enable-linger {{ xworkspace_console_user }} || true
systemctl start "user@${uid}.service" || true
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user daemon-reload
become: true
when: ansible_os_family != 'Darwin'
- name: Restart xworkspace-console service
ansible.builtin.shell: |
uid="$(id -u {{ xworkspace_console_user }})"
loginctl enable-linger {{ xworkspace_console_user }} || true
systemctl start "user@${uid}.service" || true
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-console.service
become: true
when: ansible_os_family != 'Darwin'
- name: Restart xworkspace-api service
ansible.builtin.shell: |
uid="$(id -u {{ xworkspace_console_user }})"
loginctl enable-linger {{ xworkspace_console_user }} || true
systemctl start "user@${uid}.service" || true
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-api.service
become: true
when: ansible_os_family != 'Darwin'
- name: Restart xworkspace-ttyd service
ansible.builtin.shell: |
uid="$(id -u {{ xworkspace_console_user }})"
loginctl enable-linger {{ xworkspace_console_user }} || true
systemctl start "user@${uid}.service" || true
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-ttyd.service
become: true
when: ansible_os_family != 'Darwin'
- name: Hide XFCE desktop icons
ansible.builtin.command: xfconf-query -c xfce4-desktop -p /desktop-icons/style -t int -s 0 --create
changed_when: true
failed_when: false
- name: Ensure Caddy fragment directory exists
ansible.builtin.file:
path: /etc/caddy/conf.d
state: directory
owner: root
group: root
mode: "0755"
when: ansible_os_family != 'Darwin'
- name: Deploy xworkspace-console public Caddy site
ansible.builtin.copy:
dest: "/etc/caddy/conf.d/{{ xworkspace_console_domain }}.caddy"
owner: root
group: root
mode: "0644"
content: |
{{ xworkspace_console_domain }} {
reverse_proxy 127.0.0.1:{{ xworkspace_console_port }}
}
when:
- xworkspace_console_public_access | bool
- ansible_os_family != 'Darwin'
register: xworkspace_caddy_deploy
- name: Remove xworkspace-console public Caddy site when disabled
ansible.builtin.file:
path: "/etc/caddy/conf.d/{{ xworkspace_console_domain }}.caddy"
state: absent
when:
- not (xworkspace_console_public_access | bool)
- ansible_os_family != 'Darwin'
register: xworkspace_caddy_remove
- name: Reload Caddy if xworkspace-console proxy changed
ansible.builtin.service:
name: caddy
state: reloaded
when:
- (xworkspace_caddy_deploy.changed or xworkspace_caddy_remove.changed) and not ansible_check_mode
- ansible_os_family != 'Darwin'
failed_when: false
- name: Import macOS specific XWorkspace console tasks
ansible.builtin.include_tasks: xworkspace_console_macos.yml
when: ansible_os_family == 'Darwin'