# 发布台 · 个人 API / MCP 接入

服务：https://fabu.uwenai.com

这是你一个人使用的内容接力服务。提交标题、正文、话题与素材后获取二维码，在手机浏览器打开并点击底部常驻的「发布到小红书」。小红书内最终检查和确认发布由你完成。用户已于 2026-10-22 反馈直接跳转可用；未记录具体手机/浏览器版本，也不据此保证所有版本、视频类型均兼容。服务不检测最终发布结果。

## 1. 个人 Key

个人 API Key 单独生成，保存在服务器 `/etc/fabu/env` 的 `API_KEYS_JSON`，与你原有的网页工作台 Key 分开，访问的是同一个人的任务。两种 Key 都可用于 REST / MCP；手机任务链接里的临时令牌不能连接 MCP，也不能列举所有任务。

调用时只使用请求头：

```text
Authorization: Bearer YOUR_PERSONAL_KEY
```

Key 不应放在链接、查询参数或工具参数里。HTTPS 强制启用。接入文档和网页不返回真实 Key。Key 丢失时可在可信终端查看 `/etc/fabu/env`；需要撤销时替换 `API_KEYS_JSON` 中对应值并重启 `fabu`，原网页登录 Key 和任务数据保留。

## 2. REST：上传图片，生成二维码

```sh
export FABU_API_KEY='你的个人 Key'
curl --fail-with-body https://fabu.uwenai.com/api/publish-tasks \
  -H "Authorization: Bearer $FABU_API_KEY" \
  -F 'title=周末的生活记录' \
  -F 'content=把今天值得记录的片段分享给你。' \
  -F 'topics=["生活记录","周末"]' \
  -F 'media_type=image' \
  -F 'media=@/absolute/path/01.jpg;type=image/jpeg' \
  -F 'media=@/absolute/path/02.png;type=image/png'
```

返回 HTTP 201：

```json
{
  "task_id": "随机 UUID",
  "publish_url": "https://fabu.uwenai.com/publish/?handoff=experimental#任务ID.临时令牌",
  "qr_data": "与 publish_url 相同的完整链接",
  "qrcode": "data:image/png;base64,...",
  "status": "ready",
  "expires_at": 1792800000
}
```

图片顺序就是 `media` 文件字段的顺序。`qrcode` 可直接作为图片 src；`qr_data` 可交给你自己的二维码生成器。请把 `#` 之后的完整内容一并保留，不能只用任务 ID 打开手机页面。

视频使用 `media_type=video`，只传一个 `media=@/path/video.mp4;type=video/mp4`。图片和视频不混传。不接收远程媒体 URL，先在调用端读取/上传本地文件，服务不会代为抓取 URL。

| 方法 | 路径 | 用途 |
| --- | --- | --- |
| POST | `/api/publish-tasks` | 上传文件创建任务 |
| GET | `/api/publish-tasks/` | 查看自己的最近 100 条任务 |
| GET | `/api/publish-tasks/{task_id}` | 查询内容、媒体及状态 |
| POST | `/api/publish-tasks/{task_id}/cancel` | 取消任务，重复操作幂等 |
| GET | `/api/publish-tasks/{task_id}/launch-plan?platform=ios或android` | 手机页面准备跳转链接；不会推进状态 |
| POST | `/api/publish-tasks/{task_id}/handoff?adapter=experimental` | 记录跳转意图；不表示 App 已打开、填充或发布 |

查询示例：

```sh
curl --fail-with-body https://fabu.uwenai.com/api/publish-tasks/ \
  -H "Authorization: Bearer $FABU_API_KEY"
```

## 3. MCP：连接地址与工具

- 传输：Streamable HTTP。
- 地址：`https://fabu.uwenai.com/mcp`（无须加尾部斜杠）。
- 鉴权：自定义 `Authorization: Bearer ...` 请求头。
- 这是个人静态 Key 鉴权，不提供 OAuth 登录或动态客户端注册；请使用支持自定义请求头的 MCP 客户端。

以下是常见客户端的 JSON 配置结构，外层配置字段请以客户端要求为准：

```json
{
  "mcpServers": {
    "fabu": {
      "url": "https://fabu.uwenai.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_PERSONAL_KEY"
      }
    }
  }
}
```

| 工具 | 用途 |
| --- | --- |
| `get_service_info` | 地址、限制、当前跳转能力 |
| `create_publish_task` | 以文件 base64 创建任务，返回结构化数据和真正的 PNG 二维码图片块 |
| `list_publish_tasks` | 查看自己的任务列表 |
| `get_publish_task` | 按 `task_id` 查询任务 |
| `cancel_publish_task` | 取消任务 |

创建工具参数：

```json
{
  "title": "周末的生活记录",
  "content": "把今天值得记录的片段分享给你。",
  "topics": ["生活记录", "周末"],
  "media_type": "image",
  "media": [
    {"mime_type": "image/jpeg", "data_base64": "文件字节的标准base64，不带data:前缀"}
  ]
}
```

MCP 单次所有媒体解码后的总量最多 **20 MiB**，单图仍最多 10 MiB，图文最多 9 张。较大的视频/图片集合请使用 REST multipart 上传，再用 MCP 查询其任务。不要把本地文件路径当成服务端路径传入工具。

Python 官方 SDK 2.3.0 调用示例（Python 3.10+，本项目部署使用 3.12）：

```python
import asyncio, base64, os
from pathlib import Path
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client

async def main():
    async with httpx2.AsyncClient(
        headers={"Authorization": "Bearer " + os.environ["FABU_API_KEY"]},
        timeout=180,
    ) as http:
        transport = streamable_http_client(
            "https://fabu.uwenai.com/mcp", http_client=http
        )
        async with Client(transport, mode="legacy") as client:
            result = await client.call_tool("create_publish_task", {
                "title": "周末记录", "content": "今天的一个小片段。",
                "topics": ["生活记录"], "media_type": "image",
                "media": [{"mime_type": "image/jpeg", "data_base64":
                    base64.b64encode(Path("01.jpg").read_bytes()).decode()}],
            })
            if result.is_error:
                raise RuntimeError("创建失败，请检查返回的错误说明")
            # 其中包含临时访问令牌，请勿写入公开日志。
            print(result.structured_content["publish_url"])
            for block in result.content:
                if block.type == "image":
                    Path("publish-qr.png").write_bytes(base64.b64decode(block.data))

asyncio.run(main())
```

依赖安装：`pip install mcp==2.3.0`。SDK 文档：[MCP Python SDK](https://py.sdk.modelcontextprotocol.io/)。

## 4. 限制与状态

- 标题 1–20 字；正文加话题最多 1000 字；话题最多 10 个，去掉前导 `#`，不含空格。
- 图片：1–9 张静态 JPEG、PNG、WebP，单张 ≤10 MiB、≤2500 万像素；服务会校正方向并转为去除 EXIF 的 JPEG。
- 视频：1 个 MP4，≤100 MiB，H.264 / 可选 AAC，≤10 分钟，最大宽高均 ≤3840；不做转码。
- 默认 24 小时失效。素材地址短期签名有效，取消/过期后拒绝新访问；已保存到手机的素材无法撤回。
- `ready`：可用；`handoff_started`：收到跳转意图；`awaiting_user_confirmation`：手动交接；其余 `cancelled` / `expired` / `failed` 为停止状态。没有 `published` 状态。
- 无 Key / 错误 Key 不能连接 MCP；REST 查询他人或无权限任务返回 404。参数错误 422、过期 410、超限 413、繁忙/限流 429、空间不足 507。
- MCP 上传和 REST 复用同一套媒体校验、权限、配额和清理逻辑。不会绕过安全校验或读取服务器任意本地文件。

手机浏览器点击底部按钮后，由系统处理跳转。微信请在系统浏览器中打开；未安装 App 或版本不兼容时，可展开其他发布方式继续复制、保存和手动发布。这个服务不自动点击小红书最终发布，也不伪造发布结果。
