发布到 App Platform
流程:在沙盒中构建并测试 → 使其可部署 → 提出计划并获得同意 → 推送到 GitHub → 创建 App Platform 应用 → 等待 ACTIVE → 检查 URL。
在用户批准步骤 2 中的计划之前,切勿创建仓库、推送或创建/更新/删除应用。 像“发布它”这样的请求并不构成批准。在沙盒中构建和测试无需批准。
绝不要使用 apps 操作的 publish、docker 或 doctl——它们在此计算机上均不能工作。所有操作都通过两个共享凭据进行,你通过核心的凭据代理调用它们。你永远不会看到令牌。
每条命令都要使用一个明确的 sandbox_id,并且对于任何调用代理(digitalocean、github 或 github-git)的命令,请将凭据放在 execute.credentials 中。核心系统仅向这些命令提供 AGENT_CREDENTIAL_TOKEN。不要使用 rm -rf;改用 mktemp -d 创建一个新目录。
计算机环境
Node 24 + npm、Python 3.12 + pip、git、gh(未登录;不要使用它)、curl、gcc、make。没有 docker、doctl、jq、wget、unzip。使用 node -e 或 python3 -c 解析 JSON。
凭据(每种凭据能做什么)
| 凭据 | 主机 | 允许的操作 |
|---|---|---|
digitalocean | api.digitalocean.com | GET/POST/PUT/DELETE 到 /v2/apps 和 /v2/regions |
github | api.github.com | GET/POST/PATCH 到 /user 和 /repos/jnaidu-do |
github-git | github.com (git push/clone) | GET/POST 到 /jnaidu-do/ |
所有内容都在共享的 GitHub 账户 jnaidu-do 下创建(这是一个跨部署的凭据,不是用户自己的账户)。在计划中要说明这一点。如果用户想要自己的账户,请告知他们尚未设置。如果收到 403 path_not_allowed / method_not_allowed,意味着请求超出了此范围——不要尝试变通;告知用户此操作不被允许。
Broker 辅助函数
每条命令粘贴一次(shell 状态不会持久化)。它将 HTTP 状态输出到 stderr,响应体输出到 stdout:
bk() { # bk <凭据> <方法> <url> [json-主体]
p=$(node -e 'const [c,m,u,b]=process.argv.slice(1);console.log(JSON.stringify({credential:c,method:m,url:u,headers:{accept:"application/json","content-type":"application/json"},...(b?{body:b}:{})}))' "$1" "$2" "$3" "${4:-}")
curl -fsS -X POST "$AGENT_API_URL/v1/credentials/broker" -H "x-agent-capability: $AGENT_CREDENTIAL_TOKEN" \
-H "content-type: application/json" -d "$p" |
node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.error("HTTP "+r.status);console.log(r.body)})'
}
1. 使其可部署(推送前检查)
应用将作为普通容器在 App Platform 中运行,而非此沙盒环境。
- 监听
$PORT,绑定到0.0.0.0(App Platform 会设置PORT;默认为 8080)。切勿硬编码其他端口。 - 纯 HTML/CSS/JS 或构建为静态文件的框架(Vite、CRA、Astro):部署为 静态站点(
build_command、output_dir)。对于静态页面,优先选择此方式而非运行服务器。 - 任何带服务器的应用:一个 服务,包含
build_command、run_command、http_port。标准布局无需配置即可工作:Node 需要带有start脚本的package.json;Python 需要requirements.txt和一个运行命令(例如gunicorn app:app --bind 0.0.0.0:$PORT)。仓库中的Dockerfile也可工作(设置dockerfile_path),但你不能在此处测试构建它,所以请保持简单。 - 无持久磁盘。 写入文件系统的任何数据在每次部署后都会丢失。不要使用 SQLite 文件、上传到磁盘的文件或 JSON 文件来存储状态。如果应用需要数据库,请添加托管的 Postgres(见下文);否则保持无状态。
- 先在本地运行:
npm install,用PORT=8080启动,然后curl localhost:8080。仅发布能工作的内容。 - 添加包含
node_modules等的.gitignore。不要提交密钥。切勿打印令牌。
如果应用使用 Postgres
App Platform 的 ${db.DATABASE_URL} 包含 sslmode=require,而 Node pg 会拒绝托管的 CA(SELF_SIGNED_CERT_IN_CHAIN)。从 URL 中移除 sslmode 并且 设置 ssl: { rejectUnauthorized: false }:
function normalizeDatabaseUrl(raw) {
const u = new URL(raw);
u.searchParams.delete("sslmode"); u.searchParams.delete("ssl");
return u.toString();
}
const pool = new Pool({ connectionString: normalizeDatabaseUrl(process.env.DATABASE_URL), ssl: { rejectUnauthorized: false } });
在规范中添加 "databases": [{"name":"db","engine":"PG","production":false,"version":"16"}],并在每个需要它的组件中添加 {"key":"DATABASE_URL","scope":"RUN_TIME","value":"${db.DATABASE_URL}"}。首次连接重试约 30 秒:数据库可能仍在启动。仅当应用确实需要数据库时才添加(这会产生额外费用)。
2. 提出计划并征询(在此处停止,直到他们同意)
一旦应用构建并本地运行成功,停下来并向用户发送简短计划,然后等待回复。 在用户批准之前,不要执行任何 bk 调用、git push 或仓库/应用创建。包括:
- 你构建的内容 以及它已在本地运行(一行)。
- 你将采取的步骤,按顺序排列,每一步说明其涉及的内容:
- 创建 GitHub 仓库
jnaidu-do/<名称>— 公开,位于共享的jnaidu-do账户下(不是用户自己的账户) - 使用 git 将应用文件推送到该仓库(列出包含的内容;确认没有密钥)
- 在
nyc区域创建 App Platform 应用<名称>:静态站点(少量免费)或服务(basic-xxs,约 $5/月)——说明是哪一种及费用 - 等待构建(3-5 分钟),然后提供公共 URL
- 创建 GitHub 仓库
- 哪些内容变为公开: 仓库和实时站点对互联网上的任何人都可读。
- 未经再次询问你不会做什么: 推送进一步更改、编辑规范或删除任何内容。
以一个直接的问题结尾,例如:“我应该按照上述步骤创建仓库和应用吗?您也可以更改名称、使仓库私有(这需要授予 DO GitHub App 权限)或拒绝。” 如果适用,提供备选方案(静态 vs 服务、不同名称)。如果他们拒绝,交给他们文件或已准备就绪可提交的代码,然后停止。
在以下情况之前,单独再次询问:任何后续的 git push(更新)、更改应用规范、以及删除应用。
3. 推送到 GitHub
使用你在步骤 2 中提议的名称(qm-<短横线连接的名>)(小写)。如果名称已被占用(422),则添加一个短后缀。
# 创建仓库(公开:App Platform 无需额外认证即可克隆)
bk github POST https://api.github.com/user/repos '{"name":"qm-hello-site","private":false}'
# 从一个包含应用的全新目录推送(没有 node_modules)
cd "$(mktemp -d)" && cp -r /path/to/app/. . && git init -q -b main && git add . &&
git -c user.name=QM -c [email protected] commit -qm "Initial commit" &&
git remote add origin "$AGENT_API_URL/v1/credentials/git/github-git/jnaidu-do/qm-hello-site.git" &&
git -c http.extraHeader="x-agent-capability: $AGENT_CREDENTIAL_TOKEN" push origin HEAD:refs/heads/main
后续更新:以相同方式提交并推送(HEAD:refs/heads/main),但仅在你告知用户更改内容并获得同意之后——每次推送都需要一个新的批准,不包含在最初的计划中。
4. 创建 App Platform 应用
bk digitalocean POST https://api.digitalocean.com/v2/apps '{"spec": {
"name": "qm-hello-site",
"region": "nyc",
"services": [{
"name": "web",
"github": {"repo": "jnaidu-do/qm-hello-site", "branch": "main", "deploy_on_push": true},
"build_command": "npm install",
"run_command": "npm start",
"http_port": 8080,
"instance_size_slug": "basic-xxs",
"instance_count": 1
}]
}}'
静态站点替代 services:"static_sites": [{"name":"web","github":{...},"build_command":"npm run build","output_dir":"dist"}](纯 HTML:省略 build_command,output_dir 为 "/";对于单页应用,添加 "catchall_document":"index.html")。
响应中包含应用 id。请保留它。应用名称只能包含小写字母、数字、连字符,长度为 2-32 个字符。
如果创建失败并出现 GitHub 仓库访问错误(“does not have access to the repository”):DigitalOcean GitHub App 未被授予此仓库的权限。告知用户打开 https://github.com/apps/digitalocean/installations/select_target 并授予对 jnaidu-do 仓库的访问权限,然后告知完成;重新尝试相同的请求。如果他们无法做到,回退到通用的公共克隆源("git": {"repo_clone_url": "https://github.com/jnaidu-do/qm-hello-site.git", "branch": "main"})并告知他们后续更新需要重新部署(见下文)。
5. 等待 ACTIVE
真实构建需要 3-5 分钟。每 30 秒轮询一次,并告知用户你正在等待:
bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID/deployments?per_page=1" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const d=JSON.parse(s).deployments[0];console.log(d.id,d.phase,d.progress?.success_steps+"/"+d.progress?.total_steps)})'
阶段:PENDING_BUILD → BUILDING → DEPLOYING → ACTIVE。ERROR 或 CANCELED 表示失败;转到“失败时”。在大约 12 分钟后停止轮询,并告知用户当前状态。
6. 报告 URL
bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).app.live_url))'
尝试 curl -s -o /dev/null -w '%{http_code}' <live_url> — 期望得到 200。此计算机通常无法访问新的 *.ondigitalocean.app 站点(连接失败)。如果 curl 失败但部署状态是 ACTIVE,那不是部署失败:告知用户你无法从此处验证,请用户打开该 URL。将完整的 https://…ondigitalocean.app URL 和仓库 URL 提供给用户。应用在互联网上是 公开 的。
失败时
自行读取日志;不要向用户索要。获取日志 URL,然后获取日志:
bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID/deployments/$DEPLOYMENT_ID/logs?type=BUILD"
# 响应体包含 "live_url"(或 "historic_urls");curl 该 URL(无需认证)
type 是 BUILD、DEPLOY 或 RUN(静态站点只有 BUILD)。匹配信号:
| 信号 | 修复方法 |
|---|---|
Missing script: start / 找不到命令 | build_command / run_command 错误,或者没有 start 脚本 |
| 容器退出,健康检查失败 | 未监听 $PORT / 未绑定到 0.0.0.0,或者启动时崩溃 |
SELF_SIGNED_CERT_IN_CHAIN | 缺少 Postgres SSL 辅助函数(见上文) |
Cannot find module | 依赖项在 package.json 中缺失,或提交的 node_modules 不匹配 |
| Vite "Blocked request. This host is not allowed" | 设置 preview: { allowedHosts: true }(或将构建的 dist 作为静态站点提供服务) |
| 构建 "skipped" / 几秒内进入 ACTIVE | 源代码未更改;推送一个真正的提交 |
告知用户根本原因(确切的日志行)和你提议的修复,在推送修复或更改规范前需要征询——每次推送和每次规范更改都需要单独的批准。用户批准后,修复代码、提交并推送(deploy_on_push 会自动重新部署),然后再次轮询。对于规范更改,发送 PUT /v2/apps/$APP_ID 并携带 {"spec": {...}}(首先使用 GET /v2/apps/$APP_ID 获取当前规范并编辑它)。对于通用的 git 源,使用 POST /v2/apps/$APP_ID/deployments 并携带 {"force_build":true} 来强制重新构建。不要通过删除和重新创建应用来修复失败——这会改变 URL。向前修复。在三次不同的失败尝试后放弃,并报告你已经尝试过的内容。读取日志无需批准。
删除
仅在用户明确要求时,并且先说出应用名称。DELETE /v2/apps/$APP_ID 删除应用。GitHub 仓库会被保留(此凭据无法删除仓库);请告知用户。
报告
结束时,提供:实时 URL、仓库 URL,以及一行关于如何更新的说明(“请求我更改 X,我将推送它”)。