notebooklm-py 工件生成结果的密封类型设计GenerationStatus 角色分区、迁移路径与推迟决策ADR-0020 深度解析【免费下载链接】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本篇技术指南围绕 notebooklm-py 仓库中的 ADR-0020 密封异步结果类型Sealed async result types for artifact generation展开深入剖析其决策背景、设计蓝图A1–A6、推迟结论与触发重建条件并结合当前仓库的源码实现与测试证据逐条验证。读者读完后将能完整理解 NotebookLM 工件Audio Overview、Report、Video 等生成任务的状态类型为何采用扁平 dataclass 字符串枚举 测试强制分区的现状以及一套密封联合类型sealed union方案在真实数据约束下的可行性边界与分阶段迁移路径。一、背景一个返回类型、两种角色、约 13 个消费方ADR-0020 讨论的核心对象是GenerationStatus定义于 src/notebooklm/_types/artifacts.pydataclass class GenerationStatus: task_id: str # Same as artifact_id - used for polling and becomes Artifact.id status: GenerationState url: str | None None error: str | None None error_code: str | None None # e.g., USER_DISPLAYABLE_ERROR for rate limits metadata: dict[str, Any] | None None它是一个非 frozen 的dataclass由约 13 个方法以两种截然不同的角色返回角色返回值状态集合方法Snapshot快照pending / in_progress / completed / failed / not_found / unknowngenerate_*系列 ×10、revise_slide、retry_failed、poll_statusTerminal终态completed / failed / removedwait_for_completion两个角色之间的状态分区是严格的not_found只由轮询侧产出、removed只由等待侧产出wait_for_completion在超时场景下抛出ArtifactTimeoutError而非返回一个TimedOut变体。关键点是这条分区目前只靠生产者约定成立并由测试强制而不是由类型强制——这就是本 ADR 的出发点。从源码看kicokoff启动类方法不接收wait标志因此它们的返回角色是稳定的这也是设计讨论得以成立的前提。二、现状盘点枚举、规范化器与测试强制分区在 ADR 提出之时基线mainat #1447f0d2d1be, v0.8.0GenerationStatus.status已经完成从裸字符串到枚举的升级#1447本 ADR 之外已完成的工作GenerationState(str, Enum)是当前仓库的真实实现poll 集合PENDING、IN_PROGRESS、COMPLETED、FAILED、NOT_FOUND、UNKNOWN罕见后端状态SUGGESTED当前无生产者被ArtifactListingService.list_raw的服务器端过滤无条件排除建模属防御性深度、PENDING_REVIEW语义未确认保留可检索性wait-onlyREMOVED——由wait_for_completion在持续消失sustained delisting时合成。GenerationState.is_terminal是生成是否已结束的唯一权威定义COMPLETED / FAILED / REMOVED且新加入的成员默认非终态——因为等待一个已完成任务的代价是一次浪费的轮询而终结一个仍在运行任务的代价是丢失结果。_TERMINAL_GENERATION_STATESfrozenset 直接由该属性派生保证分区只定义一次见 src/notebooklm/_types/artifacts.py。_status_from_code是上游 int→state 规范化器而不是设计迁移的接缝。它把 API 状态码映射为GenerationStateNone默认映射到PENDING未识别码落到UNKNOWN永远不会发射NOT_FOUND/REMOVED——这两个状态是在 src/notebooklm/_artifact/polling.py 中直接构造的poll_status在工件 ID 缺席于列表时返回GenerationStatus(task_idtask_id, statusGenerationState.NOT_FOUND)L150wait_for_completion的轮询循环在consecutive_not_found连续缺失满足双阈值 max_not_found且耗时 min_not_found_window或连续次数 max_not_found * 2的窗口无关触发后构造携带REMOVED与说明性错误文本的GenerationStatusL441-L453。分区被测试钉死。tests/unit/test_generation_state.py中的test_poll_status_never_returns_removed逐码验证poll_status的每一种输入都不可能返回removedtest_status_from_code_never_returns_wait_only_states则把_status_from_code的不可发射范围固定在测试里test_is_terminal_partitions_the_enum_exactly断言is_terminal恰好三成员test_generation_status_is_terminal_works_on_a_plain_string_status验证is_terminal是唯一按哈希集合查找的谓词其余is_*谓词按比较因此对裸字符串构造的实例同样有效——这是str在GenerationStateMRO 中先于Enum、str.__hash__胜出的机制后果测试直接钉住该机制以防基类重排。三、残留问题类型卫生的三个缺口#1447 之后仍有三处类型不卫生type hygiene问题这也是 ADR 动议的动机非法字段组合仍可表示url / error / error_code全部可选理论上可以构造出既带 error 又带 url或failed 却带 url的自相矛盾实例分区不在类型里not_found仅轮询、removed仅等待这一约束只是约定 测试类型系统本身不阻止误用is_rate_limited的判定脆弱它先检查error_code USER_DISPLAYABLE_ERROR失败后回退到对error文本做子串匹配rate limit/quota/limit exceeded且把removed与failed同等对待以便限流重试策略在服务端静默丢弃工件时仍能工作见 src/notebooklm/_types/artifacts.py。值得强调的是 ADR 的判断#1342已经移除了承重的重载无法启动即抛异常的路径因此这是类型卫生问题而非正确性缺口——这也直接影响了最终的推迟结论。四、决策 A密封结果类型的设计蓝图A1–A6ADR 将如果构建长什么样完整规格化共六条。A1 — 两种角色类型PollResultPending | InProgress | Completed | Failed | NotFound | Unknown快照方法用WaitOutcomeCompleted | Failed | Removed等待器用。分区被编码进类型本身这是相对现状的核心增益之一。A2 — 独立 frozen 变体而非GenerationStatus子类理由frozen 变体无法继承非 frozen 的GenerationStatus可变的子类也无法让状态不可变。字段排序问题可以用kw_onlyTrue规避但不可变性无法规避。但 A2 附带一个缩小收益的冷水警告头号卖点——每个变体的必需字段——在当前数据面前大体无法实现Completed.url对非媒体工件合法地为NoneArtifactRow.is_media_ready对非媒体类型恒真kickoff 即完成_parse_generation_result位于_artifacts.py时同样为NoneFailed.error有时为Nonepoll_status在 src/notebooklm/_artifact/polling.py 中的构造只填充url与metadata不填errorRemoved携带error但没有error_code等待循环的 removed 构造见上节 L441-L450。因此Completed.url/Failed.error只能保持str | None或者让Completed拆成媒体完成url: str与文档完成url: str | None两个变体。变体买到的实际价值是角色分离 穷尽式match 结构化失败而不是非法状态不可表示。ADR 明确建议在投入 A1–A6 之前先重新评估下文 Alternatives 中的子类型细化方案。A3 — 超时保持为异常不引入TimedOut变体与 ADR-0019 的异常模型保持一致。A4 —failure_reason: FailureReasonRATE_LIMIT | OTHER | UNKNOWN挂在Failed/Removed上推导规则必须钉死error_code USER_DISPLAYABLE_ERROR或当前的消息启发式——因error_code经常缺失而保留在分类器内部→RATE_LIMIT存在失败但非限流 →OTHER无法分类无error_code、消息也不匹配→UNKNOWN。关键认知A4 并不会免费去掉子串匹配它只是把匹配从散落各处的调用点收拢进单一分类器。error/error_code在变体上仍然可选。A5 — 兼容性仅限鸭子类型 / 源码层面通过共享的Protocol/ mixin 暴露.statusis_*让谓词型消费者可以渐进迁移_app/generate_retry.py中的鸭子类型hasattr(status, is_complete)从源码看ADR 记载的 CLI 服务层 duck-type 在重构后由 src/notebooklm/_app/generate_retry.py 承担类似职责_artifact/polling.py轮询循环的status.is_complete or status.is_failedL387match/.status调用方。但它不保留名义类型检查nominal checks以下引用具体类型的点都是明确的迁移工作generate_retry.py中的isinstance(result, GenerationStatus)门L105、L209CLI 层对 JSON 字段的直接镜像wait_for_completion的on_status_change回调polling.py 每次状态转移都调用它并传入具体实例ArtifactTimeoutError携带的status_transitions历史polling 循环把每次转移的GenerationStatus追加进列表并传入超时错误构造L331、L383。此外frozen 变体会丢弃 str-Enum 的裸字符串构造容忍GenerationStatus(statuscompleted)今天合法is_*谓词按比较故仍工作测试test_raw_string_constructed_predicates钉住了这一点而 frozen 联合类型不接受——这是一个需要明示的有意行为变更。A6 — 加法优先的迁移带一个诚实的缺口Phase 1加法新增poll_result()/wait_result()返回变体内部通过一个GenerationStatus → variant适配器实现适配器对构造出的.status即GenerationState成员做match。再次强调_status_from_code不是接缝它是上游规范化器从不发射NOT_FOUND/REMOVED。约 13 个扁平方法继续返回GenerationStatus。Gapkickoff 没有廉价的加法对12 个快照 kickoff 方法generate_*、revise_slide、retry_failed各自加*_result()需要新增 12 个方法。选项 (a) 提供公开的GenerationStatus.as_poll_result()转换器供调用方选用(b) 接受kickoff 返回翻转只发生在 Phase 3 破坏性变更。ADR 选择 (a) 作为更便宜的桥梁。Phase 2跑道按 ADR-0018 的真实契约弃用扁平返回方法迁移 CLI / services / docs以及15 个测试文件中 105 处GenerationStatus(...)构造点。Phase 3单一破坏性翻转在大版本移除 / 重标注扁平路径并翻转 kickoff 返回值。原地重标注会被 scripts/audit_public_api_compat.py 标记为changed-return所以只可能发生在该阶段。五、决策 B推迟且被强化采纳 A1–A6 作为设计档案design of record但现在不构建。理由链条比 v1 更强承重重载已移除#1342str-Enum 测试强制分区已经捕获了可廉价兑现的价值头号卖点按变体的必需字段在真实数据面前大体不可实现A2——因此现在做破坏性全拆分相比非破坏性的子类型细化只多买到角色分离 穷尽 match 结构化 failure_reason。这个边际收益不足以支撑一个跨 13 个方法、CLI、回调/异常、105 处测试构造点的多阶段改造尤其是在 v0.8.0 刚发布之后。若触发器触发应先重新评估子类型细化方案Alternatives——只有在更轻的路径被否决后A1–A6 才是规格。重新评估触发器满足任一即构建(a) 扁平形态引发具体的、反复出现的 bug(b) 已开启的 planned major / 版本跑道窗口(c) 出现需要按变体字段的新功能(d) 研究侧收敛——ResearchStatus已经是str, Enum且含NOT_FOUND一个共享的密封结果模式可以只建一次、在两个生命周期artifact 与 research中摊销项目看重模式与其门槛一起构建一次而非按命名空间反复重新决策。六、范围与后果In类型形态、角色拆分、超时 /failure_reason/ 兼容性决策、迁移序列。Out异常模型、GenerationState枚举#1447 已完成、Source/Research状态类型以及——在决策 B 下——实现本身。后果分两种情形推迟推荐零新增代码设计被记录在案既不会被反复重提也不会被意外欠债取代 ADR-0019 的Tier 3 推迟条款#1447 打下的地基让未来 Phase 1 保持廉价。若构建获得穷尽式match、角色区分的 poll/wait 类型、收敛进单一分类器的failure_reason不获得非法状态不可表示字段仍可选A2。代价是跑道期双表面并存、CLI/services/回调/异常/105 处测试构造点迁移、丢失裸字符串构造、以及大版本上的一次破坏性翻转。七、备选方案对比方案结论理由Status quo现状扁平GenerationStatus推荐的静息状态作为推迟基线被接受子类型细化Completed(GenerationStatus)…触发时更强的候选非破坏性、覆盖全部 13 个方法标注仍为- GenerationStatus、获得isinstance/match与变体方法。既然 A2 已证明必需字段无论如何不可实现它就在无跑道、无破坏性翻转的前提下交付了可兑现的收益角色判别、match。其相对独立联合的唯一损失真正不可变 必需字段恰是数据上不成立的部分原地重标注方法拒绝changed-return破坏、无跑道TimedOut/RateLimited作为变体拒绝超时是异常性的A3限流是失败细节A4八、如何继续深入源码阅读路线图若想验证本文全部论断可按以下路径阅读当前仓库类型层src/notebooklm/_types/artifacts.py——GenerationState枚举L483、_status_from_codeL596、GenerationStatus与全部is_*谓词L621轮询 / 等待层src/notebooklm/_artifact/polling.py——poll_status的NOT_FOUND构造、wait_for_completion的退避重试、removed判定与超时错误组装测试强制分区tests/unit/test_generation_state.py——test_poll_status_never_returns_removed、test_status_from_code_never_returns_wait_only_states、test_is_terminal_partitions_the_enum_exactly等兼容性现场src/notebooklm/_app/generate_retry.py——isinstance(result, GenerationStatus)的名义检查与hasattr(status, is_complete)的鸭子类型并存正是 A5 所述迁移工作的真实样本异常模型src/notebooklm/exceptions.py 中的ArtifactTimeoutError层级以及 src/notebooklm/_app/errors.py 对超时异常的分类映射前序决策ADR-0019 错误与返回契约Tier 3 条款即被本 ADR 取代与 ADR-0018 弃用策略Phase 2 跑道的契约依据。这套现状收益已被廉价兑现 头号收益在数据面前不可实现 触发条件明确 更轻方案优先的推理结构不仅是 NotebookLM 工件生成链路的设计注记也为其他把状态机 异步轮询暴露为公共 API 的库提供了一个可复用的类型演进决策模板。【免费下载链接】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),仅供参考