---
name: wjs-voicedrop
description: VoiceDrop 账号的完整 API 工具箱——文章(list/read/write/版本/删除/边车/标签)、文风(读写/版本/语料偷师 collect/dataset/服务端蒸馏 extract/单篇重挖 restyle)、照片/音频(列出/上传/下载)、触发挖矿(mine)、算力余额与账单、公开分享链接、发公众号草稿、社区(分享/浏览/回复/举报)。认证用用户自己的 token 且自带登录：6+4 手机设备配对(本 skill 内置 vd-login.mjs，自己就能登录)、App 复制的 anon token，或 Apple 登录 session。别名："vd"、"voicedrop"、"口述" 都指这个 app（口述 = VoiceDrop 这款语音口述 app），用户提到这三个词中任意一个、且涉及录音/文章/文风/照片/挖矿/算力/社区等操作，都用本 skill。触发词："vd"、"voicedrop"、"口述"、"口述 app"、"voicedrop api"、"voicedrop 登录"、"列出 voicedrop 文章/照片/录音"、"读/写 voicedrop 文章"、"voicedrop 触发挖矿"、"voicedrop 算力余额"、"蒸馏文风"、"偷师"、"重新挖"、"voicedrop 社区"、"/wjs-voicedrop"。
---

# VoiceDrop API Skill

> **安装**：本 skill = 这个目录。把它放到 `~/.claude/skills/wjs-voicedrop/` 即装好——`git clone https://github.com/jianshuo/claude-skills` 后 `cp -R wjs-voicedrop ~/.claude/skills/`（或直接对 Claude Code 说「安装 https://github.com/jianshuo/claude-skills/tree/main/wjs-voicedrop」）。登录脚本 `vd-login.mjs` 需 Node ≥ 20。

VoiceDrop 后端的完整 HTTP 接口工具箱。所有资源（文章、文风、照片、音频）都能列出、读、写；还能触发挖矿、蒸馏文风、单篇重挖、查算力、发公众号、逛社区。**先认证拿 `$TOKEN`，再调任意接口。**

两个 base URL：

| 服务 | Base URL | 提供 |
|---|---|---|
| Files API（Cloudflare Pages） | `https://jianshuo.dev/files/api` | 文件/文章/文风(含语料)/照片/音频/分享/公众号/社区/Apple 登录 |
| Agent Worker（Durable Objects） | `https://jianshuo.dev/agent` | 挖矿触发、文风蒸馏、单篇重挖、算力余额/账单、语音编辑(WS)、库指令(WS)、设备配对 |

---

## 认证（用户 token，优先 6+4 设备配对）

所有接口都要 `Authorization: Bearer $TOKEN`，例外是几个公开端点：`GET /files/api/photo/*`（公开照片）、`GET /files/api/asset/wechat-covers/*`（公众号封面图库）、`GET /voicedrop/<id>`（分享页 HTML，`?s=<index>` 选第几节）。token 取用户自己的凭证：

```bash
# 6+4 手机设备配对登录（本 skill 自带的 vd-login.mjs 存下来的用户身份）
CRED=~/.config/voicedrop/credentials
TOKEN=$(python3 -c "import json;print(json.load(open('$CRED'))['token'])")
SCOPE=$(python3 -c "import json;print(json.load(open('$CRED'))['scope'])")   # users/anon-xxxx/
```

**取 token 的方式（都只能访问自己 scope 的数据）：**

| 方式 | token 形态 | scope（能看谁） | 怎么拿 |
|---|---|---|---|
| **6+4 设备配对** | `anon_…`（该用户的完整密钥） | 该用户自己 `users/anon-<hash>/` | 下面「自助登录」流程，凭证落 `~/.config/voicedrop/credentials` |
| **App 临时** | `anon_…` | 该用户自己 | App 设置 → 账户/访问令牌 → 复制 |
| **Apple 登录 session** | JWT（HS256，带 `apple` 标记） | 该用户自己 `users/<sub>/` | `POST /files/api/auth/apple`，body `{identityToken}`（App 内 Sign in with Apple 后端换发）→ `{session,scope}`。**写社区必须用这种** |
| **24h 只读 token** | 短期 token（`ro:true`） | 该用户自己，仅 `list`/`download` | `GET /files/api/token/articles` → `{token,url,expires_in:86400}`，用于把文章列表只读地交给外部工具；干别的一律 403 `read-only token` |

### 6+4 自助登录（本 skill 自带，无需别的 skill）

本目录自带 `vd-login.mjs`（零依赖，Node ≥ 20）——它扮演设备配对协议里的「新设备」，和手机（老设备）配一次对，把账号密钥**端到端加密**取到本地。**先查是否已登录**，没有再走两步握手：

```bash
VD=~/.claude/skills/wjs-voicedrop/vd-login.mjs
node "$VD" status        # 已登录 → {"ok":true,"scope":"users/anon-…/","live":true}；未登录 → ok:false
```

**前提**：用户手机在线、app 在前台、已登录目标账号、是支持设备配对的版本。

**两步握手（每条子命令只打印一行 JSON，按 `ok` 解析，别抓散文）：**

1. 问用户要 **6 位十六进制码**（手机 **设置 → 账户** 里那串短 ID）。
2. `node "$VD" start <6hex>`：
   - `{"ok":true,"pairingId":…,"matchCount":N}` → 告诉用户**手机这会儿会弹一个 4 位数字码**。
   - `{"ok":false,"error":"no_match"}` → 6 位码错了，或手机离线/后台/旧版本 → 回第 1 步。
3. 问用户要**手机上弹出的 4 位数字码**。
4. `node "$VD" finish <4digit>`：
   - `{"ok":true,"scope":"users/anon-…/"}` → 登录成功，凭证已写 `~/.config/voicedrop/credentials`（0600）。
   - `{"ok":false,"error":"wrong_code","remaining":N}` → 码输错了，问对的再重跑 `finish`（配对还活着，共 5 次 / 2 分钟）。
   - `{"ok":false,"error":"expired"|"too_many_attempts"|"timeout"|"cancelled"}` → 从第 1 步重来。

其它子命令：`node "$VD" logout`（删凭证）。

**安全须知（用户没听过就说一遍）**：这是把账号的**完整密钥**拷到磁盘，**不是**可吊销的子 token（VoiceDrop 的 anon 身份签不出子 token）。谁拿到 `~/.config/voicedrop/credentials` 就有该账号全部权限，且无法单独吊销这台机器。保持文件私有，**绝不**提交或同步到任何可读处。

| 登录报错 | 原因 / 处理 |
|---|---|
| `no_match` | 6 位码错，或手机离线/后台/旧版本 |
| `wrong_code` + `remaining` | 4 位码输错，用对的重跑 `finish`（5 次 / 2 分钟） |
| `timeout` | 手机没响应——把 app 切到前台，从 `start` 重来 |
| `cancelled` | 用户在手机上点了「不是我」 |
| `expired` | `start` 到 `finish` 超过 2 分钟，重跑 `start` |

**key 写法**：所有 key 都是相对自己 scope 的——文章 stem 直接写 `VoiceDrop-xxx`，文件名直接写 `photos/...`；服务端自动拼上你的 `users/anon-<hash>/` 前缀，越不出自己的数据。

---

## 全部接口速查

| 资源 | 操作 | 方法 + 路径 |
|---|---|---|
| **文章** | 列出 | `GET /files/api/articles` |
| | 读 | `GET /files/api/articles/<stem>` |
| | 写（版本化） | `PUT /files/api/articles/<stem>` |
| | 版本历史 | `GET /files/api/articles/<stem>/history` |
| | 切版本(撤销/重做) | `PATCH /files/api/articles/<stem>/head` |
| | 删除(连边车) | `DELETE /files/api/articles/<stem>` |
| | 写 SRT 边车 | `PUT /files/api/articles/<stem>/srt` |
| | 标记无语音 | `PUT /files/api/articles/<stem>/empty` |
| | 标记算力不足 | `PUT /files/api/articles/<stem>/blocked` |
| **文风** | 读 | `GET /files/api/style` |
| | 写（版本化） | `PUT /files/api/style` |
| | 版本历史 | `GET /files/api/style/history` |
| | 切版本(撤销/重做) | `PATCH /files/api/style/head` |
| **文风语料(偷师)** | 收一条样本 | `POST /files/api/style/collect` |
| | 语料清单 | `GET /files/api/style/dataset` |
| | 清空语料 | `DELETE /files/api/style/dataset` |
| **蒸馏/重挖** | 服务端蒸馏文风 | `POST /agent/style/extract` |
| | 单篇按某版文风重挖 | `POST /agent/restyle` |
| **照片** | 列出 | `GET /files/api/list`（筛 `photos/`） |
| | 上传 | `PUT /files/api/upload/photos/<sessionTs>/<offset>-<rand>.jpg` |
| | 下载(私有) | `GET /files/api/download/photos/<...>.jpg` |
| | 下载(公开) | `GET /files/api/photo/<完整 R2 key>`（无需 token） |
| **音频** | 列出(推荐) | `GET /files/api/recordings`（索引直出，~0.5s，含状态位） |
| | 列出(通用) | `GET /files/api/list`（筛 `VoiceDrop-*.m4a`，全量 ~2.5s） |
| | 上传 | `PUT /files/api/upload/VoiceDrop-<...>.m4a`（自动触发挖矿） |
| | 下载 | `GET /files/api/download/VoiceDrop-<...>.m4a` |
| **挖矿** | 触发 | `POST /agent/mine/trigger`（推荐）或 `POST /files/api/mine` |
| **算力** | 余额 | `GET /agent/usage/balance` |
| | 账单流水 | `GET /agent/usage/ledger?limit=N` |
| **分享** | 生成公开链接 | `GET /files/api/share/articles/<stem>.json` |
| **公众号** | 发/更新草稿 | `POST /files/api/wechat/articles/<stem>.json` |
| | 封面图库(公开) | `GET /files/api/asset/wechat-covers/` |
| **社区** | 分享文章到社区 | `POST /files/api/community/share/articles/<stem>.json`（需 Apple 登录） |
| | 信息流 | `GET /files/api/community/list` |
| | 读一条(含全文/照片) | `GET /files/api/community/get/<shareId>` |
| | 回复列表 | `GET /files/api/community/replies/<shareId>` |
| | 某文章是否已分享 | `GET /files/api/community/shared/articles/<stem>.json` |
| | 撤下自己的分享 | `POST /files/api/community/unshare/<shareId>`（需 Apple 登录） |
| | 举报 | `POST /files/api/community/report/<shareId>` |
| **身份** | 我是谁 | `GET /files/api/whoami` |
| | Apple 登录换 session | `POST /files/api/auth/apple` |
| | 领 24h 只读 token | `GET /files/api/token/articles` |
| **通用文件** | 删除任意文件 | `DELETE /files/api/file/<name>` |
| **UI 配置** | 长按菜单配置 | `GET /agent/ui-config` |

---

## 文章 articles（版本化 CRUD）

**列出**（最新在前）：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/articles
# → {"articles":[{stem,title,head,createdAt,updatedAt,count,tags?}]}   count=节数, head=当前版本号
#   tags=标签数组（doc 顶层字段，语音 tag_article 工具写入，可 remove:true 删）；无标签时字段不出现
```

格式化展示：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/articles | python3 -c "
import json,sys; from datetime import datetime
for a in json.load(sys.stdin).get('articles',[]):
    dt=datetime.fromtimestamp(a['createdAt']/1000).strftime('%Y-%m-%d')
    print(f\"  [{dt}] {a['title']}  (stem={a['stem']}, {a['count']} 节, v{a['head']})\")"
```

**读全文**：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/articles/<stem>
# → {transcript, srt, articles:[{title,body}], createdAt, updatedAt, status, model, ...}
#   body 是 Markdown 正文；[[photo:<relkey>]] 是内嵌照片标记
```

**写**（版本化——每次 PUT 追加一个新版本，head 前移）：

```bash
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"articles":[{"title":"新标题","body":"新正文..."}]}' \
  https://jianshuo.dev/files/api/articles/<stem>
# → {ok:true, head:<新版本号>}
```

**版本历史 / 切版本（撤销重做）**：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/articles/<stem>/history
# → {head, versions:[...]}
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"head":2}' https://jianshuo.dev/files/api/articles/<stem>/head     # 只移指针，不新增版本
```

**删除**（连 `.srt/.empty/.blocked/.tags` 边车一起删，**不删音频**）：

```bash
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/articles/<stem>
```

边车写入：`PUT .../articles/<stem>/srt`（body=SRT 文本）、`/empty`（body `{"reason":"no-speech"}`）、`/blocked`（body `{"reason":"no-credit"}`）。

**标签 tags**：HTTP 侧只读——列表接口带出 `tags` 数组；写入没有专门端点，靠语音库指令（`/agent/command` WS 里的 `tag_article` 工具，可 `remove:true` 删）。要用 HTTP 改标签，只能 `PUT` 整篇文章 doc 时带上顶层 `tags` 字段。

---

## 文风（版本化 /style）

挖矿时这份文风会被叠加进 system prompt。存储已升级为**版本化的 `CLAUDE.json`**（schema-3，和文章同一套 history / undo / 回滚），统一走 `/files/api/style`——别再用旧的 `download/upload CLAUDE.md`。

> **名字暂留 `CLAUDE.md`**：`# 我的名字` 仍由旧 `CLAUDE.md` 保管（作者抽取路径读它）；写入只往 `CLAUDE.json` 写文风正文，不再碰名字。端点名字里仍叫 `CLAUDE.md` 只是历史包袱，实际读写的是 `CLAUDE.json`。

**读当前文风**：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/style
# → {style, name, styles, head, createdAt, updatedAt, default?}
#   name=作者名；styles=App 里选中的预设风格编号数组(≤3)；default:true=还没存过、返回的是默认王建硕文风
# 还没存过 CLAUDE.json、只有旧 CLAUDE.md → {style, name, head:0, legacy:true}
#   legacy:true = 读的是旧 md 的「# 我的文风」段；首次 PUT 即落 CLAUDE.json、旧 md 退役
# 两者都没有 → 404
```

**写（版本化——每次 PUT 追加一个新版本，head 前移）**：

```bash
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"style":"单句成段，段落 1–3 句居多；多用具体数字……"}' \
  https://jianshuo.dev/files/api/style
# → {ok:true, head:<新版本号>}    空 style → 400 empty_content
#   body 可选带 "source":"agent"（默认用户 token 记为 app）
#   body 还可带 "name"（作者名）和 "styles"（≤3 个预设风格编号）——只改这两个（不带 style）是改 profile，不新增版本
```

**版本历史 / 回滚（撤销重做，只移 head 指针、不新增版本）**：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/style/history
# → {head, versions:[{v, savedAt, source, style}]}    oldest-first，最多留 10 版
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"head":2}' https://jianshuo.dev/files/api/style/head      # 回滚到 v2
# → {ok:true, head:2}    版本号不存在 → 404
#   注意：回滚到旧版后再 PUT，会先截掉 head 之后的「未来」版本再追加新版（git HEAD 式）
```

### 文风语料（偷师）+ 服务端蒸馏 + 单篇重挖

App 的「偷师」功能：先往语料库收样本，攒够了让服务端一键蒸馏成新文风版本。

```bash
# 收一条语料样本（type/title/source 可选）
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"type":"article","title":"样本标题","text":"样本正文……","source":"web"}' \
  https://jianshuo.dev/files/api/style/collect
# → {ok:true, id}    空 text → 400 empty_text    （落在 <scope>style/<id>.json）

# 看语料清单（只有元数据，不含正文）
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/style/dataset
# → {items:[{id,type,title,chars,source,collectedAt}], count, totalChars}

# 清空语料库
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/style/dataset
# → {ok:true, deleted:N}

# 服务端蒸馏：把语料库蒸成新文风（后台跑，落成 CLAUDE.json 新版本 + 自动生成一篇「你的写作风格」介绍文章）
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"clearAfter":true}' https://jianshuo.dev/agent/style/extract
# → {ok:true}（异步）   语料不够 → 400 insufficient-corpus {totalChars,min} / 400 empty-dataset / 500 distill-failed

# 单篇重挖：用某个文风版本把一条录音重新挖一遍（同步，等结果）
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"stem":"VoiceDrop-2026-07-02-...","styleV":3}' https://jianshuo.dev/agent/restyle
# → 200 重挖结果；失败 → 422；参数错 → 400 bad-request    （styleV 不传=当前 head）
```

---

## 照片 photos

没有专门的「列照片」接口——用通用 `GET /list` 筛 `photos/`：

```bash
# 列出本账号所有照片（最新在前，按 uploaded 真实时间）
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/list \
  | python3 -c "import json,sys
ph=[f for f in json.load(sys.stdin)['files'] if f['name'].startswith('photos/') or '/photos/' in f['name']]
ph.sort(key=lambda f:f.get('uploaded',''), reverse=True)   # R2 上传时间倒序=最新在前
[print(f.get('uploaded',''),f['name'],f['size']) for f in ph]"

# 下载（私有，scoped）
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://jianshuo.dev/files/api/download/photos/<sessionTs>/<offset>-<rand>.jpg" -o out.jpg

# 下载（公开，无需 token——任何展示面都走这个；key 必须是完整 R2 key）
curl -s "https://jianshuo.dev/files/api/photo/users/<sub>/photos/<sessionTs>/<offset>-<rand>.jpg" -o out.jpg

# 上传（≤1200px 方形 JPEG）
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg \
  "https://jianshuo.dev/files/api/upload/photos/<sessionTs>/<offset>-<rand>.jpg"
```

**命名约定**：`photos/<sessionTs>/<offset>-<rand>.jpg`。`sessionTs`=录音开始的 `yyyy-MM-dd-HHmmss`；`offset`=距录音起点的整数秒；`<rand>`=3 位 base36 防同秒撞 key。正文里引用照片用 `[[photo:photos/<sessionTs>/<offset>-<rand>.jpg]]`（token 就是相对 key）。

---

## 音频 audio（录音）

**首选专用 `GET /recordings`**（2026-07-13 起）：索引直出（~0.5s，别再全量 `/list` 了），每条自带四个状态位，App「我的录音」同款数据源：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/recordings
# → {recordings:[{name,uploaded,hasArticles,isEmpty,blocked,hasTags}]}
#   hasArticles=已成文  isEmpty=无语音  blocked=算力不足/过长  hasTags=挖矿前预置标签
#   不排序——要最新在前就按 uploaded 倒序（ISO-8601 UTC，字典序==时间序）
```

老办法（通用 `GET /list` 筛 `VoiceDrop-*.m4a`，全量 ~2.5s，仅当需要连其他文件一起看时用）：

> **顺序**：`/articles` 端点服务端就按 `createdAt` 倒序（最新在前）。`/list` 是通用接口**不排序**，返回 R2 原始字典序。**别按文件名排**——名字里的时间戳不是可靠时钟（时钟偏差、staging/改名、导入的文件都可能对不上）；要排就按 `uploaded`（R2 上传时间，ISO-8601 UTC 字符串，字典序==时间序）。下面的例子已按 `uploaded` 倒序，和 App「我的录音」一致。

```bash
# 列出所有录音（最新在前，按 uploaded 真实时间）
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/list \
  | python3 -c "import json,sys
recs=[f for f in json.load(sys.stdin)['files'] if f['name'].endswith('.m4a')]
recs.sort(key=lambda f:f.get('uploaded',''), reverse=True)   # R2 上传时间倒序=最新在前
[print(f.get('uploaded',''),f['name'],f['size']) for f in recs]"

# 下载
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://jianshuo.dev/files/api/download/VoiceDrop-2026-06-20-143052-...m4a" -o rec.m4a

# 上传（leaf 是 VoiceDrop-*.m4a → 自动触发挖矿）
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: audio/mp4" \
  --data-binary @rec.m4a \
  "https://jianshuo.dev/files/api/upload/VoiceDrop-<ts>-<dur>-<weekday>-<period>.m4a"
```

**命名约定**：`VoiceDrop-<ts>-<dur>-<weekday>-<period>[-<city>-<district>].m4a`（全 ASCII）。**处理状态**靠边车判断：`articles/<stem>.json` 存在=已成文；`.empty`=无语音；`.blocked`=算力不足/录音过长。

---

## 操作

**触发挖矿**（处理待处理录音）：

```bash
# 推荐：Worker 直连，任何有效 token 都行
curl -s -X POST -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/agent/mine/trigger
# → 202 queued（Miner DO 接单；服务端每 6 小时也有 cron 自动扫一遍）
# 备用：Pages 转发
curl -s -X POST -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/mine   # → {ok:true}
```

完整挖矿语义（ASR→Claude→多文章）见 `/wjs-mining-voicedrop`。

**算力余额 + 账单**：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/agent/usage/balance
# → {suanli, yuan, granted_suanli, spent_suanli}   （23 算力 = ¥1）
curl -s -H "Authorization: Bearer $TOKEN" "https://jianshuo.dev/agent/usage/ledger?limit=50"
# → {entries:[{ts(毫秒), kind:grant|spend, reason:signup|asr|mine|edit|image-edit|campaign:*, suanli, balance_suanli, detail}]}
# 注意：usage 子系统 fail-soft——内部出错时返回 200 {error:"usage-unavailable",degraded:true}，别当成余额为 0
```

**生成公开分享链接 / 发公众号草稿**：

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/share/articles/<stem>.json
# → {url:"https://jianshuo.dev/voicedrop/<id>"}    分享页可加 ?s=<index> 直达第几节
curl -s -X POST -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/wechat/articles/<stem>.json
# → {ok,created,updated} / 409 wechat_not_configured / 502 {errcode,errmsg}
# 公众号封面图库（公开）：GET /files/api/asset/wechat-covers/ → {covers}；GET /files/api/asset/wechat-covers/<名字> 取图
```

---

## 社区 community（VD 社区信息流）

读随便看（任何有效 token）；**写（share/unshare）必须是 Apple 登录的 session**，否则 403 `needs_apple_signin`。分享出去的是「指针」——社区帖实时读你文章的当前版本，改文章社区跟着变。

```bash
# 信息流（最新在前；mine=true 是自己发的；replyTo 有值=这是条回复）
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/community/list
# → {posts:[{shareId,author,title,firstSharedAt,updatedAt,count,mine,replyTo?}]}

# 读一条（全文 + 照片 + 作者）
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/community/get/<shareId>
# → 帖子 + articles + owner + photos    404 not found

# 某条帖子的回复（最旧在前）
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/community/replies/<shareId>

# 把自己一篇文章分享到社区（body 可带 {"replyTo":"<shareId>"} 作为回复）
curl -s -X POST -H "Authorization: Bearer $APPLE_SESSION" \
  https://jianshuo.dev/files/api/community/share/articles/<stem>.json
# → {ok,shareId}    403 needs_apple_signin / 403 content_flagged（关键词审核没过）/ 400 empty article

# 查自己某篇是否已分享 / 撤下 / 举报
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/community/shared/articles/<stem>.json   # → {shared,shareId?}
curl -s -X POST -H "Authorization: Bearer $APPLE_SESSION" https://jianshuo.dev/files/api/community/unshare/<shareId>   # 仅限自己的帖，403 not owner
curl -s -X POST -H "Authorization: Bearer $TOKEN" -d '{"reason":"垃圾内容"}' https://jianshuo.dev/files/api/community/report/<shareId>   # 立即隐藏待审
```

---

## 工作流：distill —— 蒸馏文风并上传

> 现在有**两条路**：① App 同款的服务端一键蒸馏——`POST /style/collect` 攒语料 → `POST /agent/style/extract`（见上面「文风语料」小节），交给服务器后台跑；② 下面的本地手工蒸馏——自己挑文章、自己派子 agent 提炼、预览确认后 PUT，可控性最强。

从样本文章提炼可执行的「文风规则」，写进 `CLAUDE.md`，让 miner 自动带上。

1. `GET /articles` 列出 → 请用户选 **3–6 篇**（<3 篇提醒指纹不稳）。
2. 逐篇 `GET /articles/<stem>`，取 `articles[*].body`（不要 `transcript` 口述原文）。
3. 派子 agent 分析（隔离上下文）：「提炼这位作者**最突出、可执行**的语言习惯：句长偏好、段落密度、人称、词汇倾向、论证方式、结尾习惯、绝不做的事。**只分析怎么写，不分析思想立场。**输出 15–20 条 bullet，每条一句，用『用 X』『不用 Y』『X 必须在 Y 前』这种可执行语言。」
4. 回收润色：每条要能被不认识该作者的模型直接照做（「喜欢短句」太模糊；「单句成段，段落 1–3 句居多」才够用）。去掉与服务器 SYSTEM prompt 重复的。
5. 组装文风正文，**预览给用户确认后**用上面的 `PUT /files/api/style`（JSON body `{"style":"…"}`）上传；落为新版本，随时可 `PATCH /style/head` 回滚。（名字不在这里写——它仍留在旧 `CLAUDE.md`。）
6. 成功 → 告知「下次录音挖文章时自动生效」。

---

## 实时接口（WebSocket，了解即可，curl 调不动）

- `wss://jianshuo.dev/agent/edit?stem=<stem>` — 语音编辑某篇文章（App 持麦克风串行发指令；发 `{type:"instruct",text,images?,articleIndex?}`，收 `snapshot/status/updated/reply/error`；每篇上限 100 次编辑，算力不足回 `no-credit`）。
- `wss://jianshuo.dev/agent/command` — **库级语音指令**（LibraryAgent DO）：跨文章操作——合并、删除、重挖、打标签（`tag_article`）、改文风等；删这类破坏性操作有 `confirm/cancel` 二次确认往返。
- `wss://jianshuo.dev/agent/status` — 实时挖矿状态推送（待处理→听录音→挖文章→已成文）。
- `wss://jianshuo.dev/agent/asr` — 火山流式 ASR 代理（语音听写）。
- `/agent/link/*`（`start/verify/complete/cancel` + `wss …/link/socket`） — 设备配对（6+4）协议端点，由本 skill 自带的 `vd-login.mjs` 封装（见上面「6+4 自助登录」）。

---

## Admin 专用端点（需要 `FILES_TOKEN`，普通用户 token 调不动）

日常用不到，列出来备查（管理员 = Cloudflare 环境变量 `FILES_TOKEN` 持有者）：

| 端点 | 用途 |
|---|---|
| `GET /files/api/articles/<sub>`、`/style/<sub>/...` | 以任意用户身份读写文章/文风（路径多一段用户 sub） |
| `GET /files/api/community/reports` / `POST /files/api/community/resolve/<shareId>` | 看举报队列 / 裁决（body `{action:"remove"\|"restore"}`） |
| `POST /agent/usage/grant`、`POST /agent/usage/grant/batch` | 给用户送算力（可 `expire_days` 过期、`all:true` 全员） |
| `GET /agent/usage/admin/accounts` | 全部账户余额一览 |
| `GET /files/api/llmlog/dates`、`/llmlog/list?date=`、`/minelog/dates`、`/minelog/list?date=` | LLM 调用日志 / 挖矿日志 |
| `GET /agent/llm-health` | Anthropic 直连 vs ENAM relay 健康探针 |
| `POST /agent/notify` | 手动往某用户 StatusHub 推状态 |
| `GET`/`PUT /agent/prompt-registry`、`/agent/prompt-lab/*` | 挖矿 prompt 调优台（prompt.jianshuo.dev 用） |
| `POST /agent/paint-callback` | paint.jianshuo.dev 画图回调（专用 `PAINT_CALLBACK_TOKEN`，不是给人调的） |

---

## 常见错误

| 错误 | 修正 |
|---|---|
| `401 unauthorized` | token 没设/过期 → 重走上面「6+4 自助登录」，或 App 设置重新复制 |
| `403 forbidden` / `read-only token` | 用户 token 只能动自己 scope；24h 临时 token 只能 list/download |
| `403 needs_apple_signin` | 社区 share/unshare 必须用 Apple 登录的 session token，anon token 不行 |
| `403 content_flagged` | 社区分享被关键词审核拦下（返回 `{categories}` 或 `{term}`） |
| `articles` 返回空数组 | 还没成文 → 触发挖矿或等 miner |
| `404 not found`（文章） | stem 写错 |
| `409 wechat_not_configured` | 该用户没配公众号 appid/secret（App 设置里填） |
| 照片 `400 not a photo` | `/photo/` 公开接口的 key 必须匹配 `users/<id>/photos/*.(jpg|jpeg|png)` |
| `400 insufficient-corpus` / `empty-dataset` | 语料不够蒸馏——先 `POST /style/collect` 攒够 `min` 字数 |
| `restyle` 回 422 | 重挖失败（看 body 里的原因；styleV 是否存在、录音是否还在） |
| usage 接口回 `degraded:true` | 算力子系统暂不可用（fail-soft），不是余额为 0，稍后重试 |
| 上传文风后 App 没变 | 切离再切回「设置」tab 重新加载 |
