chore: align AI agent runtime playbooks
This commit is contained in:
parent
7fbba293a0
commit
69e7691287
9
deploy_QMD.yml
Normal file
9
deploy_QMD.yml
Normal 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
9
deploy_agent_hermes.yml
Normal 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]
|
||||
|
||||
@ -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]
|
||||
@ -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
|
||||
|
||||
205
docs/yitu-it-series-r2-assets.md
Normal file
205
docs/yitu-it-series-r2-assets.md
Normal 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
|
||||

|
||||

|
||||
```
|
||||
|
||||
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.
|
||||
15
examples/yitu-it-series-r2.env.example
Normal file
15
examples/yitu-it-series-r2.env.example
Normal 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
|
||||
14
host_vars/cn-xworkmate-bridge.svc.plus.yml
Normal file
14
host_vars/cn-xworkmate-bridge.svc.plus.yml
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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
|
||||
|
||||
26
roles/vhosts/qmd/defaults/main.yml
Normal file
26
roles/vhosts/qmd/defaults/main.yml
Normal 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.
|
||||
139
roles/vhosts/qmd/tasks/main.yml
Normal file
139
roles/vhosts/qmd/tasks/main.yml
Normal 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('') }}"
|
||||
12
roles/vhosts/qmd/templates/index.yml.j2
Normal file
12
roles/vhosts/qmd/templates/index.yml.j2
Normal 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 %}
|
||||
18
roles/vhosts/qmd/templates/qmd-mcp.user.service.j2
Normal file
18
roles/vhosts/qmd/templates/qmd-mcp.user.service.j2
Normal 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
|
||||
|
||||
3
roles/vhosts/qmd/templates/qmd.env.j2
Normal file
3
roles/vhosts/qmd/templates/qmd.env.j2
Normal file
@ -0,0 +1,3 @@
|
||||
QMD_EMBED_API_BASE_URL={{ qmd_embed_api_base_url }}
|
||||
QMD_EMBED_MODEL={{ qmd_embed_model }}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
243
scripts/sync-yitu-it-series-r2.sh
Executable 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
|
||||
@ -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/
|
||||
|
||||
9
setup-ai-agent-skills.yml
Normal file
9
setup-ai-agent-skills.yml
Normal 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]
|
||||
@ -1,6 +1,7 @@
|
||||
---
|
||||
cloudflare_dns_default_source_hosts:
|
||||
- cn_front_host
|
||||
- cn_xworkmate_bridge_host
|
||||
- jp_xhttp_contabo_host
|
||||
- tky_proxy_host
|
||||
|
||||
|
||||
@ -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/
|
||||
Loading…
Reference in New Issue
Block a user