DeepSeek Harness 插件

fhidalgodev/dsh-odoo-sdd

Star 数 ★ 5 下载量(近 30 天) 1,717 分类 工作流与自动化 收录于 2026-09-18 npm dsh-odoo-sdd

面向 Odoo 的规范驱动开发流水线:15 个工具把需求从不可变规范推进到架构、再到模块代码或线上配置,最终要求一个由真实 Odoo 实例证据支撑的持久判定;闸门 fail-closed,批量操作需在声明的环境中经人工批准,复用 Odoo 自带的 CSV/Excel 导入器,并有一份可回滚本插件所写数据的日志。

安装

# npm 包(预构建)

dsh plugin --profile web add dsh-odoo-sdd

# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)

dsh plugin --profile web add github:fhidalgodev/dsh-odoo-sdd

装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。

README

面向 Odoo 的规范驱动开发


⚡ 概述

dsh-odoo-sdd 把 DeepSeek Harness 变成一条规范驱动(SDD)的 Odoo 开发流水线。有两个理念把它撑起来:

  • 闭环反馈 —— agent 会安装和升级模块、读取服务器 traceback,并针对一个 真实运行中的 Odoo 实例重试。插件从不启动 Docker 或 odoo-bin:你通过一个 被 gitignore 的 .env 把它指向你已有的实例(dev/staging),工具之间只说 标准 JSON-RPC。
  • 流水线安全 —— 每个阶段都持久化到磁盘,门禁在缺少显式 APPROVED 标记时 fail-closed,连续三次失败会强制做根因诊断,verify/fix 迭代次数有上限, stop.md 会中止一切,而且结论是诚实的:失败的验证会持久化为 FAILED, 永远不可能被报告成成功。
  • 没有 spec 就没有变更 —— 在流水线关闭之后才到来的请求是一次新的变更, 哪怕它出现在同一个对话里("顺便把 X 也改了"):它要么得到自己的(小份)spec, 要么得到你显式的 waive。这道守卫对实例变更和源码编辑一视同仁,并且会说明三条 出路里哪一条可用,而不是悄悄改掉。

[!NOTE] 它不是什么: 不是基础设施编排器,不是通过聊天管理凭据的管家,也不是 自动提交器。它不写任何 commit,也从不向你索要密码。

环境要求

需求 原因
DSH ≥ 0.1.2-rc.1,运行在 Node ≥ 20 上 插件以 Cordis bundle 的形式挂载,并使用 tools 服务
一个可通过 HTTP(S) 访问的现有 Odoo 实例 闭环需要一台真实服务器来安装模块并读取 traceback
一个可丢弃的 dev/staging 数据库 验证过程会安装模块并写入测试数据
(可选) 一个 Playwright 浏览器工具 仅用于验证的 UI 层以及叙述式手册的截图;没有它时,这些场景会被标记为需要人工检查
(可选) playwright-core ≥ 1.63.0 作为可选依赖随插件发布;截图引擎会驱动 Chromium、Chrome 或 Edge,一个都找不到时会返回 NEEDS_BROWSER 并附上一行补救命令

🔭 工作原理

flowchart TD
    A(["想法或需求"]) --> C1["1 CLARIFY<br/>访谈与安全问题"]
    C1 --> R2["2 READ_SPEC<br/>不可变的 spec.md"]
    R2 -->|APPROVED| A3["3 ARCHITECTURE<br/>模型、视图、安全、测试计划"]
    A3 -->|APPROVED| W4["4 WRITE_CODE<br/>模块源码与 OCA 文档"]
    W4 --> V5["5 VERIFY<br/>静态、安装、RPC、UI"]
    V5 -->|PASSED| D9(["handoff.md - DONE"])
    V5 -->|FAILED| F6["FIX_LOOP<br/>根因,最多 5 次迭代"]
    F6 --> V5
    W4 -.-> M7["odoo_module install 或 upgrade"]
    M7 -.->|traceback| E8["odoo_errors"]
    E8 -.-> F6

规范是唯一的真相来源,而且它不可变:代码去适应 spec,绝不反过来。门禁默认由 人来回答;当你选择自主模式时,也可以把它们委派给一个人工代理 agent。


✨ 核心特性

  • 🎯 永远先有 spec,再写代码。 spec.md 携带编号的验收标准;磁盘上没有 持久化的 PASSED 结论,就永远到不了 DONE。
  • 🧬 稳定的需求标识。 每个需求块获得一个稳定的 REQ-<AREA>-NN id(章节 slug + 序号), 外加一枚提交到 req-baseline.json 的指纹。在已有 id 之下改写需求措辞就是 DRIFT: 在你回退它或显式接受它(sdd_phase operation=baseline)之前,推进会被拒绝,这样一份 计划或一个测试就绝不会悄悄引用一条已经不再那么说的需求。
  • 🔗 双向覆盖。 用 # REQ-<AREA>-NN 标注代码和测试,odoo_validate module_dir=… spec_dir=… 会同时检查两个方向 —— 没有实现的需求,以及 spec 已不再声明的孤立标注。下限: must/shall 在 1.0 处阻断,should 在 0.8 处报告,may 仅作信息。每条需求都会落在 covered / untested / test-only / unimplemented 这架梯子上。
  • 🔍 歧义 lint(建议性)。 mark_spec_loaded 报告没有量化的形容词、含糊的量词、 TBD 占位符、没有施动者的被动句以及 and/or 复合句,每一条都给出具体改写 —— 只是建议, 绝不是门禁。
  • 🧪 真的会失败的测试。 odoo_tests 检查安装日志看不见的测试套件:Odoo 从不导入的 测试文件(没有任何东西加载它)、什么都不断言的测试、被吞掉的异常、破坏逐测试回滚的 cr.commit()、time.sleep、跳过 —— 并把每一条验收标准映射到提到它的测试。scaffold 写出 RED 桩(在实现之前一直抛错),而不是空洞的通过。
  • 🔴 修 bug 时先 RED 后 GREEN。 在 mode=bug 下,流水线拒绝离开 WRITE_CODE, 直到 test-plan.md 记录了先失败的复现(red (evidence: …),修复后则是 pass (red first: …))—— 一个从未失败过的回归测试证明不了任何东西。
  • 🔁 真实的反馈闭环。 odoo_module install 返回服务器自己的输出或 traceback;odoo_errors 读取 ir.logging;失败会变成一个持久化的 FAILED 结论,而不是一份乐观的总结。
  • 🧾 不只是模块。 同一套机器也会跑功能型 spec(mode=functional): 配置一个在线实例,并以人工批准的批次加载数据,CSV/Excel 走 Odoo 自己的 导入器,最后用一份别人能照着复现的 runbook 收尾。 → 功能路径
  • 🔒 有凭据不等于有授权。 一个项目打开第一个 socket 之前,需要一份绑定 url + db + user 的显式人工授权(.sdd/grants.json)。
  • ⏪ 说真话的回滚。 checkpoint 会快照文件,并记录每一次 odoo_execute 的前像;restore 总会报告 checkpoint 之后创建的文件。撤销不了的东西,它会直说。
  • 🧱 文档也是门禁。 odoo_docs 产出 OCA 的 readme/ 片段、Apps 的 index.html 和强制的 changelog 条目 —— 而且它对一个没有 spec、没有阶段、 没有实例的已有模块同样可用。
  • 🧪 无需实例的静态层。 odoo_validate(结构 + ACL 一致性)和 odoo_security_scan(原生 SQL、sudo()、auth="none"、QWeb t-raw……) 在任何东西被安装之前就给出带 file:line 的发现。
  • 📖 手册像故事一样写,像代码一样检查。 odoo_manual 从实例读取一个模型的 功能 canon(按钮以及显示它们的状态、必填字段、notebook 页、菜单、访问规则、 确切的 raise UserError 位置),规划出分镜,用 canon 校验散文,默认只读地 截取带标注的截图,并渲染出一份人能照着走的手册 —— 每条指令配一张带标注的 截图,并在 canon 表明该按钮可能报错时给出恢复路径。
  • 🧑⚖️ 模型无法自行放松的策略。 白名单、守卫和委派模式,任何改动都需要 原生人工批准。
  • 🤖 受监督或自主。 同一条流水线,可以由人逐个回答门禁,也可以由人工代理 agent 无人值守地跑 goal 轮次,直到 DONE 或 BLOCKED。
  • 📁 spec 放在你想要的地方。 放在每个项目旁边,或者把每个项目的 spec 收集到一个可搜索的文件夹里。
  • 🖥️ Linux、macOS 和 Windows。 路径、原子写入和 .env 权限都按平台处理, 并在 CI 的 Windows 上测试。

🚀 快速开始

1. 安装到 profile

dsh plugin --profile web add dsh-odoo-sdd

那会从 npm registry 安装已发布的包 —— 不用 clone,不用 build,你这边没有 任何要编译的东西。dsh plugin 是一个很薄的 pnpm 转发器:它在 profile 目录里 运行 pnpm add,然后注册 bundle(dsh.profile.bundles)。有两个后果值得知道:

  • pnpm 必须在你的 PATH 上(不在时 dsh plugin 会报出来)。
  • 任何 pnpm spec 都能用,所以你可以锁定版本: dsh plugin --profile web add dsh-odoo-sdd@0.18.0。

更想用原生 npm —— 一个依赖这个插件的项目,或者一个 CI job?

npm install dsh-odoo-sdd        # 0.18.0,发布时带有 provenance 证明

[!IMPORTANT] 安装后请重启 DSH 并刷新浏览器标签页。客户端改动(Odoo SDD 设置面板) 从已安装的包里加载。

正在改这个插件本身? 那就安装这份 checkout。lib/ 是构建产物,不提交; npm install 会通过 prepare 钩子构建它,你也可以随时显式要求:

git clone https://github.com/fhidalgodev/dsh-odoo-sdd && cd dsh-odoo-sdd
npm install          # devDependencies: typescript,随后 prepare 构建 lib/
npm run host:deps    # 可选 peers,编译时需要(no-save)
npm run build        # 生成 lib/ —— 必需,包的 main 是 lib/index.js
dsh plugin --profile odoo add .

[!NOTE] 从 git 安装会在使用者的机器上跑那次 prepare 构建,而 pnpm 会阻止依赖的 构建脚本,直到你允许它们:命令会告诉你需要在 profile 的 pnpm-workspace.yaml 的 allowBuilds 下加入的确切键名。从 registry 安装 完全不需要这些 —— tarball 里已经带了 lib/。

dsh plugin add 会把 bundle 记录进 profile 的 package.json (dsh.profile.bundles),而这个包自带一个 Cordis 补丁 (cordis.patch.yml),它会插入自己的那一行 —— 所以没有手工组装步骤。 dsh --profile <name> --dump-config 会打印组装后的配置树,而不启动任何东西。

2. 给它凭据(每个项目一次)

让 agent 运行 odoo_setup mode=check。它会写一个不含密钥的脚手架, 密码由你自己填:

odoo_setup mode=interactive url=http://localhost:8069 db=odoo_dev username=admin
# 然后在打印出来的文件里填写 ODOO_PASSWORD(推荐使用 Odoo API key)
odoo_setup mode=authorize   # 只问你一次,授权这个确切的目标

或者手动来 —— 插件会使用下列位置中第一个存在的:

# 位置 范围
1 ODOO_SDD_ENV_FILE 显式的环境变量覆盖
2 <project>/.sdd/instances/<name>.env 每个目标一个文件,当前激活的实例
3 <project>/.sdd/.env 单目标布局(也会作为实例 default 出现)
4 ~/.config/dsh-odoo-sdd/.env(遵循 $XDG_CONFIG_HOME) 用户范围 —— 一套开发凭据供所有项目使用
5 <project>/.env 旧位置,仍然支持(会被标记为 legacy)

一个项目里的多个目标。 需要本地 CE 服务器、EE 预发服务器和客户实例的 工作区,不再覆盖唯一的 .env:每个目标在 .sdd/instances/ 下有自己的文件, 其中一个是激活的。

odoo_setup mode=instance instance=list                       # 有哪些,非机密身份
odoo_setup mode=instance instance=add name=staging url=https://stg.example.com db=stg username=ci
# 在 .sdd/instances/staging.env 中手工填写密钥,然后:
odoo_setup mode=authorize name=staging                       # 授权**那个**目标
odoo_setup mode=instance instance=use name=staging            # 为**本会话**激活它
odoo_setup mode=instance instance=remove name=staging         # 需要 confirm_destructive=true
odoo_setup mode=revoke name=staging                           # 只撤销该目标的授权

激活的目标属于「会话」,不属于项目。 每个对话保存自己的指针 (.sdd/instances/active-<sessionId>.json),因此同一项目上的两个会话可以同时使用 两个环境 —— 一个在预发,另一个在客户的服务器上 —— 互不影响。 未做选择的会话会被「询问」,而不会得到默认值。 新对话中的第一次 Odoo 操作会被拒绝 并返回该问题,其中包含可用目标及其非机密身份:

This session has no environment chosen yet, and this project defines 2 instance(s):
  - acme: https://… db=acme user=admin
  - globex: https://… db=globex user=admin   (the project's last used)
ASK THE DEVELOPER which one THIS session should use, then record the answer:
  - an existing one: `odoo_setup mode=instance instance=use name=<name>`
  - a new one: `odoo_setup mode=instance instance=add name=<name> url=… db=… username=…`, …

继承项目最近使用的目标只在第一次方便,之后就很危险:另一个会话切换环境会静默地把正在 工作的会话重定向,而它的工具回答仍然只是 OK。每个会话问一次即可消除这一点。 .sdd/instances/active.json 仅作为问题中显示的建议保留。没有具名实例的项目 (只有 .sdd/.env,单目标布局)永远不会被询问,而 requireInstanceChoice: false 可为偏好者恢复旧的继承行为。

授权是按目标的(url+db+user),因此同一台服务器上的两个实例是两个授权。 authorize 接受目标的名称,这正是让切换可恢复的关键:授权会在 12 小时后过期, 而"必须先激活才能授权"曾是一个死循环 —— use 要求目标有有效授权,而 authorize 只能看到当前激活的那个。直接命名即可打破循环:无需离开 main 就能授权 staging。

激活一个没有有效授权的目标仍会被拒绝:指针决定变更发往哪里,所以它不能指向 没有人批准过的目标。当存在多个实例且没有激活任何一个时,所有连接工具 都会拒绝,而不是按顺序挑选 —— 旧的 .sdd/.env 会作为 default 原样继续工作, 直到你自己决定迁移。

mkdir -p ~/.config/dsh-odoo-sdd && cd ~/.config/dsh-odoo-sdd
cp <plugin>/.env.example .env && chmod 600 .env
# 填写:ODOO_URL, ODOO_DB, ODOO_USERNAME,以及**一个**密钥:
#   ODOO_PASSWORD=<账户密码>   或   ODOO_API_KEY=<API 密钥>

密钥有两个被接受的名字。 会读取 ODOO_PASSWORD 和 ODOO_API_KEY,推荐使用 API 密钥:Odoo 在通过 RPC 接受密码的任何地方都接受它(设置 → 用户 → API 密钥, 并使用全局范围的密钥)。如果两者都设置了,ODOO_PASSWORD 优先,插件会报告 另一个被忽略,这样轮换后的凭据不会被漏掉。

只有一个地方 API 密钥不适用,这是 Odoo 的规则而非插件的规定:odoo_session 和 odoo_import 认证的是 Web 会话,而 Odoo 以 interactive: True 认证交互式 登录,仅在相反情况下才查询 API 密钥表。若需要这两者,请配置账户密码;其他一切 都可以用密钥完成。

插件使用哪个 API

服务器 传输 凭据
Odoo 19+ POST /json/2/<model>/<method> —— 具名参数,Authorization: Bearer API 密钥
任意版本 POST /jsonrpc —— execute_kw 密码或 API 密钥

在 19+ 上插件使用现代的 JSON-2 API:没有 uid 往返,具名参数由 Odoo 自身校验, @api.readonly 方法路由到只读副本,错误以 HTTP 状态加结构化响应体返回。当路由缺失 在三种情况下会自动回退到经典 JSON-RPC:路由缺失、服务器版本更旧,或者该凭据在那里根本 不可用。最后一种很重要:JSON-2 只接受 Authorization: Bearer,因此用密码配置的目标每次调用都会 返回 401 —— 而 401 意味着请求从未执行,所以改用另一传输方式不会造成重复副作用。服务器确实处理过 并拒绝的调用(403、422、领域错误)绝不重发。 json2 会报告路由缺失,而不是静默降级。

业务动作(action_*、button_*、do_*)始终使用 execute_kw:JSON-2 没有位置 参数形式,而为插件并不知道签名的方法臆造位置→名称映射,正是它拒绝做出的那种静默 误发。

[!WARNING] 绝不要把密码粘贴到聊天、spec、commit 或 issue 里。插件会拒绝组/其他用户可读的 .env,在每一次工具输出中脱敏密钥,并把会话 cookie 存到 .sdd/session.json (mode 600),从不把它们返回给模型。

3. 提出你的需求

为 Odoo 19 实现一个销售订单审批模块,使用 SDD 工作流。

agent 会从会话的 skill 目录里取出 odoo-sdd-workflow 并遵循协议。如果你想 说得更明确 —— 或者想确保完整指令被加载 —— 把消息以 /odoo-sdd-workflow 开头。

如果要在运行中的实例上做配置和数据工作,请改成这样说:

在我的 dev 实例里配置公司、税和会计科目表,然后导入
这个 customers.csv —— 功能型 SDD,dev 环境,不要碰生产。

那会选择 odoo-functional-sdd(或显式写 /odoo-functional-sdd) 以及下面描述的 functional spec 模式。


🧭 五个阶段

阶段 会发生什么 离开它的门禁
CLARIFY 记录意图(mode create/bug、licensed,以及可选的 translations —— 见下文)并回答安全访谈:组、ACL、记录规则、sudo() 的理由、公共路由 sdd_phase clarify
READ_SPEC 吸收 spec.md:业务背景、编号的验收标准、约束、目标 Odoo 版本。在此阶段写代码是被禁止的。 APPROVED + mark_spec_loaded
ARCHITECTURE 模型、视图(在重要之处包含额外的视图类型和 search 视图)、报表、安全矩阵和 test-plan.md APPROVED
WRITE_CODE 用按版本固定的 Odoo 模式实现模块及其 OCA 文档 静态门禁全绿
VERIFY 递增金字塔:静态 → 安装/升级 → RPC/数据 → 仅对关键流程做 UI(Playwright) 持久化的 PASSED 结论
FIX_LOOP 根因修复。连续 3 次失败强制顾问诊断;5 次迭代强制 BLOCKED 诚实的结论

字符串要翻译吗?(translations)

CLARIFY 会连同授权问题一起询问,答案记录在 spec 上 —— 不是全局设置,与版本选择完全一致。 同一个工作区可以既有委内瑞拉的客户项目,也有完全不交付语言的内部修复。

sdd_phase operation=clarify spec_id=<id> mode=… licensed=… translations=["es_VE","es_PA"]
  • 省略 translations 意味着该工作不翻译。 这是一个正常答案,因此与 mode 和 licensed 不同,它不是 fail-closed 关卡。
  • 代码是 gettext 形式(ll 或 ll_CC:es_VE、es_PA、pt_BR、fr)。格式错误的代码会 在还能纠正时被拒绝,而不是变成一个服务器永远不会加载的文件。
  • 声明语言会让 odoo_i18n 进入计划,并在 test-plan.md 中加入对应的验收标准: "存在 i18n/<lang>.po 且没有导出的条目留有空 msgstr" —— 由 odoo_i18n operation=check 验证。
  • sdd_phase status 会报告它们,因此"我们翻译了吗?"无需打开 state.json 即可回答。

在 ARCHITECTURE 阶段,agent 还会主动询问那些"早决定很便宜、晚发现很昂贵"的 事情:form/tree 之外的额外视图类型(包括用于描述模型如何被搜索的 search 视图 —— 自定义过滤器、收藏夹)、报表(通过 ir.actions.report/QWeb 的 PDF、SQL、CSV/XLSX、外部工具)、web tours (onboarding、测试,或都不需要 —— 并说明加载它的 asset bundle,因为没有任何 bundle 加载的 tour 永远不会执行)以及演示数据(哪些文件、用途是什么)。每一项 都要明确回答:"form + tree only"、"no reports needed"、"no tours needed"、 "no demo data"。这些是指引性决定:记录在 ## Views / ## Reports / ## Tours / ## Demo data 中,并在 sdd_phase status 里作为警告呈现,按设计不 阻塞 —— 安全模型才是唯一 fail-closed 的内容门禁。各版本的 tour API、执行它的 HttpCase 以及演示数据的陷阱都在 resources/references/tours-and-demo.md。

每个 spec 的产物(全部在磁盘上,可断点续跑):

specs/<NNN>-<slug>/
├── spec.md · architecture.md · test-plan.md
├── verify-verdict.txt   # 持久化的诚实结论
├── state.json           # 阶段、失败次数、迭代次数
├── kb.json              # 决策、被弃用的选项、blockers、诊断
├── docs-report.md · security-report.md
└── handoff.md           # 运行收尾时由 sdd_handoff 写入

🧩 功能路径(配置与导入)

不是每一项 Odoo 工作都是写代码。搭建一家公司、它的税、它的用户和它的主数据, 属于配置与数据,而且发生在在线实例上 —— 在那里,一次误点不是失败的测试, 而是一条真实记录。同一套 SDD 机器用一个不同的中间阶段和更严格的收尾规则覆盖它。

graph TD
    C1["1 CLARIFY<br/>目标、实例、ENVIRONMENT"] --> R2["2 READ_SPEC<br/>spec.md、标准、来源"]
    R2 -->|APPROVED| A3["3 ARCHITECTURE<br/>to-be 流程、批次、风险"]
    A3 -->|APPROVED| X4["4 APPLY_CONFIG<br/>发现 + 已批准的批次"]
    X4 --> V5["5 VERIFY<br/>重新读取记录,逐条 AC 取证"]
    V5 -->|PASSED| D9(["runbook.md + handoff.md - DONE"])
    V5 -->|FAILED| F6["FIX_LOOP"]
    F6 --> X4

    style X4 fill:#1e1e2e,stroke:#f9e2af,stroke-width:2px,color:#cdd6f4
开发型运行 功能型运行
在 CLARIFY 选择 mode=create 或 mode=bug mode=functional
中间阶段 WRITE_CODE(模块源码) APPLY_CONFIG(针对实例的批次)
交付物 模块 + OCA 文档 配置好的实例 + functional-runbook.md
Skill odoo-sdd-workflow odoo-functional-sdd

一次改动是如何到达实例的。 没有任何东西是"写着看看会怎样":

  1. 先做发现,在它自己的审批之下:哪些模型、哪些字段、多少条记录。读取 不是变更,但一个被批准的 scope 才能阻止"只是看看"变成一次改动。
  2. 计划:设计变成批次。每个批次声明它的目标和环境、用到的版本和能力、公司 和上下文、它覆盖的验收标准、有序的操作、记录标识、前置条件、预期结果、风险、 恢复方式和手工步骤。
  3. 批准:人看到确切的批次,并通过原生审批通道批准它。回执绑定 spec、设计、 计划和批次的哈希 —— 改动其中任何一个,审批即失效。
  4. 应用:一次一个操作,重新校验那些哈希,在调用之前和结果之后分别 持久化每个操作的状态。
  5. 未知结果不是重试。 一次变更之后的超时可能意味着 Odoo 已经提交了,所以 该操作会被标记为 indeterminate,批次停止,运行挂起,直到有人来对账。
  6. 诚实地收尾:sdd_phase succeed 要求每条验收标准都有明确的 pass,安全 审查是强制的,runbook 也是强制的(谁来做、在哪个公司、前置条件、验证过的 菜单路径、带字段标签的步骤、预期结果、如何检查以及如何撤销)—— 无论文档策略 怎么说。

环境必须声明,绝不假设。 目标上的 ODOO_SDD_ENVIRONMENT 会说 dev、 staging 或 production。声明了与目标不同环境的计划会被拒绝 (environment-mismatch),未声明环境的目标会被要求补上 (NEEDS_ENVIRONMENT),而生产环境额外需要一份声明的备份引用以及它自己的审批。 高风险改动先在 staging 中验证。

导入走 Odoo 自己的导入器,绝不走手写解析器:

odoo_import use=prepare file=... model=res.partner   # 上传文件,带它自己的审批
odoo_import use=preview   ...                        # ODOO 读到了什么:工作表、表头、样本
odoo_import use=map       ...                        # 每一列一个决定,不能留空
odoo_import use=plan      ...                        # 变成一个 `apply` 批次
odoo_functional operation=approve / apply            # 批次路径,保持不变

版本契约是显式的(主版本 10–19:旧端点上用 file/import_id + JSONP,新端点上用 ufile/id + JSON,应用时用 do/execute_import),不在已验证版本族内的版本会被 拒绝并告知该调查什么,而不是靠猜。上传之后被改动的文件会让映射失效;应答里 出现 nextrow 意味着导入器在文件中途停下了,会被报告为部分完成,绝不会报告为 成功 —— 而它已经计入的行绝不会被重发。会话 cookie 留在 .sdd/session.json (mode 600)里,不进入任何工具结果。

[!NOTE] 功能路径需要一个实例,而导入器需要一个 web 会话:在 odoo_import use=prepare 之前先运行一次 odoo_session。


🧰 18 个工具

工具 用途
odoo_connect 探测实例:服务器版本 + 认证。报告经过掩码处理;区分 NEEDS_SETUP / NEEDS_SECRET / DEFERRED / SKIPPED 状态(从不在聊天中索要密钥)。
odoo_setup 上手引导:check(级联 + gitignore + 委派模式)、interactive(不含密钥的 chmod 600 脚手架)、authorize(通过原生审批向开发者本人索要一个绑定当前 url/db/user 的连接授权)、revoke(撤销授权)、purge(先给出计划,然后在 confirm_destructive=true 加人工批准的前提下,只删除插件自己在 .sdd/ 下的状态)、later、skip、reset、autonomy(supervised | autonomous,需人工批准)。密钥永远不会作为参数被接受。
odoo_module 对 ir.module.module 执行 info / install / upgrade(button_immediate_*)。原样返回服务器自己的输出或 traceback,并做脱敏 —— 这就是闭环反馈。
odoo_execute 针对实例的通用 RPC(execute_kw):任意模型的任何公共方法都可调用,而它需要什么由它是什么决定。读操作(search、search_read、search_count、read、read_group、fields_get、name_get、name_search、default_get、exists、check_access_rights、check_access_rule)可自由执行,并可用 fields/limit/order/offset 做投影和分页(小数或负数的 offset 会被拒绝,绝不被截断);CRUD 变更操作(create/write/unlink)需要 confirm_destructive=true 且模型在 executeAllowlist 中,并且会被记入日志,以便数据撤销时重放它们;每一个业务动作(action_*、button_*、do_*……,例如确认一张订单)都需要 confirm_destructive=true 且无需白名单,因为插件无法重放它的效果 —— 它永远不会被记入日志,对它不存在撤销,而超时会报告为一个 INDETERMINATE 结果,而不是普通的失败 —— 同一条规则现在也适用于 CRUD:一个从未收到应答的 create 可能已经提交,把它报告成可重试的失败正是「一个效果变成两个」的方式。声明 precondition/postcondition(与批次使用的形状相同:{domain, expect, count}),在调用之前带上状态守卫、在调用之后带上状态证明:如果前置条件失败,什么都不会发出;如果后置条件失败,应答会说明调用确实已发出、实例可能只改了一半,而 CRUD 变更会保留它的日志回执(新建 id + 前像),因此仍然可以被补偿;没有它们时,OK 只意味着服务器接受了这次调用。私有方法(_name)会被拒绝,而这堵墙是 Odoo 自己的:execute_kw 通过 get_public_method 分发,后者对 _…、init、@api.private 和内部属性名会回答 AccessError —— 所以要在源码里读该模型(原生/自定义仓库由 odoo_config mode=read 报告),并调用包装它的公共按钮或动作(sale.order._create_invoices 是私有的;sale.advance.payment.inv 向导暴露了公共的 create_invoices())。要执行多个操作,或者希望每次运行都有人工审批时,请使用带 kind: "method" 的功能批次。context 原样转发 —— 在多公司实例上用 allowed_company_ids/company_id —— 服务器仍然会应用它自己的 ACL。
odoo_validate 本地、无需实例的模块结构检查:__manifest__.py 是否存在 + depends、声明的数据 XML 文件是否存在、有模型时是否有 security/ir.model.access.csv。返回 file:line 级别的发现,以及它解析出的 module_dir 和项目根目录(相对路径按会话所在文件夹解析,绝不按进程 cwd 解析)。
odoo_errors 读取最近的 ir.logging 服务器错误 —— 相当于远程拉取环境日志。
odoo_session 铸造一个无密码的 web 会话(connect_as_user 模式),存放在 .sdd/session.json(chmod 600),供 Playwright UI 测试使用。cookie 本身永远不会被返回。
sdd_phase 阶段状态机:init、status(logbook 摘要、spec 目录和 specs 位置)、mark_spec_loaded、advance(fail-closed 门禁:原生审批由宿主询问人类,approval_marker/approval_source 保留为没有审批接口的宿主所记录的兜底路径)、fail(失败阶梯 + FAILED 结论)、succeed(PASSED 结论;在 VERIFY/FIX_LOOP 之外会被拒绝,并且除非 SPEC 声明的每个 AC 在 test-plan.md 中都有一行明确写着 pass —— 被省略的标准不等于通过的标准;结论会记录所用证据的指纹,因此 DONE 会拒绝被编辑改旧的结论)、rollback(恢复本 SPEC 的 checkpoint,并回到该模式实际工作的阶段 —— WRITE_CODE,或功能型 spec 的 APPLY_CONFIG)、diagnose、waive(记录 —— 在原生审批下 —— 本会话可以在没有 spec 的情况下工作:开发者那句"你自己看着办"会原样保存在 .sdd/waiver.json 里,由 status 和 handoff 报告;revoke: true 会删除它,且不需要审批)。
sdd_checkpoint 回滚面:create(对工作区做快照,成为活动 checkpoint)、list、restore(恢复文件,并在 restore_data=true 和 confirm_destructive=true 时恢复已记录的数据变更:撤销会在该变更用过的公司上下文中运行,把读取形态转换成写入值,标记每个操作以免重试时重复补偿它,拒绝来自其他目标的日志,并报告每一个它无法恢复的字段;它总是报告 checkpoint 之后创建的文件,且只有 remove_created=true 才会删除它们)、drop、journal。
odoo_docs 模块文档,可独立使用(不需要 spec、阶段、checkpoint 或实例),因此一个已有模块也能直接被文档化:check(把 OCA 片段映射到 Diátaxis、版本方案、changelog、index.html、docstring、xpath 注释、OWL 指令 → 带 file:line 的 ERROR/WARN)、plan、scaffold(只创建、绝不覆盖的骨架)和 report(持久化 docs-report.md;只有当不存在仍是骨架的片段时才是 APPROVED)。对已发布模块的任何改动都必须写 changelog 条目。
odoo_security_scan 本地静态安全审查(不需要实例):拼接式原生 SQL、eval/exec/pickle、硬编码密钥、没有理由的 sudo()、auth="none"、被关闭的 CSRF、QWeb t-raw。发现项带有 file:line + 修复提示;任何 ERROR 都会阻止 DONE。
odoo_manual 基于证据的叙述式用户手册。canon 从实例读取一个模型的功能 canon(视图、header 按钮以及显示它们的状态、必填字段、notebook 页、动作、菜单、访问规则、用户组、raise UserError/ValidationError 与 _sql_constraints 位置)并连同指纹持久化:手册只能描述 canon 说过的话。plan 把 canon 变成一份分镜(流程、有序步骤、每步一句话):再次运行会合并,已经写好的叙述、截图和散文都会保留。lint 用 canon 校验分镜和渲染后的散文(CANON-STALE、TARGET-UNKNOWN、PLACEHOLDER-UNBOUND、ACTS-MISSING、RECOVERY-MISSING、西语和英语的 AI 腔模式 → 带确切行号的 ERROR/WARN)。explore 返回真实屏幕的 ARIA 快照,因此不会去猜任何 xml id 或标签。capture 驱动浏览器:只读步骤总是执行,而动作步骤(new/fill/select/press/click)需要 allow_actions=true —— 守卫会把它当作一次变更。render 写出手册和一个 Apps 页面(只创建,除非 overwrite=true)。import 把 Playwright codegen 录制转换成一条流程,并拒绝任何携带凭据的录制。手册存放在「设置」指定的位置(manualsMode:specs 与 spec 同目录、docs 在 manualsDir 下、module 作为模块 readme/manual/ 下的 OCA 片段);没有 spec 的独立工作绝不会因此被阻塞。canon 接收访谈——as_user(手册写给谁)、admin、module/modules 与 groups——并把它记录下来:清单随即覆盖该模块拥有的每一个菜单和每一个动作(菜单打开的动作,即使是原生的),每个模型都会带上它的字段(标签、类型、必填、关联/选择、用户组)、表单和列表按钮、statusbar 状态以及按用户组的访问行。plan 为每个菜单开一条流程、为每个用户组的巡览开一条流程,并播种一份 docs 清单:每个字段、每个按钮一格空白。lint 在菜单、动作、字段、按钮、工作流或用户组仍未被覆盖时报告 MODULE-COVERAGE、INTERVIEW-MISSING、FIELD-COVERAGE、BUTTON-COVERAGE、WORKFLOW-COVERAGE 与 GROUP-COVERAGE —— 带理由的 skip 属于已记录的覆盖,而 ROLE-UNVERIFIED 只是警告,因为截图是以已连接用户的身份跑的。render 为每个模型加一张表(类型、必填、关联、用户组,以及你写的那句话)并为每个访问组加一节。只要分镜已存在,odoo_docs 就会在文档结论中加入 ## User manual 一节 —— 没有分镜时则保持不变。
odoo_tests 管道的测试一侧,可独立使用(不需要 spec、阶段、checkpoint 或实例):check 扫描 Python 测试中安装日志看不到的问题 —— tests/__init__.py 从未导入的 tests/*.py(Odoo 只加载被导入的模块,因此该测试从不运行)、不是 Odoo 测试用例的类、没有任何断言或断言恒真的 test_*、被吞掉的异常、time.sleep、显式 skip、测试内的 cr.commit()(破坏每个测试的回滚)、17+ 上的 SavepointCase、没有 start_tour 的 HttpCase,并以断言密度作为参考指标。plan 把 spec 的每个验收标准映射到提到它的测试(按 AC id 或按其需求的 REQ-<AREA>-NN),并列出没有任何测试的标准。scaffold 为每个未覆盖的标准写一个 RED stub(函数体抛出 NotImplementedError,绝不是空洞的通过),只创建且幂等,并追加 import 让文件真正被加载。
sdd_handoff 在运行收尾时写入 specs/<id>/handoff.md(最终阶段、结论、决策、blockers、checkpoint、完整的按 spec 的数据日志、生效配置、下一步)。
odoo_config 读取或更新持久化配置,并回答**"我现在在哪个项目里?"**:解析出的根目录、它的来源(会话 cwd / 已配置 / 进程 cwd)、specs 基础目录、生效的 spec 目录和正在使用的配置文件。
odoo_i18n 使用 Odoo 自身的模型 处理翻译:base.language.export 提供可翻译术语,base.language.install 激活语言。export 写入 <模块>/i18n/<lang>.po(缺失时创建 i18n/ 目录)并与已有文件合并 —— 已写好的译文绝不会被导出的内容替换,新术语会被追加,模块已删除的术语保留为 #~ 而不是删除。status 报告每种语言的覆盖率,check 在仍有条目缺少文本时失败。它不翻译:导出给出消息 ID,文本由人来写。感知版本(Odoo 16 是分界,也正是移除 ir.translation 的版本)。
odoo_import 通过 Odoo 自己的导入器(base_import)准备 CSV/XLS/XLSX 导入,绝不使用本插件自己的解析器:prepare 用 web 会话和它自己的审批上传被授权的文件,preview 报告 Odoo 读到了什么(工作表、表头、有界样本、可导入字段),map 为每一列记录一个决定,plan 把它变成一个 apply 批次 —— 再由 odoo_functional 像其他批次一样批准并执行,所以这个工具自己永远不会应用导入。JSONP 应答会作为数据解析(绝不执行),会话 cookie 永不离开插件,不在已验证版本族内的版本会被拒绝并告知该调查什么。
odoo_functional 功能路径的批次执行器:plan(以 fail-closed 方式校验并存储一个批次)、approve(绑定 spec、设计、计划和批次哈希的原生人工审批)、apply(一次一个操作地执行,在调用前后持久化每个状态,并且只有在回读该操作声明的状态之后才记为已应用 —— 一个返回了应答却没有改变记录状态的调用是失败,不是成功)、inspect(在其自身范围内的只读发现)、status、reconcile(裁决返回为未知的结果)、verify(按验收标准给出证据)和 compensate(从日志构建撤销批次)。声明的环境会为运行设门禁,生产环境还需要一份声明的备份;批次运行期间,所有其他变更路径都会被拒绝。

🎛️ 委派模式

流水线一开始会问要委派多少 —— 每个项目用 odoo_setup mode=autonomy decision=... 记录一次:

模式 谁回答门禁 如何结束
Supervised(默认) 你,在每个受门禁控制的阶段 你批准,或者运行停下来等待
Autonomous 一个人工代理 agent(resources/roles/human-proxy.md),只发出 fail-closed 的行首 APPROVED 或 NEEDS_REVISION create_goal 无人值守地跑一轮轮迭代,直到 DONE 或 BLOCKED

[!TIP] 在自主模式下刹车依然上膛:stop.md、迭代上限和诊断阶梯都还在,而 BLOCKED 是运行唯一会呼叫人的方式。连接授权不受这个开关覆盖 —— 实例仍然需要由人 授权一次。


🧠 上下文工程分层

层 组件
identity resources/roles/*.md —— 架构师、开发者、qa、顾问、human-proxy、security-reviewer、文档等人设,带角色 + 边界。两个内置 skill 都把 resources/ 声明为 resourceBase,因此每条 roles/… 与 references/… 路径都从宿主交给模型的那个基目录解析
odoo_connection odoo-client.ts —— JSON-RPC 认证、execute_kw、会话铸造
executors odoo_module、odoo_execute、odoo_validate、odoo_errors
schemas 带必需章节的分阶段模板;transition() 会拒绝交付物缺少这些章节的阶段
knowledge 按版本固定的 Odoo 模式 skill(委派出去,由 skill 自身校验)
skills SKILL.md —— 5 阶段编排流程,在 apply() 时自动注册到宿主
logbook kb.json —— 决策、被弃用的选项、blockers;提出方案前先读它
audit .sdd/audit.jsonl —— 经过脱敏的追加式工具活动日志,由一个全局的 tools/result 监听器写入(不只是 Odoo 工具)
rollback .sdd/checkpoints/<id>/ —— manifest + 文件快照 + 数据日志,可按 spec 恢复
security odoo_security_scan 规则 + security-reviewer 人设 + CLARIFY 中强制的安全访谈
test tests/smoke.mjs —— 无需实例的不变量套件(状态机、安全、策略守卫、RPC 形态、根/specs 布局、真实 Cordis 宿主契约)+ tests/client.mjs —— 浏览器 bundle 契约和设置面板渲染

🛡️ 安全性、回滚与可追溯性

流水线假设 agent 迟早会出错,所以每一条变更路径都有回退方式,也有证明发生过什么 的方式。

  • 有凭据不等于有授权。 在任何工具向实例打开 socket 之前,必须已经有一个 人批准过那个确切的目标。odoo_setup mode=authorize 通过宿主的原生审批 通道发问,只有 allowed-once 结果才会在 .sdd/grants.json(0600,被 gitignore)里存下回执。回执绑定 url + db + username 的指纹,所以改动其中 任何一个都会让它失效;mode=revoke 会删除它。没有有效回执时,根本不会发出 任何客户端,因此一个配置好的 .env 不可能被悄悄使用。在 AUTONOMOUS 模式下 没有回答者,所以运行会报告 NOT AUTHORIZED 并挂起 —— 这正是重点。

  • 模型无法放松自己的策略。 修改白名单或策略守卫(odoo_config mode=set) 以及切换委派模式(odoo_setup mode=autonomy),每一项都需要原生审批。

  • 没有 spec,就没有变更 —— 除非你显式 waive。 无论请求是什么,哪怕它就是 同一个对话里的"顺便把 X 也改了",一次变更都需要处于写作阶段的 spec (WRITE_CODE、VERIFY、FIX_LOOP、APPLY_CONFIG),或者一个你为本次 会话批准的 waiver。这道门禁覆盖两个面:实例变更(odoo_execute、 odoo_module、odoo_import prepare)以及通过宿主 write/edit 工具做的源码 编辑。写 spec 文档本身(specs/、.sdd/)永远不会被挡 —— 否则那条出路也会被 挡住。小改动就是小 spec:mode=bug、一条验收标准、没有设计访谈。如果你不想 为它写 spec,也可以用 sdd_phase operation=waive detail="...":它会把你的话记录下来、需要你的审批,并且只覆盖 本次会话:下一次对话会重新回到这条策略之下,而在它之下做的每一项变更仍然会 作为决策记入 KB。requireSpecForChanges: false 是人的关闭开关。 诚实的漏洞: 守卫看到的是宿主工具调用,所以通过 bash 做的变更 (heredoc、sed -i)不受门禁 —— 流水线是路径,不是监狱。

    在哪里询问(specPolicyScope,默认 "odoo")。要求 spec 只在有 Odoo 工作的 地方才有意义,而守卫过去只检查一件事:路径是否落在会话根目录内。因此按配置文件级别 安装的插件会封锁与 Odoo 毫无关系的文件夹里的写入。使用 "odoo" 时,文件编辑只在 目录显示 Odoo 工作迹象时才受门禁:

    • .sdd/ 中有真正的插件状态(config、specs、checkpoints、waiver)—— 不是只含 audit.jsonl 的 .sdd/,审计监听器会为每一次工具调用写入它,因此它只意味着 "这里跑过东西",而不是"这是 Odoo 项目";
    • 根目录有 __manifest__.py —— 该目录就是一个模块;
    • 下一层有 __manifest__.py —— 该目录包含模块;
    • 本会话中已对该根目录使用过 odoo_* 工具。

    实例变更始终受门禁:它本身就是 odoo_* 调用,也就是证据本身。stop.md 仍然 停止一切,无论是否已采用,因为它是紧急停止而非范围问题。"everywhere" 恢复旧的 全面行为,而拒绝信息现在会指出触发策略的信号,不再让你猜测。

    三条出路,三条都可用。(1)为本次变更开一个小的 spec,(2)继续已覆盖它的 spec,(3)请开发者豁免 —— sdd_phase operation=waive detail="..."。第三条过去要求 spec_id,这使它在唯一需要它的场景中不可用:没有 spec 的目录,你必须先创建一个 spec 才能说你不想要 spec。豁免是按项目和会话的,现在 spec_id 对它(以及 waive revoke=true)是可选的,而其他所有操作仍然要求它。

  • 变更前先 checkpoint。 在 requireCheckpointBeforeMutation 开启时(默认), odoo_execute 的变更会被拒绝,直到 sdd_checkpoint create 已对活动 spec 做过 快照 —— 并且在 WRITE_CODE 之前直接被拒绝。快照会跳过符号链接(lstat), 也绝不复制 .env 或密钥材料。

  • fail-closed 守卫。 内部守卫失败时会带一个可见的理由拒绝,而不是放行调用。

  • 文件回滚。 sdd_checkpoint restore 把快照中的文件逐字节放回; sdd_phase rollback 把 spec 退回 WRITE_CODE 并记录失败,让循环从一个已知 状态重新开始。checkpoint 快照的是项目树,所以在 central specs 布局下, spec 文档(它们位于项目之外)故意不属于快照的一部分:spec 是不可变的真相 来源,不是要回滚的代码。

  • 数据回滚(尽力而为,并且对此诚实)。 每一次通过 odoo_execute 的 create/write/unlink 都会把它的前像记录进 checkpoint 日志,并盖上数据库 以及它所应用到的目标(url+db+user)的戳;restore restore_data=true confirm_destructive=true 会反向重放,并拒绝来自其他目标的日志。重放在该 变更用过的公司上下文中运行,把读取形态转换成写入值(many2one、x2many),在 补偿每个操作时标记它,使重试不会重复其中任何一个,并报告它无法恢复的字段 (二进制内容、只读或非存储字段)。被重新创建的记录会拿到新的 id —— 报告会说明这一点。它覆盖的是通过插件写入的数据 —— 不包括模块安装/升级的 副作用,那些不会在数据库层面被还原。

  • restore 会报告漂移。 restore 总会列出 checkpoint 之后创建的文件, 所以不会有东西被悄悄留下;remove_created=true 会删除它们(仅在快照的根之内) 以与快照完全一致。

  • 文档是门禁,不是脚注。 ARCHITECTURE 把决定记录在 ## Documentation, WRITE_CODE 产出 OCA 片段 + Apps 的 index.html + 强制的 changelog 条目, 而 DONE 由 documentationPolicy 把关(默认 required;另有 optional 和 off)。插件做不到的事,它会直说:gen-odoo-readme、towncrier、Ruff 和 pylint 需要 shell,所以片段才是真相来源,编译 README.rst 仍然是你的步骤。

  • 生命周期:插件拥有自己的状态,也能把它交还。 odoo_setup mode=purge 会打印一份计划(它拥有什么、它有意保留什么),并且只有在 confirm_destructive=true 加原生人工批准之后才删除。它绝不碰 .env (你的凭据)、stop.md(你的刹车)或 specs/(你的文档)。

  • 持久化状态。 流水线状态、KB、结论、授权和日志都用原子替换写入,损坏的 文件会被隔离在原文件旁边而不是被覆盖:sdd_phase status 会报告这次恢复。

  • 可追溯性。 .sdd/audit.jsonl 记录每一次工具调用及其结果 (ok / error / denied)、耗时和阶段;sdd_phase status 打印 logbook; sdd_handoff 把整次运行冻结进 handoff.md。

  • 紧急刹车。 stop.md(位于 .sdd/stop.md 或 specs/<active>/stop.md) 会中止所有工具;迭代上限和诊断阶梯会把运行导向 BLOCKED,而不是无限循环。


🔐 安全与网络策略

  • 不回传任何数据。 插件唯一的外发网络调用发往你写在 .env 里的实例 URL。没有遥测,没有更新检查,没有第三方端点。
  • 传输守卫(fail-closed)。 http:// 只对回环主机(localhost、127.x、 ::1、*.localhost)接受;其他任何目标都必须是 https://,否则凭据加载 会被拒绝 —— 对远程主机用明文 http 会把 API key 明文发出去。
  • 密钥隔离。 密钥只由 credentials.ts 读取一次,并且只注入到 RPC 参数里。 每一份工具输出在展示给模型或持久化进 KB 之前,都会经过两层脱敏(已知密钥 + 通用的 password= / Bearer / api_key= / session_id 形态)以及家目录路径 掩码(/home/user/… → ~/…)。
  • 会话 cookie 永远不会到达模型。 odoo_session 把 cookie 写入 .sdd/session.json(chmod 600),只返回路径。截图引擎甚至不写 cookie:它在内存中 认证 web 会话,并把 cookie 留在浏览器上下文里。
  • 截图浏览器被锁定在实例源上。 任何离开实例基础 URL 的请求都会被中止,因此试图访问 第三方主机的页面会失败而不是加载。截图默认只读:只做观察的步骤(goto、menu、 wait、assert、explore、screenshot、confirm)总是执行;会动手的步骤 (new、fill、select、press、click)需要 allow_actions=true 和 confirm_destructive=true,守卫会把这次调用归类为一次变更(spec 处于写作阶段,或者 有 waiver)—— 而一次点击没有可补偿的条目:与 CRUD 调用不同,插件无法撤销 UI 做过 的事。请针对可丢弃的数据库做截图。
  • 录制只作为数据读取,凭据会被拒绝。 odoo_manual operation=import 解析 codegen 文本;携带密码字段、Authorization 头或 cookie 的录制会按行被拒绝,而且那个值绝不会 被回显出来。
  • 在 git 还能看到它时,凭据文件不会被写入(fail-closed)。 在脚手架出一个 .env 之前,插件会去问 git 自己 —— 忽略规则问 git check-ignore,索引问 git ls-files —— 当受保护的路径不在 .gitignore 里或已经被跟踪时,就以 status: needs-gitignore 拒绝,在后一种情况下它给出的唯一修法是 git rm --cached。忽略规则永远保护不了已经 进入索引的文件,而这个缺口正是一个带着有效密码的 .sdd/.env 曾经到达远程分支的原因。 在 git 缺席的地方,回退方案会读取 .gitignore 文本并说明这一点,而不是声称已证明。 可用 requireGitignoreBeforeCredentials: false 按项目关掉。
  • 构建之外没有安装脚本。 唯一的生命周期脚本是 prepare/prepack,它们把 src/ 编译成随包发布的 lib/,别的什么都不做 —— 没有网络,没有 postinstall,没有 shell。两者的失败策略故意相反:prepare(在安装时运行) 从不弄挂一次安装 —— 没有 devDependencies 或没有可选宿主 peers 时,它会说明 自己无法检查什么,并在不做类型检查的情况下产出,好让插件仍能加载 —— 而 prepack(在发布时运行)拒绝打包一个它无法通过类型检查的构建。prepack 同时保证已发布的 tarball 绝不会缺少它 main 所承诺的入口点(那个失败真实 发生过:一个干净的 clone 打包了 39 个文件,其中 lib/ 下零个;它的续集也 是真的:一个在 npm install 期间做类型检查的 prepare,在安装那些 peers 的 步骤能运行之前就让每个 CI job 都失败了)。
  • 宿主要求声明了两次,遵循 dsh-market 的发现约定:engines.dsh 以及在 @deepseek-ai/{cordis,dsh-tools,schemastery} 上 lockstep 的可选 peer 范围。 在没有 tools 服务的宿主上,插件会带一个显式错误拒绝挂载,而不是带着故障启动。

🖥️ 平台支持

DSH 能跑的地方插件就能跑,并声称支持 Linux、macOS 和 Windows —— 这个声明 由 CI 矩阵来检验而不是断言(ubuntu-latest 和 windows-latest,Node 20 和 22)。

关注点 行为
路径 module_dir 和显式根目录同时接受 POSIX(/opt/odoo)和 Windows(C:\odoo、UNC)写法;绝对路径绝不会被拼接到项目根目录之下。
.env 权限 会请求仅属主可读(0600),并在 chmod 之后重新检查:在无法表达权限位的文件系统上(Windows、FAT/exFAT、某些挂载),插件会说"已请求仅属主模式"并追加一条说明,而不是假装文件是私有的。在真正的 POSIX 文件系统上,无法收紧的宽松权限仍然会被拒绝。
原子写入 状态文件先写到同级的临时文件再改名就位,对 EPERM/EACCES/EBUSY 做有界退避重试 —— 这正是 Windows 因为编辑器、索引器或杀毒软件持有打开句柄而拒绝改名的情况。
项目根目录 每次调用都从会话所在文件夹解析;进程 cwd 只是最后手段,并会被报告为 LAST RESORT。
符号链接 checkpoint 绝不跟随符号链接离开项目树;在操作系统或用户权限不允许创建符号链接的地方,测试套件会跳过它的符号链接断言 —— 并且说明这一点,而不是悄悄通过。

📁 spec 存放在哪里

项目根目录就是当前会话中打开的文件夹,所以它不是插件级的设置。spec 文档跟着它走:

布局 路径 何时选它
project(默认) <projectRoot>/<specsDir>/<specId> spec 应该随代码一起走
central <specsRoot>/<projectSlug>/<specId> 你维护很多模块仓库,想要一个可搜索的统一位置

在 central 布局下,每个项目都有自己的子文件夹,带一个 .dsh-project-root 标记; 如果两个项目共用同一个目录名,还会加一个哈希后缀 —— 外来的文件夹绝不会被收养。 .sdd/ 始终留在项目里。

<projectRoot>/
├── .sdd/                       # 插件自有,已 gitignore
│   ├── .env                    # 凭据(chmod 600)
│   ├── config.json             # 项目配置
│   ├── grants.json             # 人工授权回执
│   ├── instances/<name>.env    # 每个被命名目标的凭据(chmod 600);一个是激活的
│   ├── session.json            # Playwright cookie
│   ├── audit.jsonl             # 每一次工具调用,已脱敏
│   ├── setup-state.json        # 上手引导 + 委派决定
│   ├── active/<sessionId>.json # 本会话的指针(spec、阶段、checkpoint)
│   ├── active.json             # 项目最近使用的 spec(参考用;见 adoptProjectPointerHint)
│   └── checkpoints/<id>/       # manifest + 文件快照 + 数据日志
└── specs/<NNN>-<slug>/         # 或者 central 文件夹

spec 指针属于「会话」,不属于项目。 每个对话写入自己的 .sdd/active/<sessionId>.json,因此同一个项目上打开的两个会话不再互相覆盖, 也无法用对方的 spec 来授权改动。.sdd/active.json 仅保留为项目最近使用的 spec:没有自己指针的会话,在它足够新(< 12 小时)且该 spec 仍然存在时,会把它 当作提示采用。若要严格范围 —— 每个会话在声明之前都没有 spec (sdd_phase status spec_id=…)—— 请在面板或 odoo_config 中设置 adoptProjectPointerHint: false。checkpoint 的快照仍属于项目;而哪个是 你的回滚目标是每个会话各自的,删除时会清除所有指向它的指针引用。

[!TIP] 同时打开多个项目时,读一读每个工具结果都带的那行 Project root: … [provenance],或者问 odoo_config mode=read —— 它会返回 解析出的根目录、它的来源和生效的 spec 目录。


⚙️ 配置

在 Web UI 中打开 Settings → Odoo SDD。一切都可以在那里编辑,另外在你需要的 地方还提供了几个可复制粘贴的预设。

**宿主要求:**设置面板需要 dsh >= 0.2.0-rc.1。该版本移除了面板原先绑定的 settingsScope 服务,改为根据插件自身的 Config schema 生成表单(条目 odoo-sdd)。 在更旧的宿主上该分区不会出现,插件会在控制台说明原因;工具与流水线照常工作, 可用 odoo_config mode=read|set 在对话中编辑同样的值。

# ~/.dsh/profiles/<profile>/cordis.patch.yml(可选:同样的字段,作为补丁)
- insert:
    - id: odoo-sdd
      config:
        specsMode: project      # project | central
        specsRoot: ''           # specsMode=central 时的绝对文件夹
        specsDir: specs         # specsMode=project 时项目内的文件夹
        executeAllowlist: []    # odoo_execute 可以 create/write/unlink 的模型
        methodAllowlist: []     # 功能批次可调用的 "model.method" 对(任意模型)
                                # 即席 RPC 不需要列表:任意公共方法 + confirm_destructive
        communityRepoUrl: https://github.com/odoo/odoo
        enterpriseRepoUrl: https://github.com/odoo/enterprise
        communityRepoPath: ''   # 本地 OS 路径;设置后会**覆盖** communityRepoUrl
        enterpriseRepoPath: ''  # 企业版同理
        odooApi: auto           # auto | json2 | jsonrpc(auto 在 401 时回退)
        autonomy: supervised    # supervised | autonomous
        licensed: community     # community | enterprise(OCA 总会被搜索)
        requireCheckpointBeforeMutation: true
        requireSpecForChanges: true     # 每一项变更都需要处于写作阶段的 spec(或 waiver)
        specPolicyScope: odoo           # odoo(仅在检测到 Odoo 工作时)| everywhere
        securityReviewRequired: true
        securityInterviewRequired: true
        auditAllTools: true
        maxCheckpoints: 5
        documentationPolicy: required   # required | optional | off
        documentationLanguage: ''       # 留空 = 英文,除非项目另有规定
        manualsMode: specs              # specs(spec 同目录)| docs(在 manualsDir 下)| module(readme/manual/)
        manualsDir: docs/manuals        # 仅当 manualsMode 为 docs 时使用

[!IMPORTANT] 有两个配置存储,更具体的那一个获胜:Settings(用户级, ~/.dsh/settings.yaml)和项目的 .sdd/config.json(由 odoo_config mode=set 按项目写入)。如果某个键在面板里改过之后看起来被忽略 了,那就是项目文件把它钉住了 —— odoo_config mode=read 会报告生效值。


🤖 模型体验

agent 看到 18 个自包含描述的工具。典型流程: sdd_phase init → 安全访谈 + odoo_connect → 带 APPROVED 的门禁阶段 → sdd_checkpoint create → 写代码 → odoo_security_scan → odoo_module install → 出现 traceback 时用 odoo_errors + sdd_phase fail (它可能强制一次诊断)→ 修复(或 sdd_phase rollback)→ 重新验证 → sdd_phase succeed → sdd_handoff → DONE。工具响应是可执行的文本:服务器 traceback、门禁拒绝理由和补救说明。

工作流 skill 在挂载时注册,所以它的名称和描述会自动出现在每个会话的 skill 目录中。完整指令在模型选中它时加载(目录里只有摘要),或者在你输入 /odoo-sdd-workflow 时加载。


⚠️ 已知限制与待办工作

  • 远程测试 —— 没有实例的 shell 访问就无法运行 --test-enable;第二层验证是 RPC/UI 测试。待办:如果你暴露测试运行器,就提供一个可选的 odoo_run_tests 工具。
  • 数据回滚是尽力而为 —— 测试会写入所连接的数据库,而且没有临时克隆 (这是设计决定:目标由你提供并归你所有)。sdd_checkpoint 能撤销通过 odoo_execute 写入的数据,但模块安装/升级不会在数据库层面被还原。请使用 可丢弃的数据库。
  • 静态安全扫描的范围 —— odoo_security_scan 是基于规则的源码文本检查 (没有 AST,没有污点追踪),所以它能抓住常见的 Odoo 错误,但不是全部;它补充 人工审查,绝不替代人工审查。
  • 变更策略管的是工具调用,不是 shell —— write/edit 和 Odoo 工具会受门禁 约束;而通过 bash 写入的文件(heredoc、sed -i)不会,因为宿主不会事后检查 一条 shell 命令写了什么。这条策略让流水线成为阻力最小的路径、让例外保持可见; 它不是沙箱。
  • 多实例 —— 每个项目一个目标(.env)。待办:具名实例配置 (dev、staging)。
  • 面板中仅作信息的字段 —— autonomy 和 securityInterviewRequired 会被 存储和报告,但流水线从 .sdd/setup-state.json 读取委派决定(用 odoo_setup mode=autonomy 设置),并通过 ARCHITECTURE 的内容门禁强制执行安全 访谈。已记为待办工作。
  • 手动截图需要浏览器和密码 —— playwright-core 是可选依赖,浏览器本身并不随包发布: 没有 Chromium、Chrome 或 Edge 时,截图会返回 NEEDS_BROWSER 以及那行安装命令。web 会话 还需要账号密码(Odoo 在交互式登录中拒绝 API key),所以一个只有 API key 的项目仍然 能编写和 lint 一份手册,但无法自动驱动截图。
  • 手册一次只覆盖一个模型 —— canon 是按模型读取的,所以一次跨模型的走查就是针对多份 canon 规划出的多条流程,而分镜会保留它规划时所用 canon 的指纹(实例变动时 CANON-STALE 会警告)。
  • UI 家族 15、16、18、19 是声明,不是已验证 —— 截图选择器在 17 上做过端到端验证; 其他家族共享同样的结构,但在你依赖它们之前必须对着一个 dev 实例检查,而且 familyFor 会拒绝一个它不认识的 major,而不是去猜。
  • 散文 lint 是建议性的 —— AI 腔规则是启发式的,在 v1 里每一条都被设计成 WARN:只有 结构性规则(每个被截图的步骤一句话,过程内不出现标题)和 canon 规则能够阻断。
  • 没有富 UI 渲染器 —— 工具输出就是 DSH web GUI 里的文本。

❓ 故障排查

现象 含义 怎么做
NOT CONFIGURED 级联中没有可用的 .env odoo_setup mode=interactive
NEEDS_SECRET 脚手架存在,但 ODOO_PASSWORD 为空 在文件里填写,绝不在聊天里填
NOT AUTHORIZED 凭据存在,但没有针对该目标的有效人工授权 odoo_setup mode=authorize
Instance unreachable 版本探测失败 检查 URL/端口,以及实例是否在运行
变更总是被拒绝 没有 checkpoint,或者 spec 还没到 WRITE_CODE 批准门禁,然后 sdd_checkpoint create
一切都停了 stop.md 存在 读它,然后删除它
面板改动似乎被忽略 项目的 .sdd/config.json 优先级高于全局 Settings 层 odoo_config mode=read 显示生效值
某个 spec 目录"找不到" 你在另一个项目文件夹里 在会话中打开那个项目的文件夹
odoo_manual capture 返回 NEEDS_BROWSER 找不到 Chromium/Chrome/Edge npx playwright-core install chromium,或者传 browser: "chrome"
odoo_manual capture 返回 NEEDS_WEB_SESSION 目标只有 API key,而 Odoo 在交互式登录中拒绝它 在 .env 里放一个账号密码,或者用 odoo_session 创建 .sdd/session.json
截下来的屏幕语言不对 浏览器跟随登录用户的语言,而插件从不写 res.users 用一个语言与手册一致的用户来截图
allow_actions=true 被拒绝 守卫把它归类为一次变更 把 spec 推进到写作阶段,或者登记一份 waiver —— 并使用可丢弃的数据库
截图进行到一半就停了 第一个失败的步骤会中止运行并写下它已经拿到的内容 修好分镜,然后用 from_step: "<失败步骤的 id>" 重跑
odoo_docs 在没有片段缺失时仍报 NEEDS_CONTENT 一条手册 ERROR(截图缺少对应图片、canon 已过期),或者有骨架还留在原地 读报告里的 ## User manual 一节

🧩 实现细节

插件形态

遵循 DSH 工具插件约定(dsh-tool-todo、dsh-tool-goal):具名导出 name、 inject、Config(schemastery schema)和 apply(ctx, config),并用 @deepseek-ai/dsh-tools 的 defineTool 注册每个工具。浏览器那一半是一个普通的 JS ModuleLoader bundle,它贡献 Odoo SDD 设置区块。

源码地图

文件 作用
src/index.ts 插件入口:注册 18 个工具、解析配置和策略守卫
src/types.ts 公开的 payload 类型(绝不包含密钥材料)
src/credentials.ts 凭据级联、.env 加载/校验、权限验证、redact()、fail-closed
src/odoo-client.ts JSON-RPC 客户端:common.version、authenticate、execute_kw、button_immediate_*、ir.logging、/web/session/authenticate
src/tools-runtime.ts 面向 Odoo 的工具主体:odoo_execute(白名单 + 前像捕获)、odoo_validate、odoo_module、odoo_errors
src/sdd-state.ts 阶段状态机、门禁、追加式 KB、结论、安全内容门禁、stop.md
src/checkpoints.ts checkpoint 存储:manifest、文件快照/恢复、数据日志、清理预算
src/security-scan.ts 无需实例的静态安全规则(scanModule),发现项带 file:line
src/audit.ts 已脱敏的追加式审计日志(.sdd/audit.jsonl)和 withAudit 包装器
src/setup-state.ts 上手引导决定 + 委派模式的持久化(.sdd/setup-state.json)
src/grants.ts 人工授权回执(.sdd/grants.json),绑定指纹且 fail-closed
src/atomic.ts 原子写入(对 EPERM/EACCES/EBUSY 做有界重试)以及损坏隔离 + 恢复报告
src/paths.ts 跨平台的 module_dir 解析
src/specs-location.ts spec 存放位置:project 与 central 布局、带来源的会话根解析、slug/标记/冲突处理
src/lifecycle.ts 归属清单和 purge 原语(只处理自有状态;绝不碰 .env/stop.md/specs/)
src/docs-scan.ts 文档规则:OCA 片段 + Diátaxis、版本方案、changelog、index.html、docstring、xpath、OWL
src/docs-tool.ts odoo_docs 工具(check/plan/scaffold/report),无需流水线即可使用
src/test-scan.ts 模块 Python 测试的静态规则(未被导入的测试模块、用例类、断言密度、被吞掉的错误、cr.commit、sleep、跳过、按版本区分的 SavepointCase)
src/tests-tool.ts odoo_tests 工具(check/plan/scaffold),无需流水线即可使用
src/manual-model.ts 手册工作区解析(3 种模式,带包含性检查)、带稳定 id 的分镜读写、从 canon 出计划、canon 规则校验、步骤安全分级
src/manual-canon.ts 功能 canon:XML arch 分词器(17 的 invisible= 表达式、14–16 的 attrs/states、statusbar 状态)、模块错误扫描、按版本区分的读取清单(≤15 用 load_views,16+ 用 get_views,19 中 groups_id→group_ids)以及一枚稳定的指纹
src/manual-tool.ts odoo_manual 工具:canon / plan / lint / render / explore / capture / import,带只创建策略和失败后恢复的路径
src/prose-lint.ts 西语和英语的建议性散文规则(AI 腔模式、句子长度、可读性)以及 OCA 格式所依赖的两条结构性 ERROR 规则
src/manual-render.ts 确定性的 Markdown + Apps 页面渲染、逐文件 sha256,以及一个绝不会被覆盖(除非 overwrite=true)的手工编辑文件
src/manual-capture.ts 浏览器引擎:源锁定、只读优先的步骤门禁、带标注的 PNG(1440×900,标注层已移除)、ARIA 快照、capture.json
src/manual-import.ts Playwright codegen 方言读取器:canon 标签映射、以 WARN 代替猜测的通用步骤、按行拒绝凭据
src/manual-audit.ts 只读审计,让文档结论能覆盖一份已有手册(截图覆盖率、canon 新旧、散文健康度),没有手册时不改动任何东西
src/project-conventions.ts 从项目自己的规则解析文档语言,默认英文

安全决策

  • 密钥只存在于 credentials.ts 内部和 RPC 调用参数中;每一份输出都经过 redact()(包括 user:pass@ 形式的 URL)。
  • 传输守卫在认证之前就拒绝既不是 HTTPS 也不是回环的任何目标。
  • 变更是双重 fail-closed:白名单在每次调用时实时读取,而策略守卫会拒绝 create/write/unlink,除非 checkpoint 存在且 spec 处于 WRITE_CODE 或更后的阶段。
  • 密钥永远不会作为工具参数被接受,永远不会写入审计日志,也永远不会通过聊天 索要。
  • 状态损坏 ⇒ 重启(进度绝不被伪造);门禁含糊 ⇒ 拒绝;缺少验证 ⇒ 到不了 DONE;安全缺口 ⇒ ARCHITECTURE 门禁被拒绝。

构建与测试

npm run typecheck   # tsc --noEmit
npm run build       # 生成 lib/(必需:包的 main 是 lib/index.js)
npm test            # 服务器不变量 + Cordis 宿主契约 + 客户端 bundle + README + functional/import
npm run test:package  # tarball 内容(运行时加载但 `files` 漏掉的文件)

这些正是 CI 在 Linux 和 Windows 上运行的步骤, 所以在本机跑 npm run typecheck && npm test 就能复现这条流水线。

test:package 还会模拟发布:它复制一份不含 lib/ 的包(也就是全新 clone 的样子),对它运行 npm pack,并断言 tarball 里仍然含有编译后的入口点。正是 这项检查让一个已发布的版本不会变成"装得上却加载不了"。接着它模拟安装那一侧 —— 一棵有编译器但没有可选宿主 peers 的树,而这正是 CI 的 npm install —— 并 断言在那里 prepare 以 0 退出,而 prepack 会拒绝。

发布(维护者)

npm 不跟随 GitHub。 它们是两个独立的 registry:一次 push、一个 tag 或一个 GitHub Release 更新的是 GitHub,别的什么都没有,而 npm publish 更新的是 npm, 别的什么都没有。已发布的版本是不可变的 —— 它不能被覆盖,只能被超越 —— 所以每次发布都要提升 package.json 的版本号。

lib/ 是构建产物且被 gitignore,所以 tarball 由 prepack 钩子构建 —— 绝不手工 构建,绝不来自陈旧的树。第一次发布是手动的(npm 只允许为一个已经存在的包注册 trusted publisher):

npm login                 # 只需一次;之后 `npm whoami` 应该有回应
npm publish               # prepack 运行 `tsc` 并打包产物
npm view dsh-odoo-sdd version   # 核实 registry 上实际是什么版本

之后, .github/workflows/publish.yml 会接手: 发布一个 GitHub Release(或手动 workflow_dispatch)会运行与 CI 相同的门禁 —— typecheck、build、测试、发布模拟 —— 检查 release tag 与 package.json 一致, 拒绝一个已经在 registry 上的版本,并通过 npm 的 OIDC trusted publishing 以 provenance 发布,所以这个仓库里不存在任何 NPM_TOKEN。

那套自动化的一次性配置:在 npmjs.com → 该包 → Settings → Trusted publishers → Add → provider 选 GitHub Actions,owner 填 fhidalgodev,repository 填 dsh-odoo-sdd,workflow 文件名填 publish.yml, environment 留空(不一致会得到 403 npm-trusted-publisher-not-configured)。 更想用 token?创建一个带 bypass 2FA 的 granular access token,并把它存成 NPM_TOKEN secret —— workflow 里写了存在哪里。

所以一次发布就是:提升版本号 → merge → 发布 Release,最终 tag、Release 和 npm 上的版本三者一致。

同样的钩子也让从仓库安装可用:prepare 在 TypeScript 存在时编译源码 (npm i github:fhidalgodev/dsh-odoo-sdd),不存在时带一条提示跳过 (file: 安装没有 devDependencies)。


⭐ Star History

图表由 star-history.com API 实时生成。

🙏 致谢

这个插件为 Odoo 开发者社区而构建,它立足于三个生态:

  • Odoo Community Association (OCA) —— 这条流水线 所执行的编码规范、模块布局和质量门禁。
  • DeepSeek Harness (DSH) —— 这个插件所 运行的插件架构(Cordis tools/plugins、skills、subagents)。
  • Playwright —— odoo_manual operation=capture 背后的浏览器引擎,以及导入所读取的 codegen 录制。

手册写作指南取材于已出版的文档实践,而不是另立一套自家风格:Diátaxis (OCA readme 已经在用的片段象限)、Information Mapping 和 John Carroll 的 极简主义用于面向任务的过程,ISO/IEC 26514 用于用户文档的结构,开篇一幕则 采用 StoryBrand / "lead with insight" 这一派叙事框架。反 AI 腔的散文清单参考了 公开的 storytelling 与 content skill 合集(尤其是 artemnovitckii/content-skills、 yaxeen/storytelling-skills、 Donchitos/team-narrative、 fabric 和 lyndonkl)—— 留下来的规则是那些能抓住一个真实 小毛病、又不惩罚一句清楚句子的规则。

感谢每一位贡献模式、评审和想法来塑造 SDD 工作流的人。贡献者:


📜 许可证

MIT © Franyer Hidalgo

内容来自项目 README(GitHub)↗

链接

同类插件

查看整个分类 →

社区评论

评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。