# HtmlDock AI 发布文档
你的任务:把当前对话里生成的成果发布到 HtmlDock,并返回最终可访问链接。
成果可能是:① 单个 HTML 页面(一段完整 HTML);② 含多个文件的站点(前端构建产物 / dist 目录)。
是哪种就走对应的接口 —— 判断规则见下面第 4 条与《发布多文件站点(ZIP)》章节。
## ★ 先读这一段(动手前必读)
本文档是你动手前唯一需要遵守的规则来源,请完整读一遍再调第一个接口 ——
不要凭你对同类平台的既有印象行事,与你的认知冲突时一律以本文档为准。
下面 5 条最容易踩、代价最大,先记住:
1. 能更新就别重新发布:这条内容之前发过、只是要改,走更新接口
(单页 PATCH /api/v1/agent/pages/{pageNo},站点 POST /api/v1/agent/sites/{siteNo})。
重新发布会多一个新链接,旧链接、绑定域名、访问密码都留在旧对象上,积分还多扣一份。
2. 拿不准是"新发布"还是"更新",就问用户一句、别猜 —— 猜错会覆盖掉用户的另一个页面,内容不可恢复。
判断表见《先判断:这次是新发布,还是更新?》★。
3. 默认 renewMode="auto"(每天 1 积分自动续期、页面不下线),除非用户明确要求别的模式。
4. 本地资源只有单页这一条路径要单独处理:单页 HTML 里的本地图片用 images[] 一并上传;
多文件站点不要拆图片,整个目录打成 ZIP 走站点接口,包里相对路径天然生效。
5. 成功回链接、失败说原因:成功把响应里的 url 原样给用户;失败如实说明,不要假装成功。
400「内容含违规信息」是内容审核没过 —— 不是故障、旧内容还在,改内容再发;
不要重试同一份内容,也不要改成规避审核的写法(见《内容审核》★)。
## 执行规则(重要)
1. 优先直接调用 API 自动发布,不要先教用户手动操作。
2. 默认使用当前上下文里最后一个完整的**成品**版本(单页 = 那一个 HTML;站点 = 那一整套文件);如果有多个版本,取最后一个完整版本。
3. 本地资源**只有单页 HTML 这一条路径**需要单独处理:如果 HTML 中存在本地图片、相对路径资源或依赖文件,
必须按下文《本地图片 / 相对路径资源处理规则》用 images 数组一并上传。
多文件站点**不要拆图片** —— 整个目录打成 ZIP 上传,相对路径在包里天然生效(见第 4 条)。
4. 如果目标不是单段 HTML,而是整个前端工程 / 构建产物 dist/ 目录,请改走「发布多文件站点(ZIP)」章节:把目录打成 ZIP 再上传,不要试图把所有文件塞进一个 html。
5. 默认使用 renewMode="auto",除非用户明确指定其他模式。
6. 发布成功后:把响应里的 url 原样返回给用户,不要只说「已发布成功」;
同时**把响应里的 pageNo(页面编号)/ siteNo(站点编号)记在上下文里** ——
用户接着让你改这一版时,直接用它走更新接口,既不用回头翻列表,也不用问用户要链接。
7. **这条页面/站点已经发过、只是要改内容时,必须走更新接口,不要重新发布**:
- 单页:PATCH /api/v1/agent/pages/{pageNo}
- 站点:POST /api/v1/agent/sites/{siteNo}
重新发布会每改一次多一个新链接(旧链接、绑定域名、访问密码都留在旧对象上,积分还得多扣一份)。
拿不准"这次算新发还是更新"时,按《先判断:这次是新发布,还是更新?》那节的决策表走;
表里标了「要问」的情况就停下来问用户,不要自己猜 —— 猜错会覆盖掉用户的另一个页面。
8. 如果无法调用 HTTP、拿不到完整成品(单页 = 完整 HTML;站点 = 完整目录)、缺少本地文件、或发布失败,必须明确说明原因,不要假装成功。
## ⚠️ 编码警告(必读)
服务端按 UTF-8 解析 JSON。如果你用非 UTF-8 编码发送(典型场景:Windows GBK 终端里用 curl -d 内联含中文的 HTML),
中文会变成乱码,页面会显示异常。
正确做法:
- curl:必须从文件读取 —— curl --data-binary @payload.json;禁止用 -d 内联含中文的 HTML
- Python:读文件时显式 open(path, "r", encoding="utf-8");requests 用 json= 参数会自动按 UTF-8 编码
- Node.js:fs.readFileSync(path, "utf-8")
- 其它语言:确保构造出的 JSON 字符串始终是 UTF-8 字节
只从文件读取、再用 HTTP 库的 JSON 方法发送,通常不会出问题。只有 shell 内联、管道拼接、终端编码不匹配时才会触发。
## 最小可执行示例
HTML 里的图片都是 http(s) 外链、或者根本没有图片时,直接这样发:
接口:
POST https://api.htmldock.cn/api/v1/agent/pages
Content-Type: application/json
Authorization: Bearer {用户的 Token}
先写一个 payload.json(注意 html 里的换行是 JSON 的 \n 转义):
```json
{
"html": "<!DOCTYPE html>\n<html lang=\"zh\">\n<head>\n <title>我的页面</title>\n</head>\n<body>\n <h1>Hello World</h1>\n</body>\n</html>",
"title": "我的页面",
"renewMode": "auto"
}
```
然后调用(--data-binary + @文件,不要用 -d 内联):
curl -X POST https://api.htmldock.cn/api/v1/agent/pages \
-H "Authorization: Bearer {用户的 Token}" \
-H "Content-Type: application/json" \
--data-binary @payload.json
## 请求字段(POST /api/v1/agent/pages)
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| html | string | 是 | - | 完整 HTML 代码,上限约 220 万字符,超出返回 400 |
| title | string | 否 | 自动从 <title> 提取 | 页面标题,最长 200 字符 |
| renewMode | "auto" 或 "fixed" | 否 | "auto" | 续期模式,只接受这两个取值 |
| expireDays | number | 否 | 7 | 仅 renewMode="fixed" 时使用;只允许 1、7、30、365 |
| images | array | 否 | [] | 本地资源附件,见下文;单次上限 50 张 |
renewMode 说明:
- auto:每天自动扣 1 积分续期,页面一直在线、不设死期;积分不足时续期失败,页面到期下线。推荐。
- fixed:一次性按 expireDays 买断,到期自动下线,之后不再产生每天扣费。
## 成功响应(HTTP 200)
⚠️ 重要:本接口的响应体**就是结果对象本身,没有 code / msg / data 包装**(和部分平台的习惯不同)。
请直接读响应体顶层字段,不要去找 data.url。
```json
{
"pageNo": "7K3M9Q2XBZ",
"url": "https://ab12.htmldock.link/",
"title": "我的页面",
"status": "ONLINE",
"expireAt": null,
"expireDays": null,
"renewMode": "auto",
"creditsRemaining": 42.3,
"creditsCharged": 0,
"imagesUploaded": 0,
"warnings": []
}
```
| 字段 | 说明 |
|------|------|
| pageNo | **页面号(10 位)—— 请记在上下文里**。后续用户让你改这一版时,用它走 PATCH /api/v1/agent/pages/{pageNo} 更新;删除用 DELETE /api/v1/pages/{pageNo} |
| url | **最终访问链接**(用户的绑定域名,没有则平台分配的二级域名)。只有一个链接,直接给用户,不用挑 |
| title | 实际落库的标题(可能是从 <title> 提取的) |
| status | 页面状态,正常为 ONLINE |
| expireAt | 到期时间;renewMode="auto" 时为 null |
| expireDays | 仅 renewMode="fixed" 时有值 |
| creditsRemaining | 本次发布(含附件扣费)之后的可用积分合计 |
| creditsCharged | 本次**实扣**积分(发布后按前后余额差算)。免费套餐 / auto 模式下可能为 0 |
| imagesUploaded | 实际上传并替换的附件张数 |
| warnings | 非致命提示(例如 HTML 引用了本地资源但 images 里没提供)。**出现 warnings 不代表发布失败**,但要转述给用户 |
成功后直接把 url 返回给用户即可。
## 错误处理
错误响应体格式(固定三个字段):
```json
{
"code": 429,
"message": "发布过于频繁:同一个 Token 每小时最多 30 次,请稍后再试",
"timestamp": "2026-09-16T15:20:31"
}
```
code 与 HTTP 状态码一致,message 是可直接向用户转述的中文原因。
| HTTP | 含义 | 常见原因 | AI 应该怎么处理 |
|------|------|----------|------------------|
| 400 | 请求参数错误 | html 为空、renewMode 不是 auto/fixed、expireDays 不在允许列表、images 超限或 fileName 重复、附件 base64 非法 | 按 message 修正请求体后重试,不要原样重发 |
| 400 | **内容审核未通过**(看 message 是不是「内容含违规信息…」) | 内容被平台安全审核拦下 | **不是接口故障、也不是没更新成功**:改内容后再发,见《内容审核》一节。别重试同一份内容 |
| 401 | 鉴权失败 | 没有 Token、Token 错误或已被重置 | 明确提示用户到 /ai-publish 重新获取 Token;不要用旧 Token 继续重试 |
| 402 | 积分不足 | 余额不够支付本次附件(0.1 积分/张) | 直接告诉用户积分不足并提示去 /credits 补充,不要反复重试 |
| 404 | 路径不存在 | 接口路径拼错,或用了本文档没有的端点 | 对照本文档核对路径与请求方法,不要重试同一个 URL |
| 405 | 请求方法不对 | 用 GET 访问只支持 POST 的接口(例如 GET /api/v1/agent/pages) | 看响应头 Allow 里列出的方法并改对,这是请求写法问题、不是服务端故障,别重试 |
| 410 | 内容已失效 | 极少见:页面元数据还在但底层内容对象已丢失 | 告诉用户该页面内容已失效、需要重新发布,不要重试 |
| 413 | 请求体过大 | HTML 太大或附件太多 | 精简 HTML 或减少附件后重试 |
| 429 | 频率限制 | 同一 Token 每小时超过 30 次 | 告诉用户稍后再试,不要连续刷接口 |
| 500 | 服务端异常 | 平台自身故障 | 说明是服务端异常、建议稍后重试,不要假装成功。注意:参数/方法/路径写错不会返回 500,收到 500 请不要往自己的请求上找原因 |
## 内容审核(发布和更新都要过这一关)
**所有发布与更新都会先过一遍内容安全审核。** 没过审是很常见的情况,别把它误判成"接口坏了"或"更新失败"。
### 情况一:被拦下 → HTTP 400
响应 message 只会是这两句之一:
- 页面:内容含违规信息,请修改后重新发布
- 站点:内容含违规信息,请修改后重新部署
⚠️ 关键认知:**这不是接口故障,也不是"没更新成功"** —— 是内容没过审,请求被**整单拒绝**了。
- 更新被拦时,页面**保持原来的旧内容**(不会变空白、也不会改一半),不需要你回滚或补救
- **重试同一份内容没有用**:同样的内容会一直被拦(判定结果有缓存),必须**改内容**再发
正确做法:
1. 明确告诉用户"内容没通过平台审核",把 400 的 message 原样转述
2. **不要猜测、也不要编造具体是哪句话/哪个词违规** —— 服务端不返回命中原因(这是合规要求),你猜不出来
3. 让用户改内容再发。常见触发点:涉政/色情/暴恐表述、诱导分享或引流话术、站外跳转与收款码、
绝对化宣传("最便宜""全网第一")、仿冒官方或知名品牌的页面
4. **不要做"换个说法绕过去"的改写** —— 那是规避审核,属于违规操作,也不要连续重发同一份 payload
### 情况二:疑似但放行 → 正常 200
内容照常上线、链接正常可用。它只是进了人工复核队列,后续可能被运营处理
(页面状态会变为 PENDING_HUMAN / REJECTED / FROZEN)。
这种情况你**不用做任何处理**,也不用跟用户说"可能有风险",正常返回链接即可。
如果失败,请明确告诉用户:是哪一步失败、失败原因、是否还缺本地资源 / Token / HTML 内容。
## 本地图片 / 相对路径资源处理规则
如果 HTML 里引用了本地文件 —— 即 src、href、poster、data-src、srcset 或 CSS url(...) 中的路径**不以 http:// 或 https:// 或 data: 开头**
(例如 src="./logo.png"、background:url(./bg.png))—— 这些资源必须通过 images 数组一并上传。
你要做的:
1. 扫描 HTML 中所有本地资源引用。
2. 找到这些文件的真实内容。
3. 内容转为 base64。
4. 放进 images 数组。
5. fileName 用文件名即可。**匹配规则是按文件名的最后一段匹配**:
HTML 里写 "./logo.png"、"/assets/logo.png"、"logo.png" 都只需一条 fileName="logo.png"。
images 数组格式:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| fileName | string | 是 | 文件名;同一次请求内不可重复 |
| data | string | 是 | 文件内容的 base64 |
| mimeType | string | 否 | MIME 类型,如 image/png;不传则由服务端按扩展名推断 |
完整请求示例(HTML 引用了两个本地图片):
```json
{
"html": "<!DOCTYPE html>\n<html>\n<body>\n <img src=\"./logo.png\">\n <img src=\"./photo.jpg\">\n</body>\n</html>",
"title": "我的产品页",
"renewMode": "auto",
"images": [
{
"fileName": "logo.png",
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
},
{
"fileName": "photo.jpg",
"data": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDA..."
}
]
}
```
data 支持两种写法:
- 纯 base64 字符串
- 带前缀的 data URI,如 data:image/png;base64,...
服务端会自动:校验格式与大小、上传到对象存储、把 HTML 里的本地路径替换成直链、扣掉附件对应的积分。
已经是 http:// 、https:// 或 data: 的引用**不会被改动**,也不需要放进 images。
如果 HTML 引用了本地资源但 images 里没提供:接口仍会发布成功,但会在响应的 warnings 里列出这些文件名,
页面上它们会裂图。请补上 images 后重新发布。
## 先读账号状态,再决定怎么发
如果你是 agent,不要只看页面内容 —— 先读用户的套餐和积分状态,判断每天是否还有足够积分继续自动续期。
GET https://api.htmldock.cn/api/v1/agent/me
Authorization: Bearer {用户的 Token}
返回字段:
| 字段 | 说明 |
|------|------|
| tier / tierName | 套餐 id(free / pro / premium)与中文名 |
| totalCredits | 可用积分合计 = permanentCredits + planCredits |
| permanentCredits | 永久积分余额(不过期) |
| planCredits / planCreditsCap | 套餐积分余额(扣费时优先消耗)与套餐积分池上限 |
| dailyCredits | 当前套餐每日自动补充的积分 |
| pageCount | 当前在线页面数 |
| autoPageCount | 自动续期页面数(每个每天消耗 1 积分) |
| totalDailyConsumption | 当前每天固定消耗的积分 |
| estimatedDaysLeft | -1 = 收支平衡或净流入,余额不会耗尽;0 = 目前没有固定消耗;正数 = 按当前净支出估算还能撑多少天 |
要连续发布多个页面时,先看 estimatedDaysLeft,快见底就停下来提醒用户充值,而不是一直发到报 402。
## 先判断:这次是新发布,还是更新?(★ 动手前必读)
判断错了代价不小,两个方向都会出问题:
- **该更新却新建**:每改一次多一个链接,旧链接、绑定域名、访问密码全留在旧对象上,积分还多扣一份
- **该新建却更新**:会**覆盖掉用户的另一个已有页面**,内容不可恢复 —— 这个方向更严重
按下表顺序判断,**有明确依据就别问,没把握才问**:
| 情况 | 怎么做 | 要不要问用户 |
|------|--------|-------------|
| 上下文里已有 pageNo / 访问链接(你上一轮刚发过、或用户直接给了链接) | 直接走更新接口 | **不问** |
| 上下文里没有任何已发布痕迹,用户只是说"帮我发布这个页面" | 直接新建 | **不问** |
| 用户说"改一下 / 更新 / 再发一版 / 刚才那个页面",但没给链接也没给编号 | 先自己查:页面 `GET /api/v1/pages`(返回 pageNo / title / 可直接访问的 url),站点 `GET /api/v1/zip-sites`(返回 siteNo / title / primaryHost,地址 = `https://{primaryHost}/`) | 见下 |
| 查到**唯一**明显匹配(标题对得上) | 更新它,但**必须先确认一句** | **要问一句** |
| 查到**多个候选**或都不像 | 把候选列出来让用户挑,不要自己挑 | **要问** |
唯一匹配时的确认话术(覆盖不可逆,这一句不能省):
找到你之前发的《XXX》(xxx.htmldock.link),我把这一版覆盖上去吗?
**不要做的事**:
- 不要每次发布前都例行问一句「你要新建还是更新?」—— 有明确依据时直接做,反复打断是最差的体验
- 不要凭"感觉像"去猜 pageNo 直接覆盖 —— 拿错就是毁掉用户的另一个页面
- 用户给了旧链接、但这次是**全新内容**(不是同一作品的迭代)→ 那是新建,不是更新
## 更新已有页面(★ 能更新就别重新发布)
**要改的页面已经存在时,一律用下面的 PATCH 更新,不要重复 POST /agent/pages。**
重复发布会每改一次就多一个新链接 —— 旧链接、绑定域名、访问密码全留在旧页面上,积分也白扣一份。
接口:
PATCH https://api.htmldock.cn/api/v1/agent/pages/{pageNo}
Content-Type: application/json
Authorization: Bearer {用户的 Token}
body —— 字段全部可选,传什么改什么(没传的字段 = 不动):
html 新的完整 HTML(整体替换,不是局部补丁)
title 新标题
images[] 本地资源附件,规则与发布完全一致(0.1 积分/张)
renewMode auto / fixed;**不传 = 完全不动续期设置**
expireDays 仅 fixed 有意义,允许 1 / 7 / 30 / 365
curl(同样要从文件读,别用 -d 内联中文):
curl -X PATCH https://api.htmldock.cn/api/v1/agent/pages/{pageNo} \
-H "Authorization: Bearer {用户的 Token}" \
-H "Content-Type: application/json" \
--data-binary @patch.json
更新不会改变:pageNo、访问链接、绑定域名、访问密码 —— 用户可以反复迭代同一个作品。
不知道 pageNo 时先查:
1. 列出用户的页面:GET /api/v1/pages
2. 读取旧内容:GET /api/v1/raw-html/{pageNo}(返回原始 HTML,无需鉴权)
拿到 pageNo 后再 PATCH。
⚠️ 三条容易踩的:
- **不要顺手传 renewMode**:不传 = 不动续期;把 auto 页切成 fixed 又不给天数,会按 365 天实扣。
- **只改标题不扣积分**;改内容按「用户自己在网页上编辑」的同一套规则扣(同一页面当天只扣一次,
自动续期中的页面不扣,免费套餐不扣)。
- 页面已过期(EXPIRED)会返回 **410**:先续期(传 renewMode=fixed + expireDays)再改内容。
- 更新**同样要过内容审核**:被拦时返回 400「内容含违规信息,请修改后重新发布」,
**旧内容原样保留**(不是改坏了,也不是没成功)—— 改内容后重发即可,详见《内容审核》一节。
响应里的 creditsCharged 是本次**实扣**积分(当天已扣过就是 0)。
删除页面:DELETE /api/v1/pages/{pageNo}(成功返回 204,**删除不退积分**)。
## 发布多文件站点(ZIP / dist)
单页发布 POST /api/v1/agent/pages 只接受一段 HTML 字符串,装不下带 assets/ 的完整前端工程;
而且单段 HTML 本身还有 2MB 体积上限,超出直接被拒(400)。
这两种情况都改走下面这个接口:上传一个 ZIP 压缩包做整站多文件托管 —— 哪怕只是单个 .html 超了 2MB,
把它作为 index.html 放在压缩包根目录、打成一个 ZIP 即可(不必真有多文件)。单文件 / 整包上限按档位配额(见下表)。
接口:
POST https://api.htmldock.cn/api/v1/agent/sites
Authorization: Bearer {用户的 Token}
Content-Type: multipart/form-data
字段:
file 必填。ZIP 文件(二进制),典型来源是前端构建产物 dist/
title 可选。站点标题;不传时用 ZIP 文件名(去掉 .zip)兜底
tier 可选。档位 S / M / L / XL,不传默认 M
accessPassword 可选。访问密码(访问门槛,不是加密;别向用户承诺"绝对打不开")
enableFeedback 可选。true 开启访客留言,默认 false
customSubdomain 可选。自选二级域名前缀
档位限制(都是解压后的口径,不是压缩包大小):
| tier | 单文件上限 | 解压后总量上限 | 文件数上限 | 积分/天 |
|------|-----------|---------------|-----------|--------|
| S | 2 MB | 2 MB | 20 | 2 |
| M | 10 MB | 10 MB | 100 | 4 |
| L | 50 MB | 50 MB | 500 | 8 |
| XL | 200 MB | 200 MB | 2000 | 32 |
★ 带视频 / 音频的站点(XL 档就是为这个准备的):
· 视频压缩率极低,**包体基本等于原始体积** —— 一个 120MB 的 mp4 就得按 120MB+ 算,
L 档(50MB)一定装不下,直接选 XL,别先试 L 再重传(每次重传都是一次完整上传)。
· 音视频文件由 OSS 直链下发(服务端 302 过去),**支持拖动进度条、iOS Safari 也能播**;
播放地址可以直接给用户,也可以右键/长按下载。
· 注意流量:观众每看一次就是一整个文件的下行流量,长期挂着的视频请提示用户注意成本。
计费:**部署只扣第 1 天**(S=2 / M=4 / L=8 / XL=32 积分),之后每天 02:00 自动扣同样的积分续 1 天。
不再一次性买断 30 天。余额不足不会立刻下线 —— 已付费的那一天照常在线,
到到期时间才停止访问;停止访问后内容还会保留 7 天,期间充值可在「站点托管」点「恢复」重新上线。
打包规则(这三条最容易踩):
1. 文件类型有白名单,只有这些扩展名能上传:
页面/脚本:html htm css js json webmanifest map wasm
图片:png jpg jpeg gif webp svg ico avif
字体:woff woff2 ttf otf eot
音频:mp3 wav ogg oga m4a aac flac weba
视频:mp4 webm mov m4v ogv(vtt 为字幕)
文档/数据/3D:txt md pdf xml csv glb gltf
压缩包里出现任何一个白名单外的文件,整个包都会被拒绝(400)。
白名单只负责挡可执行文件与服务端脚本(exe / sh / bat / dll / php / jsp / py / jar 等),
浏览器当静态资源用的一律放行 —— 2026-09-28 起音视频、PDF、字体、3D 都已放开。
体积不用按扩展名挑:单文件大小与整包总量都由档位配额兜底(见上表),
超限返回 400「超过档位限额」,把大文件压一压或升档即可。
⚠️ sourcemap(.map)现在允许上传,但它通常很大、最容易撑爆档位配额,
构建时建议关掉或排除:cd dist && zip -r ../site.zip . -x "*.map"
2. 入口文件 = 站点里的 index.html(index.htm 同义),**在哪一层都行**(dist/index.html、
sub/index.html 都能识别;同一包里有多份 index.html 时取路径最浅的那个)。
只有整个包里都没有 index.html 时,才退回"遇到的第一个 .html"。所以正常的前端构建产物
直接打包就行,不需要为了入口去调整压缩顺序。
3. 不要在压缩包里再套一层压缩包,嵌套 ZIP 会被拒绝。
入口在子目录是支持的:压缩包里是 dist/index.html 时,站点会以 dist/ 为根,
页面里的 ./assets/app.js 会被正确解析。也可以 cd dist && zip -r ../site.zip . ,
让 index.html 直接落在压缩包根目录(推荐,这样地址更干净)。
成功响应(HTTP 200):
```json
{
"siteNo": "A2NC0H5QXS",
"url": "https://xx.htmldock.link",
"title": "我的站点",
"status": "ACTIVE",
"tier": "M",
"tierLimit": "10 MB / 100 文件",
"fileCount": 42,
"totalBytes": 1834921,
"expireAt": "2026-10-16T15:20:31Z",
"creditsCharged": 120,
"creditsRemaining": 380,
"passwordProtected": false,
"feedbackEnabled": false
}
```
siteNo 是这个站点的唯一标识(10 位字母数字)。**请把它记在上下文里**:
后续要改访问密码、开关留言、绑定域名时,路径里用的就是它(POST /api/v1/zip-sites/{siteNo}/password 等);
**用户让你改完代码重新打包发布时用它覆盖更新**(POST /api/v1/agent/sites/{siteNo})。
不要用站点列表里的顺序去猜。
把 url 给用户即可 —— 它就是站点真正能打开的地址。
url 直接给用户访问即可;creditsCharged 是本次扣掉的积分,请如实告诉用户。
错误:
| 状态 | 含义 | 你怎么做 |
|------|------|---------|
| 400 | ZIP 或参数不合法 | 按 message 定位:不支持的文件类型 / 缺少 HTML 入口 / 超过档位限额 / 不允许嵌套 ZIP / 非法路径 / tier 只支持 S M L XL |
| 401 | 鉴权失败 | 提示用户到 /ai-publish 重新获取 Token,不要用旧 Token 重试 |
| 402 | 积分不足 | 报出 message 里的数字,提示用户充值或换更小的档位 |
| 429 | 频率限制 | 稍后再试,不要连续重试 |
完整 curl 示例:
cd dist && zip -r ../site.zip . -x "*.map" && cd ..
curl -X POST "https://api.htmldock.cn/api/v1/agent/sites" \
-H "Authorization: Bearer hmld_xxxxxxxx" \
-F "title=我的站点" \
-F "tier=M" \
-F "file=@site.zip"
## 覆盖更新已有站点(★ 改完 dist 重新打包时用这个)
站点已经存在、只是内容要更新时(典型场景:改完代码重新 build 出新的 dist/),
**不要重新 POST /agent/sites 建新站点** —— 那会多出一个站点、多一份每天扣费,
旧的访问地址和绑定域名还留在旧站点上。
接口:
POST https://api.htmldock.cn/api/v1/agent/sites/{siteNo}
Authorization: Bearer {用户的 Token}
Content-Type: multipart/form-data
字段:
file 必填。新的 ZIP(二进制)
tier 必填。S / M / L / XL —— 这里是必填(新建时才可省略):它是计费档位,
缺省兜底 M 会悄悄把站点档位和每天成本一起改掉
title 可选。不传沿用原站点标题
curl:
curl -X POST https://api.htmldock.cn/api/v1/agent/sites/{siteNo} \
-H "Authorization: Bearer {用户的 Token}" \
-F "tier=M" \
-F "file=@dist.zip"
计费与「用户在网页上点重新部署」完全一致:**同一天反复覆盖只扣一次**,
跨天才按档位日成本(S=2 / M=4 / L=8 / XL=32)再扣 1 天;响应 creditsCharged 为 0 就是这次没扣钱。
更新不会改变:siteNo、访问地址、绑定域名、访问密码、留言开关。
不知道 siteNo 时先查:GET /api/v1/zip-sites(返回 siteNo / title / primaryHost,
对外地址 = `https://{primaryHost}/`)。拿到 siteNo 再 POST 覆盖更新。
★ 改档位怎么算(比如原来是 M=4,这次传 tier=L=8):
· tier 是「明天起每天多少钱」,**不是差价** —— 不补收差额,也不退还;
· 当天已经扣过(今天发布过 / 用户在网页上更新过)→ 这次 creditsCharged = 0,
但新档位**立刻生效**,从第二天起按 L=8 每天扣;
· 当天还没扣过(站点已过期、或跨天后的第一次)→ 按**新档位**扣 1 天(L=8),
顺带把站点重新上线 1 天;同一天再覆盖一次仍然是 0。
· 结论:升档当天不多花钱,降档当天也不退钱。别为了让这次「少扣点」故意降档 ——
档位装不下会直接 400(超过档位限额),白跑一趟。
站点发布与单页发布共用同一把 Token、**同一个每小时限流窗口**:两个接口加起来算总次数。
## 同一把 Token 还能调用的接口
| 接口 | 作用 |
|------|------|
| POST /api/v1/agent/pages | 发布**新**页面(html + 可选 images[]) |
| **PATCH /api/v1/agent/pages/{pageNo}** | **更新已有页面 —— 改内容优先走这个,别重复发布**(见上) |
| POST /api/v1/agent/sites | 发布**新**的多文件站点(multipart 上传 ZIP) |
| **POST /api/v1/agent/sites/{siteNo}** | **覆盖更新已有站点 —— 重新打包 dist 时走这个**(见上) |
| GET /api/v1/agent/me | 账号概览(套餐 / 积分 / 页面数 / 每天消耗 / 还能撑几天) |
| GET /api/v1/agent/token | 取回当前 Agent Token(含完整明文;没有会自动签发一把,幂等) |
| POST /api/v1/agent/token | 重置 Agent Token(旧 Token 立即失效,返回新明文) |
| GET /api/v1/pages | 列出用户名下的所有页面(查 pageNo 用;响应带 hostedUrl / domainUrl,可直接给用户) |
| GET /api/v1/zip-sites | 列出用户名下的所有站点(查 siteNo 用)。⚠️ 响应里**没有 url 字段**,对外地址自己拼:`https://{primaryHost}/`;`primaryHost` 为空 = 该站点还没绑域名 |
| DELETE /api/v1/pages/{pageNo} | 删除页面,成功返回 204(**删除不退积分**) |
| DELETE /api/v1/zip-sites/{siteNo} | 卸载站点,成功返回 204(**不退还已扣积分**,OSS 内容立即清理、不可恢复) |
| GET /api/v1/raw-html/{pageNo} | 取回页面原始 HTML(无需鉴权),用于「读出来改一版再发」 |
| PATCH /api/v1/pages/{pageNo} | 网页端同款修改接口,同一把 Token 也能调。但**更新页面请优先用 agent 版**:它支持 images[] 附件,且响应带 creditsCharged |
## 计费与限流
- 页面在线:1 积分 / 页 / 天(renewMode="auto" 每天扣,fixed 一次性买断)
- **改内容(更新接口)**:与用户自己在网页上编辑完全同规则 —— 同一页面**当天只扣一次**,
自动续期中的页面改内容不扣,免费套餐不扣;**只改标题完全不扣**。所以反复迭代同一页不会反复烧积分
(但重新发布会按新页面再扣一份,这也是必须用更新接口的原因)
- 本地图片附件:0.1 积分 / 张
- 多文件站点(ZIP):按档位 S/M/L/XL 分别为 2 / 4 / 8 / 32 积分一天,部署只扣第 1 天,之后每天自动续扣
- 鉴权、参数校验、限流都在扣费之前完成;发布整体失败会回滚,不产生页面也不扣积分
- 频率限制:同一把 Token 每小时最多 30 次发布(单页发布与站点发布共用这个窗口)
## 代码示例
Python(注意 UTF-8):
```python
import requests
with open("page.html", "r", encoding="utf-8") as f:
html = f.read()
resp = requests.post(
"https://api.htmldock.cn/api/v1/agent/pages",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer <TOKEN>",
},
json={
"html": html,
"title": "我的页面",
"renewMode": "auto",
"images": images, # [{"fileName": "logo.png", "data": "..."}]
},
)
data = resp.json()
if resp.status_code == 200:
print(data["url"]) # 顶层字段,没有 data 包装
else:
print(resp.status_code, data.get("message"))
```
Node.js(注意 UTF-8):
```js
import fs from "fs";
const html = fs.readFileSync("page.html", "utf-8");
const resp = await fetch("https://api.htmldock.cn/api/v1/agent/pages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer <TOKEN>",
},
body: JSON.stringify({
html,
title: "我的页面",
renewMode: "auto",
images, // [{ fileName: "logo.png", data: "..." }]
}),
});
const data = await resp.json();
console.log(resp.ok ? data.url : `${resp.status} ${data.message}`);
```
## 如果你不能发 HTTP 请求
只有在你确实无法调用 HTTP 时,才改为引导用户手动操作:
1. 打开 https://htmldock.cn/editor
2. 粘贴 HTML
3. 若本地资源需要先转直链,打开 https://htmldock.cn/image-host 上传后替换链接
4. 点发布
不要在能自动发布时退回手动说明。