面向哲思的编程与架构
笔记哲思阅读动态搜索RSS 订阅
切换到深色模式
搜索
RSS 订阅
切换到深色模式
© 2026 Vic Chen. All rights reserved.CC BY-NC-ND 4.0
← 笔记
从阅读到知识(一):个人阅读档案的设计与实现

从阅读到知识(一):个人阅读档案的设计与实现

2026年6月5日1,9536分钟

微信读书没有官方 API 的那几年,我用抓包拼出了一套数据管道,定期爬书架和阅读记录。2026 年 5 月官方开放 Agent Skill,迁移只花了一个下午。但技术问题好解决,更难的是设计判断——那些满屏图表的阅读看板,对自己和访客真的有用吗?


目录
  • TL;DR
  • 1. 没有 API 的时候
  • 2. 转折:平台开放
  • 2.1 Skill
  • 2.2 迁移
  • 2.3 哲思
  • 3. 展示:一个值得认真对待的问题
  • 4. 实现细节
  • 4.1 手写 SVG 图表
  • 4.2 BookModal 与 Portal
  • 5. 现在能做什么,还不能做什么
目录
  • TL;DR
  • 1. 没有 API 的时候
  • 2. 转折:平台开放
  • 2.1 Skill
  • 2.2 迁移
  • 2.3 哲思
  • 3. 展示:一个值得认真对待的问题
  • 4. 实现细节
  • 4.1 手写 SVG 图表
  • 4.2 BookModal 与 Portal
  • 5. 现在能做什么,还不能做什么
目录
  1. TL;DR
  2. 1. 没有 API 的时候
  3. 2. 转折:平台开放
  4. 2.1 Skill
  5. 2.2 迁移
  6. 2.3 哲思
  7. 3. 展示:一个值得认真对待的问题
  8. 4. 实现细节
  9. 4.1 手写 SVG 图表
  10. 4.2 BookModal 与 Portal
  11. 5. 现在能做什么,还不能做什么
阅读
相关文章
  • 01
    从阅读到知识(三):孤岛与碰撞2026/06
  • 02
    从阅读到知识(二):脱离原书之后2026/06
  • 03
    给网站加朗读功能(二):声音复刻而不是通用音色2026/07
← 上一篇「批注」功能的设计与演进
下一篇 →网页内容导出(一):PDF 与 @media print 的分层处理

评论

© 2026 Vic Chen · 面向哲思的编程与架构CC BY-NC-ND 4.0

TL;DR

这是「从阅读到知识」系列的第一篇,讲的是数据层:如何拿到微信读书的数据,以及如何判断展示什么。

第一个问题的答案从抓包演变为官方 Skill,中间发生了一件值得记录的事——平台从封闭走向开放。第二个问题更难,因为它不是技术问题,而是设计判断:一个满屏图表的阅读看板,对读者(和自己)究竟有多大价值?


1. 没有 API 的时候

微信读书在很长一段时间里没有对外开放任何接口。想拿到自己的书架、阅读时长、划线记录,唯一的办法是抓包。

iOS 端的请求没有签名校验,鉴权靠两个字段:vid(用户 ID)和 skey(会话密钥)。把它们塞进请求头,就能直接访问 i.weread.qq.com 的内部接口:

fetch.py
HEADERS = {
    "Host": "i.weread.qq.com",
    "vid": "xxxxxxxxxxx",
    "skey": SKEY,
    "user-agent": "WeRead/9.4.0 (iPhone; iOS 26.1; Scale/3.00)",
    "v": "9.4.0.63",
}
 
def get_shelf():
    url = "https://i.weread.qq.com/shelf/sync"
    params = {"userVid": "xxxxxxxxxxx", "synckey": "0"}
    response = requests.get(url, params=params, headers=HEADERS)
    return response.json()

接口路径基本能猜到:/shelf/sync 是书架,/readdata/detail 是阅读时长,/user/notebooks 是笔记本。数据结构也比较稳定。

这套方案能用,但很脆弱:

  • skey 过期。微信读书的会话密钥不是长期有效的,过期后需要重新抓包拿新的 skey,手动更新配置。
  • 版本升级随时失效。App 更新后接口可能变更,User-Agent 对不上会拿不到数据。
  • 合规灰色地带。抓包绕过了平台本身的数据访问机制,这件事本身不太舒服。

用了一段时间,够用,但一直有点不踏实。


2. 转折:平台开放

2026 年 5 月,微信读书开放了官方的 Agent Skill:WeChatReading Skill 。

2.1 Skill

WeChatReading Skill 是微信读书为 AI Agent 设计的标准化接入方式。和 MCP(Model Context Protocol)的思路类似,它把平台的数据能力打包成一份可描述的接口规范,让 AI 助手能够直接调用——搜索书籍、查看书架、读取划线、获取阅读统计,都在其中。

开放的能力包括:

能力说明
书架管理/shelf/sync,含电子书、有声书/专辑、文章收藏
书籍信息/book/info、/book/getprogress,书籍详情与阅读进度
阅读统计/readdata/detail,支持 weekly / monthly / annually / overall 四种维度
笔记划线/user/notebooks、/book/bookmarklist,个人笔记与划线内容
书籍点评公开点评查询
发现推荐个性化推荐、相似推荐
搜索/store/search,在书城搜索书籍

鉴权方式变为 API Key(格式 wrk-xxxxxxxx),所有请求通过统一入口代理:

src/app/api/weread/route.ts
export async function POST(req: NextRequest) {
  const apiKey = process.env.WEREAD_API_KEY;
 
  const body = await req.json();
  const res = await fetch("https://i.weread.qq.com/api/agent/gateway", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`






请求体里用 api_name 指定接口,其余业务参数平铺在同一层:

// 书架
{ api_name: "/shelf/sync", skill_version: "1.0.4" }
 
// 阅读统计(按年)
{ api_name: "/readdata/detail", mode: "annually", baseTime: 1704067200, skill_version: "1.0.4" }
 
// 某本书的划线
{ api_name: "/book/bookmarklist", bookId: "xxx", count: 3

skill_version 是版本上报字段,服务端用它检查客户端是否需要升级——如果回包出现 upgrade_info,说明 Skill 有新版本,需要更新后再继续请求。

ℹ
平台通过版本字段保留了主动推送更新的通道:客户端每次请求都上报版本号,服务端可以在任意时刻要求客户端先升级再继续。这个设计让平台在开放接口的同时,保留了对接入方行为的约束能力。

2.2 迁移

原来的抓包方案是分散的 GET 请求,每个接口有自己的 URL 和请求头。迁移后,所有请求收拢成一个 POST,接口路径几乎没变,数据结构也基本保持一致。改动主要集中在鉴权方式:从手动维护 skey 换成环境变量里的 API Key。迁移花了一个下午。

2.3 哲思

✓
一个值得记录的观察:从抓包到官方 Skill,背后是一个正在发生的趋势:AI 应用的需求在推动平台开放。不只是微信读书——越来越多的平台开始为 AI Agent 提供标准化接口,因为有足够多的开发者想把平台数据接入自己的工作流。平台的开放边界,最终是被需求划定的。

这个方向的下一步可能是更完整的 MCP 支持、更细粒度的权限控制,以及目前还未开放的数据——比如社交数据、书评互动、跨用户的阅读行为。


3. 展示:一个值得认真对待的问题

有了稳定的数据来源,下一个问题是:展示什么? 我看了一些现有的微信读书看板产品。大部分都很酷炫:年度阅读热力图、分类雷达图、书籍完成率、阅读时段分布……数据非常全面,图表非常漂亮。但我在想两个问题:

  1. 对自己:我每次打开这个页面,真正想知道的是什么?是我上个月读了多少小时,还是我现在在读什么、读到了哪里?统计数据有价值,但它是回顾性的,而不是日常需要的。

  2. 对访客:我的阅读记录,对访客有什么用?他们需要知道我的阅读时段分布吗?或者,他们只是想知道我在读什么书,偶尔发现一本他们也感兴趣的?

这两个问题导向了一个相反于「展示一切」的设计方向:降低信息密度,让书本身说话。

具体的选择:

  • 当前在读放在最顶部,横向时间轴,最多 10 本,只有封面、书名、进度。
  • 书架按分类展开,横向滚动,封面是主角,不在封面上叠加文字。
  • 统计数据收到页面最后,不做重点展示,想看的人可以滑动下去看。

hover 封面会出现一个放大镜按钮,点开才是详细信息(作者、分类、划线摘录)。信息藏在交互里,而不是直接铺在页面上。


4. 实现细节

4.1 手写 SVG 图表

统计部分有四个图表:年度阅读趋势、月度时长分布、阅读时段、偏好分类。没有引入任何图表库。

理由很简单:Next.js 的 bundle size 敏感,Recharts 压缩后也有 ~200KB;而这几个图表的需求很确定,不需要库的通用性。

核心是一个坐标映射函数:

const py = (v: number, max: number, h: number, pad: Pad) =>
  pad.top + (h - pad.top - pad.bottom) * (1 - v / max);

把数据值映射到 SVG 像素坐标。所有图表共用这一个函数,差别只是 max 和 pad 的取值。

年度趋势图有双 Y 轴——左轴是小时数(柱状),右轴是本数(折线)。两套独立的 py 函数,共用同一个 SVG 画布:

const pyHours = (v: number) => py(v, hoursMax, CHART_H, PAD);
const pyBooks = (v: number) => py(v, booksMax, CHART_H, { ...PAD, left: 0 });

月度分布图支持年份切换,数据懒加载并缓存在组件 state 里:

const [cache, setCache] = useState<Record<number, number[] | "loading">>({});
 
useEffect(() => {
  if (cache[selYear]) return;
  setCache(prev => ({ ...prev, [selYear]: "loading" }));
  fetchMonthHours(selYear).then(data =>
    setCache

"loading" 作为哨兵值,避免重复请求。

4.2 BookModal 与 Portal

封面 hover 的详情模态框用了 createPortal,挂载到 document.body。原因是书架容器有 overflow: hidden,如果模态框渲染在容器内部会被裁剪。

return createPortal(
  <div style={{ position: "fixed", inset: 0, zIndex: 200 }}>
    {/* 遮罩 + 内容 */}
  </div>,
  document.body
);

划线数据在模态框打开时才发起请求,结果缓存在组件自身的 state 里:

useEffect(() => {
  if (!open || highlights !== null) return;
  fetchHighlights(bookId).then(setHighlights);
}, [open, bookId]);

highlights !== null 保证每本书只请求一次,关闭再打开不会重复拉取。整个懒加载的时序如下:

图1:划线数据懒加载的时序

5. 现在能做什么,还不能做什么

「阅读」这个页面现在能回答:

  • 我在读什么,读到了哪里
  • 我读过哪些书,大概偏向哪些领域
  • 我在某本书里划了哪些线

但它还不能回答:

  • 我从这些书里真正记住了什么(从阅读到知识(二):脱离原书之后)
  • 某个主题下,不同书的观点是怎么互相呼应或矛盾的(从阅读到知识(三):孤岛与碰撞)
  • 我读到的那些「值得一试」的方法,我后来用了吗

这些问题,是数据层解决不了的。把划线从数据库里捞出来展示,和让这些划线真正进入思考过程,是两件事。

下一篇会从这里继续:从展示到使用——让划线不只是存档,而是能被用上。

,
"Content-Type": "application/json",
},
body: JSON.stringify({ ...body, skill_version: "1.0.4" }),
});
return Response.json(await res.json());
}
,
skill_version
:
"1.0.4"
}
(
prev
=>
({
...
prev, [selYear]: data }))
);
}, [selYear]);