| name | wjs-voicedrop |
| description | VoiceDrop 账号的完整 API 工具箱——读写文章(list/read/write/history/版本)、读写文风 CLAUDE.md、列出/上传/下载照片、列出/上传/下载音频录音、触发挖矿(mine)、查算力余额与账单、生成分享链接、发公众号草稿。认证用用户自己的 token 且自带登录:6+4 手机设备配对(本 skill 内置 vd-login.mjs,自己就能登录)或 App 复制的 anon token。触发词:"voicedrop api"、"voicedrop 登录"、"登录 voicedrop"、"voicedrop list"、"列出 voicedrop 文章/照片/录音"、"读/写 voicedrop 文章"、"上传/下载 voicedrop 照片/音频"、"voicedrop 触发挖矿"、"voicedrop 算力余额"、"voicedrop distill"、"蒸馏文风"、"/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 | 文件/文章/照片/音频/分享/公众号 |
| Agent Worker(Durable Objects) | https://jianshuo.dev/agent | 挖矿触发、算力余额/账单、语音编辑(WS)、设备配对 |
认证(用户 token,优先 6+4 设备配对)
所有接口(除公开的 GET /files/api/photo/*)都要 Authorization: Bearer $TOKEN。token 取用户自己的凭证:
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'])")
两种取 token 的方式(都是同一个用户、只能访问自己的数据):
| 方式 | token 形态 | scope(能看谁) | 怎么拿 |
|---|
| 6+4 设备配对 | anon_…(该用户的完整密钥) | 该用户自己 users/anon-<hash>/ | 下面「自助登录」流程,凭证落 ~/.config/voicedrop/credentials |
| App 临时 | anon_… | 该用户自己 | App 设置 → 账户/访问令牌 → 复制 |
6+4 自助登录(本 skill 自带,无需别的 skill)
本目录自带 vd-login.mjs(零依赖,Node ≥ 20)——它扮演设备配对协议里的「新设备」,和手机(老设备)配一次对,把账号密钥端到端加密取到本地。先查是否已登录,没有再走两步握手:
VD=~/.claude/skills/wjs-voicedrop/vd-login.mjs
node "$VD" status
前提:用户手机在线、app 在前台、已登录目标账号、是支持设备配对的版本。
两步握手(每条子命令只打印一行 JSON,按 ok 解析,别抓散文):
- 问用户要 6 位十六进制码(手机 设置 → 账户 里那串短 ID)。
node "$VD" start <6hex>:
{"ok":true,"pairingId":…,"matchCount":N} → 告诉用户手机这会儿会弹一个 4 位数字码。
{"ok":false,"error":"no_match"} → 6 位码错了,或手机离线/后台/旧版本 → 回第 1 步。
- 问用户要手机上弹出的 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 → {style,head,…} |
| 写(版本化) | PUT /files/api/style,body {"style":"…"} |
| 版本历史 | GET /files/api/style/history |
| 撤销/重做 | PATCH /files/api/style/head,body {"head":N} |
| 名字 | 读/写 | 暂留老 download/upload CLAUDE.md(# 我的名字,以后再搬家) |
| 照片 | 列出 | 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/list(筛 VoiceDrop-*.m4a) |
文章 articles(版本化 CRUD)
列出(最新在前):
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/articles
格式化展示:
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']})\")"
读全文:
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/articles/<stem>
写(版本化——每次 PUT 追加一个新版本,head 前移):
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"articles":[{"title":"新标题","body":"新正文..."}]}' \
https://jianshuo.dev/files/api/articles/<stem>
版本历史 / 切版本(撤销重做):
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/articles/<stem>/history
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 边车一起删,不删音频):
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"})。
文风 style(版本化)
挖矿时这份会被叠加进 system prompt(# 我的名字 + # 我的文风)。文风现在走独立的版本化端点 /files/api/style(每次 PUT 追加一个版本、head 前移,可撤销重做);名字仍暂留在老的 CLAUDE.md 的 # 我的名字 段,以后再搬家。
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/style
python3 -c "import json,os;print(json.dumps({'style':open('/tmp/style.txt').read()}, ensure_ascii=False))" > /tmp/style.json
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
--data-binary @/tmp/style.json https://jianshuo.dev/files/api/style
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/style/history
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"head":0}' https://jianshuo.dev/files/api/style/head
旧账号首次读会返回 {..., "legacy":true, "head":0}(从老 CLAUDE.md 的 # 我的文风 段镜像而来);第一次 PUT /style 后即转为真正的版本化存储,legacy 消失。
名字(# 我的名字)暂时仍用老接口读写:
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/download/CLAUDE.md
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: text/markdown; charset=utf-8" \
--data-binary @/tmp/voicedrop-claudemd.txt \
https://jianshuo.dev/files/api/upload/CLAUDE.md
照片 photos
没有专门的「列照片」接口——用通用 GET /list 筛 photos/:
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]"
curl -s -H "Authorization: Bearer $TOKEN" \
"https://jianshuo.dev/files/api/download/photos/<sessionTs>/<offset>-<rand>.jpg" -o out.jpg
curl -s "https://jianshuo.dev/files/api/photo/users/<sub>/photos/<sessionTs>/<offset>-<rand>.jpg" -o out.jpg
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 /list 筛 VoiceDrop-*.m4a:
顺序:/articles 端点服务端就按 createdAt 倒序(最新在前)。/list 是通用接口不排序,返回 R2 原始字典序。别按文件名排——名字里的时间戳不是可靠时钟(时钟偏差、staging/改名、导入的文件都可能对不上);要排就按 uploaded(R2 上传时间,ISO-8601 UTC 字符串,字典序==时间序)。下面的例子已按 uploaded 倒序,和 App「我的录音」一致。
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
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=算力不足/录音过长。
操作
触发挖矿(处理待处理录音):
curl -s -X POST -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/agent/mine/trigger
curl -s -X POST -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/mine
完整挖矿语义(ASR→Claude→多文章)见 /wjs-mining-voicedrop。
算力余额 + 账单:
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/agent/usage/balance
curl -s -H "Authorization: Bearer $TOKEN" "https://jianshuo.dev/agent/usage/ledger?limit=50"
生成公开分享链接 / 发公众号草稿:
curl -s -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/share/articles/<stem>.json
curl -s -X POST -H "Authorization: Bearer $TOKEN" https://jianshuo.dev/files/api/wechat/articles/<stem>.json
工作流:distill —— 蒸馏文风并上传
从样本文章提炼可执行的「文风规则」,写进 CLAUDE.md,让 miner 自动带上。
GET /articles 列出 → 请用户选 3–6 篇(<3 篇提醒指纹不稳)。
- 逐篇
GET /articles/<stem>,取 articles[*].body(不要 transcript 口述原文)。
- 派子 agent 分析(隔离上下文):「提炼这位作者最突出、可执行的语言习惯:句长偏好、段落密度、人称、词汇倾向、论证方式、结尾习惯、绝不做的事。**只分析怎么写,不分析思想立场。**输出 15–20 条 bullet,每条一句,用『用 X』『不用 Y』『X 必须在 Y 前』这种可执行语言。」
- 回收润色:每条要能被不认识该作者的模型直接照做(「喜欢短句」太模糊;「单句成段,段落 1–3 句居多」才够用)。去掉与服务器 SYSTEM prompt 重复的。
- 组装文风 bullet 列表,预览给用户确认后用上面的版本化
PUT /files/api/style 上传(名字段仍在 CLAUDE.md)。版本化写入可随时 PATCH /style/head 回滚。
- 成功 → 告知「下次录音挖文章时自动生效」。
实时接口(WebSocket,了解即可,curl 调不动)
wss://jianshuo.dev/agent/edit?stem=<stem> — 语音编辑某篇文章(App 持麦克风串行发指令)。
wss://jianshuo.dev/agent/status — 实时挖矿状态推送(待处理→听录音→挖文章→已成文)。
wss://jianshuo.dev/agent/asr — 火山流式 ASR 代理(语音听写)。
/agent/link/* — 设备配对(6+4)协议端点,由本 skill 自带的 vd-login.mjs 封装(见上面「6+4 自助登录」)。
常见错误
| 错误 | 修正 |
|---|
401 unauthorized | token 没设/过期 → 重走上面「6+4 自助登录」,或 App 设置重新复制 |
403 forbidden / read-only token | 用户 token 只能动自己 scope;写社区(share)需 Apple 登录过的 session;24h 临时 token 只能 list/download |
articles 返回空数组 | 还没成文 → 触发挖矿或等 miner |
404 not found(文章) | stem 写错 |
409 wechat_not_configured | 该用户没配公众号 appid/secret(App 设置里填) |
照片 400 not a photo | /photo/ 公开接口的 key 必须匹配 `users//photos/*.(jpg |
| 上传文风后 App 没变 | 切离再切回「设置」tab 重新加载 |