# 微信公众号草稿箱/发布 API · Markdown 一键转公众号 + 小程序订阅消息 · 接入文档

> 面向第三方调用方(exomind CLI、其它系统)。覆盖公众号发布(§1-8)与小程序通道(§9-11:订阅消息发送、内容安全校验)。小游戏后端暂不开放接入。
> 人类可读版:https://yyps.net/docs

本服务是**微信公众号发布共享服务**,调用方经一个 HTTP 接口把文章发到公众号草稿箱。

## 1. 概述

1. 在 auth 服务申请 API Key(绑定 service_name,如 `exomind`)
2. 带 `x-api-key` header 调 `POST /api/mp/publish`
3. 同步拿到 `draft_id`(草稿 media_id),完成。封面按 `cover` 策略处理。

## 2. 认证(API Key)

API Key 经 `x-api-key` header 传递,由 auth 服务签发:

```bash
curl -H "x-api-key: YOUR_API_KEY" https://yyps.net/api/mp/publish
```

> ⚠️ 不是 `Authorization: Bearer ...`(那是 admin 后台 JWT,与调用方无关)。不带或无效 Key → 401。

## 3. 发布接口

`POST https://yyps.net/api/mp/publish` (Content-Type: application/json)

| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `account` | 是 | 账号 key(如 ailang) |
| `title` | 是 | 文章标题 |
| `content` | 是 | 正文(HTML 或 Markdown) |
| `content_format` | 否 | `html`/`markdown`,默认 html |
| `cover` | 否 | 封面策略 `ai`/`auto`/`provided`/`none`,默认 `ai`(详见第 4 节) |
| `cover_image_prompt` | 否 | cover=ai 时直传 AI 画面 prompt;不传则服务端 LLM 读全文提炼 |
| `thumb_media_id` | 否 | cover=provided 时必填,封面 media_id |
| `author` | 否 | 作者名(显示在文章顶部) |
| `digest` | 否 | 摘要(不填微信自动抓正文前 54 字) |
| `theme` | 否 | Markdown 渲染配色主题:10 个预设(4 基础 `default`/`blue`/`green`/`dark` + 6 精选 `indigo`/`soft-blue`/`haze-blue`/`slate-blue`/`violet`/`ink-green`)+ custom 主题(theme_key,见第 7 节);仅 markdown 生效 |
| `show_cover_pic` | 否 | 正文顶部是否显示封面图(0/1),默认 1 |
| `need_open_comment` | 否 | 是否开启留言(0/1),默认 1 |
| `only_fans_can_comment` | 否 | 是否仅粉丝可留言(0/1),默认 0 |
| `idempotency_key` | 建议 | 调用方稳定业务键,建议 `article-id:revision`。同账号重复提交返回原草稿;同 key 不可用于不同原文 |
| `force_publish` | 否 | `true` 时显式跳过去重并创建新草稿,默认 false |
| `timestamp` / `nonce` | 建议 | 防重放(timestamp 5 分钟内,nonce 一次性) |

## 4. 封面策略 cover(核心)

`cover` 是封面策略枚举,互斥,默认 `ai`:

| cover | 行为 |
| --- | --- |
| `ai`(默认) | 发文用兜底封面秒回,后台 AI 生成(LLM 提炼画面 + 21:9)再 draft/update 换上,~1-2 分钟生效 |
| `auto` | 服务端兜底(默认封面 / 素材库最近一张),不 AI |
| `provided` | 用调用方传的 thumb_media_id(须永久素材 id) |
| `none` | 强制无封面(罕见,微信 draft/add 通常要求 thumb) |

**兜底链**服务端兜(provided → default_covers → 素材库最近),调用方**不用管封面来源**,服务端保证"发得出"。

`cover=ai` 时传 `cover_image_prompt` 则按它出图(适合自带强 LLM 的调用方),不传则服务端 LLM 读全文提炼。AI 生成在后台异步,发文秒回不阻塞。

## 5. 响应

```json
{
  "success": true,
  "message": "发布成功",
  "draft_id": "T1NF4457kCm1K5GW1HRWT...",
  "duplicate": false
}
```

> `draft_id` 即草稿 media_id。未传 `idempotency_key` 时,服务端仍按原始 account/title/content 指纹自动判重。重复成功返回 `success:true`、`duplicate:true`、原 `draft_id` 和 `duplicate_of`;处理中重复会带 `duplicate_status:"processing"`。渲染主题、封面和正文配图产生的微信 URL 不参与指纹。

## 6. 示例 curl

**① cover=ai(零配置,推荐)**

```bash
curl -X POST https://yyps.net/api/mp/publish \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"account":"ailang","title":"测试文章","content":"<p>正文</p>","content_format":"html","cover":"ai","idempotency_key":"article-123:1"}'
```

**② cover=auto(服务端兜底,不 AI)**

```bash
curl -X POST https://yyps.net/api/mp/publish \
  -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"account":"ailang","title":"测试","content":"<p>正文</p>","content_format":"html","cover":"auto"}'
```

**③ cover=ai + 直传 AI 画面 prompt**

```bash
curl -X POST https://yyps.net/api/mp/publish \
  -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"account":"ailang","title":"测试","content":"<p>正文</p>","content_format":"html","cover":"ai","cover_image_prompt":"横幅封面:橘猫坐窗台看雨,水彩风格,灰蓝调,右侧留白"}'
```

## 7. 主题列表接口(theme 可选值动态查)

`GET https://yyps.net/api/publish/themes/public` (公开,无需认证)

返回所有 is_public 主题(10 个预设 + admin 自建 custom),取 `theme_key` 作为发文 `theme` 参数。

```bash
curl https://yyps.net/api/publish/themes/public
# {"data":[{"theme_key":"blue","theme_name":"科技蓝",...},{"theme_key":"ink-green","theme_name":"墨绿",...},{"theme_key":"<custom>",...}]}
```

### 7.1 账号默认主题查询(本地渲染取色)

`GET https://yyps.net/api/mp/account-theme?account=<账号key>` (API Key 认证:`x-api-key`)

返回该账号当前生效主题(admin「账号管理」配置的默认主题;未配置时 `theme_key`/`theme_config` 为 `null`)。
`content_format=html` 的调用方(**本地渲染**)应先取本接口 → 用 `theme_config` 本地渲染着色 → 再发布,
实现「账号配置主题 → 生成文章生效」。注意:服务端不对 html 做主题着色(主题仅对 markdown 生效);发布时另有微信兼容改写(列表/外链/加粗兜底,与主题无关)。

```bash
curl -H "x-api-key: $KEY" "https://yyps.net/api/mp/account-theme?account=ailang"
# {"success":true,"account":"ailang","theme_key":"blue","theme_name":"科技蓝","theme_config":{"h1_color":"#177dd4",...}}
```

## 8. 失败与降级

- 发文**始终同步返回**(不会因 AI 封面生成超时阻塞——AI 在后台)。
- `cover=ai` 的 AI 生成失败**不阻断发文**,草稿保留兜底封面(可在 admin 发布历史点"重新生成封面"重试)。
- 账号未配任何封面 → 友好报错"账号未配封面,请配默认封面或传 thumb",不会裸抛微信 40007。

## 12. 渲染接口(公开,无需认证)

`POST https://yyps.net/api/render` —— Markdown / IR → 微信兼容 HTML。

- 请求:`content`(Markdown) 与 `ir`(render-ir/v1 结构化 IR) 二选一;`theme` 传主题库 key——
  内置 `default`/`blue`/`green`/`dark` + 系统精选 `indigo`/`soft-blue`/`haze-blue`/`slate-blue`/`violet`/`ink-green`,
  亦支持公开自建 custom;缺省 `default`,未知名回落 `default`;`custom_config` 内联主题配置(优先于 `theme`)。
- 响应:`{"success":true,"html":"<section>…</section>","message":"渲染成功"}`
- 说明:该接口只做渲染,**不过发布链路的微信兼容改写**;完整说明见仓库 `docs/render-api.md`。

```bash
curl -X POST https://yyps.net/api/render -H "Content-Type: application/json" \
  -d '{"content":"## 标题\n\n正文**加粗**","theme":"blue"}'
```
- `success:false` 时 `message` 含原因,可直接展示给调用方。

## 9. 小程序通道 · 概述与鉴权

小程序通道把小程序的**服务端能力**(订阅消息发送、内容安全校验)以纯通道形式开放:小程序 AppSecret 由本服务加密托管,调用方**不持小程序 secret**,只经 HTTP 调用(密钥单点真相)。

- 鉴权与公众号发布同体系:`x-api-key` header(auth 服务签发的 service key)。无效/缺失 → 401。
- **权限码**:与发布接口不同(发布仅需有效 key),小程序通道两端点要求 key 持有对应权限码(auth 侧签发时勾选),缺失 → 403:

| 端点 | 权限码 |
| --- | --- |
| `POST /api/miniprogram/subscribe/send` | `subscribe:send` |
| `POST /api/miniprogram/sec-check` | `sec_check:check` |

> 持 `admin:manage` 的 key 天然豁免权限检查。

- **appid 前提**:请求携带的 `appid` 须为本服务已注册且启用的 active 小程序账号,未注册/停用 → 403。
- **量护栏**:每 key 1 小时滑动窗口计数,成功+失败都计,超限 → 429(调用方按通道异常处理,fail-open)。护栏防 key 泄露后被刷量:订阅发送 3000 次/h(正常量≈订阅用户数/天);内容安全校验 1000 次/h(每次调用消耗微信 msgSecCheck 配额)。

## 10. 订阅消息发送接口

`POST https://yyps.net/api/miniprogram/subscribe/send` (Content-Type: application/json)

向单个用户发送一条小程序订阅消息(如每日简报提醒)。接收者须已订阅该模板,否则微信拒绝(HTTP 200 + `reason:"no_credit"`)。

| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `appid` | 是 | 小程序 AppID(须已注册 active) |
| `template_id` | 是 | 订阅消息模板 ID(须挂在该小程序账号下,否则 400) |
| `touser` | 是 | 接收者 openid |
| `data` | 是 | 模板数据,按微信字段名:`{"thing2":{"value":"…"},"thing4":{"value":"…"}}` |
| `page` | 否 | 点击卡片落点,如 `pages/index/index?from=submsg` |
| `dedupe_key` | 建议 | 幂等键(≤200 字符)。同键重复请求不重复发送,直接回上次结果(`duplicate:true`) |
| `miniprogram_state` | 否 | 跳转小程序版本 `formal`(默认)/`trial`/`developer` |
| `timestamp` / `nonce` | 建议 | 防重放(timestamp 5 分钟内,nonce 一次性) |

**响应**:`{success, message, duplicate, errcode?, reason?, code?}`

```json
{"success":true,"message":"发送成功","duplicate":false,"errcode":0,"reason":"ok"}
```

状态码约定:**微信业务拒绝走 HTTP 200 + `success:false` + 语义化 `reason`**(这是正常业务结果,不是通道故障);参数/权限错误 4xx(`code` 与 HTTP 状态码一致);上游故障 5xx。

| reason | 含义 |
| --- | --- |
| `no_credit` | 用户未订阅或订阅额度已用尽(微信 43101),未送达 |
| `invalid_openid` | openid 无效(微信 40003) |
| `data_invalid` / `invalid_page` | 模板数据 / 落点页不合法 |
| `rate_limited` | 触发微信频率限制(45009),稍后重试 |
| `unauthorized` | 该账号无权调用(48001) |

```bash
curl -X POST https://yyps.net/api/miniprogram/subscribe/send \
  -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"appid":"wx0123456789abcdef","template_id":"AbCdEfGh...","touser":"oXYZ123...","data":{"thing1":{"value":"今日简报已生成"}},"dedupe_key":"daily-brief:2026-09-17:oXYZ123"}'
```

## 11. 内容安全校验接口

`POST https://yyps.net/api/miniprogram/sec-check` (Content-Type: application/json)

校验一段文本是否合规(微信 msgSecCheck 2.0)。纯通道:业务策略(何时校验、校验哪些文本)由调用方决定。

| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `appid` | 是 | 小程序 AppID(须已注册 active) |
| `content` | 是 | 待校验文本;超 2000 字符服务端截断(中文按字符计),长文本请自行拆分或只传关键字段 |
| `openid` | 建议 | 用户 openid。**微信 2.0 接口必填**(用户须近两小时访问过小程序);缺失时微信报错 → 502,调用方按 fail-open 放行 |

**响应**:`{pass, label?, keyword?, reason?, errcode?}`

```json
{"pass":false,"label":"20006","keyword":"赌博","reason":"blocked"}
```

| 场景 | HTTP | 响应 |
| --- | --- | --- |
| 校验通过 | 200 | `{"pass":true,"reason":"ok","errcode":0}` |
| 命中违规 | 200 | `pass:false` + `label`/`keyword`(内容结论,**应拦截**) |
| 空文本 | 200 | `pass:true`(无内容可校验) |
| 缺权限 / appid 未注册 | 403 | `reason:"permission_denied"` / `"appid_not_allowed"` |
| appid 为空 | 400 | `reason:"invalid_appid"` |
| 微信侧异常 | 502 | `reason:"wechat:<errcode>"` 或 `"service_error"` |
| 量护栏超限 | 429 | `reason:"rate_limited"`(通道状态,fail-open 放行) |

**核心约定:只有 HTTP 200 + `pass:false` 是内容结论;其余(403/429/502 等)都是通道状态,调用方应 fail-open 放行**——内容安全闸门不因服务抖动误伤正常业务。

```bash
curl -X POST https://yyps.net/api/miniprogram/sec-check \
  -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"appid":"wx0123456789abcdef","content":"分享卡片标题文本","openid":"oXYZ123..."}'
```
