From 6b2ef1137a0bc345cd055d7704884f690c3042a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Sat, 7 Mar 2026 23:54:43 +0800 Subject: [PATCH] add static-site-deploy --- skills/static-site-deploy/SKILL.md | 132 +++++++++++++ skills/static-site-deploy/scripts/deploy.sh | 177 ++++++++++++++++++ skills/static-site-deploy/scripts/download.sh | 152 +++++++++++++++ skills/static-site-deploy/scripts/list.sh | 122 ++++++++++++ .../scripts/static-site-deploy.yml | 9 + 5 files changed, 592 insertions(+) create mode 100644 skills/static-site-deploy/SKILL.md create mode 100755 skills/static-site-deploy/scripts/deploy.sh create mode 100755 skills/static-site-deploy/scripts/download.sh create mode 100755 skills/static-site-deploy/scripts/list.sh create mode 100644 skills/static-site-deploy/scripts/static-site-deploy.yml diff --git a/skills/static-site-deploy/SKILL.md b/skills/static-site-deploy/SKILL.md new file mode 100644 index 0000000..2f3aca5 --- /dev/null +++ b/skills/static-site-deploy/SKILL.md @@ -0,0 +1,132 @@ +--- +name: static-site-deploy +description: |- + Deploy, download, and browse static HTML sites on VPS via FTP. +triggers: + - deploy static site + - deploy html + - deploy to vps + - static deploy + - download from vps + - list files on vps + - browse ftp + - 部署静态网站 + - 部署HTML + - 部署到VPS + - 从服务器下载 + - 浏览服务器文件 +--- + +# Static Site Deploy + +Upload, download, and browse static HTML files on a VPS via FTP. + +## Use when + +- Deploying static HTML/CSS/JS files to a VPS +- Downloading deployed files from VPS +- Browsing files and directories on VPS +- Publishing a single-page app, landing page, or documentation site +- User says "deploy", "upload to server", "publish site", "download from server", or "list files" + +## Prerequisites + +- ASSISTANT_ID environment variable must be set (bot_id) +- Deployment script configured with FTP credentials + +## Deployment Workflow + +### Phase 1: Prepare + +1. Identify the source directory (e.g., `./dist/`, `./build/`, `./public/`, or user-specified) +2. Determine `project-name`: + - From user input, OR + - From the source directory's parent folder name +3. Verify ASSISTANT_ID environment variable is set + +### Phase 2: Deploy + +Execute the deploy script: + +```bash +# Basic usage +bash scripts/deploy.sh + +# With custom config +bash scripts/deploy.sh --config /path/to/config.yml + +# Dry run (preview without uploading) +bash scripts/deploy.sh --dry-run +``` + +The script will: +- Upload files to VPS +- Show deployment summary and progress +- Verify deployment via FTP and HTTP checks + +### Phase 3: Verify + +The script automatically verifies: +1. **FTP verification** - confirms files are present on server +2. **HTTP verification** - confirms site is accessible (expects HTTP 200) + +Final output shows: +``` +=== Deploy Complete === +URL: https://domain/path/to/deploy/ +``` + +## Error Handling + +| Error | Detection | Fix | +|-------|-----------|-----| +| FTP connection refused | curl returns "connection refused" | Check FTP service is running, verify port | +| FTP auth failed | curl returns 530 | Check username/password in config | +| Upload permission denied | curl returns 553 | Check FTP user write permission on web_root | +| HTTP 404 | curl returns 404 | Confirm Nginx root matches FTP upload path | +| HTTP 403 | curl returns 403 | Fix permissions: files 644, directories 755 | + +## Implementation Notes + +- Use `scripts/deploy.sh` for uploading files to VPS +- Use `scripts/download.sh` for downloading files from VPS +- Use `scripts/list.sh` for browsing FTP directory contents +- Verify ASSISTANT_ID environment variable is set before calling scripts +- Show the command to the user before executing +- The scripts handle all FTP operations, progress display, and verification + +## List Files Workflow + +To browse files on VPS: + +```bash +# List all projects under bot directory +bash scripts/list.sh + +# List files in a specific project +bash scripts/list.sh +``` + +The list script will: +- Read bot_id from ASSISTANT_ID environment variable +- Show directory contents from `/{bot_id}/` or `/{bot_id}/{project_name}/` +- Mark directories with trailing `/` +- Only list current directory level (non-recursive) + +## Download Workflow + +To download files from VPS: + +```bash +# Basic usage +bash scripts/download.sh + +# With custom config +bash scripts/download.sh --config /path/to/config.yml +``` + +The download script will: +- Read bot_id from ASSISTANT_ID environment variable +- Download files from `/{bot_id}/{project_name}/` +- Save to the specified target directory +- Show download summary and file count diff --git a/skills/static-site-deploy/scripts/deploy.sh b/skills/static-site-deploy/scripts/deploy.sh new file mode 100755 index 0000000..84fc74e --- /dev/null +++ b/skills/static-site-deploy/scripts/deploy.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Static Site Deploy - FTP upload helper script +# Usage: deploy.sh [--config path] [--dry-run] + +# ─── Argument Parsing ─────────────────────────────────────────────── + +SOURCE_DIR="" +PROJECT_NAME="" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/static-site-deploy.yml" +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --config) + CONFIG_FILE="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + *) + if [[ -z "$SOURCE_DIR" ]]; then + SOURCE_DIR="$1" + elif [[ -z "$PROJECT_NAME" ]]; then + PROJECT_NAME="$1" + else + echo "Error: unexpected argument '$1'" >&2 + exit 1 + fi + shift + ;; + esac +done + +if [[ -z "$SOURCE_DIR" || -z "$PROJECT_NAME" ]]; then + echo "Usage: deploy.sh [--config path] [--dry-run]" >&2 + exit 1 +fi + +if [[ ! -d "$SOURCE_DIR" ]]; then + echo "Error: source directory '$SOURCE_DIR' does not exist" >&2 + exit 1 +fi + +# Read bot_id from environment variable +BOT_ID="${ASSISTANT_ID:-}" +if [[ -z "$BOT_ID" ]]; then + echo "Error: ASSISTANT_ID environment variable is not set" >&2 + exit 1 +fi + +# ─── Config Parsing ───────────────────────────────────────────────── + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Error: config file '$CONFIG_FILE' not found" >&2 + exit 1 +fi + +# Simple YAML parser (no external dependencies) +parse_yaml() { + local key="$1" + grep "^${key}:" "$CONFIG_FILE" | sed "s/^${key}:[[:space:]]*//" | sed 's/[[:space:]]*#.*//' | sed 's/[[:space:]]*$//' | sed 's/^["'\'']\(.*\)["'\'']$/\1/' +} + +HOST=$(parse_yaml "host") +FTP_USER=$(parse_yaml "ftp_user") +FTP_PASS=$(parse_yaml "ftp_pass") +FTP_PORT=$(parse_yaml "ftp_port") +USE_FTPS=$(parse_yaml "use_ftps") +WEB_ROOT=$(parse_yaml "web_root") +DOMAIN=$(parse_yaml "domain") + +# Defaults +FTP_PORT="${FTP_PORT:-21}" +USE_FTPS="${USE_FTPS:-true}" + +# Validate required fields +for field in HOST FTP_USER FTP_PASS WEB_ROOT DOMAIN; do + if [[ -z "${!field}" ]]; then + echo "Error: missing required config field: $(echo "$field" | tr '[:upper:]' '[:lower:]')" >&2 + exit 1 + fi +done + +REMOTE_PATH="${WEB_ROOT}/${BOT_ID}/${PROJECT_NAME}" +SITE_URL="https://${DOMAIN}/${BOT_ID}/${PROJECT_NAME}/" + +# ─── Summary ──────────────────────────────────────────────────────── + +FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l | tr -d ' ') +TOTAL_SIZE=$(du -sh "$SOURCE_DIR" 2>/dev/null | cut -f1) + +echo "=== Static Site Deploy ===" +echo "Bot ID: $BOT_ID" +echo "Source: $SOURCE_DIR ($FILE_COUNT files, $TOTAL_SIZE)" +echo "Project: $PROJECT_NAME" +echo "Remote: ftp://${FTP_USER}@${HOST}:${FTP_PORT}${REMOTE_PATH}/" +echo "URL: $SITE_URL" +echo "FTPS: $USE_FTPS" +echo "" + +if [[ "$DRY_RUN" == "true" ]]; then + echo "[DRY RUN] Files that would be uploaded:" + find "$SOURCE_DIR" -type f | while read -r file; do + rel_path="${file#"$SOURCE_DIR"}" + rel_path="${rel_path#/}" + echo " $rel_path" + done + echo "" + echo "[DRY RUN] No files were uploaded." + exit 0 +fi + +# ─── Upload ───────────────────────────────────────────────────────── + +echo "Uploading with curl..." + +curl_ssl_flag="" +if [[ "$USE_FTPS" == "true" ]]; then + curl_ssl_flag="--ssl-reqd" +fi + +ftp_base="ftp://${FTP_USER}:${FTP_PASS}@${HOST}:${FTP_PORT}" + +# Upload files one by one +uploaded=0 +failed=0 + +find "$SOURCE_DIR" -type f | while read -r file; do + rel_path="${file#"$SOURCE_DIR"}" + rel_path="${rel_path#/}" + remote_url="${ftp_base}${REMOTE_PATH}/${rel_path}" + + if curl -s -T "$file" --ftp-create-dirs $curl_ssl_flag "$remote_url"; then + echo " OK: $rel_path" + uploaded=$((uploaded + 1)) + else + echo " FAIL: $rel_path" >&2 + failed=$((failed + 1)) + fi +done + +if [[ $failed -gt 0 ]]; then + echo "Warning: $failed file(s) failed to upload" >&2 +fi + +echo "" + +# ─── Verify ───────────────────────────────────────────────────────── + +echo "=== Verification ===" + +# FTP verification +echo -n "FTP check... " +ftp_list_url="ftp://${FTP_USER}:${FTP_PASS}@${HOST}:${FTP_PORT}${REMOTE_PATH}/" +if curl -s --list-only "$ftp_list_url" --connect-timeout 5 >/dev/null 2>&1; then + echo "OK (files present on server)" +else + echo "WARN (could not list remote directory)" +fi + +# HTTP verification +echo -n "HTTP check... " +http_code=$(curl -sL -o /dev/null -w '%{http_code}' "$SITE_URL" --connect-timeout 10 2>/dev/null || echo "000") +if [[ "$http_code" == "200" ]]; then + echo "OK (HTTP $http_code)" +else + echo "WARN (HTTP $http_code - check Nginx config if not 200)" +fi + +echo "" +echo "=== Deploy Complete ===" +echo "URL: $SITE_URL" diff --git a/skills/static-site-deploy/scripts/download.sh b/skills/static-site-deploy/scripts/download.sh new file mode 100755 index 0000000..7924005 --- /dev/null +++ b/skills/static-site-deploy/scripts/download.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Static Site Download - FTP download helper script +# Usage: download.sh [--config path] + +# ─── Argument Parsing ─────────────────────────────────────────────── + +PROJECT_NAME="" +TARGET_DIR="" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/static-site-deploy.yml" + +while [[ $# -gt 0 ]]; do + case "$1" in + --config) + CONFIG_FILE="$2" + shift 2 + ;; + *) + if [[ -z "$PROJECT_NAME" ]]; then + PROJECT_NAME="$1" + elif [[ -z "$TARGET_DIR" ]]; then + TARGET_DIR="$1" + else + echo "Error: unexpected argument '$1'" >&2 + exit 1 + fi + shift + ;; + esac +done + +if [[ -z "$PROJECT_NAME" || -z "$TARGET_DIR" ]]; then + echo "Usage: download.sh [--config path]" >&2 + exit 1 +fi + +# Read bot_id from environment variable +BOT_ID="${ASSISTANT_ID:-}" +if [[ -z "$BOT_ID" ]]; then + echo "Error: ASSISTANT_ID environment variable is not set" >&2 + exit 1 +fi + +# ─── Config Parsing ───────────────────────────────────────────────── + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Error: config file '$CONFIG_FILE' not found" >&2 + exit 1 +fi + +# Simple YAML parser (no external dependencies) +parse_yaml() { + local key="$1" + grep "^${key}:" "$CONFIG_FILE" | sed "s/^${key}:[[:space:]]*//" | sed 's/[[:space:]]*#.*//' | sed 's/[[:space:]]*$//' | sed 's/^[\"'\'']\(.*\)[\"'\'']$/\1/' +} + +HOST=$(parse_yaml "host") +FTP_USER=$(parse_yaml "ftp_user") +FTP_PASS=$(parse_yaml "ftp_pass") +FTP_PORT=$(parse_yaml "ftp_port") +USE_FTPS=$(parse_yaml "use_ftps") +WEB_ROOT=$(parse_yaml "web_root") +DOMAIN=$(parse_yaml "domain") + +# Defaults +FTP_PORT="${FTP_PORT:-21}" +USE_FTPS="${USE_FTPS:-true}" + +# Validate required fields +for field in HOST FTP_USER FTP_PASS WEB_ROOT DOMAIN; do + if [[ -z "${!field}" ]]; then + echo "Error: missing required config field: $(echo "$field" | tr '[:upper:]' '[:lower:]')" >&2 + exit 1 + fi +done + +REMOTE_PATH="${WEB_ROOT}/${BOT_ID}/${PROJECT_NAME}" +SITE_URL="https://${DOMAIN}/${BOT_ID}/${PROJECT_NAME}/" + +# ─── Summary ──────────────────────────────────────────────────────── + +echo "=== Static Site Download ===" +echo "Bot ID: $BOT_ID" +echo "Project: $PROJECT_NAME" +echo "Remote: ftp://${FTP_USER}@${HOST}:${FTP_PORT}${REMOTE_PATH}/" +echo "Target: $TARGET_DIR" +echo "FTPS: $USE_FTPS" +echo "" + +# Create target directory +mkdir -p "$TARGET_DIR" + +# ─── Download ─────────────────────────────────────────────────────── + +echo "Downloading with curl..." + +curl_ssl_flag="" +if [[ "$USE_FTPS" == "true" ]]; then + curl_ssl_flag="--ssl-reqd" +fi + +ftp_base="ftp://${FTP_USER}:${FTP_PASS}@${HOST}:${FTP_PORT}" + +# List all files +echo "Listing remote files..." +file_list=$(curl -s --list-only $curl_ssl_flag "${ftp_base}${REMOTE_PATH}/" 2>/dev/null || echo "") + +if [[ -z "$file_list" ]]; then + echo "Error: Could not list remote directory or directory is empty" >&2 + exit 1 +fi + +downloaded=0 +failed=0 + +# Download each file +while IFS= read -r file; do + # Skip . and .. + if [[ "$file" == "." || "$file" == ".." ]]; then + continue + fi + + remote_url="${ftp_base}${REMOTE_PATH}/${file}" + local_file="${TARGET_DIR}/${file}" + + if curl -s -o "$local_file" $curl_ssl_flag "$remote_url" 2>/dev/null; then + echo " OK: $file" + downloaded=$((downloaded + 1)) + else + echo " FAIL: $file" >&2 + failed=$((failed + 1)) + fi +done <<< "$file_list" + +echo "" +echo "Downloaded: $downloaded file(s)" +if [[ $failed -gt 0 ]]; then + echo "Warning: $failed file(s) failed to download" >&2 +fi + +echo "" + +# ─── Summary ──────────────────────────────────────────────────────── + +FILE_COUNT=$(find "$TARGET_DIR" -type f 2>/dev/null | wc -l | tr -d ' ') +TOTAL_SIZE=$(du -sh "$TARGET_DIR" 2>/dev/null | cut -f1) + +echo "=== Download Complete ===" +echo "Target: $TARGET_DIR ($FILE_COUNT files, $TOTAL_SIZE)" +echo "Source URL: $SITE_URL" diff --git a/skills/static-site-deploy/scripts/list.sh b/skills/static-site-deploy/scripts/list.sh new file mode 100755 index 0000000..0f08bdc --- /dev/null +++ b/skills/static-site-deploy/scripts/list.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Static Site List - FTP directory listing helper script +# Usage: list.sh [project-name] [--config path] + +# ─── Argument Parsing ─────────────────────────────────────────────── + +PROJECT_NAME="" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/static-site-deploy.yml" + +while [[ $# -gt 0 ]]; do + case "$1" in + --config) + CONFIG_FILE="$2" + shift 2 + ;; + *) + if [[ -z "$PROJECT_NAME" ]]; then + PROJECT_NAME="$1" + else + echo "Error: unexpected argument '$1'" >&2 + exit 1 + fi + shift + ;; + esac +done + +# Read bot_id from environment variable +BOT_ID="${ASSISTANT_ID:-}" +if [[ -z "$BOT_ID" ]]; then + echo "Error: ASSISTANT_ID environment variable is not set" >&2 + exit 1 +fi + +# ─── Config Parsing ───────────────────────────────────────────────── + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Error: config file '$CONFIG_FILE' not found" >&2 + exit 1 +fi + +# Simple YAML parser (no external dependencies) +parse_yaml() { + local key="$1" + grep "^${key}:" "$CONFIG_FILE" | sed "s/^${key}:[[:space:]]*//" | sed 's/[[:space:]]*#.*//' | sed 's/[[:space:]]*$//' | sed 's/^[\"'\'']\(.*\)[\"'\'']$/\1/' +} + +HOST=$(parse_yaml "host") +FTP_USER=$(parse_yaml "ftp_user") +FTP_PASS=$(parse_yaml "ftp_pass") +FTP_PORT=$(parse_yaml "ftp_port") +USE_FTPS=$(parse_yaml "use_ftps") +WEB_ROOT=$(parse_yaml "web_root") +DOMAIN=$(parse_yaml "domain") + +# Defaults +FTP_PORT="${FTP_PORT:-21}" +USE_FTPS="${USE_FTPS:-true}" + +# Validate required fields +for field in HOST FTP_USER FTP_PASS WEB_ROOT DOMAIN; do + if [[ -z "${!field}" ]]; then + echo "Error: missing required config field: $(echo "$field" | tr '[:upper:]' '[:lower:]')" >&2 + exit 1 + fi +done + +# Determine remote path +if [[ -z "$PROJECT_NAME" ]]; then + # List bot directory + REMOTE_PATH="${WEB_ROOT}/${BOT_ID}" + DISPLAY_PATH="/${BOT_ID}/" +else + # List specific project + REMOTE_PATH="${WEB_ROOT}/${BOT_ID}/${PROJECT_NAME}" + DISPLAY_PATH="/${BOT_ID}/${PROJECT_NAME}/" +fi + +# ─── List Files ───────────────────────────────────────────────────── + +echo "=== FTP Directory Listing ===" +echo "Bot ID: $BOT_ID" +echo "Path: $DISPLAY_PATH" +echo "Remote: ftp://${FTP_USER}@${HOST}:${FTP_PORT}${REMOTE_PATH}/" +echo "" + +curl_ssl_flag="" +if [[ "$USE_FTPS" == "true" ]]; then + curl_ssl_flag="--ssl-reqd" +fi + +ftp_url="ftp://${FTP_USER}:${FTP_PASS}@${HOST}:${FTP_PORT}${REMOTE_PATH}/" + +# List directory +listing=$(curl -s --list-only $curl_ssl_flag "$ftp_url" 2>&1) + +if [[ $? -ne 0 ]]; then + echo "Error: Failed to list directory" >&2 + echo "$listing" >&2 + exit 1 +fi + +# Format output +echo "$listing" | while IFS= read -r item; do + # Skip . and .. + if [[ "$item" == "." || "$item" == ".." ]]; then + continue + fi + + # Check if it's a directory by trying to list it + if curl -s --list-only $curl_ssl_flag "${ftp_url}${item}/" >/dev/null 2>&1; then + echo "${item}/" + else + echo "$item" + fi +done + +echo "" +echo "=== Listing Complete ===" diff --git a/skills/static-site-deploy/scripts/static-site-deploy.yml b/skills/static-site-deploy/scripts/static-site-deploy.yml new file mode 100644 index 0000000..95fa199 --- /dev/null +++ b/skills/static-site-deploy/scripts/static-site-deploy.yml @@ -0,0 +1,9 @@ +host: 1.94.251.96 +ftp_user: deploy_aitravelmaster +ftp_pass: yWBJr3PWpJSZsJYe +ftp_port: 21 # optional, default 21 +use_ftps: false # optional, default true (FTP over TLS) + +# Web settings +web_root: / # remote base directory for uploads +domain: deploy.aitravelmaster.com # domain for generating access URLs