数字员工配置解析链路¶
本文档描述数字员工从"用户点击创建"到"Pod 运行在 K8s"的完整配置合并流程。 涉及 TFRServer(实例级)和 TFRTenant(租户基础设施级)两条独立链路。
对应代码位置已在各阶段标注,可直接跳转查阅。
1. 总览:两条独立的配置链路¶
部署一个数字员工会产生两个 K8s 自定义资源(CR),它们各自有独立的配置解析链路:
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ TFRTenant(租户基础设施) TFRServer(数字员工实例) │
│ ───────────────────── ─────────────────────── │
│ 每个组织/namespace 一个 每个数字员工实例一个 │
│ Operator 负责创建: Operator 负责创建: │
│ PostgreSQL, Redis, MinIO 等 API/Worker/RobotWorker Pod │
│ │
│ 配置来源(优先级从高到低): 配置来源(优先级从高到低): │
│ 集群 TenantConfig 用户提交 Config ← 优先级最高 │
│ └→ 环境变量兜底 └→ 模板 DefaultSpec │
│ └→ 代码硬编码默认值 └→ 代码硬编码默认值 │
│ └→ 环境变量兜底 │
│ └→ 集群 ServerConfig │
│ (仅填充空字段, │
│ imagePullSecret │
│ 除外:无条件覆盖) │
│ └→ ConfzData 自动注入 │
│ │
│ 对用户不可见,管理员配置 用户可见、可部分控制 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2. TFRServer 配置链路:从创建到部署的完整数据流¶
一个 TFRServerSpec 的最终内容由以下阶段按执行顺序依次构建:
阶段 A:创建时分离存储¶
发生时机: 用户点击"创建数字员工"时,HTTP 请求到达后端
代码位置: internal/user/service/digital_employee_service/crud_service.go — Create 方法
输入 1: 模板的 defaultSpec (JSON) —— 管理员在 AdminPortal 配置的模板默认值
输入 2: 用户提交的 config (JSON) —— 用户在 FrontPortal 创建表单中填写的值(configSchema 业务参数)
↓
分离存储:
defaultSpec → instance.Spec (JSONB) —— 纯 TFRServerSpec 字段
config → instance.UserConfig.Feature —— 业务参数(temperature、knowledgeBase 等)
↓
输出: instance.Spec —— 只包含 TFRServerSpec 字段,不混合业务参数
instance.UserConfig —— 包含密码哈希 + Feature 业务参数
存储规则: Spec 只存模板 defaultSpec(纯 TFRServerSpec 字段),用户 config(configSchema 表单业务参数)存入 UserConfig.Feature。两者不再浅合并。
Feature 注入规则: UserConfig.Feature 通过 BuildConfzData() 注入到 ConfzData.Feature。Feature map 中匹配 FeatureConfig 结构体的字段生效,不匹配的字段静默丢弃。
举例:
模板 defaultSpec: {"imagePullPolicy":"Always", "apiImage":"reg/api:v2"}
用户 config: {"uploadFileSizeLimitMb":"50", "temperature":0.7}
↓
instance.Spec (存入DB): {"imagePullPolicy":"Always", "apiImage":"reg/api:v2"}
instance.UserConfig.Feature: {"uploadFileSizeLimitMb":"50", "temperature":0.7}
↓ BuildConfzData
ConfzData.Feature: {"uploadFileSizeLimitMb":"50"} // temperature 不匹配 FeatureConfig,被丢弃
前端展示建议:在创建向导的配置步骤中,可以展示模板
configSchema定义的表单字段, 用户填写的值会存入 UserConfig.Feature,通过 ConfzData.Feature 注入到应用。
阶段 B:构建 TFRServerSpec¶
发生时机: 后台任务执行器准备部署时,从 DB 读取 instance 记录
代码位置: internal/user/service/digital_employee_service/multi_cluster_instance_service.go — buildTFRServerSpec
输入: instance.Spec (从 DB 读取)
↓
json.Unmarshal → TFRServerSpec 结构体
↓
TFRServerDefaults.ApplyTo() — 仅填充空字段(不覆盖已有值)
↓
注入 ConfzData(见第 5 节)
↓
输出: params.Spec (TFRServerSpec) —— 传给执行器
TFRServerDefaults 硬编码默认值(仅填充空字段):
代码位置: internal/shared/k8s/crd/defaults.go — DefaultTFRServerDefaults / ApplyTo
| 字段 | 默认值 | 说明 |
|---|---|---|
imagePullPolicy |
IfNotPresent |
仅当 Spec 中该字段为空时填充 |
apiResources |
requests: 250m CPU, 256Mi 内存 / limits: 1000m, 1Gi | 仅当 Spec 中该字段为 nil 时填充 |
workerResources |
requests: 500m, 512Mi / limits: 2000m, 2Gi | 同上 |
robotWorkerResources |
requests: 500m, 512Mi / limits: 2000m, 2Gi | 同上 |
注意:
apiImage、workerImage、robotWorkerImage在此阶段不设置默认值—— 它们由后续的集群配置提供。
阶段 C:执行器应用集群级配置¶
发生时机: 执行器拿到 params.Spec 后,在实际创建 CR 之前
代码位置: internal/user/service/k8s_service/crd_service/executor.go — Execute 方法
这是最复杂的一层,按代码执行顺序分 3 步:
C-1. 再次应用代码默认值(executor 自己也持有一份 defaults,双重保障)
C-2. 环境变量兜底(仅 ImagePullSecret 一个字段)
if params.Spec.ImagePullSecret == "" {
params.Spec.ImagePullSecret = os.Getenv("TFRS_SERVER_IMAGE_PULL_SECRET")
}
只有当 Spec 中 ImagePullSecret 仍然为空时才使用环境变量。
C-3. DB 集群配置覆盖(K8sCluster.ServerConfig)
数据来源: internal/shared/db/models/k8s_cluster.go — ClusterServerConfig 结构体
cluster := 从 DB 读取当前部署集群
cfg := cluster.GetServerConfig()
// 以下字段:仅在 Spec 为空时才用集群配置填充
if spec.APIImage == "" → spec.APIImage = cfg.APIImage
if spec.WorkerImage == "" → spec.WorkerImage = cfg.WorkerImage
if spec.RobotWorkerImage == "" → spec.RobotWorkerImage = cfg.RobotWorkerImage
if spec.ImagePullPolicy == "" → spec.ImagePullPolicy = cfg.ImagePullPolicy
// 特殊:ImagePullSecret 无条件覆盖(不检查是否为空)
if cfg.ImagePullSecret != "" → spec.ImagePullSecret = cfg.ImagePullSecret // 强制覆盖!
ClusterServerConfig 的字段(管理员在集群管理页配置):
| 字段 | JSON key | 覆盖行为 | 说明 |
|---|---|---|---|
apiImage |
"apiImage" |
仅填充空字段 | TFRServer API 组件镜像地址 |
workerImage |
"workerImage" |
仅填充空字段 | Worker 组件镜像地址 |
robotWorkerImage |
"robotWorkerImage" |
仅填充空字段 | RobotWorker 组件镜像地址 |
imagePullPolicy |
"imagePullPolicy" |
仅填充空字段 | 镜像拉取策略 |
imagePullSecret |
"imagePullSecret" |
无条件覆盖 | 镜像拉取凭证名称 |
为什么 imagePullSecret 要无条件覆盖? 因为不同集群的私有镜像仓库凭证不同, 必须确保使用目标集群的正确凭证,否则 Pod 无法拉取镜像。这是运维层面的强约束。
阶段 D:最终 CR 提交到 K8s¶
执行器将经过 A→B→C 所有阶段处理后的 TFRServerSpec,组装成 TFRServer CR YAML 并通过 K8s API 创建到目标集群的目标 namespace 中。
完整举例:从头到尾追踪一个字段¶
示例 1:imagePullPolicy — 用户配置生效
| 阶段 | 值 | 发生了什么 |
|---|---|---|
| 管理员设置模板 defaultSpec | "Always" |
模板中预设 |
| 用户创建实例提交 config | "IfNotPresent" |
用户希望用本地缓存 |
| A. 创建时合并 | → "IfNotPresent" |
用户 config 覆盖了模板默认值 |
| B. buildTFRServerSpec | → "IfNotPresent" |
ApplyTo 检查非空,跳过 |
| C-1. executor defaults | → "IfNotPresent" |
检查非空,跳过 |
| C-2. 环境变量 | — | 环境变量仅影响 imagePullSecret,跳过 |
| C-3. 集群配置 | → "IfNotPresent" |
集群配置的 imagePullPolicy 仅填充空字段,跳过 |
| 最终值 | "IfNotPresent" |
用户的选择生效了 |
示例 2:imagePullSecret — 集群配置强制覆盖
| 阶段 | 值 | 发生了什么 |
|---|---|---|
| 模板 defaultSpec | (未设置) | 模板通常不设置此字段 |
| 用户 config | (未提交) | 用户通常不关心此字段 |
| A. 创建时合并 | → "" (空) |
无来源 |
| B. buildTFRServerSpec | → "" (空) |
defaults 中也为空 |
| C-1. executor defaults | → "" (空) |
无默认值 |
| C-2. 环境变量 | → "env-registry-secret" |
环境变量 TFRS_SERVER_IMAGE_PULL_SECRET 填充了 |
| C-3. 集群配置 | → "cluster-a-secret" |
集群配置无条件覆盖,即使环境变量已填充 |
| 最终值 | "cluster-a-secret" |
集群级凭证强制生效 |
示例 3:apiImage — 模板配置优先于集群配置
| 阶段 | 值 | 发生了什么 |
|---|---|---|
| 模板 defaultSpec | "myregistry/api:v2.1" |
管理员在模板中指定了镜像 |
| 用户 config | (未提交) | 用户不关心镜像 |
| A. 创建时合并 | → "myregistry/api:v2.1" |
保留模板值 |
| B. buildTFRServerSpec | → "myregistry/api:v2.1" |
defaults 中 apiImage 为空,跳过 |
| C-3. 集群配置 | → "myregistry/api:v2.1" |
集群配置的 apiImage 仅填充空字段,已有值不覆盖 |
| 最终值 | "myregistry/api:v2.1" |
模板配置生效 |
示例 3b:apiImage — 模板和用户都没设,集群配置兜底
| 阶段 | 值 | 发生了什么 |
|---|---|---|
| 模板 + 用户 | (空) | 无来源 |
| C-3. 集群配置 | → "cluster-registry/api:v3.0" |
集群配置填充了空字段 |
| 最终值 | "cluster-registry/api:v3.0" |
集群配置作为兜底 |
TFRServerSpec 全字段配置来源汇总¶
| TFRServerSpec 字段 | 模板 DefaultSpec 可设? | 用户 Config 可设? | 代码默认值? | 环境变量? | 集群 ServerConfig? | 覆盖行为 |
|---|---|---|---|---|---|---|
apiImage |
可 | 否(不再合并到 Spec) | 无 | 无 | 可 | 仅填充空字段 |
workerImage |
可 | 否 | 无 | 无 | 可 | 仅填充空字段 |
robotWorkerImage |
可 | 否 | 无 | 无 | 可 | 仅填充空字段 |
imagePullPolicy |
可 | 否 | IfNotPresent |
无 | 可 | 仅填充空字段 |
imagePullSecret |
可 | 否 | 无 | TFRS_SERVER_IMAGE_PULL_SECRET |
可 | 集群配置无条件覆盖 |
apiResources |
可 | 否 | 250m/256Mi~1/1Gi | 无 | 无 | 仅填充 nil |
workerResources |
可 | 否 | 500m/512Mi~2/2Gi | 无 | 无 | 仅填充 nil |
robotWorkerResources |
可 | 否 | 500m/512Mi~2/2Gi | 无 | 无 | 仅填充 nil |
confzData |
不可 | 不可 | — | — | — | 系统自动注入(见第 5 节) |
suspended |
不可 | 不可 | — | — | — | FSM 控制 |
3. TFRTenant 配置链路(租户基础设施)¶
TFRTenant 是 Operator 管理的租户级基础设施(PostgreSQL、Redis、MinIO),每个组织 namespace 一个,在数字员工首次部署到某集群时自动创建。
对用户完全不可见,仅管理员在集群管理界面配置。
代码位置: internal/user/service/k8s_service/multi_cluster_service_manager.go — resolveTenantConfig()
1. 尝试读取 DB 集群配置: K8sCluster.TenantConfig (JSONB)
│
├─ 有配置 → 使用 DB 配置(缺失部分用兜底补齐)
│ imagePullSecrets: DB 中配置的列表
│ storage: DB 中配置的 storageClass + defaultSize
│ tfrobotFrontImage/tfrsUtilsWorkerImage: DB 中配置的组件镜像
│
└─ 无配置 → 全部使用兜底值
imagePullSecrets: 环境变量 TFRS_IMAGE_PULL_SECRET || "tfrs-private-registry"
storage: 环境变量 TFRS_STORAGE_CLASS || "cbs"
环境变量 TFRS_DEFAULT_STORAGE_SIZE || "10Gi"
ClusterTenantConfig 的字段(管理员在集群管理页配置):
数据来源: internal/shared/db/models/k8s_cluster.go — ClusterTenantConfig 结构体
| 字段 | JSON key | 说明 |
|---|---|---|
imagePullSecrets |
"imagePullSecrets" |
私有镜像拉取凭据列表 [{secretName, sourceNamespace}] |
storage |
"storage" |
租户存储配置 {storageClass, defaultSize} |
tfrobotFrontImage |
"tfrobotFrontImage" |
TFRobotFront 组件自定义镜像地址 |
tfrsUtilsWorkerImage |
"tfrsUtilsWorkerImage" |
TFRSUtils Worker 组件自定义镜像地址 |
TFRTenant 创建后,Operator 会自动在该 namespace 中创建 PostgreSQL、Redis、MinIO 等基础设施。 这些基础设施的连接信息(DSN、密码等)会通过 Operator 自动注入到后续创建的 TFRServer CR 中, 代码层面不管理,前端也无需关注。
4. 环境变量汇总¶
以下环境变量参与配置解析链路,在 .env 文件或部署环境中设置:
| 环境变量 | 影响的链路 | 作用 | 默认值(代码硬编码) |
|---|---|---|---|
TFRS_SERVER_IMAGE_PULL_SECRET |
TFRServer 阶段 C-2 | 实例级镜像拉取凭证兜底 | "" (空) |
TFRS_IMAGE_PULL_SECRET |
TFRTenant | 租户级镜像拉取凭证兜底 | "tfrs-private-registry" |
TFRS_STORAGE_CLASS |
TFRTenant | 租户级存储类型兜底 | "cbs" |
TFRS_DEFAULT_STORAGE_SIZE |
TFRTenant | 租户级存储大小兜底 | "10Gi" |
环境变量在所有链路中都是中间优先级:高于代码硬编码默认值,低于 DB 集群配置。
5. ConfzData(应用密钥配置)¶
ConfzData 是注入到 TFRServer CR 中的应用级敏感配置,不参与上述优先级合并,而是在阶段 B 由 BuildConfzData() 自动构建。
代码位置: internal/user/service/digital_employee_service/confz_builder.go
来源 1: instance.SystemConfig (JSONB) — 实例创建时自动生成的系统密钥
来源 2: instance.UserConfig.Password — 用户提交的密码(Argon2id 哈希)
来源 3: instance.UserConfig.Feature — 用户提交的业务参数(configSchema 表单)
来源 4: instance 字段 — robotId, namespace
↓
输出: ConfzData 结构体,注入到 TFRServerSpec.confzData
| ConfzData 字段 | 来源 | 用户可配? | 前端展示? |
|---|---|---|---|
admin.adminKey |
系统自动生成(创建时随机) | 否 | 否(敏感) |
admin.adminSecret |
系统自动生成 | 否 | 否(敏感) |
admin.jwtSecret |
系统自动生成 | 否 | 否(敏感) |
admin.password |
用户创建时设置(Argon2id 哈希后存储) | 是(创建时) | 否(敏感) |
robot.robotId |
用户指定或系统自动生成 | 是(创建时) | 是 |
robot.namespace |
组织的 K8s namespace | 否(自动分配) | 可选 |
robot.dbSchema |
= robotId | 否 | 否 |
robot.sessionSecret |
系统自动生成 | 否 | 否(敏感) |
feature.* |
用户创建/更新时提交(UserConfig.Feature → FeatureConfig) | 是 | 否 |
postgresql.* |
Operator 从 TFRTenant 自动注入 | 否 | 否 |
redis.* |
Operator 从 TFRTenant 自动注入 | 否 | 否 |
6. 前端展示建议¶
管理端(AdminPortal)¶
| 页面 | 建议展示 | 对应配置层 |
|---|---|---|
| 模板编辑 — defaultSpec | JSON 编辑器,标注 "创建数字员工时,这些值将作为初始配置,用户可覆盖" | 阶段 A 输入 |
| 模板详情 — defaultSpec | 只读 JSON 展示,附说明 "这些是模板预设的 TFRServerSpec 字段" | 阶段 A 输入 |
| 集群管理 — ServerConfig | 表单编辑 apiImage / workerImage / robotWorkerImage / imagePullPolicy / imagePullSecret | 阶段 C-3 |
| 集群管理 — ServerConfig.imagePullSecret | 表单旁标注 "此项将强制覆盖所有部署到该集群的实例的镜像拉取凭证" | 阶段 C-3(无条件覆盖) |
| 集群管理 — ServerConfig 其他字段 | 标注 "仅当实例和模板都未设置时,此值才生效" | 阶段 C-3(仅填充空字段) |
| 集群管理 — TenantConfig | 表单编辑 imagePullSecrets / storage / 组件镜像。标注 "租户基础设施配置,对用户不可见" | TFRTenant 链路 |
用户端(FrontPortal)¶
| 页面 | 建议展示 | 说明 |
|---|---|---|
| 创建向导 — 配置步骤 | 将模板 defaultSpec 的可配置字段作为表单初始值展示,用户可修改。已修改的字段标记 "已自定义",未修改的标记 "模板默认" |
让用户理解哪些是自己改过的 |
| 实例详情 — 配置信息 | 展示 instance.config(即合并后的 Spec),这是阶段 A 的输出 |
用户能看到最终存入 DB 的配置 |
| 实例列表 | crName 列(原 releaseName),标题用 "资源名称" |
字段重命名 |
注意: 用户端只能看到阶段 A 的结果。instance.Spec 存储纯 TFRServerSpec 字段(来自模板 defaultSpec), 用户提交的业务参数(configSchema 表单)存储在 UserConfig.Feature 中,通过 ConfzData.Feature 注入。 阶段 B/C 的代码默认值和集群配置是运维层面透明应用的,用户不感知也不需要感知。 管理员可以在 AdminPortal 的集群管理页面看到和配置 B/C 层的参数。
配置来源可视化(可选增强)¶
在管理端的实例调试/详情页面,可以标注每个配置字段的最终来源: