# 模型渠道管理 实施计划 > **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:** 为 Neta-monorepo 新增集中式 LLM 模型渠道管理,改造 Agent/Skill 编辑页为渠道级联选择器,桥接运行时 provider:model 机制。 **Architecture:** 后端新增 `netaclaw_model_channel` 表 + Entity/Service/Controller(参照 AI_flow 实现),前端新增模型渠道管理页 + 级联选择器组件,改造现有 Agent 编辑页和 Skill 模型配置组件。运行时在执行前查询渠道表透明获取凭证。 **Tech Stack:** Midway.js 3.20 + TypeORM + Vue 3 + Element Plus + TypeScript --- ## 文件结构 ### 新增文件 | 文件 | 职责 | |------|------| | `packages/backend/src/modules/netaclaw/entity/model_channel.ts` | TypeORM Entity,映射 `netaclaw_model_channel` 表 | | `packages/backend/src/modules/netaclaw/service/model_channel.ts` | 业务逻辑:allModels、testConnection、resolveForAgent | | `packages/backend/src/modules/netaclaw/controller/admin/model_channel.ts` | Admin API:CRUD + allModels + testConnection | | `packages/frontend/src/modules/agent/views/model-channel.vue` | 模型渠道管理页面 | | `packages/frontend/src/modules/agent/components/model-channel-selector.vue` | 渠道级联选择器组件 | ### 修改文件 | 文件 | 改动 | |------|------| | `packages/backend/src/entities.ts` | 新增 model_channel Entity 导入 | | `packages/backend/src/modules/netaclaw/runtime/agent.ts` | 接入渠道解析逻辑 | | `packages/frontend/src/modules/agent/config.ts` | 新增 model-channel 路由 | | `packages/frontend/src/modules/agent/views/agent-edit.vue` | 模型配置 Tab 改为级联选择器 | | `packages/frontend/src/modules/agent/components/skill-model.vue` | 改为级联选择器 | --- ## Task 1: 后端 Entity **Files:** - Create: `packages/backend/src/modules/netaclaw/entity/model_channel.ts` - Modify: `packages/backend/src/entities.ts` - [ ] **Step 1: 创建 model_channel Entity 文件** ```typescript // packages/backend/src/modules/netaclaw/entity/model_channel.ts import { BaseEntity } from '../../base/entity/base.js'; import { Column, Entity, Index } from 'typeorm'; /** * 模型渠道配置表 - 集中管理 LLM 供应商渠道 * 一个渠道对应一组 baseUrl + apiKey,可包含多个模型 */ @Entity('netaclaw_model_channel') export class NetaClawModelChannelEntity extends BaseEntity { @Index({ unique: true }) @Column({ comment: '渠道名称', length: 100 }) name: string; @Index() @Column({ comment: '供应商类型', length: 50 }) supplier: string; @Column({ comment: 'API基础地址', length: 500 }) baseUrl: string; @Column({ comment: 'API密钥', length: 500, nullable: true }) apiKey: string; @Column({ comment: '可用模型列表 [{name,capability}]', type: 'json', nullable: true, }) models: { name: string; capability: string }[]; @Column({ comment: '描述', type: 'text', nullable: true }) description: string; @Index() @Column({ comment: '状态 0=禁用 1=启用', default: 1 }) status: number; } ``` - [ ] **Step 2: 注册 Entity 到 entities.ts** 在 `packages/backend/src/entities.ts` 文件末尾的 import 区域和数组中添加: 在最后一行 import(`import * as entity34 from './modules/netaclaw/entity/skill';`)之后添加: ```typescript import * as entity35 from './modules/netaclaw/entity/model_channel'; ``` 在 `entities` 数组末尾(`...Object.values(entity34),` 之后)添加: ```typescript ...Object.values(entity35), ``` - [ ] **Step 3: 启动后端验证表自动创建** Run: `cd packages/backend && npx midway-bin dev` Expected: 服务启动成功,MySQL 中自动创建 `netaclaw_model_channel` 表(因为 config.local.ts 中 `synchronize: true`) - [ ] **Step 4: 提交** ```bash git add packages/backend/src/modules/netaclaw/entity/model_channel.ts packages/backend/src/entities.ts git commit -m "feat(backend): 新增 netaclaw_model_channel Entity" ``` --- ## Task 2: 后端 Service **Files:** - Create: `packages/backend/src/modules/netaclaw/service/model_channel.ts` - [ ] **Step 1: 创建 model_channel Service** ```typescript // packages/backend/src/modules/netaclaw/service/model_channel.ts import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository, Equal, Like } from 'typeorm'; import { NetaClawModelChannelEntity } from '../entity/model_channel.js'; /** * 模型渠道管理服务 */ @Provide() @Scope(ScopeEnum.Singleton) export class NetaClawModelChannelService { @InjectEntityModel(NetaClawModelChannelEntity) channelRepo: Repository; /** * 标准化 models 字段(兼容旧的字符串数组格式) */ private normalizeModels( models: any[] ): { name: string; capability: string }[] { if (!Array.isArray(models)) return []; return models.map(m => { if (typeof m === 'string') return { name: m, capability: 'text' }; return { name: m.name || '', capability: m.capability || 'text' }; }); } /** 分页查询 */ async page(params: { page?: number; size?: number; keyWord?: string; supplier?: string; }) { const { page = 1, size = 20, keyWord, supplier } = params; const where: any = {}; if (keyWord) where.name = Like(`%${keyWord}%`); if (supplier) where.supplier = Equal(supplier); const [list, total] = await this.channelRepo.findAndCount({ where, order: { id: 'DESC' }, skip: (page - 1) * size, take: size, }); return { list, pagination: { page, size, total } }; } /** 新增 */ async add(data: Partial) { const entity = this.channelRepo.create(data); return this.channelRepo.save(entity); } /** 更新 */ async update(data: Partial) { await this.channelRepo.save(data); } /** 删除 */ async delete(ids: number[]) { await this.channelRepo.delete(ids); } /** 详情 */ async info(id: number) { return this.channelRepo.findOneBy({ id }); } /** * 获取所有启用渠道的模型列表(供 Agent/Skill 选择用) */ async allModels() { const channels = await this.channelRepo.find({ where: { status: Equal(1) }, order: { createTime: 'DESC' }, }); return channels.map(ch => ({ channelId: ch.id, channelName: ch.name, supplier: ch.supplier, baseUrl: ch.baseUrl, apiKey: ch.apiKey, models: this.normalizeModels(ch.models), })); } /** * 测试渠道连通性 */ async testConnection(id: number) { const channel = await this.channelRepo.findOneBy({ id: Equal(id) }); if (!channel) throw new Error('渠道不存在'); const models = this.normalizeModels(channel.models); if (models.length === 0) throw new Error('渠道未配置模型'); // 使用 Neta 的 Provider 机制测试 const { parseModelRef, getProvider, initDefaultProviders } = await import( '../runtime/model_selection.js' ); initDefaultProviders(); const supplierToProvider: Record = { openai: 'openai', anthropic: 'anthropic', deepseek: 'deepseek', zhipu: 'openai', tongyi: 'openai', minimax: 'openai', volcengine: 'openai', ollama: 'openai', azure: 'openai', }; const providerName = supplierToProvider[channel.supplier] || 'openai'; const startTime = Date.now(); try { const provider = getProvider(providerName); await provider.chat({ model: models[0].name, apiKey: channel.apiKey, baseUrl: channel.baseUrl, messages: [{ role: 'user', content: 'hi' }], maxTokens: 5, }); const elapsed = Date.now() - startTime; return { success: true, elapsed, message: `连接成功,耗时 ${elapsed}ms` }; } catch (e: any) { const elapsed = Date.now() - startTime; return { success: false, elapsed, message: `连接失败: ${e.message || e}`, }; } } /** * 为 Agent 运行时解析渠道凭证 * 将 channelId + modelId 转换为 provider:model + apiKey + baseUrl */ async resolveForAgent( channelId: number, modelId: string ): Promise<{ provider: string; model: string; apiKey: string; baseUrl?: string; }> { const channel = await this.channelRepo.findOneBy({ id: Equal(channelId) }); if (!channel) throw new Error(`渠道 ${channelId} 不存在`); if (channel.status !== 1) throw new Error(`渠道 ${channel.name} 已禁用`); const supplierToProvider: Record = { openai: 'openai', anthropic: 'anthropic', deepseek: 'deepseek', zhipu: 'openai', tongyi: 'openai', minimax: 'openai', volcengine: 'openai', ollama: 'openai', azure: 'openai', }; return { provider: supplierToProvider[channel.supplier] || 'openai', model: modelId, apiKey: channel.apiKey, baseUrl: channel.baseUrl, }; } } ``` - [ ] **Step 2: 提交** ```bash git add packages/backend/src/modules/netaclaw/service/model_channel.ts git commit -m "feat(backend): 新增 NetaClawModelChannelService" ``` --- ## Task 3: 后端 Controller **Files:** - Create: `packages/backend/src/modules/netaclaw/controller/admin/model_channel.ts` - [ ] **Step 1: 创建 Admin Controller** ```typescript // packages/backend/src/modules/netaclaw/controller/admin/model_channel.ts import { Provide, Inject, Post, Get, Body, Query, Controller } from '@midwayjs/core'; import { Context } from '@midwayjs/koa'; import { NetaClawModelChannelService } from '../../service/model_channel.js'; @Provide() @Controller('/admin/netaclaw/model_channel') export class NetaClawModelChannelAdminController { @Inject() ctx: Context; @Inject() channelService: NetaClawModelChannelService; @Post('/page') async page( @Body() body: { page?: number; size?: number; keyWord?: string; supplier?: string } ) { return { code: 1000, data: await this.channelService.page(body) }; } @Post('/add') async add(@Body() body: any) { const result = await this.channelService.add(body); return { code: 1000, data: result }; } @Post('/update') async update(@Body() body: any) { await this.channelService.update(body); return { code: 1000, message: 'success' }; } @Post('/delete') async delete(@Body() body: { ids: number[] }) { await this.channelService.delete(body.ids); return { code: 1000, message: 'success' }; } @Get('/info') async info(@Query('id') id: number) { return { code: 1000, data: await this.channelService.info(id) }; } @Get('/allModels') async allModels() { return { code: 1000, data: await this.channelService.allModels() }; } @Post('/testConnection') async testConnection(@Body('id') id: number) { return { code: 1000, data: await this.channelService.testConnection(id) }; } } ``` - [ ] **Step 2: 启动后端验证 API 可达** Run: `cd packages/backend && npx midway-bin dev` 验证: `curl -X POST http://localhost:8003/admin/netaclaw/model_channel/page -H 'Content-Type: application/json' -d '{"page":1,"size":20}'` Expected: 返回 `{"code":1000,"data":{"list":[],"pagination":{"page":1,"size":20,"total":0}}}` - [ ] **Step 3: 提交** ```bash git add packages/backend/src/modules/netaclaw/controller/admin/model_channel.ts git commit -m "feat(backend): 新增模型渠道管理 Admin Controller" ``` --- ## Task 4: 插入系统参数 **Files:** - 数据操作: `base_sys_param` 表 - [ ] **Step 1: 通过 MCP 工具插入 model_suppliers 参数** 在 `base_sys_param` 表中插入: ```sql INSERT INTO base_sys_param (keyName, name, data, dataType, remark, createTime, updateTime) VALUES ( 'model_suppliers', 'AI模型供应商', '[{"value":"openai","label":"OpenAI","tagType":""},{"value":"anthropic","label":"Anthropic","tagType":"purple"},{"value":"deepseek","label":"DeepSeek","tagType":"success"},{"value":"zhipu","label":"智谱AI","tagType":"success"},{"value":"tongyi","label":"通义千问","tagType":"warning"},{"value":"minimax","label":"MiniMax","tagType":"danger"},{"value":"volcengine","label":"火山引擎","tagType":"warning"},{"value":"ollama","label":"Ollama","tagType":"info"},{"value":"azure","label":"Azure OpenAI","tagType":""}]', 0, 'AI模型渠道管理用,tagType控制前端标签颜色', NOW(), NOW() ); ``` - [ ] **Step 2: 插入 model_capabilities 参数** ```sql INSERT INTO base_sys_param (keyName, name, data, dataType, remark, createTime, updateTime) VALUES ( 'model_capabilities', '模型能力类型', '[{"value":"text","label":"纯文本"},{"value":"multimodal","label":"多模态"},{"value":"vision","label":"视觉"},{"value":"image_gen","label":"文生图"},{"value":"tts","label":"语音合成"},{"value":"stt","label":"语音识别"}]', 0, 'AI模型能力分类,可在参数列表页面自由扩展', NOW(), NOW() ); ``` - [ ] **Step 3: 验证参数可读取** 通过后端 API 验证:`curl http://localhost:8003/admin/base/sys/param/dataByKey?key=model_suppliers` Expected: 返回 JSON 数组 --- ## Task 5: 前端路由配置 **Files:** - Modify: `packages/frontend/src/modules/agent/config.ts` - [ ] **Step 1: 添加 model-channel 路由** 在 `packages/frontend/src/modules/agent/config.ts` 的 `views` 数组中,在 `detection-result` 条目之前添加: ```typescript { path: '/agent/model-channel', meta: { label: '模型管理' }, component: () => import('./views/model-channel.vue') }, ``` 修改后的完整 views 数组: ```typescript views: [ { path: '/agent/chat', meta: { label: 'Agent 对话' }, component: () => import('./views/chat.vue') }, { path: '/agent/agents', meta: { label: 'Agent 管理' }, component: () => import('./views/agent-list.vue') }, { path: '/agent/skills', meta: { label: 'Skill 管理' }, component: () => import('./views/skills.vue') }, { path: '/agent/model-channel', meta: { label: '模型管理' }, component: () => import('./views/model-channel.vue') }, { path: '/agent/detection-result', meta: { label: '检测结果' }, component: () => import('./views/detection-result.vue') } ] ``` - [ ] **Step 2: 提交** ```bash git add packages/frontend/src/modules/agent/config.ts git commit -m "feat(frontend): 添加模型管理路由" ``` --- ## Task 6: 前端模型渠道管理页面 **Files:** - Create: `packages/frontend/src/modules/agent/views/model-channel.vue` - [ ] **Step 1: 创建模型渠道管理页面** 从 AI_flow 的 `model-channel.vue` 适配(端口 8003,API 路径 `/admin/netaclaw/model_channel/`)。 创建文件 `packages/frontend/src/modules/agent/views/model-channel.vue`,内容参照 AI_flow 的 `AI_flow_FronEnd/src/modules/agent/views/model-channel.vue`,做以下适配: 1. API 路径从 `/admin/agent/model_channel/` 改为 `/admin/netaclaw/model_channel/` 2. 供应商参数加载端点不变(`/admin/base/sys/param/dataByKey?key=model_suppliers`) 3. 能力类型参数加载端点不变(`/admin/base/sys/param/dataByKey?key=model_capabilities`) 4. `supplierTagType()` 函数改为从 `supplierOptions` 中读取 `tagType` 字段,而非硬编码映射 关键改动点——`supplierTagType` 函数: ```typescript // 旧(AI_flow 硬编码): function supplierTagType(val: string): '' | 'success' | 'warning' | 'danger' | 'info' { const map: Record = { openai: '', zhipu: 'success', tongyi: 'warning', minimax: 'danger', ollama: 'info', deepseek: 'success', azure: '', volcengine: 'warning' }; return map[val] || 'info'; } // 新(从参数读取): function supplierTagType(val: string): '' | 'success' | 'warning' | 'danger' | 'info' { const found = supplierOptions.value.find(s => s.value === val); return (found as any)?.tagType || 'info'; } ``` 其余代码(模板、筛选、表格、抽屉、批量导入、分页、样式)与 AI_flow 基本一致,只改 API 路径前缀。 - [ ] **Step 2: 启动前端验证页面渲染** Run: `cd packages/frontend && pnpm dev` 在浏览器访问 `http://localhost:9001`,登录后导航到「Agent管理 > 模型管理」,验证: - 页面正常渲染 - 新增渠道抽屉可打开 - 供应商下拉选项从参数表加载 - [ ] **Step 3: 提交** ```bash git add packages/frontend/src/modules/agent/views/model-channel.vue git commit -m "feat(frontend): 新增模型渠道管理页面" ``` --- ## Task 7: 前端渠道选择器组件 **Files:** - Create: `packages/frontend/src/modules/agent/components/model-channel-selector.vue` - [ ] **Step 1: 创建渠道级联选择器** 从 AI_flow 的 `model-channel-selector.vue` 适配,API 路径改为 `/admin/netaclaw/model_channel/allModels`。 创建文件 `packages/frontend/src/modules/agent/components/model-channel-selector.vue`,内容参照 AI_flow 的 `AI_flow_FronEnd/src/modules/agent/components/model-channel-selector.vue`,做以下适配: 1. `loadChannels()` 中 API 路径从 `/admin/agent/model_channel/allModels` 改为 `/admin/netaclaw/model_channel/allModels` 2. 其余逻辑(v-model 双向绑定、capabilityFilter、渠道筛选、模型选择)保持一致 3. 新增「自定义配置」模式:当用户选择 `channelId=0` 时,展示 apiUrl/apiKey/modelId 手动输入表单 在 `` 之后,添加自定义配置表单: ```html ``` Script 中添加 customConfig 响应式数据和 onCustomChange 方法: ```typescript const customConfig = ref({ apiUrl: '', apiKey: '', modelId: '' }); function onCustomChange() { const val: ModelValue = { channelId: 0, modelId: customConfig.value.modelId, apiUrl: customConfig.value.apiUrl, apiKey: customConfig.value.apiKey, }; emit('update:modelValue', val); emit('change', val); } // 在 onChannelChange 中,channelId===0 时触发 custom 模式 function onChannelChange(channelId: number) { selectedModelId.value = ''; if (channelId === 0) { // 回填已有自定义配置 if (props.modelValue?.channelId === 0) { customConfig.value = { apiUrl: props.modelValue.apiUrl || '', apiKey: props.modelValue.apiKey || '', modelId: props.modelValue.modelId || '', }; } emit('update:modelValue', { channelId: 0, ...customConfig.value }); emit('change', { channelId: 0, ...customConfig.value }); } } ``` - [ ] **Step 2: 提交** ```bash git add packages/frontend/src/modules/agent/components/model-channel-selector.vue git commit -m "feat(frontend): 新增模型渠道级联选择器组件" ``` --- ## Task 8: 改造 Agent 编辑页 **Files:** - Modify: `packages/frontend/src/modules/agent/views/agent-edit.vue:76-115` - [ ] **Step 1: 导入选择器组件** 在 `agent-edit.vue` 的 `