十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

notebooklm-py CLI 的 `--json` 类型化错误信封契约:从 `ClickException` 盲区到全路径 JSON 化(ADR-0015 深度解读)

notebooklm-py CLI 的 `--json` 类型化错误信封契约:从 `ClickException` 盲区到全路径 JSON 化(ADR-0015 深度解读) notebooklm-py CLI 的--json类型化错误信封契约从ClickException盲区到全路径 JSON 化ADR-0015 深度解读【免费下载链接】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-0015 为核心骨架完整讲解该 CLI 在--json模式下如何把click.ClickException及其子类UsageError、BadParameter等统一收编进类型化 JSON 错误信封覆盖 parse-time 与 post-parse 两个阶段的语义差异、根命令SectionedGroup的非 standalone 模式实现、以及内联 marker 注释 守卫测试的强制机制。读完你将掌握notebooklm命令在--json下每一种失败路径的退出码与 stdout/stderr 行为如何正确地在命令体与服务层中触发VALIDATION_ERROR信封以及如何用json.loads(stdout)编写可靠的自动化脚本。背景稳定错误契约与ClickException的盲区notebooklm-py的 CLI入口为 src/notebooklm/notebooklm_cli.py为自动化场景维护了一套稳定的错误契约在--json模式下每一条致命命令路径都会在stdout上输出一个typed JSON error envelope类型化 JSON 错误信封——一个扁平对象形状如下{ error: true, code: STABLE_CODE, message: human text, ...extras }同时进程以 docs/cli-exit-codes.md 表格中对应的标准退出码退出。该契约的规范实现是 src/notebooklm/cli/error_handler.py 中的_output_error(...)以及它导出的公开别名output_error。对于库异常AuthError、RateLimitError、ValidationError、NotFoundError等契约早已清晰——handle_errors(...)上下文管理器会按异常类型分派到对应分支映射出RATE_LIMITED、AUTH_ERROR、VALIDATION_ERROR、NOT_FOUND等稳定code。真正的模糊地带在click.ClickException及其子类上handle_errors(...)在except click.ClickException: raise分支error_handler.py 第 464-466 行中刻意原样重抛它们随后 Click 用自己的Usage: ... / Error: ...散文格式把错误写到 stderr 并退出。这意味着自动化脚本只要踩到一条 Click 异常路径json.loads(stdout)就会立刻失败。该 ADR 的定位正是填补 docs/cli-exit-codes.md 对 post-parseUsageError的沉默原文档第 52 行的表格行只描述了 Click 重抛自身异常第 64 行的 JSON 小节只描述了库异常的信封两者从未交叉。ADR-0015 把这条缝隙正式封上。问题的根源两个不同阶段的ClickExceptionClickException子类会在两个语义截然不同的阶段被抛出它们在 Click 层面的表现相同都渲染 usage 文本、都以类级exit_code退出但对调用方意义完全不同Parse-time解析期抛出发生在命令体运行之前。Click 自己的解析器在 argv 未通过选项/类型校验时抛出UsageError/BadParameter例如--limit foo--limit是IntRange、未知选项、缺少必需参数。此时命令函数尚未被调用handle_errors(...)还不在调用栈上--json也许已经在命令行上被输入了但解析器从未完成对它解析所以从 Click 解析器内部满足调用方对 JSON 信封的期望在结构上是不可能的。Post-parse解析后抛出发生在命令体内部或它所调用的服务层中此时 argv 解析已成功--json标志的值已绑定到click.Context.params。这些是程序自己做出的验证决策——标志组合冲突、计算出的前置条件、文件格式校验——只是恰好用抛出ClickException子类来表达而不是抛库异常。它们会命中 error_handler.py 的except click.ClickException: raise分支从而跳过信封。CLI 审计编号P1#2 command-bodyUsageError/BadParameterbypass原文定位在审计报告 54-90 行枚举了骑乘这条 bypass 的 post-parse 抛出点结合当前仓库源码可逐一印证站点原 ADR 列举触发场景仓库现状cli/services/download.py:257--force/--no-clobber冲突当前 cli/services/download.py 中该冲突消息Cannot specify both --force and --no-clobber仍存在cli/services/generate.py:394--style custom要求--style-prompt当前 cli/generate_cmd.py 第 121 行仍有custom_style_prompt_required校验字面量cli/research_cmd.py:158--cited-only要求--import-allcli/research_cmd.py 第 577 行仍有--cited-only requires --import-all的UsageErrorcli/source_cmd.py:626--cited-only要求--import-all同类校验仍在cli/chat_cmd.py:193--new与--conversation-id互斥cli/chat_cmd.py 第 357 行仍有互斥UsageErrorcli/generate_cmd.py:57语言代码校验校验逻辑仍在对这些站点中的任何一个执行cmd ... --json结果都是退出码2、Click 的 usage 文本落在 stderr、stdout 上没有 JSON。依赖json.loads(stdout)分支的自动化脚本即 docs/cli-exit-codes.md 中公布的配方就此中断。审计的元分析meta-audit从三个角度重新框定了这一发现ADR-0015 逐一采纳C1将契约决策声明为 stop-sentinel——在契约被决定并记录之前不应有任何实现 PR 改造这些站点因为两个候选修复形态路由进信封 vs 把ClickException整体豁免出--json会驱动互相矛盾的补丁。C3将层级归属扩大——同样的形态存在于服务模块cli/services/generate.py383/394/396/398 行、cli/services/download.py257/259/261 行、cli/services/source_mutations.py18 行因此任何契约决定同时适用于命令代码与服务代码。当前 cli/services/source_mutations.py 第 123 行仍返回VALIDATION_ERROR这一错误码印证该契约已渗透到服务层。I1纠正审计中docs/cli-exit-codes.md内部自相矛盾的说法——文档只是沉默ADR-0015 负责填补。决策post-parseClickException全部流经类型化信封ADR-0015 的核心决定是一句话在--json下从命令体或服务层抛出的每一个 post-parseclick.ClickException子类失败都必须输出 docs/cli-exit-codes.md 定义的类型化 JSON 错误信封以对应标准码退出且 stderr 不写任何 usage 文本。决策分五条规则逐条落地Parse-timeClickException原样保留后被 2026-06-02 修订取代见下文专节。Click 解析器继续为 argv 级校验失败抛出UsageError/BadParameter/ClickExceptionClick 继续把Usage: ... / Error: ...写到 stderr 并以2UsageError/BadParameter或1基类ClickException退出。error_handler.py 的重抛保持不动。handle_errors本身在这种情况下不产生 JSON 信封——因为解析器触发时它还没被进入——但位于其上方的根组会兜底见修订节。Post-parseClickException流经类型化信封。命令体和服务层代码若要在 JSON 契约下表达验证失败不得直接抛出click.UsageError/click.BadParameter/click.ClickException必须改为调用cli.error_handler的output_error(...)或抛出handle_errors(...)已经映射到信封上的库异常ValidationError/ConfigurationError。标志组合或前置条件冲突的自然选择是VALIDATION_ERROR退出1完整映射表见 docs/cli-exit-codes.md。本 ADR 不引入任何新的错误码键。线上格式不变。信封仍是_output_error早已产生的扁平对象{ error: true, code: CODE, message: text, ...extras }。已经用json.loads(stdout)解析并按code分支的调用方无需任何迁移。文本模式契约保留。未设置--json时post-parse 验证失败仍以人类可读消息出现在 stderr 并以对应标准码退出。对按规则 2 重构的站点消息来自output_error(...)而非 Click 的Usage: ... / Error: ...格式化器因此不再附带命令 usage 页脚——这是与规则 2 一致的最小用户可见差异规则 5 标记的站点则保持 Click 格式化器不变。标记化的残余ClickException抛出。少量站点今天正确地抛着ClickException子类并应继续如此——它们处在命令产生任何输出之前的输入校验边界上匹配 Click 自己的解析器风格错误渲染是正确的 UX例如 cli/input.py 中的 UTF-8 / 文件读取校验、cli/profile_cmd.py的 profile 名称参数校验、cli/resolve.py 第 58 行的实体 ID 参数校验、cli/services/login/profile_targets.py的共享 profile 名校验。这些站点由内联 marker 注释穷尽追踪# cli-input-validation: reason用于绕过信封的 Click 异常# cli-raw-exit: reason用于error_handler.py之外的裸SystemExit站点。tests/_guardrails/test_error_handler_allowlist.py 强制这些 marker 存在、非空且不过期。新站点需要携带带理由的本地 marker任何其他post-parse 验证失败的默认形态都是规则 2。源码级剖析output_error与handle_errors的映射引擎决策的规范实现集中在 src/notebooklm/cli/error_handler.py 中是理解全契约的钥匙。_output_error第 159-195 行是唯一产出信封的函数。JSON 分支构造{error: true, code: code, message: message}把extra字典展开进顶层用click.echo(json.dumps(response, indent2, defaultstr, ensure_asciiFalse))输出到 stdout随后raise SystemExit(exit_code)。文本分支用safe_echo(message, errTrue)写 stderr可附加hint同样以SystemExit退出。注意两个关键细节defaultstr让信封可以序列化非 JSON 原生类型如时间对象模块顶部的注释明确模块外的click.ClickException/ 裸raise SystemExit站点由内联 marker 治理第 33-38 行旧的行号 allowlist 已在 issue #1298 中移除因为任何编辑都会导致行号漂移、无行为变化却触发 CI 失败。handle_errors(...)第 254 行起是库异常的分派引擎。它内部的emit(...)闭包先把幂等探测未决unconfirmedTrue的写操作改写为UNCONFIRMED_WRITE码并替换掉分支原有的重试建议避免自动化按RATE_LIMITED的指引盲目重试造成重复写入再把操作元数据operation_metadata_payload、未决写入提示exception_json_fields、部分上传保留的source_id/stage折叠进信封最后调用_output_error。异常映射完整对应 docs/cli-exit-codes.md 的中央表格异常或失败JSONcode退出码标记unconfirmedTrue的异常UNCONFIRMED_WRITE继承匹配分支库错误1未预期异常2RateLimitErrorRATE_LIMITED1AuthErrorAUTH_ERROR1ValidationErrorVALIDATION_ERROR1ConfigurationErrorCONFIG_ERROR1NetworkErrorNETWORK_ERROR1NotebookLimitErrorNOTEBOOK_LIMIT1ArtifactTimeoutErrorARTIFACT_TIMEOUT1NotFoundError及领域*NotFoundErrorNOT_FOUND1其他NotebookLMErrorNOTEBOOKLM_ERROR1KeyboardInterruptCANCELLED130未处理的ExceptionUNEXPECTED_ERROR2NOT_FOUND分支尤其值得注意它通过_NOT_FOUND_ID_ATTRS元组notebook_id、source_id、artifact_id、note_id、mind_map_id、label_id、collection_id在运行时反射出具体子类携带的资源 ID同时在原生键和通用id键下暴露并支持 issue #1787 的近拼写 did you mean 候选candidates字段 did_you_mean_hint。ArtifactTimeoutError分支则序列化task_id、timeout_seconds、status_history、status_transitions等完整字段供自动化诊断轮询停滞。output_error是_output_error的公开别名第 202 行专供跨 CLI 包边界上行的层级如cli/services/*导入以满足 tests/_guardrails/test_cli_boundary.py 强制执行的公开边界契约。这是规则 2 落地的正式入口。2026-06-02 修订parse-timeClickException也被 JSON 包装原决策规则 1 声称 parse-timeClickException保持不变、此场景不产生 JSON 信封。这一前提handle_errors确实永远看不到解析期错误是正确的但结论已不再成立——因为 CLI 在handle_errors之上长出了第二条、更高的边界。根组 src/notebooklm/cli/grouped.py 中的SectionedGroup.main以non-standalone 模式运行 Click 超类的main专门为了捕获 Click 本会自行渲染的解析期click.ClickException。其 docstring 记录了这一意图在这个根边界捕获失败可以为每一个当前与未来的子命令选项统一转换一次失败。实现要点如下_json_requested(args)第 26-35 行在原始 argv上扫描--json标志遇到--分隔符即停止因为此时 Click 尚未解析出json_output参数值。捕获click.ClickException后若_json_requested(args)为真调用_emit_json_click_error(exc)第 38-45 行即output_error(exc.format_message(), VALIDATION_ERROR, json_outputTrue, exit_codeexc.exit_code)否则走exc.show()exit_with_code(exc.exit_code)的文本路径。click.Abort在同一边界同等处理--json下输出CANCELLED信封 退出1文本模式下输出Aborted!到 stderr 退出1。由此--json下 parse-time 失败的具体行为是类型化 JSON 信封输出到stdout——{ error: true, code: VALIDATION_ERROR, message: Click 格式化后的消息 }——stderr 完全无输出退出码被保留而非归一化信封透传exit_codeexc.exit_code所以UsageError/BadParameter仍以2退出基类ClickException仍以1退出。这与 post-parse 信封路径按决策规则 2/4 统一为退出1不同parse-time 包装只改变通道stderr → JSON stdout不改变退出码。修订的动机与原始契约完全同源一个传入--json的 JSON 消费者即使失败发生在 argv 层面非法--limit、未知选项、缺少必需参数也绝不应在 stderr 上收到 usage 散文而非可解析信封。在根边界转换一次比原立场argv 级失败在结构上无法包装——这在 Click 解析器内部是真的但在以 non-standalone 模式运行解析器并捕获其抛出的边界上不成立对自动化严格更有用。契约测试行为被钉死在测试套件里修订与决策均由 tests/unit/cli/test_json_validation_contract.py 钉死test_json_validation_errors_emit_json参数化覆盖非法 limit / 非法 interval / 非法 retry / 缺少参数 / 未知选项 / 根回调校验失败六类 parse-time 场景断言 stdout 上有VALIDATION_ERROR信封、stderr 为空、退出码非零、error is True。test_command_body_click_validation_emit_json验证命令体note create的位置参数与--content冲突在--json下同样产出VALIDATION_ERROR信封且消息包含 Cannot use both。test_json_abort_emit_json用SectionedGroup定义的最小根组钉死click.Abort在--json下输出{ error: true, code: CANCELLED, message: Cancelled by user }且退出1。test_validated_json_options_emit_json_on_bad_values程序化遍历整个 CLI 命令树_walk_leaf_commands为每个带--json选项的叶子命令的每个可校验参数生成非法值IntRange取 min-1、IntParamType取not-an-int、Choice取非法项断言全部产出VALIDATION_ERROR信封——这是每条当前与未来子命令选项都得到统一信封承诺的机器验证。test_text_validation_errors_keep_click_usage_output钉死文本模式不变——退出2、stderr 含Usage:、stdout 为空。此外 tests/_guardrails/test_error_handler_allowlist.py 以 AST 静态分析扫描全部cli/*.py排除error_handler.py强制执行 marker 治理每个ClickException及信封旁路子类UsageError、BadParameter、MissingParameter、NoSuchOption、BadArgumentUsage、FileError调用点必须有# cli-input-validation:markerraise SystemExit必须有# cli-raw-exit:markermarker 与调用点 1:1 匹配一行上的单个 marker 不能同时满足两个调用陈旧 marker没有调用点可认领与空理由 marker 均判失败裸SystemExit站点另有MAX_RAW_SYSEXIT_SITES 5的上限兜底click.Abort与click.exceptions.Exit被刻意排除它们是控制流而非错误消息退出issue #1307。从当前仓库实际扫描可见 marker 已铺满各校验边界例如 cli/input.py 的 UTF-8/互斥/prompt-file 校验31/72/82/88/92/99 行、cli/chat_cmd.py 第 357 行的互斥校验、cli/note_cmd.py 第 158 行的位置参数冲突、cli/resolve.py 第 58 行的实体 ID 校验等每条都带明确的理由字符串。自动化落地--json下的可靠脚本配方综合 ADR 与 docs/cli-exit-codes.md 的用法--json下的推荐脚本模式是把 stdout 重定向到文件用$?分支退出码用jq读取code作为机器可读错误类别notebooklm ask -n $NOTEBOOK_ID Summarize --json out.json case $? in 0) ;; # 成功 1) jq -r .code out.json 2 ;; # 预期内的命令失败含 VALIDATION_ERROR / RATE_LIMITED / AUTH_ERROR / NOT_FOUND … 2) echo invalid invocation or CLI bug 2 ;; 130) echo cancelled 2 ;; esac实践要点不要用?2区分坏 argv与坏组合。ADR-0015 有意把 post-parse 验证失败从 Click 的2归一到12保留给系统/未预期错误与 parse-time 路径标志组合失败属于用户/应用错误归1。修订后 parse-time 失败在--json下虽输出VALIDATION_ERROR信封但仍以2退出——退出码语义因此是精确且无歧义的。code是稳定契约message允许变化。docs/cli-exit-codes.md 明确说明用稳定的 JSONcode区分错误类别人类可读消息可以变化。NOT_FOUND信封可携带id与资源专属 ID 字段RATE_LIMITED携带retry_after。stdout 纯净性是硬保证--json下信封只出现在 stdoutstderr 为空test_json_validation_contract.py直接断言result.stderr 与result.output result.stdout配套的 tests/unit/test_json_error_exit.py 与 tests/unit/test_json_stdout_purity.py 守护 JSON 纯净性错误路径 argv 用例可直接追加而不需要新测试基建。后果与权衡期望得到的收益--json成为覆盖所有命令体失败的可靠机器契约——自动化按json.loads(stdout)code分支时验证冲突与RATE_LIMITED、AUTH_ERROR表现完全一致。docs/cli-exit-codes.md 不再对 post-parseUsageError沉默关闭元审计项 I1消灭一类但文档说……的报告。命令代码与服务代码收敛到单一错误发射路径output_error(...)/ 库异常经handle_errors(...)与 ADR-0008 的cli/services/抽取模式干净组合服务模块不再需要在抛 Click 异常跳过信封与导入output_error耦合 CLI 层之间二选一ADR-0008 边界成为强制 typed-outcome 返回的正确位置。测试可沿用既有 JSON 纯净性扫描无需新基建。必须接受的代价部分 post-parse 失败此前带 Click 的Usage: ... / Error: ...页脚按规则 4这些站点的文本模式输出会失去 usage 页脚换成一致的 stderr 消息。审计认为可接受usage 页脚约定服务于 argv 形状错误而 post-parse 失败是语义形状错误消息体本身已经自解释。post-parse 失败的退出码从2Click 的UsageError.exit_code变为1标准VALIDATION_ERROR退出。这是有意的2保留给系统/未预期错误与 parse-time 路径。以前在 post-parseUsageError后按?2分支的脚本本来就是在混淆坏 argv与坏组合新语义无歧义。契约制造了 parse-time 与 post-parseClickException处理之间的永久分叉审查者在命令/服务代码中新增校验时必须套用规则 2修改 Click 解析器配置时必须套用规则 1。分叉已在本文档记录避免下一位审查者重新争辩。元审计 C4 枚举的 Pattern A / Pattern B 服务层站点约 10 个模块仍需要后续 PR 真正路由进信封ADR-0015 只记录契约本身不移动任何代码。每个后续 PR 引用本 ADR 并从一个 bypass 站点移除一个站点。当前 cli/services/source_mutations.py 第 123 行已出现VALIDATION_ERROR码说明收敛工作正在逐步落地。被否决的替代方案及其理由ADR 记录了对四条替代路径的评估理解它们有助于把握契约边界把ClickException子类整体豁免出--json文档化例外。被否决这把审计标记的行为冻结为契约--json自动化在命令体验证失败时仍无法依赖 JSON 输出文档还得枚举哪些校验被覆盖、哪些没有矩阵随新命令不稳定且审计 P1 排名元审计 C2明确把自动化破坏列为影响最高的用户可见问题。把每个ClickException抛出都转成库ValidationError。作为契约决策被否决但作为每个站点的合法修复形态被接受。部分当前ClickException站点确实是 argv 级的规则 5 白名单中的那些强行走ValidationError会给交互用户暴露更不友好的消息。决策是路由进信封不是停用 Click 异常白名单存在的意义正是让每个站点选择正确形态。把json_output灌进每个服务辅助函数、让辅助函数直接调output_error(..., json_output...)。作为主要契约形态被否决会让服务模块耦合 CLI 错误层违反 ADR-0008 的边界但作为任何单站点在修复压力下的最小可行补丁被接受。首选长期形态是服务返回 typed outcome命令把 outcome 路由进output_error短期补丁是服务直接调output_error。两者都满足本 ADR。让handle_errors无条件捕获ClickException并转换为信封。对handle_errors被否决这会连 parse-timeClickException一起吸收但handle_errors在命令体内部进入parse-time 路径永远到不了它Click 在调用命令前就通过BaseCommand.main渲染解析器错误。拦截 parse-time 错误需要子类化 Click 的命令/组机制原判断这超出错误处理契约变更的范围——而 2026-06-02 更新明确该子类化 Click 组机制的思路随后被采纳只是落在根组而非handle_errorsSectionedGroup.main以 non-standalone 模式运行 Click 超类并在--json下把 parse-timeClickException转成 JSON 信封。这是独立于handle_errors的边界所以handle_errors自身看不到 parse-time 错误的推理依然成立。延伸阅读docs/cli-exit-codes.md完整退出码约定、JSON 信封形状、source stale/source wait等命令专属契约。src/notebooklm/cli/error_handler.py信封产生与异常映射的规范实现。src/notebooklm/cli/grouped.py根组SectionedGroup.main的 non-standalone 模式与 parse-time JSON 包装。tests/unit/cli/test_json_validation_contract.py全命令树非法参数扫描与信封断言。tests/_guardrails/test_error_handler_allowlist.pymarker 强制机制与原始SystemExit上限。docs/adr/0008-cli-services-extraction-pattern.mdcli/services/抽取模式与信封契约组合的层级边界。【免费下载链接】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),仅供参考
返回列表