You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

301 lines
12 KiB

package console
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"yunyan/comm"
"yunyan/lego/sys/log"
"yunyan/lego/sys/mysql"
"yunyan/lego/sys/postgres"
"yunyan/pb"
)
// ============================ 应用参数归一:业务库 config 表 → svc_config ============================
//
// 2026-09-14 后台「服务与环境配置」的「应用参数」标签页下线。它是业务库 config 表的裸 key/value 编辑器,
// 而 user_getappconfig 原先把这张表**整张**塞进 env 下发——COS 密钥、灵犀产品密钥、连「会员与算力」页
// 写进去的 COMPUTE_RATE_* 都会明文发到每个客户端手里。逐项核过两台机的 22 行之后的处置:
//
// - 客户端还在读的键搬进 svc_config,字段带 env_key、仍按老键名下发(客户端零改动即可):
// AGENT_TYPE / MOBILE_ELF_AGENT_ID / MOBILE_ELF_PRODUCT_ID / MOBILE_ELF_PRODUCT_KEY / YIDONG_PID
// → 服务 llm_mobile_elf(中国移动 灵犀 / 移动精灵)
// iapCustomerServiceQQ → 服务 app_params(应用参数)
// - 谁都不读的死键直接删(appCfgDeadKeys:音乐 / Spotify / 公码 / 导航 / 腾讯 COS / MeetServers);
// COS 那 5 个是腾讯云直传时代的密钥,上传 2026-09-04 起走 OSS 预签名,且已确认线上没有老包在读。
// - 服务端自用的(算力系数 / 运营参数 / 迁移标记)留在表里,由「会员与算力」页维护,**不再下发**。
// - 其余不认识的键原样保留并告警:它们已经没有任何后台入口、也不下发了,
// 要么加进 appParamsPlan 落点表,要么手工清。
//
// 服务实例照内置模板建(builtinSvcTemplates 里的 llm_mobile_elf / app_params),值从 config 表取。
// 幂等、每次启动跑:服务已存在只补空字段;config 表里的行只在**落库成功之后**才删。
//
// 必须排在 migrateGlobalScopeToApps 之后:直接写 app_name=<应用名> 的行。作用域键同那里一样是
// app_registry.app_name(不是部署名 name)。同一应用有多个部署(多套业务库)时按 id 升序,
// 第一个部署的值为准,后面的只清表。
// appParamFieldSpec config 表里的一个键在服务上的落点。ConfigKey 同时也是字段的下发键名(env_key)。
type appParamFieldSpec struct {
ConfigKey string
FieldKey string
}
// appParamSvcSpec 一个落点服务。模板 id 指向 builtinSvcTemplates,名称/类别/字段定义都从模板取。
type appParamSvcSpec struct {
TplId string
CreateIfEmpty bool // config 表里一个值都没有时是否也建:app_params 要建出来让运营有地方填;移动精灵没值就不建
Fields []appParamFieldSpec
}
var appParamsPlan = []appParamSvcSpec{
{
TplId: "llm_mobile_elf",
Fields: []appParamFieldSpec{
{ConfigKey: "AGENT_TYPE", FieldKey: "agent_type"},
{ConfigKey: "MOBILE_ELF_AGENT_ID", FieldKey: "agent_id"},
{ConfigKey: "MOBILE_ELF_PRODUCT_ID", FieldKey: "product_id"},
{ConfigKey: "MOBILE_ELF_PRODUCT_KEY", FieldKey: "product_key"},
{ConfigKey: "YIDONG_PID", FieldKey: "pid"},
},
},
{
TplId: "app_params", CreateIfEmpty: true,
Fields: []appParamFieldSpec{
{ConfigKey: "iapCustomerServiceQQ", FieldKey: "iap_customer_service_qq"},
},
},
}
// appCfgDeadKeys 确认服务端、客户端(含原生插件)、后台都没有读取方的键 → 原因。
// 判定时间 2026-09-14,方法是对 apps/services、apps/client/lib、local_plugins、apps/admin 全文 grep。
var appCfgDeadKeys = map[string]string{
"APP_MUSICSERVICE_TYPE": "音乐模块已删",
"APP_ANDROID_MUSICSERVICE_TYPE": "音乐模块已删",
"APP_IOS_MUSICSERVICE_TYPE": "音乐模块已删",
"SPOTIFY_CLIENT_ID": "音乐模块已删",
"SPOTIFY_REDIRECT_URL": "音乐模块已删",
"APP_AUTHCODE_OPEN": "厂家公码已下线",
"APP_AUTHCODE_JUMP": "厂家公码已下线",
"APP_NAVIGATION_MODE": "老 agent 模块已删,无人读取",
"COS_APP_ID": "上传已改 OSS 预签名,且无老包",
"COS_BUCKET_NAME": "上传已改 OSS 预签名,且无老包",
"COS_REGION": "上传已改 OSS 预签名,且无老包",
"COS_SECRET_ID": "上传已改 OSS 预签名,且无老包",
"COS_SECRET_KEY": "上传已改 OSS 预签名,且无老包",
"MeetServers": "会议选型走 echomeet_orch,无人读取",
}
// appCfgServerKeys 服务端自用、要留在 config 表里的键(由「会员与算力」页维护;迁移标记由 home 写)。
func appCfgServerKeys() map[string]bool {
return map[string]bool{
comm.ConfigKeyComputeRateTranslate: true,
comm.ConfigKeyComputeRateMeeting: true,
comm.ConfigKeyComputeRateAIChat: true,
comm.ConfigKeyComputeGate: true,
comm.ConfigKeyNewUserGiftVipDays: true,
comm.ConfigKeyNewUserGiftCompute: true,
comm.ConfigKeyVipWarnDays: true,
comm.ConfigKeyComputeWarn: true,
"COMPUTE_LEGACY_MIGRATED": true, // modules/user/model_user.go legacyComputeMigratedKey
}
}
func migrateAppParamsToSvc(encKey string, apps []*AppRegistry) {
sort.Slice(apps, func(i, j int) bool { return apps[i].Id < apps[j].Id })
seenScope := map[string]bool{}
for _, app := range apps {
if !app.Enabled || app.ServiceDsn == "" {
continue
}
scope := strings.TrimSpace(app.AppName)
if scope == "" {
log.Warnf("console.appparams: 部署 %s 未归属应用(app_name 为空),它的 config 表不处理", app.Name)
continue
}
db, err := openDriver(app.ServiceDsn)
if err != nil {
log.Warnf("console.appparams: 部署 %s 业务库连接失败,跳过: %v", app.Name, err)
continue
}
items := make([]*pb.DBAppConfigItem, 0)
if err := db.Find(comm.TableAppConfig, &items, ""); err != nil {
log.Warnf("console.appparams: 部署 %s 读 config 表失败,跳过: %v", app.Name, err)
continue
}
if len(items) == 0 {
continue
}
vals := map[string]string{}
for _, it := range items {
k, v := normalizeAppCfgRow(it)
if k == "" {
continue
}
if old, ok := vals[k]; !ok || (strings.TrimSpace(old) == "" && strings.TrimSpace(v) != "") {
vals[k] = v
}
}
migrated := map[string]bool{}
for _, spec := range appParamsPlan {
// 同一应用的第二个部署:服务行已由第一个部署建好,这里只把键标成已迁走去清表,不再覆盖值。
keys, err := upsertAppParamSvc(scope, spec, vals, encKey, seenScope[scope])
if err != nil {
log.Warnf("console.appparams: 应用 %s 服务 %s 迁移失败: %v", scope, spec.TplId, err)
continue
}
for k := range keys {
migrated[k] = true
}
}
seenScope[scope] = true
cleanAppParamRows(app.Name, db, items, migrated)
}
}
// upsertAppParamSvc 建或补一个落点服务,返回这次确认已落到 svc_config 里的 config 键。
// - 服务不存在:按模板建;config 里一个值都没有且 !CreateIfEmpty 则不建(返回空)。
// - 服务已存在:只给空着的字段填值、补缺失的字段与 env_key;运营填过的值不覆盖。
// - 只要字段在服务上存在(不管值是取自 config 还是运营填的),对应 config 键就算迁走——
// svc_config 从此是唯一出处,config 表那行留着只会误导。
//
// ⚠️ 返回的键只在落库成功之后才有效,调用方拿它去删 config 表——提前返回会让两张表都没值。
func upsertAppParamSvc(scope string, spec appParamSvcSpec, vals map[string]string, encKey string, scopeSeen bool) (map[string]bool, error) {
tpl := findBuiltinTemplate(spec.TplId)
if tpl == nil {
return nil, fmt.Errorf("内置模板 %s 不存在", spec.TplId)
}
hasValue := false
for _, f := range spec.Fields {
if strings.TrimSpace(vals[f.ConfigKey]) != "" {
hasValue = true
break
}
}
now := time.Now().Unix()
svc := &comm.ThirdSvcConfig{}
exists := true
if err := postgres.FindOne(comm.TableSvcConfig, svc, "app_name=? AND id=?", scope, tpl.Id); err != nil {
if err != postgres.ErrNoDocuments {
return nil, err
}
exists = false
if !hasValue && !spec.CreateIfEmpty {
return nil, nil
}
fields := make([]comm.SvcField, len(tpl.Fields))
copy(fields, tpl.Fields)
svc = &comm.ThirdSvcConfig{
AppName: scope, Id: tpl.Id, Name: tpl.Name, Provider: tpl.Provider,
Categories: strconv.Itoa(int(tpl.Category)), Description: tpl.Description,
Enable: true, Fields: fields, Createtime: now,
}
}
idx := map[string]int{}
for i, f := range svc.Fields {
idx[f.Key] = i
}
done := map[string]bool{}
changed := !exists
// 同一应用的后续部署只清表不写值——除非服务是这次才建的(首个部署没值没建)。
takeValues := !scopeSeen || !exists
for _, fs := range spec.Fields {
i, ok := idx[fs.FieldKey]
if !ok {
// 模板里没有的字段(模板被运营改过):按明文补一个,别让值丢了。
svc.Fields = append(svc.Fields, comm.SvcField{Key: fs.FieldKey, Description: fs.ConfigKey, Sort: int32(len(svc.Fields))})
i = len(svc.Fields) - 1
idx[fs.FieldKey] = i
changed = true
}
f := &svc.Fields[i]
if f.EnvKey == "" {
f.EnvKey = fs.ConfigKey
changed = true
}
if v := vals[fs.ConfigKey]; takeValues && strings.TrimSpace(v) != "" && strings.TrimSpace(f.DefValue) == "" {
stored, err := storeVal(v, f.Encrypted, encKey)
if err != nil {
return nil, fmt.Errorf("字段 %s 加密失败: %w", fs.FieldKey, err)
}
f.DefValue = stored
changed = true
}
done[fs.ConfigKey] = true
}
// 服务停用 = 这些键一个都不下发。既然 config 表里有值在用,就得让它继续生效(同 migrate_envtosvc 对 stt_azure 的处理)。
if exists && !svc.Enable && hasValue && takeValues {
svc.Enable = true
changed = true
log.Infof("console.appparams: 应用 %s 的服务 %s 原为停用,因承接了 config 表在用的键已启用", scope, tpl.Id)
}
if changed {
svc.Updatetime = now
var err error
if exists {
err = postgres.Save(comm.TableSvcConfig, svc)
} else {
err = postgres.Insert(comm.TableSvcConfig, svc)
}
if err != nil {
return nil, err
}
log.Infof("console.appparams: 应用 %s 服务 %s %s(承接 config 键 %d 个)", scope, tpl.Id, map[bool]string{true: "已补字段", false: "已建"}[exists], len(done))
}
return done, nil
}
// findBuiltinTemplate 按 id 取内置模板定义(代码里的,不查库——模板表里的可能被运营改过)。
func findBuiltinTemplate(id string) *ThirdSvcTemplate {
for _, t := range builtinSvcTemplates() {
if t.Id == id {
tt := t
return &tt
}
}
return nil
}
// cleanAppParamRows 删已迁走的与确认死掉的行;服务端自用的留下;不认识的留下并告警。
func cleanAppParamRows(deployName string, db mysql.ISys, items []*pb.DBAppConfigItem, migrated map[string]bool) {
serverKeys := appCfgServerKeys()
dropIds := make([]uint64, 0)
dropKeys := make([]string, 0)
unknown := make([]string, 0)
for _, it := range items {
k, _ := normalizeAppCfgRow(it)
if k == "" {
continue
}
reason := ""
switch {
case migrated[k]:
reason = "已迁入 svc_config"
case appCfgDeadKeys[k] != "":
reason = appCfgDeadKeys[k]
case serverKeys[k]:
continue
default:
unknown = append(unknown, k)
continue
}
dropIds = append(dropIds, it.Id)
dropKeys = append(dropKeys, k+"("+reason+")")
}
if len(dropIds) > 0 {
if err := db.Delete(comm.TableAppConfig, "id IN ?", dropIds); err != nil {
log.Warnf("console.appparams: 部署 %s 清理 config 表失败: %v", deployName, err)
} else {
log.Infof("console.appparams: 部署 %s 的 config 表清掉 %d 行:%s", deployName, len(dropKeys), strings.Join(dropKeys, ", "))
}
}
if len(unknown) > 0 {
log.Warnf("console.appparams: 部署 %s 的 config 表还有 %d 个后台已无入口、也不再下发的键:%s——"+
"要么加进 migrate_appparams.go 的落点表,要么手工删", deployName, len(unknown), strings.Join(unknown, ", "))
}
}