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>
839 lines
37 KiB
YAML
839 lines
37 KiB
YAML
---
|
||
- 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 家目录 700,ubuntu 无法进入)。
|
||
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'
|