# 模型渠道管理 实施计划

> **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改造"
```
