Files
ttstd-infinite-canvas/backend/cmd/reseed-logical-model-sources/main.go
tongtongstudio eb18c59a54
Some checks failed
Publish container images / Quality checks (push) Has been cancelled
Publish container images / Resolve build version (push) Has been cancelled
Publish container images / Build open-ai-canvas-backend (amd64) (push) Has been cancelled
Publish container images / Build open-ai-canvas-backend (arm64) (push) Has been cancelled
Publish container images / Build open-ai-canvas-web (multi) (push) Has been cancelled
Publish container images / Publish open-ai-canvas-backend manifest (push) Has been cancelled
Publish container images / Publish open-ai-canvas-web manifest (push) Has been cancelled
Publish container images / Publish release assets (push) Has been cancelled
feat(config): 统一加载 .env 并强化本地启动脚本
2026-09-22 19:42:00 +08:00

168 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"sort"
"strings"
"infinite-canvas/backend/internal/config"
"infinite-canvas/backend/internal/database"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
"infinite-canvas/backend/internal/service"
"gorm.io/gorm"
)
// 此命令把“单条系统渠道线路驱动、但尚未绑定来源”的前台模型规范为系统渠道投影。
// 保留产品名称、排序、说明和旧 ID 映射;能力、默认参数、路由和价格统一从系统模型生成。
type sourcePlan struct {
logicalModel model.LogicalModel
source model.ChannelModel
}
func main() {
config.LoadDotEnv()
apply := flag.Bool("apply", false, "将符合条件的前台模型改为系统渠道模型驱动")
flag.Parse()
db, err := database.Open(database.Config{Driver: "postgres", DSN: os.Getenv("DATABASE_URL"), DataDir: os.Getenv("CANVAS_BACKEND_DATA_DIR")})
if err != nil {
log.Fatal(err)
}
if db.Dialector.Name() != "postgres" {
log.Fatal("前台模型目录刷新只允许连接 PostgreSQL")
}
if err := database.ConfigurePool(db); err != nil {
log.Fatal(err)
}
if err := database.MigrateSchema(db); err != nil {
log.Fatal(err)
}
repo := repository.New(db)
plans, err := buildPlans(repo)
if err != nil {
log.Fatal(err)
}
for _, plan := range plans {
log.Printf("%s: 当前前台模型=%s,系统模型=%s,协议=%s", plan.logicalModel.Code, plan.logicalModel.ID, plan.source.ModelKey, plan.source.Protocol)
}
if !*apply {
log.Print("dry-run 完成;确认后使用 --apply 写入。未匹配单条系统线路的前台模型不会被修改")
return
}
if err := ensureNoActiveTasks(db, plans); err != nil {
log.Fatal(err)
}
actor, err := migrationAdmin(db)
if err != nil {
log.Fatal(err)
}
svc := service.New(repo, os.Getenv("CANVAS_BACKEND_DATA_DIR"))
for _, plan := range plans {
if _, err := svc.SaveAdminLogicalModel(actor, plan.logicalModel.ID, service.LogicalModelRequest{
Code: plan.logicalModel.Code,
Name: plan.logicalModel.Name,
Icon: plan.logicalModel.Icon,
Description: plan.logicalModel.Description,
Capability: plan.source.Capability,
Enabled: plan.logicalModel.Enabled,
SortOrder: plan.logicalModel.SortOrder,
LegacyModelIDs: decodeLegacyModelIDs(plan.logicalModel.LegacyModelIDsJSON),
SourceChannelModelID: plan.source.ID,
}); err != nil {
log.Fatalf("刷新前台模型 %s 失败:%v", plan.logicalModel.Code, err)
}
log.Printf("已刷新前台模型:%s", plan.logicalModel.Code)
}
log.Print("前台模型目录刷新完成:能力、默认参数、路线和价格均以系统渠道模型为准")
}
func buildPlans(repo *repository.Repository) ([]sourcePlan, error) {
items, err := repo.LogicalModels(false)
if err != nil {
return nil, err
}
plans := make([]sourcePlan, 0)
for _, item := range items {
if strings.TrimSpace(item.SourceChannelModelID) != "" {
continue
}
graph, graphErr := repo.LogicalModelGraph(item.ID, false)
if graphErr != nil {
return nil, fmt.Errorf("读取前台模型 %s 的当前线路:%w", item.Code, graphErr)
}
if graph.Revision == nil || len(graph.Routes) != 1 || len(graph.ChannelModels) != 1 {
return nil, fmt.Errorf("前台模型 %s 不是单条有效系统线路,拒绝自动改写", item.Code)
}
route := graph.Routes[0]
source := graph.ChannelModels[0]
if route.ChannelModelID != source.ID {
return nil, fmt.Errorf("前台模型 %s 的当前线路与系统模型不一致", item.Code)
}
channel, channelErr := repo.AdminSystemChannel(source.ChannelID)
if channelErr != nil {
return nil, fmt.Errorf("前台模型 %s 的线路不是系统渠道:%w", item.Code, channelErr)
}
if !channel.Enabled || !source.Enabled || !source.PriceConfigured {
return nil, fmt.Errorf("前台模型 %s 的系统模型未启用或未配置价格", item.Code)
}
plans = append(plans, sourcePlan{logicalModel: item, source: source})
}
sort.Slice(plans, func(i, j int) bool {
return plans[i].logicalModel.Code < plans[j].logicalModel.Code
})
return plans, nil
}
func ensureNoActiveTasks(db *gorm.DB, plans []sourcePlan) error {
ids := make([]string, 0, len(plans))
for _, plan := range plans {
ids = append(ids, plan.logicalModel.ID)
}
if len(ids) == 0 {
return nil
}
var active []struct {
LogicalModelID string
Status string
Count int64
}
if err := db.Model(&model.Task{}).
Select("logical_model_id, status, count(*) AS count").
Where("logical_model_id IN ? AND status IN ?", ids, []model.TaskStatus{model.TaskStatusQueued, model.TaskStatusRunning}).
Group("logical_model_id, status").
Scan(&active).Error; err != nil {
return err
}
if len(active) == 0 {
return nil
}
parts := make([]string, 0, len(active))
for _, item := range active {
parts = append(parts, fmt.Sprintf("%s/%s=%d", item.LogicalModelID, item.Status, item.Count))
}
return fmt.Errorf("存在 queued 或 running 前台模型任务,拒绝刷新:%s", strings.Join(parts, ", "))
}
func migrationAdmin(db *gorm.DB) (*model.User, error) {
var actor model.User
if err := db.Where("role = ?", model.UserRoleAdmin).Order("created_at asc").First(&actor).Error; err != nil {
return nil, fmt.Errorf("需要现有管理员作为迁移审计主体:%w", err)
}
return &actor, nil
}
func decodeLegacyModelIDs(raw string) []string {
var ids []string
if err := json.Unmarshal([]byte(raw), &ids); err != nil {
return []string{}
}
return ids
}