Skip to main content
Glama

WeRead MCP Server

by freestylefly
import * as crypto from 'crypto'; import axios from 'axios'; import dotenv from 'dotenv'; dotenv.config(); // API URL常量 const WEREAD_URL = "https://weread.qq.com/"; const WEREAD_NOTEBOOKS_URL = "https://weread.qq.com/api/user/notebook"; const WEREAD_BOOK_INFO_URL = "https://weread.qq.com/api/book/info"; const WEREAD_BOOKMARKLIST_URL = "https://weread.qq.com/web/book/bookmarklist"; const WEREAD_CHAPTER_INFO_URL = "https://weread.qq.com/web/book/chapterInfos"; const WEREAD_REVIEW_LIST_URL = "https://weread.qq.com/web/review/list"; const WEREAD_READ_INFO_URL = "https://weread.qq.com/web/book/getProgress"; const WEREAD_SHELF_SYNC_URL = "https://weread.qq.com/web/shelf/sync"; const WEREAD_BEST_REVIEW_URL = "https://weread.qq.com/web/review/list/best"; interface ChapterInfo { chapterUid: number; chapterIdx: number; updateTime: number; readAhead: number; title: string; level: number; } // 获取命令行参数 interface CommandArgs { WEREAD_COOKIE?: string; CC_URL?: string; CC_ID?: string; CC_PASSWORD?: string; } export class WeReadApi { private cookie: string = ""; private axiosInstance: any; private initialized: boolean = false; private commandArgs: CommandArgs = {}; constructor() { this.parseCommandArgs(); this.initAsync().catch(error => { console.error("初始化WeReadApi失败:", error); }); } // 解析命令行参数 private parseCommandArgs(): void { try { const args = process.argv; const argsIndex = args.findIndex(arg => arg === '--args'); if (argsIndex !== -1 && argsIndex + 1 < args.length) { try { this.commandArgs = JSON.parse(args[argsIndex + 1]); } catch (error) { console.error("解析命令行参数失败:", error); } } } catch (error) { console.error("处理命令行参数时出错:", error); } } private async initAsync(): Promise<void> { if (this.initialized) return; try { this.cookie = await this.getCookie(); this.axiosInstance = axios.create({ headers: { 'Cookie': this.cookie, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' }, timeout: 60000 // 设置60秒超时 }); this.initialized = true; } catch (error) { console.error("初始化失败:", error); throw error; } } private async tryGetCloudCookie(url: string, id: string, password: string): Promise<string | null> { if (url.endsWith("/")) { url = url.slice(0, -1); } const reqUrl = `${url}/get/${id}`; const data = { password }; try { const response = await axios.post(reqUrl, data, { timeout: 30000 }); if (response.status === 200) { const responseData = response.data; if (responseData.cookie_data) { // 打印所有可用的域名 const domains = Object.keys(responseData.cookie_data); // 首先尝试 "weread.qq.com" 域名 if ("weread.qq.com" in responseData.cookie_data) { return this.extractCookiesFromDomain(responseData.cookie_data, "weread.qq.com"); } // 然后尝试 "weread" 域名 if ("weread" in responseData.cookie_data) { // 检查这些Cookie是否真的是weread.qq.com的Cookie const wereadCookies = responseData.cookie_data["weread"]; const validWereadCookies = wereadCookies.filter((cookie: any) => cookie.domain && (cookie.domain === ".weread.qq.com" || cookie.domain === "weread.qq.com") ); if (validWereadCookies.length > 0) { const cookieItems = validWereadCookies.map((cookie: any) => `${cookie.name}=${cookie.value}`); return cookieItems.join("; "); } else { console.warn(`[CookieCloud] weread域名下的Cookie不属于微信读书`); } } // 最后尝试遍历所有域名,寻找包含weread.qq.com域名的Cookie console.warn(`[CookieCloud] 尝试从所有域名中查找微信读书Cookie`); for (const domain of domains) { const cookiesInDomain = responseData.cookie_data[domain]; if (Array.isArray(cookiesInDomain)) { const wereadCookies = cookiesInDomain.filter((cookie: any) => cookie.domain && (cookie.domain === ".weread.qq.com" || cookie.domain === "weread.qq.com") ); if (wereadCookies.length > 0) { console.warn(`[CookieCloud] 在${domain}域名下找到${wereadCookies.length}个微信读书Cookie`); const cookieItems = wereadCookies.map((cookie: any) => `${cookie.name}=${cookie.value}`); return cookieItems.join("; "); } } } } else { console.warn(`[CookieCloud] 响应中没有cookie_data字段`); } } console.warn("[CookieCloud] 从Cookie Cloud获取数据成功,但未找到微信读书Cookie"); } catch (error: any) { console.error("[CookieCloud] 从Cookie Cloud获取Cookie失败:", error.message); if (error.response) { console.error(`[CookieCloud] 响应状态: ${error.response.status}`); } } return null; } // 从指定域名提取Cookie private extractCookiesFromDomain(cookieData: any, domain: string): string | null { const cookies = cookieData[domain]; if (!Array.isArray(cookies) || cookies.length === 0) { return null; } const cookieItems = []; for (const cookie of cookies) { if (cookie.name && cookie.value) { cookieItems.push(`${cookie.name}=${cookie.value}`); } } if (cookieItems.length === 0) { return null; } return cookieItems.join("; "); } private async getCookie(): Promise<string> { // 优先级: // 1. 命令行参数中的WEREAD_COOKIE // 2. 命令行参数中的Cookie Cloud配置 // 3. 环境变量中的Cookie Cloud配置 // 4. 环境变量中的WEREAD_COOKIE let cookie: string | null = null; // 1. 检查命令行参数中的直接Cookie if (this.commandArgs.WEREAD_COOKIE) { return this.commandArgs.WEREAD_COOKIE; } // 2. 检查命令行参数中的Cookie Cloud配置 if (this.commandArgs.CC_URL && this.commandArgs.CC_ID && this.commandArgs.CC_PASSWORD) { try { cookie = await this.tryGetCloudCookie( this.commandArgs.CC_URL, this.commandArgs.CC_ID, this.commandArgs.CC_PASSWORD ); if (cookie) { return cookie; } } catch (error) { console.warn("使用命令行参数中的Cookie Cloud配置获取Cookie失败"); } } // 3. 尝试环境变量中的Cookie Cloud配置 const envUrl = process.env.CC_URL; const envId = process.env.CC_ID; const envPassword = process.env.CC_PASSWORD; if (envUrl && envId && envPassword) { try { cookie = await this.tryGetCloudCookie(envUrl, envId, envPassword); if (cookie) { return cookie; } } catch (error) { console.warn("使用环境变量中的Cookie Cloud配置获取Cookie失败"); } } // 4. 回退到环境变量中的直接Cookie const envCookie = process.env.WEREAD_COOKIE; if (!envCookie || !envCookie.trim()) { throw new Error("没有找到cookie,请按照文档填写cookie或配置Cookie Cloud"); } return envCookie; } private handleErrcode(errcode: number): void { if (errcode === -2012 || errcode === -2010) { console.error("微信读书Cookie过期了,请参考文档重新设置。"); } } private async retry<T>(func: () => Promise<T>, maxAttempts = 3, waitMs = 5000): Promise<T> { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await func(); } catch (error: any) { if (error.response) { console.error(`响应状态: ${error.response.status}`); } if (attempt === maxAttempts) { throw error; } const randomWait = waitMs + Math.floor(Math.random() * 3000); console.warn(`第${attempt}次尝试失败,${randomWait}ms后重试...`); await new Promise(resolve => setTimeout(resolve, randomWait)); } } throw new Error("所有重试都失败了"); } private async ensureInitialized(): Promise<void> { if (!this.initialized) { await this.initAsync(); } } private getStandardHeaders(): Record<string, string> { return { 'Cookie': this.cookie, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', 'Connection': 'keep-alive', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'sec-ch-ua': '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'same-origin', 'upgrade-insecure-requests': '1' }; } private async visitHomepage(): Promise<void> { try { await axios.get(WEREAD_URL, { headers: this.getStandardHeaders(), timeout: 30000 }); } catch (error: any) { console.error("访问主页失败:", error.message); } } private async makeApiRequest<T>( url: string, method: 'get' | 'post' = 'get', params: Record<string, any> = {}, data: any = null ): Promise<T> { await this.ensureInitialized(); // 向所有GET请求添加时间戳避免缓存 if (method === 'get') { params._ = new Date().getTime(); } try { let response; if (method === 'get') { response = await this.axiosInstance.get(url, { params }); } else { response = await this.axiosInstance.post(url, data, { params }); } if (response.data.errcode !== undefined && response.data.errcode !== 0) { this.handleErrcode(response.data.errcode); throw new Error(`API返回错误: ${response.data.errmsg || 'Unknown error'} (code: ${response.data.errcode})`); } return response.data; } catch (error: any) { console.error(`API请求失败 (${url}):`, error.message); throw error; } } // 获取书架信息(存在笔记的书籍) public async getBookshelf(): Promise<any> { await this.ensureInitialized(); return this.retry(async () => { const data = await this.makeApiRequest<any>(WEREAD_NOTEBOOKS_URL, "get"); return data; }); } // 获取所有书架书籍信息 public async getEntireShelf(): Promise<any> { await this.ensureInitialized(); return this.retry(async () => { return await this.makeApiRequest<any>(WEREAD_SHELF_SYNC_URL, "get"); }); } // 获取笔记本列表 public async getNotebooklist(): Promise<any[]> { await this.ensureInitialized(); return this.retry(async () => { const data = await this.makeApiRequest<any>(WEREAD_NOTEBOOKS_URL, "get"); return data.books || []; }); } // 获取书籍信息 public async getBookinfo(bookId: string): Promise<any> { await this.ensureInitialized(); return this.retry(async () => { return await this.makeApiRequest<any>(WEREAD_BOOK_INFO_URL, "get", { bookId }); }); } // 获取书籍的划线记录 public async getBookmarkList(bookId: string): Promise<any[]> { await this.ensureInitialized(); return this.retry(async () => { const data = await this.makeApiRequest<any>(WEREAD_BOOKMARKLIST_URL, "get", { bookId }); let bookmarks = data.updated || []; // 确保每个划线对象格式一致 bookmarks = bookmarks.filter((mark: any) => mark.markText && mark.chapterUid); return bookmarks; }); } // 获取阅读进度 public async getReadInfo(bookId: string): Promise<any> { await this.ensureInitialized(); return this.retry(async () => { return await this.makeApiRequest<any>(WEREAD_READ_INFO_URL, "get", { bookId }); }); } // 获取笔记/想法列表 public async getReviewList(bookId: string): Promise<any[]> { await this.ensureInitialized(); return this.retry(async () => { const data = await this.makeApiRequest<any>(WEREAD_REVIEW_LIST_URL, "get", { bookId, listType: 4, maxIdx: 0, count: 0, listMode: 2, syncKey: 0 }); let reviews = data.reviews || []; // 转换成正确的格式 reviews = reviews.map((x: any) => x.review); // 为书评添加chapterUid reviews = reviews.map((x: any) => { if (x.type === 4) { return { chapterUid: 1000000, ...x }; } return x; }); return reviews; }); } // 获取热门书评 public async getBestReviews(bookId: string, count: number = 10, maxIdx: number = 0, synckey: number = 0): Promise<any> { await this.ensureInitialized(); return this.retry(async () => { const data = await this.makeApiRequest<any>(WEREAD_BEST_REVIEW_URL, "get", { bookId, synckey, maxIdx, count }); return data; }); } // 获取章节信息 public async getChapterInfo(bookId: string): Promise<Record<string, ChapterInfo>> { await this.ensureInitialized(); return this.retry(async () => { try { // 1. 首先访问主页,确保会话有效 await this.visitHomepage(); // 2. 获取笔记本列表,进一步初始化会话 await this.getNotebooklist(); // 3. 添加随机延迟,模拟真实用户行为 const delay = 1000 + Math.floor(Math.random() * 2000); await new Promise(resolve => setTimeout(resolve, delay)); // 4. 从cookie中提取关键信息 let wr_vid = ''; let wr_skey = ''; // 提取wr_vid和wr_skey const vidMatch = this.cookie.match(/wr_vid=([^;]+)/); const skeyMatch = this.cookie.match(/wr_skey=([^;]+)/); if (vidMatch) wr_vid = vidMatch[1]; if (skeyMatch) wr_skey = skeyMatch[1]; // 5. 请求章节信息 - 模拟浏览器行为 const url = `${WEREAD_CHAPTER_INFO_URL}`; // 添加请求参数 const params: Record<string, any> = { _: new Date().getTime() }; // 使用包含更多信息的请求头 const headers = { 'Cookie': this.cookie, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', 'Content-Type': 'application/json;charset=UTF-8', 'Accept': 'application/json, text/plain, */*', 'Origin': 'https://weread.qq.com', 'Referer': `https://weread.qq.com/web/reader/${bookId}`, 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', }; // 使用正确的请求体格式 const body = JSON.stringify({ bookIds: [bookId] }); // 直接通过axios请求 const response = await axios({ method: 'post', url: url, params: params, headers: headers, data: body, timeout: 60000 }); const data = response.data; // 6. 处理结果 - 增加多种可能的响应格式处理 let update = null; // 格式1: {data: [{bookId: "xxx", updated: []}]} if (data.data && data.data.length === 1 && data.data[0].updated) { update = data.data[0].updated; } // 格式2: {updated: []} else if (data.updated && Array.isArray(data.updated)) { update = data.updated; } // 格式3: [{bookId: "xxx", updated: []}] else if (Array.isArray(data) && data.length > 0 && data[0].updated) { update = data[0].updated; } // 格式4: 数组本身就是章节列表 else if (Array.isArray(data) && data.length > 0 && data[0].chapterUid) { update = data; } if (update) { // 添加点评章节 update.push({ chapterUid: 1000000, chapterIdx: 1000000, updateTime: 1683825006, readAhead: 0, title: "点评", level: 1 }); // 确保chapterUid始终以字符串形式作为键 const result = update.reduce((acc: Record<string, ChapterInfo>, curr: ChapterInfo) => { // 显式转换为字符串 const chapterUidStr = String(curr.chapterUid); acc[chapterUidStr] = curr; return acc; }, {}); return result; } else if (data.errCode) { this.handleErrcode(data.errCode); throw new Error(`API返回错误: ${data.errMsg || 'Unknown error'} (code: ${data.errCode})`); } else if (data.errcode) { this.handleErrcode(data.errcode); throw new Error(`API返回错误: ${data.errmsg || 'Unknown error'} (code: ${data.errcode})`); } else { throw new Error(`获取章节信息失败,返回格式不符合预期`); } } catch (error: any) { console.error(`获取章节信息失败:`, error.message); if (error.response) { console.error(`状态码: ${error.response.status}`); } throw error; } }); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freestylefly/mcp-server-weread'

If you have feedback or need assistance with the MCP directory API, please join our Discord server