TencentDB Agent Memory 的两个路由守卫:八条路由一处没接线

2026-08-16

翻别人家前端的权限实现,最省事的办法是先找 RouteGuards 这类文件——名字摆在那儿,一看就知道作者想在路由这一层拦什么。TencentDB Agent MemoryMemoryPanel 里就有这么一个文件。但把它和路由表放在一起看,会发现一件需要照实记下来的事:守卫定义好了,路由表上一处都没用。

先说清楚版本与分支的前提。本文所有路径都来自该仓库 feat/server_team 分支(这就是它的默认分支,不是 main 也不是 master)在 2026-08-16 的快照。MemoryPanel 这个目录的后端包名是 team-memory-control、版本 0.1.0MemoryPanel/package.json),前端包名是 community-loop-lab-web、版本同样是 0.1.0 且标了 "private": trueMemoryPanel/web/package.json)。整个项目的主模块还处在 beta 阶段,参数、文件与接口随版本变动,下面写到的每一处位置都请以仓库最新内容为准。

两个守卫本身长什么样

MemoryPanel/web/src/components/RouteGuards.tsx 定义了两个组件:

  • ResourceGuard:12-22):admin 角色访问资源页时重定向到工作台。
  • MemberManageGuard:27-41):reviewer 不可见。

注意第二个的名字:文件里的函数名逐字是 MemberManageGuard,顺手写成「MemoryManageGuard」是很容易发生的事。写脚本 grep 的时候拼错一个词,就会得到「没有这个守卫」的错误结论。

两个组件都从 @/services/useCurrentRole 取当前角色。角色枚举是三值的 'admin' | 'member' | 'reviewer'MemoryPanel/web/src/services/useCurrentRole.ts:18),同一组取值在 web/src/lib/api/types.ts:60web/src/services/backendStore.ts:34web/src/lib/api/teams.ts:52 里都出现过。useCurrentRole.ts:8-12 的注释把两层角色说得很清楚:admin 是全局角色,与是否创建或加入任何 team 无关,职责是管理 team(建团队、录入成员),不管理具体资源;member 是 team 内角色,负责在 team 内管理 agent、skill、wiki、code、memory 这些资源;判断顺序必须先判全局 admin,再查 active team 里的成员角色。

路由表这一侧

MemoryPanel/web/src/routes/index.tsx:18-33createHashRouter:39,注释说明是为了兼容旧版 hash 路由、避免刷新 404)注册了 8 条子路由:

路径页面组件
/(index)WorkbenchPage
/wikiWikiPage
/codeCodePage
/skillsSkillsPage
/memoryChatMemoryPage
/team/membersMembersPage
/team/agentsAgentsPage
/team/api-keysApiKeysPage

这 8 条 element 里直接放的都是页面组件本身,没有一条被 ResourceGuardMemberManageGuard 包起来。

你自己怎么核这一条

不要信任何人的转述,包括这篇。在你本地的仓库副本里,MemoryPanel/web/src/ 下跑一次这样的检索就够了:

grep -rn "ResourceGuard\|MemberManageGuard" MemoryPanel/web/src

我们跑出来的命中行全部落在 RouteGuards.tsx 自己身上:文件头注释的第 4、5 行,以及两个 export function 的定义行 12 与 27。也就是说,除了它自己,没有第二个文件提到这两个名字。判定动作只有两步:第一步看 grep 的命中文件里有没有 routes/index.tsx;第二步直接打开 routes/index.tsx,逐条看 8 个 element 有没有嵌套包装。两步都对不上,才算复现了这个观察。

反过来,什么情况说明你看到的不是这回事?如果你手里的副本不是 feat/server_team 分支、或者不是这个时间点的快照,路由表随时可能已经改了;如果你 grep 的是仓库根而不是 MemoryPanel/web/src,会把无关目录的同名字符串也捞进来;如果你把函数名拼成了「MemoryManageGuard」,那就是 0 命中,得到的是另一个结论。

那么哪一处按角色过滤是真的在跑

在这一层里,我们能找到的、确实接进渲染路径的按角色过滤只有一处:MemoryPanel/web/src/layouts/ConsoleLayout.tsx:129

if (userRole === 'reviewer' && meta.id === 'team_members') continue;

它作用在菜单生成上——reviewer 角色看不到「成员管理」这一项。菜单侧的 PageId 共 8 个(web/src/constants/menu.tsx:19-27):workbench_board / wiki / code / skills / chat_memory / team_members / team_agents / api_keys,分组顺序是 ['workbench', 'organization', 'assets']:58),workbench_board 还标了 affix: true,是固定标签页。也就是说,菜单里少一项和路由能不能进是两件事,前者改的是导航的可见性。

围绕 admin 的几处口径,彼此不一致

顺着这条线往下翻,会撞上几处互相对不上的表述。按纪律,这里只摆位置和原文要点,不推断哪个是对的,也不延伸。

第一组,关于 admin 能不能进资源页:

  • web/src/layouts/ConsoleLayout.tsx:123 注释写「admin 可访问所有页面(含资源管理)」。
  • web/src/components/RouteGuards.tsx:4:13-15 写的是「ResourceGuard:admin 角色访问资源页 → 重定向到工作台」。
  • web/src/services/permissions.ts:17 提到在 admin 逻辑下 ResourcePage 会显示 AdminResourceLock
  • 中文文案 web/src/i18n/zh-CN.ts:186 是「资源管理功能暂未对管理员开放」。

四处说法不一致,而其中依赖守卫生效的那一处(ResourceGuard),我们在 8 条路由上一处都没找到接线。

第二组,关于 admin 有没有全局特权:

  • web/src/services/permissions.ts:9 的注释写「全局 admin 判断:admin 拥有所有权限,可见所有内容」。
  • web/src/services/backendStore.ts:334 的注释写「admin 不再拥有全局特权,与 member 一致:只能操作自己 owner 的资产」。

对应到实现:isGlobalAdminpermissions.ts:20-22)的函数体是 return isAdminFlag === true;,唯一权威来源是 auth/verify 响应里的 user.user_type === 'system_admin',由 LoginGate 在登录时写入 AuthState.isAdmin:11-12);而 canManageAssetbackendStore.ts:327-338)的当前实现是「资产的 owner_user_id === userId 为真,或者同 team 且 isTeamAdmin 为真,其余一律 false」,它的第 4 个入参 _isGlobalAdminFlag 带下划线前缀且未被使用(:331)。两处注释放在一起就是不一致,说到这里为止。

前端这一层不是唯一的校验点

值得同时记住的是,Panel 的后端有自己的入站校验,不依赖前端守卫是否接线。

MemoryPanel/src/panel/http/middleware/validate-panel-headers.ts 是统一入站门:缺 x-tdai-service-id 返回 400 MISSING_INSTANCE_ID:34-37),缺 x-tdai-user-key 返回 400 MISSING_USER_KEY:50-53),唯一例外是 auth/verify——该 action 的 user_key 只放 body(:9:49)。往里一层,MemoryPanel/src/panel/http/routes/knowledge/common.tsresolveCallerUserId:56-67)拿 ctx.userKey 调内核 auth/verify 反查身份,requireTeamMember:89-100)在非法 user_key 时返回 401 INVALID_USER_KEY、非成员时返回 403 NOT_TEAM_MEMBER

Chat Memory 的读权限判定函数是 authorizeChatMemoryReadMemoryPanel/src/panel/http/routes/chat-memory.ts:1999-2036),三条放行路径任一命中即可:调用者是资产 Owner;资产 visibility === "team" 且调用者是该 team 成员(注释明确写「外 team 用户即便知道 asset_id 也不算可读」);或者该资产已绑定到调用者名下的某个 agent。异常时 fallthrough 到拒绝(:2032-2035)。写与删更紧:/chat-memory/clear 逐条校验后仅放行资产 Owner,任一不符整批拒绝(:1391-1410);/chat-memory/layer-delete 同样仅 Owner,注释给的理由是「读可以借入,但删除内容只允许 Owner」(:1437-1438)。

把这两侧并排放着看就够了:前端守卫的接线状态是一件事,后端端点上写了哪些校验是另一件事,两者各自是什么样,上面都给了确切的文件与行号,你可以自己去核。我们没有部署过这个面板,也没有对任何端点发过一个请求,因此这里不给「够不够」的结论。

还有一批「演示阶段」的自述

顺带说一句读这块代码时的背景。MemoryPanel/web/src/services/ 下有 5 个 store 明确自述为演示阶段的 localStorage 实现,共用底座 storage-utils.ts,其 :12-13 原文写「这些 store 均为前端演示阶段的 localStorage 实现,后端上线后整批替换为 fetch 即可」。资产范围覆盖层 asset-scope-store.ts:14 也写着「后端上线后换成一张 asset_acl 表即可」。前端还有一个能力开关 PANEL_CAPABILITIES = { assets: false }web/src/lib/api/base.ts:22-24),:18-20 的注释说明为 false 时应展示「暂未开放」占位,不要发起注定 501 的请求。这些都属于原样记录的「阶段性」标注,不能当成已完成能力来读。

所以,如果你正打算基于这个前端做二次开发,本文的可操作结论只有一句:不要因为仓库里有一个 RouteGuards.tsx 就默认路由级拦截已经在跑,先跑一遍上面那条 grep,再打开 routes/index.tsx 逐条看 8 个 element。至于要不要把它们接上、接在哪几条路由上,取决于你自己的角色模型,项目没有给出通用答案。

延伸阅读


本文依据 TencentDB Agent Memory 官方仓库(github.com/TencentCloud/TencentDB-Agent-Memoryfeat/server_team 分支上的 README、INSTALL、CHANGELOG、ROADMAP 与四个模块的源码整理, 核对日 2026-08-16,对应仓库快照 97f9465。该仓库的默认分支即为 feat/server_team。 本文内容为仓库源码与文档口径,我们没有部署、也没有运行过该项目的任何一个模块, 因此不涉及运行效果、检索质量与性能的任何描述。 该项目主模块处于 beta 阶段、其余模块版本号仍为 0.1.0,参数与接口随版本变动,请以仓库最新内容为准。 该项目会采集并存储团队的对话、文档与代码,属于敏感数据,是否使用请结合自身合规要求评估。 安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。