diff --git a/clis/weread/book.js b/clis/weread/book.js index eb9fb6bc3..7634abeed 100644 --- a/clis/weread/book.js +++ b/clis/weread/book.js @@ -1,6 +1,6 @@ import { cli, Strategy } from '@jackwener/opencli/registry'; import { CliError } from '@jackwener/opencli/errors'; -import { fetchPrivateApi, fetchWebApi, resolveShelfReader, WEREAD_UA, WEREAD_WEB_ORIGIN, } from './utils.js'; +import { fetchWebApiWithCookies, fetchWebApi, resolveShelfReader, WEREAD_UA, WEREAD_WEB_ORIGIN, } from './utils.js'; function decodeHtmlText(value) { return value .replace(/<[^>]+>/g, '') @@ -191,7 +191,7 @@ cli({ func: async (page, args) => { const bookId = String(args['book-id'] || '').trim(); try { - const data = await fetchPrivateApi(page, '/book/info', { bookId }); + const data = await fetchWebApiWithCookies(page, '/book/info', { bookId }); // newRating is 0-1000 scale per community docs; needs runtime verification const rating = data.newRating ? `${(data.newRating / 10).toFixed(1)}%` : '-'; return [{ diff --git a/clis/weread/highlights.js b/clis/weread/highlights.js index c56ea382d..17794f602 100644 --- a/clis/weread/highlights.js +++ b/clis/weread/highlights.js @@ -1,5 +1,5 @@ import { cli, Strategy } from '@jackwener/opencli/registry'; -import { fetchPrivateApi, formatDate } from './utils.js'; +import { fetchWebApiWithCookies, formatDate } from './utils.js'; cli({ site: 'weread', name: 'highlights', @@ -13,7 +13,7 @@ cli({ ], columns: ['chapter', 'text', 'createTime'], func: async (page, args) => { - const data = await fetchPrivateApi(page, '/book/bookmarklist', { bookId: args['book-id'] }); + const data = await fetchWebApiWithCookies(page, '/book/bookmarklist', { bookId: args['book-id'] }); const items = data?.updated ?? []; return items.slice(0, Number(args.limit)).map((item) => ({ chapter: item.chapterName ?? '', diff --git a/clis/weread/notes.js b/clis/weread/notes.js index 58de35600..24a81238b 100644 --- a/clis/weread/notes.js +++ b/clis/weread/notes.js @@ -1,5 +1,5 @@ import { cli, Strategy } from '@jackwener/opencli/registry'; -import { fetchPrivateApi, formatDate } from './utils.js'; +import { fetchWebApiWithCookies, formatDate } from './utils.js'; cli({ site: 'weread', name: 'notes', @@ -13,7 +13,7 @@ cli({ ], columns: ['chapter', 'text', 'review', 'createTime'], func: async (page, args) => { - const data = await fetchPrivateApi(page, '/review/list', { + const data = await fetchWebApiWithCookies(page, '/review/list', { bookId: args['book-id'], listType: '11', mine: '1', diff --git a/clis/weread/shelf.js b/clis/weread/shelf.js index 32412b411..2a83efd31 100644 --- a/clis/weread/shelf.js +++ b/clis/weread/shelf.js @@ -1,7 +1,7 @@ import { cli, Strategy } from '@jackwener/opencli/registry'; import { CliError } from '@jackwener/opencli/errors'; import { log } from '@jackwener/opencli/logger'; -import { buildWebShelfEntries, fetchPrivateApi, loadWebShelfSnapshot, } from './utils.js'; +import { buildWebShelfEntries, fetchWebApiWithCookies, loadWebShelfSnapshot, } from './utils.js'; function normalizeShelfLimit(limit) { if (!Number.isFinite(limit)) return 0; @@ -46,7 +46,7 @@ cli({ if (limit <= 0) return []; try { - const data = await fetchPrivateApi(page, '/shelf/sync', { synckey: '0', lectureSynckey: '0' }); + const data = await fetchWebApiWithCookies(page, '/shelf/sync', { synckey: '0', lectureSynckey: '0' }); return normalizePrivateApiRows(data, limit); } catch (error) { diff --git a/clis/weread/utils.js b/clis/weread/utils.js index 766b9c31c..df5492424 100644 --- a/clis/weread/utils.js +++ b/clis/weread/utils.js @@ -97,6 +97,60 @@ function buildShelfSnapshotPollScript(storageKeys, requireTrustedIndexes) { })) `; } +/** + * Fetch a WeRead web endpoint with cookies extracted from the browser. + * Uses the same-origin web API (weread.qq.com/web/*) which correctly receives + * auth cookies, unlike the cross-subdomain i.weread.qq.com private API. + */ +export async function fetchWebApiWithCookies(page, path, params) { + const url = new URL(`${WEB_API}${path}`); + if (params) { + for (const [k, v] of Object.entries(params)) + url.searchParams.set(k, v); + } + const urlStr = url.toString(); + const [apiCookies, domainCookies] = await Promise.all([ + page.getCookies({ url: urlStr }), + page.getCookies({ domain: WEREAD_DOMAIN }), + ]); + const merged = new Map(); + for (const c of domainCookies) + merged.set(c.name, c); + for (const c of apiCookies) + merged.set(c.name, c); + const cookieHeader = buildCookieHeader(Array.from(merged.values())); + let resp; + try { + resp = await fetch(urlStr, { + headers: { + 'User-Agent': WEREAD_UA, + 'Origin': 'https://weread.qq.com', + 'Referer': 'https://weread.qq.com/', + ...(cookieHeader ? { 'Cookie': cookieHeader } : {}), + }, + }); + } + catch (error) { + throw new CliError('FETCH_ERROR', `Failed to fetch ${path}: ${error instanceof Error ? error.message : String(error)}`, 'WeRead API may be temporarily unavailable'); + } + let data; + try { + data = await resp.json(); + } + catch { + throw new CliError('PARSE_ERROR', `Invalid JSON response for ${path}`, 'WeRead may have returned an HTML error page'); + } + if (isAuthErrorResponse(resp, data)) { + throw new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'); + } + if (!resp.ok) { + throw new CliError('FETCH_ERROR', `HTTP ${resp.status} for ${path}`, 'WeRead API may be temporarily unavailable'); + } + if (data?.errcode != null && data.errcode !== 0) { + throw new CliError('API_ERROR', data.errmsg ?? `WeRead API error ${data.errcode}`); + } + return data; +} /** * Fetch a public WeRead web endpoint (Node.js direct fetch). * Used by search and ranking commands (browser: false).