Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

204 lines
7.9 KiB
JavaScript

import { AuthRequiredError, CliError } from '@jackwener/opencli/errors';
import { NOTEBOOKLM_DOMAIN } from './shared.js';
export function unwrapNotebooklmEvaluateResult(payload) {
if (payload && typeof payload === 'object' && !Array.isArray(payload) && 'session' in payload && 'data' in payload) {
return payload.data;
}
return payload;
}
export function extractNotebooklmPageAuthFromHtml(html, sourcePath = '/', preferredTokens) {
const csrfMatch = html.match(/"SNlM0e":"([^"]+)"/);
const sessionMatch = html.match(/"FdrFJe":"([^"]+)"/);
const csrfToken = preferredTokens?.csrfToken?.trim() || (csrfMatch ? csrfMatch[1] : '');
const sessionId = preferredTokens?.sessionId?.trim() || (sessionMatch ? sessionMatch[1] : '');
if (!csrfToken || !sessionId) {
throw new CliError('NOTEBOOKLM_TOKENS', 'NotebookLM page tokens were not found in the current page HTML', 'Open the NotebookLM notebook page in Chrome, wait for it to finish loading, then retry with --verbose if it still fails.');
}
return { csrfToken, sessionId, sourcePath: sourcePath || '/', authuser: preferredTokens?.authuser ?? '' };
}
async function probeNotebooklmPageAuth(page) {
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
const wiz = window.WIZ_global_data || {};
const html = document.documentElement.innerHTML;
const authMatch = (location.search || '').match(/[?&]authuser=(\\d+)/);
const pathMatch = (location.pathname || '').match(/^\\/u\\/(\\d+)\\//);
return {
html,
sourcePath: location.pathname || '/',
readyState: document.readyState || '',
csrfToken: typeof wiz.SNlM0e === 'string' ? wiz.SNlM0e : '',
sessionId: typeof wiz.FdrFJe === 'string' ? wiz.FdrFJe : '',
authuser: authMatch ? authMatch[1] : (pathMatch ? pathMatch[1] : ''),
};
})()`));
return {
html: String(raw?.html ?? ''),
sourcePath: String(raw?.sourcePath ?? '/'),
readyState: String(raw?.readyState ?? ''),
csrfToken: String(raw?.csrfToken ?? ''),
sessionId: String(raw?.sessionId ?? ''),
authuser: String(raw?.authuser ?? ''),
};
}
export async function getNotebooklmPageAuth(page) {
let lastError;
for (let attempt = 0; attempt < 2; attempt += 1) {
const probe = await probeNotebooklmPageAuth(page);
try {
return extractNotebooklmPageAuthFromHtml(probe.html, probe.sourcePath, { csrfToken: probe.csrfToken, sessionId: probe.sessionId, authuser: probe.authuser });
}
catch (error) {
lastError = error;
if (attempt === 0 && typeof page.wait === 'function') {
await page.wait(0.5).catch(() => undefined);
continue;
}
}
}
throw lastError;
}
export function buildNotebooklmRpcBody(rpcId, params, csrfToken) {
const rpcRequest = [[[rpcId, JSON.stringify(params), null, 'generic']]];
return `f.req=${encodeURIComponent(JSON.stringify(rpcRequest))}&at=${encodeURIComponent(csrfToken)}&`;
}
export function stripNotebooklmAntiXssi(rawBody) {
if (!rawBody.startsWith(")]}'"))
return rawBody;
return rawBody.replace(/^\)\]\}'\r?\n/, '');
}
export function parseNotebooklmChunkedResponse(rawBody) {
const cleaned = stripNotebooklmAntiXssi(rawBody).trim();
if (!cleaned)
return [];
const lines = cleaned.split('\n');
const chunks = [];
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i].trim();
if (!line)
continue;
if (/^\d+$/.test(line)) {
const nextLine = lines[i + 1];
if (!nextLine)
continue;
try {
chunks.push(JSON.parse(nextLine));
}
catch {
// Ignore malformed chunks and keep scanning.
}
i += 1;
continue;
}
if (line.startsWith('[')) {
try {
chunks.push(JSON.parse(line));
}
catch {
// Ignore malformed chunks and keep scanning.
}
}
}
return chunks;
}
export function extractNotebooklmRpcResult(rawBody, rpcId) {
const chunks = parseNotebooklmChunkedResponse(rawBody);
for (const chunk of chunks) {
if (!Array.isArray(chunk))
continue;
const items = Array.isArray(chunk[0]) ? chunk : [chunk];
for (const item of items) {
if (!Array.isArray(item) || item.length < 1)
continue;
if (item[0] === 'er') {
const errorCode = typeof item[2] === 'number'
? item[2]
: typeof item[5] === 'number'
? item[5]
: null;
if (errorCode === 401 || errorCode === 403) {
throw new AuthRequiredError(NOTEBOOKLM_DOMAIN, `NotebookLM RPC returned auth error (${errorCode})`);
}
throw new CliError('NOTEBOOKLM_RPC', `NotebookLM RPC failed${errorCode ? ` (code=${errorCode})` : ''}`, 'Retry from an already logged-in NotebookLM session, or inspect the raw response with debug logging.');
}
if (item[0] === 'wrb.fr' && item[1] === rpcId) {
const payload = item[2];
if (typeof payload === 'string') {
try {
return JSON.parse(payload);
}
catch {
return payload;
}
}
return payload;
}
}
}
return null;
}
export async function fetchNotebooklmInPage(page, url, options = {}) {
const method = options.method ?? 'GET';
const headers = options.headers ?? {};
const body = options.body ?? '';
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(async () => {
const request = {
url: ${JSON.stringify(url)},
method: ${JSON.stringify(method)},
headers: ${JSON.stringify(headers)},
body: ${JSON.stringify(body)},
};
const response = await fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.method === 'GET' ? undefined : request.body,
credentials: 'include',
});
return {
ok: response.ok,
status: response.status,
body: await response.text(),
finalUrl: response.url,
};
})()`));
return {
ok: Boolean(raw?.ok),
status: Number(raw?.status ?? 0),
body: String(raw?.body ?? ''),
finalUrl: String(raw?.finalUrl ?? url),
};
}
export async function callNotebooklmRpc(page, rpcId, params, options = {}) {
const auth = await getNotebooklmPageAuth(page);
const requestBody = buildNotebooklmRpcBody(rpcId, params, auth.csrfToken);
const authuser = auth.authuser || '';
const url = `https://${NOTEBOOKLM_DOMAIN}/_/LabsTailwindUi/data/batchexecute` +
`?rpcids=${rpcId}&source-path=${encodeURIComponent(auth.sourcePath)}` +
(authuser ? `&authuser=${encodeURIComponent(authuser)}` : '') +
`&hl=${encodeURIComponent(options.hl ?? 'en')}` +
`&f.sid=${encodeURIComponent(auth.sessionId)}&rt=c`;
const response = await fetchNotebooklmInPage(page, url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
body: requestBody,
});
if (response.status === 401 || response.status === 403) {
throw new AuthRequiredError(NOTEBOOKLM_DOMAIN, `NotebookLM RPC returned auth error (${response.status})`);
}
if (!response.ok) {
throw new CliError('NOTEBOOKLM_RPC', `NotebookLM RPC request failed with HTTP ${response.status}`, 'Retry from the NotebookLM home page in an already logged-in Chrome session.');
}
return {
auth,
url,
requestBody,
response,
result: extractNotebooklmRpcResult(response.body, rpcId),
};
}