1. 项目概述当AI编程助手遇上你的私人知识库最近在折腾一个挺有意思的自动化项目核心想法是把AI编程助手比如Cursor、Claude Code、通义灵码这类和我的个人知识库比如NotebookLM、Obsidian、Dify里的文档库给“焊”在一起。听起来有点抽象简单说就是让AI在帮我写代码、调试的时候能自动、实时地去我的知识库里翻资料而不是每次都让我手动复制粘贴一堆文档链接或者上下文。这个想法的落地我选用了Playwright这个现代浏览器自动化框架作为“连接器”。为什么是Playwright因为它足够稳也足够灵活。相比传统的SeleniumPlaywright对现代Web应用尤其是大量使用JavaScript的单页应用的支持更好而且自带无头浏览器跨平台部署也方便。更重要的是它的API设计对开发者很友好写出来的自动化脚本更像是在写业务逻辑而不是在跟浏览器驱动较劲。这个项目的核心我称之为“NotebookLM Skill”本质上是一个基于Playwright的自动化技能它能在AI编程助手的工作流中自动触发对NotebookLM或其他类似知识库界面的查询、内容提取和结果回填。这个项目适合谁如果你是一个经常需要参考内部文档、API手册、历史代码片段来写新代码的开发者或者你管理着一个不断更新的技术知识库并希望AI助手能更“懂”你的上下文那么这个自动化方案能显著提升你的效率。它把“人肉搜索知识库”这个环节给自动化了让AI真正成为你团队或你个人知识体系的延伸。2. 核心思路与架构设计为什么是Playwright MCP2.1 技术选型的底层逻辑这个项目的核心挑战在于“桥接”一边是AI编程助手运行在IDE或聊天界面中另一边是独立的知识库Web应用如NotebookLM的网页版。它们之间没有现成的API可以直连。因此我们需要一个能模拟真人操作浏览器、与Web界面交互的“机器人”。这就是自动化测试框架的用武之地。在Selenium、Cypress和Playwright之间我最终选择了Playwright主要基于以下几点考量稳定性与可靠性Playwright由微软维护对Chromium、Firefox和WebKit三大浏览器引擎提供了一致且稳定的支持。其自动等待机制auto-waiting非常出色能智能等待元素可交互、网络请求完成大大减少了因页面加载延迟导致的脚本失败这对于操作知识库这种动态内容丰富的页面至关重要。执行速度与资源占用Playwright启动浏览器上下文BrowserContext的速度快且支持无头模式运行对系统资源消耗相对较低。我们的Skill可能需要在后台持续或频繁触发效率是关键。强大的选择器与录制功能Playwright提供了文本选择器、># 1. 创建项目目录并初始化虚拟环境 mkdir notebooklm-playwright-skill cd notebooklm-playwright-skill python -m venv venv # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate # 2. 安装核心依赖 pip install playwright mcp # 3. 安装Playwright所需的浏览器这里选择Chromium够用且轻量 playwright install chromium # 4. 初始化项目结构 mkdir -p tools touch main.py tools/notebooklm_tool.py .env关键依赖说明playwright: 主库用于浏览器自动化。mcp: 一个Python的MCP服务器SDK它帮助我们快速将函数包装成MCP工具并处理协议通信。你也可以选择使用mcp-sdk或其他实现。.env文件用于存储敏感配置如知识库的登录Cookie切勿提交到版本库。3.2 核心工具函数实现操控NotebookLM我们首先实现最核心的部分用Playwright操作NotebookLM页面。在tools/notebooklm_tool.py中我们创建一个类来封装所有操作。# tools/notebooklm_tool.py import asyncio from typing import Optional, List, Dict from playwright.async_api import async_playwright, Browser, Page, BrowserContext import os import json class NotebookLMSearcher: def __init__(self, headless: bool True): self.headless headless self.browser: Optional[Browser] None self.context: Optional[BrowserContext] None self.page: Optional[Page] None # 从环境变量加载登录态避免每次输入密码 self.cookies self._load_cookies_from_env() def _load_cookies_from_env(self) - Optional[List[Dict]]: 从环境变量NOTION_COOKIES示例加载cookies。格式应为JSON字符串。 cookies_str os.getenv(NOTEBOOKLM_COOKIES) if cookies_str: try: return json.loads(cookies_str) except json.JSONDecodeError: print(警告环境变量中的Cookie格式无效。) return None async def __aenter__(self): 异步上下文管理器入口启动浏览器和上下文。 self.playwright await async_playwright().start() # 使用Chromium可配置为持久化上下文以保存登录状态 launch_options { headless: self.headless, args: [--disable-blink-featuresAutomationControlled] # 避免被检测为自动化脚本 } self.browser await self.playwright.chromium.launch(**launch_options) # 创建上下文如果已有cookies则直接加载 self.context await self.browser.new_context() if self.cookies: await self.context.add_cookies(self.cookies) self.page await self.context.new_page() # 设置默认超时和视口 self.page.set_default_timeout(30000) # 30秒 await self.page.set_viewport_size({width: 1920, height: 1080}) return self async def __aexit__(self, exc_type, exc_val, exc_tb): 异步上下文管理器出口关闭资源。 if self.browser: await self.browser.close() await self.playwright.stop() async def search(self, query: str, max_results: int 3) - str: 在NotebookLM中执行搜索并返回格式化文本。 这是最核心的函数其定位选择器需要根据NotebookLM的实际页面结构调整。 if not self.page: raise RuntimeError(Page not initialized. Use within an async context manager.) # 1. 导航至NotebookLM首页或搜索页 # 假设NotebookLM的搜索功能在主页有一个全局搜索框 await self.page.goto(https://notebooklm.google.com/) # 示例URL请替换为实际地址 await self.page.wait_for_load_state(networkidle) # 等待页面基本加载完成 # 2. 定位搜索框并输入查询词 # 这里的选择器是关键需要使用浏览器开发者工具仔细检查元素。 # 示例搜索框可能有一个特定的aria-label或data-testid search_box_selector input[aria-label搜索笔记本] # 请根据实际页面修改 await self.page.fill(search_box_selector, query) await self.page.press(search_box_selector, Enter) # 3. 等待搜索结果出现 # 等待一个标志性的结果容器元素出现 results_container_selector [data-testidsearch-results] # 请根据实际页面修改 await self.page.wait_for_selector(results_container_selector, statevisible, timeout10000) # 4. 提取搜索结果文本 # 假设每个结果项都在一个具有特定类的article或div里 result_item_selector f{results_container_selector} article # 请根据实际页面修改 await self.page.wait_for_selector(result_item_selector, timeout5000) result_elements await self.page.query_selector_all(result_item_selector) extracted_texts [] for i, element in enumerate(result_elements[:max_results]): # 提取标题和内容摘要 title await element.query_selector(h3, h4) content await element.query_selector(p, div.content) title_text await title.inner_text() if title else f结果 {i1} content_text await content.inner_text() if content else 无法提取内容 extracted_texts.append(f【{title_text}】\n{content_text[:500]}...) # 限制内容长度 # 5. 格式化返回 if not extracted_texts: return f在知识库中未找到关于 {query} 的明确结果。 combined_result f根据知识库关于 {query} 的搜索结果如下\n\n \n\n---\n\n.join(extracted_texts) return combined_result实操心得选择器的稳定性是这个工具成败的关键。不要依赖容易变化的CSS类名如.js-search-result。优先使用># main.py import asyncio from mcp import ClientSession, StdioServerParameters from mcp.server import Server from mcp.server.models import InitializationOptions import tools.notebooklm_tool as nlm_tools from typing import Any import uvicorn from fastapi import FastAPI # 假设我们使用基于HTTP的MCP服务器FastAPI是一个常见选择 app FastAPI() # 创建MCP Server实例 mcp_server Server(notebooklm-playwright-skill) # 定义工具Tool mcp_server.list_tools() async def handle_list_tools() - list[dict[str, Any]]: 向客户端声明本服务提供的工具。 return [ { name: search_notebooklm, description: 在NotebookLM知识库中搜索相关信息。当你需要参考内部文档、设计稿、API说明或历史决策时使用此工具。, inputSchema: { type: object, properties: { query: { type: string, description: 搜索查询关键词例如用户登录模块的数据库设计 }, max_results: { type: integer, description: 最大返回结果数量默认3, default: 3 } }, required: [query] } } ] mcp_server.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any]) - dict[str, Any]: 处理客户端对工具的调用。 if name search_notebooklm: query arguments.get(query, ) max_results arguments.get(max_results, 3) if not query: return {content: [{type: text, text: 错误查询参数 query 不能为空。}]} # 异步执行Playwright搜索 try: async with nlm_tools.NotebookLMSearcher(headlessTrue) as searcher: search_result await searcher.search(query, max_results) return { content: [{ type: text, text: search_result }] } except Exception as e: return {content: [{type: text, text: f搜索过程中发生错误{str(e)}}]} else: return {content: [{type: text, text: f未知工具{name}}]} # 为了简化这里展示一个基于FastAPI的HTTP MCP服务器端点示例 # 实际部署时MCP协议通常通过stdio或socket通信但HTTP更容易演示 app.post(/mcp/tools/call) async def call_tool_endpoint(tool_call: dict): 一个模拟MCP工具调用的HTTP端点。 tool_name tool_call.get(name) arguments tool_call.get(arguments, {}) result await handle_call_tool(tool_name, arguments) return result if __name__ __main__: # 启动FastAPI服务器示例实际MCP集成可能需要stdio服务器 print(启动NotebookLM Playwright Skill MCP服务器...) uvicorn.run(app, host127.0.0.1, port8000)3.4 配置AI助手以连接MCP服务最后一步是配置你的AI编程助手让它知道我们这个MCP服务的存在。以Claude Desktop为例找到Claude Desktop的配置文件。通常在~/Library/Application Support/Claude/claude_desktop_config.json(Mac) 或%APPDATA%\Claude\claude_desktop_config.json(Windows)。在配置文件中添加MCP服务器配置。我们的示例运行在HTTP端口8000但标准MCP更常用stdio。这里假设我们已调整服务器为stdio模式需修改main.py使用mcp.server.stdio.run_server。// claude_desktop_config.json (部分) { mcpServers: { notebooklm-skill: { command: python, args: [ /绝对路径/to/your/notebooklm-playwright-skill/main.py ], env: { NOTEBOOKLM_COOKIES: [你的加密Cookie JSON字符串] } } } }对于Cursor或其他支持MCP的IDE/助手配置方式类似通常在其设置或配置文件中指定MCP服务器的启动命令。配置完成后重启AI助手。你应该能在助手的工具列表中看到search_notebooklm。当你在聊天中输入“查一下我们项目关于错误处理的规范”助手就会自动调用这个工具并返回从你的NotebookLM中搜到的内容。4. 关键细节与避坑指南4.1 登录态管理与持久化直接硬编码密码或每次手动登录是不可行的。最佳实践是使用浏览器上下文的持久化存储。# 在NotebookLMSearcher的__init__或启动方法中 import asyncio from playwright.async_api import async_playwright import os async def create_persistent_context(user_data_dir: str): playwright await async_playwright().start() # 指定用户数据目录浏览器会保存cookies、localStorage等到此目录 browser await playwright.chromium.launch_persistent_context( user_data_dir, headlessFalse, # 首次登录可能需要非无头模式 args[ --disable-blink-featuresAutomationControlled, ] ) return browser # 使用方式 async def main(): user_data_dir ./playwright_data browser await create_persistent_context(user_data_dir) page await browser.new_page() await page.goto(https://notebooklm.google.com) # 如果是首次手动登录一次。关闭浏览器后登录态会被保存。 # 下次启动时使用相同的user_data_dir即可自动保持登录。重要提示user_data_dir路径应妥善保管并添加到.gitignore中。在无头服务器上部署时可能需要使用xvfb等虚拟显示软件来辅助首次登录。4.2 页面等待与选择器策略不稳定的选择器是自动化脚本失败的首要原因。除了使用># 1. 文本选择器非常直观但注意文本可能变化或翻译 await page.click(text登录) # 点击包含“登录”文本的元素 # 2. 组合选择器增加特异性 await page.wait_for_selector(div.results article:has-text(API)) # 3. 使用 page.wait_for_function 等待特定条件 await page.wait_for_function( () { const container document.querySelector([data-testidresults]); return container container.children.length 0; } ) # 4. 应对动态加载监听网络请求或DOM变化 async with page.expect_response(lambda response: /api/search in response.url) as response_info: await page.fill(input#search, query) response await response_info.value # 可以直接从API响应中解析数据比抓取DOM更稳定 # 5. 超时与重试策略 from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10)) async def robust_search(page, query): # 封装可能失败的操作 await page.fill(input#search, query) await page.click(button[typesubmit]) # ... 等待和提取结果4.3 错误处理与日志记录一个健壮的Skill必须能妥善处理各种异常并留下清晰的日志供排查。import logging from playwright.async_api import TimeoutError as PlaywrightTimeoutError logging.basicConfig(levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) async def safe_search(searcher, query): try: result await searcher.search(query) logger.info(f成功执行搜索: {query}) return result except PlaywrightTimeoutError as e: logger.error(f搜索超时元素未找到: {query}。错误: {e}) # 可以尝试截图保存现场 await searcher.page.screenshot(pathferror_screenshot_{int(time.time())}.png) return f搜索超时可能页面结构已变化或网络缓慢。 except Exception as e: logger.exception(f搜索过程中发生未预期错误: {query}) # 记录完整堆栈 return f系统内部错误请稍后重试或检查日志。在MCP工具的handle_call_tool函数中调用这个safe_search函数确保任何Playwright层面的错误都不会导致整个MCP服务崩溃而是返回友好的错误信息给AI助手。5. 扩展思路与应用场景5.1 从NotebookLM扩展到其他知识库这个Skill的架构是通用的。要适配其他知识库如Dify、Obsidian Publish、Confluence你只需要重写NotebookLMSearcher类中的页面导航、选择器和提取逻辑。以Dify知识库为例修改search方法中的目标URL为Dify工作空间地址。使用浏览器开发者工具分析Dify知识库搜索框和结果列表的HTML结构。更新search_box_selector和results_container_selector。可能需要处理Dify不同的交互方式如即时搜索、按钮触发。你可以创建多个工具类DifySearcher、ConfluenceSearcher并在MCP服务器中注册多个工具让AI助手根据上下文选择调用哪一个。5.2 与自动化工作流如n8n集成这个Playwright Skill本身可以作为一个独立的服务。你可以将其封装成HTTP API就像上面FastAPI的示例然后被任何能发送HTTP请求的系统调用。n8n场景在n8n中创建一个Webhook节点接收来自GitHub Issue、Slack消息或邮件的请求提取其中的关键词然后通过HTTP Request节点调用我们的Skill API获取知识库内容最后将内容连同原始请求一起发送到AI API如OpenAI、Claude生成处理建议再自动回复到原始渠道。CI/CD场景在代码审查Pull Request流程中自动分析PR描述和修改内容调用Skill搜索相关的设计文档、代码规范生成更全面的自动化审查评论。5.3 性能优化与部署考量浏览器实例复用频繁启动关闭浏览器开销很大。可以考虑使用一个长期运行的浏览器实例通过BrowserContext来隔离不同的搜索会话。但要注意内存泄漏和状态清理。并发控制如果同时有多个搜索请求需要管理好浏览器页面Page的并发数避免资源竞争。可以使用连接池模式。无服务器部署在AWS Lambda或Google Cloud Functions上部署Playwright需要特殊处理因为需要包含浏览器二进制文件。可以使用社区提供的Docker镜像如mcr.microsoft.com/playwright/python或专门为无服务器优化的版本如playwright-python的install chromium时指定--with-deps。监控与告警为MCP服务添加健康检查端点并监控其日志。如果搜索失败率升高及时发出告警。6. 常见问题与故障排查实录在实际搭建和运行过程中我遇到了不少坑。这里记录下最典型的几个问题及其解决方案。6.1 Playwright脚本在无头模式下运行失败但非无头模式正常现象脚本在headlessTrue时无法找到元素或页面空白headlessFalse则一切正常。原因有些网站会检测无头浏览器特征如navigator.webdriver属性并进行反爬或渲染拦截。此外无头模式下某些CSS或JavaScript的加载行为可能略有不同。解决方案启动浏览器时添加反检测参数browser await playwright.chromium.launch(headlessTrue, args[ --disable-blink-featuresAutomationControlled, --disable-dev-shm-usage, --no-sandbox, # 在容器环境中通常需要 ])在Page上下文中注入脚本覆盖常见的检测变量await page.add_init_script( Object.defineProperty(navigator, webdriver, { get: () undefined }); )如果问题依旧可以尝试使用headlessnewChromium的新无头模式或者暂时使用headlessFalse配合xvfb在服务器上运行。6.2 选择器突然失效元素找不到现象之前运行良好的脚本某天突然报错TimeoutError: Waiting for selector “...”。原因知识库网站前端更新DOM结构或元素属性发生了变化。解决方案防御性定位优先使用># 更稳健的选择器示例 # 坏 await page.click(.btn-primary) # 类名易变 # 好 await page.click(button:has-text(提交)) # 文本相对稳定 # 更好 await page.click([data-testidsubmit-button])多层等待与备用选择器在关键操作前增加对页面状态的综合判断。# 等待页面加载完成 await page.wait_for_load_state(networkidle) # 同时等待多个可能的选择器之一出现 try: await page.wait_for_selector(selector_v1, timeout5000) except: await page.wait_for_selector(selector_v2, timeout5000)建立监控与告警将脚本的日常运行纳入监控一旦连续失败立即通知维护者检查页面结构。6.3 MCP服务已启动但AI助手无法发现或调用工具现象Claude Desktop或Cursor的MCP工具列表里没有出现search_notebooklm。排查步骤检查配置文件确认MCP服务器配置的command和args路径绝对正确并且有执行权限。配置文件修改后必须重启AI助手客户端。检查服务器日志在启动MCP服务的终端查看是否有错误输出。确保Python依赖已正确安装特别是playwright和mcp。验证MCP服务器通信MCP协议通常通过stdio通信。你可以手动模拟客户端调用检查服务器是否正常响应。或者临时将服务器改为HTTP模式并用curl测试。# 假设服务器监听8000端口提供 /tools/call 端点 curl -X POST http://localhost:8000/tools/call -H Content-Type: application/json -d {name:search_notebooklm,arguments:{query:test}}查看客户端日志Claude Desktop等客户端通常有日志文件里面会记录MCP服务器初始化失败的具体原因。6.4 搜索返回内容杂乱或包含无关信息现象Skill能返回文本但里面混入了导航栏、侧边栏、广告等无关内容。原因内容提取的选择器过于宽泛没有精准定位到搜索结果区域。解决方案使用浏览器开发者工具仔细检查搜索结果区域的DOM结构找到最外层、最具唯一性的容器。使用Playwright的locatorAPI进行更精确的范围限定。# 先定位到结果容器 results_locator page.locator([data-testidsearch-results-list]) # 然后在这个容器内查找所有结果项 result_items await results_locator.locator(article).all() for item in result_items: # 提取内容 text await item.inner_text()提取后做后处理使用简单的规则如关键词过滤、长度过滤或正则表达式清洗文本去除明显的页眉页脚信息。6.5 在Docker或CI环境中运行失败现象本地运行正常但在Docker容器或GitHub Actions中运行Playwright脚本时失败。原因这些环境缺少必要的系统依赖如字体、图形库。解决方案使用Playwright官方提供的Docker镜像它包含了所有依赖。FROM mcr.microsoft.com/playwright/python:v1.45.0-noble COPY . /app WORKDIR /app RUN pip install -r requirements.txt CMD [python, main.py]如果必须基于其他镜像则需要手动安装依赖。对于Debian/Ubuntu系统RUN apt-get update apt-get install -y \ wget \ gnupg \ fonts-liberation \ libasound2 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libcups2 \ libdbus-1-3 \ libdrm2 \ libgbm1 \ libgtk-3-0 \ libnspr4 \ libnss3 \ libxcomposite1 \ libxdamage1 \ libxfixes3 \ libxrandr2 \ xdg-utils \ --no-install-recommends \ rm -rf /var/lib/apt/lists/*在CI脚本中确保在安装Python包后运行playwright install chromium --with-deps。这个项目从构思到实现最深的体会是将自动化测试框架用于“正向”的业务流程自动化其潜力远不止于测试。Playwright这类工具本质上是一个高度可控、可编程的“数字员工”它能精准地操作任何Web界面。当把它和MCP这样的协议结合就为AI大模型装上了操控具体软件的手和眼。过程中最大的挑战不是代码本身而是如何应对Web前端的变化无常这要求我们写的自动化脚本要有足够的韧性和可观测性。现在我的AI助手已经能“主动”翻阅团队的知识库来回答我的问题这种感觉就像多了一个过目不忘、随时待命的资深同事。如果你也在构建类似的知识工作流不妨从这个小Skill开始试试它带来的效率提升是实实在在的。