skills/deploy-server/SKILL.md

```
name: deploy-server
description: Use this skill to provision or update a Welley server on a supported cloud (currently DigitalOcean) when the user asks to deploy a new instance or run a server update. Covers single-instance deployments and SSH-based updates, including interactive collection of missing credentials, SSH keys, domain, region, and sizing.
```

# 部署服务器

## 概览

这个 skill 会在 DigitalOcean 上创建单实例的 Welley 服务,也可以通过 SSH 执行更新。它会引导 agent 收集缺少的输入信息,从指定来源获取可用的实例规格,创建虚拟机,配置主机,用 `wlycli deployment create` 完成部署,最后进行验证。

命令名称必须与 CLI 一致:

-   `wlycli deployment create`
-   `wlycli deployment update`
-   `wlycli deployment delete`

## 默认值(DigitalOcean)

-   区域:`nyc3`
-   镜像:`latest LTS available via API`(通过 `images?type=distribution` 获取)
-   Droplet 名称:随机生成,格式为 `wlsly-<adjective>-<surname>`(例如 `wlsly-bright-hopper`)。请使用 `references/digitalocean.md` 中的名称生成代码片段。如果创建托管数据库,数据库也使用同一个名称。
-   方案目标:Basic / Regular(共享 CPU)
-   规格目标:2 vCPU / 4 GB 内存 / 80 GB SSD / 4 TB 流量(约 $24/月)

## 覆盖默认值的规则

如果用户指定了任何默认值(区域、镜像、Droplet 名称、项目、规格),agent 必须:

1.  把用户的修改内容原样复述一遍。
2.  先请用户确认,再继续往下做。

## 交互式输入模板

只问缺少的信息。请使用下面这些原话提问,以减少歧义。 分几轮逐步提问。第 1 轮:每次最多问 2 个问题。第 2 轮可以给出一个打包的“全部使用默认值”选项。 如果运行环境支持选项,每个问题给出 2 到 3 个选项(A/B/C)。除非必要,避免开放式提问。

第 1 轮问题(身份与访问):

1.  “你有可以使用的 DigitalOcean API token 吗?还是我先等你提供?”
2.  “这台服务器要使用哪个域名(例如 `super-puper.example.com`)?这个域名的 DNS 由你管理吗?”
3.  “你已经有可用的 SSH 公钥了吗?还是由我来创建并上传一个?”
4.  “要把这台 Droplet 放进已有的 DigitalOcean 项目吗?如果要,请提供项目名称或 ID。”
5.  “DNS 要求:你需要为 `<domain>` 和 `storage.<domain>`(没有 CDN 时)创建 A/AAAA 记录。你能在运行 certbot 之前完成吗?”
6.  “邮件 DNS 要求:`--smtp-from` 的域名需要配置 SPF、DMARC 和 DKIM 记录,否则发给新收件人的邀请邮件和确认邮件可能会被悄悄丢弃。这些记录已经配好了吗?还是需要我列出你的 SMTP 服务商要求的记录?”
7.  如果用户问到在哪里获取 DO token,请提供这个链接:

```
https://cloud.digitalocean.com/account/api/tokens
```

第 2 轮问题(规格、位置和部署选项):

1.  “区域使用默认的 `nyc3`,还是换一个区域?”
2.  “规格使用默认的 2 vCPU / 2 GB,还是换一个 CPU/内存目标?”
3.  “我使用 API 中最新的 Ubuntu LTS,还是你指定某个 Ubuntu 版本?”
4.  “引导完成后使用哪个 SSH 用户(默认:`wlsly`)?可以保持默认。”
5.  “你想用托管的 Postgres 数据库,还是用自带的 Postgres 容器?”
6.  如果选择托管 Postgres:“数据库使用哪个区域和规格?我可以默认用 `nyc3` 和 1 vCPU / 2 GB。”
7.  如果选择托管 Postgres:“要我按服务商列出的月费价格继续创建吗?”
8.  “请提供必需的 SMTP 设置(host、user、password、from)。OTEL 是可选的,需要时再提供。”
9.  “创建时必须提供管理员邮箱。请现在提供(参数:`--email` / `-e`)。如果没有提供,我会让你输入两次来确认。”

在第 2 轮提供“全部使用默认值”的选项,加快收集输入的速度。

如果有任何回答缺失,就针对性地追问(一到两个问题)。

## 流程(DigitalOcean,单实例)

1.  **收集必需的输入信息(缺少就问)。**

-   DigitalOcean API token
-   项目名称或 ID(可选;缺少或找不到时使用默认项目)
-   区域
-   实例规格目标(例如 4 vCPU / 16 GB 内存)
-   域名
-   SSH 公钥(或确认由我们创建并注册一个)
-   用于首次登录和找回密码的管理员邮箱(创建时必填;`--email` / `-e`)。如果缺少,请让用户输入两次并确认一致。
-   可选:自定义 TLS 证书,或使用 certbot
-   确认有 DNS 操作权限(必须能为 `<domain>` 创建 A/AAAA 记录,如果用到 `storage.<domain>`,也要为它创建)

1.  **获取实例类型并选择规格。**

-   使用 `references/digitalocean.md` 中的规格代码片段,列出实例类型并按 CPU/内存筛选。
-   如果有多个匹配项,请向用户确认。

2b. **确定 Ubuntu 镜像 slug。**

-   调用 `GET /v2/images?type=distribution&per_page=200`,筛选出 Ubuntu。
-   如果用户指定了版本,先确认这个 slug 存在,再向用户确认。
-   否则从列表中选最新的 LTS slug,创建之前先向用户确认。

1.  **创建 Droplet。**

-   直接使用 DigitalOcean API(doctl 是可选的)。
-   镜像:优先使用用户指定的;否则从 API 列表中选最新的 LTS。
-   附上 SSH 密钥指纹,或者创建并上传一个密钥。
-   启用监控。
-   如果提供了项目名称/ID 但找不到,就不指定项目来创建 Droplet(使用默认项目)。
-   注意:托管数据库的创建可以和 Droplet 的创建同时进行。

## 并行处理指引

agent 应该让互不依赖的任务同时进行,以缩短总耗时:

-   在收集用户输入(第 2 轮)的同时,如果 token、区域、规格、镜像和 SSH 密钥都已确认,就可以开始创建 Droplet。
-   Droplet 创建期间,开始创建托管 Postgres(如果用户需要)。
-   Droplet 启动期间,继续收集剩下的输入(SMTP、OTEL、管理员邮箱)。
-   如果某一步缺少关键输入,就先暂停这一步,继续做其他可以并行的任务。
-   如果 DNS 在 DigitalOcean 上管理,一拿到 Droplet 的 IP 就创建/更新 A/AAAA 记录(不用等到完整的部署步骤)。

1.  **DNS / 域名。**

-   询问是在 DigitalOcean 创建或更新 DNS,还是继续使用外部 DNS。
-   如果使用 DigitalOcean DNS,创建指向 Droplet IP 的 A/AAAA 记录。

1.  **通过 SSH 配置主机。**

-   创建一个非 root 用户(例如 `wlsly`),授予 sudo 权限,并复制 SSH authorized keys。
-   **退出 root,用这个非 root 用户重新登录**,然后再继续。
-   安装 Docker 和 docker compose。
-   从 AppStore 下载 Welley CLI 二进制文件,并设为可执行:
    -   基础 URL:`https://appstore.allroads.social/v1/social.wellesley.wlycli/latest?develop=stable&platform=<os>/<arch>`
    -   按系统和架构选择正确的 `platform` 值:
        -   Linux x86\_64:`linux/amd64`
        -   Linux arm64:`linux/arm64`
        -   macOS x86\_64:`darwin/amd64`
        -   macOS arm64:`darwin/arm64`
        -   Windows x86\_64:`windows/amd64`
    -   示例(Linux x86\_64):
        
        ```
        curl -fL 'https://appstore.allroads.social/v1/social.wellesley.wlycli/latest?develop=stable&platform=linux/amd64' -o wlycli
        chmod +x wlycli
        ```
        
    -   预期的响应头包括:
        -   `x-artifact-display-name: Welley CLI`
        -   `x-artifact-full-name: social.wellesley.wlycli`
        -   `x-artifact-kind: cli`
        -   `x-artifact-platform: <os>/<arch>`,例如 `linux/amd64`
        -   `x-artifact-sha256: <sha256>`
        -   `x-artifact-version: <version>`
        -   `x-channel: develop`
    -   安装之前,最好先用 HEAD 请求检查响应头,确认文件名称、类型、平台和渠道与要下载的二进制文件一致。
-   验证 CLI 二进制文件:运行 `wlycli -v` 和 `wlycli deployment --help`,确认构建信息符合预期。

1.  **部署 Welley。**

-   带上必需的选项运行 `wlycli deployment create`。
-   使用单实例的默认设置(除非用户要求,否则不使用外部 Postgres)。
-   如果用户要求托管 Postgres,先创建它(DigitalOcean 数据库),再把返回的 host/port/db/user/password 填入 `--postgres-*` 参数。
-   如果 API/界面没有返回数据库用户的密码,请在运行 `wlycli` 之前重置该数据库用户的认证信息,获取新的凭据。
-   非交互式运行必须传入 `--email` / `-e`,否则命令会失败。
-   `--base-dir` 指向上级目录,CLI 会自动创建项目子目录。
-   如果项目目录或容器已经存在,CLI 会拒绝运行;重试之前必须先清理。

1.  **证书(必需)。**

-   默认:使用 Certbot 的 standalone 模式(非交互式)。
-   **在运行 certbot 之前,用户必须把域名(如果用到 `storage.<domain>`,也包括它)的 DNS A/AAAA 记录指向 Droplet 的 IP。**
-   CLI 支持 `--public-ip`(覆盖检测到的 IP),如果必须绕过检查,还有 `--skip-dns-preflight`。
-   **不要假设 DNS 已经配置好。请询问用户,并等待明确的确认。**
-   **除非用户明确要求,否则不要使用 `--skip-dns-preflight`。**
-   如果没有配置 CDN,certbot 的证书申请必须包含 `storage.<domain>`。
-   只有在用户明确要求时才使用自签名证书,并传入 `--self-signed-certs`(用 Go 生成自签名证书)。
-   向用户提供 DNS 设置说明,并等待确认:
-   “请创建 A/AAAA 记录,让 `<domain>` 解析到 Droplet 的 IP。”
-   “如果用到 `storage.<domain>`,也请为它创建 A/AAAA 记录。”
-   “DNS 生效后告诉我,我再继续。除非你让我验证解析结果,否则我看不到 DNS 是否已经生效。”
-   agent 也可以自己验证 DNS 是否生效:运行 `ping -c 1 <domain>`,并把得到的 IP 与 Droplet 的 IP 比对。如果一致,就继续。

7b. **邮件 DNS(SPF / DMARC / DKIM)。**

-   来自没有配置 SPF 和 DMARC 的域名的邮件,发给首次收件人时常常会被 Gmail 和 Outlook 丢弃,即使 SMTP 服务商显示已送达。
-   当 `--smtp-from` 的域名没有 SPF 记录,或者在 `_dmarc.<domain>` 或上级域名下没有 DMARC 记录时,`deployment create` 会打印一条 `Warning:`。它不会阻止部署。不要忽略这条警告:要把它展示给用户,并告诉用户需要添加哪些记录:
    -   `<mail-domain>` TXT `v=spf1 include:<provider> ~all`(具体的 include 由 SMTP 服务商提供;如果已有 SPF 记录,就合并进去,不要再添加第二条)
    -   `_dmarc.<mail-domain>` TXT `v=DMARC1; p=none; rua=mailto:dmarc@<mail-domain>`
    -   DKIM:域名验证时由 SMTP 服务商生成的 `<selector>._domainkey.<mail-domain>` 记录
-   用 `dig +short TXT <mail-domain>` 和 `dig +short TXT _dmarc.<mail-domain>` 检查。
-   CLI 无法检查 DKIM(selector 因服务商而异)。即使 `create` 没有打印警告,也一定要请用户确认 DKIM 已在 SMTP 服务商处验证通过。对于 Amazon SES 和类似的服务商,SPF 检查的是服务商自己的 envelope 域名,所以只有 DKIM 才能让 DMARC 通过。在 SES 中:_Verified identities > `<mail-domain>` > DKIM_ 必须显示为 _Successful_。

1.  **验证并汇报。**

-   检查 `docker ps` 和各服务的健康检查端点。
-   提供最终的连接信息和后续步骤。

## SSH 更新流程

当用户要求“更新服务器”时:

1.  询问目标主机/IP,以及 SSH 用户和密钥。
2.  运行 `wlycli deployment update --domain <domain> [--force-restart]`。
3.  检查容器和 Nginx 的状态。

## 资源

-   DigitalOcean API 代码片段和示例:`references/digitalocean.md`。

## 密钥对应的参数

密钥类信息请使用下面这些参数名:

-   SMTP host:`--smtp-host`
-   SMTP user:`--smtp-user`
-   SMTP password:`--smtp-password`
-   SMTP from:`--smtp-from`
-   OTEL endpoint:`--otel-endpoint`
-   OTEL headers:`--otel-headers`

## 最简创建命令

必需的参数:

-   `--domain`
-   `--email`
-   `--smtp-host`
-   `--smtp-user`
-   `--smtp-password`
-   `--smtp-from`

最简示例:

```
wlycli deployment create \
  --domain example.com \
  --email admin@example.com \
  --smtp-host smtp.example.com \
  --smtp-user user \
  --smtp-password pass \
  --smtp-from noreply@example.com
```

## 托管 Postgres 的默认值(DigitalOcean 与 CLI)

使用 DigitalOcean 托管 Postgres 时,不要依赖 CLI 的默认值。请明确设置所有 `--postgres-*` 参数:

-   DO 的典型默认值:
    -   端口:`25060`
    -   数据库:`defaultdb`
    -   用户:`doadmin`
-   CLI 的默认值(省略参数时):
    -   端口:`5432`
    -   数据库:`wlsly`
    -   用户:`wlsly`

## CLI 使用说明

-   检查版本:`wlycli -v` 或 `wlycli --version`(参数形式)。没有 `version` 子命令。
-   只有想使用非默认位置时才需要 `--base-dir`(默认是 `~/.welley`)。如果有权限,CLI 会自动创建这个目录。
-   通过 SSH 非交互式运行时,应带上 `--email` 和其他必需的参数,避免出现提示。
-   如果用户已经在服务器上放了 CLI 二进制文件(例如 `~/wcli`),就直接使用它。除非用户要求,否则不要移动它。
-   所有 DO API 调用都要**原样复制** `references/digitalocean.md` 中的代码片段。不要重写、改编或临时发挥 Python 解析部分,哪怕是很小的改动(例如转义、引号),在不同的 shell 里也可能出错。
-   **SSH 远程脚本:**始终使用 `ssh host 'bash -s' << 'EOF'`(带引号的 heredoc)。不要使用带双引号的 `ssh host "..."`,因为 zsh 会在发送到远程主机之前,先在本地展开 `$(...)` 和 `[...]`。

## 故障恢复

如果 `deployment create` 失败,需要重试:

-   使用 `wlycli deployment delete --domain <domain> --yes-please-delete-all-the-data` 清理。
-   delete 会尝试通过容器清理归 root 所有的文件。如果仍然失败,请用更高的权限删除 `--base-dir` 下的项目目录,然后重新运行。

## 端口 80 预检测试(可选)

如果 certbot 失败,重新运行之前先测试端口 80 是否可以访问:

```
docker run --rm -d -p 80:80 --name test-http nginx:alpine
curl -s http://<domain>:80
docker rm -f test-http
```

## 其他文档

-   [**Welley CLI** docs/index.md](https://welley.com/zh/administrators/docs/cli/)
-   [**快速开始：最小化 Welley 部署** docs/quick-start.md](https://welley.com/zh/administrators/docs/quick-start/)
-   [**openai.yaml** skills/deploy-server/agents/openai.yaml](https://welley.com/zh/administrators/docs/deploy-server-openai-yaml/)
-   [**DigitalOcean 参考** skills/deploy-server/references/digitalocean.md](https://welley.com/zh/administrators/docs/deploy-server-digitalocean/)
