很多团队做企业知识门户时,第一反应都是:
给外部客户或合作方发一个链接,让他们点进去看文章就行。
但 Odoo 企业版 Knowledge 的设计并不是“链接即访问”。
它真正解决的是下面这串问题:
- 这个人是内部员工、portal/share user,还是还没注册的外部伙伴;
- 邀请链接点开后,是应该先登录、先注册,还是直接看文章;
- 文章是“everyone 可见”,还是“只有被邀请成员可见”;
- 子文章是不是继承根文章的可见性;
- 用户打开
/knowledge/home或/knowledge/article/<id>时,应该进后台表单,还是前端门户壳。
所以这套模块真正保护的不是“让人看到文档”,而是:
把外部协作访问限制在被显式授予的成员范围内,同时给内部用户和 share user 两套不同入口。
这也是为什么你明明看到文章勾了某种“对所有人可见”的效果,portal 用户却不一定能看。
主链路怎么走
1)邀请链接不是直接放行,而是先校验 member + hash
入口在 knowledge/controllers/main.py 的 article_invite()。
这条路由先根据 member_id 找 knowledge.article.member,再用 _get_invitation_hash() 校验 URL 里的 invitation_hash。只有 member 记录存在且 token 对得上,才会继续。
这意味着 Knowledge 的邀请不是“谁拿到 URL 谁都能进”,而是建立在具体成员记录上的一次性访问桥。
如果 token 不对,标准行为是直接 NotFound,不是给你展示一个“也许你没权限”的模糊提示。这种处理更像安全边界,而不是营销页入口。
2)未注册外部成员先被送去注册,而不是直接变成 portal 访客
article_invite() 里最值得注意的一段,是当 partner.user_ids 不存在时的处理。
也就是说,这个合作伙伴虽然已经被邀请为 article member,但还没有真正的用户账号。此时系统会:
- 检查当前实例是否允许 signup invitation;
- 必要时对 partner 调
signup_prepare(); - 生成一个最终会跳回
/knowledge/article/<id>的 signup URL; - 强制用户先完成注册,再回文章。
这里的意思非常明确:
Knowledge 的外部访问不是匿名阅读,而是“先把你变成一个明确可识别的用户,再给你成员权限”。
这和很多人理解的“发个公共知识库链接”完全不同。
3)已注册用户也不是“直接看”,而是先走登录,再按身份分流
如果 partner 已经有用户账号,article_invite() 会把请求重定向到:
/web/login?redirect=/knowledge/article/<id>
登录完成后,真正决定去哪里的是 redirect_to_article() 和 access_knowledge_home():
- 内部用户
_is_internal()为真,走_redirect_to_backend_view(),进入/odoo/knowledge/<id>或 action form; - 非内部用户,也就是 share/portal user,走
_redirect_to_portal_view(),渲染knowledge_portal_view。
这就是企业版非常明确的产品切分:
- 内部员工看后台知识工作台;
- 外部成员看前端门户壳。
不是一个页面套两种权限,而是两种入口从路由层就分叉了。
为什么“everyone 可见”不等于 portal 都能看
这是最容易误解的一点。
在 knowledge.article._compute_is_article_visible() 里,源码明确写了注释:
portal users are forced to be members of the articles and do not benefit from
is_article_visible_by_everyone
实现也确实如此:
- 对内部用户,如果文章
is_article_visible_by_everyone,就可以直接算 visible; - 对非内部用户,
visible_articles一开始就是空集; - 后续只能靠
knowledge.article.member里 partner 对 article 或 root article 的成员权限,去把is_article_visible算成真。
换句话说:
“everyone 可见”主要服务于内部范围的可见性继承,不是给外部门户用户发无限制通行证。
这背后的产品逻辑很合理:Knowledge 的 share user 协作场景是“受控共享”,不是“公开文档站”。
root article 继承同样是权限边界的一部分
_compute_is_article_visible() 不只检查文章本身的 member,还会看 root_article_id 对应的 member。也就是说,如果你被加在一棵根文章上,下面的子树可以沿着这条根关系继承可见性。
这就让企业版可以支持一种很常见的对外交付方式:
- 不必逐篇邀请;
- 以根文章为单位,把一整棵知识树交给某个客户或合作方;
- 但仍然不是整个系统里的所有文章都开放。
门户首页为什么看起来“全都有”,其实不是
在 knowledge/controllers/portal.py 里,_prepare_knowledge_article_domain() 默认返回空 domain,看起来像没做限制;但真正生效的限制并不靠这里硬编码,而是靠文章自身的 is_article_visible 搜索逻辑和成员关系。
这是一种常见但容易被误判的企业版写法:
- 门户控制器保持轻;
- 权限判断下沉到模型层;
- 无论你从首页、文章页还是搜索过来,最终都吃同一套可见性规则。
这能避免“列表能看到、详情打不开”或“详情能直达、搜索却漏结果”的权限分裂问题。
实战里最常见的误区
误区 1:把 Knowledge 门户当成网站 CMS
如果你的需求是“任何拿到链接的人都能匿名看文档”,Knowledge 不是最合适的第一选择。它天生更偏向成员协作和受控共享,而不是 public website 文档站。
误区 2:只配 article member,不检查 partner 是否真的有用户
很多实施以为把联系人加成 member 就结束了。实际上,如果对方还没有 user_ids,访问链会先进入注册流程。没理解这一层,就会把“客户打不开文档”误判成权限 bug。
误区 3:以为子文章必须逐篇单独授权
如果权限是沿 root article 继承的,那么更好的做法往往是按文档树来设计协作边界,而不是每篇文章都单独维护成员,后者既脆弱又难审计。
给实施和二开的建议
- 先定义协作模型,再选模块。 想做匿名公开文档站,用 Website/Helpdesk Knowledge 这类公开链路;想做受控共享,用 Knowledge 门户。
- 邀请失败优先查三件事: member 记录、hash 是否匹配、partner 是否已完成注册。
- 按根文章建权限。 如果一组内容天然属于同一客户或项目,优先把权限挂在 root article,而不是散到叶子文章。
- 自定义门户搜索时,别绕开模型层域。 列表、全文搜索、直达详情都应该复用
is_article_visible一致判定。
一句话总结
Odoo 企业版 Knowledge 门户不是“发个邀请链接,对方就能看文档”。
它的真实设计是:先校验 member 邀请,再把外部对象变成明确用户,随后按 internal/share user 分流到后台或门户,并且只让成员关系覆盖到的文章树可见。
理解了这点,你就不会再把门户权限问题误判成“链接失效”,而会回到真正的诊断顺序:邀请记录、注册状态、身份分流、文章树可见性。
DISCUSSION
评论区