chore: align AI agent runtime playbooks

This commit is contained in:
Haitao Pan 2026-05-26 12:58:56 +08:00
parent 7fbba293a0
commit 69e7691287
36 changed files with 1086 additions and 92 deletions

9
deploy_QMD.yml Normal file
View File

@ -0,0 +1,9 @@
---
- name: Deploy QMD extended memory
hosts: "{{ qmd_hosts | default('all') }}"
become: true
gather_facts: true
roles:
- role: roles/vhosts/qmd/
tags: [qmd]

9
deploy_agent_hermes.yml Normal file
View File

@ -0,0 +1,9 @@
---
- name: Deploy Hermes ACP agent adapter
hosts: "{{ acp_hermes_hosts | default('all') }}"
become: true
gather_facts: true
roles:
- role: roles/vhosts/acp_server_hermes/
tags: [acp_hermes, hermes]

View File

@ -1,9 +0,0 @@
---
- name: Deploy shared agent skills
hosts: all
become: true
gather_facts: true
roles:
- role: roles/agent_skills/
when: agent_skills_enabled | default(true) | bool
tags: [agent_skills]

View File

@ -1,8 +1,4 @@
---
- import_playbook: deploy_agent_skills.yml
vars:
agent_skills_enabled: "{{ xworkmate_bridge_agent_skills_enabled | default(false) }}"
- name: Ensure minimal XFCE XRDP desktop baseline
hosts: "{{ xworkmate_bridge_hosts | default('all') }}"
become: true
@ -11,9 +7,15 @@
- role: roles/vhosts/xfce_xrdp_minimal/
tags: [xfce, xfce_xrdp_minimal]
- import_playbook: setup-ai-agent-runtime.yml
- import_playbook: setup-ai-agent-skills.yml
vars:
ai_agent_runtime_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) or xworkmate_bridge_agent_skills_enabled | default(false) }}"
ai_agent_runtime_nodejs_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_python_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_browser_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_docs_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_fonts_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_verify_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_skills_enabled: "{{ xworkmate_bridge_agent_skills_enabled | default(false) }}"
- name: Deploy ACP vhosts through xworkmate bridge

View File

@ -0,0 +1,205 @@
# yitu-it-series R2 assets
This runbook migrates the local Google Drive `自媒体` directory to Cloudflare R2 for the Docusaurus AI Native knowledge base.
## Architecture
```text
GitHub -> Docusaurus -> Cloudflare Pages -> ebook.svc.plus
Google Drive local folder
-> rclone
-> Cloudflare R2 bucket: yitu-it-series
-> R2 custom domain: img.svc.plus
-> Docusaurus Markdown image URLs
```
## Source and target
```text
Local source:
/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体
R2 bucket:
yitu-it-series
Public asset domain:
https://img.svc.plus
```
## Recommended object layout
```text
yitu-it-series/
├── covers/
├── xiaohongshu/
├── observability/
├── storage/
├── networking/
├── ai-native/
├── security/
├── platform-engineering/
└── ebook-assets/
```
Use stable, semantic paths for published content:
```text
covers/season-1/single-machine-to-platform-cover-v1.png
security/least-privilege/root-to-rootless-v1.png
ai-native/agentic-infra/ai-native-platform-v1.png
ebook-assets/diagrams/cloud-native-to-ai-native-v1.png
```
Prefer versioned object names instead of overwriting an already published image. This keeps Cloudflare CDN behavior predictable and preserves old articles.
## Cloudflare API token
Create two token scopes if possible:
```text
Bootstrap token:
- Account: Cloudflare R2: Edit
- Zone: DNS: Edit, Zone: Read for svc.plus
- Used only for bucket/custom-domain setup
Long-running R2 S3 token:
- R2 Object Read & Write
- Scope limited to bucket yitu-it-series
- Used by rclone sync
```
Required environment variables:
```bash
export CF_ACCOUNT_ID="..."
export CF_ZONE_ID="..."
export CLOUDFLARE_API_TOKEN="..."
export R2_ACCESS_KEY_ID="..."
export R2_SECRET_ACCESS_KEY="..."
```
## Commands
From the playbooks directory:
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
chmod +x scripts/sync-yitu-it-series-r2.sh
scripts/sync-yitu-it-series-r2.sh doctor
scripts/sync-yitu-it-series-r2.sh create-bucket
scripts/sync-yitu-it-series-r2.sh configure-rclone
scripts/sync-yitu-it-series-r2.sh dry-run
scripts/sync-yitu-it-series-r2.sh copy
scripts/sync-yitu-it-series-r2.sh check
scripts/sync-yitu-it-series-r2.sh tree
scripts/sync-yitu-it-series-r2.sh configure-custom-domain
```
Use `copy` for the first production migration when preserving all historical remote files matters. Use `sync` for steady-state mirroring after the source layout is stable.
## Performance profile
Default large AI image profile:
```bash
export RCLONE_TRANSFERS=16
export RCLONE_CHECKERS=32
export RCLONE_S3_UPLOAD_CUTOFF=128M
export RCLONE_S3_CHUNK_SIZE=128M
```
Many small images:
```bash
export RCLONE_TRANSFERS=32
export RCLONE_CHECKERS=64
```
Large source files such as PSD/video:
```bash
export RCLONE_TRANSFERS=4
export RCLONE_CHECKERS=16
export RCLONE_S3_UPLOAD_CUTOFF=256M
export RCLONE_S3_CHUNK_SIZE=256M
```
## Incremental sync
Install a macOS launchd sync job:
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
scripts/sync-yitu-it-series-r2.sh install-launchd
launchctl list | grep yitu-it-series
```
Remove it:
```bash
scripts/sync-yitu-it-series-r2.sh uninstall-launchd
```
## R2 custom domain
Target:
```text
img.svc.plus -> R2 bucket yitu-it-series
```
The script calls the Cloudflare R2 custom domain API:
```bash
scripts/sync-yitu-it-series-r2.sh configure-custom-domain
```
Recommended Cloudflare cache rule:
```text
If hostname equals img.svc.plus:
- Cache eligible
- Edge TTL: 30 days or longer
- Browser TTL: 7-30 days, or respect origin
```
## Docusaurus references
Markdown:
```md
![AI Native 基础设施演进](https://img.svc.plus/ai-native/ai-native-infra-cover-v1.png)
![最小权限演进](https://img.svc.plus/security/least-privilege-cover-v1.png)
```
MDX:
```mdx
<img
src="https://img.svc.plus/platform-engineering/platform-engineering-roadmap-v1.png"
alt="Platform Engineering Roadmap"
loading="lazy"
/>
```
Front matter:
```md
---
title: AI Native 基础设施演进
description: 从云原生到 AI Native 的平台工程知识库
image: https://img.svc.plus/covers/ai-native-infra-cover-v1.png
---
```
## AI Native knowledge-base practices
- Keep Docusaurus focused on Markdown, MDX, navigation, SEO, and search.
- Keep heavy generated images and ebook assets in R2.
- Reference published assets with absolute `https://img.svc.plus/...` URLs.
- Keep object names immutable after publication; publish revisions with `-v2`, `-v3`.
- Run `rclone check` before replacing local Markdown image references.
- Keep raw generation artifacts separate from article-ready assets when possible.
- Use topic directories that match the ebook taxonomy so future RAG/vector indexing can attach image context to chapters.

View File

@ -0,0 +1,15 @@
CF_ACCOUNT_ID=
CF_ZONE_ID=
CLOUDFLARE_API_TOKEN=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=yitu-it-series
R2_REMOTE=cloudflare-r2
R2_CUSTOM_DOMAIN=img.svc.plus
LOCAL_SRC=/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体
RCLONE_TRANSFERS=16
RCLONE_CHECKERS=32
RCLONE_S3_UPLOAD_CUTOFF=128M
RCLONE_S3_CHUNK_SIZE=128M

View File

@ -0,0 +1,14 @@
---
xworkmate_bridge_domain: cn-xworkmate-bridge.svc.plus
xworkmate_bridge_public_base_url: https://cn-xworkmate-bridge.svc.plus
xworkmate_bridge_service_domain: cn-xworkmate-bridge.svc.plus
xworkmate_bridge_service_public_base_url: https://cn-xworkmate-bridge.svc.plus
xworkmate_bridge_binary_path: /usr/local/bin/xworkmate-bridge
xworkmate_bridge_service_user: root
xworkmate_bridge_service_group: root
xworkmate_bridge_service_home: /root
xworkmate_bridge_required_services: []
xworkmate_bridge_required_listeners:
- host: 127.0.0.1
port: "8787"
name: bridge

View File

@ -7,6 +7,10 @@ cn-front.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=r
# services: cn-homepage.svc.plus
cn-homepage.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root
[cn_xworkmate_bridge_host]
# services: cn-xworkmate-bridge.svc.plus
cn-xworkmate-bridge.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root service_domains=cn-xworkmate-bridge.svc.plus
[global_homepage_host]
# services: global-homepage.svc.plus
global-homepage.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root

View File

@ -1,18 +1,31 @@
# Agent Skills
Synchronizes the controller user's `~/.agents/skills/` directory to an Ubuntu
runtime user's canonical skills directory, then exposes the same directory to
agent-specific skill locations.
Synchronizes controller skill sources to an Ubuntu runtime user's canonical
skills directory, then exposes the same directory to agent-specific skill
locations.
Default source and target:
- local source: `~/.agents/skills/`
- local marketplace source: `~/.agents/skills/`
- local repository source: `../xworkspace-core-skills/skills/`
- remote canonical path: `/home/ubuntu/.agents/skills/`
- default agent targets: `codex`, `gemini`, `opencode`, `hermers`, `openclaw`
The repository source is categorized by capability domain, for example
`video-production/`, `image-production/`, `animation/`, and `workspace-core/`.
The role syncs those categories as-is, then creates root-level symlinks for
nested skills so runtimes that scan one directory level can still discover them.
Set `agent_skills_xworkspace_core_enabled=false` to use only the marketplace
source, or `agent_skills_remote_flatten_nested_skills=false` to disable root
symlink materialization.
The role keeps one remote source of truth and links each agent's skills entry to
that canonical directory. Existing non-symlink target directories are rejected by
default to avoid silently deleting agent-owned content. Set
that canonical directory where the online runtime already uses links. Existing
non-symlink target directories are rejected by default to avoid silently deleting
agent-owned content. The live `ubuntu` Codex runtime on
`xworkmate-bridge.svc.plus` keeps `/home/ubuntu/.codex/skills` as a real
directory, so it is preserved by default through
`agent_skills_preserve_existing_target_dirs`. Set
`agent_skills_replace_existing_target_dirs=true` only when those target
directories should be replaced.
@ -42,6 +55,11 @@ already present locally. Set
skills when neither installer is available; the role still fails later if a
required skill cannot be resolved.
Required-skill checks search both the marketplace source and the categorized
repository source recursively. Auto-install still writes only to
`~/.agents/skills/`; repository-owned skills should be changed in
`xworkspace-core-skills`.
After install, optional local quality gates run for each resolved skill when the
command exists:
@ -58,16 +76,20 @@ Default sync excludes local runtime artifacts such as `.venv/`, `__pycache__/`,
`.pyc`, and `.DS_Store`; skills should ship source, scripts, templates, and
references rather than controller-local virtual environments.
The sync defaults to overlay mode (`agent_skills_delete_removed=false`) so it
does not remove skills that already exist on the live runtime catalog. Enable
deletion only for controlled rebuilds of `/home/ubuntu/.agents/skills/`.
Example:
```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus deploy_agent_skills.yml
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml --tags agent_skills
```
Bootstrap-only example that keeps the existing local source strict but skips
quality gate failures from newly installed marketplace skills:
```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus deploy_agent_skills.yml \
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml --tags agent_skills \
-e agent_skills_quality_gate_fail_on_error=false
```

View File

@ -4,13 +4,19 @@ agent_skills_group: "{{ agent_skills_user }}"
agent_skills_home: "/home/{{ agent_skills_user }}"
agent_skills_local_source_dir: "{{ lookup('ansible.builtin.env', 'HOME') }}/.agents/skills"
agent_skills_xworkspace_core_enabled: true
agent_skills_xworkspace_core_required: true
agent_skills_xworkspace_core_source_dir: "{{ playbook_dir | dirname }}/xworkspace-core-skills/skills"
agent_skills_remote_dir: "{{ agent_skills_home }}/.agents/skills"
agent_skills_local_source_create: true
agent_skills_delete_removed: true
agent_skills_delete_removed: false
agent_skills_rsync_compress: false
agent_skills_rsync_timeout: 120
agent_skills_install_rsync: true
agent_skills_replace_existing_target_dirs: false
agent_skills_preserve_existing_target_dirs:
- "{{ agent_skills_home }}/.codex/skills"
agent_skills_remote_flatten_nested_skills: true
agent_skills_auto_install_enabled: true
agent_skills_auto_install_fail_on_missing_installer: true
agent_skills_quality_gate_enabled: true
@ -31,8 +37,10 @@ agent_skills_rsync_excludes:
- .venv/
- __pycache__/
- "*.pyc"
- "*/__pycache__/"
- "*/.DS_Store"
agent_skills_rsync_extra_opts:
- "--delete-excluded"
- "--protocol=29"
- "--out-format=<<CHANGED>>%i"
agent_skills_typical_scenario_skills:
@ -121,9 +129,6 @@ agent_skills_targets:
paths:
- "{{ agent_skills_home }}/.opencode/skills"
- "{{ agent_skills_home }}/.config/opencode/skills"
- name: hermers
paths:
- "{{ agent_skills_home }}/.hermers/skills"
- name: openclaw
paths:
- "{{ agent_skills_home }}/.openclaw/skills"

View File

@ -6,9 +6,10 @@
- agent_skills_group | length > 0
- agent_skills_home | length > 0
- agent_skills_local_source_dir | length > 0
- (not agent_skills_xworkspace_core_enabled | bool) or agent_skills_xworkspace_core_source_dir | length > 0
- agent_skills_remote_dir | length > 0
- agent_skills_targets | length > 0
fail_msg: "agent_skills_user, home, source, remote dir, and targets must be set."
fail_msg: "agent_skills_user, home, source dirs, remote dir, and targets must be set."
- name: Build required agent skills list
ansible.builtin.set_fact:
@ -21,6 +22,7 @@
mode: "0755"
delegate_to: localhost
become: false
check_mode: false
when:
- agent_skills_local_source_create | bool
- agent_skills_auto_install_enabled | bool
@ -38,14 +40,52 @@
- agent_skills_local_source.stat.isdir | default(false)
fail_msg: "Local skills source directory does not exist: {{ agent_skills_local_source_dir }}"
- name: Inspect xworkspace core skills source directory
ansible.builtin.stat:
path: "{{ agent_skills_xworkspace_core_source_dir }}"
register: agent_skills_xworkspace_core_source
delegate_to: localhost
become: false
when: agent_skills_xworkspace_core_enabled | bool
- name: Assert xworkspace core skills source directory exists
ansible.builtin.assert:
that:
- agent_skills_xworkspace_core_source.stat.isdir | default(false)
fail_msg: "xworkspace core skills source directory does not exist: {{ agent_skills_xworkspace_core_source_dir }}"
when:
- agent_skills_xworkspace_core_enabled | bool
- agent_skills_xworkspace_core_required | bool
- name: Build effective agent skills source directories
ansible.builtin.set_fact:
agent_skills_effective_source_dirs: >-
{{
[agent_skills_local_source_dir]
+ (
(
agent_skills_xworkspace_core_enabled | bool
and agent_skills_xworkspace_core_source.stat.isdir | default(false)
)
| ternary([agent_skills_xworkspace_core_source_dir], [])
)
}}
- name: Inspect required local scenario skills
ansible.builtin.shell: |
set -eu
for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "{{ agent_skills_local_source_dir }}/$candidate/SKILL.md" ]; then
printf '%s\n' "{{ agent_skills_local_source_dir }}/$candidate"
exit 0
fi
for source_dir in {{ agent_skills_effective_source_dirs | map('quote') | join(' ') }}; do
for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "$source_dir/$candidate/SKILL.md" ]; then
printf '%s\n' "$source_dir/$candidate"
exit 0
fi
match="$(find "$source_dir" -type f -path "*/$candidate/SKILL.md" -print -quit)"
if [ -n "$match" ]; then
dirname "$match"
exit 0
fi
done
done
exit 1
args:
@ -58,6 +98,7 @@
label: "{{ item.name }}"
delegate_to: localhost
become: false
check_mode: false
- name: Build missing scenario skills list
ansible.builtin.set_fact:
@ -116,11 +157,18 @@
- name: Reinspect required local scenario skills after auto install
ansible.builtin.shell: |
set -eu
for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "{{ agent_skills_local_source_dir }}/$candidate/SKILL.md" ]; then
printf '%s\n' "{{ agent_skills_local_source_dir }}/$candidate"
exit 0
fi
for source_dir in {{ agent_skills_effective_source_dirs | map('quote') | join(' ') }}; do
for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "$source_dir/$candidate/SKILL.md" ]; then
printf '%s\n' "$source_dir/$candidate"
exit 0
fi
match="$(find "$source_dir" -type f -path "*/$candidate/SKILL.md" -print -quit)"
if [ -n "$match" ]; then
dirname "$match"
exit 0
fi
done
done
exit 1
args:
@ -134,6 +182,7 @@
delegate_to: localhost
become: false
when: agent_skills_auto_install_enabled | bool
check_mode: false
- name: Build unresolved scenario skills list
ansible.builtin.set_fact:
@ -199,6 +248,7 @@
when:
- agent_skills_quality_gate_enabled | bool
- agent_skills_resolved_local_paths | length > 0
check_mode: false
- name: Detect local top-level symlink skills
ansible.builtin.find:
@ -255,10 +305,15 @@
'--partial',
'--timeout=' ~ (agent_skills_rsync_timeout | string)
]
+ (['--dry-run'] if ansible_check_mode else [])
+ (['-z'] if (agent_skills_rsync_compress | bool) else [])
+ (['--delete'] if (agent_skills_delete_removed | bool) else [])
+ (['--delete'] if (agent_skills_delete_removed | bool and agent_skills_source_index == 0) else [])
+ (['--delete-excluded'] if (agent_skills_delete_removed | bool and agent_skills_source_index == 0) else [])
+ (
(agent_skills_rsync_excludes + agent_skills_local_symlink_excludes)
(
agent_skills_rsync_excludes
+ ((agent_skills_source_index == 0) | ternary(agent_skills_local_symlink_excludes, []))
)
| map('regex_replace', '^(.*)$', '--exclude=\1')
| list
)
@ -266,7 +321,7 @@
+ [
'-e',
'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null',
agent_skills_local_source_dir ~ '/',
item ~ '/',
(
ansible_user | default(ansible_ssh_user) | default('root')
) ~ '@' ~ (
@ -276,6 +331,11 @@
}}
register: agent_skills_rsync_result
changed_when: "'<<CHANGED>>' in agent_skills_rsync_result.stdout"
check_mode: false
loop: "{{ agent_skills_effective_source_dirs }}"
loop_control:
index_var: agent_skills_source_index
label: "{{ item }}"
delegate_to: localhost
become: false
@ -287,6 +347,47 @@
group: "{{ agent_skills_group }}"
recurse: true
- name: Link nested categorized skills at canonical root
ansible.builtin.shell: |
set -eu
changed=0
while IFS= read -r skill_manifest; do
skill_dir="$(dirname "$skill_manifest")"
skill_name="$(basename "$skill_dir")"
link_path={{ agent_skills_remote_dir | quote }}/"$skill_name"
if [ -e "$link_path" ] && [ ! -L "$link_path" ]; then
continue
fi
current_target=""
if [ -L "$link_path" ]; then
current_target="$(readlink "$link_path")"
fi
if [ "$current_target" != "$skill_dir" ]; then
if [ "{{ ansible_check_mode | ternary('true', 'false') }}" != "true" ]; then
ln -sfn "$skill_dir" "$link_path"
fi
changed=1
fi
done < <(find {{ agent_skills_remote_dir | quote }} -mindepth 3 -name SKILL.md -type f -print)
if [ "$changed" = "1" ]; then
echo "<<CHANGED>>linked nested skills"
fi
args:
executable: /bin/bash
register: agent_skills_flatten_result
changed_when: "'<<CHANGED>>' in agent_skills_flatten_result.stdout"
check_mode: false
when: agent_skills_remote_flatten_nested_skills | bool
- name: Set canonical agent skills ownership after nested links
ansible.builtin.file:
path: "{{ agent_skills_remote_dir }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
recurse: true
when: agent_skills_remote_flatten_nested_skills | bool
- name: Flatten agent skills target paths
ansible.builtin.set_fact:
agent_skills_target_paths: "{{ agent_skills_targets | subelements('paths') | map('last') | list }}"
@ -307,6 +408,7 @@
when:
- item.stat.exists | default(false)
- not item.stat.islnk | default(false)
- item.item not in agent_skills_preserve_existing_target_dirs
- not agent_skills_replace_existing_target_dirs | bool
- name: Replace existing non-link target directories when enabled
@ -317,6 +419,7 @@
when:
- item.stat.exists | default(false)
- not item.stat.islnk | default(false)
- item.item not in agent_skills_preserve_existing_target_dirs
- agent_skills_replace_existing_target_dirs | bool
- name: Build agent skills target parent paths
@ -345,10 +448,9 @@
src: "{{ agent_skills_remote_dir }}"
dest: "{{ item }}"
state: link
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
force: true
loop: "{{ agent_skills_target_paths }}"
when: item not in agent_skills_preserve_existing_target_dirs
- name: Verify canonical skill manifests are present
ansible.builtin.find:

View File

@ -6,25 +6,32 @@ role entrypoint. The role installs:
- base tools: `curl`, `wget`, `git`, `jq`, `rsync`, `unzip`
- Node.js runtime for Playwright-based agents
- Python 3 toolchain for scripts and helpers
- system `chromium` browser
- existing system browser, preferring the live `/usr/local/bin/chromium` wrapper
or Google Chrome before installing browser packages
- `pandoc` + XeLaTeX PDF toolchain
- Chinese fonts for document rendering
- shared agent skills via `roles/agent_skills`
- shared agent skills via `roles/agent_skills`, including the categorized
`../xworkspace-core-skills/skills/` repository source by default
Design constraints:
- system packages are the primary source of truth
- Playwright uses system `chromium` instead of downloading browsers
- Playwright uses the resolved system browser instead of downloading browsers
- Chinese PDF rendering is treated as a runtime requirement, not an optional add-on
Default Playwright environment:
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`
- `PLAYWRIGHT_BROWSERS_PATH=0`
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium`
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/local/bin/chromium` when that live
wrapper exists
Example:
```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-runtime.yml
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml
```
`setup-ai-agent-skills.yml` runs `roles/ai_agent_runtime`, which installs system
dependencies and syncs the current Skill catalog through the embedded
`roles/agent_skills` step in one pass.

View File

@ -9,7 +9,7 @@ ai_agent_runtime_base_packages:
- wget
ai_agent_runtime_nodejs_enabled: true
ai_agent_runtime_nodejs_version: "22.x"
ai_agent_runtime_nodejs_version: "24.x"
ai_agent_runtime_install_yarn: true
ai_agent_runtime_yarn_version: ""
ai_agent_runtime_npm_global_packages: []
@ -29,8 +29,8 @@ ai_agent_runtime_python_packages:
ai_agent_runtime_browser_enabled: true
ai_agent_runtime_browser_packages:
- chromium
ai_agent_runtime_browser_executable: /usr/bin/chromium
- google-chrome-stable
ai_agent_runtime_browser_executable: /usr/local/bin/chromium
ai_agent_runtime_docs_enabled: true
ai_agent_runtime_doc_packages:

View File

@ -1,26 +1,18 @@
---
- name: Install AI runtime browser packages
ansible.builtin.apt:
name: "{{ ai_agent_runtime_browser_packages }}"
state: present
update_cache: true
install_recommends: false
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true
- name: Resolve Chromium executable
- name: Resolve existing Chromium executable
ansible.builtin.shell: |
set -eu
for candidate in \
"{{ ai_agent_runtime_browser_executable }}" \
/usr/local/bin/chromium \
/usr/local/bin/chromium-browser \
/usr/bin/google-chrome \
/usr/bin/google-chrome-stable \
chromium \
chromium-browser \
google-chrome \
google-chrome-stable \
/usr/bin/chromium \
/usr/local/bin/chromium \
/snap/bin/chromium; do
if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
@ -36,6 +28,51 @@
executable: /bin/sh
register: ai_agent_runtime_browser_resolve
changed_when: false
failed_when: false
check_mode: false
- name: Install AI runtime browser packages when no browser exists
ansible.builtin.apt:
name: "{{ ai_agent_runtime_browser_packages }}"
state: present
update_cache: true
install_recommends: false
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true
when: ai_agent_runtime_browser_resolve.rc != 0
- name: Resolve Chromium executable
ansible.builtin.shell: |
set -eu
for candidate in \
"{{ ai_agent_runtime_browser_executable }}" \
/usr/local/bin/chromium \
/usr/local/bin/chromium-browser \
/usr/bin/google-chrome \
/usr/bin/google-chrome-stable \
chromium \
chromium-browser \
google-chrome \
google-chrome-stable \
/usr/bin/chromium \
/snap/bin/chromium; do
if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
exit 0
fi
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
exit 0
fi
done
exit 1
args:
executable: /bin/sh
register: ai_agent_runtime_browser_resolve
changed_when: false
check_mode: false
- name: Set resolved Chromium executable
ansible.builtin.set_fact:

View File

@ -39,7 +39,10 @@
- name: Configure shared agent skills
ansible.builtin.include_role:
name: "{{ ai_agent_runtime_skills_role_name }}"
apply:
tags: agent_skills
when: ai_agent_runtime_skills_enabled | bool
tags: [agent_skills]
- name: Verify AI agent runtime
ansible.builtin.include_tasks: verify.yml

View File

@ -3,48 +3,56 @@
ansible.builtin.command: node --version
register: ai_agent_runtime_node_version
changed_when: false
check_mode: false
when: ai_agent_runtime_nodejs_enabled | bool
- name: Check npm version
ansible.builtin.command: npm --version
register: ai_agent_runtime_npm_version
changed_when: false
check_mode: false
when: ai_agent_runtime_nodejs_enabled | bool
- name: Check python version
ansible.builtin.command: python3 --version
register: ai_agent_runtime_python_version
changed_when: false
check_mode: false
when: ai_agent_runtime_python_enabled | bool
- name: Check pip version
ansible.builtin.command: pip3 --version
register: ai_agent_runtime_pip_version
changed_when: false
check_mode: false
when: ai_agent_runtime_python_enabled | bool
- name: Check chromium version
ansible.builtin.command: "{{ ai_agent_runtime_browser_resolved_executable | default(ai_agent_runtime_browser_executable) }} --version"
register: ai_agent_runtime_chromium_version
changed_when: false
check_mode: false
when: ai_agent_runtime_browser_enabled | bool
- name: Check pandoc version
ansible.builtin.command: pandoc --version
register: ai_agent_runtime_pandoc_version
changed_when: false
check_mode: false
when: ai_agent_runtime_docs_enabled | bool
- name: Check xelatex version
ansible.builtin.command: xelatex --version
register: ai_agent_runtime_xelatex_version
changed_when: false
check_mode: false
when: ai_agent_runtime_docs_enabled | bool
- name: Check Chinese font inventory
ansible.builtin.command: fc-list :lang=zh family
register: ai_agent_runtime_chinese_fonts
changed_when: false
check_mode: false
when:
- ai_agent_runtime_fonts_enabled | bool
- ai_agent_runtime_verify_chinese_fonts | bool

View File

@ -25,6 +25,7 @@
register: acp_codex_bridge_binary_attrs
changed_when: false
failed_when: false
check_mode: false
- name: Remove immutable flag from Codex bridge binary when present
ansible.builtin.command:
@ -74,6 +75,7 @@
register: acp_codex_service_attrs
changed_when: false
failed_when: false
check_mode: false
- name: Remove immutable flag from Codex ACP systemd service when present
ansible.builtin.command:

View File

@ -26,6 +26,7 @@
register: acp_hermes_bridge_binary_attrs
changed_when: false
failed_when: false
check_mode: false
- name: Remove immutable flag from Hermes bridge binary when present
ansible.builtin.command:
@ -58,6 +59,7 @@
register: acp_hermes_service_attrs
changed_when: false
failed_when: false
check_mode: false
- name: Remove immutable flag from Hermes ACP systemd service when present
ansible.builtin.command:
@ -80,6 +82,7 @@
changed_when: false
failed_when: false
no_log: true
check_mode: false
- name: Resolve Hermes ACP auth token
ansible.builtin.set_fact:

View File

@ -44,7 +44,17 @@ gateway_openclaw_acp_max_concurrent_sessions: 2
gateway_openclaw_acp_backend: acpx
gateway_openclaw_acp_default_agent: codex
gateway_openclaw_codex_app_server_url: ws://127.0.0.1:9001
gateway_openclaw_default_model: deepseek/deepseek-v4-flash
gateway_openclaw_default_model:
primary: nvidia/nemotron-3-super-120b-a12b
fallbacks:
- nvidia/minimaxai/minimax-m2.5
- nvidia/z-ai/glm5
gateway_openclaw_default_models:
nvidia/nemotron-3-super-120b-a12b: {}
nvidia/minimaxai/minimax-m2.5: {}
nvidia/z-ai/glm5: {}
openai-codex/gpt-5.5: {}
gateway_openclaw_main_agent_model: nvidia/nemotron-3-super-120b-a12b
gateway_openclaw_main_agent_skills:
- acp-router
@ -76,3 +86,75 @@ gateway_openclaw_main_agent_skills:
- video-translator
- web-search
- self-improving
- ai-tech-news-video
- it-infra-continuous-png
- it-infra-evolution-video
- product-intro-video
- sound-fx-for-video
- sketch-animation-video
- skylv-hermes-agent-integration
- hermes-agent-integration
- qmd
gateway_openclaw_mcp_servers:
qmd:
url: http://localhost:8181/mcp
transport: streamable-http
gateway_openclaw_model_providers:
nvidia:
api: openai-completions
baseUrl: https://integrate.api.nvidia.com/v1
models:
- id: nvidia/nemotron-3-super-120b-a12b
name: NVIDIA Nemotron 3 Super 120B
input: [text]
contextWindow: 262144
maxTokens: 8192
reasoning: false
compat:
requiresStringContent: true
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0
- id: moonshotai/kimi-k2.5
name: Kimi K2.5
input: [text]
contextWindow: 262144
maxTokens: 8192
reasoning: false
compat:
requiresStringContent: true
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0
- id: minimaxai/minimax-m2.5
name: MiniMax M2.5
input: [text]
contextWindow: 196608
maxTokens: 8192
reasoning: false
compat:
requiresStringContent: true
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0
- id: z-ai/glm5
name: GLM-5
input: [text]
contextWindow: 202752
maxTokens: 8192
reasoning: false
compat:
requiresStringContent: true
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0

View File

@ -26,11 +26,13 @@
"bootstrapMaxChars": 50000,
"bootstrapTotalMaxChars": 300000,
"model": {{ gateway_openclaw_default_model | to_json }},
"models": {{ gateway_openclaw_default_models | to_json }},
"thinkingDefault": "low"
},
"list": [
{
"id": "main",
"model": {{ gateway_openclaw_main_agent_model | to_json }},
"skills": {{ gateway_openclaw_main_agent_skills | unique | list | to_json }}
}
]
@ -85,7 +87,7 @@
},
"models": {
"mode": "merge",
"providers": {}
"providers": {{ gateway_openclaw_model_providers | to_json }}
},
"wizard": {
"lastRunAt": "2026-04-19T10:52:37.655Z",
@ -106,6 +108,9 @@
"defaultAgent": {{ gateway_openclaw_acp_default_agent | to_json }},
"maxConcurrentSessions": {{ gateway_openclaw_acp_max_concurrent_sessions | int }}
},
"mcp": {
"servers": {{ gateway_openclaw_mcp_servers | to_json }}
},
"plugins": {
"entries": {
"nvidia": {"enabled": true},
@ -119,9 +124,14 @@
"appServer": {
"transport": "websocket",
"url": {{ gateway_openclaw_codex_app_server_url | to_json }}
},
"discovery": {
"enabled": true
}
}
},
"memory-wiki": {"enabled": true},
"openai": {"enabled": true},
"openclaw-multi-session-plugins": {"enabled": true},
"device-pair": {"enabled": false},
"phone-control": {"enabled": false},

View File

@ -4,6 +4,7 @@
register: node_version_check
changed_when: false
failed_when: false
check_mode: false
- name: Get Node.js version number
set_fact:
@ -83,12 +84,14 @@
register: npm_version_check
changed_when: false
failed_when: false
check_mode: false
- name: Get current Yarn version
command: yarn --version
register: yarn_version_check
changed_when: false
failed_when: false
check_mode: false
when: install_yarn | default(true)
- name: Normalize desired Yarn version

View File

@ -0,0 +1,26 @@
---
qmd_user: ubuntu
qmd_group: "{{ qmd_user }}"
qmd_home: "/home/{{ qmd_user }}"
qmd_binary_path: "{{ qmd_home }}/.bun/bin/qmd"
qmd_config_dir: "{{ qmd_home }}/.config/qmd"
qmd_cache_dir: "{{ qmd_home }}/.cache/qmd"
qmd_config_dir_mode: "0775"
qmd_cache_dir_mode: "0775"
qmd_index_config_path: "{{ qmd_config_dir }}/index.yml"
qmd_index_config_mode: "0664"
qmd_env_path: "{{ qmd_config_dir }}/qmd.env"
qmd_mcp_service_name: qmd-mcp
qmd_mcp_service_unit_path: "{{ qmd_home }}/.config/systemd/user/{{ qmd_mcp_service_name }}.service"
qmd_service_uid: "1000"
qmd_mcp_host: 127.0.0.1
qmd_mcp_port: 8181
qmd_mcp_url: "http://localhost:{{ qmd_mcp_port }}/mcp"
qmd_embed_api_base_url: https://integrate.api.nvidia.com/v1
qmd_embed_model: nvidia/llama-nemotron-embed-1b-v2
qmd_collections:
openclaw-workspace:
path: "{{ qmd_home }}/.openclaw/workspace"
pattern: "**/*.md"
context:
"": OpenClaw workspace long-term memory, daily notes, user context, local tools notes, generated plans and markdown artifacts.

View File

@ -0,0 +1,139 @@
---
- name: Assert QMD is only supported on Debian family
ansible.builtin.assert:
that:
- ansible_facts.os_family == "Debian"
fail_msg: "roles/vhosts/qmd currently supports Debian-based hosts only."
- name: Ensure QMD config and cache directories exist
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ qmd_user }}"
group: "{{ qmd_group }}"
mode: "{{ item.mode }}"
loop:
- path: "{{ qmd_config_dir }}"
mode: "{{ qmd_config_dir_mode }}"
- path: "{{ qmd_cache_dir }}"
mode: "{{ qmd_cache_dir_mode }}"
- path: "{{ qmd_mcp_service_unit_path | dirname }}"
mode: "0755"
- name: Deploy QMD collection index config
ansible.builtin.template:
src: index.yml.j2
dest: "{{ qmd_index_config_path }}"
owner: "{{ qmd_user }}"
group: "{{ qmd_group }}"
mode: "{{ qmd_index_config_mode }}"
- name: Deploy QMD external embedding environment
ansible.builtin.template:
src: qmd.env.j2
dest: "{{ qmd_env_path }}"
owner: "{{ qmd_user }}"
group: "{{ qmd_group }}"
mode: "0600"
diff: false
- name: Inspect QMD binary
ansible.builtin.stat:
path: "{{ qmd_binary_path }}"
register: qmd_binary
- name: Fail when QMD binary is missing or not executable
ansible.builtin.assert:
that:
- qmd_binary.stat.exists | default(false)
- qmd_binary.stat.executable | default(false)
fail_msg: "QMD binary is missing or not executable: {{ qmd_binary_path }}"
- name: Deploy QMD MCP user systemd unit
ansible.builtin.template:
src: qmd-mcp.user.service.j2
dest: "{{ qmd_mcp_service_unit_path }}"
owner: "{{ qmd_user }}"
group: "{{ qmd_group }}"
mode: "0644"
register: qmd_mcp_user_service_unit
- name: Enable QMD service user linger
ansible.builtin.command:
cmd: "loginctl enable-linger {{ qmd_user }}"
creates: "/var/lib/systemd/linger/{{ qmd_user }}"
when:
- not ansible_check_mode
- name: Ensure QMD service user manager is running
ansible.builtin.systemd:
name: "user@{{ qmd_service_uid }}.service"
state: started
when:
- not ansible_check_mode
- name: Reload QMD user systemd manager
ansible.builtin.command:
cmd: systemctl --user daemon-reload
environment:
HOME: "{{ qmd_home }}"
XDG_RUNTIME_DIR: "/run/user/{{ qmd_service_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ qmd_service_uid }}/bus"
become: true
become_user: "{{ qmd_user }}"
changed_when: false
when:
- not ansible_check_mode
- name: Ensure QMD MCP daemon is enabled and running
ansible.builtin.command:
cmd: >-
systemctl --user enable
{{ '--now' if not (qmd_mcp_user_service_unit.changed | default(false)) else '' }}
{{ qmd_mcp_service_name }}.service
environment:
HOME: "{{ qmd_home }}"
XDG_RUNTIME_DIR: "/run/user/{{ qmd_service_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ qmd_service_uid }}/bus"
become: true
become_user: "{{ qmd_user }}"
register: qmd_mcp_service_enable
changed_when: >-
'Created symlink' in (qmd_mcp_service_enable.stdout | default('')) or
'Created symlink' in (qmd_mcp_service_enable.stderr | default(''))
when:
- not ansible_check_mode
- name: Restart QMD MCP daemon after unit changes
ansible.builtin.command:
cmd: "systemctl --user restart {{ qmd_mcp_service_name }}.service"
environment:
HOME: "{{ qmd_home }}"
XDG_RUNTIME_DIR: "/run/user/{{ qmd_service_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ qmd_service_uid }}/bus"
become: true
become_user: "{{ qmd_user }}"
when:
- qmd_mcp_user_service_unit.changed | default(false)
- not ansible_check_mode
- name: Validate QMD status
ansible.builtin.command:
cmd: "{{ qmd_binary_path }} status"
environment:
HOME: "{{ qmd_home }}"
QMD_EMBED_API_BASE_URL: "{{ qmd_embed_api_base_url }}"
QMD_EMBED_MODEL: "{{ qmd_embed_model }}"
become: true
become_user: "{{ qmd_user }}"
register: qmd_status
changed_when: false
check_mode: false
- name: Show QMD validation summary
ansible.builtin.debug:
msg:
- "QMD MCP URL: {{ qmd_mcp_url }}"
- "QMD index config: {{ qmd_index_config_path }}"
- "QMD embedding model: {{ qmd_embed_model }}"
- "{{ qmd_status.stdout | default('') }}"

View File

@ -0,0 +1,12 @@
collections:
{% for name, collection in qmd_collections | dictsort %}
{{ name }}:
path: {{ collection.path }}
pattern: {{ collection.pattern | to_json }}
{% if collection.context is defined %}
context:
{% for path, text in collection.context | dictsort %}
{{ path | to_json }}: {{ text }}
{% endfor %}
{% endif %}
{% endfor %}

View File

@ -0,0 +1,18 @@
[Unit]
Description=QMD MCP daemon
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory={{ qmd_home }}
Environment=HOME={{ qmd_home }}
Environment=PATH={{ qmd_home }}/.bun/bin:{{ qmd_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin
EnvironmentFile={{ qmd_env_path }}
ExecStart={{ qmd_binary_path }} mcp --http --port {{ qmd_mcp_port }}
Restart=always
RestartSec=5
[Install]
WantedBy=default.target

View File

@ -0,0 +1,3 @@
QMD_EMBED_API_BASE_URL={{ qmd_embed_api_base_url }}
QMD_EMBED_MODEL={{ qmd_embed_model }}

View File

@ -21,6 +21,7 @@ xfce_manage_user: false
xfce_user_groups: []
xfce_user_shell: /bin/bash
xfce_user_password_plaintext: ""
xfce_user_update_password: on_create
xfce_google_chrome_version: "148.0.7778.167-1"
xfce_google_chrome_apt_key_url: "https://dl.google.com/linux/linux_signing_key.pub"

View File

@ -12,6 +12,8 @@
- snapd.apparmor.service
become: true
failed_when: false
when:
- not ansible_check_mode
- name: Block snap and snap-backed browser transitional packages
ansible.builtin.copy:
@ -120,14 +122,16 @@
mode: "0755"
become: true
- name: Keep Chromium compatibility commands pointed at Chrome deb
ansible.builtin.file:
src: /usr/local/bin/chromium-xrdp
- name: Keep Chromium compatibility commands disabled
ansible.builtin.copy:
content: |
#!/bin/sh
echo "Chromium is disabled on this host. Use google-chrome instead." >&2
exit 126
dest: "{{ item }}"
state: link
force: true
owner: root
group: root
mode: "0755"
loop:
- /usr/local/bin/chromium
- /usr/local/bin/chromium-browser
@ -162,6 +166,8 @@
environment:
HOME: "{{ xfce_user_home }}"
changed_when: false
when:
- not ansible_check_mode
- name: Set xdg default web browser to Google Chrome XRDP desktop entry
ansible.builtin.command: "xdg-settings set default-web-browser {{ xfce_google_chrome_desktop_file }}"
@ -170,6 +176,8 @@
environment:
HOME: "{{ xfce_user_home }}"
changed_when: false
when:
- not ansible_check_mode
- name: Set system browser alternatives to Google Chrome deb
ansible.builtin.command: "update-alternatives --set {{ item }} /usr/bin/google-chrome-stable"
@ -178,12 +186,15 @@
- gnome-www-browser
become: true
changed_when: false
when:
- not ansible_check_mode
- name: Verify Google Chrome deb browser installation
ansible.builtin.command: /usr/local/bin/chromium-xrdp --version
register: xfce_google_chrome_version_check
changed_when: false
become: true
check_mode: false
- name: Show Google Chrome deb browser version
ansible.builtin.debug:

View File

@ -22,11 +22,13 @@
ansible.builtin.user:
name: "{{ xfce_user }}"
password: "{{ xfce_user_password_plaintext | password_hash('sha512') }}"
update_password: always
update_password: "{{ xfce_user_update_password }}"
password_lock: false
become: true
no_log: true
when: xfce_manage_user | bool
when:
- xfce_manage_user | bool
- not ansible_check_mode
- name: Ensure the desktop user can sudo
ansible.builtin.user:

View File

@ -2,6 +2,7 @@
xworkmate_bridge_service_name: xworkmate-bridge
xworkmate_bridge_service_user: ubuntu
xworkmate_bridge_service_group: ubuntu
xworkmate_bridge_service_home: "/home/{{ xworkmate_bridge_service_user }}"
xworkmate_bridge_auth_token: "{{ lookup('ansible.builtin.env', 'BRIDGE_AUTH_TOKEN') | default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true), true) }}"
xworkmate_bridge_listen_host: 127.0.0.1
xworkmate_bridge_listen_port: 8787

View File

@ -1,7 +1,11 @@
[Unit]
Description=XWorkmate bridge control plane
{% if xworkmate_bridge_required_services | length > 0 %}
Requires={{ xworkmate_bridge_required_services | join(' ') }}
After=network-online.target {{ xworkmate_bridge_required_services | join(' ') }}
{% else %}
After=network-online.target
{% endif %}
Wants=network-online.target
[Service]
@ -9,7 +13,7 @@ Type=simple
User={{ xworkmate_bridge_service_user }}
Group={{ xworkmate_bridge_service_group }}
WorkingDirectory={{ xworkmate_bridge_base_dir }}
Environment="HOME=/home/{{ xworkmate_bridge_service_user }}"
Environment="HOME={{ xworkmate_bridge_service_home }}"
Environment="TERM=xterm-256color"
{% for key, value in xworkmate_bridge_service_environment | dictsort %}
{% if value | string | trim | length > 0 %}

243
scripts/sync-yitu-it-series-r2.sh Executable file
View File

@ -0,0 +1,243 @@
#!/usr/bin/env bash
set -euo pipefail
LOCAL_SRC_DEFAULT="/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLAYBOOKS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
LOCAL_SRC="${LOCAL_SRC:-$LOCAL_SRC_DEFAULT}"
R2_BUCKET="${R2_BUCKET:-yitu-it-series}"
R2_REMOTE="${R2_REMOTE:-cloudflare-r2}"
R2_CUSTOM_DOMAIN="${R2_CUSTOM_DOMAIN:-img.svc.plus}"
RCLONE_BIN="${RCLONE_BIN:-rclone}"
WRANGLER_BIN="${WRANGLER_BIN:-npx --yes wrangler@latest}"
LOG_FILE="${LOG_FILE:-$HOME/rclone-yitu-it-series.log}"
SYNC_FLAGS=(
--progress
--stats 30s
--transfers "${RCLONE_TRANSFERS:-16}"
--checkers "${RCLONE_CHECKERS:-32}"
--fast-list
--s3-upload-cutoff "${RCLONE_S3_UPLOAD_CUTOFF:-128M}"
--s3-chunk-size "${RCLONE_S3_CHUNK_SIZE:-128M}"
--retries "${RCLONE_RETRIES:-8}"
--low-level-retries "${RCLONE_LOW_LEVEL_RETRIES:-20}"
--timeout "${RCLONE_TIMEOUT:-5m}"
--contimeout "${RCLONE_CONTIMEOUT:-30s}"
--exclude ".DS_Store"
--exclude "Icon?"
--exclude "._*"
--exclude ".Spotlight-V100/**"
--exclude ".Trashes/**"
--log-file "$LOG_FILE"
--log-level "${RCLONE_LOG_LEVEL:-INFO}"
)
usage() {
cat <<'EOF'
Usage:
sync-yitu-it-series-r2.sh doctor
sync-yitu-it-series-r2.sh create-bucket
sync-yitu-it-series-r2.sh configure-rclone
sync-yitu-it-series-r2.sh dry-run
sync-yitu-it-series-r2.sh sync
sync-yitu-it-series-r2.sh copy
sync-yitu-it-series-r2.sh check
sync-yitu-it-series-r2.sh tree
sync-yitu-it-series-r2.sh configure-custom-domain
sync-yitu-it-series-r2.sh install-launchd
sync-yitu-it-series-r2.sh uninstall-launchd
Required for configure-rclone:
CF_ACCOUNT_ID or CLOUDFLARE_ACCOUNT_ID
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
Required for create-bucket:
CLOUDFLARE_API_TOKEN or an active Wrangler login
Required for configure-custom-domain:
CF_ACCOUNT_ID or CLOUDFLARE_ACCOUNT_ID
CF_ZONE_ID or CLOUDFLARE_ZONE_ID
CLOUDFLARE_API_TOKEN
EOF
}
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Missing command: $cmd" >&2
exit 1
fi
}
account_id() {
printf '%s' "${CF_ACCOUNT_ID:-${CLOUDFLARE_ACCOUNT_ID:-}}"
}
zone_id() {
printf '%s' "${CF_ZONE_ID:-${CLOUDFLARE_ZONE_ID:-}}"
}
require_env() {
local name="$1"
if [[ -z "${!name:-}" ]]; then
echo "Missing environment variable: $name" >&2
exit 1
fi
}
require_account_id() {
if [[ -z "$(account_id)" ]]; then
echo "Missing CF_ACCOUNT_ID or CLOUDFLARE_ACCOUNT_ID" >&2
exit 1
fi
}
remote_endpoint() {
require_account_id
printf 'https://%s.r2.cloudflarestorage.com' "$(account_id)"
}
doctor() {
require_cmd "$RCLONE_BIN"
echo "Local source: $LOCAL_SRC"
test -d "$LOCAL_SRC"
"$RCLONE_BIN" version | sed -n '1,8p'
"$RCLONE_BIN" size "$LOCAL_SRC" \
--exclude ".DS_Store" \
--exclude "Icon?" \
--exclude "._*"
echo
echo "Rclone remotes:"
"$RCLONE_BIN" listremotes || true
echo
echo "Target: ${R2_REMOTE}:${R2_BUCKET}/"
}
create_bucket() {
require_account_id
if [[ -n "${CLOUDFLARE_API_TOKEN:-}" ]]; then
CLOUDFLARE_ACCOUNT_ID="$(account_id)" CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
$WRANGLER_BIN r2 bucket create "$R2_BUCKET"
else
CLOUDFLARE_ACCOUNT_ID="$(account_id)" $WRANGLER_BIN r2 bucket create "$R2_BUCKET"
fi
}
configure_rclone() {
require_account_id
require_env R2_ACCESS_KEY_ID
require_env R2_SECRET_ACCESS_KEY
"$RCLONE_BIN" config create "$R2_REMOTE" s3 \
provider Cloudflare \
access_key_id "$R2_ACCESS_KEY_ID" \
secret_access_key "$R2_SECRET_ACCESS_KEY" \
endpoint "$(remote_endpoint)" \
region auto \
acl private \
no_check_bucket true
"$RCLONE_BIN" lsd "${R2_REMOTE}:"
}
sync_dry_run() {
"$RCLONE_BIN" sync "$LOCAL_SRC" "${R2_REMOTE}:${R2_BUCKET}/" --dry-run "${SYNC_FLAGS[@]}"
}
sync_run() {
"$RCLONE_BIN" sync "$LOCAL_SRC" "${R2_REMOTE}:${R2_BUCKET}/" "${SYNC_FLAGS[@]}"
}
copy_run() {
"$RCLONE_BIN" copy "$LOCAL_SRC" "${R2_REMOTE}:${R2_BUCKET}/" "${SYNC_FLAGS[@]}"
}
check_run() {
"$RCLONE_BIN" check "$LOCAL_SRC" "${R2_REMOTE}:${R2_BUCKET}/" \
--one-way \
--size-only \
--exclude ".DS_Store" \
--exclude "Icon?" \
--exclude "._*"
}
tree_run() {
"$RCLONE_BIN" tree "${R2_REMOTE}:${R2_BUCKET}/" --max-depth "${TREE_MAX_DEPTH:-2}"
}
configure_custom_domain() {
require_account_id
require_env CLOUDFLARE_API_TOKEN
local zid
zid="$(zone_id)"
if [[ -z "$zid" ]]; then
echo "Missing CF_ZONE_ID or CLOUDFLARE_ZONE_ID" >&2
exit 1
fi
curl -fsS "https://api.cloudflare.com/client/v4/accounts/$(account_id)/r2/buckets/${R2_BUCKET}/domains/custom" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-d "{
\"domain\": \"${R2_CUSTOM_DOMAIN}\",
\"enabled\": true,
\"zoneId\": \"${zid}\",
\"minTLS\": \"1.2\"
}" | jq .
}
install_launchd() {
local plist="$HOME/Library/LaunchAgents/plus.svc.yitu-it-series.rclone-sync.plist"
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>plus.svc.yitu-it-series.rclone-sync</string>
<key>ProgramArguments</key>
<array>
<string>$SCRIPT_DIR/sync-yitu-it-series-r2.sh</string>
<string>sync</string>
</array>
<key>WorkingDirectory</key>
<string>$PLAYBOOKS_DIR</string>
<key>StartInterval</key>
<integer>${LAUNCHD_START_INTERVAL:-1800}</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>$HOME/rclone-yitu-it-series.launchd.out.log</string>
<key>StandardErrorPath</key>
<string>$HOME/rclone-yitu-it-series.launchd.err.log</string>
</dict>
</plist>
EOF
launchctl unload "$plist" >/dev/null 2>&1 || true
launchctl load "$plist"
launchctl start plus.svc.yitu-it-series.rclone-sync
echo "Installed $plist"
}
uninstall_launchd() {
local plist="$HOME/Library/LaunchAgents/plus.svc.yitu-it-series.rclone-sync.plist"
launchctl unload "$plist" >/dev/null 2>&1 || true
rm -f "$plist"
echo "Removed $plist"
}
case "${1:-}" in
doctor) doctor ;;
create-bucket) create_bucket ;;
configure-rclone) configure_rclone ;;
dry-run) sync_dry_run ;;
sync) sync_run ;;
copy) copy_run ;;
check) check_run ;;
tree) tree_run ;;
configure-custom-domain) configure_custom_domain ;;
install-launchd) install_launchd ;;
uninstall-launchd) uninstall_launchd ;;
-h|--help|help|"") usage ;;
*) usage; exit 2 ;;
esac

View File

@ -1,9 +1,12 @@
---
- name: Setup AI agent runtime
hosts: all
- name: Setup AI agent runtime desktop
hosts: jp-xhttp-contabo.svc.plus
become: true
gather_facts: true
vars:
xfce_manage_user: true
xfce_user_password_plaintext: "L@xiaomin1250"
xfce_user_shell: /bin/bash
xfce_enable_ufw: false
roles:
- role: roles/ai_agent_runtime/
when: ai_agent_runtime_enabled | default(true) | bool
tags: [ai_agent_runtime]
- roles/vhosts/xfce_xrdp_minimal/

View File

@ -0,0 +1,9 @@
---
- name: Setup AI agent skills
hosts: all
become: true
gather_facts: true
roles:
- role: roles/ai_agent_runtime/
when: ai_agent_runtime_enabled | default(true) | bool
tags: [ai_agent_runtime]

View File

@ -1,6 +1,7 @@
---
cloudflare_dns_default_source_hosts:
- cn_front_host
- cn_xworkmate_bridge_host
- jp_xhttp_contabo_host
- tky_proxy_host

View File

@ -1,12 +0,0 @@
---
- name: Setup minimal XFCE + XRDP desktop
hosts: jp-xhttp-contabo.svc.plus
become: true
gather_facts: true
vars:
xfce_manage_user: true
xfce_user_password_plaintext: "L@xiaomin1250"
xfce_user_shell: /bin/bash
xfce_enable_ufw: false
roles:
- roles/vhosts/xfce_xrdp_minimal/