Skip to content

平台管理与系统管理员 ​

Yuheng 的权限分两层:工作区内的四级角色(见工作区、用户与认证授权),以及平台级的系统管理员。这一篇讲后者——它管的不是某个知识库,而是整个部署。

先划清界限:

工作区 Owner系统管理员
作用范围单个工作区整个部署
怎么获得部署的第一个注册用户是默认工作区的 Owner;之后的工作区由系统管理员创建时指定 Owner,工作区内再由 Owner 提升他人部署的第一个注册用户自动获得;之后由现有系统管理员提升,或用环境变量引导
管什么工作区成员、工作区组、模型、知识库、数据源、工作区审计工作区与账号(建工作区、建账号、把人加进任意工作区)、全局系统设置、任务队列、平台 API Key、跨工作区审计、重置用户密码
是否自动叠加—不会:在某工作区是 Owner 不代表是系统管理员,反之亦然

还有第三个标志 CanAccessAllTenants(跨工作区访问),它管的是「能不能读写别人的工作区数据」,与系统管理员也是分开的:系统管理员默认看不到别人工作区里的知识库内容。

1. 第一个系统管理员怎么来 ​

默认情况下,部署的第一个注册用户就是系统管理员。 注册模式 auth.registration_mode 的默认值 auto 只在用户表为空时开放注册,此时注册的人被标为系统管理员,之后注册自动关闭(internal/handler/auth.go 的 registrationState;「表为空才插入」在 CreateFirstUser 里用 PostgreSQL advisory lock 保证,两个人同时注册也只会产生一个管理员)。

注册模式改成 self_serve 或 invite_only、或者第一个用户不是该当管理员的人时,可以用环境变量引导(cmd/server/bootstrap.go):

  1. 先用正常流程注册一个账号(或由其他方式创建);
  2. 给 app 服务设 YUHENG_BOOTSTRAP_SYSTEM_ADMIN_EMAIL=<该账号邮箱>,重启;
  3. 启动时 bootstrapSystemAdmin() 检查——仅当当前部署一个系统管理员都没有时,把该邮箱对应的用户提升为系统管理员。

几个刻意的设计:

  • 不会创建用户:邮箱还没注册时只打一条 WARN,下次重启再试。账号创建涉及密码哈希、工作区分配、审计,不适合在启动钩子里走捷径;
  • 幂等且会自动失效:一旦存在系统管理员,这个变量就不再授权——避免管理员在界面上刚撤销的权限被下次重启悄悄恢复。所以它可以长期留在部署清单里;
  • 失败不阻断启动:整个 bootstrap 是 best-effort,配错了变量也能把服务拉起来再改。

之后新增/移除管理员在「系统设置」页里操作即可(对应 POST /system/admin/promote / revoke,GET /system/admin/list 列出现有管理员)。撤销有两道保险:不能撤销自己,也不能撤销最后一个系统管理员——否则平台会永久失去系统级管理能力。对已经不是管理员的用户重复撤销返回 200(幂等),但审计记录里 changed=false,便于事后区分真实撤销与空操作。

2. 控制台能做什么 ​

界面入口在「设置」侧栏的「系统管理」分组,仅系统管理员可见(前端白名单见 frontend/src/config/settingsAccess.ts 的 SYSTEM_ADMIN_SETTINGS_SECTIONS),五个分区:

分区作用接口
用户与工作区部署里有哪些工作区、哪些账号、谁属于哪个工作区:创建工作区(总是带 Owner)、查看与修改任意工作区的成员、创建账号(可直接选定工作区与角色)、重置密码、提升/撤销系统管理员,见 §2.1/tenants/all、POST /tenants、/system/admin/tenants/:id/members*、/system/admin/users/*、`/system/admin/promote
系统设置全局运行时开关(注册模式、默认配额、上传大小、并发、SSRF 白名单、集中管理基础设施等),见 §3GET/PUT/DELETE /system/admin/settings[/:key]
任务队列查看 asynq 各队列实时积压、逐个任务的重试/归档/删除、批量清空归档任务;无 Redis 时返回 available=false/system/admin/runtime/queues*
平台 API Key面向控制面自动化的 platform 作用域 Key,能力包括 system_tenants_read/manage、system_settings_read/manage、system_runtime_read/manage、system_audit_read/system/admin/api-keys
系统审计日志tenant_id = 0 的平台级事件(改设置、提升/撤销管理员、队列操作等)。工作区级审计接口按 tenant 过滤,看不到这些行GET /system/admin/audit-log

2.1 用户与工作区 ​

模型是「用户是全局身份,一个企业一个工作区」:注册只建账号,工作区只能由系统管理员创建(详见工作区、用户与认证授权)。于是「谁在哪个工作区」这件事要有人管,这一页就是为此而设——工作区 Owner 在「工作区 → 成员」里管自己工作区的人,系统管理员在这里管自己不在的工作区、建新工作区、建账号。

页面(frontend/src/views/system/SystemUsersWorkspaces.vue)分两块:

块能做什么接口
工作区列出部署里的全部工作区及成员数;创建工作区(Owner 是自己,或填一位已注册用户的邮箱);打开任一工作区的成员抽屉:按邮箱添加已有账号并指定角色、改角色、移出。最后一位 Owner 不能被降级或移出GET /tenants/all(带 member_count)、POST /tenants(owner_email)、GET/POST /system/admin/tenants/:id/members、PATCH/DELETE /system/admin/tenants/:id/members/:user_id
用户创建账号(密码留空由服务端生成并只显示一次);创建时可选「加入工作区」并指定角色;重置密码;提升/撤销系统管理员POST /system/admin/users/create(tenant_id、role)、POST /system/admin/users/reset-password、POST /system/admin/promote / revoke、GET /system/admin/list

几条规则:

  • /system/admin/tenants/:id/members* 与工作区自己的 /tenants/:id/members* 是同一组 handler,只是守卫从「该工作区 Owner + URL 工作区须等于当前工作区」换成 SystemAdmin(),所以系统管理员可以操作一个自己不是成员的工作区——给刚为别人建的工作区加第一批人,或把一个还没有任何工作区的账号加进来。改角色这里是 PATCH(请求体只有一个字段)。
  • 工作区删除仍由工作区 Owner 在工作区设置里做,且工作区里还有别人、或它是部署最后一个工作区时会被拒绝(code 2007 / 2008);系统管理员要删工作区,先把成员移走。
  • 平台 API Key 带 system_tenants_read / system_tenants_manage 也能走这些接口,用于自动化开通;用平台 Key 建工作区时必须给 owner_email,因为 Key 不是人、不能当 Owner(code 2006)。
  • 审计:建 / 删工作区记在平台级审计里(system.tenant_created,details 含 name、owner_user_id、owner_email;system.tenant_deleted);成员变更仍记为工作区级的 rbac.member_added / member_role_changed / member_removed,但系统管理员在自己不在的工作区里操作时 actor_role 是 system_admin,而不是他在别处恰好持有的角色;建账号记 system.user_created。

还没有任何工作区的用户登录后看到「你的账号还没有加入任何工作区,请联系管理员」;系统管理员自己落到这一页时则可以直接建工作区。

另外几个不在上表、但同属系统管理员的能力:

  • 平台级解析引擎默认配置(GET/PUT /system/admin/parser-engine-config,界面在「设置 → 解析引擎 → 平台默认」):介于部署环境变量与各工作区自己的覆盖之间;
  • 批量套用默认存储配额(POST /system/admin/tenants/apply-default-storage-quota):把当前的默认配额写到所有已存在的工作区上。之所以挂在 /tenants 而不是 /settings 下,是因为它改的是工作区数据而不是设置行。

3. 运行时可改的系统设置 ​

internal/application/service/system_setting.go 维护一张注册表,表内的键可以在控制台里改,数据库值盖过环境变量;除标明需要重启的,改完立即生效:

键类型默认(对应环境变量)生效时机
file.max_size_mbint50(MAX_FILE_SIZE_MB)立即;默认部署下最大可调到 512(docreader gRPC 硬顶 DOCREADER_GRPC_MAX_FILE_SIZE_MB)
file.video_max_size_mbint2048(MAX_VIDEO_FILE_SIZE_MB)立即;超过前端 nginx 启动时的请求体上限需改环境变量并重启前端
auth.registration_modeauto / self_serve / invite_onlyauto(只在还没有用户时开放,首个注册者成为系统管理员)立即
tenant.default_storage_quota_gbint0(不限;YUHENG_TENANT_DEFAULT_STORAGE_QUOTA_GB)仅新建工作区时读取,不回写已有工作区;企业级工作区通常不需要配额,「批量套用默认存储配额」可以把它写到已有工作区上
tenant.auto_accept_invitationboolfalse立即;开启后邀请已注册用户时直接加入工作区,不再等对方接受
ssrf.whitelist字符串列表空(SSRF_WHITELIST)立即(SSRF_WHITELIST_EXTRA 仍只由部署方维护,不在此覆盖)
asynq.core/postprocess/enrichment/maintenance/shared/wiki_concurrencyint见异步任务系统需重启
model.max_concurrencyint32(YUHENG_MODEL_MAX_CONCURRENCY)立即;只限制后台任务,不影响交互式对话
governance.centralized_infraboolfalse立即;开启后模型、网络搜索、向量存储、存储后端、解析引擎、Ollama 的写操作只允许系统管理员,工作区 Owner/Admin 保留只读(PlatformManaged 路由守卫)

配置来源不只有环境变量

上表这些键一旦在控制台里改过,数据库里就有了一行记录,之后改环境变量不再有效果。排查「明明改了 env 却没生效」时先看这里;把设置项重置(DELETE /system/admin/settings/:key)会删掉 DB 行,重新回落到环境变量或内置默认值。

4. 集中管控基础设施 ​

面向内部部署:平台运维统一配置模型、解析引擎、向量存储、存储后端、网络搜索等基础设施,其他注册用户只管使用,不接触凭据。它与工作区 RBAC 正交——RBAC 管「一个工作区里谁能做什么」,这里管「基础设施归谁配」。

为什么工作区角色解决不了 ​

工作区角色回答的是「在这个工作区里谁能做什么」:Owner / Admin 管工作区的成员、知识库、模型。企业工作区的 Admin 很可能是业务部门的负责人,而模型 API Key、存储凭据、解析引擎端点属于平台运维;角色阶梯里没有一个位置能表达「是工作区管理员,但不配基础设施」——把门槛提到 Owner 也只是换一批业务侧的人。要表达「只有平台运维能配」,需要一个与工作区角色正交的维度,这就是 governance.centralized_infra。

开启 ​

在「设置 → 系统管理 → 系统设置」的「访问」一栏打开「集中管控基础设施」,立即生效、无需重启。也可以用环境变量 YUHENG_GOVERNANCE_CENTRALIZED_INFRA=true 作为部署期默认值(数据库里的值优先,见 §3 的提示)。

前置条件:

  1. 先有系统管理员,见 §1;
  2. 配好 SYSTEM_AES_KEY:没有它时,模型、存储后端、数据源、API Key 等凭据以明文落库(加密只在配置了这个密钥时进行)。资源共享出去意味着更多人依赖这份数据,这是前置条件而不是可选项。

开关关闭时行为与未开启前完全一致,可以先验证再切。

开启后的变化 ​

  • 下列设置分区对非系统管理员隐藏(前端表 PLATFORM_MANAGED_SETTINGS_SECTIONS):模型管理、Ollama、向量数据库引擎、解析引擎、存储引擎、网络搜索;
  • 对应的写接口走 PlatformManaged 路由守卫,只允许系统管理员;工作区 Owner / Admin 保留只读;
  • 入口隐藏不影响使用:建知识库时的选择器走各自的 Viewer+ 只读接口,平台资源照常出现在下拉框里,只是凭据与端点被响应 DTO 抹掉;
  • 工作区级的事务不受影响:工作区信息、成员、消息管理、API Key 仍由工作区 Owner / Admin 管理。设为工作区默认存储后端也仍是工作区 Admin 的权限。

平台共享资源 ​

集中管控回答「谁能配」,共享回答「配好的东西谁能用」:普通用户失去写权限之后,需要有平台提供的实例可选。共享与取消共享只有系统管理员能操作(authorizePlatformSharingChange),被共享的行标记为 is_builtin,对所有工作区可见:

资源怎么共享取消共享的护栏
模型模型卡片菜单 → 设为平台共享仍被任意工作区的知识库绑定时拒绝
向量存储卡片菜单 → 设为平台共享其他工作区仍有知识库绑定时拒绝
存储后端卡片菜单 → 设为平台共享(PUT /storage-backends/:id/sharing);部署存储 env 始终共享、不可取消其他工作区仍有默认存储、知识库、文档空间绑定或存着文件时拒绝
网络搜索卡片菜单 → 设为平台共享(PUT /web-search-providers/:id/sharing)无(按次选用,失效时退化为联网搜索不可用)
解析引擎「平台默认」档位,见下不适用(是配置块,不是实例)

共享资源对非系统管理员只读:凭据从不返回,端点、Base URL 与额外配置也一并抹掉,只留名称、类型等够选择器渲染的信息。

护栏之所以必要:knowledge_bases 上的 embedding_model_id、vector_store_id、storage_backend_id 都是没有外键约束的字符串列。取消共享或删除一个还被别的工作区引用的资源,那个工作区的检索会静默失效——不报错,只是查不出东西。所以这些操作放行前会做跨工作区引用扫描。存储后端的停用、删除、取消共享都按所有工作区统计引用;共享中的存储后端仍须先取消共享再删除,让「从所有工作区撤下」始终是系统管理员的明确决定。网络搜索要先取消共享再删除:删除路径上的引用检查按工作区计,直接删会留下其他工作区的悬空引用。

解析引擎的三层配置 ​

解析引擎不是实例列表而是一个配置块,所以走继承而不是共享:

部署环境变量  <  平台默认(系统管理员)  <  工作区覆盖
  • 部署环境变量在 docreader 内部生效,每个引擎读自己的变量;
  • 平台默认在「设置 → 解析引擎 → 平台默认」(仅系统管理员,接口 GET/PUT /system/admin/parser-engine-config);
  • 工作区覆盖在「设置 → 解析引擎 → 本工作区」,集中管控开启后对非系统管理员隐藏。

合并是逐字段的(internal/types/parser_engine_platform.go):只覆盖了 MinerU 端点的工作区,仍然继承平台的其余引擎设置。布尔开关用指针类型正是为此——工作区显式关掉某个选项会覆盖平台默认的开启,工作区从未提及的字段才落到平台默认。

运维注意 ​

  • Embedding 模型换不得:已被知识库使用的 embedding 模型一换,已有向量全部失效,必须重建索引。
  • 额度没有护栏:模型共享出去后所有工作区共用同一份 API 额度;model.max_concurrency 限制的是并发,不是配额。
  • 收紧注册:内网可达但不隔离时,把 auth.registration_mode 设为 invite_only,避免陌生人注册后使用平台模型。
  • YAML 与界面的所有权:系统管理员在界面上保存过某个由 config/builtin_models.yaml 下发的内置模型后,该行的 managed_by 被清空,YAML 之后不再覆盖它;反过来,界面拒绝删除仍由 YAML 托管的行(删了下次启动会被写回)。
  • 改入口规则时前后端一起改:前端表 PLATFORM_MANAGED_SETTINGS_SECTIONS 与后端 PlatformManaged 路由不一致,会出现「能打开但存不进去」或「界面没有但接口还能调」。

相关 ​

实现参考 ​

路径内容
cmd/server/bootstrap.goYUHENG_BOOTSTRAP_SYSTEM_ADMIN_EMAIL 引导
internal/handler/auth.go、internal/application/repository/user.go(CreateFirstUser)首个注册者成为系统管理员
internal/handler/system.go提升/撤销管理员、创建用户(可带工作区与角色)、重置密码、平台 API Key
internal/handler/tenant.go、internal/handler/tenant_member.go创建 / 删除工作区、工作区目录(/tenants/all、/search)、任意工作区的成员管理
internal/middleware/access.go(CanManageTenantCatalog、RequireTenantCatalogAccess)工作区目录守卫:系统管理员 / 跨工作区超管 / 平台 Key
internal/application/service/system_setting.go、internal/types/system_setting.go运行时系统设置注册表
internal/router/routes_auth_tenant.go/system/admin/* 路由
internal/router/rbac.go、internal/middleware/rbac.go(RequirePlatformManaged)PlatformManaged 守卫
internal/application/service/platform_shared_infra.go共享资源的写守卫
internal/types/parser_engine_platform.go解析引擎配置的分层合并
frontend/src/config/settingsAccess.ts(SYSTEM_ADMIN_SETTINGS_SECTIONS、PLATFORM_MANAGED_SETTINGS_SECTIONS)、frontend/src/views/system/SystemSettings.vue、SystemUsersWorkspaces.vue控制台入口、系统设置页、用户与工作区页

基于 Yuheng v0.1.0 源码整理 · MIT License