name: publish-website description: 将当前会话中生成的 Web 项目发布为线上托管应用。负责环境信息获取、项目类型探测、前端构建(复用 deploy-website 的探测规则)、基于应用内容自动生成元数据、静态资源打包,并通过 multipart 表单 POST 到 showcase API 完成上传。支持纯静态、Node.js 前端、以及容器化后端项目。 arguments:
- name: workspace description: 待发布项目的绝对路径,默认使用当前工作目录 required: false
发布应用(Publish Website)
将纯前端项目或带后端的容器化项目发布为线上应用。本 Skill 以严格的流水线方式执行——不得跳过步骤,不得在未完成时声称成功。
流水线总览
- 获取环境信息(client_id)
- 询问是否包含后端服务(分流到 static / backend 子流水线)
- 探测项目类型(复用
deploy-website探测规则;后端覆盖 Node-Express、FastAPI、Django+gunicorn、Spring Boot jar、Go、Rust 等) - 准备产物:
- static 分支:必要时构建前端,准备静态产物
- backend 分支(步骤 3b):生成 Dockerfile、build、run、healthcheck、save 镜像
- 基于应用内容自动生成应用名称与描述,再向用户逐项确认;询问应用作者
- 打包为
/tmp/dist.zip(static 分支)或确认/tmp/showcase-image.tar.gz(backend 分支) - 确定
ticket(首次提交需询问用户是否复用既有应用) - 通过 multipart 表单 POST 到 showcase API,缓存返回的
ticket,向用户返回site_url
服务端注册与管理员审核发生在 Skill 执行结束之后,不属于本 Skill 的职责范围。
关于 ticket(应用更新密钥)
ticket是 showcase 服务用来识别"同一个应用"的凭证:首次创建会下发一个ticket,后续若想更新该应用而不是新建,需在请求体中带上同一个ticket- 会话级缓存:当前会话中首次成功创建应用后拿到的
ticket,必须缓存于会话上下文(例如记在内存/笔记中),同一会话内后续每次提交都自动使用该ticket,不得再向用户询问 - 仅当本会话从未提交过应用时,才需要询问用户是不是要复用其他任务中创建的应用(步骤 7a)
- 跨 kind 切换:同一
ticket可以从static切换为backend(或反之),服务端会将原应用整体替换为新 kind 并重新进入待审核状态;在用户确认时必须明确告知「将把原应用从 X 切换为 Y,并重新进入待审核状态」
步骤 1 —— 获取环境信息
执行 hostname 命令获取当前主机名,作为后续上传请求中的 client_id:
hostname
将输出的字符串原样记录为 client_id。不得自行编造或使用其他值;若命令失败,终止并向用户报告。
步骤 2 —— 询问是否包含后端服务
使用 question 工具单独提问一次。「包含(容器化部署)」选项必须显式列出无持久化存储这一硬约束——用户在做选择时就必须知情,避免后期才发现:
question: 该应用是否包含后端服务?
header: 后端服务
options:
- label: 不包含(纯前端)
description: 静态资源直接上传到 showcase,无服务端进程。
- label: 包含(容器化部署)
description: |
⚠ 容器无持久化存储。服务更新、异常重启或被运维重建容器时会重置文件系统,
所有运行时写入(SQLite/日志/用户上传/缓存)都会丢失。
仅适合无状态服务,或所有持久数据都外接到独立服务(远程 DB / 对象存储等)的场景。
- 用户选择 不包含(纯前端) → 进入 static 子流水线(步骤 3 → 4 → 5 → 6 → 7 → 8)
- 用户选择 包含(容器化部署) → 进入 backend 子流水线(步骤 3 → 3b → 5 → 7 → 8;步骤 4/6 由 3b 取代)
步骤 3 —— 探测项目类型
复用 deploy-website 中的探测逻辑。根据步骤 2 的选择分流:
static 分支(不包含后端)
| 探测结果 | 走向 |
|---|---|
存在 package.json(Node 项目) |
→ Node 构建分支(步骤 4 分支 B/C) |
仅有 index.html / 静态 HTML 文件 |
→ 静态分支(步骤 4 分支 A) |
| 探测到 PHP / Python / Go / Ruby / Java / Rust / Django / Rails | → 回复 检测到后端项目({lang}),如需发布请在步骤 2 选择"包含(容器化部署)"。 并终止 |
包管理器探测(仅 Node 项目)
优先级顺序:
pnpm-lock.yaml→pnpmyarn.lock→yarnpackage-lock.json→npm- 都没有 → 默认
npm
构建命令解析(仅 Node 项目)
按优先级:
package.json的scripts.build→<pkgMgr> run build- 已知框架默认产物目录——Vite/CRA/Astro →
dist,Next.js 静态导出 →out,react-scripts →build - README 兜底:扫描
README*中包含关键字build/compile/dist的命令并提取 - 以上都失败 → 询问用户指定构建命令,不要猜测
预期产物目录
记录预期的输出目录(dist / out / build),供步骤 4 使用。
backend 分支(包含后端)
探测项目语言/框架,至少覆盖以下场景:
| 探测特征 | 推断类型 |
|---|---|
package.json 中出现 express / fastify / koa / hapi |
Node-Express 系 |
requirements.txt / pyproject.toml 中含 fastapi / uvicorn |
FastAPI |
manage.py + requirements.txt 含 django + gunicorn |
Django + gunicorn |
顶层 pom.xml / build.gradle 且产物为 *.jar(Spring Boot) |
Spring Boot jar |
go.mod |
Go |
Cargo.toml |
Rust |
| 其他 | 询问用户基础镜像与启动命令,不要猜测 |
记录推断结果,供步骤 3b 生成 Dockerfile 使用。
步骤 3b —— backend 子流水线
仅当步骤 2 选择「包含(容器化部署)」时执行。完成后跳过步骤 4/6 直接进入步骤 5、7、8。
3b.0 平台限制(生成 Dockerfile 前必须遵守)
容器在 showcase 平台上没有持久化存储:
- 服务更新发布、容器异常崩溃、运维侧重启都会重建容器(旧实例销毁、新实例从镜像启动)
- 不挂载任何 volume / bind mount,对文件系统的所有写入在重建时丢失
- 不允许应用依赖本地文件状态延续(SQLite、文件型缓存、上传目录、日志归档、session 文件 …)
因此生成的应用与 Dockerfile 不得:
- 在 Dockerfile 里
VOLUME声明数据目录(声明无效,反而误导用户) - 把 SQLite / Bolt / BadgerDB 等嵌入式 DB 文件落在容器内
- 假设上次启动写入的文件下次还在
- 把用户上传或运行时生成的资源写到容器内路径
允许的状态外置方式:远程 DB(RDS、Supabase、PlanetScale…)、对象存储(S3 兼容)、外部缓存(远端 Redis)等。如果项目本身就是有状态的、不外接持久层,回到步骤 2,与用户确认后改回纯前端或终止本次发布。
3b.1 生成 Dockerfile
AI 基于步骤 3 的探测结果生成多阶段 alpine Dockerfile,写入 /tmp/Dockerfile。不得把 Dockerfile 写到用户工作目录。
硬性写法约束:
- 最终(runtime)stage 必须基于 alpine 或 alpine 风味的语言镜像(如
eclipse-temurin:21-alpine-jdk) - runtime stage 禁止
apt-get/dnf/yum - 禁止
ADD <url>—— 所有外部资源在 builder stage 用RUN curl/wget显式落盘 - 必须多阶段;runtime stage 只
COPY --from=builder编译产物 / 运行时依赖 CMD必须使用 exec 形式(JSON 数组),例如CMD ["node","server.js"]- 必须
EXPOSE <service_port>,且与后续 multipart 字段service_port一致 - 所有
FROM引用的 Docker Hub 镜像必须加registry.monkeycode-ai.online/代理前缀:- 无 namespace 的官方镜像(
alpine/node/python/golang/nginx/rust/caddy等)必须插入library/:FROM registry.monkeycode-ai.online/library/alpine:3.20 - 已有 namespace 的镜像(如
eclipse-temurin/...)不要再插library/:FROM registry.monkeycode-ai.online/eclipse-temurin:21-alpine-jdk FROM scratch不走代理,保留原样- 该前缀只在生成 Dockerfile 时注入;showcase 服务端 load 镜像后引用本地 image id,不再受代理影响
- 无 namespace 的官方镜像(
依赖下载镜像约定(builder stage 必须遵守)
构建环境默认在国内,直连官方源大概率超时。builder stage 凡是要下载依赖,必须先切到以下国内镜像,按语言对号入座:
| 生态 | 镜像 | Dockerfile 写法 |
|---|---|---|
| Go | goproxy.cn | ENV GOPROXY=https://goproxy.cn,direct |
| Node (npm/pnpm) | npmmirror.com | RUN npm config set registry https://registry.npmmirror.com(pnpm 同样读 npm 配置;yarn 用 yarn config set registry https://registry.npmmirror.com) |
| Python (pip) | 清华 TUNA PyPI | RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt |
| Rust (cargo) | 清华 TUNA crates.io | 见下方 config.toml 片段 |
| Java (Maven) | 阿里云 | settings.xml mirror 指到 https://maven.aliyun.com/repository/public,或 Gradle repositories { maven { url "https://maven.aliyun.com/repository/public" } } |
| Alpine apk | 清华 TUNA | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories |
| Debian/Ubuntu apt | 清华 TUNA | RUN sed -i 's@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources(老镜像没有 .sources 文件时改 /etc/apt/sources.list) |
Rust 的 cargo 镜像配置(builder stage 内):
RUN mkdir -p "${CARGO_HOME:-$HOME/.cargo}" && printf '%s\n' \
'[source.crates-io]' \
'replace-with = "tuna"' \
'' \
'[source.tuna]' \
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
> "${CARGO_HOME:-$HOME/.cargo}/config.toml"
注意:
- 镜像切换语句必须放在第一条依赖下载命令之前
- 这些配置只进 builder stage;runtime stage 本来就禁止装包,不需要
- 如果某个镜像站故障导致下载失败,回退到官方源重试一次再判定失败
3b.2 确认容器运行时(必须)
优先使用 docker;只有当 docker 不可用时,才回退到 podman。
用系统包管理器装任何东西之前,默认先把系统源切到清华 TUNA(构建环境在国内,直连官方源大概率超时;源已经是国内镜像时跳过):
if command -v docker >/dev/null 2>&1; then
RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then
RUNTIME=podman
else
# 都不在 PATH 中 → 通过包管理器安装 podman(不要尝试装 docker daemon)
# 安装前默认切到清华 TUNA 源
if command -v apt-get >/dev/null 2>&1; then
sudo sed -i 's@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources 2>/dev/null \
|| sudo sed -i 's@archive.ubuntu.com@mirrors.tuna.tsinghua.edu.cn@g; s@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list
sudo apt-get update && sudo apt-get install -y podman
elif command -v dnf >/dev/null 2>&1; then
# CentOS/Rocky/Alma:repo 文件注释 mirrorlist,baseurl 指向清华
sudo sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#\?baseurl=http[s]\?://[^/]*|baseurl=https://mirrors.tuna.tsinghua.edu.cn|g' \
-i /etc/yum.repos.d/*.repo 2>/dev/null || true
sudo dnf install -y podman
elif command -v yum >/dev/null 2>&1; then
sudo sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#\?baseurl=http[s]\?://[^/]*|baseurl=https://mirrors.tuna.tsinghua.edu.cn|g' \
-i /etc/yum.repos.d/*.repo 2>/dev/null || true
sudo yum install -y podman
elif command -v apk >/dev/null 2>&1; then
sudo sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
sudo apk add --no-cache podman
elif command -v pacman >/dev/null 2>&1; then
echo 'Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch' | sudo tee /etc/pacman.d/mirrorlist >/dev/null
sudo pacman -Sy --noconfirm podman
elif command -v brew >/dev/null 2>&1; then
brew install podman && podman machine init && podman machine start
else
echo "无可用的包管理器,无法安装容器运行时" >&2
exit 1
fi
RUNTIME=podman
fi
echo "using container runtime: $RUNTIME"
后续步骤一律使用 "$RUNTIME" 代替字面 docker,因 docker / podman CLI 在 build / run / save 路径上参数兼容(podman 是 rootless,第一次跑可能需要 podman system migrate 一次,遇到再处理)。
3b.3 本地 build
TAG="showcase-publish-$(openssl rand -hex 4):tmp"
"$RUNTIME" build -t "$TAG" -f /tmp/Dockerfile .
build 失败时:打印 stderr 末段(最多 200 行),立即终止,不得继续上传。
3b.4 本地 run + healthcheck
选择一个未占用的 host 端口(脚本探测,不要硬编码);$SVC 为生成 Dockerfile 时确定的容器内业务端口(service_port)。
"$RUNTIME" run -d --rm \
--cpus=1 --memory=1g --memory-swap=1g \
-p $HOST:$SVC \
--name "${TAG%:*}-run" \
"$TAG"
禁止 --privileged、--network host、build context 之外的 bind mount。
AI 根据应用类型选择 healthcheck path(如 /、/healthz、/api/health)与可接受状态码集合(默认 {200,204,302,401})。在 30s 内每 2s 探测一次:
for i in $(seq 1 15); do
code=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$HOST<healthcheck_path>" || echo 000)
case "$code" in
200|204|302|401) ok=1; break;;
esac
sleep 2
done
任一失败(容器启动失败 / healthcheck 30s 内未命中可接受状态码):
- 打印
"$RUNTIME" logs <container>末段(≤200 行) "$RUNTIME" stop <container>+"$RUNTIME" rmi $TAG+rm /tmp/Dockerfile- 立即终止,不得继续上传
3b.5 导出镜像
healthcheck 通过后:
"$RUNTIME" stop "${TAG%:*}-run"
# 服务端走 Docker daemon 加载,必须输出 docker-archive 格式;
# docker save 默认即此格式,podman save 必须显式指定 --format docker-archive。
if [ "$RUNTIME" = "podman" ]; then
"$RUNTIME" save --format docker-archive "$TAG" | gzip -1 > /tmp/showcase-image.tar.gz
else
"$RUNTIME" save "$TAG" | gzip -1 > /tmp/showcase-image.tar.gz
fi
强制自检:
size=$(stat -c%s /tmp/showcase-image.tar.gz)
test "$size" -le $((500*1024*1024)) || { echo "镜像超过 500MB"; exit 1; }
超过 500MB → 终止并提示用户精简产物(多阶段编译 + alpine + 仅拷贝必要文件)。
3b.6 准备 multipart 字段
记录后续步骤 8 需要的字段:
kind=backendsite_image=@/tmp/showcase-image.tar.gzservice_port=<容器内业务端口>healthcheck_path=<3b.4 中使用的 path>
后端分支不生成
/tmp/dist.zip、不携带site_zip_file。
3b.7 收尾(仅在步骤 8 成功 / 失败后均需执行)
"$RUNTIME" rmi "$TAG" 2>/dev/null || true
rm -f /tmp/Dockerfile
rm -f /tmp/showcase-image.tar.gz
步骤 4 —— 准备静态产物(仅 static 分支)
backend 分支跳过本节,直接进入步骤 5。
目标:得到一个顶层直接包含 index.html 的目录(记为 <artifact_dir>)。
打包前先清理旧产物:
rm -f /tmp/dist.zip
分支 A —— 纯静态 HTML 项目
直接使用项目根目录作为 <artifact_dir>。
分支 B —— 已有 dist 的 Node 项目
若预期产物目录存在且包含 index.html,则将其作为 <artifact_dir>。
分支 C —— 无 dist 的 Node 项目(需构建)
- 若
node_modules不存在,执行<pkgMgr> install。install 前先把 registry 切到 npmmirror(npm/pnpm:npm config set registry https://registry.npmmirror.com;yarn:yarn config set registry https://registry.npmmirror.com),直连 registry.npmjs.org 在国内大概率超时。失败时输出 stderr 尾部并终止。 - 执行解析出的构建命令。失败时输出 stderr 尾部并终止,不得盲目重试。
- 定位
index.html:- 先在预期产物目录内查找
- 找不到则兜底:
find . -maxdepth 3 -name index.html -not -path './node_modules/*' - 多个候选时取路径最短者
- 仍未找到 → 终止并报告
构建完成但未找到 index.html
步骤 5 —— 生成并确认应用元数据
先自动生成,再逐项询问用户。每个字段单独发起一次 question 工具调用,不得把多个字段合并到一个问题里。
5a. 基于应用内容自动生成 site_name 与 site_description
- static 分支:从
<artifact_dir>/index.html的<title>/<meta name="description">/<h1>/ 首屏正文综合生成;Node 项目可参考根package.json的name与description - backend 分支:从项目根
package.json/pyproject.toml/pom.xml/Cargo.toml/go.mod中的 name + description 综合生成;若有 README,提取首段简介
输出:
- 自动生成的
site_name(一句话短标题,<= 30 字) - 自动生成的
site_description(一句话简介,<= 80 字)
若无可解析内容,则在后续询问中不要给出"满意"选项的默认值,让用户必须自行输入。
5b. 询问应用名称(question 工具,独立一次调用)
question: 自动识别到的应用名称为「<生成的 site_name>」,是否使用?
header: 应用名称
options:
- 满意,就用这个
- 用户选 满意,就用这个 → 采用自动生成的值
- 用户走 Other 自行输入 → 采用其输入
5c. 询问应用描述(question 工具,独立一次调用)
question: 自动识别到的应用描述为「<生成的 site_description>」,是否使用?
header: 应用描述
options:
- 满意,就用这个
- 处理逻辑同 5b。
5d. 询问应用作者(question 工具,独立一次调用)
question: 请输入应用作者的 ID(可在下方选项中选择或自行输入)
header: 应用作者
options:
- 匿名作者
- 用户选 匿名作者 →
site_author = "anonymous" - 用户走 Other 自行输入 → 采用其输入
步骤 6 —— 打包(仅 static 分支)
backend 分支跳过本节;产物已在 3b.4 准备完毕。
必须先 cd 进入 <artifact_dir> 再打包,确保 zip 内没有包裹目录;同时排除开发相关文件:
cd <artifact_dir> && zip -r /tmp/dist.zip . \
-x ".git/*" \
-x ".git" \
-x "node_modules/*" \
-x "node_modules" \
-x "src/*" \
-x ".env*" \
-x "*.log" \
-x ".DS_Store" \
-x ".vscode/*" \
-x ".idea/*" \
-x "package.json" \
-x "package-lock.json" \
-x "pnpm-lock.yaml" \
-x "yarn.lock" \
-x "tsconfig*.json" \
-x "*.ts" \
-x "*.tsx" \
-x "vite.config.*" \
-x "webpack.config.*" \
-x "next.config.*"
说明:
- 对于分支 B/C(产物目录在
dist/out/build内),目录本身已是构建后的静态资源,大多数排除项不会命中,但保留排除规则作为防御性兜底- 对于分支 A(产物目录就是项目根),上述排除项可有效避免把源码、依赖、版本控制目录、配置文件打入包
- 如果
<artifact_dir>中确有需要的.ts/.tsx资源(极少见),需调整排除项;否则保持上述默认
强制自检:
unzip -l /tmp/dist.zip | head -30
必须满足:
index.html位于顶层(无任何路径前缀)- 输出中不应出现
.git/、node_modules/、src/、package.json等开发文件
任一不满足则立即终止——不得上传不合规的包。
步骤 7 —— 确定 ticket (即 密钥)
判断当前会话是否已有缓存的密钥 (ticket):
- 已有缓存的密钥(即本会话此前已成功提交过应用) → 直接复用,跳过 7a,进入步骤 8
- 没有缓存的密钥(本会话首次提交) → 进入 7a 询问用户
7a. 询问是否复用既有应用(首次提交时执行一次)
使用 question 工具,只提供一个显式备选项;剩下的 Other 输入框本身就代表"有,输入密钥更新已有应用"——其 placeholder 即为该文案:
question: 之前在其他任务中提交过本应用吗?是需要更新已有应用,还是提交新应用?如果需要更新,请选择【其他】并填入之前任务提供的密钥。
header: 是否更新现有应用?
options:
- 没有,提交新应用
# Other: 输入框的 placeholder/语义为"有,输入密钥即可更新现有应用",用户在此处直接填密钥
- 用户选择 没有,提交新应用 → 密钥留空(不携带
ticket字段) - 用户在 Other 输入框中填入密钥 → 取用户输入的字符串作为
ticket - 若用户输入的内容为空字符串或纯空格 → 视为未提供,按"提交新应用"处理
跨 kind 提示:若用户提供了 ticket 且本次 kind 与他记忆中的原应用 kind 不一致(无法在 client 侧自动判定,按用户口述),必须在提交前提示「将把原应用从 X 切换为 Y,并重新进入待审核状态」并取得确认;服务端会在切换时整体替换原应用。
步骤 8 —— 发布
通过 multipart 表单 POST 一次性提交所有字段与产物到 showcase API。
8a. 调用 API
static 分支(不带 ticket / 带 ticket 二选一):
curl -f -X POST \
-F "client_id=<client_id>" \
-F "kind=static" \
-F "site_name=<应用名称>" \
-F "site_author=<应用作者>" \
-F "site_description=<应用描述>" \
[ -F "ticket=<ticket>" ] \
-F "site_zip_file=@/tmp/dist.zip" \
https://ugc-submit.showcase.monkeycode-ai.online/v1/create
backend 分支:
curl -f -X POST \
-F "client_id=<client_id>" \
-F "kind=backend" \
-F "site_name=<应用名称>" \
-F "site_author=<应用作者>" \
-F "site_description=<应用描述>" \
[ -F "ticket=<ticket>" ] \
-F "site_image=@/tmp/showcase-image.tar.gz" \
-F "service_port=<容器内业务端口>" \
-F "healthcheck_path=<healthcheck path>" \
https://ugc-submit.showcase.monkeycode-ai.online/v1/create
字段说明:
| 字段 | static | backend | 来源 |
|---|---|---|---|
client_id |
必填 | 必填 | 步骤 1 中 hostname 命令的输出 |
kind |
必填(static) |
必填(backend) |
步骤 2 的选择 |
site_name |
必填 | 必填 | 步骤 5b |
site_author |
必填 | 必填 | 步骤 5d |
site_description |
必填 | 必填 | 步骤 5c |
site_zip_file |
必填 | 不得出现 | 步骤 6 |
site_image |
不得出现 | 必填 | 步骤 3b.4 |
service_port |
不得出现 | 必填 | 步骤 3b |
healthcheck_path |
不得出现 | 选填(默认 /) |
步骤 3b.3 |
ticket |
选填 | 选填 | 步骤 7 |
要点:
-f让 HTTP 非 2xx 状态返回非零退出码- 失败最多重试 1 次(应对网络抖动)
- 所有字段都需经过 shell 转义
- 不得额外传入
user_id/task_id等字段 - 不得混用 kind 与产物字段(如
kind=static同时携带site_image)
8b. 解析响应
服务端响应结构:
{
"status": 200,
"data": {
"message": "success or error detail",
"site_url": "https://xxxxx.showcase.monkeycode-ai.online",
"ticket": "<会话内复用此 ticket>"
}
}
处理规则:
status为 2xx 且data.site_url非空 → 视为成功,提取site_url- 若
data.ticket非空:将其缓存到当前会话上下文,本会话后续每次提交都自动使用该ticket,不得再次询问用户 - 若
data.ticket与请求中携带的ticket不一致:必须在最终反馈中明确告知用户新的ticket值 - 其他情况 → 视为失败,将
data.message作为错误原因向用户报告,不得伪装成功
8c. 向用户反馈
成功时(site_url 必须单独一行作为可点击链接渲染,不得包在代码块里;不得提示绑定微信或公众号):
应用已提交发布,预览地址:
<site_url>
应用上线前需经过管理员审核。如果想了解审核状态,可在这里询问,我会查询并告诉你。
若服务端返回的 data.ticket 与请求中携带的 ticket 不同:
应用已提交发布,预览地址:
<site_url>
本应用的更新凭证 ticket 为:`<new_ticket>`
后续如需继续更新本应用,请在新会话中向我提供此 ticket。
应用上线前需经过管理员审核。如果想了解审核状态,可在这里询问,我会查询并告诉你。
失败时报告 HTTP 状态码与 data.message,不得编造应用地址。
查询审核状态(用户主动询问时执行)
用户在本会话内询问审核 / 上线 / 拒绝原因 / 下线原因等问题时,调用:
GET https://ugc-submit.showcase.monkeycode-ai.online/v1/status?client_id=<client_id>&ticket=<ticket>
client_id:步骤 1hostname拿到的值;必须与提交时一致ticket:会话内已缓存的ticket
服务端用 ticket 找 site,再校验 client_id 与该 site 匹配;两者其中之一对不上即返回 404 site_not_found。
响应字段
成功时返回 { code: 0, data: {...} },data 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
slug |
string | 应用 slug |
status |
string | pending_review / online / offline / rejected |
kind |
string | static / backend |
block_resubmit |
bool | 当为 true 时同 client_id 已被禁止再次提交,再调 /v1/create 会得到 403 resubmit_blocked |
takedown_reason |
string(可选) | 管理员在拒绝 / 下线时填写的原因;status 为 rejected 或 offline 时一般存在 |
last_deployed_at |
int64(可选) | 上次部署的毫秒时间戳 |
向用户反馈的话术
按 status 分四类:
pending_review→ "应用已提交,审核中。"online→ "审核已通过,应用已上线,访问地址:<site_url>"rejected→ "审核未通过。" + 如有takedown_reason加上 "原因:<takedown_reason>"。如果block_resubmit=true,告知用户「管理员已禁止当前 client_id 再次提交本应用」offline→ "应用已下线。" + 如有takedown_reason加上 "原因:<takedown_reason>"
禁止:
- 不得主动轮询本接口;只有用户提问时才调用
- 不得猜测原因;
takedown_reason为空时只能说"管理员未填写原因" - 不得在反馈中暴露
slug以外的 raw JSON
硬性规则(必须遵守)
通用
- 使用系统包管理器(apt/yum/dnf/apk/pacman)安装任何软件前,默认先把系统源切到清华 TUNA(
mirrors.tuna.tsinghua.edu.cn),不要等超时了再换 - 不得自行编造
client_id:必须来自hostname命令的真实输出 ticket仅在本会话首次提交时询问用户;首次提交成功拿到的ticket必须缓存到会话上下文,后续提交自动复用,不得反复询问- 不得自行编造
ticket:要么来自用户输入,要么来自服务端返回 - 应用名称/描述的自动生成必须基于真实应用内容,不得凭空捏造;用户提供的输入优先级最高
- 应用元数据三项必须分三次
question工具询问,不得合并 - 不得在请求中传
user_id/task_id - 不得伪装成功:任一步失败必须如实报告
- 不得轮询审核状态:Skill 在上传后即结束
- 不得在最终反馈中提示绑定微信或公众号
- 不得在最终反馈中把
site_url包入代码块 - 服务端返回的
data.ticket与请求携带的ticket不一致时,必须在最终反馈中显式告知用户新的ticket - 必须按
status与data.site_url同时判定成功 - 跨 kind 切换不得在 client 侧报错;必须提示「将把原应用从 X 切换为 Y,并重新进入待审核状态」
static 分支专用
- 打包前必须
rm -f /tmp/dist.zip - zip 根目录必须是
index.html且不得包含.git、node_modules、src、package.json等开发文件,通过unzip -l验证
backend 分支专用(Dockerfile + 容器化)
- 容器无持久化存储——服务更新、异常崩溃、运维重启都会重建容器,文件系统所有写入都会丢失:
- Dockerfile 不得
VOLUME数据目录 - 镜像内不得依赖 SQLite / Bolt / BadgerDB 等嵌入式 DB 落地
- 应用不得假设上次启动写入的文件下次还在
- 用户上传 / 运行时生成的资源必须外接(远程 DB、对象存储等)
- 进入 backend 分支前必须已在步骤 2 的
questiondescription 里向用户说明该限制
- Dockerfile 不得
- 最终(runtime)stage 必须基于 alpine(或 alpine 风味的语言镜像,如
eclipse-temurin:21-alpine-jdk) - runtime stage 禁止
apt-get/dnf/yum - 禁止
ADD <url> - 必须多阶段;runtime stage 只 COPY 产物,不得编译
CMD必须为 exec 形式(JSON 数组)- 必须
EXPOSE与 multipart 字段service_port一致的端口 - Skill 不得把 Dockerfile 写到用户工作目录;必须写到
/tmp/Dockerfile - 优先使用
docker,缺失时必须用包管理器安装podman后继续,绝不可手动安装 docker engine;所有 build / run / save 命令统一以"$RUNTIME"引用 - 本地
"$RUNTIME" run禁止--privileged、--network host、build context 之外的 bind mount - build / run / healthcheck 任一失败必须 abort 并打印 stderr 末段,禁止继续上传
- 镜像 tar.gz 必须 ≤ 500MB
- 收尾必须执行
"$RUNTIME" rmi <tag>、rm /tmp/Dockerfile、rm /tmp/showcase-image.tar.gz(无论成功失败) - 所有
FROM引用的 Docker Hub 镜像必须加registry.monkeycode-ai.online/代理前缀:- 无 namespace 的官方镜像必须插入
library/(如registry.monkeycode-ai.online/library/alpine:3.20、registry.monkeycode-ai.online/library/node:20-alpine) - 已有 namespace 的镜像不要再插
library/(如registry.monkeycode-ai.online/eclipse-temurin:21-alpine-jdk) FROM scratch不走代理
- 无 namespace 的官方镜像必须插入
- builder stage 依赖下载必须走国内镜像(见 3b.1「依赖下载镜像约定」):Go→goproxy.cn、npm/pnpm/yarn→npmmirror.com、pip→清华 PyPI、cargo→清华 crates.io、Maven/Gradle→阿里云、apk/apt/yum→清华 TUNA;镜像切换语句必须在第一条依赖下载命令之前
错误处理速查
| 失败点 | 处理动作 |
|---|---|
hostname 命令失败 |
报告错误并终止 |
| 找不到项目根 | 询问用户路径,不得猜测 |
| static 分支探测到后端语言 | 提示用户改回步骤 2 选「包含」并终止本次 |
| 构建命令无法解析 | 询问用户指定命令 |
install 失败 |
输出 stderr 尾部并终止 |
build 失败(前端 / "$RUNTIME" build) |
输出 stderr 尾部并终止 |
既无 docker 又无 podman,且包管理器不可用 |
报告"无可用容器运行时"并终止 |
构建后找不到 index.html |
输出目录结构并终止 |
| 应用内容无任何可用元数据 | 自动生成留空,由用户在 question 工具的 Other 中输入 |
| zip 自检发现开发文件混入 | 调整排除项重新打包;仍存在则终止 |
| 镜像 tar.gz > 500MB | 提示精简产物(多阶段编译 + alpine + 仅拷贝必要文件)并终止 |
"$RUNTIME" run 启动失败 |
打印 "$RUNTIME" logs 末段,清理容器/镜像/Dockerfile,终止 |
| healthcheck 30s 内未命中可接受状态码 | 打印 "$RUNTIME" logs 末段,清理,终止 |
| API 请求非 2xx | 重试 1 次;仍失败则报告 status 与 data.message 后终止 |
data.site_url 为空 |
视为失败,报告 data.message 后终止 |
用户输入的 ticket 实际无效(API 返回错误) |
报告 data.message 后终止;不得自动转为新建应用 |
data.ticket 缺失(旧版服务端) |
仍按成功处理,但本会话后续提交无法走更新流程 |
API 返回 kind_mismatch |
提示「上传字段与所选类型不符,本次发布已取消」并终止 |
API 返回 image_too_large |
提示「镜像超过 500MB,请精简产物(多阶段编译 + alpine + 仅拷贝必要文件)」并终止 |
API 返回 image_invalid |
提示「镜像 tar 校验失败,请确认 "$RUNTIME" save 流程未中断;如使用 podman 须显式 --format docker-archive」并终止 |
API 返回 container_start_failed |
透传 data.detail(≤200 字)并终止 |
API 返回 healthcheck_failed |
提示「服务端启动后 healthcheck 失败:"$RUNTIME" run + curl 自查」并终止 |