mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-23 00:00:52 +00:00
feat: add model switching functionality for AI agent (#11839)
This commit is contained in:
@@ -71,6 +71,26 @@ func (b *BaseApi) DeleteAgent(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent model config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentModelConfigUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/model/update [post]
|
||||
func (b *BaseApi) UpdateAgentModelConfig(c *gin.Context) {
|
||||
var req dto.AgentModelConfigUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateModelConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Providers
|
||||
// @Success 200 {object} []dto.ProviderInfo
|
||||
|
||||
@@ -38,6 +38,7 @@ type AgentItem struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
AccountID uint `json:"accountId"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
Container string `json:"containerName"`
|
||||
WebUIPort int `json:"webUIPort"`
|
||||
@@ -54,6 +55,12 @@ type AgentDeleteReq struct {
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
}
|
||||
|
||||
type AgentModelConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentAccountCreateReq struct {
|
||||
Provider string `json:"provider" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
|
||||
+188
-11
@@ -32,6 +32,7 @@ type IAgentService interface {
|
||||
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
|
||||
Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error)
|
||||
Delete(req dto.AgentDeleteReq) error
|
||||
UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
GetProviders() ([]dto.ProviderInfo, error)
|
||||
CreateAccount(req dto.AgentAccountCreateReq) error
|
||||
UpdateAccount(req dto.AgentAccountUpdateReq) error
|
||||
@@ -209,6 +210,64 @@ func (a AgentService) Delete(req dto.AgentDeleteReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error {
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !account.Verified {
|
||||
return buserr.New("ErrAgentAccountNotVerified")
|
||||
}
|
||||
provider := strings.ToLower(strings.TrimSpace(account.Provider))
|
||||
if !isSupportedAgentProvider(provider) {
|
||||
return buserr.New("ErrAgentProviderNotSupported")
|
||||
}
|
||||
modelName := strings.TrimSpace(req.Model)
|
||||
if modelName == "" {
|
||||
return buserr.New("ErrAgentProviderMismatch")
|
||||
}
|
||||
if !strings.HasPrefix(modelName, provider+"/") {
|
||||
return buserr.New("ErrAgentProviderMismatch")
|
||||
}
|
||||
baseURL := strings.TrimSpace(account.BaseURL)
|
||||
if provider != "ollama" {
|
||||
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
|
||||
baseURL = defaultURL
|
||||
}
|
||||
}
|
||||
if provider == "ollama" && baseURL == "" {
|
||||
return buserr.New("ErrAgentBaseURLRequired")
|
||||
}
|
||||
if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" {
|
||||
return buserr.New("ErrAgentApiKeyRequired")
|
||||
}
|
||||
confDir := ""
|
||||
if agent.ConfigPath != "" {
|
||||
confDir = path.Dir(agent.ConfigPath)
|
||||
} else if agent.AppInstallID > 0 {
|
||||
install, errGet := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
|
||||
if errGet == nil {
|
||||
confDir = path.Join(install.GetPath(), "data", "conf")
|
||||
}
|
||||
}
|
||||
if confDir == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, provider, modelName, baseURL, account.APIKey, agent.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
agent.Provider = provider
|
||||
agent.Model = modelName
|
||||
agent.BaseURL = baseURL
|
||||
agent.APIKey = account.APIKey
|
||||
agent.AccountID = account.ID
|
||||
return agentRepo.Save(agent)
|
||||
}
|
||||
|
||||
func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
||||
definitions := providerDefinitions()
|
||||
providers := make([]dto.ProviderInfo, 0, len(definitions))
|
||||
@@ -612,6 +671,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
Status: agent.Status,
|
||||
Message: agent.Message,
|
||||
AppInstallID: agent.AppInstallID,
|
||||
AccountID: agent.AccountID,
|
||||
ConfigPath: agent.ConfigPath,
|
||||
CreatedAt: agent.CreatedAt,
|
||||
}
|
||||
@@ -768,12 +828,20 @@ type modelProvider struct {
|
||||
}
|
||||
|
||||
type modelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Reasoning bool `json:"reasoning"`
|
||||
Input []string `json:"input"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Reasoning bool `json:"reasoning"`
|
||||
Input []string `json:"input"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
Cost modelCost `json:"cost"`
|
||||
}
|
||||
|
||||
type modelCost struct {
|
||||
Input float64 `json:"input"`
|
||||
Output float64 `json:"output"`
|
||||
CacheRead float64 `json:"cacheRead"`
|
||||
CacheWrite float64 `json:"cacheWrite"`
|
||||
}
|
||||
|
||||
func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token string) error {
|
||||
@@ -814,16 +882,28 @@ func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token st
|
||||
}
|
||||
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
modelID := modelName
|
||||
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
|
||||
modelID = parts[1]
|
||||
}
|
||||
configProvider := provider
|
||||
primaryModel := modelName
|
||||
if provider == "kimi" {
|
||||
configProvider = "moonshot"
|
||||
primaryModel = "moonshot/" + modelID
|
||||
}
|
||||
if provider == "deepseek" {
|
||||
cfg.Agents.Defaults.Model.Primary = modelName
|
||||
base := baseURL
|
||||
if base == "" {
|
||||
base = "https://api.deepseek.com/v1"
|
||||
}
|
||||
plainKey := strings.TrimSpace(apiKey)
|
||||
cfg.Models = &modelsConfig{
|
||||
Mode: "merge",
|
||||
Providers: map[string]modelProvider{
|
||||
"deepseek": {
|
||||
ApiKey: "${DEEPSEEK_API_KEY}",
|
||||
ApiKey: plainKey,
|
||||
BaseUrl: base,
|
||||
Api: "openai-completions",
|
||||
Models: []modelEntry{
|
||||
@@ -834,16 +914,44 @@ func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token st
|
||||
Input: []string{"text"},
|
||||
ContextWindow: 128000,
|
||||
MaxTokens: 8192,
|
||||
Cost: modelCost{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if provider == "moonshot" || provider == "kimi" {
|
||||
cfg.Agents.Defaults.Model.Primary = primaryModel
|
||||
base := baseURL
|
||||
if base == "" {
|
||||
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
|
||||
base = defaultURL
|
||||
}
|
||||
}
|
||||
plainKey := strings.TrimSpace(apiKey)
|
||||
cfg.Models = &modelsConfig{
|
||||
Mode: "merge",
|
||||
Providers: map[string]modelProvider{
|
||||
configProvider: {
|
||||
ApiKey: plainKey,
|
||||
BaseUrl: base,
|
||||
Api: "openai-completions",
|
||||
Models: []modelEntry{
|
||||
{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: strings.Contains(modelID, "thinking"),
|
||||
Input: []string{"text"},
|
||||
ContextWindow: 256000,
|
||||
MaxTokens: 8192,
|
||||
Cost: modelCost{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if provider == "ollama" {
|
||||
modelID := modelName
|
||||
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
|
||||
modelID = parts[1]
|
||||
}
|
||||
cfg.Agents.Defaults.Model.Primary = modelName
|
||||
cfg.Models = &modelsConfig{
|
||||
Mode: "merge",
|
||||
Providers: map[string]modelProvider{
|
||||
@@ -859,6 +967,37 @@ func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token st
|
||||
Input: []string{"text"},
|
||||
ContextWindow: 160000,
|
||||
MaxTokens: 8192,
|
||||
Cost: modelCost{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if provider == "kimi-coding" {
|
||||
cfg.Agents.Defaults.Model.Primary = modelName
|
||||
base := baseURL
|
||||
if base == "" {
|
||||
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
|
||||
base = defaultURL
|
||||
}
|
||||
}
|
||||
plainKey := strings.TrimSpace(apiKey)
|
||||
cfg.Models = &modelsConfig{
|
||||
Mode: "merge",
|
||||
Providers: map[string]modelProvider{
|
||||
"kimi-coding": {
|
||||
ApiKey: plainKey,
|
||||
BaseUrl: base,
|
||||
Api: "anthropic-messages",
|
||||
Models: []modelEntry{
|
||||
{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: true,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: 200000,
|
||||
MaxTokens: 8192,
|
||||
Cost: modelCost{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -896,6 +1035,12 @@ func providerEnvKey(provider string) string {
|
||||
return "MINIMAX_API_KEY"
|
||||
case "deepseek":
|
||||
return "DEEPSEEK_API_KEY"
|
||||
case "moonshot":
|
||||
return "MOONSHOT_API_KEY"
|
||||
case "kimi":
|
||||
return "KIMI_API_KEY"
|
||||
case "kimi-coding":
|
||||
return "KIMI_API_KEY"
|
||||
case "qwen":
|
||||
return "QWEN_API_KEY"
|
||||
case "ollama":
|
||||
@@ -969,6 +1114,31 @@ func providerDefinitions() map[string]providerDefinition {
|
||||
{ID: "minimax/Minimax-M2.1", Name: "Minimax M2.1"},
|
||||
},
|
||||
},
|
||||
"moonshot": {
|
||||
Sort: 7,
|
||||
BaseURL: "https://api.moonshot.ai/v1",
|
||||
Models: []dto.ProviderModelInfo{
|
||||
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||
},
|
||||
},
|
||||
"kimi": {
|
||||
Sort: 8,
|
||||
BaseURL: "https://api.moonshot.cn/v1",
|
||||
Models: []dto.ProviderModelInfo{
|
||||
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||
},
|
||||
},
|
||||
"kimi-coding": {
|
||||
Sort: 9,
|
||||
BaseURL: "https://api.moonshot.cn/anthropic/v1",
|
||||
Models: []dto.ProviderModelInfo{
|
||||
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -999,6 +1169,13 @@ func buildVerifyRequest(provider, baseURL, apiKey string) (string, map[string]st
|
||||
return base + "/models", headers
|
||||
}
|
||||
return base + "/v1/models", headers
|
||||
case "kimi-coding":
|
||||
headers["x-api-key"] = apiKey
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
if strings.Contains(base, "/v1") {
|
||||
return base + "/models", headers
|
||||
}
|
||||
return base + "/v1/models", headers
|
||||
case "gemini":
|
||||
if strings.Contains(base, "/v1beta") {
|
||||
return fmt.Sprintf("%s/models?key=%s", base, apiKey), headers
|
||||
|
||||
@@ -948,6 +948,12 @@ func defaultBaseURL(provider string) (string, bool) {
|
||||
return "https://api.minimax.chat/v1", true
|
||||
case "deepseek":
|
||||
return "https://api.deepseek.com/v1", true
|
||||
case "moonshot":
|
||||
return "https://api.moonshot.ai/v1", true
|
||||
case "kimi":
|
||||
return "https://api.moonshot.cn/v1", true
|
||||
case "kimi-coding":
|
||||
return "https://api.moonshot.cn/anthropic/v1", true
|
||||
case "qwen":
|
||||
return "https://dashscope.aliyuncs.com/compatible-mode/v1", true
|
||||
default:
|
||||
|
||||
@@ -43,6 +43,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/agents", baseApi.CreateAgent)
|
||||
aiToolsRouter.POST("/agents/search", baseApi.PageAgents)
|
||||
aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent)
|
||||
aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig)
|
||||
aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders)
|
||||
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
|
||||
aiToolsRouter.POST("/agents/accounts/update", baseApi.UpdateAgentAccount)
|
||||
|
||||
@@ -272,6 +272,7 @@ export namespace AI {
|
||||
status: string;
|
||||
message: string;
|
||||
appInstallId: number;
|
||||
accountId: number;
|
||||
appVersion: string;
|
||||
containerName: string;
|
||||
webUIPort: number;
|
||||
@@ -282,13 +283,18 @@ export namespace AI {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
export interface AgentDeleteReq {
|
||||
id: number;
|
||||
taskID: string;
|
||||
forceDelete: boolean;
|
||||
}
|
||||
|
||||
export interface AgentModelConfigUpdateReq {
|
||||
agentId: number;
|
||||
accountId: number;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface ProviderModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -104,6 +104,10 @@ export const deleteAgent = (req: AI.AgentDeleteReq) => {
|
||||
return http.post(`/ai/agents/delete`, req);
|
||||
};
|
||||
|
||||
export const updateAgentModelConfig = (req: AI.AgentModelConfigUpdateReq) => {
|
||||
return http.post(`/ai/agents/model/update`, req);
|
||||
};
|
||||
|
||||
export const getAgentProviders = () => {
|
||||
return http.get<AI.ProviderInfo[]>(`/ai/agents/providers`);
|
||||
};
|
||||
|
||||
@@ -696,6 +696,7 @@ const message = {
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
configTitle: 'Configuration',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
feishu: 'Feishu',
|
||||
dmPolicy: 'DM Policy',
|
||||
|
||||
@@ -692,6 +692,7 @@ const message = {
|
||||
manualModel: 'Entrada manual de modelo',
|
||||
verified: 'Verificado',
|
||||
configTitle: 'Configuration',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
feishu: 'Feishu',
|
||||
dmPolicy: 'DM Policy',
|
||||
|
||||
@@ -681,6 +681,7 @@ const message = {
|
||||
manualModel: '手動入力',
|
||||
verified: '検証済み',
|
||||
configTitle: 'Configuration',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
feishu: 'Feishu',
|
||||
dmPolicy: 'DM Policy',
|
||||
|
||||
@@ -678,6 +678,7 @@ const message = {
|
||||
manualModel: '수동 입력',
|
||||
verified: '검증됨',
|
||||
configTitle: 'Configuration',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
feishu: 'Feishu',
|
||||
dmPolicy: 'DM Policy',
|
||||
|
||||
@@ -693,6 +693,7 @@ const message = {
|
||||
manualModel: 'Input manual',
|
||||
verified: 'Disahkan',
|
||||
configTitle: 'Configuration',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
feishu: 'Feishu',
|
||||
dmPolicy: 'DM Policy',
|
||||
|
||||
@@ -690,6 +690,7 @@ const message = {
|
||||
manualModel: 'Entrada manual',
|
||||
verified: 'Verificado',
|
||||
configTitle: 'Configuration',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
feishu: 'Feishu',
|
||||
dmPolicy: 'DM Policy',
|
||||
|
||||
@@ -686,6 +686,7 @@ const message = {
|
||||
manualModel: 'Ручной ввод',
|
||||
verified: 'Проверено',
|
||||
configTitle: 'Configuration',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
feishu: 'Feishu',
|
||||
dmPolicy: 'DM Policy',
|
||||
|
||||
@@ -700,6 +700,7 @@ const message = {
|
||||
manualModel: 'Manuel giriş',
|
||||
verified: 'Doğrulandı',
|
||||
configTitle: 'Configuration',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
feishu: 'Feishu',
|
||||
dmPolicy: 'DM Policy',
|
||||
|
||||
@@ -668,6 +668,7 @@ const message = {
|
||||
manualModel: '手動輸入模型',
|
||||
verified: '驗證狀態',
|
||||
configTitle: '配置',
|
||||
switchModelSuccess: '模型切換成功',
|
||||
channelsTab: '聊天渠道',
|
||||
feishu: '飛書',
|
||||
dmPolicy: '私聊策略',
|
||||
|
||||
@@ -670,6 +670,7 @@ const message = {
|
||||
manualModel: '手动输入模型',
|
||||
verified: '验证状态',
|
||||
configTitle: '配置',
|
||||
switchModelSuccess: '模型切换成功',
|
||||
channelsTab: '聊天渠道',
|
||||
feishu: '飞书',
|
||||
dmPolicy: '私聊策略',
|
||||
|
||||
@@ -174,6 +174,9 @@ const providerLabelMap: Record<string, string> = {
|
||||
openai: 'OpenAI',
|
||||
ollama: 'Ollama',
|
||||
minimax: 'MiniMax',
|
||||
moonshot: 'Moonshot',
|
||||
kimi: 'Kimi',
|
||||
'kimi-coding': 'Kimi Coding',
|
||||
qwen: 'Qwen',
|
||||
deepseek: 'DeepSeek',
|
||||
anthropic: 'Anthropic',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="header" size="large" @close="handleClose">
|
||||
<el-tabs v-model="activeTab" tab-position="left" class="config-tabs" @tab-click="handleTabClick">
|
||||
<el-tab-pane :label="t('aiTools.model.model')" name="model">
|
||||
<ModelTab ref="modelRef" @updated="handleModelUpdated" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('aiTools.agents.channelsTab')" name="channels">
|
||||
<ChannelsTab ref="channelsRef" />
|
||||
</el-tab-pane>
|
||||
@@ -14,13 +17,25 @@ import type { TabsPaneContext } from 'element-plus';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import ChannelsTab from './tabs/channels.vue';
|
||||
import ModelTab from './tabs/model.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(['updated']);
|
||||
const open = ref(false);
|
||||
const activeTab = ref('channels');
|
||||
const activeTab = ref('model');
|
||||
const header = ref('');
|
||||
const agentId = ref(0);
|
||||
const currentAgent = ref<AI.AgentItem>();
|
||||
const channelsRef = ref();
|
||||
const modelRef = ref();
|
||||
|
||||
const loadModel = async () => {
|
||||
if (!currentAgent.value) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await modelRef.value?.load(currentAgent.value);
|
||||
};
|
||||
|
||||
const loadChannels = async () => {
|
||||
if (agentId.value <= 0) {
|
||||
@@ -31,21 +46,29 @@ const loadChannels = async () => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
activeTab.value = 'channels';
|
||||
activeTab.value = 'model';
|
||||
};
|
||||
|
||||
const handleTabClick = async (pane: TabsPaneContext) => {
|
||||
if (pane.paneName === 'model' && currentAgent.value) {
|
||||
await loadModel();
|
||||
}
|
||||
if (pane.paneName === 'channels' && agentId.value > 0) {
|
||||
await loadChannels();
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelUpdated = () => {
|
||||
emit('updated');
|
||||
};
|
||||
|
||||
const openDrawer = async (agent: AI.AgentItem) => {
|
||||
agentId.value = agent.id;
|
||||
currentAgent.value = agent;
|
||||
header.value = `${agent.name} - ${t('aiTools.agents.configTitle')}`;
|
||||
activeTab.value = 'channels';
|
||||
activeTab.value = 'model';
|
||||
open.value = true;
|
||||
await loadChannels();
|
||||
await loadModel();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
|
||||
<el-form-item :label="t('aiTools.agents.account')" prop="accountId">
|
||||
<el-select v-model="form.accountId" @change="handleAccountChange">
|
||||
<el-option v-for="item in accountOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('aiTools.agents.manualModel')">
|
||||
<el-switch v-model="form.manualModel" @change="handleManualModelChange" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('aiTools.model.model')" prop="model">
|
||||
<el-input v-if="form.manualModel" v-model="form.model" />
|
||||
<el-select v-else v-model="form.model" filterable>
|
||||
<el-option v-for="item in modelOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="saveModel">
|
||||
{{ t('commons.button.save') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { getAgentProviders, pageAgentAccounts, updateAgentModelConfig } from '@/api/modules/ai';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const emit = defineEmits(['updated']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const agentId = ref(0);
|
||||
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
|
||||
const accountOptions = ref<AI.AgentAccountItem[]>([]);
|
||||
const modelOptions = ref<AI.ProviderModelInfo[]>([]);
|
||||
|
||||
const form = reactive({
|
||||
accountId: undefined as unknown as number,
|
||||
manualModel: false,
|
||||
model: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
accountId: [Rules.requiredSelect],
|
||||
model: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const loadProviders = async () => {
|
||||
if (Object.keys(providerModels.value).length > 0) {
|
||||
return;
|
||||
}
|
||||
const res = await getAgentProviders();
|
||||
const data = res.data || [];
|
||||
providerModels.value = data.reduce((acc, item) => {
|
||||
acc[item.provider] = item.models || [];
|
||||
return acc;
|
||||
}, {} as Record<string, AI.ProviderModelInfo[]>);
|
||||
};
|
||||
|
||||
const loadAccounts = async () => {
|
||||
const res = await pageAgentAccounts({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
provider: '',
|
||||
name: '',
|
||||
});
|
||||
accountOptions.value = res.data.items || [];
|
||||
};
|
||||
|
||||
const setModelsByProvider = (provider: string) => {
|
||||
modelOptions.value = providerModels.value[provider] || [];
|
||||
};
|
||||
|
||||
const handleAccountChange = () => {
|
||||
const selected = accountOptions.value.find((item) => item.id === form.accountId);
|
||||
if (!selected) {
|
||||
modelOptions.value = [];
|
||||
form.model = '';
|
||||
return;
|
||||
}
|
||||
setModelsByProvider(selected.provider);
|
||||
if (!form.manualModel && (!form.model || !form.model.startsWith(`${selected.provider}/`))) {
|
||||
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualModelChange = (val: boolean) => {
|
||||
if (val) {
|
||||
return;
|
||||
}
|
||||
const selected = accountOptions.value.find((item) => item.id === form.accountId);
|
||||
if (!selected) {
|
||||
form.model = '';
|
||||
return;
|
||||
}
|
||||
if (!form.model || !form.model.startsWith(`${selected.provider}/`)) {
|
||||
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
|
||||
}
|
||||
};
|
||||
|
||||
const load = async (agent: AI.AgentItem) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
agentId.value = agent.id;
|
||||
await loadProviders();
|
||||
await loadAccounts();
|
||||
if (accountOptions.value.length === 0) {
|
||||
form.accountId = undefined as unknown as number;
|
||||
form.model = '';
|
||||
modelOptions.value = [];
|
||||
return;
|
||||
}
|
||||
const currentAccount =
|
||||
accountOptions.value.find((item) => item.id === agent.accountId) || accountOptions.value[0];
|
||||
form.accountId = currentAccount.id;
|
||||
setModelsByProvider(currentAccount.provider);
|
||||
const inProviderModels = modelOptions.value.some((item) => item.id === agent.model);
|
||||
form.manualModel = !inProviderModels;
|
||||
if (agent.model && (form.manualModel || agent.model.startsWith(`${currentAccount.provider}/`))) {
|
||||
form.model = agent.model;
|
||||
} else {
|
||||
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveModel = async () => {
|
||||
if (!agentId.value || !formRef.value) {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateAgentModelConfig({
|
||||
agentId: agentId.value,
|
||||
accountId: form.accountId,
|
||||
model: form.model,
|
||||
});
|
||||
MsgSuccess(t('aiTools.agents.switchModelSuccess'));
|
||||
emit('updated');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
load,
|
||||
});
|
||||
</script>
|
||||
@@ -81,7 +81,7 @@
|
||||
<AddDialog ref="addRef" @search="search" @task="openTaskLog" />
|
||||
<TaskLog ref="taskLogRef" @close="search" />
|
||||
<DeleteDialog ref="deleteRef" @close="search" />
|
||||
<ConfigDrawer ref="configRef" />
|
||||
<ConfigDrawer ref="configRef" @updated="search" />
|
||||
<AppUpgrade ref="upgradeRef" @close="search" />
|
||||
<ComposeLogs ref="composeLogRef" />
|
||||
<TerminalDialog ref="dialogTerminalRef" />
|
||||
@@ -127,6 +127,9 @@ const providerLabelMap: Record<string, string> = {
|
||||
openai: 'OpenAI',
|
||||
ollama: 'Ollama',
|
||||
minimax: 'MiniMax',
|
||||
moonshot: 'Moonshot',
|
||||
kimi: 'Kimi',
|
||||
'kimi-coding': 'Kimi Coding',
|
||||
qwen: 'Qwen',
|
||||
deepseek: 'DeepSeek',
|
||||
anthropic: 'Anthropic',
|
||||
|
||||
@@ -58,6 +58,9 @@ const providerLabelMap: Record<string, string> = {
|
||||
openai: 'OpenAI',
|
||||
ollama: 'Ollama',
|
||||
minimax: 'MiniMax',
|
||||
moonshot: 'Moonshot',
|
||||
kimi: 'Kimi',
|
||||
'kimi-coding': 'Kimi Coding',
|
||||
qwen: 'Qwen',
|
||||
anthropic: 'Anthropic',
|
||||
gemini: 'Gemini',
|
||||
|
||||
@@ -61,6 +61,9 @@ const providerLabelMap: Record<string, string> = {
|
||||
openai: 'OpenAI',
|
||||
ollama: 'Ollama',
|
||||
minimax: 'MiniMax',
|
||||
moonshot: 'Moonshot',
|
||||
kimi: 'Kimi',
|
||||
'kimi-coding': 'Kimi Coding',
|
||||
qwen: 'Qwen',
|
||||
deepseek: 'DeepSeek',
|
||||
anthropic: 'Anthropic',
|
||||
|
||||
Reference in New Issue
Block a user