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
|
- name: Ensure minimal XFCE XRDP desktop baseline
|
||||||
hosts: "{{ xworkmate_bridge_hosts | default('all') }}"
|
hosts: "{{ xworkmate_bridge_hosts | default('all') }}"
|
||||||
become: true
|
become: true
|
||||||
@ -11,9 +7,15 @@
|
|||||||
- role: roles/vhosts/xfce_xrdp_minimal/
|
- role: roles/vhosts/xfce_xrdp_minimal/
|
||||||
tags: [xfce, xfce_xrdp_minimal]
|
tags: [xfce, xfce_xrdp_minimal]
|
||||||
|
|
||||||
- import_playbook: setup-ai-agent-runtime.yml
|
- import_playbook: setup-ai-agent-skills.yml
|
||||||
vars:
|
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) }}"
|
ai_agent_runtime_skills_enabled: "{{ xworkmate_bridge_agent_skills_enabled | default(false) }}"
|
||||||
|
|
||||||
- name: Deploy ACP vhosts through xworkmate bridge
|
- 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
|
# services: cn-homepage.svc.plus
|
||||||
cn-homepage.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root
|
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]
|
[global_homepage_host]
|
||||||
# services: global-homepage.svc.plus
|
# services: global-homepage.svc.plus
|
||||||
global-homepage.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root
|
global-homepage.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root
|
||||||
|
|||||||
@ -1,18 +1,31 @@
|
|||||||
# Agent Skills
|
# Agent Skills
|
||||||
|
|
||||||
Synchronizes the controller user's `~/.agents/skills/` directory to an Ubuntu
|
Synchronizes controller skill sources to an Ubuntu runtime user's canonical
|
||||||
runtime user's canonical skills directory, then exposes the same directory to
|
skills directory, then exposes the same directory to agent-specific skill
|
||||||
agent-specific skill locations.
|
locations.
|
||||||
|
|
||||||
Default source and target:
|
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/`
|
- remote canonical path: `/home/ubuntu/.agents/skills/`
|
||||||
- default agent targets: `codex`, `gemini`, `opencode`, `hermers`, `openclaw`
|
- 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
|
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
|
that canonical directory where the online runtime already uses links. Existing
|
||||||
default to avoid silently deleting agent-owned content. Set
|
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
|
`agent_skills_replace_existing_target_dirs=true` only when those target
|
||||||
directories should be replaced.
|
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
|
skills when neither installer is available; the role still fails later if a
|
||||||
required skill cannot be resolved.
|
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
|
After install, optional local quality gates run for each resolved skill when the
|
||||||
command exists:
|
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
|
`.pyc`, and `.DS_Store`; skills should ship source, scripts, templates, and
|
||||||
references rather than controller-local virtual environments.
|
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:
|
Example:
|
||||||
|
|
||||||
```bash
|
```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
|
Bootstrap-only example that keeps the existing local source strict but skips
|
||||||
quality gate failures from newly installed marketplace skills:
|
quality gate failures from newly installed marketplace skills:
|
||||||
|
|
||||||
```bash
|
```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
|
-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_home: "/home/{{ agent_skills_user }}"
|
||||||
|
|
||||||
agent_skills_local_source_dir: "{{ lookup('ansible.builtin.env', 'HOME') }}/.agents/skills"
|
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_remote_dir: "{{ agent_skills_home }}/.agents/skills"
|
||||||
agent_skills_local_source_create: true
|
agent_skills_local_source_create: true
|
||||||
agent_skills_delete_removed: true
|
agent_skills_delete_removed: false
|
||||||
agent_skills_rsync_compress: false
|
agent_skills_rsync_compress: false
|
||||||
agent_skills_rsync_timeout: 120
|
agent_skills_rsync_timeout: 120
|
||||||
agent_skills_install_rsync: true
|
agent_skills_install_rsync: true
|
||||||
agent_skills_replace_existing_target_dirs: false
|
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_enabled: true
|
||||||
agent_skills_auto_install_fail_on_missing_installer: true
|
agent_skills_auto_install_fail_on_missing_installer: true
|
||||||
agent_skills_quality_gate_enabled: true
|
agent_skills_quality_gate_enabled: true
|
||||||
@ -31,8 +37,10 @@ agent_skills_rsync_excludes:
|
|||||||
- .venv/
|
- .venv/
|
||||||
- __pycache__/
|
- __pycache__/
|
||||||
- "*.pyc"
|
- "*.pyc"
|
||||||
|
- "*/__pycache__/"
|
||||||
|
- "*/.DS_Store"
|
||||||
agent_skills_rsync_extra_opts:
|
agent_skills_rsync_extra_opts:
|
||||||
- "--delete-excluded"
|
- "--protocol=29"
|
||||||
- "--out-format=<<CHANGED>>%i"
|
- "--out-format=<<CHANGED>>%i"
|
||||||
|
|
||||||
agent_skills_typical_scenario_skills:
|
agent_skills_typical_scenario_skills:
|
||||||
@ -121,9 +129,6 @@ agent_skills_targets:
|
|||||||
paths:
|
paths:
|
||||||
- "{{ agent_skills_home }}/.opencode/skills"
|
- "{{ agent_skills_home }}/.opencode/skills"
|
||||||
- "{{ agent_skills_home }}/.config/opencode/skills"
|
- "{{ agent_skills_home }}/.config/opencode/skills"
|
||||||
- name: hermers
|
|
||||||
paths:
|
|
||||||
- "{{ agent_skills_home }}/.hermers/skills"
|
|
||||||
- name: openclaw
|
- name: openclaw
|
||||||
paths:
|
paths:
|
||||||
- "{{ agent_skills_home }}/.openclaw/skills"
|
- "{{ agent_skills_home }}/.openclaw/skills"
|
||||||
|
|||||||
@ -6,9 +6,10 @@
|
|||||||
- agent_skills_group | length > 0
|
- agent_skills_group | length > 0
|
||||||
- agent_skills_home | length > 0
|
- agent_skills_home | length > 0
|
||||||
- agent_skills_local_source_dir | 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_remote_dir | length > 0
|
||||||
- agent_skills_targets | 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
|
- name: Build required agent skills list
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
@ -21,6 +22,7 @@
|
|||||||
mode: "0755"
|
mode: "0755"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
become: false
|
become: false
|
||||||
|
check_mode: false
|
||||||
when:
|
when:
|
||||||
- agent_skills_local_source_create | bool
|
- agent_skills_local_source_create | bool
|
||||||
- agent_skills_auto_install_enabled | bool
|
- agent_skills_auto_install_enabled | bool
|
||||||
@ -38,14 +40,52 @@
|
|||||||
- agent_skills_local_source.stat.isdir | default(false)
|
- agent_skills_local_source.stat.isdir | default(false)
|
||||||
fail_msg: "Local skills source directory does not exist: {{ agent_skills_local_source_dir }}"
|
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
|
- name: Inspect required local scenario skills
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
set -eu
|
set -eu
|
||||||
|
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
|
for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
|
||||||
if [ -f "{{ agent_skills_local_source_dir }}/$candidate/SKILL.md" ]; then
|
if [ -f "$source_dir/$candidate/SKILL.md" ]; then
|
||||||
printf '%s\n' "{{ agent_skills_local_source_dir }}/$candidate"
|
printf '%s\n' "$source_dir/$candidate"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
match="$(find "$source_dir" -type f -path "*/$candidate/SKILL.md" -print -quit)"
|
||||||
|
if [ -n "$match" ]; then
|
||||||
|
dirname "$match"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
done
|
done
|
||||||
exit 1
|
exit 1
|
||||||
args:
|
args:
|
||||||
@ -58,6 +98,7 @@
|
|||||||
label: "{{ item.name }}"
|
label: "{{ item.name }}"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
become: false
|
become: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Build missing scenario skills list
|
- name: Build missing scenario skills list
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
@ -116,11 +157,18 @@
|
|||||||
- name: Reinspect required local scenario skills after auto install
|
- name: Reinspect required local scenario skills after auto install
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
set -eu
|
set -eu
|
||||||
|
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
|
for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
|
||||||
if [ -f "{{ agent_skills_local_source_dir }}/$candidate/SKILL.md" ]; then
|
if [ -f "$source_dir/$candidate/SKILL.md" ]; then
|
||||||
printf '%s\n' "{{ agent_skills_local_source_dir }}/$candidate"
|
printf '%s\n' "$source_dir/$candidate"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
match="$(find "$source_dir" -type f -path "*/$candidate/SKILL.md" -print -quit)"
|
||||||
|
if [ -n "$match" ]; then
|
||||||
|
dirname "$match"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
done
|
done
|
||||||
exit 1
|
exit 1
|
||||||
args:
|
args:
|
||||||
@ -134,6 +182,7 @@
|
|||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
become: false
|
become: false
|
||||||
when: agent_skills_auto_install_enabled | bool
|
when: agent_skills_auto_install_enabled | bool
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Build unresolved scenario skills list
|
- name: Build unresolved scenario skills list
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
@ -199,6 +248,7 @@
|
|||||||
when:
|
when:
|
||||||
- agent_skills_quality_gate_enabled | bool
|
- agent_skills_quality_gate_enabled | bool
|
||||||
- agent_skills_resolved_local_paths | length > 0
|
- agent_skills_resolved_local_paths | length > 0
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Detect local top-level symlink skills
|
- name: Detect local top-level symlink skills
|
||||||
ansible.builtin.find:
|
ansible.builtin.find:
|
||||||
@ -255,10 +305,15 @@
|
|||||||
'--partial',
|
'--partial',
|
||||||
'--timeout=' ~ (agent_skills_rsync_timeout | string)
|
'--timeout=' ~ (agent_skills_rsync_timeout | string)
|
||||||
]
|
]
|
||||||
|
+ (['--dry-run'] if ansible_check_mode else [])
|
||||||
+ (['-z'] if (agent_skills_rsync_compress | bool) 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')
|
| map('regex_replace', '^(.*)$', '--exclude=\1')
|
||||||
| list
|
| list
|
||||||
)
|
)
|
||||||
@ -266,7 +321,7 @@
|
|||||||
+ [
|
+ [
|
||||||
'-e',
|
'-e',
|
||||||
'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null',
|
'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null',
|
||||||
agent_skills_local_source_dir ~ '/',
|
item ~ '/',
|
||||||
(
|
(
|
||||||
ansible_user | default(ansible_ssh_user) | default('root')
|
ansible_user | default(ansible_ssh_user) | default('root')
|
||||||
) ~ '@' ~ (
|
) ~ '@' ~ (
|
||||||
@ -276,6 +331,11 @@
|
|||||||
}}
|
}}
|
||||||
register: agent_skills_rsync_result
|
register: agent_skills_rsync_result
|
||||||
changed_when: "'<<CHANGED>>' in agent_skills_rsync_result.stdout"
|
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
|
delegate_to: localhost
|
||||||
become: false
|
become: false
|
||||||
|
|
||||||
@ -287,6 +347,47 @@
|
|||||||
group: "{{ agent_skills_group }}"
|
group: "{{ agent_skills_group }}"
|
||||||
recurse: true
|
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
|
- name: Flatten agent skills target paths
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
agent_skills_target_paths: "{{ agent_skills_targets | subelements('paths') | map('last') | list }}"
|
agent_skills_target_paths: "{{ agent_skills_targets | subelements('paths') | map('last') | list }}"
|
||||||
@ -307,6 +408,7 @@
|
|||||||
when:
|
when:
|
||||||
- item.stat.exists | default(false)
|
- item.stat.exists | default(false)
|
||||||
- not item.stat.islnk | 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
|
- not agent_skills_replace_existing_target_dirs | bool
|
||||||
|
|
||||||
- name: Replace existing non-link target directories when enabled
|
- name: Replace existing non-link target directories when enabled
|
||||||
@ -317,6 +419,7 @@
|
|||||||
when:
|
when:
|
||||||
- item.stat.exists | default(false)
|
- item.stat.exists | default(false)
|
||||||
- not item.stat.islnk | 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
|
- agent_skills_replace_existing_target_dirs | bool
|
||||||
|
|
||||||
- name: Build agent skills target parent paths
|
- name: Build agent skills target parent paths
|
||||||
@ -345,10 +448,9 @@
|
|||||||
src: "{{ agent_skills_remote_dir }}"
|
src: "{{ agent_skills_remote_dir }}"
|
||||||
dest: "{{ item }}"
|
dest: "{{ item }}"
|
||||||
state: link
|
state: link
|
||||||
owner: "{{ agent_skills_user }}"
|
|
||||||
group: "{{ agent_skills_group }}"
|
|
||||||
force: true
|
force: true
|
||||||
loop: "{{ agent_skills_target_paths }}"
|
loop: "{{ agent_skills_target_paths }}"
|
||||||
|
when: item not in agent_skills_preserve_existing_target_dirs
|
||||||
|
|
||||||
- name: Verify canonical skill manifests are present
|
- name: Verify canonical skill manifests are present
|
||||||
ansible.builtin.find:
|
ansible.builtin.find:
|
||||||
|
|||||||
@ -6,25 +6,32 @@ role entrypoint. The role installs:
|
|||||||
- base tools: `curl`, `wget`, `git`, `jq`, `rsync`, `unzip`
|
- base tools: `curl`, `wget`, `git`, `jq`, `rsync`, `unzip`
|
||||||
- Node.js runtime for Playwright-based agents
|
- Node.js runtime for Playwright-based agents
|
||||||
- Python 3 toolchain for scripts and helpers
|
- 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
|
- `pandoc` + XeLaTeX PDF toolchain
|
||||||
- Chinese fonts for document rendering
|
- 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:
|
Design constraints:
|
||||||
|
|
||||||
- system packages are the primary source of truth
|
- 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
|
- Chinese PDF rendering is treated as a runtime requirement, not an optional add-on
|
||||||
|
|
||||||
Default Playwright environment:
|
Default Playwright environment:
|
||||||
|
|
||||||
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`
|
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`
|
||||||
- `PLAYWRIGHT_BROWSERS_PATH=0`
|
- `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:
|
Example:
|
||||||
|
|
||||||
```bash
|
```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
|
- wget
|
||||||
|
|
||||||
ai_agent_runtime_nodejs_enabled: true
|
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_install_yarn: true
|
||||||
ai_agent_runtime_yarn_version: ""
|
ai_agent_runtime_yarn_version: ""
|
||||||
ai_agent_runtime_npm_global_packages: []
|
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_enabled: true
|
||||||
ai_agent_runtime_browser_packages:
|
ai_agent_runtime_browser_packages:
|
||||||
- chromium
|
- google-chrome-stable
|
||||||
ai_agent_runtime_browser_executable: /usr/bin/chromium
|
ai_agent_runtime_browser_executable: /usr/local/bin/chromium
|
||||||
|
|
||||||
ai_agent_runtime_docs_enabled: true
|
ai_agent_runtime_docs_enabled: true
|
||||||
ai_agent_runtime_doc_packages:
|
ai_agent_runtime_doc_packages:
|
||||||
|
|||||||
@ -1,26 +1,18 @@
|
|||||||
---
|
---
|
||||||
- name: Install AI runtime browser packages
|
- name: Resolve existing Chromium executable
|
||||||
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
|
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
set -eu
|
set -eu
|
||||||
for candidate in \
|
for candidate in \
|
||||||
"{{ ai_agent_runtime_browser_executable }}" \
|
"{{ 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 \
|
||||||
chromium-browser \
|
chromium-browser \
|
||||||
google-chrome \
|
google-chrome \
|
||||||
google-chrome-stable \
|
google-chrome-stable \
|
||||||
/usr/bin/chromium \
|
/usr/bin/chromium \
|
||||||
/usr/local/bin/chromium \
|
|
||||||
/snap/bin/chromium; do
|
/snap/bin/chromium; do
|
||||||
if command -v "$candidate" >/dev/null 2>&1; then
|
if command -v "$candidate" >/dev/null 2>&1; then
|
||||||
command -v "$candidate"
|
command -v "$candidate"
|
||||||
@ -36,6 +28,51 @@
|
|||||||
executable: /bin/sh
|
executable: /bin/sh
|
||||||
register: ai_agent_runtime_browser_resolve
|
register: ai_agent_runtime_browser_resolve
|
||||||
changed_when: false
|
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
|
- name: Set resolved Chromium executable
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
- name: Configure shared agent skills
|
- name: Configure shared agent skills
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: "{{ ai_agent_runtime_skills_role_name }}"
|
name: "{{ ai_agent_runtime_skills_role_name }}"
|
||||||
|
apply:
|
||||||
|
tags: agent_skills
|
||||||
when: ai_agent_runtime_skills_enabled | bool
|
when: ai_agent_runtime_skills_enabled | bool
|
||||||
|
tags: [agent_skills]
|
||||||
|
|
||||||
- name: Verify AI agent runtime
|
- name: Verify AI agent runtime
|
||||||
ansible.builtin.include_tasks: verify.yml
|
ansible.builtin.include_tasks: verify.yml
|
||||||
|
|||||||
@ -3,48 +3,56 @@
|
|||||||
ansible.builtin.command: node --version
|
ansible.builtin.command: node --version
|
||||||
register: ai_agent_runtime_node_version
|
register: ai_agent_runtime_node_version
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
when: ai_agent_runtime_nodejs_enabled | bool
|
when: ai_agent_runtime_nodejs_enabled | bool
|
||||||
|
|
||||||
- name: Check npm version
|
- name: Check npm version
|
||||||
ansible.builtin.command: npm --version
|
ansible.builtin.command: npm --version
|
||||||
register: ai_agent_runtime_npm_version
|
register: ai_agent_runtime_npm_version
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
when: ai_agent_runtime_nodejs_enabled | bool
|
when: ai_agent_runtime_nodejs_enabled | bool
|
||||||
|
|
||||||
- name: Check python version
|
- name: Check python version
|
||||||
ansible.builtin.command: python3 --version
|
ansible.builtin.command: python3 --version
|
||||||
register: ai_agent_runtime_python_version
|
register: ai_agent_runtime_python_version
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
when: ai_agent_runtime_python_enabled | bool
|
when: ai_agent_runtime_python_enabled | bool
|
||||||
|
|
||||||
- name: Check pip version
|
- name: Check pip version
|
||||||
ansible.builtin.command: pip3 --version
|
ansible.builtin.command: pip3 --version
|
||||||
register: ai_agent_runtime_pip_version
|
register: ai_agent_runtime_pip_version
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
when: ai_agent_runtime_python_enabled | bool
|
when: ai_agent_runtime_python_enabled | bool
|
||||||
|
|
||||||
- name: Check chromium version
|
- name: Check chromium version
|
||||||
ansible.builtin.command: "{{ ai_agent_runtime_browser_resolved_executable | default(ai_agent_runtime_browser_executable) }} --version"
|
ansible.builtin.command: "{{ ai_agent_runtime_browser_resolved_executable | default(ai_agent_runtime_browser_executable) }} --version"
|
||||||
register: ai_agent_runtime_chromium_version
|
register: ai_agent_runtime_chromium_version
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
when: ai_agent_runtime_browser_enabled | bool
|
when: ai_agent_runtime_browser_enabled | bool
|
||||||
|
|
||||||
- name: Check pandoc version
|
- name: Check pandoc version
|
||||||
ansible.builtin.command: pandoc --version
|
ansible.builtin.command: pandoc --version
|
||||||
register: ai_agent_runtime_pandoc_version
|
register: ai_agent_runtime_pandoc_version
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
when: ai_agent_runtime_docs_enabled | bool
|
when: ai_agent_runtime_docs_enabled | bool
|
||||||
|
|
||||||
- name: Check xelatex version
|
- name: Check xelatex version
|
||||||
ansible.builtin.command: xelatex --version
|
ansible.builtin.command: xelatex --version
|
||||||
register: ai_agent_runtime_xelatex_version
|
register: ai_agent_runtime_xelatex_version
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
when: ai_agent_runtime_docs_enabled | bool
|
when: ai_agent_runtime_docs_enabled | bool
|
||||||
|
|
||||||
- name: Check Chinese font inventory
|
- name: Check Chinese font inventory
|
||||||
ansible.builtin.command: fc-list :lang=zh family
|
ansible.builtin.command: fc-list :lang=zh family
|
||||||
register: ai_agent_runtime_chinese_fonts
|
register: ai_agent_runtime_chinese_fonts
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
when:
|
when:
|
||||||
- ai_agent_runtime_fonts_enabled | bool
|
- ai_agent_runtime_fonts_enabled | bool
|
||||||
- ai_agent_runtime_verify_chinese_fonts | bool
|
- ai_agent_runtime_verify_chinese_fonts | bool
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
register: acp_codex_bridge_binary_attrs
|
register: acp_codex_bridge_binary_attrs
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Remove immutable flag from Codex bridge binary when present
|
- name: Remove immutable flag from Codex bridge binary when present
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
@ -74,6 +75,7 @@
|
|||||||
register: acp_codex_service_attrs
|
register: acp_codex_service_attrs
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Remove immutable flag from Codex ACP systemd service when present
|
- name: Remove immutable flag from Codex ACP systemd service when present
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
register: acp_hermes_bridge_binary_attrs
|
register: acp_hermes_bridge_binary_attrs
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Remove immutable flag from Hermes bridge binary when present
|
- name: Remove immutable flag from Hermes bridge binary when present
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
@ -58,6 +59,7 @@
|
|||||||
register: acp_hermes_service_attrs
|
register: acp_hermes_service_attrs
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Remove immutable flag from Hermes ACP systemd service when present
|
- name: Remove immutable flag from Hermes ACP systemd service when present
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
@ -80,6 +82,7 @@
|
|||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Resolve Hermes ACP auth token
|
- name: Resolve Hermes ACP auth token
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|||||||
@ -44,7 +44,17 @@ gateway_openclaw_acp_max_concurrent_sessions: 2
|
|||||||
gateway_openclaw_acp_backend: acpx
|
gateway_openclaw_acp_backend: acpx
|
||||||
gateway_openclaw_acp_default_agent: codex
|
gateway_openclaw_acp_default_agent: codex
|
||||||
gateway_openclaw_codex_app_server_url: ws://127.0.0.1:9001
|
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:
|
gateway_openclaw_main_agent_skills:
|
||||||
- acp-router
|
- acp-router
|
||||||
@ -76,3 +86,75 @@ gateway_openclaw_main_agent_skills:
|
|||||||
- video-translator
|
- video-translator
|
||||||
- web-search
|
- web-search
|
||||||
- self-improving
|
- 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,
|
"bootstrapMaxChars": 50000,
|
||||||
"bootstrapTotalMaxChars": 300000,
|
"bootstrapTotalMaxChars": 300000,
|
||||||
"model": {{ gateway_openclaw_default_model | to_json }},
|
"model": {{ gateway_openclaw_default_model | to_json }},
|
||||||
|
"models": {{ gateway_openclaw_default_models | to_json }},
|
||||||
"thinkingDefault": "low"
|
"thinkingDefault": "low"
|
||||||
},
|
},
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"id": "main",
|
"id": "main",
|
||||||
|
"model": {{ gateway_openclaw_main_agent_model | to_json }},
|
||||||
"skills": {{ gateway_openclaw_main_agent_skills | unique | list | to_json }}
|
"skills": {{ gateway_openclaw_main_agent_skills | unique | list | to_json }}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -85,7 +87,7 @@
|
|||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"mode": "merge",
|
"mode": "merge",
|
||||||
"providers": {}
|
"providers": {{ gateway_openclaw_model_providers | to_json }}
|
||||||
},
|
},
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"lastRunAt": "2026-04-19T10:52:37.655Z",
|
"lastRunAt": "2026-04-19T10:52:37.655Z",
|
||||||
@ -106,6 +108,9 @@
|
|||||||
"defaultAgent": {{ gateway_openclaw_acp_default_agent | to_json }},
|
"defaultAgent": {{ gateway_openclaw_acp_default_agent | to_json }},
|
||||||
"maxConcurrentSessions": {{ gateway_openclaw_acp_max_concurrent_sessions | int }}
|
"maxConcurrentSessions": {{ gateway_openclaw_acp_max_concurrent_sessions | int }}
|
||||||
},
|
},
|
||||||
|
"mcp": {
|
||||||
|
"servers": {{ gateway_openclaw_mcp_servers | to_json }}
|
||||||
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"entries": {
|
"entries": {
|
||||||
"nvidia": {"enabled": true},
|
"nvidia": {"enabled": true},
|
||||||
@ -119,9 +124,14 @@
|
|||||||
"appServer": {
|
"appServer": {
|
||||||
"transport": "websocket",
|
"transport": "websocket",
|
||||||
"url": {{ gateway_openclaw_codex_app_server_url | to_json }}
|
"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},
|
"openclaw-multi-session-plugins": {"enabled": true},
|
||||||
"device-pair": {"enabled": false},
|
"device-pair": {"enabled": false},
|
||||||
"phone-control": {"enabled": false},
|
"phone-control": {"enabled": false},
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
register: node_version_check
|
register: node_version_check
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Get Node.js version number
|
- name: Get Node.js version number
|
||||||
set_fact:
|
set_fact:
|
||||||
@ -83,12 +84,14 @@
|
|||||||
register: npm_version_check
|
register: npm_version_check
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Get current Yarn version
|
- name: Get current Yarn version
|
||||||
command: yarn --version
|
command: yarn --version
|
||||||
register: yarn_version_check
|
register: yarn_version_check
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
when: install_yarn | default(true)
|
when: install_yarn | default(true)
|
||||||
|
|
||||||
- name: Normalize desired Yarn version
|
- 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_groups: []
|
||||||
xfce_user_shell: /bin/bash
|
xfce_user_shell: /bin/bash
|
||||||
xfce_user_password_plaintext: ""
|
xfce_user_password_plaintext: ""
|
||||||
|
xfce_user_update_password: on_create
|
||||||
|
|
||||||
xfce_google_chrome_version: "148.0.7778.167-1"
|
xfce_google_chrome_version: "148.0.7778.167-1"
|
||||||
xfce_google_chrome_apt_key_url: "https://dl.google.com/linux/linux_signing_key.pub"
|
xfce_google_chrome_apt_key_url: "https://dl.google.com/linux/linux_signing_key.pub"
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
- snapd.apparmor.service
|
- snapd.apparmor.service
|
||||||
become: true
|
become: true
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
- name: Block snap and snap-backed browser transitional packages
|
- name: Block snap and snap-backed browser transitional packages
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
@ -120,14 +122,16 @@
|
|||||||
mode: "0755"
|
mode: "0755"
|
||||||
become: true
|
become: true
|
||||||
|
|
||||||
- name: Keep Chromium compatibility commands pointed at Chrome deb
|
- name: Keep Chromium compatibility commands disabled
|
||||||
ansible.builtin.file:
|
ansible.builtin.copy:
|
||||||
src: /usr/local/bin/chromium-xrdp
|
content: |
|
||||||
|
#!/bin/sh
|
||||||
|
echo "Chromium is disabled on this host. Use google-chrome instead." >&2
|
||||||
|
exit 126
|
||||||
dest: "{{ item }}"
|
dest: "{{ item }}"
|
||||||
state: link
|
|
||||||
force: true
|
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
|
mode: "0755"
|
||||||
loop:
|
loop:
|
||||||
- /usr/local/bin/chromium
|
- /usr/local/bin/chromium
|
||||||
- /usr/local/bin/chromium-browser
|
- /usr/local/bin/chromium-browser
|
||||||
@ -162,6 +166,8 @@
|
|||||||
environment:
|
environment:
|
||||||
HOME: "{{ xfce_user_home }}"
|
HOME: "{{ xfce_user_home }}"
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
- name: Set xdg default web browser to Google Chrome XRDP desktop entry
|
- 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 }}"
|
ansible.builtin.command: "xdg-settings set default-web-browser {{ xfce_google_chrome_desktop_file }}"
|
||||||
@ -170,6 +176,8 @@
|
|||||||
environment:
|
environment:
|
||||||
HOME: "{{ xfce_user_home }}"
|
HOME: "{{ xfce_user_home }}"
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
- name: Set system browser alternatives to Google Chrome deb
|
- name: Set system browser alternatives to Google Chrome deb
|
||||||
ansible.builtin.command: "update-alternatives --set {{ item }} /usr/bin/google-chrome-stable"
|
ansible.builtin.command: "update-alternatives --set {{ item }} /usr/bin/google-chrome-stable"
|
||||||
@ -178,12 +186,15 @@
|
|||||||
- gnome-www-browser
|
- gnome-www-browser
|
||||||
become: true
|
become: true
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
- name: Verify Google Chrome deb browser installation
|
- name: Verify Google Chrome deb browser installation
|
||||||
ansible.builtin.command: /usr/local/bin/chromium-xrdp --version
|
ansible.builtin.command: /usr/local/bin/chromium-xrdp --version
|
||||||
register: xfce_google_chrome_version_check
|
register: xfce_google_chrome_version_check
|
||||||
changed_when: false
|
changed_when: false
|
||||||
become: true
|
become: true
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Show Google Chrome deb browser version
|
- name: Show Google Chrome deb browser version
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
|
|||||||
@ -22,11 +22,13 @@
|
|||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ xfce_user }}"
|
name: "{{ xfce_user }}"
|
||||||
password: "{{ xfce_user_password_plaintext | password_hash('sha512') }}"
|
password: "{{ xfce_user_password_plaintext | password_hash('sha512') }}"
|
||||||
update_password: always
|
update_password: "{{ xfce_user_update_password }}"
|
||||||
password_lock: false
|
password_lock: false
|
||||||
become: true
|
become: true
|
||||||
no_log: 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
|
- name: Ensure the desktop user can sudo
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
xworkmate_bridge_service_name: xworkmate-bridge
|
xworkmate_bridge_service_name: xworkmate-bridge
|
||||||
xworkmate_bridge_service_user: ubuntu
|
xworkmate_bridge_service_user: ubuntu
|
||||||
xworkmate_bridge_service_group: 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_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_host: 127.0.0.1
|
||||||
xworkmate_bridge_listen_port: 8787
|
xworkmate_bridge_listen_port: 8787
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=XWorkmate bridge control plane
|
Description=XWorkmate bridge control plane
|
||||||
|
{% if xworkmate_bridge_required_services | length > 0 %}
|
||||||
Requires={{ xworkmate_bridge_required_services | join(' ') }}
|
Requires={{ xworkmate_bridge_required_services | join(' ') }}
|
||||||
After=network-online.target {{ xworkmate_bridge_required_services | join(' ') }}
|
After=network-online.target {{ xworkmate_bridge_required_services | join(' ') }}
|
||||||
|
{% else %}
|
||||||
|
After=network-online.target
|
||||||
|
{% endif %}
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@ -9,7 +13,7 @@ Type=simple
|
|||||||
User={{ xworkmate_bridge_service_user }}
|
User={{ xworkmate_bridge_service_user }}
|
||||||
Group={{ xworkmate_bridge_service_group }}
|
Group={{ xworkmate_bridge_service_group }}
|
||||||
WorkingDirectory={{ xworkmate_bridge_base_dir }}
|
WorkingDirectory={{ xworkmate_bridge_base_dir }}
|
||||||
Environment="HOME=/home/{{ xworkmate_bridge_service_user }}"
|
Environment="HOME={{ xworkmate_bridge_service_home }}"
|
||||||
Environment="TERM=xterm-256color"
|
Environment="TERM=xterm-256color"
|
||||||
{% for key, value in xworkmate_bridge_service_environment | dictsort %}
|
{% for key, value in xworkmate_bridge_service_environment | dictsort %}
|
||||||
{% if value | string | trim | length > 0 %}
|
{% 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
|
- name: Setup AI agent runtime desktop
|
||||||
hosts: all
|
hosts: jp-xhttp-contabo.svc.plus
|
||||||
become: true
|
become: true
|
||||||
gather_facts: 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:
|
||||||
- role: roles/ai_agent_runtime/
|
- roles/vhosts/xfce_xrdp_minimal/
|
||||||
when: ai_agent_runtime_enabled | default(true) | bool
|
|
||||||
tags: [ai_agent_runtime]
|
|
||||||
|
|||||||
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:
|
cloudflare_dns_default_source_hosts:
|
||||||
- cn_front_host
|
- cn_front_host
|
||||||
|
- cn_xworkmate_bridge_host
|
||||||
- jp_xhttp_contabo_host
|
- jp_xhttp_contabo_host
|
||||||
- tky_proxy_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