1057 lines
30 KiB
Markdown
1057 lines
30 KiB
Markdown
|
|
# 模型渠道管理 实施计划
|
|||
|
|
|
|||
|
|
> **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<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: 提交**
|
|||
|
|
|
|||
|
|
```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<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: 提交**
|
|||
|
|
|
|||
|
|
```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 手动输入表单
|
|||
|
|
|
|||
|
|
在 `<el-form-item v-if="selectedChannelId && selectedChannelId !== 0" label="选择模型">` 之后,添加自定义配置表单:
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<!-- 自定义配置(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 方法:
|
|||
|
|
|
|||
|
|
```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` 的 `<script lang="ts" setup>` 中,在 `import type { SkillMeta } ...` 之后添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import ModelChannelSelector from '../components/model-channel-selector.vue';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 替换模型配置 Tab 模板**
|
|||
|
|
|
|||
|
|
将 `agent-edit.vue` 第 76-115 行(Tab 4: 模型配置)替换为:
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<!-- 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:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const modelConfig = ref({
|
|||
|
|
channelId: null as number | null,
|
|||
|
|
modelId: '',
|
|||
|
|
apiKey: '',
|
|||
|
|
apiUrl: '',
|
|||
|
|
contextWindow: null as number | null
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
更新 `clearModelConfig` 函数:
|
|||
|
|
```typescript
|
|||
|
|
function clearModelConfig() {
|
|||
|
|
modelConfig.value = { channelId: null, modelId: '', apiKey: '', apiUrl: '', contextWindow: null };
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
移除 `fillDefaultLLM` 函数和 `defaultLLM` 常量(不再需要)。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 更新 loadAgent 中的 modelConfig 回填**
|
|||
|
|
|
|||
|
|
在 `loadAgent()` 函数中,更新 modelConfig 回填逻辑:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
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 判断逻辑:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const mc =
|
|||
|
|
modelConfig.value.channelId || modelConfig.value.modelId || modelConfig.value.contextWindow
|
|||
|
|
? modelConfig.value
|
|||
|
|
: null;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: 更新 resetForm 中的 modelConfig**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
modelConfig.value = { channelId: null, modelId: '', apiKey: '', apiUrl: '', contextWindow: null };
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: 浏览器验证**
|
|||
|
|
|
|||
|
|
访问 Agent 管理 → 编辑任一 Agent → 模型配置 Tab
|
|||
|
|
|
|||
|
|
验证:
|
|||
|
|
- 渠道选择器正常展示
|
|||
|
|
- 选择渠道后模型下拉出现
|
|||
|
|
- 选择「自定义配置」后手动输入框出现
|
|||
|
|
- 保存后重新打开,选择状态正确回填
|
|||
|
|
|
|||
|
|
- [ ] **Step 8: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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>` 中添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import ModelChannelSelector from './model-channel-selector.vue';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 替换「可配置模型」模板**
|
|||
|
|
|
|||
|
|
将 `skill-model.vue` 第 30-104 行(`<template v-else>` 可配置模型部分)替换为:
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<!-- 可配置模型 -->
|
|||
|
|
<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 中添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
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` 函数,加载已保存的渠道配置:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
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**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
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**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
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: 移除不再需要的代码**
|
|||
|
|
|
|||
|
|
移除以下不再需要的代码:
|
|||
|
|
- `presets` ref 和 `PresetModel` interface
|
|||
|
|
- `loadPresets()` 函数
|
|||
|
|
- `applyPreset()` 函数
|
|||
|
|
- `modelForm` ref
|
|||
|
|
- `DEFAULT_MODEL` 常量
|
|||
|
|
- `selectedPresetId` ref
|
|||
|
|
- preset-section 相关的 CSS 样式
|
|||
|
|
|
|||
|
|
- [ ] **Step 8: 浏览器验证**
|
|||
|
|
|
|||
|
|
访问 Skill 管理 → 点击任一 Skill 的「配置」按钮 → 模型配置 Tab
|
|||
|
|
|
|||
|
|
验证:
|
|||
|
|
- 渠道选择器正常展示
|
|||
|
|
- multimodal 类型 Skill 只展示 multimodal 能力的模型
|
|||
|
|
- 保存后重新打开,选择状态正确回填
|
|||
|
|
|
|||
|
|
- [ ] **Step 9: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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 支持:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
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);` 之前,添加渠道解析:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 渠道解析:如果指定了 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: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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 管理目录下新增菜单项:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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: 最终提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add -A
|
|||
|
|
git commit -m "feat: 模型渠道管理完整实现 - Entity/Service/Controller/前端页面/选择器/Agent与Skill改造"
|
|||
|
|
```
|