文件预览

full-check.js

查看 Content Forge 技能包中的文件内容。

文件内容

scripts/full-check.js

#!/usr/bin/env node
/**
 * NotebookLM 全生成类型验证脚本
 *
 * 用法:
 *   node scripts/full-check.js
 *
 * 验证 9 种生成类型 + 2 个基础 checkpoint:
 *   - 新建 notebook + 上传文本
 *   - 依次测试:mindmap, flashcards, quiz, presentation, infographic, datatable
 *   - 依次测试:audio, video, report
 */

const { chromium } = require('playwright');
const fs = require('fs');

const STORAGE_STATE = '~/.notebooklm/storage-state.json';
const OUTPUT_DIR = '/tmp/nlm-fullcheck';

const TEST_CONTENT = `NotebookLM 全类型验证测试

这是一段用于验证 NotebookLM 所有生成类型的测试文本。
内容涵盖:AI Agent 设计模式、市场预测、技术趋势。

2024 年是 AI Agent 落地元年,主要模式包括 ReAct、Plan-and-Solve、多智能体协同。
全球 Agent 市场预计 2030 年突破 500 亿美元。
大模型技术在代码生成、文档理解、多模态推理方面持续突破。
`;

// 9 种生成类型
const GENERATE_TYPES = [
  { key: 'mindmap',     label: '思维导图',   aria: '思维导图',       category: 'notebook' },
  { key: 'flashcards',  label: '闪卡',       aria: '闪卡',           category: 'notebook' },
  { key: 'quiz',        label: '测验',       aria: '测验',           category: 'notebook' },
  { key: 'presentation',label: '演示文稿',   aria: '演示文稿',       category: 'notebook' },
  { key: 'infographic', label: '信息图',     aria: '信息图',         category: 'notebook' },
  { key: 'datatable',   label: '数据表格',   aria: '数据表格',       category: 'notebook' },
  { key: 'audio',       label: '音频概览',   aria: '音频概览',       category: 'account' },
  { key: 'video',       label: '视频概览',   aria: '视频概览',       category: 'account' },
  { key: 'report',      label: '报告',       aria: '报告',           category: 'account' },
];

async function main() {
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({ storageState: STORAGE_STATE, viewport: { width: 1440, height: 900 } });
  const page = await context.newPage();

  const results = [];
  let notebookId = null;

  try {
    // === Step 1: 登录 + 首页 ===
    console.log('\n[基础] 打开首页...');
    await page.goto('https://notebooklm.google.com/', { waitUntil: 'domcontentloaded', timeout: 60000 });
    await page.waitForTimeout(4000);
    const currentUrl = page.url();
    const authOk = currentUrl.includes('notebooklm.google.com') && !currentUrl.includes('accounts.google.com');
    console.log(authOk ? '✅ 登录态有效' : '❌ 登录态失效');
    if (!authOk) throw new Error('Auth failed');

    // === Step 2: 新建 notebook ===
    console.log('\n[基础] 新建 notebook...');
    await page.evaluate(() => document.querySelectorAll('.cdk-overlay-container, [class*="overlay-backdrop"]').forEach(el => el.remove()));
    const newBtnCount = await page.locator('[aria-label="新建笔记本"]').count();
    if (newBtnCount === 0) throw new Error('Cannot find new notebook button');
    await page.locator('[aria-label="新建笔记本"]').first().click();
    for (let i = 0; i < 20; i++) {
      await page.waitForTimeout(2000);
      const m = page.url().match(/\/notebook\/([a-f0-9-]+)/);
      if (m) { notebookId = m[1]; break; }
    }
    console.log(notebookId ? '✅ Notebook 创建成功: ' + notebookId : '❌ Notebook 创建失败');
    if (!notebookId) throw new Error('Notebook creation failed');

    // === Step 3: 上传文本 ===
    console.log('\n[基础] 上传测试文本...');
    await page.waitForTimeout(4000);
    try {
      await page.waitForSelector('.mat-mdc-dialog-container', { timeout: 15000 });
    } catch (e) {}

    const pasted = await page.evaluate(() => {
      const all = Array.from(document.querySelectorAll('*'));
      const target = all.find(el => {
        const t = (el.textContent || '').trim();
        return (t === '复制的文字' || t === 'Copied text') && el.offsetParent !== null;
      });
      if (target) { target.click(); return true; }
      return false;
    });
    if (!pasted) throw new Error('"复制的文字" not found');
    await page.waitForTimeout(2500);

    const filled = await page.evaluate((content) => {
      const tas = Array.from(document.querySelectorAll('textarea'));
      const ta = tas.find(el => {
        const p = el.placeholder || '';
        const a = el.getAttribute('aria-label') || '';
        return p.includes('粘贴') || p.includes('Paste') || a.includes('粘贴');
      });
      if (ta) {
        ta.value = content;
        ta.dispatchEvent(new Event('input', { bubbles: true }));
        ta.dispatchEvent(new Event('change', { bubbles: true }));
        return true;
      }
      return false;
    }, TEST_CONTENT);
    if (!filled) throw new Error('Textarea fill failed');
    await page.waitForTimeout(2000);

    await page.evaluate(() => {
      const buttons = Array.from(document.querySelectorAll('button'));
      const ins = buttons.find(b => {
        const t = (b.textContent || '').trim();
        return (t === '插入' || t === 'Insert') && b.offsetParent !== null;
      });
      if (ins) ins.click();
    });

    let sourceCount = 0;
    for (let i = 0; i < 24; i++) {
      await page.waitForTimeout(5000);
      sourceCount = await page.evaluate(() => {
        const m = document.body.innerText.match(/(\d+)\s*个来源/);
        return m ? parseInt(m[1]) : 0;
      });
      if (sourceCount > 0) break;
    }
    console.log(sourceCount > 0 ? '✅ 上传成功,来源数=' + sourceCount : '❌ 上传失败');
    if (sourceCount === 0) throw new Error('Source upload failed');
    await page.waitForTimeout(8000);

    // === Step 4: 依次测试 9 种生成类型 ===
    console.log('\n========== 生成类型验证开始 ==========');
    for (const type of GENERATE_TYPES) {
      console.log(`\n[${type.key}] 测试 ${type.label}...`);

      // 清除 overlay
      await page.evaluate(() => document.querySelectorAll('.cdk-overlay-container, [class*="overlay-backdrop"]').forEach(el => el.remove()));
      await page.waitForTimeout(1000);

      // 点击生成按钮
      let clicked = false;
      try {
        const count = await page.locator('[aria-label="' + type.aria + '"]').count();
        if (count > 0) {
          await page.locator('[aria-label="' + type.aria + '"]').first().click({ timeout: 8000 });
          clicked = true;
        }
      } catch (e) {}

      if (!clicked) {
        console.log('❌ 未找到按钮: ' + type.aria);
        results.push({ key: type.key, label: type.label, status: 'button-not-found', category: type.category });
        continue;
      }

      // 等待结果
      let genStarted = false;
      let genLimit = false;
      let genError = false;
      let statusDetail = 'unknown';

      for (let i = 0; i < 10; i++) {
        await page.waitForTimeout(5000);
        const text = await page.evaluate(() => document.body.innerText);

        if (text.includes('正在生成') || text.includes('Generating') || text.includes('创建中')) {
          genStarted = true; statusDetail = 'started'; break;
        }
        if (text.includes('已达到每日') || text.includes('daily limit') || text.includes('已达上限')) {
          genLimit = true; statusDetail = 'daily-limit'; break;
        }
        // 检测错误弹窗
        if (text.includes('出错了') || text.includes('错误') || text.includes('Error')) {
          // 检查是否是配额错误
          if (text.includes('limit') || text.includes('配额') || text.includes('上限')) {
            genLimit = true; statusDetail = 'daily-limit'; break;
          }
        }
      }

      if (genStarted) {
        console.log('✅ ' + type.label + ' 已触发生成');
        results.push({ key: type.key, label: type.label, status: 'success', category: type.category });
      } else if (genLimit) {
        console.log('🟡 ' + type.label + ' 达到每日上限');
        results.push({ key: type.key, label: type.label, status: 'daily-limit', category: type.category });
      } else {
        console.log('❌ ' + type.label + ' 未知状态');
        results.push({ key: type.key, label: type.label, status: 'unknown', category: type.category });
      }

      // 截图
      await page.screenshot({ path: OUTPUT_DIR + '/' + type.key + '.png', fullPage: true });

      // 如果是成功生成,多等一会儿让生成完成
      if (genStarted) {
        console.log('  等待生成完成...');
        for (let i = 0; i < 24; i++) {
          await page.waitForTimeout(5000);
          const text = await page.evaluate(() => document.body.innerText);
          if (!text.includes('正在生成') && !text.includes('Generating') && !text.includes('创建中')) {
            console.log('  ✅ 生成完成');
            break;
          }
          if (i === 23) {
            console.log('  🟡 生成超时(>2min),可能仍在后台运行');
          }
        }
      }

      // 每个类型测试后返回 notebook 页面(防止弹窗影响下一个)
      await page.goto('https://notebooklm.google.com/notebook/' + notebookId, { waitUntil: 'domcontentloaded', timeout: 30000 });
      await page.waitForTimeout(5000);
    }

    // === 汇总 ===
    console.log('\n========== 验证结果汇总 ==========');
    const notebookOk = results.filter(r => r.category === 'notebook' && r.status === 'success').length;
    const notebookTotal = results.filter(r => r.category === 'notebook').length;
    const accountOk = results.filter(r => r.category === 'account' && r.status === 'success').length;
    const accountTotal = results.filter(r => r.category === 'account').length;
    const limitCount = results.filter(r => r.status === 'daily-limit').length;

    console.log('\n🟢 Notebook 级(新建即重置):');
    results.filter(r => r.category === 'notebook').forEach(r => {
      const icon = r.status === 'success' ? '✅' : r.status === 'daily-limit' ? '🟡' : '❌';
      console.log('  ' + icon + ' ' + r.label + ' (' + r.key + ')');
    });

    console.log('\n🔴 账号级(新 notebook 无法重置):');
    results.filter(r => r.category === 'account').forEach(r => {
      const icon = r.status === 'success' ? '✅' : r.status === 'daily-limit' ? '🟡' : '❌';
      console.log('  ' + icon + ' ' + r.label + ' (' + r.key + ')');
    });

    console.log('\n总计: ' + results.filter(r => r.status === 'success').length + '/' + results.length + ' 通过');
    console.log('Notebook: ' + notebookOk + '/' + notebookTotal);
    console.log('Account: ' + accountOk + '/' + accountTotal);
    console.log('Daily limit: ' + limitCount);
    console.log('Notebook URL: https://notebooklm.google.com/notebook/' + notebookId);
    console.log('Screenshots: ' + OUTPUT_DIR);

    fs.writeFileSync(OUTPUT_DIR + '/result.json', JSON.stringify({
      timestamp: new Date().toISOString(),
      notebookId,
      results,
      summary: { notebookOk, notebookTotal, accountOk, accountTotal, limitCount, totalOk: results.filter(r => r.status === 'success').length, total: results.length }
    }, null, 2));

    process.exit(0);
  } catch (err) {
    console.error('\nFATAL: ' + err.message);
    try { await page.screenshot({ path: OUTPUT_DIR + '/error.png', fullPage: true }); } catch (e) {}
    fs.writeFileSync(OUTPUT_DIR + '/result.json', JSON.stringify({
      timestamp: new Date().toISOString(), notebookId, results, error: err.message
    }, null, 2));
    process.exit(1);
  } finally {
    await browser.close();
  }
}

main();