ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

notebooklm-py 架构决策实录:Capability Protocol 模式从「胖联合体」到「可组合能力」的演进

notebooklm-py 架构决策实录:Capability Protocol 模式从「胖联合体」到「可组合能力」的演进 notebooklm-py 架构决策实录Capability Protocol 模式从「胖联合体」到「可组合能力」的演进【免费下载链接】notebooklm-pyUnofficial Python API and agentic skill for Google Gemini Notebook. Full programmatic access to NotebookLMs features—including capabilities the web UI doesnt expose—via Python, CLI, and AI agents like Claude Code, Codex, and OpenClaw.项目地址: https://gitcode.com/GitHub_Trending/no/notebooklm-py本文以 docs/adr/0002-capability-protocol-pattern.md 为骨架还原 notebooklm-pyUnofficial Google Gemini Notebook Python API在解耦Session协作对象时采用的typing.Protocol能力模式并沿 ADR-0013 / ADR-0014 的演进脉络结合当前源码src/notebooklm/_runtime/contracts.py、src/notebooklm/_web/contracts.py、src/notebooklm/_client_assembly.py验证其最终形态。读完你会掌握为什么 Python 里我不能 import 的东西要用 Protocol 来 type、什么是能力联合fat union反模式、以及一个真实开源项目如何用两条结构性规则≥2 消费者才提升共享协议、单一消费者直接注入协作对象完成自我修正。一、问题背景八个功能子客户端与一个Session的两难在项目早期基线中NotebookLMClient对外暴露了八个命名空间化的功能 APInotebooks、sources、artifacts、chat、research、notes、settings、sharing此后客户端陆续又加入了mind_maps、labels等命名空间。每个功能 API 都在独立模块中实现_notebooks.py、_sources.py、_chat.py等并且都需要结构化访问Session的协作对象能力RPC 分发rpc_call认证路由auth routing请求 ID 分配request-id allocation轮询注册表polling registry传输簿记transport bookkeeping上传并发控制upload concurrency设计当时有两个不可谈判的约束决定了架构走向子客户端sub-client绝不能直接 importSession。因为Session需要 import 子客户端才能把它们挂到NotebookLMClient.notebooks等属性上若子客户端反向 importSession就会形成循环依赖。时至今日Mypy 仍通过TYPE_CHECKING门控来强制这一边界。子客户端必须是可类型化的。当子客户端调用executor.rpc_call(...)时Mypy 需要校验签名如果参数类型写成Any恰好会在方法 ID 漂移会静默破坏的地方让类型系统失效。二、原始设计Capability Protocol 模式与SessionCapabilities适配器代码库用一套capability Protocol模式同时解决了上述两个约束十个窄Protocol类各自描述一个独立的协作对象表面CoreRPCProvider · SourceListProvider · CoreReqIdProvider ChatStreamingProvider · PollRegistryProvider · AuthRouteProvider CookieJarProvider · TransportOperationProvider UploadConcurrencyProvider · LoopAffinityProvider每个 Protocol 描述的是当时审计能识别出的最小协作对象表面。随后一个具体的适配器类SessionCapabilities多重继承全部十个 Protocol并把每个方法转发给底层的Session实例历史实现位置为src/notebooklm/_capabilities.py:149-160。子客户端在构造函数中接收SessionCapabilities参数所有协作交互都经由它完成。2.1 当时的决策Decision 四项原则按 ADR-0002 的记录tier-10 基线时期确立的模式是在src/notebooklm/_capabilities.py中按一个协作对象表面一个 Protocol定义窄能力Protocol类定义单个具体适配器SessionCapabilities多重继承所有 Protocol 并转发给Session子客户端构造函数接收SessionCapabilities实例而非Session实例NotebookLMClient在 open 时构造一个SessionCapabilities适配器并注入到每个子客户端。当时该模式被Accepted理由有三子客户端只有一条导入路径from ._capabilities import SessionCapabilities避免了每个子客户端各自罗列 Protocol 拼盘的样板代码保证每个 Protocol 至少有一个结构化实现者Session经由适配器Mypy 能端到端验证契约该表面经历了 tier-7 线程安全改造和 tier-8 RPC/VCR 改造而未发生变动经验上足够稳定。2.2 想要与不想要的后果想要的后果在子客户端与Session之间建立了一条单一、经 Mypy 验证的接缝能力 Protocol 在一个地方记录了协作对象图便于对照当时的实时图已迁移到 docs/architecture.md。不想要的后果也是日落条款的起因每个子客户端依赖的是联合而非它真正需要的子集。NotebooksAPI和SettingsAPI根本不需要UploadConcurrencyProvider或ChatStreamingProvider但当时它们两者都对外声明了。Session在联合被钉住的情况下无法收缩到约 1,300 行以下。任何 Protocol 中出现的每个方法都必须保留在Session上或保留在委托给Session的适配器上。_core.py:450-774的 property-bridge 动物园部分原因正是联合强制Session暴露那些已被物理迁移进接缝的属性——这一点详见 ADR-0001。适配器开始泄漏私有内部_capabilities.py:230转发了_core._begin_transport_post这个带下划线前缀的方法窄协议契约已经开始滑向私有领地。ChatStreamingProvider的 docstring 公开自述为过渡态Chat-aware error mapping still lives onSession.query_postuntil that is extracted into a chat-owned transport.——即胖联合被文档明确标注为尚未完成的工作。以上_capabilities.py、_core.py及精确行号均为 ADR-0002 写作时期的历史引用_capabilities.py在 D2 cutover 时已被删除当前仓库中不存在该文件。三、审计结论胖联合是披着 Protocol 外衣的上帝接口一次内部架构审计代号 disease D2将上述结果归类为fat-union god-interface wearing a Protocol mask十个 Protocol 单独看都很窄但每个子客户端都拿到的是联合因此子客户端实际依赖的有效契约是完整的十 Protocol 表面。设计之初期望的收窄从未发生因为适配器提前把它们合并了。审计给出的建议是让每个子客户端按它实际用到的能力子集来标注类型并删除SessionCapabilities适配器。Session将结构化地满足每个窄 Protocol由于 Python 的 structural sub-typing结构化子类型天然成立运行时适配器根本不需要。这项工作被编排为 D2 cutover架构疾病治理弧线的 Wave 3。3.1 当时考虑过的替代方案ADR-0002 记录了五条被拒绝或采纳的替代路径是理解模式边界的最佳教材方案结论理由每个子客户端各自的窄Protocol类D2 cutover 的最终选择✅ 采纳为替代方案每个子客户端只声明自己实际用到的表面Session无需修改结构化子类型自动满足效果是NotebooksAPI只依赖CoreRPC AuthRoute。代价是约新增 8 个 Protocol 类。当时未选是因为优先单一路径胜过最小耦合审计在观察到长期耦合成本后重新排序构造函数注入独立协作对象 dataclass❌ 拒绝会迫使每个子客户端构造函数接收 4–7 个类型化参数每个测试都要构造那么多 fake。Protocol 模式严格来说更符合人体工学错的只是联合的形状而非结构化类型方法本身直接以Session作为子客户端类型❌ 拒绝制造循环导入问题并破坏分层。Protocol 模式正是 Python 对请把我 type 成我不能 import 的东西的标准答案typing.Protocolruntime_checkableTrueisinstance守卫不用适配器类❌ 拒绝对runtime_checkableProtocol 做isinstance检查既慢也不更安全不检查方法签名当时判断是无收益的成本在本 ADR 中直接删除SessionCapabilities❌ 拒绝删除必须与按子客户端引入 Protocol 配对进行先删会让迁移窗口内每个子客户端的core参数退化成Any丢失模式本要交付的类型安全收益。D2 cutover 以原子方式编排这次交换四、第一代修正ADR-0013 可组合能力模型Shared vs Feature-localADR-0002 的审计建议在 ADR-0013 中落地为可组合能力模型。核心是把能力分成两类并据此制定结构性的提升规则SHARED共享被 ≥2 个功能使用的能力才允许提升为_runtime/contracts.py中的模块级 Protocol。例如逻辑 RPC 分发rpc_call被每个功能 API 使用循环亲和性断言被 chat 和 artifact polling 使用。FEATURE-LOCAL功能本地只被恰好一个功能使用的能力留在所属功能模块内。例如transport_post chat 手动next_reqid簿记只有 chat 需要drain-hook 注册只有 artifact polling 注册关闭钩子。ADR-0013 明确解释了为什么需要这条≥2 消费者的硬规则ADR-0010 原本把Session: Protocol钉在恰好五个成员rpc_call、transport_post、next_reqid、assert_bound_loop、operation_scope但在_session_contracts.py中它已膨胀到八个成员——auth、kernel是为了上传流程便利而提升的register_drain_hook与独立的DrainHookRegistrationProtocol 重复。先提升再说promote it just in case正是漂移的温床必须用结构性规则而非劝说来阻止。此外 ADR-0013 还规定功能构造函数按能力命名依赖而非按宽泛的Session。纯 RPC 功能NotebooksAPI、ResearchAPI、SettingsAPI、SharingAPI只取rpc: RpcCaller多能力功能ChatAPI、ArtifactsAPI、SourceUploadPipeline则直接以关键字参数接收各协作对象。五、第二代修正ADR-0014 把接口模型补全为实现模型ADR-0013 解决了编译期interface 层面但运行时仍然把Session实例传给每个功能 API——Session仍是所有 Protocol 的通用满足者。这带来四个可观测后果详见 ADR-0014Session必须满足所有功能 Protocol 的联合给ChatRuntime加一个方法Session就必须暴露或转发该方法方法数随功能数增长。转发是结构性强制而非偶然Session.transport_post之所以存在是因为ChatRuntime要求它。测试 monkeypatch 的是Session而非协作对象这直接喂养了 ADR-0007 的禁止 monkeypatch 白名单。RpcOwnerProtocol 携带下划线前缀的Session私有内部_kernel、_perform_authed_post、_await_refresh、_increment_metrics这是私有的Session表面被结构化地类型化。ADR-0014 的六条实现规则当前仍生效的核心部分是Rule 1 — 单协作对象 Protocol 直接由协作对象满足方法下推RpcCaller由RpcExecutor直接满足LoopGuard由ClientLifecycle直接满足OperationScopeProvider与 drain-hook 注册由CallSupervisor直接满足。Rule 2 — 复合 Protocol 仅在值得时用功能本地适配器满足判断标准是意图式的三条有下游模块把整个复合作为单一依赖、或委托改变了调用形状、或多个消费者共享。否则直接构造注入底层协作对象不设适配器中间人。Rule 3 —NotebookLMClient.__init__是装配根composition root每个功能与它的满足者显式接线构造代码读起来就是一张接线图。Rule 5 — 协作对象直接接收其真实依赖RpcExecutor从持有owner引用改为kernel、transport、auth_refresh、metrics四个关键字参数RpcOwnerProtocol 随之消失。六、当前源码验证终态是什么样对照当前仓库ADR 中描述的演进确实已全部落地src/notebooklm/_capabilities.py已不存在find 结果为空SessionCapabilities适配器如 ADR-0002 的 Status 所述在 D2 cutover 时被删除。src/notebooklm/_runtime/contracts.py现在只保留LoopGuard第 31–34 行一个assert_bound_loop单方法 Protocol。该模块 docstring 明确写着按 ADR-0013只有被 ≥2 个功能共享的 Protocol 才能住在这里单一消费者能力留在所属功能模块例如AuthMetadata住在_web/sources/upload.py。曾与Session一并存在的复合 ProtocolArtifactsRuntime、UploadRuntime及其适配器 dataclass 均已退役AsyncWorkRuntime复合协议也因不足两个消费者被删除issue #1327。Kernel与RpcCaller按后端拆分修订移到了src/notebooklm/_web/contracts.pyKernel第 13–32 行描述纯传输表面——post(url, headers, body, ...)、get_http_client(...)、cookies属性、aclose()RpcCaller第 35–50 行描述窄 RPC 分发表面——rpc_call(method, params, source_path, allow_null, ...)参数签名的完整性一目了然这正是子客户端调用rpc_call时 Mypy 能校验签名的实现基础。装配根是src/notebooklm/_client_assembly.py的_assemble_client(...)第 132 行起负责归一化根级输入、选择后端、冻结生命周期并把各协作对象注入功能 API。这与 ADR-0014 Rule 3__init__是 composition root完全吻合。从代码结构看当前客户端还引入了 ADR-0002 之后才出现的命名空间mind_maps、labels、后端偏好backend_preference等它们遵循同样的模式单消费者能力本地化多消费者能力提升为共享 Protocol功能构造函数以关键字参数直接接收窄协作对象。七、留给读者的设计教训把 ADR-0002 → ADR-0013 → ADR-0014 连起来读是一条完整且罕见的自我修正弧线Protocol 是 Python 处理不能 import 的对象的类型标准答案但窄 Protocol 的联合也会退化成上帝接口——判断标准不是每个 Protocol 多窄而是每个消费者实际依赖多窄。需要一条结构性提升规则共享协议必须等第二个真实消费者出现才能提升≥2 consumers ⇒ shared否则先提升再说会一路漂移。接口模型与实现模型必须匹配编译期类型收窄了运行时却仍把统一对象传进去类型系统的好处就打了折扣方法应下推到真正拥有它们的协作对象上。适配器只在该值得时才存在单一消费者 1:1 委托时直接注入底层协作对象比适配器中间层更清晰。架构决策记录ADR的价值在此刻显现docs/adr/README.md 中的编号是只追加的ADR-0002 被标记为Superseded而非删除历史上下文得以完整保留这正是 ADR-0002 自己示范的记录决策为何存在让后来者不必重新争论或静默地重蹈覆辙。如果你想继续深挖推荐按此路径阅读先看 docs/architecture.md 的实时架构图再对照_runtime/contracts.py与_web/contracts.py的协议表面最后在_client_assembly.py的_assemble_client中观察接线方式你会得到与这篇 ADR 完全互证的全貌。【免费下载链接】notebooklm-pyUnofficial Python API and agentic skill for Google Gemini Notebook. Full programmatic access to NotebookLMs features—including capabilities the web UI doesnt expose—via Python, CLI, and AI agents like Claude Code, Codex, and OpenClaw.项目地址: https://gitcode.com/GitHub_Trending/no/notebooklm-py创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表