VoiceDrop API ← 开发者

VoiceDrop API

VoiceDrop 后端由 三个独立的 HTTP 服务 组成,都跑在 Cloudflare 上、同挂 jianshuo.dev 这个 zone。一个客户端通常只跟前两个打交道。

服务Base URL用途
Files APIhttps://jianshuo.dev/files/api/账户、录音/文章文件、分享、公众号、社区。绝大多数调用在这里。
Agent Workerhttps://jianshuo.dev/agent/(WS:wss://jianshuo.dev/agent/触发挖矿、实时状态推送、语音改稿。
Reco Workerhttps://jianshuo.dev/reco/社区 feed 排序 + 互动上报。可随时拔掉,挂了不影响主流程。

每个服务都另有一个 *.jianshuo.workers.dev 镜像(voicedrop-agent.jianshuo.workers.devvoicedrop-reco.jianshuo.workers.dev),行为一致。同 zone 内服务端互调用镜像绕过 Pages 路由;客户端用上面的 jianshuo.dev/... 即可。

数据流一句话:上传 .m4a → 服务端挖矿(ASR + Claude)写出 articles/<stem>.json → 客户端读文章、分享、发公众号 / 发社区。


1. 认证

所有请求用 Authorization: Bearer <token>(Files API 也接受 ?token=<token> 查询参数)。共两档凭证:

凭证形态怎么拿scope(数据隔离)
anon tokenanon_ 开头、≥20 位的高熵字符串客户端自己生成一次,存好(同一个用户始终用同一个 token)users/anon-<sha256(token)[:32]>/
session JWTh.p.sig(HS256)POST auth/apple 用 Apple identityToken 换JWT 内的 scope

关键点:

换 session JWT

POST /files/api/auth/apple
Content-Type: application/json

{ "identityToken": "<Apple identity token>", "fullName": "张三", "email": "..." }

fullName / email 仅首次授权时由 Apple 下发,可选。带上当前的 anon token(Authorization 头)则把这个 Apple ID 绑到该 anon 盒,数据不搬家。

返回:{ "session": "<JWT, 有效期 365 天>", "scope": "users/<sub>/" }


2. 通用约定


3. Files API

auth/applephoto/*asset/wechat-covers/* 外,全部需要 token。

账户

方法 / 路径说明返回
POST auth/apple见上{session, scope}
GET whoami当前 token 解析出的 scope{scope:"users/<sub>/"}
GET token/articles签发 24h 只读文章链接{token, url, expires_in:86400}

文件(裸 R2 读写)

方法 / 路径说明返回
GET list列出 scope 内全部对象(已分页聚合,不截断){files:[{name,size,uploaded}]}
PUT upload/<name>上传(请求体即文件原始字节,Content-Type 透传)。name 形如 VoiceDrop-*.m4a 时自动触发挖矿。{ok:true, name}
GET download/<name>下载原始字节。HEAD 同路径只取元信息。文件字节
DELETE file/<name>删除单个对象{ok:true}

录音文件名约定VoiceDrop-<ts>-<dur>-<weekday>-<period>[-<city>-<district>].m4a(纯 ASCII)。只有这个前缀+后缀会触发自动挖矿。

文章(高层 CRUD,自带版本控制)

<stem> = 录音文件去掉后缀的主名。优先用这组接口而不是裸 download/articles/<stem>.json——它会把内部版本结构(versions[head])拍平成顶层 articles

方法 / 路径说明返回
GET articles列出全部文章{articles:[{stem,title,head,createdAt,updatedAt,count}]}
GET articles/<stem>读一篇(当前 head 版本)文章文档(见 §6)
PUT articles/<stem>写入(自动存为新版本){ok, head}
DELETE articles/<stem>删文章 + .srt + .empty 边车{ok}
GET articles/<stem>/history版本历史{head, versions:[...]}
PATCH articles/<stem>/head只移动 head 指针(撤销 / 重做,不产生新版本),body {head:<n>}{ok, head}
PUT articles/<stem>/srt写字幕边车(请求体为 SRT 文本){ok}
PUT articles/<stem>/empty标记无语音,body {reason?}{ok}

文风(版本化)

挖矿 / 改稿 prompt 会叠加用户的文风。存储是版本化的 CLAUDE.json(schema-3,和文章同一套 versions[head] 结构)。文风是唯一版本化的字段名字等身份字段放非版本化的 doc.profile(改名字不会产生新文风版本),读取时回退老 CLAUDE.md「# 我的名字」。

方法 / 路径说明返回
GET style读当前文风 + 名字{style, name, head, createdAt, updatedAt};只有旧 CLAUDE.md{style, name, head:0, legacy:true};都没有 → 404
PUT style写入。body {style?, name?, source?}:传 style 存为新版本;传 name 只更新 profile.name不产生版本);可同时传{ok, head}stylename 全空 → 400 empty_content
GET style/history版本历史{head, versions:[{v,savedAt,source,style}]}(oldest-first,最多 10 版)
PATCH style/head只移动 head 指针(撤销 / 重做,不产生新版本),body {head:<n>}{ok, head};版本不存在 → 404

分享 & 公众号

方法 / 路径说明返回
GET share/articles/<stem>.json生成 / 取该文章的公开短链{url:"https://jianshuo.dev/voicedrop/<id>"}
POST wechat/articles/<stem>.json同步把文章发成公众号草稿(已发过则原地更新)。需先配好 WECHAT.json{ok,created,updated}409 wechat_not_configured502 {errcode,errmsg} 透传真实微信错误

社区(跨用户公共空间)

帖子是 指向某篇文章的活指针(schema-2,无内容拷贝)——源文章一改,社区里立即同步。

方法 / 路径说明返回
POST community/share/articles/<stem>.json分享 / 重新分享自己的一篇。body 可带 {replyTo:<shareId>} 表示回复。需 Apple session,否则 403 needs_apple_signin{ok, shareId}
GET community/list全部帖子,按首次分享时间倒序{posts:[{shareId,author,title,firstSharedAt,count,mine,replyTo?}]}
GET community/get/<shareId>读一帖(含活文章内容){shareId,author,title,articles:[{title,body}],owner,firstSharedAt,replyTo?}
GET community/replies/<shareId>某帖的回复,按时间正序{posts:[...]}
GET community/shared/articles/<stem>.json我这篇是否已分享(驱动「分享 / 更新」按钮){shared:bool, shareId?}
POST community/unshare/<shareId>撤下自己的帖(owner only{ok}

渲染社区帖里的照片:用返回的 owner 拼上正文里 [[photo:<relkey>]] 标记的 key,得到完整 key,再走下面的公开 photo/<key> 取图。

公开资源(无需 token)

方法 / 路径说明
GET photo/<完整 R2 key>取一张会话照片。只接受 users/*/photos/*.(jpg|jpeg|png),CORS *、公开缓存。所有照片展示都走这个唯一端点。
GET asset/wechat-covers列出公众号封面图名 → {covers:[...]}
GET asset/wechat-covers/<name>取一张封面图字节

4. Agent Worker

Base:https://jianshuo.dev/agent/。鉴权同 Files API(anon 或 session,编辑需可写 token)。

触发挖矿

POST /agent/mine/trigger
Authorization: Bearer <any valid user token>

唤醒服务端 miner 处理所有待处理录音。幂等,已处理的会跳过。返回 202 queued。上传 .m4a 时服务端已自动调,一般无需手动。

wss://…/agent/edit?stem=<stem> — 语音改稿

打开后是一条长连接,可多轮往返;服务端按文章持久化历史,跨轮有上下文。

客户端 → 服务端(发改稿指令):

{
  "type": "instruct",
  "text": "把第3行改简洁点,删掉图2",
  "images": [
    { "data": "<base64>", "key": "photos/<sessionTs>/<offset>-<rand>.jpg", "mediaType": "image/jpeg" }
  ]
}

images 可选,附带新照片。)

服务端 → 客户端(每条指令依次推这些):

{ "type": "status",  "state": "working" }
{ "type": "updated", "article": { ... 顶层 articles 的完整文档 } }
{ "type": "reply",   "text": "改好了", "ok": true }
{ "type": "error",   "message": "<原因>" }

协议规则(客户端必须遵守):

wss://…/agent/status — 实时状态推送

只读订阅,客户端不发消息。每当某条录音状态变化,服务端推:

{ "type": "status_update", "stem": "VoiceDrop-xxx", "status": "asr" }

status 取值:asr(听录音)· mining(挖文章)· ready(已成文)· empty(无语音)。据此把列表行的徽章原地翻状态,免轮询。


5. Reco Worker

Base:https://jianshuo.dev/reco/。鉴权:anon 或 session token(不接受 temp token)。可拔掉——客户端应自带 2s 超时,reco 挂 / 超时就回退按时间倒序,feed 照常。

POST /reco/rank — feed 排序

GET community/list 拿到的帖子交给 reco 排序。请求:

{ "posts": [ { "shareId":"abc", "firstSharedAt":1700000000000, "replyCount":2, "author":"张三" } ] }

返回:

{ "order": ["<shareId>", "..."],
  "liked": ["<shareId>", "..."] }

order = 排好序的 shareId;liked = 当前用户点过 ❤️ 的。评分 = (1 + view·1 + finish·4 + like·3 + reply·5 + report·(-9)) / (ageHours+2)^1.5,再按作者打散。

POST /reco/engage/<shareId> — 互动上报(fire-and-forget)

{ "action": "view" }

actionview(进帖)· finish(读到底)· like(❤️)· report(举报)。


6. 数据模型

文章文档(GET articles/<stem> 返回)

{
  "schema": 3,
  "id": "VoiceDrop-xxx",
  "sourceAudio": "VoiceDrop-xxx.m4a",
  "createdAt": 1700000000000,
  "updatedAt": 1700000000000,
  "transcript": "原始口述转写……",
  "srt": "1\n00:00:00,000 --> ...",
  "status": "ready",
  "model": "claude-sonnet-4-6",
  "articles": [ { "title": "标题", "body": "正文 markdown,含 [[photo:<relkey>]] 标记" } ]
}

R2 key 速查(均在 users/<sub>/ 下)

key内容
VoiceDrop-<ts>-….m4a录音(上传它触发挖矿)
articles/<stem>.json文章(存在 = 已成文)
articles/<stem>.empty无语音标记
articles/<stem>.srt字幕边车
photos/<sessionTs>/<offset>-<rand>.jpg会话照片(<offset> = 距录音起点的整数秒)
CLAUDE.json文风(版本化 schema-3,喂给挖矿/改稿 prompt)——见上「文风」接口
CLAUDE.md仅存用户名字(旧文风回退源;写入只往 CLAUDE.json
WECHAT.json公众号配置 {appid,secret,enabled,coverMediaIds}

7. 端到端示例(cURL)

TOKEN="anon_xxxxxxxxxxxxxxxxxxxx"
BASE="https://jianshuo.dev/files/api"

# 1) 上传录音(自动触发挖矿)
curl -X PUT "$BASE/upload/VoiceDrop-20260627-093012-Sat-morning.m4a" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: audio/m4a" \
  --data-binary @rec.m4a

# 2)(可选)手动催一把挖矿
curl -X POST "https://jianshuo.dev/agent/mine/trigger" \
  -H "Authorization: Bearer $TOKEN"

# 3) 列文章,读一篇
curl "$BASE/articles" -H "Authorization: Bearer $TOKEN"
curl "$BASE/articles/VoiceDrop-20260627-093012-Sat-morning" -H "Authorization: Bearer $TOKEN"

# 4) 生成公开分享短链
curl "$BASE/share/articles/VoiceDrop-20260627-093012-Sat-morning.json" \
  -H "Authorization: Bearer $TOKEN"

# 5) 发成公众号草稿(需先配 WECHAT.json)
curl -X POST "$BASE/wechat/articles/VoiceDrop-20260627-093012-Sat-morning.json" \
  -H "Authorization: Bearer $TOKEN"

实时进度可订阅 wss://jianshuo.dev/agent/status(带同一个 Authorization),看着这条录音的徽章从 asrminingready 翻过去。