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
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"
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"
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"
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"
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 已有 fmt、gorm、common 的 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"
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"
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"
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"
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"
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 字段
在浏览器中依次验证: