30 KiB
模型渠道管理 实施计划
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 文件
// 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';)之后添加:
import * as entity35 from './modules/netaclaw/entity/model_channel';
在 entities 数组末尾(...Object.values(entity34), 之后)添加:
...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: 提交
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
// 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<NetaClawModelChannelEntity>;
/**
* 标准化 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<NetaClawModelChannelEntity>) {
const entity = this.channelRepo.create(data);
return this.channelRepo.save(entity);
}
/** 更新 */
async update(data: Partial<NetaClawModelChannelEntity>) {
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<string, string> = {
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<string, string> = {
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: 提交
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
// 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: 提交
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 表中插入:
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 参数
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 条目之前添加:
{
path: '/agent/model-channel',
meta: { label: '模型管理' },
component: () => import('./views/model-channel.vue')
},
修改后的完整 views 数组:
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: 提交
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,做以下适配:
- API 路径从
/admin/agent/model_channel/改为/admin/netaclaw/model_channel/ - 供应商参数加载端点不变(
/admin/base/sys/param/dataByKey?key=model_suppliers) - 能力类型参数加载端点不变(
/admin/base/sys/param/dataByKey?key=model_capabilities) supplierTagType()函数改为从supplierOptions中读取tagType字段,而非硬编码映射
关键改动点——supplierTagType 函数:
// 旧(AI_flow 硬编码):
function supplierTagType(val: string): '' | 'success' | 'warning' | 'danger' | 'info' {
const map: Record<string, any> = {
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: 提交
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,做以下适配:
loadChannels()中 API 路径从/admin/agent/model_channel/allModels改为/admin/netaclaw/model_channel/allModels- 其余逻辑(v-model 双向绑定、capabilityFilter、渠道筛选、模型选择)保持一致
- 新增「自定义配置」模式:当用户选择
channelId=0时,展示 apiUrl/apiKey/modelId 手动输入表单
在 <el-form-item v-if="selectedChannelId && selectedChannelId !== 0" label="选择模型"> 之后,添加自定义配置表单:
<!-- 自定义配置(channelId=0) -->
<template v-if="selectedChannelId === 0">
<el-form-item label="API 地址">
<el-input v-model="customConfig.apiUrl" placeholder="https://api.openai.com/v1" />
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="customConfig.apiKey" placeholder="输入 API Key" show-password />
</el-form-item>
<el-form-item label="模型 ID">
<el-input v-model="customConfig.modelId" placeholder="如: gpt-4o" @change="onCustomChange" />
</el-form-item>
</template>
Script 中添加 customConfig 响应式数据和 onCustomChange 方法:
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: 提交
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 的 <script lang="ts" setup> 中,在 import type { SkillMeta } ... 之后添加:
import ModelChannelSelector from '../components/model-channel-selector.vue';
- Step 2: 替换模型配置 Tab 模板
将 agent-edit.vue 第 76-115 行(Tab 4: 模型配置)替换为:
<!-- Tab 4: 模型配置 -->
<el-tab-pane label="模型配置" name="model">
<p class="tab-desc">不配置则使用系统默认LLM:</p>
<model-channel-selector v-model="modelConfig" />
<el-form label-width="100px" style="margin-top: 12px">
<el-form-item label="上下文窗口">
<el-input-number
v-model="modelConfig.contextWindow"
:min="1"
:max="2048"
controls-position="right"
/>
<span class="form-tip">单位: k tokens,如 256 表示 256k</span>
</el-form-item>
<el-form-item>
<el-button size="small" @click="clearModelConfig">清空(使用默认)</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
- Step 3: 更新 modelConfig 数据结构
将 modelConfig ref 改为支持 channelId:
const modelConfig = ref({
channelId: null as number | null,
modelId: '',
apiKey: '',
apiUrl: '',
contextWindow: null as number | null
});
更新 clearModelConfig 函数:
function clearModelConfig() {
modelConfig.value = { channelId: null, modelId: '', apiKey: '', apiUrl: '', contextWindow: null };
}
移除 fillDefaultLLM 函数和 defaultLLM 常量(不再需要)。
- Step 4: 更新 loadAgent 中的 modelConfig 回填
在 loadAgent() 函数中,更新 modelConfig 回填逻辑:
const mc = parseJson(agent.modelConfig, {});
modelConfig.value = {
channelId: mc.channelId || null,
modelId: mc.modelId || '',
apiKey: mc.apiKey || '',
apiUrl: mc.apiUrl || '',
contextWindow: mc.contextWindow || null
};
- Step 5: 更新 handleSave 中的 modelConfig 序列化
在 handleSave() 函数中,更新 mc 判断逻辑:
const mc =
modelConfig.value.channelId || modelConfig.value.modelId || modelConfig.value.contextWindow
? modelConfig.value
: null;
- Step 6: 更新 resetForm 中的 modelConfig
modelConfig.value = { channelId: null, modelId: '', apiKey: '', apiUrl: '', contextWindow: null };
- Step 7: 浏览器验证
访问 Agent 管理 → 编辑任一 Agent → 模型配置 Tab
验证:
-
渠道选择器正常展示
-
选择渠道后模型下拉出现
-
选择「自定义配置」后手动输入框出现
-
保存后重新打开,选择状态正确回填
-
Step 8: 提交
git add packages/frontend/src/modules/agent/views/agent-edit.vue
git commit -m "feat(frontend): Agent编辑页模型配置改为渠道级联选择器"
Task 9: 改造 Skill 模型配置组件
Files:
-
Modify:
packages/frontend/src/modules/agent/components/skill-model.vue -
Step 1: 导入选择器组件
在 skill-model.vue 的 <script lang="ts" setup> 中添加:
import ModelChannelSelector from './model-channel-selector.vue';
- Step 2: 替换「可配置模型」模板
将 skill-model.vue 第 30-104 行(<template v-else> 可配置模型部分)替换为:
<!-- 可配置模型 -->
<template v-else>
<model-channel-selector
v-model="channelModelValue"
:capability-filter="skillCapabilityFilter"
@change="onChannelModelChange"
/>
<!-- 操作按钮 -->
<div class="model-footer">
<el-button
type="primary"
@click="handleSave"
:loading="saving"
>
保存模型配置
</el-button>
<el-button @click="handleReset">恢复默认</el-button>
</div>
</template>
- Step 3: 添加渠道模型相关逻辑
在 script 中添加:
import { computed } from 'vue';
// 渠道选择器的 v-model 值
const channelModelValue = ref<any>({});
// 根据 Skill 类型推断能力过滤
const skillCapabilityFilter = computed(() => {
const t = props.skill?.skillType;
if (t === 'multimodal') return 'multimodal';
if (t === 'llm') return 'text';
return undefined;
});
function onChannelModelChange(val: any) {
channelModelValue.value = val;
}
- Step 4: 更新 loadModelConfig
改写 loadModelConfig 函数,加载已保存的渠道配置:
async function loadModelConfig() {
const name = props.skill?.name;
if (!name || !needsModel.value || fixedModel.value) return;
channelModelValue.value = {};
try {
const resp = await fetch(
`${config.baseUrl}/admin/netaclaw/skill/modelConfig?name=${name}`,
{ headers: { Authorization: user.token || '' } }
);
const data = await resp.json();
if (data.code === 1000 && data.data) {
const saved = data.data;
channelModelValue.value = {
channelId: saved.channelId || null,
modelId: saved.modelId || '',
apiUrl: saved.apiUrl || '',
apiKey: saved.apiKey || '',
};
}
} catch {
// 加载失败用空值
}
}
- Step 5: 更新 handleSave
async function handleSave() {
const val = channelModelValue.value;
if (!val.channelId && !val.modelId) {
ElMessage.warning('请选择渠道和模型,或使用自定义配置');
return;
}
saving.value = true;
try {
const resp = await fetch(`${config.baseUrl}/admin/netaclaw/skill/updateModelConfig`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: user.token || ''
},
body: JSON.stringify({
name: props.skill.name,
model: {
channelId: val.channelId,
modelId: val.modelId,
apiUrl: val.apiUrl,
apiKey: val.apiKey,
}
})
});
const data = await resp.json();
if (data.code === 1000) {
ElMessage.success('模型配置已保存');
} else {
ElMessage.error(data.message || '保存失败');
}
} catch {
ElMessage.error('保存失败');
} finally {
saving.value = false;
}
}
- Step 6: 更新 handleReset
async function handleReset() {
channelModelValue.value = {};
saving.value = true;
try {
const resp = await fetch(`${config.baseUrl}/admin/netaclaw/skill/updateModelConfig`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: user.token || ''
},
body: JSON.stringify({
name: props.skill.name,
model: null
})
});
const data = await resp.json();
if (data.code === 1000) {
ElMessage.success('已清除模型配置');
}
} catch {
// 静默
} finally {
saving.value = false;
}
}
- Step 7: 移除不再需要的代码
移除以下不再需要的代码:
-
presetsref 和PresetModelinterface -
loadPresets()函数 -
applyPreset()函数 -
modelFormref -
DEFAULT_MODEL常量 -
selectedPresetIdref -
preset-section 相关的 CSS 样式
-
Step 8: 浏览器验证
访问 Skill 管理 → 点击任一 Skill 的「配置」按钮 → 模型配置 Tab
验证:
-
渠道选择器正常展示
-
multimodal 类型 Skill 只展示 multimodal 能力的模型
-
保存后重新打开,选择状态正确回填
-
Step 9: 提交
git add packages/frontend/src/modules/agent/components/skill-model.vue
git commit -m "feat(frontend): Skill模型配置改为渠道级联选择器"
Task 10: 后端运行时桥接
Files:
-
Modify:
packages/backend/src/modules/netaclaw/runtime/agent.ts:29-44 -
Step 1: 改造 runAgent 函数
在 packages/backend/src/modules/netaclaw/runtime/agent.ts 中,修改 AgentConfig 接口(第 7-17 行)添加 channelId 支持:
export interface AgentConfig {
name: string;
systemPrompt: string;
model: string; // "provider:model" 或 "model"
apiKey: string;
baseUrl?: string;
channelId?: number; // 新增:渠道ID(有值时覆盖 model/apiKey/baseUrl)
modelId?: string; // 新增:渠道中的模型ID
temperature?: number;
maxTokens?: number;
maxToolRounds?: number;
skills?: string[];
}
- Step 2: 在 runAgent 函数开头添加渠道解析逻辑
在 runAgent 函数中(第 29 行 export async function runAgent 之后),在 const modelRef = parseModelRef(agentConfig.model); 之前,添加渠道解析:
// 渠道解析:如果指定了 channelId,从数据库查询凭证覆盖配置
if (agentConfig.channelId && agentConfig.channelId > 0) {
try {
// 动态导入以避免循环依赖
const { MidwayContainer } = await import('@midwayjs/core');
const { getCurrentApplicationContext } = await import('@midwayjs/core');
const ctx = getCurrentApplicationContext();
const { NetaClawModelChannelService } = await import(
'../service/model_channel.js'
);
const channelService = await ctx.getAsync(NetaClawModelChannelService);
const resolved = await channelService.resolveForAgent(
agentConfig.channelId,
agentConfig.modelId || agentConfig.model
);
agentConfig.model = `${resolved.provider}:${resolved.model}`;
agentConfig.apiKey = resolved.apiKey;
if (resolved.baseUrl) agentConfig.baseUrl = resolved.baseUrl;
} catch (e: any) {
console.warn(`渠道解析失败,使用原始配置: ${e.message}`);
}
}
- Step 3: 提交
git add packages/backend/src/modules/netaclaw/runtime/agent.ts
git commit -m "feat(backend): Agent运行时接入渠道解析"
Task 11: 数据库菜单配置
Files:
-
数据操作:
base_sys_menu表 -
Step 1: 插入模型管理菜单
通过 MCP 工具执行 SQL,在 Agent 管理目录下新增菜单项:
INSERT INTO base_sys_menu (parentId, name, router, icon, orderNum, type, isShow, keepAlive, createTime, updateTime)
VALUES (
112,
'模型管理',
'/agent/model-channel',
'icon-model',
4,
1,
1,
1,
NOW(),
NOW()
);
注意:parentId=112 是 Agent 管理目录的 ID(需确认,可先查询 SELECT id, name FROM base_sys_menu WHERE name='Agent管理' AND type=0)。
- Step 2: 浏览器验证
刷新页面,确认左侧菜单中「Agent管理」目录下出现「模型管理」菜单项,点击可跳转到模型渠道管理页面。
Task 12: 端到端验证
- Step 1: 新增渠道测试
在模型管理页面新增一个渠道:
- 名称: "测试-Anthropic"
- 供应商: Anthropic
- Base URL:
https://api.anthropic.com - API Key: (填入测试用 Key)
- 模型: claude-sonnet-4-20250514 (text)
保存后确认列表中显示。
- Step 2: Agent 引用渠道测试
编辑一个 Agent → 模型配置 Tab → 选择刚建的渠道 → 选择模型 → 保存。
验证:保存成功,重新打开后选择器正确回填。
- Step 3: Skill 引用渠道测试
打开一个 Skill 的详情 → 模型配置 Tab → 选择渠道和模型 → 保存。
验证:保存成功,重新打开后选择器正确回填。
- Step 4: 最终提交
git add -A
git commit -m "feat: 模型渠道管理完整实现 - Entity/Service/Controller/前端页面/选择器/Agent与Skill改造"