Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions clis/weread/book.js
Original file line number Diff line number Diff line change
@@ -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, '')
Expand Down Expand Up @@ -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 [{
Expand Down
4 changes: 2 additions & 2 deletions clis/weread/highlights.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 ?? '',
Expand Down
4 changes: 2 additions & 2 deletions clis/weread/notes.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions clis/weread/shelf.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
54 changes: 54 additions & 0 deletions clis/weread/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading