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

815 lines
32 KiB
JavaScript

import { ArgumentError, AuthRequiredError, CliError, CommandExecutionError } from '@jackwener/opencli/errors';
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_HOME_URL, } from './shared.js';
import { callNotebooklmRpc, getNotebooklmPageAuth, unwrapNotebooklmEvaluateResult, } from './rpc.js';
export { buildNotebooklmRpcBody, extractNotebooklmRpcResult, fetchNotebooklmInPage, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, stripNotebooklmAntiXssi, } from './rpc.js';
const NOTEBOOKLM_LIST_RPC_ID = 'wXbhsf';
const NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID = 'rLM1Ne';
const NOTEBOOKLM_HISTORY_THREADS_RPC_ID = 'hPTbtc';
const NOTEBOOKLM_HISTORY_DETAIL_RPC_ID = 'khqZz';
function unwrapNotebooklmSingletonResult(result) {
let current = result;
while (Array.isArray(current) && current.length === 1 && Array.isArray(current[0])) {
current = current[0];
}
return current;
}
export function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
export function parseNotebooklmIdFromUrl(url) {
const match = url.match(/\/notebook\/([^/?#]+)/);
return match?.[1] ?? '';
}
const NOTEBOOK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function ensureNotebookUuid(candidate) {
if (!NOTEBOOK_UUID_RE.test(candidate)) {
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', `NotebookLM notebook id "${candidate}" is not a valid UUID`, 'Pass a notebook id from `opencli notebooklm list` or a full notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
}
return candidate;
}
export function parseNotebooklmNotebookTarget(value) {
const normalized = value.trim();
if (!normalized) {
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook id is required', 'Pass a notebook id from `opencli notebooklm list` or a full notebook URL.');
}
if (/^https?:\/\//i.test(normalized)) {
let parsed;
try {
parsed = new URL(normalized);
}
catch {
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL is invalid', 'Pass a full NotebookLM notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
}
if (parsed.protocol !== 'https:' || parsed.hostname !== NOTEBOOKLM_DOMAIN || parsed.username || parsed.password || parsed.port) {
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL must be a canonical https://notebooklm.google.com URL', 'Pass a notebook id from `opencli notebooklm list` or a full NotebookLM notebook URL.');
}
const notebookId = parseNotebooklmIdFromUrl(normalized);
if (!notebookId) {
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL is invalid', 'Pass a full NotebookLM notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
}
return ensureNotebookUuid(notebookId);
}
const pathMatch = normalized.match(/(?:^|\/)notebook\/([^/?#]+)/);
if (pathMatch?.[1])
return ensureNotebookUuid(pathMatch[1]);
return ensureNotebookUuid(normalized);
}
export function getNotebooklmAuthuser() {
const v = process.env.OPENCLI_NOTEBOOKLM_AUTHUSER;
return typeof v === 'string' && /^\d+$/.test(v) ? v : '';
}
export function requireNotebooklmExecute(value, action) {
if (value !== true) {
throw new ArgumentError(`Refusing to ${action}: pass --execute to perform this NotebookLM write`);
}
}
export function buildNotebooklmNotebookUrl(notebookId) {
const u = new URL(`/notebook/${encodeURIComponent(notebookId)}`, NOTEBOOKLM_HOME_URL);
const authuser = getNotebooklmAuthuser();
if (authuser) u.searchParams.set('authuser', authuser);
return u.toString();
}
export function classifyNotebooklmPage(url) {
try {
const parsed = new URL(url);
if (parsed.hostname !== NOTEBOOKLM_DOMAIN)
return 'unknown';
if (/\/notebook\/[^/?#]+/.test(parsed.pathname))
return 'notebook';
return 'home';
}
catch {
return 'unknown';
}
}
export function normalizeNotebooklmTitle(value, fallback = '') {
if (typeof value !== 'string')
return fallback;
let normalized = value.replace(/\s+/g, ' ').trim();
if (/^Untitled\b/i.test(normalized) && /otebook$/i.test(normalized) && normalized !== 'Untitled notebook') {
normalized = 'Untitled notebook';
}
return normalized || fallback;
}
function normalizeNotebooklmCreatedAt(value) {
if (typeof value !== 'string')
return null;
const trimmed = value.trim();
if (!trimmed)
return null;
const parsed = Date.parse(trimmed);
if (Number.isNaN(parsed))
return trimmed;
return new Date(parsed).toISOString();
}
function toNotebooklmIsoTimestamp(epochSeconds) {
if (typeof epochSeconds === 'number' && Number.isFinite(epochSeconds)) {
try {
return new Date(epochSeconds * 1000).toISOString();
}
catch {
return null;
}
}
if (Array.isArray(epochSeconds) && typeof epochSeconds[0] === 'number' && Number.isFinite(epochSeconds[0])) {
const seconds = epochSeconds[0];
const nanos = typeof epochSeconds[1] === 'number' && Number.isFinite(epochSeconds[1]) ? epochSeconds[1] : 0;
try {
return new Date(seconds * 1000 + Math.floor(nanos / 1_000_000)).toISOString();
}
catch {
return null;
}
}
return null;
}
function parseNotebooklmSourceTypeCode(value) {
if (typeof value === 'number' && Number.isFinite(value))
return value;
if (!Array.isArray(value) || typeof value[1] !== 'number' || !Number.isFinite(value[1]))
return null;
return value[1];
}
function parseNotebooklmSourceType(value) {
const code = parseNotebooklmSourceTypeCode(value);
if (code === 8)
return 'pasted-text';
if (code === 9)
return 'youtube';
if (code === 2)
return 'generated-text';
if (code === 3)
return 'pdf';
if (code === 4)
return 'audio';
if (code === 5)
return 'web';
if (code === 6)
return 'video';
return code == null ? null : `type-${code}`;
}
function findFirstNotebooklmString(value) {
if (typeof value === 'string' && value.trim())
return value.trim();
if (!Array.isArray(value))
return null;
for (const item of value) {
const found = findFirstNotebooklmString(item);
if (found)
return found;
}
return null;
}
function isNotebooklmUuid(value) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
}
function collectNotebooklmStrings(value, results) {
if (typeof value === 'string') {
const normalized = normalizeNotebooklmTitle(value);
if (!normalized)
return results;
if (isNotebooklmUuid(normalized))
return results;
if (/^[\d\s]+$/.test(normalized))
return results;
if (/^(null|undefined)$/i.test(normalized))
return results;
results.push(normalized);
return results;
}
if (!Array.isArray(value))
return results;
for (const item of value)
collectNotebooklmStrings(item, results);
return results;
}
function collectNotebooklmLeafStrings(value, results) {
if (typeof value === 'string') {
const normalized = value.trim();
if (normalized)
results.push(normalized);
return results;
}
if (!Array.isArray(value))
return results;
for (const item of value)
collectNotebooklmLeafStrings(item, results);
return results;
}
function collectNotebooklmThreadIds(value, results, seen) {
if (typeof value === 'string') {
const normalized = value.trim();
if (isNotebooklmUuid(normalized) && !seen.has(normalized)) {
seen.add(normalized);
results.push(normalized);
}
return results;
}
if (!Array.isArray(value))
return results;
for (const item of value)
collectNotebooklmThreadIds(item, results, seen);
return results;
}
export function parseNotebooklmHistoryThreadIdsResult(result) {
return collectNotebooklmThreadIds(result, [], new Set());
}
export function extractNotebooklmHistoryPreview(result) {
const strings = collectNotebooklmStrings(result, []);
return strings.length > 0 ? strings[0] : null;
}
export function parseNotebooklmNoteListRawRows(rows, notebookId, url) {
const parsed = rows.map((row) => {
const title = normalizeNotebooklmTitle(row.title, '');
const text = String(row.text ?? '')
.replace(/\bsticky_note_2\b/g, ' ')
.replace(/\bmore_vert\b/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!title)
return null;
const suffix = text.startsWith(title)
? text.slice(title.length).trim()
: text.replace(title, '').trim();
return {
notebook_id: notebookId,
title,
created_at: suffix || null,
url,
source: 'studio-list',
};
});
return parsed.filter((row) => row !== null);
}
function parseNotebooklmSummaryRawRow(row, notebookId, url) {
const title = normalizeNotebooklmTitle(row?.title, 'Untitled Notebook');
const summary = String(row?.summary ?? '').trim();
if (!summary)
return null;
return {
notebook_id: notebookId,
title,
summary,
url,
source: 'summary-dom',
};
}
function parseNotebooklmVisibleNoteRawRow(row, notebookId, url) {
const title = normalizeNotebooklmTitle(row?.title, '');
const content = String(row?.content ?? '').replace(/\r\n/g, '\n').trim();
if (!title)
return null;
return {
notebook_id: notebookId,
id: null,
title,
content,
url,
source: 'studio-editor',
};
}
export function parseNotebooklmListResult(result) {
if (!Array.isArray(result) || result.length === 0)
return [];
const rawNotebooks = Array.isArray(result[0]) ? result[0] : result;
if (!Array.isArray(rawNotebooks))
return [];
return rawNotebooks
.filter((item) => Array.isArray(item))
.map((item) => {
const meta = Array.isArray(item[5]) ? item[5] : [];
const timestamps = Array.isArray(meta[5]) ? meta[5] : [];
const id = typeof item[2] === 'string' ? item[2] : '';
const title = typeof item[0] === 'string'
? item[0].replace(/^thought\s*\n/, '')
: '';
return {
id,
title: normalizeNotebooklmTitle(title, 'Untitled Notebook'),
url: `https://${NOTEBOOKLM_DOMAIN}/notebook/${id}`,
source: 'rpc',
is_owner: meta.length > 1 ? meta[1] === false : true,
created_at: timestamps.length > 0 ? toNotebooklmIsoTimestamp(timestamps[0]) : null,
};
})
.filter((row) => row.id);
}
export function parseNotebooklmNotebookDetailResult(result) {
const detail = unwrapNotebooklmSingletonResult(result);
if (!Array.isArray(detail) || detail.length < 3)
return null;
const id = typeof detail[2] === 'string' ? detail[2] : '';
if (!id)
return null;
const title = normalizeNotebooklmTitle(detail[0], 'Untitled Notebook');
const emoji = typeof detail[3] === 'string' ? detail[3] : null;
const meta = Array.isArray(detail[5]) ? detail[5] : [];
const sources = Array.isArray(detail[1]) ? detail[1] : [];
return {
id,
title,
url: `https://${NOTEBOOKLM_DOMAIN}/notebook/${id}`,
source: 'rpc',
is_owner: meta.length > 1 ? meta[1] === false : true,
created_at: toNotebooklmIsoTimestamp(meta[8]),
updated_at: toNotebooklmIsoTimestamp(meta[5]),
emoji,
source_count: sources.length,
};
}
export function parseNotebooklmSourceListResult(result) {
const detail = unwrapNotebooklmSingletonResult(result);
const notebook = parseNotebooklmNotebookDetailResult(detail);
if (!notebook || !Array.isArray(detail))
return [];
const rawSources = Array.isArray(detail[1]) ? detail[1] : [];
return rawSources
.filter((entry) => Array.isArray(entry))
.map((entry) => {
const id = findFirstNotebooklmString(entry[0]) ?? '';
const title = normalizeNotebooklmTitle(entry[1], 'Untitled source');
const meta = Array.isArray(entry[2]) ? entry[2] : [];
const typeInfo = typeof meta[4] === 'number' ? meta[4] : entry[3];
return {
id,
notebook_id: notebook.id,
title,
url: notebook.url,
source: 'rpc',
type: parseNotebooklmSourceType(typeInfo),
type_code: parseNotebooklmSourceTypeCode(typeInfo),
size: typeof meta[1] === 'number' && Number.isFinite(meta[1]) ? meta[1] : null,
created_at: toNotebooklmIsoTimestamp(meta[2]),
updated_at: toNotebooklmIsoTimestamp(meta[14]),
};
})
.filter((row) => row.id);
}
export function parseNotebooklmSourceGuideResult(result, source) {
if (!Array.isArray(result) || result.length === 0 || !Array.isArray(result[0]))
return null;
const outer = result[0];
const guide = Array.isArray(outer) && outer.length > 0 && Array.isArray(outer[0])
? outer[0]
: outer;
if (!Array.isArray(guide))
return null;
const summary = Array.isArray(guide[1]) && typeof guide[1][0] === 'string'
? guide[1][0].trim()
: '';
const keywords = Array.isArray(guide[2]) && Array.isArray(guide[2][0])
? guide[2][0].filter((item) => typeof item === 'string' && item.trim().length > 0)
: [];
if (!summary)
return null;
return {
source_id: source.id,
notebook_id: source.notebook_id,
title: source.title,
type: source.type ?? null,
summary,
keywords,
source: 'rpc',
};
}
export function parseNotebooklmSourceFulltextResult(result, notebookId, fallbackUrl) {
if (!Array.isArray(result) || result.length === 0 || !Array.isArray(result[0]))
return null;
const source = result[0];
const sourceId = findFirstNotebooklmString(source[0]) ?? '';
const title = normalizeNotebooklmTitle(source[1], 'Untitled source');
const meta = Array.isArray(source[2]) ? source[2] : [];
const url = Array.isArray(meta[7]) && typeof meta[7][0] === 'string' ? meta[7][0] : fallbackUrl;
const kind = parseNotebooklmSourceType([null, meta[4]]);
const contentRoot = Array.isArray(result[3]) && result[3].length > 0 ? result[3][0] : [];
const content = collectNotebooklmLeafStrings(contentRoot, []).join('\n').trim();
if (!sourceId || !content)
return null;
return {
source_id: sourceId,
notebook_id: notebookId,
title,
kind,
content,
char_count: content.length,
url,
source: 'rpc',
};
}
export function findNotebooklmSourceRow(rows, query) {
const needle = query.trim().toLowerCase();
if (!needle)
return null;
const exactId = rows.find((row) => row.id.trim().toLowerCase() === needle);
if (exactId)
return exactId;
const exactTitle = rows.find((row) => row.title.trim().toLowerCase() === needle);
if (exactTitle)
return exactTitle;
const partialMatches = rows.filter((row) => row.title.trim().toLowerCase().includes(needle));
if (partialMatches.length === 1)
return partialMatches[0];
return null;
}
export function findNotebooklmNoteRow(rows, query) {
const needle = query.trim().toLowerCase();
if (!needle)
return null;
const exactTitle = rows.find((row) => row.title.trim().toLowerCase() === needle);
if (exactTitle)
return exactTitle;
const partialMatches = rows.filter((row) => row.title.trim().toLowerCase().includes(needle));
if (partialMatches.length === 1)
return partialMatches[0];
return null;
}
export async function listNotebooklmViaRpc(page) {
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_LIST_RPC_ID, [null, 1, null, [2]]);
return parseNotebooklmListResult(rpc.result);
}
export async function getNotebooklmDetailViaRpc(page) {
const state = await getNotebooklmPageState(page);
if (state.kind !== 'notebook' || !state.notebookId)
return null;
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID, [state.notebookId, null, [2], null, 0]);
return parseNotebooklmNotebookDetailResult(rpc.result);
}
export async function getNotebooklmNotebookDetailById(page, notebookId) {
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID, [notebookId, null, [2], null, 0]);
return { detail: parseNotebooklmNotebookDetailResult(rpc.result), sources: parseNotebooklmSourceListResult(rpc.result) };
}
export async function verifyNotebooklmNotebookExists(page, notebookId, action) {
try {
const { detail } = await getNotebooklmNotebookDetailById(page, notebookId);
if (!detail || detail.id !== notebookId) {
throw new CommandExecutionError(`NotebookLM ${action} succeeded but the notebook ${notebookId} was not found in the post-write verification`);
}
return detail;
}
catch (error) {
if (error instanceof AuthRequiredError || error instanceof CommandExecutionError)
throw error;
throw new CommandExecutionError(`NotebookLM ${action} post-write verification failed: ${error?.message || error}`);
}
}
export async function verifyNotebooklmSourceAdded(page, notebookId, sourceId, action) {
try {
const { detail, sources } = await getNotebooklmNotebookDetailById(page, notebookId);
if (!detail || detail.id !== notebookId) {
throw new CommandExecutionError(`NotebookLM ${action} succeeded but the notebook ${notebookId} was not found in the post-write verification`);
}
const matched = sources.find((s) => s.id === sourceId);
if (!matched) {
throw new CommandExecutionError(`NotebookLM ${action} succeeded but source ${sourceId} did not appear in the notebook's source list`);
}
return matched;
}
catch (error) {
if (error instanceof AuthRequiredError || error instanceof CommandExecutionError)
throw error;
throw new CommandExecutionError(`NotebookLM ${action} post-write verification failed: ${error?.message || error}`);
}
}
export async function listNotebooklmSourcesViaRpc(page) {
const state = await getNotebooklmPageState(page);
if (state.kind !== 'notebook' || !state.notebookId)
return [];
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID, [state.notebookId, null, [2], null, 0]);
return parseNotebooklmSourceListResult(rpc.result);
}
export async function listNotebooklmHistoryViaRpc(page) {
const state = await getNotebooklmPageState(page);
if (state.kind !== 'notebook' || !state.notebookId)
return [];
const threadsRpc = await callNotebooklmRpc(page, NOTEBOOKLM_HISTORY_THREADS_RPC_ID, [[], null, state.notebookId, 20]);
const threadIds = parseNotebooklmHistoryThreadIdsResult(threadsRpc.result);
if (threadIds.length === 0)
return [];
const rows = [];
for (const threadId of threadIds) {
const detailRpc = await callNotebooklmRpc(page, NOTEBOOKLM_HISTORY_DETAIL_RPC_ID, [[], null, null, threadId, 20]);
rows.push({
notebook_id: state.notebookId,
thread_id: threadId,
item_count: Array.isArray(detailRpc.result) ? detailRpc.result.length : 0,
preview: extractNotebooklmHistoryPreview(detailRpc.result),
url: state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`,
source: 'rpc',
});
}
return rows;
}
export async function listNotebooklmNotesFromPage(page) {
const state = await getNotebooklmPageState(page);
if (state.kind !== 'notebook' || !state.notebookId)
return [];
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
return Array.from(document.querySelectorAll('artifact-library-note')).map((node) => {
const titleNode = node.querySelector('.artifact-title');
return {
title: (titleNode?.textContent || '').trim(),
text: (node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim(),
};
});
})()`));
if (!Array.isArray(raw) || raw.length === 0)
return [];
return parseNotebooklmNoteListRawRows(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
}
export async function readNotebooklmSummaryFromPage(page) {
const state = await getNotebooklmPageState(page);
if (state.kind !== 'notebook' || !state.notebookId)
return null;
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
const title = normalize(document.querySelector('.notebook-title, h1, [data-testid="notebook-title"]')?.textContent || document.title || '');
const summaryNode = document.querySelector('.notebook-summary, .summary-content, [class*="summary"]');
const summary = normalize(summaryNode?.textContent || '');
return { title, summary };
})()`));
return parseNotebooklmSummaryRawRow(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
}
export async function getNotebooklmSummaryViaRpc(page) {
const state = await getNotebooklmPageState(page);
if (state.kind !== 'notebook' || !state.notebookId)
return null;
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID, [state.notebookId, null, [2], null, 0]);
const detail = unwrapNotebooklmSingletonResult(rpc.result);
if (!Array.isArray(detail))
return null;
const title = normalizeNotebooklmTitle(detail[0], 'Untitled Notebook');
const summary = detail
.filter((value, index) => index !== 0 && index !== 2 && index !== 3)
.find((value) => typeof value === 'string' && value.trim().length >= 80);
if (typeof summary !== 'string')
return null;
return {
notebook_id: state.notebookId,
title,
summary: summary.trim(),
url: state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`,
source: 'rpc',
};
}
export async function getNotebooklmSourceFulltextViaRpc(page, sourceId) {
const state = await getNotebooklmPageState(page);
if (state.kind !== 'notebook' || !state.notebookId || !sourceId)
return null;
const rpc = await callNotebooklmRpc(page, 'hizoJc', [[sourceId], [2], [2]]);
return parseNotebooklmSourceFulltextResult(rpc.result, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
}
export async function getNotebooklmSourceGuideViaRpc(page, source) {
if (!source.id)
return null;
const rpc = await callNotebooklmRpc(page, 'tr032e', [[[[source.id]]]]);
return parseNotebooklmSourceGuideResult(rpc.result, source);
}
export async function readNotebooklmVisibleNoteFromPage(page) {
const state = await getNotebooklmPageState(page);
if (state.kind !== 'notebook' || !state.notebookId)
return null;
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
const normalizeText = (value) => (value || '').replace(/\\u00a0/g, ' ').replace(/\\r\\n/g, '\\n').trim();
const titleNode = document.querySelector('.note-header__editable-title');
const title = titleNode instanceof HTMLInputElement || titleNode instanceof HTMLTextAreaElement
? titleNode.value
: (titleNode?.textContent || '');
const editor = document.querySelector('.note-editor .ql-editor, .note-editor [contenteditable="true"], .note-editor textarea');
let content = '';
if (editor instanceof HTMLTextAreaElement || editor instanceof HTMLInputElement) {
content = editor.value || '';
} else if (editor) {
content = editor.innerText || editor.textContent || '';
}
return {
title: normalizeText(title),
content: normalizeText(content),
};
})()`));
return parseNotebooklmVisibleNoteRawRow(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
}
export async function ensureNotebooklmHome(page) {
const currentUrl = page.getCurrentUrl
? await page.getCurrentUrl().catch(() => null)
: null;
const currentKind = currentUrl ? classifyNotebooklmPage(currentUrl) : 'unknown';
if (currentKind === 'home')
return;
const authuser = getNotebooklmAuthuser();
const target = authuser ? `${NOTEBOOKLM_HOME_URL}?authuser=${encodeURIComponent(authuser)}` : NOTEBOOKLM_HOME_URL;
try {
await page.goto(target);
await page.wait(2);
}
catch (error) {
throw new CommandExecutionError(`Failed to open NotebookLM home: ${error?.message || error}`);
}
}
export async function getNotebooklmPageState(page) {
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
const url = window.location.href;
const title = document.title || '';
const hostname = window.location.hostname || '';
const notebookMatch = url.match(/\\/notebook\\/([^/?#]+)/);
const notebookId = notebookMatch ? notebookMatch[1] : '';
const path = window.location.pathname || '/';
const kind = notebookId
? 'notebook'
: (hostname === 'notebooklm.google.com' ? 'home' : 'unknown');
const textNodes = Array.from(document.querySelectorAll('a, button, [role="button"], h1, h2'))
.map(node => (node.textContent || '').trim().toLowerCase())
.filter(Boolean);
const loginRequired = textNodes.some(text =>
text.includes('sign in') ||
text.includes('log in') ||
text.includes('登录') ||
text.includes('登入')
);
const notebookCount = Array.from(document.querySelectorAll('a[href*="/notebook/"]'))
.map(node => node instanceof HTMLAnchorElement ? node.href : '')
.filter(Boolean)
.reduce((count, href, index, list) => list.indexOf(href) === index ? count + 1 : count, 0);
return { url, title, hostname, kind, notebookId, loginRequired, notebookCount, path };
})()`));
const state = {
url: String(raw?.url ?? ''),
title: normalizeNotebooklmTitle(raw?.title, 'NotebookLM'),
hostname: String(raw?.hostname ?? ''),
kind: raw?.kind === 'notebook' || raw?.kind === 'home' ? raw.kind : 'unknown',
notebookId: String(raw?.notebookId ?? ''),
loginRequired: Boolean(raw?.loginRequired),
notebookCount: Number(raw?.notebookCount ?? 0),
};
// Notebook pages can still contain "sign in" or login-related text fragments
// even when the active Google session is valid. Prefer the real page tokens
// as the stronger auth signal before declaring the session unauthenticated.
if (state.hostname === NOTEBOOKLM_DOMAIN && state.loginRequired) {
try {
await getNotebooklmPageAuth(page);
state.loginRequired = false;
}
catch {
// Keep the heuristic result when page auth tokens are genuinely unavailable.
}
}
return state;
}
export async function readCurrentNotebooklm(page) {
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
const url = window.location.href;
const match = url.match(/\\/notebook\\/([^/?#]+)/);
if (!match) return null;
const titleNode = document.querySelector('h1, [data-testid="notebook-title"], [role="heading"]');
const title = (titleNode?.textContent || document.title || '').trim();
return {
id: match[1],
title,
url,
source: 'current-page',
};
})()`));
if (!raw)
return null;
return {
id: String(raw.id ?? ''),
title: normalizeNotebooklmTitle(raw.title, 'Untitled Notebook'),
url: String(raw.url ?? ''),
source: 'current-page',
is_owner: true,
created_at: null,
};
}
export async function listNotebooklmLinks(page) {
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
const rows = [];
const seen = new Set();
for (const node of Array.from(document.querySelectorAll('a[href*="/notebook/"]'))) {
if (!(node instanceof HTMLAnchorElement)) continue;
const href = node.href || '';
const match = href.match(/\\/notebook\\/([^/?#]+)/);
if (!match) continue;
const id = match[1];
if (seen.has(id)) continue;
seen.add(id);
const parentCard = node.closest('mat-card, [role="listitem"], article, div');
const titleNode = parentCard?.querySelector('.project-button-title, [id$="-title"]');
const subtitleTitleNode = parentCard?.querySelector('.project-button-subtitle-part[title]');
const subtitleTextNode = parentCard?.querySelector('.project-button-subtitle-part, .project-button-subtitle');
const parentText = (parentCard?.textContent || '').trim();
const parentLines = parentText
.split(/\\n+/)
.map((value) => value.trim())
.filter(Boolean);
const title = (
titleNode?.textContent ||
node.getAttribute('aria-label') ||
node.getAttribute('title') ||
parentLines.find((line) => !line.includes('个来源') && !line.includes('sources') && !line.includes('more_vert')) ||
node.textContent ||
''
).trim();
const createdAtHint = (
subtitleTitleNode?.getAttribute?.('title') ||
subtitleTextNode?.textContent ||
''
).trim();
rows.push({
id,
title,
url: href,
source: 'home-links',
is_owner: true,
created_at: createdAtHint || null,
});
}
return rows;
})()`));
if (!Array.isArray(raw))
return [];
return raw
.map((row) => ({
id: String(row.id ?? ''),
title: normalizeNotebooklmTitle(row.title, 'Untitled Notebook'),
url: String(row.url ?? ''),
source: 'home-links',
is_owner: row.is_owner === false ? false : true,
created_at: normalizeNotebooklmCreatedAt(row.created_at),
}))
.filter((row) => row.id && row.url);
}
export async function listNotebooklmSourcesFromPage(page) {
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
const notebookMatch = window.location.href.match(/\\/notebook\\/([^/?#]+)/);
const notebookId = notebookMatch ? notebookMatch[1] : '';
if (!notebookId) return [];
const skip = new Set([
'选择所有来源',
'添加来源',
'收起来源面板',
'更多',
'Web',
'Fast Research',
'提交',
'创建笔记本',
'分享笔记本',
'设置',
'对话选项',
'配置笔记本',
'音频概览',
'演示文稿',
'视频概览',
'思维导图',
'报告',
'闪卡',
'测验',
'信息图',
'数据表格',
'添加笔记',
'保存到笔记',
'复制摘要',
'摘要很棒',
'摘要欠佳',
]);
const rows = [];
const seen = new Set();
for (const node of Array.from(document.querySelectorAll('button, [role="button"], input[type="checkbox"]'))) {
const text = (node.getAttribute?.('aria-label') || node.textContent || '').trim();
if (!text || skip.has(text) || seen.has(text)) continue;
if (text.includes('个来源') || text.includes('来源') && text.length < 5) continue;
seen.add(text);
rows.push({
id: text,
notebook_id: notebookId,
title: text,
url: window.location.href,
source: 'current-page',
});
}
return rows;
})()`));
if (!Array.isArray(raw))
return [];
return raw.filter((row) => row.id && row.title);
}
export async function requireNotebooklmSession(page) {
const state = await getNotebooklmPageState(page);
if (state.hostname !== NOTEBOOKLM_DOMAIN) {
throw new CliError('NOTEBOOKLM_UNAVAILABLE', 'NotebookLM page is not available in the current browser session', `Open Chrome and navigate to ${NOTEBOOKLM_HOME_URL}`);
}
if (state.loginRequired) {
throw new AuthRequiredError(NOTEBOOKLM_DOMAIN, 'NotebookLM requires a logged-in Google session');
}
return state;
}