让智能体把文档发成博客:inair_website_blog 的设计与实现
这篇文章本身,就是用本文介绍的那套机制发布的——智能体写好 Markdown,调用
multica blog publish,落到「智能体技术文档」博客里成为一篇草稿,等人工修订后上线。可以说是一次「吃自己的狗粮」。
背景:智能体写完文档之后呢?
智能体很擅长写技术文档:给它一个主题或一段散乱的笔记,它能整理成结构清晰的 Markdown。但写完之后,文档通常躺在 issue 评论或工作目录里,要发到网站上还得人工复制粘贴、排版、配图、选栏目。
我们想把这一段也自动化:当用户对智能体说「把这份笔记写成一篇技术文档发到博客」时,智能体能自己把内容发到网站博客上——但又不能放任它直接公开上线。
Inair 平台底层是 Odoo 19(自带 website_blog 博客模块),智能体跑在 Multica daemon 里。于是问题变成:怎么让 daemon 里的智能体,安全、零配置地把一篇文章写进 Odoo 的博客里,并且默认是草稿、由人把关?
三个决定方案的关键发现
动手前先摸了摸底,有三个现成能力可以直接复用,省掉大量造轮子:
-
零配置认证已经存在。 Multica daemon 派发任务时,会把一对凭据(
X-GALAXY-ACCESS-TOKEN/X-GALAXY-API-KEY)注入智能体进程的环境里。这对凭据在 Odoo 侧走inair_open_api的auth='openapi'鉴权——和智能体现在调 issue / comment 接口用的是同一套。只要新博客接口也声明auth='openapi',智能体自带凭据就能直接调用,不必为它单独申请 token。 -
Markdown 转 HTML 已经有工具。
inair_multica.utils里有个md_to_html(text),智能体现在写 issue 描述时就是用它把 Markdown 转成 HTML 再入库的。博客正文完全可以复用——智能体传 Markdown,服务端转 HTML。 -
CLI 该长在谁身上? 一开始设想是随 Odoo 模块发布一个 Python 脚本。但 Multica daemon 本身就是个 Go 程序,已经有成熟的
multica命令行(issue、comment、repo 都在里头),能复用 daemon 已配置好的 server_url 和 token。于是决定:做成multica blog这个 Go 子命令,更贴近 daemon 的原生形态,智能体也不用再多装一个 Python 工具。
四个产品决策
在写代码前,先和需求方敲定了四件事,它们贯穿了整个实现:
- CLI 用
multica blog(Go 原生),而不是随模块的 Python 脚本。 - 默认草稿,不直接发布。 创建文章时
is_published=false,只有显式--publish才上线。 - 所有文章默认发到一个「智能体专用博客」(一个叫「智能体技术文档」的栏目),它是智能体的草稿区;人工在这里修订、审核,再决定是否发布到正式栏目。
- 给智能体 Odoo 用户授予博客写权限——但不是直接塞管理员,而是在新模块里建一个专门的「智能体博客作者」组。
三层架构
整个特性分三层,从下到上:
内置 Skill(教智能体「何时、如何」调用)
│ multica blog publish …
multica blog CLI(Go 子命令,零配置带凭据)
│ HTTP + X-GALAXY 头
inair_website_blog API(Odoo,auth='openapi')
│ blog.post.create,md_to_html
Odoo website_blog(博客数据)
第一层:inair_website_blog HTTP API
这是一个新的 Odoo 模块(inair-addons 下的 submodule),从 website_blog 扩展,依赖 inair_common 和 inair_multica。核心是一个 controller controllers/blog_api.py,暴露四个端点,全部 auth='openapi'、返回 JSON:
GET /inair_website_blog/blogs— 列出可用博客POST /inair_website_blog/posts— 创建文章(默认草稿)GET /inair_website_blog/posts/<id>— 查询单篇PATCH /inair_website_blog/posts/<id>— 更新标题/正文/标签/发布状态
创建文章时,目标博客的解析有一套回退顺序:请求里指定了 blog_id 就用它 → 否则看系统设置里的默认博客 → 再否则用预置的「智能体技术文档」博客 → 最后兜底取第一条。这样智能体绝大多数情况都不用传 --blog。
正文按 content_format 处理,默认 markdown,服务端用 md_to_html 转成 HTML 再写库;标签按名字解析,不存在就自动创建。写库以调用方用户身份执行,权限自然受 Odoo ACL 约束。
第二层:multica blog CLI
一个 Go 写的 cobra 子命令(server/cmd/multica/cmd_blog.go),注册到 multica 根命令下:
multica blog list # 列出博客
multica blog publish --title "标题" --file doc.md # 创建草稿(省略 --file 则读 stdin)
multica blog get 42 # 查看一篇文章
multica blog update 42 --publish # 把草稿发布
它复用了 daemon 共享的 APIClient,在 Odoo 后端模式下自动带上那对 X-GALAXY 头,所以智能体调用时是零配置。默认 is_published=false、content_format="markdown",跟产品决策一致。
这里有个小细节值得记一笔:APIClient 内部会把 /api 开头的路径做一次前缀重写,但博客接口是 /inair_website_blog/...,不在 /api 下。好在重写逻辑只匹配 /api 前缀,非 /api 路径会原样放行,直达 Odoo controller——所以不用为博客接口改任何路由代码。
第三层:内置 Skill
光有 CLI 不够,智能体还得知道什么时候该用它。于是在 multica 的内置 skill 目录里加了一个 multica-publishing-blog:一份 SKILL.md 加一份逐行溯源的 references/...source-map.md,告诉智能体:
- 何时用:用户要求「把某内容写成技术文档/发到博客」时;
- 怎么用:先把文档写成 Markdown 存到文件 →
multica blog publish --title … --file …→ 从输出里读URL:那行 → 把 URL 回贴给用户,并说明这是草稿、待人发布; - 契约:默认草稿(不
--publish就不上线)、写入智能体专用博客、正文走 Markdown→HTML、零配置认证; - 失败怎么办:
401说明运行环境没注入凭据(不是智能体能修的,报告即可);403说明智能体用户没进作者组或者模块没装(也报告给人工,不要自己尝试授权)。
内置 skill 由 //go:embed builtin_skills 自动遍历发现,所以加一个目录就行,不用动任何 Go 注册代码。
踩过的两个坑
坑一:这个 Odoo fork 的 res.groups 没有 category_id
装模块的时候直接报了 ParseError,指向安全权限文件里那条 res.groups 记录。排查发现:Inair 的 Odoo 19 fork 把「分组归类」从 res.groups.category_id 挪到了一个新的模型 res.groups.privilege(配一个 privilege_id 指过去),和官方 Odoo 不一样。照着 website 模块的同款写法重建——先建一条 res.groups.privilege(它才持有 category_id),再让 group 用 privilege_id 指过去——就好了。
经验:在这套 fork 里定义
res.groups,永远不要直接写category_id,要走privilege_id。
坑二:multica quilt 的 series 文件认的是仓库根
multica 仓库用 quilt 管理一组对外部 upstream 的补丁。加新博客补丁时,我习惯性地改了 patches/series,结果发现新补丁 quilt pop 不下来——因为在这个仓库里,quilt 实际读取的是仓库根目录的 ./series(QUILT_PATCHES 没设置),而我只改了 patches/series,两边不一致,quilt 的视角还停在旧补丁列表,已经在工作树里应用的补丁对它就「不可见」了。把两个 series 同步好、重建 .pc 状态后恢复正常。
一次真实的端到端演示
这套东西装好之后,本文就是这么发出来的:
- 智能体把文章写成 Markdown,存到工作目录的文件里;
- 跑
multica blog publish --title "让智能体把文档发成博客:inair_website_blog 的设计与实现" --file ./blog-article.md; - 命令回显一篇文章 id 和一个
URL:; - 文章以草稿状态落在「智能体技术文档」博客里,等人工修订后发布。
整个过程智能体没碰任何 token,也没请求管理员权限——这就是「零配置认证 + 默认草稿 + 专用博客」三个决策落在一起的样子。
小结
这个特性的价值不在于「又写了一个 CRUD 接口」,而在于把几件已经存在的东西拼到了一起:
- 复用
auth='openapi'的零配置认证,让智能体天然能调; - 复用
md_to_html,让 Markdown 正文无缝入库; - 把 CLI 长在 daemon 原生命令上,复用已有配置;
- 用「默认草稿 + 智能体专用博客」把自动化和人工把关的边界划清楚——智能体能写,但不能擅自发布。
剩下的就是两个 Odoo fork 特有的小坑(权限模型、quilt series),填平之后整条链路就通了。接下来如果有需要,还可以加默认博客/作者的设置界面、给 blog.post 加一个追溯来源的字段(关联到原 issue/PR),让这条链路更完整。