跳转至

数字员工配置解析链路

本文档描述数字员工从"用户点击创建"到"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.goCreate 方法

输入 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.gobuildTFRServerSpec

输入: instance.Spec (从 DB 读取)
    json.Unmarshal → TFRServerSpec 结构体
    TFRServerDefaults.ApplyTo() — 仅填充空字段(不覆盖已有值)
    注入 ConfzData(见第 5 节)
输出: params.Spec (TFRServerSpec)  ——  传给执行器

TFRServerDefaults 硬编码默认值(仅填充空字段):

代码位置: internal/shared/k8s/crd/defaults.goDefaultTFRServerDefaults / 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 同上

注意:apiImageworkerImagerobotWorkerImage 在此阶段不设置默认值—— 它们由后续的集群配置提供。


阶段 C:执行器应用集群级配置

发生时机: 执行器拿到 params.Spec 后,在实际创建 CR 之前

代码位置: internal/user/service/k8s_service/crd_service/executor.goExecute 方法

这是最复杂的一层,按代码执行顺序分 3 步:

C-1. 再次应用代码默认值(executor 自己也持有一份 defaults,双重保障)

e.defaults.ApplyTo(&params.Spec)  // 仅填充空字段

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.goClusterServerConfig 结构体

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.goresolveTenantConfig()

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.goClusterTenantConfig 结构体

字段 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 层的参数。

配置来源可视化(可选增强)

在管理端的实例调试/详情页面,可以标注每个配置字段的最终来源:

imagePullPolicy: IfNotPresent     [用户配置]    ← 用户在创建时覆盖了模板默认值
apiImage: registry.../tfrs-api    [模板默认]    ← 来自模板 defaultSpec
workerImage: registry.../worker   [集群配置]    ← 实例和模板都没设,集群配置兜底填充
imagePullSecret: cluster-secret   [集群配置]    ← 集群配置强制覆盖
apiResources: 250m/256Mi          [系统默认]    ← 各层都没设,代码硬编码兜底