From 8fc8281e9441dbe6d93538982ff689f07e484e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=81=97=E5=BF=98=E7=9A=84=E8=AE=B0=E5=BF=86?= <32097237+YIWANG-sketch@users.noreply.github.com> Date: Sun, 16 Feb 2025 23:15:21 +0800 Subject: [PATCH] Create cookieUpdater.mjs --- you_providers/cookieUpdater.mjs | 185 ++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 you_providers/cookieUpdater.mjs diff --git a/you_providers/cookieUpdater.mjs b/you_providers/cookieUpdater.mjs new file mode 100644 index 0000000..e18fd13 --- /dev/null +++ b/you_providers/cookieUpdater.mjs @@ -0,0 +1,185 @@ +import fs from "fs"; +import path from "path"; +import {fileURLToPath} from "url"; +import {Mutex} from "async-mutex"; + +const configMutex = new Mutex(); // 互斥锁 + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const CONFIG_FILE_PATH = path.join(__dirname, "../config.mjs"); + +// 仅在 USE_MANUAL_LOGIN 为 false 且 ENABLE_AUTO_COOKIE_UPDATE 为 true 时生效 +const ENABLE_AUTO_COOKIE_UPDATE = process.env.ENABLE_AUTO_COOKIE_UPDATE === "true"; + +/** + * cookies 解析出 DS 与 DSR + * @param {Array} cookies 获取到的 cookie 数组 + * @returns {{ ds?: string, dsr?: string }} + */ +function parseDSAndDSR(cookies) { + let dsValue, dsrValue; + for (const c of cookies) { + if (c.name === "DS") { + dsValue = c.value; + } else if (c.name === "DSR") { + dsrValue = c.value; + } + } + return {ds: dsValue, dsr: dsrValue}; +} + +/** + * 从 DS 中解析 email 字段 + * @param {string} dsToken DS cookie + * @returns {string|null} 返回 email或null + */ +function decodeEmailFromDs(dsToken) { + try { + const parts = dsToken.split("."); + if (parts.length < 2) return null; + const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8")); + return payload.email || null; + } catch (err) { + return null; + } +} + +/** + * cookie 数组转换 "name=value; name=value" + * @param {Array} cookies + * @returns {string} + */ +function cookiesToStringAll(cookies) { + return cookies.map(c => `${c.name}=${c.value}`).join("; "); +} + +/** + * cookie 转换数组 + * 每个对象如 { name, value } + * @param {string} cookieStr + * @returns {Array} + */ +function parseCookieString(cookieStr) { + return cookieStr.split("; ").map(entry => { + const [name, value] = entry.split("=", 2); + return {name, value}; + }); +} + +/** + * 本地 configObj.sessions 查找与指定 email 匹配的 session, + * @param {object} configObj 解析后 config + * @param {string} email 匹配的邮箱 + * @returns {{ index: number, oldCookie: string, ds: string, dsr: string } | null} + */ +function findSessionByEmail(configObj, email) { + if (!Array.isArray(configObj.sessions)) return null; + for (let i = 0; i < configObj.sessions.length; i++) { + const cookieStr = configObj.sessions[i].cookie || ""; + const dsMatch = /DS=([^;\s]+)/.exec(cookieStr); + if (!dsMatch) continue; + const dsValue = dsMatch[1]; + const dsEmail = decodeEmailFromDs(dsValue); + if (dsEmail && dsEmail.toLowerCase() === email.toLowerCase()) { + const dsrMatch = /DSR=([^;\s]+)/.exec(cookieStr); + const dsrValue = dsrMatch ? dsrMatch[1] : ""; + return { + index: i, + oldCookie: cookieStr, + ds: dsValue, + dsr: dsrValue + }; + } + } + return null; +} + +/** + * config.mjs 中匹配相同 email 的 session,若 DS 或 DSR 有变化,则更新整个 cookie + * @param {import('puppeteer-core').Page} page + */ +export async function updateLocalConfigCookieByEmail(page) { + if (!ENABLE_AUTO_COOKIE_UPDATE || process.env.USE_MANUAL_LOGIN === "true") { + return; + } + // 尝试从 “https://you.com/api/instrumentation” 获取 cookie + let cookieStringFromInstrumentation = ""; + try { + const instrRequest = await page.waitForRequest( + req => req.url().includes("/api/instrumentation"), + {timeout: 5000} + ); + if (instrRequest) { + cookieStringFromInstrumentation = instrRequest.headers()["cookie"]; + } + } catch (err) { + } + + let allCookiesString = ""; + if (cookieStringFromInstrumentation) { + allCookiesString = cookieStringFromInstrumentation; + } else { + // 使用 page.cookies() 获取 + const cookies = await page.cookies("https://you.com"); + allCookiesString = cookiesToStringAll(cookies); + } + + const cookieArray = parseCookieString(allCookiesString); + const {ds: newDs, dsr: newDsr} = parseDSAndDSR(cookieArray); + if (!newDs) { + console.log("网页未找到 DS,跳过更新。"); + return; + } + const newEmail = decodeEmailFromDs(newDs); + if (!newEmail) { + console.log("[网页无法从 DS 解出 email,跳过更新。"); + return; + } + + // 互斥区 + await configMutex.runExclusive(async () => { + try { + if (!fs.existsSync(CONFIG_FILE_PATH)) { + console.warn(`找不到 config.mjs: ${CONFIG_FILE_PATH}`); + return; + } + const raw = fs.readFileSync(CONFIG_FILE_PATH, "utf8"); + const jsonString = raw.replace(/^export const config\s*=\s*/, ""); + const configObj = JSON.parse(jsonString); + + const found = findSessionByEmail(configObj, newEmail); + if (!found) { + console.log(`未能在 config 中找到 email=${newEmail} 的 session,跳过更新。`); + return; + } + + if (found.ds === newDs && found.dsr === newDsr) { + console.log(`DS/DSR 未变化(email=${newEmail}),不更新。`); + return; + } + + configObj.sessions[found.index].cookie = allCookiesString; + + const newFileContent = "export const config = " + JSON.stringify(configObj, null, 4); + fs.writeFileSync(CONFIG_FILE_PATH, newFileContent, "utf8"); + + console.log(`Cookie已更新(email=${newEmail})`); + } catch (err) { + console.warn("Cookie更新过程出错:", err); + } + }); +} + +/** + * 非阻塞 + * @param {import('puppeteer-core').Page} page + */ +export function updateLocalConfigCookieByEmailNonBlocking(page) { + // 保证异步 + setImmediate(() => { + updateLocalConfigCookieByEmail(page).catch(err => + console.error("Cookie update error:", err) + ); + }); +}