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.
 
 
 

15 KiB

Channel PublicName 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 为 Channel 添加 public_name 字段,让管理员可以为每个渠道设置用户可见的友好名称,替代前端硬编码的"通道一/二/三”。

Architecture: 后端 Channel 模型新增字段 → AutoMigrate 自动建列 → 迁移函数回填数据 → 定价查询和 API 返回公共名称 → 前端表单支持编辑 → 展示层使用公共名称。

Tech Stack: Go (GORM) / React (Semi Design) / SQLite+MySQL+PostgreSQL


Task 1: Channel 模型添加 PublicName 字段

Files:

  • Modify: model/channel.go:28

当前代码(model/channel.go:28):

Name               string  `json:"name" gorm:"index"`
Weight             *uint   `json:"weight" gorm:"default:0"`

改为:

Name               string  `json:"name" gorm:"index"`
PublicName         string  `json:"public_name" gorm:"size:255;default:''"`
Weight             *uint   `json:"weight" gorm:"default:0"`

Run: cd D:/code/new-api && go build ./model/... Expected: 编译成功,无错误

git add model/channel.go
git commit -m "feat: add PublicName field to Channel model"

Task 2: 修改 GetAllChannelsForBinding 查询包含 public_name

Files:

  • Modify: model/channel.go:282

当前代码(model/channel.go:282):

err := DB.Select("id, name, type, remark").

改为:

err := DB.Select("id, name, public_name, type, remark").

Run: cd D:/code/new-api && go build ./model/... Expected: 编译成功

git add model/channel.go
git commit -m "feat: include public_name in GetAllChannelsForBinding query"

Task 3: ChannelPricingWithChannel 扩展 + SQL 查询修改

Files:

  • Modify: model/channel_pricing.go:254 (结构体)

  • Modify: model/channel_pricing.go:299 (SELECT)

  • Modify: model/channel_pricing.go:319 (GROUP BY)

当前代码(model/channel_pricing.go:254):

ChannelName      string        `json:"channel_name"`
ChannelType      int           `json:"channel_type"`

改为:

ChannelName      string        `json:"channel_name"`
ChannelPublicName string       `json:"channel_public_name"`
ChannelType      int           `json:"channel_type"`

当前代码(model/channel_pricing.go:299):

Select(`abilities.channel_id, channels.name as channel_name, channels.type as channel_type,

改为:

Select(`abilities.channel_id, channels.name as channel_name, channels.public_name as channel_public_name, channels.type as channel_type,

当前代码(model/channel_pricing.go:319):

Group("abilities.channel_id, channels.name, channels.type, channel_pricings.quota_type,

改为:

Group("abilities.channel_id, channels.name, channels.public_name, channels.type, channel_pricings.quota_type,

注意:channels.public_name 插入在 channels.name 后面。

Run: cd D:/code/new-api && go build ./model/... Expected: 编译成功

git add model/channel_pricing.go
git commit -m "feat: add public_name to channel pricing query"

Task 4: pricing.go 默认通道名称使用 public_name

Files:

  • Modify: model/pricing.go:290-303

当前代码(model/pricing.go:290-293):

var allCPs []struct {
    ChannelPricing
    ChannelName string
}

改为:

var allCPs []struct {
    ChannelPricing
    ChannelName       string
    ChannelPublicName string
}

当前代码(model/pricing.go:295):

Select("channel_pricings.*, channels.name as channel_name").

改为:

Select("channel_pricings.*, channels.name as channel_name, channels.public_name as channel_public_name").

当前代码(model/pricing.go:303):

channelNameMap[allCPs[i].ChannelId] = allCPs[i].ChannelName

改为:

name := allCPs[i].ChannelPublicName
if name == "" {
    name = allCPs[i].ChannelName
}
channelNameMap[allCPs[i].ChannelId] = name

Run: cd D:/code/new-api && go build ./model/... Expected: 编译成功

git add model/pricing.go
git commit -m "feat: use public_name as default channel display name"

Task 5: 数据回填迁移函数

Files:

  • Modify: model/main.go:306-307 (调用位置)

  • Modify: model/main.go:695 (函数定义位置)

当前代码(model/main.go:304-308):

// 将现有 sort_order=0 的模型和供应商更新为默认大数
DB.Model(&Model{}).Where("sort_order = 0").Update("sort_order", 999999)
DB.Model(&Vendor{}).Where("sort_order = 0").Update("sort_order", 999999)
return nil
}

改为:

// 将现有 sort_order=0 的模型和供应商更新为默认大数
DB.Model(&Model{}).Where("sort_order = 0").Update("sort_order", 999999)
DB.Model(&Vendor{}).Where("sort_order = 0").Update("sort_order", 999999)

migrateChannelPublicName()

return nil
}
func migrateChannelPublicName() {
	result := DB.Model(&Channel{}).
		Where("public_name = '' OR public_name IS NULL").
		Update("public_name", gorm.Expr("name"))
	if result.Error != nil {
		common.SysError("[Migration] migrateChannelPublicName failed: " + result.Error.Error())
	} else if result.RowsAffected > 0 {
		common.SysLog(fmt.Sprintf("[Migration] migrateChannelPublicName: backfilled %d channels", result.RowsAffected))
	}
}

说明:

  • AutoMigrate(migrateDB() 第 262 行 &Channel{})会先创建列

  • WHERE 条件保证幂等

  • gorm.Expr("name") 引用列名,SQLite/MySQL/PostgreSQL 通用

  • model/main.go 已有 fmtgormcommon 的 import,无需额外导入

Run: cd D:/code/new-api && go build ./model/... Expected: 编译成功

git add model/main.go
git commit -m "feat: add migration to backfill public_name from name"

Task 6: controller 层验证 + API 返回

Files:

  • Modify: controller/channel.go:582-585 (验证)

  • Modify: controller/channel.go:2110-2115 (API 返回)

当前代码(controller/channel.go:582-585):

if isAdd {
    if channel == nil || channel.Key == "" {
        return fmt.Errorf("channel cannot be empty")
    }

    // 检查模型名称长度是否超过 255

改为:

if isAdd {
    if channel == nil || channel.Key == "" {
        return fmt.Errorf("channel cannot be empty")
    }

    if strings.TrimSpace(channel.PublicName) == "" {
        return fmt.Errorf("public name cannot be empty")
    }

    // 检查模型名称长度是否超过 255

说明: 保持英文错误消息与现有代码一致。

当前代码(controller/channel.go:2110-2115):

result = append(result, gin.H{
    "id":     ch.Id,
    "name":   ch.Name,
    "type":   ch.Type,
    "remark": ch.Remark,
})

改为:

result = append(result, gin.H{
    "id":          ch.Id,
    "name":        ch.Name,
    "public_name": ch.PublicName,
    "type":        ch.Type,
    "remark":      ch.Remark,
})

Run: cd D:/code/new-api && go build ./controller/... Expected: 编译成功

git add controller/channel.go
git commit -m "feat: validate public_name on channel creation and return in binding API"

Task 7: 前端 i18n 翻译

Files:

  • Modify: web/src/i18n/locales/zh-CN.json

  • Modify: web/src/i18n/locales/en.json

"请为渠道命名" 条目(第 2454 行)之后添加:

"对外名称": "对外名称",
"用户看到的渠道名称,如"标准通道"、"高速通道"": "用户看到的渠道名称,如"标准通道"、"高速通道"",
"请填写对外名称": "请填写对外名称",
"请填写渠道名称、对外名称和渠道密钥!": "请填写渠道名称、对外名称和渠道密钥!",

注意:zh-CN.json 的 key 和 value 相同(中文 → 中文)。

"Please name the channel" 条目(第 2473 行)之后添加:

"对外名称": "Public Name",
"用户看到的渠道名称,如"标准通道"、"高速通道"": "User-facing channel name, e.g. \"Standard\", \"Fast\"",
"请填写对外名称": "Please enter a public name",
"请填写渠道名称、对外名称和渠道密钥!": "Please enter channel name, public name and key!",
git add web/src/i18n/locales/zh-CN.json web/src/i18n/locales/en.json
git commit -m "feat: add i18n translations for channel public_name"

Task 8: EditChannelModal 表单添加对外名称字段

Files:

  • Modify: web/src/components/table/channels/modals/EditChannelModal.jsx:142 (originInputs)

  • Modify: web/src/components/table/channels/modals/EditChannelModal.jsx:1980 (表单 UI)

  • Modify: web/src/components/table/channels/modals/EditChannelModal.jsx:1329 (提交校验)

当前代码(第 142-143 行):

const originInputs = {
    name: '',
    type: 1,

改为:

const originInputs = {
    name: '',
    public_name: '',
    type: 1,

当前代码(第 1972-1980 行):

<Form.Input
  field='name'
  label={t('名称')}
  placeholder={t('请为渠道命名')}
  rules={[{ required: true, message: t('请为渠道命名') }]}
  showClear
  onChange={(value) => handleInputChange('name', value)}
  autoComplete='new-password'
/>

{inputs.type === 33 && (

在第 1980 行 /> 和第 1982 行 {inputs.type === 33 之间插入:

<Form.Input
  field='name'
  label={t('名称')}
  placeholder={t('请为渠道命名')}
  rules={[{ required: true, message: t('请为渠道命名') }]}
  showClear
  onChange={(value) => handleInputChange('name', value)}
  autoComplete='new-password'
/>
<Form.Input
  field='public_name'
  label={t('对外名称')}
  placeholder={t('用户看到的渠道名称,如"标准通道"、"高速通道"')}
  rules={!isEdit ? [{ required: true, message: t('请填写对外名称') }] : []}
  showClear
  onChange={(value) => handleInputChange('public_name', value)}
  autoComplete='new-password'
/>

{inputs.type === 33 && (

说明: !isEdit 条件确保新建时必填、编辑时允许清空。

当前代码(第 1329-1332 行):

if (!isEdit && (!localInputs.name || !localInputs.key)) {
  showInfo(t('请填写渠道名称和渠道密钥!'));
  return;
}

改为:

if (!isEdit && (!localInputs.name || !localInputs.public_name || !localInputs.key)) {
  showInfo(t('请填写渠道名称、对外名称和渠道密钥!'));
  return;
}
git add web/src/components/table/channels/modals/EditChannelModal.jsx
git commit -m "feat: add public_name field to channel edit form"

Task 9: ChannelPricingCard 展示公共名称

Files:

  • Modify: web/src/components/table/model-pricing/modal/components/ChannelPricingCard.jsx:107

当前代码(第 104-108 行):

const tableData = channelPricingData.map((item, index) => ({
    key: item.channel_id || index,
    channelId: item.channel_id,
    channelName: `通道${['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] || ` ${index + 1}`}`,
    channelTags: item.tags || [],

改为:

const tableData = channelPricingData.map((item, index) => ({
    key: item.channel_id || index,
    channelId: item.channel_id,
    channelName: item.channel_public_name || ('通道' + (['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] || (index + 1))),
    channelTags: item.tags || [],

说明: 优先使用后端 channel_public_name,为空时回退到中文数字。同时修复原代码反引号嵌套语法问题。

git add web/src/components/table/model-pricing/modal/components/ChannelPricingCard.jsx
git commit -m "feat: display public_name in ChannelPricingCard"

Task 10: 端到端验证

Files: 无代码修改,纯验证

Run: cd D:/code/new-api && go build -o new-api main.go Expected: 编译成功

Run: cd D:/code/new-api && go run main.go Expected: 日志中出现 [Migration] migrateChannelPublicName: backfilled N channels

重启服务后 Expected: 日志中不再出现 backfilled(RowsAffected=0)

Run:

curl -s -X POST http://localhost:3000/api/channel/ \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"single","channel":{"type":1,"name":"测试","key":"sk-test"}}'

Expected: 返回错误 "public name cannot be empty"

Run:

curl -s -X POST http://localhost:3000/api/channel/ \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"single","channel":{"type":1,"name":"测试","public_name":"标准通道","key":"sk-test"}}'

Expected: 返回成功

Run:

curl -s http://localhost:3000/api/channel-pricing/model/gpt-4

Expected: 响应中包含 channel_public_name 字段

在浏览器中依次验证:

  1. 渠道管理 → 新建渠道 → 可见"对外名称"输入框
  2. 不填"对外名称"提交 → 显示验证错误
  3. 填写后提交成功
  4. 编辑渠道 → 可见已保存的对外名称
  5. 编辑时清空对外名称并保存 → 成功(向后兼容)
  6. 模型详情侧边栏 → ChannelPricingCard 显示对外名称
  7. 首页定价卡片 → “默认通道"标签显示对外名称
  8. 未设 public_name 的渠道 → 显示"通道一/二/三...”