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
204 lines
7.9 KiB
JavaScript
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),
|
|
};
|
|
}
|