先说结论
如果你把 Odoo 企业版 Knowledge 的评论理解成“给文章加个 chatter 面板”,那会把这个功能看浅了。
从 /home/ubuntu/odoo-temp/enterprise/knowledge 里的 knowledge.article.thread、控制器和测试一起看,官方真正实现的是一种 面向正文片段的行内讨论系统:
- 评论不是挂在整篇文章上,而是挂在文章里某段被选中的内容上;
- 这段锚点文本不会原样信任前端 HTML,而是会转成净化后的纯文本快照;
- thread 继承
mail.thread,但通知对象、邮件模板、访问按钮都按 Knowledge 场景重新裁剪; - portal 用户并不是完全不能参与,但能发什么、通知谁、能否关闭 thread,都有单独约束;
- “关闭讨论”也不是删记录,而是把 thread 标记为
is_resolved,从编辑上下文里收起来。
所以更准确的理解应该是:
Knowledge 的评论系统,不是文章级 chatter,而是“绑定正文锚点的轻量审阅线程”。它解决的是协作编辑、审阅备注和上下文讨论,而不是泛化消息流。
这也是一个非常典型的 企业, 协同办公 题目。
一、为什么官方单独建 knowledge.article.thread,而不是直接往文章上发消息
模型 models/knowledge_article_thread.py 开头的说明已经把定位说得很清楚:
- 每个 thread 都对应文章里的一个具体讨论点;
- 初始评论开启一个 thread;
- 后续回复、reaction、@mention 都围绕这个 thread 累积;
- thread 可以被关闭,从而不再出现在文章编辑器里。
字段设计也证明它不是普通 chatter:
article_id:绑定哪篇文章;article_anchor_text:记录当时选中的锚点文本;is_resolved:表示讨论已关闭。
如果直接用文章级 chatter,会出现几个问题:
- 无法精确说明“这条评论在文中的哪一段”;
- 多个局部讨论会全部挤到一条总消息流里;
- 审阅和编辑上下文会脱钩;
- 关闭某一段讨论会非常别扭。
所以 Odoo 的选择是:
文章正文负责承载知识内容,thread 负责承载局部协作语境。两者关联,但不混成同一个对象。
二、为什么还要保存 article_anchor_text:因为正文会变,讨论上下文不能跟着蒸发
Knowledge 的一个核心现实是:文章是会持续改的。
如果某条评论只是依赖“当时 DOM 里某个位置”,那后面正文被重写、重排甚至删段后,这条讨论就容易失去上下文。
所以 Odoo 在 thread 里保存了 article_anchor_text,并且 help 文案写得很明确:
原始高亮文本会被保存下来,以便即使那段文本之后被修改或删除,thread 仍然保留初始上下文。
这意味着 thread 记录的是一种 上下文快照,而不是永远实时跟随正文重算的选区。
这点很重要,因为审阅系统最怕的不是正文改变,而是评论失去“当时在说哪一段”的证据。
三、为什么锚点文本要先净化成纯文本:因为评论上下文不能变成 HTML 注入入口
create() 和 write() 里都对 article_anchor_text 做了同一件事:
- 先
html2plaintext(); - 再按
_ANCHOR_TEXT_MAX_LENGTH = 1200截断。
测试 test_knowledge_article_thread_create_w_unsafe_anchors() 甚至故意塞进:
<iframe><script>- 各种 thread beacon 标签
最后断言保存结果只是纯文本,比如:
Anchor TextShould be Purified
也就是说,Knowledge 并不把 anchor 当成可信 HTML 片段保存。
为什么要这么做?
因为 thread 的锚点只是“讨论上下文提示”,不是一段待渲染正文。
如果把原始 HTML 存下来,会带来三种风险:
- 富文本污染讨论数据;
- 恶意标签进入邮件/通知/侧栏展示;
- 未来正文 beacon 结构调整时,旧 thread 上下文难以兼容。
所以官方选择把 anchor 快照降维成纯文本,这其实很稳。
Knowledge 要保存的是“我当时评论的大意是什么”,不是“把原前端 HTML 永久带进后端”。
四、portal 用户为什么能发言,但又不是完整 mail.thread 权限
message_post() 的重写非常耐看。
如果当前用户是 portal,并且 article_id.user_has_access 为真,系统允许其发消息;但只保留四类字段:
bodypartner_idsauthor_idattachment_ids
同时强制:
message_type='comment'subtype_xmlid='mail.mt_comment'
测试 test_message_post_as_portal() 还验证了:
- portal 没有文章访问权时,直接
AccessError; - 有访问权时可以发评论;
- 但额外传入的
tracking_value_ids会被过滤掉。
这说明官方态度很明确:
- portal 参与讨论可以;
- 但不能借
message_post()当作低层 mail API 去塞命令式参数。
换句话说,Knowledge 接受“受控发言”,不接受“借评论接口扩权”。
五、为什么通知只发给显式点名的人:Knowledge thread 不是广域订阅广播
_notify_get_recipients() 直接把收件人再过滤了一遍:
recipients_data = [data for data in recipients_data if data['id'] in (msg_vals or {}).get('partner_ids', [])]
这意味着在 Knowledge thread 里,通知不是“谁关注了这个 thread 就全发”,而是偏向:
- 只通知这次明确点名的对象。
再结合 _message_compute_subject() 返回 New Mention in <article>,以及 _notify_thread_by_email() 使用专门的 knowledge_mail_notification_layout,就能看出这是一个很强的产品信号:
Knowledge thread 的通知语义更像“请你来看这段内容”,而不是“某条业务记录又产生了一条 chatter”。
这与普通业务单据上的 chatter 很不同。
业务单据偏向完整留痕;Knowledge 行内 thread 偏向局部协作和定向提醒。
六、访问按钮为什么回文章,不回 thread 自己:因为 thread 只是文章里的协作入口
_get_access_action() 也很有代表性。
只要接收者对文章有访问权,通知按钮就回:
'/knowledge/article/<article_id>'
而不是打开一个“thread 详情页”。
这其实很合理,因为 thread 脱离正文上下文几乎没有意义。
你真正想看的不是“这条评论记录本身”,而是:
- 它所在的文章;
- 它依附的段落;
- 周围相关的知识内容。
所以官方把 thread 定位成 文章内的协作入口,而不是可独立消费的内容实体。
七、“关闭讨论”为什么只是 is_resolved:因为讨论应被收起,而不是被抹掉
控制器 controllers/article_thread.py 暴露了 /knowledge/thread/resolve。
它做的不是删除 thread,而是:
- 先用 token 校验 thread;
- 再检查
thread.article_id.user_can_write; - 只有能写文章的人,才能把
is_resolved置为True; - 最后跳回文章页面,并可带
show_resolved_threads=True。
模型层 write() 也要求改 is_resolved 时 ensure_one(),测试 test_security_thread_resolution() 则验证:
- 对文章无权限的人,不能关闭 thread;
- 有文章权限的人,可以关闭。
这背后其实体现了一个重要产品判断:
- 讨论结束 ≠ 讨论应该消失;
- 更合理的做法是 退出编辑主视野,但仍保留历史。
这很像代码评审里的 resolve conversation,而不是 delete conversation。
八、附件为什么会生成 access token:因为评论附件也要能安全跨邮件/门户流转
_process_attachments_for_post() 做了一步额外动作:
self.env['ir.attachment'].browse(attachment_ids).generate_access_token()
测试 test_message_post_as_employee() 也验证,用户在 thread 里发附件后,消息附件会带 access_token。
这一步看似小,其实很关键。
因为 Knowledge thread 的讨论很可能通过:
- 邮件通知;
- portal 访问;
- 外部协作者点开的入口
来继续流转。
如果附件没有独立 token,这条讨论即使正文能打开,附件也可能在跨入口场景下断链。
所以 Odoo 顺手把评论附件也做成可受控访问对象。
九、消息拉取为什么只拉 comment、且排除 internal:因为前端 thread 面板要的是“用户讨论”,不是所有系统噪声
KnowledgeThreadController.mail_threads_messages() 最后走的 domain 很克制:
message_type = commentsubtype = mt_commentis_internal = False
这代表前端 thread 面板只想展示真正的用户讨论输入,而不是:
- 跟踪值变更;
- 系统 note;
- 仅内部可见消息。
如果把这些都塞进来,Knowledge 行内 thread 很快就会变成一条嘈杂的邮件流。
而官方显然不想这样。
十、实战里最容易误解的 4 件事
误区 1:Knowledge 评论就是文章 chatter
不是。它是段落级 thread 系统。
误区 2:anchor 会永久跟着正文实时变化
不是。它保存的是净化后的上下文快照。
误区 3:portal 能看文章,就等于拿到完整 mail.thread 能力
也不是。portal 发言字段被显著裁剪,很多 mail 参数不会被接受。
误区 4:关闭 thread 等于删除 thread
不是。它更像“已解决,不再打扰主编辑视图”。
十一、这套设计对企业知识协同的启发
如果你在 Odoo 里自己做知识审阅、SOP 评审、制度协作,knowledge.article.thread 很值得借鉴:
- 讨论对象要绑定正文片段,而不是只挂整篇文档;
- 讨论上下文要保存纯文本快照,别直接信任前端 HTML;
- portal 协作可以开放,但必须削减到底层消息能力最小集;
- 通知应偏向定向点名,而不是把知识讨论做成广播流;
- resolved 是收起,不是删除,历史可追溯仍然重要。
Odoo 企业版在这里做得很像现代文档审阅系统,而不像传统 ERP chatter。
Knowledge 里的评论,本质上是在给“知识正文的一段话”附着一条可审阅、可点名、可收起的讨论链。
这就是它比“文章下留言”高级很多的地方。
源码依据
enterprise/knowledge/models/knowledge_article_thread.pyenterprise/knowledge/controllers/article_thread.pyenterprise/knowledge/tests/test_knowledge_article_thread.pyenterprise/knowledge/tests/test_knowledge_article_thread_permissions.py
DISCUSSION
评论区