- name
- publish-website
- description
- 将当前会话中生成的 Web 项目发布为线上托管应用。负责环境信息获取、项目类型探测、前端构建(复用 deploy-website 的探测规则)、基于应用内容自动生成元数据、静态资源打包,并通过 multipart 表单 POST 到 showcase API 完成上传。支持纯静态、Node.js 前端、以及容器化后端项目。
- arguments
- [{"name":"workspace","description":"待发布项目的绝对路径,默认使用当前工作目录","required":false}]
# 发布应用(Publish Website)
将纯前端项目或带后端的容器化项目发布为线上应用。本 Skill 以严格的流水线方式执行——**不得跳过步骤**,**不得在未完成时声称成功**。
## 触发准入(本 Skill 只服务于「用户主动要求的正式发布」)
`publish-website` 是**面向公开作品集的正式发布通道**,不是开发调试期的预览工具。每次执行都会占用管理员审核资源并让站点公开曝光,因此**必须**由用户在**最新一条消息**中**明确要求**才可触发;未明确要求时一律改走 `deploy-website` 本地部署 + 平台在线预览。
### 允许触发的条件(必须全部满足)
1. 用户在**最新一条消息**中明确提出「发布 / 上线 / publish-website / 用 publish-website 技能发布」等语义等价的指令,且明确指向本 Skill
2. 该指令**不是**由 Skill 或模型自身在对话中提出的建议/追问所诱导(例如模型不能问「要发布吗?」再据此触发)
### 严禁的触发方式
- **不得**因为「本会话已经 publish 过一次」就在后续任意消息里自动重新 publish;即便用户只是继续调整代码、修 bug、改文案、优化样式,只要最新消息里没有再次明确要求发布,就**不得**重新走 publish 流程
- **不得**把「代码有更新」「站点内容有变化」「已经发布过所以顺手再发一次」当作重新触发本 Skill 的理由
- **不得**在完成一次成功发布后主动追问「要不要再发布一次 / 要不要把最新改动同步上去」——这会诱导用户误触发正式发布
- 用户在开发过程中的所有**中间版本验证**都应通过 `deploy-website` 本地部署 + 平台在线预览完成,本 Skill 不负责中间版本的展示
### 命中「重复触发」时的处理动作
当模型判断「本会话此前已成功执行过 publish-website,用户最新消息只是继续调整而未明确要求再次发布」时,**不得**进入流水线,应改为:
1. 在回复中提示:「本次调整属于中间版本迭代,建议先通过 `/deploy-website` 在平台内做在线预览确认效果;确认满意后再明确告诉我『用 publish-website 发布最新版本』,我再走正式发布流程更新线上版本」
2. 按需调用 `/deploy-website` 完成本地部署与预览
3. 结束,不得继续执行本 Skill 的任何后续步骤
## 流水线总览
1. 获取环境信息(client_id)
2. 探测项目类型并自动判定 kind(static / backend)(复用 `deploy-website` 探测规则;后端覆盖 Node-Express、FastAPI、Django+gunicorn、Spring Boot jar、Go、Rust 等)
3. 根据判定结果分流到 static / backend 子流水线
4. 准备产物:
- static 分支:必要时构建前端,准备静态产物
- backend 分支(步骤 3b):生成 Dockerfile、build、run、healthcheck、save 镜像
5. 基于应用内容自动生成应用名称与描述,再向用户逐项确认;询问应用作者
6. 打包为 `/tmp/dist.zip`(static 分支)或确认 `/tmp/showcase-image.tar.gz`(backend 分支)
7. 确定 `ticket`(首次提交需询问用户是否复用既有应用)
8. 通过 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`:
```bash
hostname
```
将输出的字符串原样记录为 `client_id`。**不得**自行编造或使用其他值;若命令失败,终止并向用户报告。
---
## 步骤 1b —— 发布内容合规性预检(硬约束)
在进入 kind 判定之前,必须先判断当前工作目录是否属于以下**禁止发布**的站点类型。命中任意一条 → **立即终止**发布,向用户说明原因并建议改用其他渠道分发;**不得**继续后续任何步骤。
### 禁止发布的站点类型清单
1. **软件下载/分发类站点**:以提供 `.apk` / `.ipa` / `.exe` / `.dmg` / `.msi` / `.pkg` 等可执行安装包下载为主要目的的站点
- 判定信号:站点目录下存在上述扩展名的二进制文件、页面文案主打「下载」「安装包」「客户端下载」「Download APK / IPA / EXE / DMG」等
- 理由:showcase 是 Web 应用作品集,不承担软件分发/托管职责,也无法承担分发链路上的安全与合规责任
2. **直接发布开源 CMS / 网站面板类项目**:未做任何二次开发、直接把开源建站系统或运维面板搬上来的
- 常见特征(含但不限于):WordPress、Joomla、Drupal、Typecho、Ghost、Halo、DedeCMS、帝国 CMS、织梦、PHPMyAdmin、宝塔面板、1Panel、cPanel、Plesk、Webmin 等
- 判定信号:目录结构、`readme` / `LICENSE` / `composer.json` / `package.json` 中出现上述项目名,或首页明显是这些系统的默认后台/安装向导
- 理由:这类项目本身就是通用平台,直接发布不构成"用户作品",且往往自带账号体系、文件上传、插件市场,与「单容器 / 无外网 / 无持久化 / 公开可见」的运行约束严重冲突
### 处理动作
命中上述任一条时,向用户明确回复:
> 检测到当前项目属于「软件下载分发 / 开源 CMS 或网站面板直接发布」类型,MonkeyCode-AI 用户作品集不接收此类站点,本次发布已取消。
然后**立即结束**本 Skill,**不得**再询问用户是否继续,**不得**进入后续任何步骤。
---
## 步骤 2 —— 自动判定 kind(static / backend)
由 Skill **基于工作目录扫描**,自动判定本次发布的 kind:
判定规则(按优先级):
1. 工作目录中存在以下任一**后端特征**之一 → 判定为 `backend`:
- `requirements.txt` / `pyproject.toml` 含 `fastapi` / `uvicorn` / `django` / `flask` / `gunicorn`
- `manage.py`
- 顶层 `pom.xml` / `build.gradle`(Spring Boot 等 JVM 项目)
- `go.mod`
- `Cargo.toml`(且非纯 wasm 前端)
- `composer.json`(PHP)
- `Gemfile` 含 `rails` / `sinatra`
- `package.json` 中出现 `express` / `fastify` / `koa` / `hapi` / `nest` 等后端框架依赖
2. 仅有 `index.html` / 静态 HTML 文件 → 判定为 `static`
3. 存在 `package.json` 且无后端框架依赖(Vite / CRA / Next 静态导出 / Vue / Astro / Nuxt SSG 等纯前端项目) → 判定为 `static`
4. 探测不出来 → 询问用户「按纯前端发布」还是「按容器化后端发布」,**仅此一次**作为兜底
判定完成后分流:
- `static` → **static 子流水线**(步骤 3 → 4 → 5 → 6 → 7 → 8)
- `backend` → **backend 子流水线**(步骤 3 → 3b → 5 → 7 → 8;步骤 4/6 由 3b 取代)
### 进入 static 分支前必须告知用户(硬约束)
判定为 `static` 后,**进入步骤 3 之前** 必须在对话中明确告知以下平台限制:
> **⚠ 即将将本应用发布到 [MonkeyCode-AI 用户作品集](https://showcase.monkeycode-ai.online/)。线上运行时有以下两条限制,请确认是否继续发布:**
>
> 1. **数据只能保存在浏览器**:纯前端应用没有任何服务端持久层,可用的存储仅限当前浏览器的 `localStorage` / `sessionStorage` / `IndexedDB`。用户换浏览器、清缓存、换设备后数据都不会跟随;多个用户之间不会共享数据。
> 2. **公开可见**:应用发布后会在 用户作品集 (showcase.monkeycode-ai.online) 列表公开可见,所有人都可以访问到你发布的应用。
然后用 `question` 工具,要求用户确认,选项为「继续发布」/「取消」。
- 用户选「继续发布」 → 进入步骤 3
- 用户选「取消」 → 终止本次发布
### 进入 backend 分支前必须告知用户(硬约束)
只告诉用户 **与线上运行阶段相关** 的注意事项,不暴露任何 镜像 build 阶段细节(Dockerfile 怎么写、是否用 supervisord、依赖如何下载等都是 Skill 自己处理的事,与用户无关)。
判定为 `backend` 后,**进入步骤 3b 之前** 必须在对话中明确告知以下平台限制:
> **⚠ 即将以容器方式,将本应用发布到 [MonkeyCode-AI 用户作品集](https://showcase.monkeycode-ai.online/)。线上运行时有以下四条限制,请确认是否继续发布:**
>
> 1. **单容器**:平台只调度一个容器。如果应用依赖数据库、对象存储、缓存、队列等组件,会与业务一起塞在同一个容器里运行,不支持外部独立服务。
> 2. **无外部网络**:容器无法访问公网与任何外部服务。远程数据库、S3、第三方 API、OAuth / 支付 / 微信、CDN、外部 LLM 等都不可达。业务在线上启动后,只能被外部访问,不应也不能访问互联网。
> 3. **无持久化存储**:服务更新、异常重启或被运维重建容器时,文件系统会被重置,所有运行时写入(SQLite、用户上传、日志、缓存等)都会丢失。
> 4. **公开可见**:应用发布后会在 用户作品集 (showcase.monkeycode-ai.online) 列表公开可见,所有人都可以访问到你发布的应用。
然后用 `question` 工具,要求用户确认,选项为「继续发布」/「取消」。
- 用户选「继续发布」 → 进入步骤 3b
- 用户选「取消」 → 终止本次发布
---
## 步骤 3 —— 探测项目类型
复用 `deploy-website` 中的探测逻辑。根据步骤 2 的自动判定结果分流:
### static 分支(kind=static)
| 探测结果 | 走向 |
|---|---|
| 存在 `package.json`(Node 项目) | → **Node 构建分支**(步骤 4 分支 B/C) |
| 仅有 `index.html` / 静态 HTML 文件 | → **静态分支**(步骤 4 分支 A) |
#### 包管理器探测(仅 Node 项目)
优先级顺序:
1. `pnpm-lock.yaml` → `pnpm`
2. `yarn.lock` → `yarn`
3. `package-lock.json` → `npm`
4. 都没有 → 默认 `npm`
#### 构建命令解析(仅 Node 项目)
按优先级:
1. `package.json` 的 `scripts.build` → `<pkgMgr> run build`
2. 已知框架默认产物目录——Vite/CRA/Astro → `dist`,Next.js 静态导出 → `out`,react-scripts → `build`
3. README 兜底:扫描 `README*` 中包含关键字 `build` / `compile` / `dist` 的命令并提取
4. 以上都失败 → **询问用户**指定构建命令,不要猜测
#### 预期产物目录
记录预期的输出目录(`dist` / `out` / `build`),供步骤 4 使用。
### backend 分支(kind=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 自动判定为 `backend` 且用户在告知运行时四条限制后选择「继续发布」时执行。完成后跳过步骤 4/6 直接进入步骤 5、7、8。
### 3b.0 平台限制(生成 Dockerfile 前必须遵守)
showcase 平台对 backend 容器有四条硬约束,必须同时满足,否则镜像无法正常运行。
#### A. 单容器 + 进程编排(按需 supervisord)
- 平台只调度**一个容器**,不支持 docker-compose / k8s pod / sidecar
- 进程编排策略**按附加组件需求二选一**(Skill 在 3b.1 自行判定):
- **单进程方案(默认)**:业务本身无 DB / 对象存储 / 缓存 / 队列等附加依赖,是一个无状态后端进程 → **直接 `ENTRYPOINT` / `CMD` 拉起业务进程**,不要引入 supervisord
- **多进程方案**:业务依赖 DB / 对象存储 / 缓存 / 队列等附加组件 → 这些组件**全部打入同一镜像**,由 **supervisord** 与业务进程一起拉起、守护、按 `priority` 排序
- 多进程方案下附加组件的本地化:
- 关系型 DB(PostgreSQL / MySQL / MariaDB)→ 整体打入镜像,初始 schema 在 builder stage 灌进数据目录
- 对象存储(MinIO / SeaweedFS)→ 整体打入镜像,bucket 在容器启动脚本里初始化
- Redis / Memcached / Elasticsearch / RabbitMQ 等 → 同理,全部本地拉起
- 业务代码连接附加组件时**必须走 `127.0.0.1` / `localhost` / Unix socket**,不得使用外部 host 名或外部 endpoint
#### B. 容器无外部网络(运行时离线)
容器运行时**完全切断公网与外网访问**:
- 出站 DNS、TCP、UDP 均不可达;远程 DB、远程 S3、远程 Redis、第三方 API、OAuth IdP、CDN、`pip` / `npm` / `apt` 等**全部不可用**
- 镜像必须做到**完全自包含**:所有运行时需要的资源在 **builder stage 提前下载并 COPY 进 runtime stage**,包括但不限于:
- 业务依赖包(已经在 builder 装好,runtime 直接拷 venv / node_modules / target/release / vendor)
- 模型权重、embedding 文件、tokenizer
- 字体、字典、地理库、本地化资源
- DB 初始 schema(`*.sql`)与种子数据
- 静态前端产物(如果是前后端一体镜像)
- HTTPS 根证书(如果业务以前是直连公网 CA 校验,现在改为只信任内部证书或干脆走内网 HTTP)
- 对外暴露:**仅 `service_port` 一个端口**,由平台反向代理对外提供 HTTP 服务
#### C. 容器无持久化存储
- 服务更新发布、容器异常崩溃、运维侧重启都会**重建容器**(旧实例销毁、新实例从镜像启动)
- 不挂载任何 volume / bind mount,对文件系统的所有写入在重建时丢失
- 容器内打包的 DB / 对象存储**也会一并清零**——重启后必须由 supervisord 启动脚本重新执行初始化 schema + 种子数据
Dockerfile **不得** `VOLUME` 声明数据目录(声明无效,反而误导用户)。
如果项目**强依赖**外部持久层或外部 API(如必须连真实的微信 / 支付 / 外部 LLM),需再次告知用户「相关功能部署后可能无法正常使用」,用户认同后才能继续构建和发布。
#### D. 资源上限
- CPU:1 核
- 内存:1 GiB(含 swap)
- 镜像 tar.gz:≤ 500 MB
打包 DB、对象存储、模型等附加组件时务必尽可能按此上限裁剪。
### 3b.1 生成 Dockerfile
AI 基于步骤 3 的探测结果生成**多阶段 alpine** `Dockerfile`,写入 `/tmp/Dockerfile`,该 Dockerfile 专门用于构建被发布到 showcase 上的容器镜像。**不得**把这个 Dockerfile 写到用户工作目录。
硬性写法约束:
- 最终(runtime)stage 必须基于 alpine 或 alpine 风味的语言镜像(如 `eclipse-temurin:21-alpine-jdk`)
- runtime stage **原则上禁止** `apt-get` / `dnf` / `yum`,如果必须安装软件包,在安装完后必须执行清理操作(如 `apt clean`)
- **禁止** `ADD <url>` —— 所有外部资源在 builder stage 用 `RUN curl/wget` 显式落盘
- 必须多阶段;runtime stage 只 `COPY --from=builder` 编译产物 / 运行时依赖(含模型权重、字体、初始化 SQL、根证书等运行时资源),不得在 runtime 跑任何联网命令
- 进程编排策略按 3b.0 §A 自动选择:单进程方案 `CMD ["业务命令", ...]`;多进程方案 `CMD ["/usr/bin/supervisord","-c","/etc/supervisord.conf","-n"]`(配置见 3b.1.a)
- `CMD` 必须为 exec 形式(JSON 数组)
- 必须 `EXPOSE <service_port>`,且与 multipart 字段 `service_port` 一致;附加组件端口(DB / Redis / MinIO 等)只走 127.0.0.1,**不得 EXPOSE**
- **所有 `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,不再受代理影响
#### 3b.1.a supervisord 配置(仅多进程方案)
> 仅当业务有附加组件依赖(DB / 对象存储 / 缓存 / 队列等)时启用本节;单进程方案跳过本节,直接在 runtime stage 写 `CMD ["业务命令", ...]` 即可。
runtime stage 必须安装 supervisord 并提供配置文件:
```dockerfile
# runtime stage 内
RUN apk add --no-cache supervisor
COPY supervisord.conf /etc/supervisord.conf
COPY --from=builder /app /app
EXPOSE <service_port>
CMD ["/usr/bin/supervisord","-c","/etc/supervisord.conf","-n"]
```
`supervisord.conf` 最小模板(按项目实际附加组件增删 `[program:*]` 段,并用 `priority` 控制启动顺序,数字越小越早启动):
```ini
[supervisord]
nodaemon=true
user=root
logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/run/supervisord.pid
[program:postgres]
priority=10
command=/usr/local/bin/docker-entrypoint.sh postgres
user=postgres
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:app]
priority=50
command=/app/start.sh
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
```
`/app/start.sh` 中业务进程启动前要做:
1. 等待附加组件就绪(`pg_isready` / `redis-cli ping` / 探活脚本,最长等 30s)
2. 幂等地灌入初始 schema 与种子数据(从 builder 阶段烤进镜像的 `*.sql`)
3. 再 `exec` 业务进程(保留 PID 1 子进程语义,方便 supervisord 回收)
> supervisord 配置文件 `supervisord.conf` 和启动脚本 `start.sh` 由 Skill **生成到 `/tmp/`** 后 `COPY` 进镜像,**不得**写入用户工作目录。
#### 3b.1.b 附加组件打包指引(仅多进程方案)
| 组件 | 推荐做法 |
|---|---|
| PostgreSQL | builder stage 装好 `postgresql` + 初始化 `initdb`;runtime stage 用 `apk add postgresql`;supervisord 启动 `postgres -D /var/lib/postgresql/data`;schema 从 builder COPY,启动脚本里 `psql -f` 灌入 |
| MySQL/MariaDB | runtime stage `apk add mariadb mariadb-client`;`mysql_install_db --user=mysql` 在 builder 完成;supervisord 启动 `mysqld --user=mysql` |
| Redis | runtime stage `apk add redis`;supervisord 启动 `redis-server --bind 127.0.0.1 --save ""`(关闭持久化或写到容器内临时目录) |
| MinIO | builder stage `wget` 拉 minio 二进制;runtime COPY;supervisord 启动 `minio server /data --address 127.0.0.1:9000`;bucket 在启动脚本里用 `mc` 初始化 |
| Elasticsearch / Kafka 等重量级组件 | 1 GiB 内存装不下,**告知用户改造为轻量替代**(如改用 SQLite FTS / Redis Streams / NATS embedded)或终止本次发布 |
#### 依赖下载镜像约定(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 内):
Auf GitHub ansehen