首页
/ Rocket.Chat Apps 引擎:动作按钮的角色过滤 now 支持按角色名(hasOneRole / hasAllRoles)

Rocket.Chat Apps 引擎:动作按钮的角色过滤 now 支持按角色名(hasOneRole / hasAllRoles)

2026-09-05 09:14:21作者:胡唯隽

本篇基于仓库中的变更集 .changeset/app-action-button-role-names.md 展开:Rocket.Chat 的 Apps 引擎在应用动作按钮(app action button)的 when 过滤条件中,让 hasOneRolehasAllRoles 两个过滤器除了接受角色 ID 之外,新增接受角色名(role name)。你将理解这一变更涉及哪两个包(@rocket.chat/apps-engine@rocket.chat/meteor,均为 minor 级更新)、为什么自定义角色必须用名字而非 ID 来匹配、客户端如何在渲染时完成"名字 → ID"解析,以及"按房间作用域授予的角色"(ownermoderatorleader 等)在哪些界面才能命中。

一、变更集说了什么

.changeset/app-action-button-role-names.md 的完整内容如下:

---
'@rocket.chat/apps-engine': minor
'@rocket.chat/meteor': minor
---

Accepts a role name in the `when.hasOneRole` and `when.hasAllRoles` filters of an app action button

要点拆解:

  • 变更语义:应用动作按钮的 when.hasOneRole(持有列表中任意一个角色即显示按钮)与 when.hasAllRoles(必须持有列表中全部角色才显示按钮)过滤器,条目现在既可以是角色 ID,也可以是角色名。
  • 涉及包
    • @rocket.chat/apps-engine(minor)——负责动作按钮描述符的类型定义,即 IUActionButtonWhen 接口的文档与约束更新;
    • @rocket.chat/meteor(minor)——负责 Web 客户端在渲染按钮时的实际过滤逻辑更新。
  • 兼容性:两个包均为 minor 版本提升,说明这是向后兼容的增量能力——原来用角色 ID 写法的 App 不受影响,新写法只是拓宽了可接受的取值。

二、类型定义层:IUActionButtonWhen 的角色过滤器

变更落在 IUIActionButtonDescriptor.ts 中的 IUActionButtonWhen 接口。与角色相关的两个字段及其官方注释(JSDoc)如下:

export interface IUActionButtonWhen {
	roomTypes?: Array<RoomTypeFilter>;
	messageActionContext?: Array<MessageActionContext>;
	hasOnePermission?: Array<string>;
	hasAllPermissions?: Array<string>;
	/**
	 * Show the button when the user holds at least one of these roles.
	 *
	 * Each entry is a role id or a role name. Prefer the name for a custom role,
	 * because its id differs between workspaces.
	 *
	 * A role scoped to `Subscriptions` — `owner`, `moderator`, `leader`, or a custom
	 * one — is granted per room, so it matches only on surfaces bound to a room. On a
	 * surface with no room of its own, the user dropdown for instance, only roles
	 * scoped to `Users` match.
	 */
	hasOneRole?: Array<string>;
	/**
	 * Show the button when the user holds every one of these roles.
	 * …(语义同上,要求全部命中)
	 */
	hasAllRoles?: Array<string>;
}

注释明确了三条关键规则:

  1. 每个条目可以是角色 ID 或角色名;对自定义角色推荐用名字,因为同一个自定义角色在不同工作空间(workspace)里的 _id 不同,而名字才是可移植的标识。
  2. 作用域为 Subscriptions 的角色按房间授予——例如 ownermoderatorleader 以及自定义的按房间角色,它们只对"绑定到某个房间"的界面生效。
  3. 没有房间上下文的界面(典型如用户下拉菜单 user dropdown)只能匹配作用域为 Users 的工作空间级角色。

外层按钮描述符 IUIActionButtonDescriptor 则通过可选的 when 字段挂接这些过滤条件,并带有 actionIdcontextlabelI18nvariantcategory 等字段;运行时由 App 注入 appId 后成为 IUIActionButton

三、客户端实现:useApplyButtonAuthFilter 如何解析角色名

实际过滤发生在 Meteor 客户端的 useApplyButtonFilters.ts。核心是 useApplyButtonAuthFilter,它从 AuthorizationContext 取出权限查询函数与 useRoleIdResolver,对按钮执行四组检查并按 AND 逻辑聚合:

export const useApplyButtonAuthFilter = (): ((button: IUIActionButton, room?: IRoom) => boolean) => {
	const uid = useUserId();
	const { queryAllPermissions, queryAtLeastOnePermission, queryRole } = useContext(AuthorizationContext);

	// An app knows the name of a custom role, not the random id the workspace gave it,
	// so accept either form in the role filters.
	const resolveRoleId = useRoleIdResolver();

	return useCallback(
		(button: IUIActionButton, room?: IRoom) => {
			const { hasAllPermissions, hasOnePermission, hasAllRoles, hasOneRole } = button.when || {};

			const hasAllPermissionsResult = hasAllPermissions ? queryAllPermissions(hasAllPermissions)[1]() : true;
			const hasOnePermissionResult = hasOnePermission ? queryAtLeastOnePermission(hasOnePermission)[1]() : true;
			const hasAllRolesResult = hasAllRoles ? !!uid && hasAllRoles.every((role) => queryRole(resolveRoleId(role), room?._id)[1]()) : true;
			const hasOneRoleResult = hasOneRole ? !!uid && hasOneRole.some((role) => queryRole(resolveRoleId(role), room?._id)[1]()) : true;

			return hasAllPermissionsResult && hasOnePermissionResult && hasAllRolesResult && hasOneRoleResult;
		},
		[queryAllPermissions, queryAtLeastOnePermission, queryRole, resolveRoleId, uid],
	);
};

逐行看与本次变更直接相关的部分:

  • hasAllRoles 使用 Array.prototype.everyhasOneRole 使用 Array.prototype.some,分别对应"全部命中"与"任一命中"的语义;未配置过滤器时默认放行(true)。
  • !!uid 前置判断保证了未登录用户只要按钮声明了角色要求,就一律被过滤。
  • 每个角色条目在进入 queryRole 之前先经过 resolveRoleId(role) 解析——这正是"接受角色名"能力的落点;room?._id 作为第二个参数传入,把按房间授予的角色检查限定在当前房间内。

同一文件中的 useApplyButtonFilters 则在房间上下文中组合三类过滤器(权限/角色、房间类型 roomTypescategory),源码中的注释同样点明了作用域规则:

export const useApplyButtonFilters = (category = 'default'): ((button: IUIActionButton) => boolean) => {
	const room = useRoom();
	if (!room) {
		throw new Error('useApplyButtonFilters must be used inside a room context');
	}
	const applyAuthFilter = useApplyButtonAuthFilter();
	return useCallback(
		// The room is the scope of the role check: without it a role scoped to
		// `Subscriptions` — `owner`, `moderator`, `leader`, or a custom one — can never match.
		(button: IUIActionButton) => applyAuthFilter(button, room) && applyRoomFilter(button, room) && applyCategoryFilter(button, category),
		[applyAuthFilter, category, room],
	);
};

其中 applyRoomFilter 通过 enumToFilterRoomTypeFilter 枚举映射到对 IRoom 的判断函数(公开/私有频道、团队、讨论、私信、Live Chat 等),与角色过滤互为 AND 条件。

四、名字到 ID 的解析:useRoleIdResolver

"条目既可以是 ID 也可以是名字"之所以成立,依赖 useRoleIdResolver.ts(由 ui-contexts 包导出)。其实现:

export const useRoleIdResolver = (): ((role: string) => IRole['_id']) => {
	const { getRoles, subscribeToRoles } = useContext(AuthorizationContext);

	const roles = useSyncExternalStore(subscribeToRoles, getRoles);

	const idsByName = useMemo(() => {
		const index = new Map<IRole['name'], IRole['_id']>();
		for (const role of roles.values()) {
			// `roles.create` and `roles.update` reject a name already taken by another role, so a
			// name maps to at most one role. Should duplicates still exist (e.g. written straight
			// to the database), the first one iterated wins instead of the last, so adding another
			// duplicate later does not silently repoint every check that names it.
			if (index.has(role.name)) {
				continue;
			}
			index.set(role.name, role._id);
		}
		return index;
	}, [roles]);

	return useCallback((role: string) => (roles.has(role) ? role : (idsByName.get(role) ?? role)), [idsByName, roles]);
};

三个值得注意的设计决策:

  1. ID 优先:如果传入字符串本身已是某角色的 _idroles.has(role)),直接原样返回;否则再查"名字 → ID"索引;查不到则原样返回(后续 queryRole 会判定为不匹配,按钮被过滤,而不是抛错)。
  2. 名字唯一性依赖服务端约束:注释指出 roles.create / roles.update 会拒绝已被占用的名字,因此名字最多映射到一个角色;即便数据库里出现重复,索引也采用"先遇到的名字胜"策略,避免后来者静默改变所有按名检查的指向。
  3. 响应式订阅:通过 useSyncExternalStore 订阅角色集合,角色增删后索引自动重建,过滤器结果随之更新。

五、测试用例:行为边界的可验证依据

useApplyButtonFilters.spec.tsmockAppRoot() 覆盖了完整的角色过滤矩阵,与上述实现一一对应:

  • 基础过滤:用户持有 adminhasAllRoles: ['admin'] 通过;仅有 user 角色时不通过;hasOneRole: ['admin', 'moderator'] 命中 moderator 即通过,两个都不持有则不通过;未声明角色过滤器的按钮默认显示;匿名(未登录)用户遇到角色要求一律不通过。
  • 自定义角色名解析:以 { _id: 'aBcDeF1234567890x', name: 'Support Agent' } 模拟自定义角色,验证——按名字(Support Agent)要求能命中持有该角色 ID 的用户;按 ID 要求同样命中(向下兼容);要求一个不存在的名字 No Such Role 时被过滤。
  • ID 与重名冲突:构造一个"陷阱角色",其 name 恰好等于另一角色的 _id,且用户只持有陷阱角色——测试断言按该 ID 要求时不显示按钮,证明解析器"ID 优先"的规则不会因名字冲突误放行。
  • 房间作用域角色ownerSubscriptions 作用域授予到某房间后——传入该房间时 hasOneRole: ['owner'] 通过;不传房间(如用户下拉)时被过滤;角色授予在别的房间时,在当前房间也被过滤;而工作空间级 admin 角色在房间上下文中依然能命中。
  • 组合过滤hasAllRoleshasAllPermissions 同时存在时按 AND 逻辑叠加,全部满足才显示。

这些用例共同划定了功能边界:角色名解析只影响"按什么键查角色",不改变按房间作用域判定的既有语义。

六、对 App 开发者的实际影响

在应用的动作按钮描述符里,现在可以这样按名字约束按钮的可见性(示意,字段取值遵循 IUIActionButtonDescriptor.ts 定义):

const button: IUIActionButtonDescriptor = {
	actionId: 'close-support-ticket',
	context: UIActionButtonContext.ROOM_ACTION,
	labelI18n: 'close_ticket',
	when: {
		// 任一命中即显示;对自定义角色推荐使用名字而非 ID
		hasOneRole: ['Support Agent', 'moderator'],
	},
};

使用要点:

  • 自定义角色优先写名字:自定义角色的 _id 在不同工作空间各不相同,用名字可保证同一份 App 代码在多实例间可移植;系统内置角色(admin 等)名字与 ID 一致,两种写法等效。
  • hasOneRolehasAllRoles 语义别混用:前者是 some(任一命中),后者是 every(全部命中),对应按钮"至少一个角色可见"和"必须同时具备多个角色"两类需求。
  • 房间作用域角色的界面限制ownermoderatorleader 及自定义的按房间角色,只会在绑定房间的界面(如房间工具栏动作)命中;在用户下拉这类无房间上下文的表面,只有 Users 作用域的角色能命中。这是由 useApplyButtonFilters.ts 中把 room?._id 透传给 queryRole 的机制保证的。
  • 未登录与未知名字均安全失败:未登录用户、以及解析不到任何角色的名字,都会让按钮被过滤,行为与"按 ID 要求一个不存在的 ID"一致。

七、小结

这条变更集对应的工作由三处源码支撑:类型层在 IUIActionButtonDescriptor.ts 中把两个角色过滤器声明为"ID 或名字";解析层在 useRoleIdResolver.ts 中以"ID 优先、名字索引、响应式重建"完成名字到 ID 的归一;执行层在 useApplyButtonFilters.ts 中把归一后的角色 ID 带入带房间作用域的 queryRole 检查。useApplyButtonFilters.spec.ts 中的测试矩阵(含名字/ID 双写、重名冲突、房间作用域)则给出了这一能力的可验证行为边界。对 App 作者而言,最直接的价值是:自定义角色第一次可以以跨工作空间可移植的名字形式,出现在动作按钮的可见性条件里

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384