企业知识协同

Odoo 企业版 Knowledge 为什么不用文章 Chatter 直接评论:行内 Thread、锚点快照、点名通知与关闭边界讲透

很多人以为 Knowledge 评论无非是给文章挂个 chatter。但从 `knowledge.article.thread` 的模型、控制器与测试看,Odoo 企业版做的是另一套机制:评论 thread 绑定的是文章里的某段内容,不是整篇文章;锚点文本会被净化成快照;portal 用户虽然可参与,但发送字段、通知收件人、附件 token、关闭权限都被严格收口。

企业 协同办公
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

如果你把 Odoo 企业版 Knowledge 的评论理解成“给文章加个 chatter 面板”,那会把这个功能看浅了。

/home/ubuntu/odoo-temp/enterprise/knowledge 里的 knowledge.article.thread、控制器和测试一起看,官方真正实现的是一种 面向正文片段的行内讨论系统

  1. 评论不是挂在整篇文章上,而是挂在文章里某段被选中的内容上;
  2. 这段锚点文本不会原样信任前端 HTML,而是会转成净化后的纯文本快照;
  3. thread 继承 mail.thread,但通知对象、邮件模板、访问按钮都按 Knowledge 场景重新裁剪;
  4. portal 用户并不是完全不能参与,但能发什么、通知谁、能否关闭 thread,都有单独约束;
  5. “关闭讨论”也不是删记录,而是把 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,会出现几个问题:

  1. 无法精确说明“这条评论在文中的哪一段”;
  2. 多个局部讨论会全部挤到一条总消息流里;
  3. 审阅和编辑上下文会脱钩;
  4. 关闭某一段讨论会非常别扭。

所以 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 Text
  • Should be Purified

也就是说,Knowledge 并不把 anchor 当成可信 HTML 片段保存。

为什么要这么做?

因为 thread 的锚点只是“讨论上下文提示”,不是一段待渲染正文。

如果把原始 HTML 存下来,会带来三种风险:

  1. 富文本污染讨论数据;
  2. 恶意标签进入邮件/通知/侧栏展示;
  3. 未来正文 beacon 结构调整时,旧 thread 上下文难以兼容。

所以官方选择把 anchor 快照降维成纯文本,这其实很稳。

Knowledge 要保存的是“我当时评论的大意是什么”,不是“把原前端 HTML 永久带进后端”。


四、portal 用户为什么能发言,但又不是完整 mail.thread 权限

message_post() 的重写非常耐看。

如果当前用户是 portal,并且 article_id.user_has_access 为真,系统允许其发消息;但只保留四类字段:

  • body
  • partner_ids
  • author_id
  • attachment_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,而是:

  1. 先用 token 校验 thread;
  2. 再检查 thread.article_id.user_can_write
  3. 只有能写文章的人,才能把 is_resolved 置为 True
  4. 最后跳回文章页面,并可带 show_resolved_threads=True

模型层 write() 也要求改 is_resolvedensure_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 = comment
  • subtype = mt_comment
  • is_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 很值得借鉴:

  1. 讨论对象要绑定正文片段,而不是只挂整篇文档
  2. 讨论上下文要保存纯文本快照,别直接信任前端 HTML
  3. portal 协作可以开放,但必须削减到底层消息能力最小集
  4. 通知应偏向定向点名,而不是把知识讨论做成广播流
  5. resolved 是收起,不是删除,历史可追溯仍然重要。

Odoo 企业版在这里做得很像现代文档审阅系统,而不像传统 ERP chatter。

Knowledge 里的评论,本质上是在给“知识正文的一段话”附着一条可审阅、可点名、可收起的讨论链。

这就是它比“文章下留言”高级很多的地方。


源码依据

  • enterprise/knowledge/models/knowledge_article_thread.py
  • enterprise/knowledge/controllers/article_thread.py
  • enterprise/knowledge/tests/test_knowledge_article_thread.py
  • enterprise/knowledge/tests/test_knowledge_article_thread_permissions.py

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。