# 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` - [ ] **Step 1: 在 Channel 结构体 Name 字段后添加 PublicName** 当前代码(`model/channel.go:28`): ```go Name string `json:"name" gorm:"index"` Weight *uint `json:"weight" gorm:"default:0"` ``` 改为: ```go Name string `json:"name" gorm:"index"` PublicName string `json:"public_name" gorm:"size:255;default:''"` Weight *uint `json:"weight" gorm:"default:0"` ``` - [ ] **Step 2: 验证编译通过** Run: `cd D:/code/new-api && go build ./model/...` Expected: 编译成功,无错误 - [ ] **Step 3: Commit** ```bash 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` - [ ] **Step 1: 修改 Select 列** 当前代码(`model/channel.go:282`): ```go err := DB.Select("id, name, type, remark"). ``` 改为: ```go err := DB.Select("id, name, public_name, type, remark"). ``` - [ ] **Step 2: 验证编译通过** Run: `cd D:/code/new-api && go build ./model/...` Expected: 编译成功 - [ ] **Step 3: Commit** ```bash 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) - [ ] **Step 1: 结构体添加 ChannelPublicName 字段** 当前代码(`model/channel_pricing.go:254`): ```go ChannelName string `json:"channel_name"` ChannelType int `json:"channel_type"` ``` 改为: ```go ChannelName string `json:"channel_name"` ChannelPublicName string `json:"channel_public_name"` ChannelType int `json:"channel_type"` ``` - [ ] **Step 2: SELECT 子句添加 channels.public_name** 当前代码(`model/channel_pricing.go:299`): ```go Select(`abilities.channel_id, channels.name as channel_name, channels.type as channel_type, ``` 改为: ```go Select(`abilities.channel_id, channels.name as channel_name, channels.public_name as channel_public_name, channels.type as channel_type, ``` - [ ] **Step 3: GROUP BY 子句添加 channels.public_name** 当前代码(`model/channel_pricing.go:319`): ```go Group("abilities.channel_id, channels.name, channels.type, channel_pricings.quota_type, ``` 改为: ```go Group("abilities.channel_id, channels.name, channels.public_name, channels.type, channel_pricings.quota_type, ``` 注意:`channels.public_name` 插入在 `channels.name` 后面。 - [ ] **Step 4: 验证编译通过** Run: `cd D:/code/new-api && go build ./model/...` Expected: 编译成功 - [ ] **Step 5: Commit** ```bash 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` - [ ] **Step 1: 扩展匿名结构体** 当前代码(`model/pricing.go:290-293`): ```go var allCPs []struct { ChannelPricing ChannelName string } ``` 改为: ```go var allCPs []struct { ChannelPricing ChannelName string ChannelPublicName string } ``` - [ ] **Step 2: 扩展 Select 子句** 当前代码(`model/pricing.go:295`): ```go Select("channel_pricings.*, channels.name as channel_name"). ``` 改为: ```go Select("channel_pricings.*, channels.name as channel_name, channels.public_name as channel_public_name"). ``` - [ ] **Step 3: 修改 channelNameMap 构建逻辑** 当前代码(`model/pricing.go:303`): ```go channelNameMap[allCPs[i].ChannelId] = allCPs[i].ChannelName ``` 改为: ```go name := allCPs[i].ChannelPublicName if name == "" { name = allCPs[i].ChannelName } channelNameMap[allCPs[i].ChannelId] = name ``` - [ ] **Step 4: 验证编译通过** Run: `cd D:/code/new-api && go build ./model/...` Expected: 编译成功 - [ ] **Step 5: Commit** ```bash 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` (函数定义位置) - [ ] **Step 1: 在 migrateDB 的 return nil 之前插入调用** 当前代码(`model/main.go:304-308`): ```go // 将现有 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 } ``` 改为: ```go // 将现有 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 } ``` - [ ] **Step 2: 在文件末尾(第 696 行后)添加迁移函数** ```go 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,无需额外导入 - [ ] **Step 3: 验证编译通过** Run: `cd D:/code/new-api && go build ./model/...` Expected: 编译成功 - [ ] **Step 4: Commit** ```bash 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 返回) - [ ] **Step 1: 在 validateChannel 的 isAdd 块中添加 public_name 校验** 当前代码(`controller/channel.go:582-585`): ```go if isAdd { if channel == nil || channel.Key == "" { return fmt.Errorf("channel cannot be empty") } // 检查模型名称长度是否超过 255 ``` 改为: ```go 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 ``` **说明:** 保持英文错误消息与现有代码一致。 - [ ] **Step 2: 在 GetUserChannelsForBinding 返回值中添加 public_name** 当前代码(`controller/channel.go:2110-2115`): ```go result = append(result, gin.H{ "id": ch.Id, "name": ch.Name, "type": ch.Type, "remark": ch.Remark, }) ``` 改为: ```go result = append(result, gin.H{ "id": ch.Id, "name": ch.Name, "public_name": ch.PublicName, "type": ch.Type, "remark": ch.Remark, }) ``` - [ ] **Step 3: 验证编译通过** Run: `cd D:/code/new-api && go build ./controller/...` Expected: 编译成功 - [ ] **Step 4: Commit** ```bash 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` - [ ] **Step 1: zh-CN.json 添加翻译** 在 `"请为渠道命名"` 条目(第 2454 行)之后添加: ```json "对外名称": "对外名称", "用户看到的渠道名称,如"标准通道"、"高速通道"": "用户看到的渠道名称,如"标准通道"、"高速通道"", "请填写对外名称": "请填写对外名称", "请填写渠道名称、对外名称和渠道密钥!": "请填写渠道名称、对外名称和渠道密钥!", ``` 注意:zh-CN.json 的 key 和 value 相同(中文 → 中文)。 - [ ] **Step 2: en.json 添加翻译** 在 `"Please name the channel"` 条目(第 2473 行)之后添加: ```json "对外名称": "Public Name", "用户看到的渠道名称,如"标准通道"、"高速通道"": "User-facing channel name, e.g. \"Standard\", \"Fast\"", "请填写对外名称": "Please enter a public name", "请填写渠道名称、对外名称和渠道密钥!": "Please enter channel name, public name and key!", ``` - [ ] **Step 3: Commit** ```bash 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` (提交校验) - [ ] **Step 1: originInputs 添加 public_name 默认值** 当前代码(第 142-143 行): ```javascript const originInputs = { name: '', type: 1, ``` 改为: ```javascript const originInputs = { name: '', public_name: '', type: 1, ``` - [ ] **Step 2: 在 name 的 Form.Input 后添加 public_name 输入框** 当前代码(第 1972-1980 行): ```jsx handleInputChange('name', value)} autoComplete='new-password' /> {inputs.type === 33 && ( ``` 在第 1980 行 `/>` 和第 1982 行 `{inputs.type === 33` 之间插入: ```jsx handleInputChange('name', value)} autoComplete='new-password' /> handleInputChange('public_name', value)} autoComplete='new-password' /> {inputs.type === 33 && ( ``` **说明:** `!isEdit` 条件确保新建时必填、编辑时允许清空。 - [ ] **Step 3: 修改提交前校验** 当前代码(第 1329-1332 行): ```javascript if (!isEdit && (!localInputs.name || !localInputs.key)) { showInfo(t('请填写渠道名称和渠道密钥!')); return; } ``` 改为: ```javascript if (!isEdit && (!localInputs.name || !localInputs.public_name || !localInputs.key)) { showInfo(t('请填写渠道名称、对外名称和渠道密钥!')); return; } ``` - [ ] **Step 4: Commit** ```bash 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` - [ ] **Step 1: 修改 channelName 赋值逻辑** 当前代码(第 104-108 行): ```javascript const tableData = channelPricingData.map((item, index) => ({ key: item.channel_id || index, channelId: item.channel_id, channelName: `通道${['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] || ` ${index + 1}`}`, channelTags: item.tags || [], ``` 改为: ```javascript 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`,为空时回退到中文数字。同时修复原代码反引号嵌套语法问题。 - [ ] **Step 2: Commit** ```bash git add web/src/components/table/model-pricing/modal/components/ChannelPricingCard.jsx git commit -m "feat: display public_name in ChannelPricingCard" ``` --- ### Task 10: 端到端验证 **Files:** 无代码修改,纯验证 - [ ] **Step 1: 后端编译** Run: `cd D:/code/new-api && go build -o new-api main.go` Expected: 编译成功 - [ ] **Step 2: 启动服务** Run: `cd D:/code/new-api && go run main.go` Expected: 日志中出现 `[Migration] migrateChannelPublicName: backfilled N channels` - [ ] **Step 3: 再次重启确认幂等** 重启服务后 Expected: 日志中不再出现 backfilled(RowsAffected=0) - [ ] **Step 4: API 测试 — 创建渠道不带 public_name** Run: ```bash 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"` - [ ] **Step 5: API 测试 — 创建渠道带 public_name** Run: ```bash 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: 返回成功 - [ ] **Step 6: API 测试 — 定价接口包含 public_name** Run: ```bash curl -s http://localhost:3000/api/channel-pricing/model/gpt-4 ``` Expected: 响应中包含 `channel_public_name` 字段 - [ ] **Step 7: 前端验证** 在浏览器中依次验证: 1. 渠道管理 → 新建渠道 → 可见"对外名称"输入框 2. 不填"对外名称"提交 → 显示验证错误 3. 填写后提交成功 4. 编辑渠道 → 可见已保存的对外名称 5. 编辑时清空对外名称并保存 → 成功(向后兼容) 6. 模型详情侧边栏 → ChannelPricingCard 显示对外名称 7. 首页定价卡片 → "默认通道"标签显示对外名称 8. 未设 public_name 的渠道 → 显示"通道一/二/三..."