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
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, ", "))
|
|
}
|
|
}
|
|
|