修改了微信读书的脚本
This commit is contained in:
@@ -214,6 +214,11 @@ pnpm reading
|
|||||||
|
|
||||||
读完的书会自动出现在「书籍」区底部的「微信读书 · 已读完」分组里,不用手工搬。
|
读完的书会自动出现在「书籍」区底部的「微信读书 · 已读完」分组里,不用手工搬。
|
||||||
|
|
||||||
|
**有声书(专辑)**:同一次请求里还会抓书架上的专辑,写进 `reading.json` 的 `albums` 字段。
|
||||||
|
「在读」页会自动多出一个「有声书」区块 —— 但没有专辑时整块不显示,所以现在看不到是正常的。
|
||||||
|
专辑没有阅读进度也没有跳转链接(微信读书那边就没给),卡片只显示集数、完结状态和最近收听。
|
||||||
|
`--top` 只作用于书,专辑一次全取;`secret: 1` 的私密收听同样会被过滤掉。
|
||||||
|
|
||||||
**平时不用管**:`pnpm build` 和 `./deploy.sh` 都会自动先抓一次。抓不到就沿用上一次的数据,**不会让部署失败**。
|
**平时不用管**:`pnpm build` 和 `./deploy.sh` 都会自动先抓一次。抓不到就沿用上一次的数据,**不会让部署失败**。
|
||||||
|
|
||||||
**首次配置**(只做一次):
|
**首次配置**(只做一次):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* 抓取微信读书「最近在读」,写出 src/data/reading.json
|
/* 抓取微信读书「最近在读」与「有声书专辑」,写出 src/data/reading.json
|
||||||
*
|
*
|
||||||
* 用法:
|
* 用法:
|
||||||
* pnpm reading 手动刷新(失败会 exit 1,明确报错)
|
* pnpm reading 手动刷新(失败会 exit 1,明确报错)
|
||||||
@@ -17,14 +17,24 @@
|
|||||||
* 只有「阅读进度」要额外调 /book/getprogress,所以请求数 = 1 + N
|
* 只有「阅读进度」要额外调 /book/getprogress,所以请求数 = 1 + N
|
||||||
* 2. book.recordReadingTime 恒为 0,但 book.readingTime 是真数据,累计时长取后者
|
* 2. book.recordReadingTime 恒为 0,但 book.readingTime 是真数据,累计时长取后者
|
||||||
* 3. 官方文档没列 book.startReadingTime,但回包里有,是「开始阅读时间」
|
* 3. 官方文档没列 book.startReadingTime,但回包里有,是「开始阅读时间」
|
||||||
|
* 4. albums[](专辑/有声书)和 books[] 是平行数组,数据结构也不一样:
|
||||||
|
* 内容包在 albumInfo / albumInfoExtra 两层里,且**没有进度、没有 deepLink**
|
||||||
|
* (至少官方文档没写)。--top 只作用于 books,专辑一次全取。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "node:fs"
|
import fs from "node:fs"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
||||||
const ENDPOINT = "https://i.weread.qq.com/api/agent/gateway"
|
const ENDPOINT = "https://i.weread.qq.com/api/agent/gateway"
|
||||||
/** 与官方 SKILL.md 顶部的 version 保持一致;服务端会用它对版本 */
|
/**
|
||||||
const SKILL_VERSION = "1.0.4"
|
* 与官方 SKILL.md 顶部的 version 保持一致;服务端会用它对版本。
|
||||||
|
*
|
||||||
|
* 升级就是改这个数字,**不需要**下载官方的 weread-skills.zip:那个包是给 agent 读的
|
||||||
|
* 提示词文档(SKILL.md + shelf.md / book.md 等),本项目一个文件都不消费它;
|
||||||
|
* 网关只拿这里的 skill_version 做比对。2026-10-10 起服务端要求 1.0.5。
|
||||||
|
* 参考:https://cdn.weread.qq.com/skills/weread-skills.zip
|
||||||
|
*/
|
||||||
|
const SKILL_VERSION = "1.0.5"
|
||||||
const OUT_FILE = "src/data/reading.json"
|
const OUT_FILE = "src/data/reading.json"
|
||||||
/** 0 = 不限制,整架都抓。书架一般也就几十本,1+N 个请求完全够用 */
|
/** 0 = 不限制,整架都抓。书架一般也就几十本,1+N 个请求完全够用 */
|
||||||
const DEFAULT_TOP = 0
|
const DEFAULT_TOP = 0
|
||||||
@@ -47,6 +57,54 @@ const apiKey = process.env.WEREAD_API_KEY
|
|||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间戳转 ISO。
|
||||||
|
*
|
||||||
|
* 微信读书的书架时间戳实测是**秒**;但专辑那边的 lectureReadUpdateTime 官方文档
|
||||||
|
* 没写单位,万一是毫秒(13 位)就会得到公元 5 万年。用位数兜一下,两边都安全。
|
||||||
|
*/
|
||||||
|
function toIso(value) {
|
||||||
|
const n = Number(value)
|
||||||
|
if (!Number.isFinite(n) || n <= 0) return null
|
||||||
|
return new Date(n > 1e12 ? n : n * 1000).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专辑/有声书。和 books[] 平行,但结构完全不同:内容在 albumInfo / albumInfoExtra 两层。
|
||||||
|
*
|
||||||
|
* 官方文档给的字段里没有 progress、也没有 deepLink,所以:
|
||||||
|
* - 不伪造进度,前端只展示「集数 / 完结状态 / 最近收听」;
|
||||||
|
* - deepLink 按几个可能的位置兜一下,取不到就是空串(卡片退化成不可点击)。
|
||||||
|
* 私密收听(secret === 1)和书一样必须过滤掉 —— 这是要发到公开网站上的。
|
||||||
|
*/
|
||||||
|
function normalizeAlbums(shelf) {
|
||||||
|
return (shelf.albums ?? [])
|
||||||
|
.filter((album) => album?.albumInfoExtra?.secret !== 1)
|
||||||
|
.map((album) => {
|
||||||
|
const info = album.albumInfo ?? {}
|
||||||
|
const extra = album.albumInfoExtra ?? {}
|
||||||
|
return {
|
||||||
|
albumId: info.albumId ?? "",
|
||||||
|
name: info.name ?? "",
|
||||||
|
/** 演播 / 作者 */
|
||||||
|
author: info.authorName ?? "",
|
||||||
|
cover: info.cover ?? "",
|
||||||
|
trackCount: Number(info.trackCount) || 0,
|
||||||
|
finished: Number(info.finish) === 1,
|
||||||
|
/** 完结状态原文,如「已完结」 */
|
||||||
|
finishStatus: info.finishStatus ?? "",
|
||||||
|
intro: info.intro ?? "",
|
||||||
|
lastListenedAt: toIso(extra.lectureReadUpdateTime),
|
||||||
|
deepLink: info.deepLink ?? album.deepLink ?? "",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const at = a.lastListenedAt ? Date.parse(a.lastListenedAt) : 0
|
||||||
|
const bt = b.lastListenedAt ? Date.parse(b.lastListenedAt) : 0
|
||||||
|
return bt - at
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function call(apiName, params = {}) {
|
async function call(apiName, params = {}) {
|
||||||
const res = await fetch(ENDPOINT, {
|
const res = await fetch(ENDPOINT, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -61,7 +119,22 @@ async function call(apiName, params = {}) {
|
|||||||
const json = await res.json().catch(() => null)
|
const json = await res.json().catch(() => null)
|
||||||
|
|
||||||
if (json?.upgrade_info) {
|
if (json?.upgrade_info) {
|
||||||
console.warn(`⚠️ 服务端要求升级 Skill:${json.upgrade_info.message ?? "(无说明)"}`)
|
/*
|
||||||
|
* 服务端只拿 skill_version 做版本比对。原来的写法只打了 message,把
|
||||||
|
* current_version / latest_version / upgrade_url 一起丢掉了,于是提示退化成
|
||||||
|
* 一句无解的「请立即升级」—— 而 message 本身是写给 agent 的通用话术
|
||||||
|
* (「必须立即暂停当前操作…不得忽略该字段」),照做就等于让远端文案驱动本地文件。
|
||||||
|
* 这里改成把可执行的信息全打出来:差了多少版、去哪儿看、改哪一行。
|
||||||
|
*/
|
||||||
|
const { current_version, latest_version, upgrade_url, message } = json.upgrade_info
|
||||||
|
console.warn(
|
||||||
|
`⚠️ 微信读书 Skill 版本过期:本地 ${current_version ?? SKILL_VERSION} → 服务端要求 ${latest_version ?? "?"}(接口仍可用,只是提醒)`,
|
||||||
|
)
|
||||||
|
if (message) console.warn(` ${message}`)
|
||||||
|
if (upgrade_url) console.warn(` 升级包(仅供查阅,本项目不消费):${upgrade_url}`)
|
||||||
|
console.warn(
|
||||||
|
` 处理方式:把本文件顶部的 SKILL_VERSION 改成 "${latest_version ?? "最新版本"}",无需下载安装任何文件。`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (res.status === 401 || json?.errcode === -2010) {
|
if (res.status === 401 || json?.errcode === -2010) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -80,6 +153,9 @@ async function call(apiName, params = {}) {
|
|||||||
async function fetchReading() {
|
async function fetchReading() {
|
||||||
const shelf = await call("/shelf/sync")
|
const shelf = await call("/shelf/sync")
|
||||||
|
|
||||||
|
// 专辑一次全取(--top 只管书),所以先算出来,避免下面提前 return 时把它丢了
|
||||||
|
const albums = normalizeAlbums(shelf)
|
||||||
|
|
||||||
const sorted = (shelf.books ?? [])
|
const sorted = (shelf.books ?? [])
|
||||||
// 私密阅读(secret=1)不要发到公开网站上
|
// 私密阅读(secret=1)不要发到公开网站上
|
||||||
.filter((book) => book.secret !== 1)
|
.filter((book) => book.secret !== 1)
|
||||||
@@ -91,7 +167,7 @@ async function fetchReading() {
|
|||||||
const candidates = TOP_N > 0 ? sorted.slice(0, TOP_N) : sorted
|
const candidates = TOP_N > 0 ? sorted.slice(0, TOP_N) : sorted
|
||||||
|
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
return { books: [], note: "书架里没有可展示的阅读记录" }
|
return { books: [], albums, note: "书架里没有可展示的阅读记录" }
|
||||||
}
|
}
|
||||||
|
|
||||||
const books = []
|
const books = []
|
||||||
@@ -140,7 +216,7 @@ async function fetchReading() {
|
|||||||
await sleep(DELAY_BETWEEN_CALLS_MS)
|
await sleep(DELAY_BETWEEN_CALLS_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { books }
|
return { books, albums }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -151,23 +227,27 @@ async function main() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { books, note } = await fetchReading()
|
const { books, albums, note } = await fetchReading()
|
||||||
if (note) console.warn(`⚠️ ${note}`)
|
if (note) console.warn(`⚠️ ${note}`)
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
source: "weread",
|
source: "weread",
|
||||||
books,
|
books,
|
||||||
|
albums,
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true })
|
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true })
|
||||||
fs.writeFileSync(OUT_FILE, `${JSON.stringify(payload, null, 2)}\n`)
|
fs.writeFileSync(OUT_FILE, `${JSON.stringify(payload, null, 2)}\n`)
|
||||||
|
|
||||||
console.log(`✅ 写入 ${books.length} 本到 ${OUT_FILE}`)
|
console.log(`✅ 写入 ${books.length} 本 + ${albums.length} 张有声书专辑到 ${OUT_FILE}`)
|
||||||
for (const book of books) {
|
for (const book of books) {
|
||||||
const progress = book.progress === null ? " ?" : `${String(book.progress).padStart(3)}%`
|
const progress = book.progress === null ? " ?" : `${String(book.progress).padStart(3)}%`
|
||||||
console.log(` ${progress} ${book.title}${book.finished ? "(已读完)" : ""}`)
|
console.log(` ${progress} ${book.title}${book.finished ? "(已读完)" : ""}`)
|
||||||
}
|
}
|
||||||
|
for (const album of albums) {
|
||||||
|
console.log(` 🎧 ${album.name}${album.trackCount ? `(${album.trackCount} 集)` : ""}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
import { formatReadingDate, type ReadingAlbum } from "@/lib/reading"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信读书「专辑 / 有声书」卡片。
|
||||||
|
*
|
||||||
|
* 为什么不复用 ReadingCard:两边能拿到的字段不一样。专辑没有 progress(官方结构里
|
||||||
|
* 就没有),文档也没给 deepLink,还少了分类、开始/读完时间。硬塞进书卡会出现
|
||||||
|
* 「进度条永远是 0%」这种假信息,所以单独一张,底部换成「集数 / 完结状态 / 最近收听」。
|
||||||
|
*
|
||||||
|
* deepLink 拿不到时不渲染成 <a>,只做展示 —— 一个点不动的链接比没有链接更糟。
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
album: ReadingAlbum
|
||||||
|
}
|
||||||
|
|
||||||
|
const { album } = Astro.props
|
||||||
|
|
||||||
|
const Tag = album.deepLink ? "a" : "div"
|
||||||
|
const linkable = Boolean(album.deepLink)
|
||||||
|
const lastListened = formatReadingDate(album.lastListenedAt)
|
||||||
|
const meta = [album.trackCount > 0 ? `${album.trackCount} 集` : "", album.finishStatus]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · ")
|
||||||
|
---
|
||||||
|
|
||||||
|
<Tag
|
||||||
|
href={linkable ? album.deepLink : undefined}
|
||||||
|
target={linkable ? "_blank" : undefined}
|
||||||
|
rel={linkable ? "noopener noreferrer" : undefined}
|
||||||
|
class:list={[
|
||||||
|
"group flex gap-4 rounded-xl border border-border/50 bg-card p-4",
|
||||||
|
linkable && "transition-shadow duration-300 hover:shadow-lg",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class="shrink-0 w-20 aspect-square rounded-lg overflow-hidden bg-surface-subtle">
|
||||||
|
{album.cover ? (
|
||||||
|
<img
|
||||||
|
src={album.cover}
|
||||||
|
alt={album.name}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div class="flex h-full w-full items-center justify-center p-2 bg-gradient-to-br from-primary/30 via-primary/20 to-primary/5">
|
||||||
|
<p class="font-semibold text-[10px] text-foreground text-center line-clamp-4">{album.name}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0 flex flex-col">
|
||||||
|
<h3
|
||||||
|
class:list={[
|
||||||
|
"font-medium text-sm text-foreground line-clamp-2 leading-snug mb-1",
|
||||||
|
linkable && "group-hover:text-primary transition-colors",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{album.name}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-muted-foreground line-clamp-1 mb-3">{album.author}</p>
|
||||||
|
|
||||||
|
<div class="mt-auto space-y-1.5">
|
||||||
|
{meta && <p class="text-[11px] text-muted-foreground/80 tabular-nums">{meta}</p>}
|
||||||
|
<p class="text-[11px] text-muted-foreground/80 tabular-nums">
|
||||||
|
{lastListened ? `最近收听 ${lastListened}` : "还没有收听记录"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
@@ -4,6 +4,7 @@ import Footer from "@/components/footer.astro"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import booksData from "../../public/data/books.json"
|
import booksData from "../../public/data/books.json"
|
||||||
import ReadingCard from "@/components/reading-card.astro"
|
import ReadingCard from "@/components/reading-card.astro"
|
||||||
|
import AlbumCard from "@/components/album-card.astro"
|
||||||
import { formatReadingDate, getReadingData } from "@/lib/reading"
|
import { formatReadingDate, getReadingData } from "@/lib/reading"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,6 +27,11 @@ const inProgressReading = readingBooks.filter((book) => !book.finished)
|
|||||||
const finishedReading = readingBooks.filter((book) => book.finished)
|
const finishedReading = readingBooks.filter((book) => book.finished)
|
||||||
const readingCount = inProgressReading.length
|
const readingCount = inProgressReading.length
|
||||||
|
|
||||||
|
// 有声书专辑:与 books[] 平行的另一组数据。
|
||||||
|
// 用 ?? [] 兜住两种情况:老版 reading.json 没有 albums 字段(--soft 回退时),
|
||||||
|
// 以及你根本没听过有声书(服务端返回空数组)。
|
||||||
|
const readingAlbums = reading?.albums ?? []
|
||||||
|
|
||||||
/** 「书籍」区里给微信读书已读书记一个分类名,同时作为可筛选的领域 */
|
/** 「书籍」区里给微信读书已读书记一个分类名,同时作为可筛选的领域 */
|
||||||
const WEREAD_CATEGORY = "微信读书"
|
const WEREAD_CATEGORY = "微信读书"
|
||||||
|
|
||||||
@@ -194,6 +200,27 @@ const recommendBadge = (status?: string) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ========================= 有声书(微信读书专辑) =========================
|
||||||
|
与 books[] 平行的一组数据,只有真的听过有声书时服务端才会返回内容。
|
||||||
|
readingAlbums 为空就整块不渲染 —— 所以在你有第一张专辑之前,这个区块
|
||||||
|
既不存在也不占位。数据由 scripts/fetch-reading.mjs 一并抓取。 */}
|
||||||
|
{isReadingView && readingAlbums.length > 0 && (
|
||||||
|
<div id="albums" class="mb-16 scroll-mt-24">
|
||||||
|
<div class="flex flex-wrap items-end justify-between gap-x-6 gap-y-2 mb-6">
|
||||||
|
<div>
|
||||||
|
<div class="text-[11px] text-primary tracking-[0.22em] font-semibold uppercase mb-1.5">Collection · 02</div>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold font-serif tracking-tight text-foreground mb-1.5">有声书</h2>
|
||||||
|
<div class="text-sm italic font-serif text-muted-foreground">耳朵也在读书</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground tabular-nums">{readingAlbums.length} 张专辑</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
{readingAlbums.map((album) => <AlbumCard album={album} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isReadingView && (
|
{!isReadingView && (
|
||||||
<>
|
<>
|
||||||
{/* ---------- Header ---------- */}
|
{/* ---------- Header ---------- */}
|
||||||
|
|||||||
@@ -28,5 +28,13 @@ slug: impact-of-execution
|
|||||||
|
|
||||||
**想到和得到之间还有一个做到**
|
**想到和得到之间还有一个做到**
|
||||||
|
|
||||||
有的时候我也在想这个问题,
|
有的时候我也在想这个问题,**做到**突出的是个人实现的过程,是改变这个物质世界的必然。很多时候,我会选择逃避,便会以拖延的形式出现。干工作没有动力,明明知道这件事情很重要了,但是却一拖再拖。
|
||||||
|
|
||||||
|
内心的驱动力就是这个中间体,映射到行动上就是执行力。自己需要不断提升自己的内驱动力,在自己的精神世界里里面不断建立一个又一个里程碑,完成一个又一个目标形成正反馈,让自己成为一个高执行力的人。
|
||||||
|
很多时候,很多事情是很难看到回报的,或者说没有那么明显的正反馈,就容易懈怠,半途而废。
|
||||||
|
然而每个人之间的差距就会体现在这里。引用一段话:每个人,每个年龄段,每个层次都有不熟悉的领域,不精通的专业。一部分人碍于面子不懂装懂,一部分人放低自己潜心学习。这与你的年龄段,与你的层次没有关系,只与你态度发生转变有关。
|
||||||
|
|
||||||
|
人非生而知之,
|
||||||
|
|
||||||
|
|
||||||
|
现在就是感觉自己很迷茫,不想工作,但又不得不工作。
|
||||||
|
|||||||
+64
-35
@@ -1,19 +1,75 @@
|
|||||||
{
|
{
|
||||||
"updatedAt": "2026-10-04T12:16:26.963Z",
|
"updatedAt": "2026-10-11T08:52:01.398Z",
|
||||||
"source": "weread",
|
"source": "weread",
|
||||||
"books": [
|
"books": [
|
||||||
|
{
|
||||||
|
"bookId": "3300207468",
|
||||||
|
"title": "快乐的死(果麦经典)",
|
||||||
|
"author": "[法]加缪",
|
||||||
|
"category": "文学-外国文学",
|
||||||
|
"cover": "https://cdn.weread.qq.com/weread/cover/12/cpplatform_tgx43jt54ezt7bamqcmjbk/t6_cpplatform_tgx43jt54ezt7bamqcmjbk1779878190.jpg",
|
||||||
|
"progress": 5,
|
||||||
|
"finished": false,
|
||||||
|
"lastReadAt": "2026-10-08T15:27:08.000Z",
|
||||||
|
"startedAt": null,
|
||||||
|
"finishedAt": null,
|
||||||
|
"readingSeconds": 262,
|
||||||
|
"deepLink": "https://weread.qq.com/book-detail?type=1&v=66b32530813abb78ag018a22"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bookId": "3300086064",
|
||||||
|
"title": "河西走廊",
|
||||||
|
"author": "《河西走廊》摄制组编",
|
||||||
|
"category": "历史-历史地理",
|
||||||
|
"cover": "https://cdn.weread.qq.com/weread/cover/89/cpplatform_htt13rz3ky797h8pvypqq1/t6_cpplatform_htt13rz3ky797h8pvypqq11707017069.jpg",
|
||||||
|
"progress": 0,
|
||||||
|
"finished": false,
|
||||||
|
"lastReadAt": "2026-10-08T12:06:21.000Z",
|
||||||
|
"startedAt": null,
|
||||||
|
"finishedAt": null,
|
||||||
|
"readingSeconds": 0,
|
||||||
|
"deepLink": "https://weread.qq.com/book-detail?type=1&v=de932a80813ab881eg014870"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bookId": "3300175025",
|
||||||
|
"title": "此刻是春天(微信读书特别版)",
|
||||||
|
"author": "卢思浩",
|
||||||
|
"category": "精品小说-治愈小说",
|
||||||
|
"cover": "https://cdn.weread.qq.com/weread/cover/4/cpplatform_3hsoybvrwapq1efqha1mvz/t6_cpplatform_3hsoybvrwapq1efqha1mvz1762325065.jpg",
|
||||||
|
"progress": 4,
|
||||||
|
"finished": false,
|
||||||
|
"lastReadAt": "2026-10-08T12:05:30.000Z",
|
||||||
|
"startedAt": "2026-01-11T10:32:09.000Z",
|
||||||
|
"finishedAt": "2026-09-27T16:39:36.000Z",
|
||||||
|
"readingSeconds": 295,
|
||||||
|
"deepLink": "https://weread.qq.com/book-detail?type=1&v=b0b32630813abaadeg0157d1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bookId": "3300095415",
|
||||||
|
"title": "觉醒年代(全集|同名影视原著)",
|
||||||
|
"author": "龙平平",
|
||||||
|
"category": "历史-历史小说",
|
||||||
|
"cover": "https://cdn.weread.qq.com/weread/cover/66/cpplatform_4opftuyz4szxsagdfgsn1i/t6_cpplatform_4opftuyz4szxsagdfgsn1i1713258044.jpg",
|
||||||
|
"progress": 0,
|
||||||
|
"finished": false,
|
||||||
|
"lastReadAt": "2026-10-08T11:44:34.000Z",
|
||||||
|
"startedAt": null,
|
||||||
|
"finishedAt": null,
|
||||||
|
"readingSeconds": 0,
|
||||||
|
"deepLink": "https://weread.qq.com/book-detail?type=1&v=e6032ec0813ab8bc5g015fd3"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"bookId": "182788",
|
"bookId": "182788",
|
||||||
"title": "北京法源寺(文学性与思想性相结合的经典作品)",
|
"title": "北京法源寺(文学性与思想性相结合的经典作品)",
|
||||||
"author": "李敖",
|
"author": "李敖",
|
||||||
"category": "历史-历史小说",
|
"category": "历史-历史小说",
|
||||||
"cover": "https://cdn.weread.qq.com/weread/cover/16/yuewen_182788/t6_yuewen_1827881778320200.jpg",
|
"cover": "https://cdn.weread.qq.com/weread/cover/16/yuewen_182788/t6_yuewen_1827881778320200.jpg",
|
||||||
"progress": 51,
|
"progress": 100,
|
||||||
"finished": false,
|
"finished": true,
|
||||||
"lastReadAt": "2026-10-04T10:57:59.000Z",
|
"lastReadAt": "2026-10-08T11:42:40.000Z",
|
||||||
"startedAt": "2026-09-21T06:26:14.000Z",
|
"startedAt": "2026-09-21T06:26:14.000Z",
|
||||||
"finishedAt": null,
|
"finishedAt": "2026-10-08T11:30:06.000Z",
|
||||||
"readingSeconds": 4379,
|
"readingSeconds": 11837,
|
||||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=dcc3296052ca04dccdbef6c"
|
"deepLink": "https://weread.qq.com/book-detail?type=1&v=dcc3296052ca04dccdbef6c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -30,20 +86,6 @@
|
|||||||
"readingSeconds": 14442,
|
"readingSeconds": 14442,
|
||||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=34c329807197141b34cf1db"
|
"deepLink": "https://weread.qq.com/book-detail?type=1&v=34c329807197141b34cf1db"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"bookId": "3300175025",
|
|
||||||
"title": "此刻是春天(微信读书特别版)",
|
|
||||||
"author": "卢思浩",
|
|
||||||
"category": "精品小说-治愈小说",
|
|
||||||
"cover": "https://cdn.weread.qq.com/weread/cover/4/cpplatform_3hsoybvrwapq1efqha1mvz/t6_cpplatform_3hsoybvrwapq1efqha1mvz1762325065.jpg",
|
|
||||||
"progress": 4,
|
|
||||||
"finished": false,
|
|
||||||
"lastReadAt": "2026-09-27T16:39:50.000Z",
|
|
||||||
"startedAt": "2026-01-11T10:32:09.000Z",
|
|
||||||
"finishedAt": "2026-09-27T16:39:36.000Z",
|
|
||||||
"readingSeconds": 231,
|
|
||||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=b0b32630813abaadeg0157d1"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"bookId": "23914527",
|
"bookId": "23914527",
|
||||||
"title": "树莓派开始,玩转Linux",
|
"title": "树莓派开始,玩转Linux",
|
||||||
@@ -58,20 +100,6 @@
|
|||||||
"readingSeconds": 14629,
|
"readingSeconds": 14629,
|
||||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=140329d0716ce81f140468e"
|
"deepLink": "https://weread.qq.com/book-detail?type=1&v=140329d0716ce81f140468e"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"bookId": "3300207468",
|
|
||||||
"title": "快乐的死(果麦经典)",
|
|
||||||
"author": "[法]加缪",
|
|
||||||
"category": "文学-外国文学",
|
|
||||||
"cover": "https://cdn.weread.qq.com/weread/cover/12/cpplatform_tgx43jt54ezt7bamqcmjbk/t6_cpplatform_tgx43jt54ezt7bamqcmjbk1779878190.jpg",
|
|
||||||
"progress": 0,
|
|
||||||
"finished": false,
|
|
||||||
"lastReadAt": "2026-09-27T10:00:32.000Z",
|
|
||||||
"startedAt": null,
|
|
||||||
"finishedAt": null,
|
|
||||||
"readingSeconds": 38,
|
|
||||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=66b32530813abb78ag018a22"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"bookId": "23796324",
|
"bookId": "23796324",
|
||||||
"title": "昨日的世界",
|
"title": "昨日的世界",
|
||||||
@@ -380,5 +408,6 @@
|
|||||||
"readingSeconds": 137,
|
"readingSeconds": 137,
|
||||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=47532ac05c659b47554a825"
|
"deepLink": "https://weread.qq.com/book-detail?type=1&v=47532ac05c659b47554a825"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"albums": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,39 @@ export type ReadingData = {
|
|||||||
updatedAt: string
|
updatedAt: string
|
||||||
source: string
|
source: string
|
||||||
books: ReadingBook[]
|
books: ReadingBook[]
|
||||||
|
/**
|
||||||
|
* 专辑 / 有声书。
|
||||||
|
*
|
||||||
|
* 可选:`--soft` 构建路径下用的可能是更早版本的 reading.json,那时还没有这个字段。
|
||||||
|
* 调用方一律写 `data.albums ?? []`。
|
||||||
|
*/
|
||||||
|
albums?: ReadingAlbum[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一张有声书专辑。
|
||||||
|
*
|
||||||
|
* 字段刻意比 ReadingBook 少 —— 微信读书给专辑的原始结构里(albumInfo /
|
||||||
|
* albumInfoExtra)**没有阅读进度,也没有 deepLink**,所以这里没有 progress,
|
||||||
|
* 前端也不该伪造一条进度条。哪天官方补了字段,先改 scripts/fetch-reading.mjs。
|
||||||
|
*/
|
||||||
|
export type ReadingAlbum = {
|
||||||
|
albumId: string
|
||||||
|
name: string
|
||||||
|
/** 演播 / 作者 */
|
||||||
|
author: string
|
||||||
|
cover: string
|
||||||
|
/** 音频集数;取不到时为 0 */
|
||||||
|
trackCount: number
|
||||||
|
/** 是否已完结(原始字段 finish === 1) */
|
||||||
|
finished: boolean
|
||||||
|
/** 完结状态原文,如「已完结」 */
|
||||||
|
finishStatus: string
|
||||||
|
intro: string
|
||||||
|
/** 最近收听时间,ISO;从没听过时为 null */
|
||||||
|
lastListenedAt: string | null
|
||||||
|
/** 跳转链接。官方文档未列此字段,实测可能为空串 */
|
||||||
|
deepLink: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DATA_FILE = path.join(process.cwd(), "src/data/reading.json")
|
const DATA_FILE = path.join(process.cwd(), "src/data/reading.json")
|
||||||
|
|||||||
Reference in New Issue
Block a user