Files
happy-life-star/web-admin/src/views/aiconfig/AiConfigList.vue
T
2025-10-30 15:12:07 +08:00

1340 lines
41 KiB
Vue

<template>
<div class="ai-config-list">
<h2 class="page-title">AI配置管理</h2>
<el-card class="search-card">
<el-form :model="searchForm" :inline="true">
<el-form-item label="关键词">
<el-input v-model="searchForm.keyword" placeholder="配置名称/键值/描述" clearable />
</el-form-item>
<el-form-item label="配置类型">
<el-select v-model="searchForm.configType" placeholder="请选择配置类型" clearable>
<el-option
v-for="item in CONFIG_TYPE_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="服务提供商">
<el-select v-model="searchForm.provider" placeholder="请选择服务提供商" clearable>
<el-option
v-for="item in PROVIDER_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="使用场景">
<el-select v-model="searchForm.usageScenario" placeholder="请选择使用场景" clearable>
<el-option
v-for="item in USAGE_SCENARIO_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.isEnabled" placeholder="请选择状态" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="环境">
<el-select v-model="searchForm.environment" placeholder="请选择环境" clearable>
<el-option
v-for="item in ENVIRONMENT_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>AI配置列表</span>
<div class="header-actions">
<el-button type="success" @click="handleRefreshStats">刷新统计</el-button>
<el-button type="primary" @click="handleAdd">新增配置</el-button>
</div>
</div>
</template>
<!-- 统计信息 -->
<div class="stats-row">
<el-row :gutter="20">
<el-col :span="6">
<el-statistic title="总配置数" :value="stats.total" />
</el-col>
<el-col :span="6">
<el-statistic title="已启用" :value="stats.enabled" />
</el-col>
<el-col :span="6">
<el-statistic title="已禁用" :value="stats.disabled" />
</el-col>
<el-col :span="6">
<el-statistic title="默认配置" :value="stats.default" />
</el-col>
</el-row>
</div>
<el-table :data="tableData" v-loading="loading" stripe>
<el-table-column prop="configName" label="配置名称" width="150" />
<el-table-column prop="configKey" label="配置键值" width="150" />
<el-table-column prop="configType" label="配置类型" width="100">
<template #default="{ row }">
<el-tag :type="getConfigTypeTagType(row.configType)">
{{ getConfigTypeLabel(row.configType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="provider" label="服务提供商" width="120">
<template #default="{ row }">
<el-tag :type="getProviderTagType(row.provider)">
{{ getProviderLabel(row.provider) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="usageScenario" label="使用场景" width="120">
<template #default="{ row }">
<el-tag type="info">
{{ getUsageScenarioLabel(row.usageScenario) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="environment" label="环境" width="100">
<template #default="{ row }">
<el-tag :type="getEnvironmentTagType(row.environment)">
{{ getEnvironmentLabel(row.environment) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="80" />
<el-table-column prop="isEnabled" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'">
{{ row.isEnabled === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="isDefault" label="默认" width="80">
<template #default="{ row }">
<el-tag v-if="row.isDefault === 1" type="warning">默认</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="150" />
<el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="info" link @click="handleView(row)">查看</el-button>
<el-button type="success" link @click="handleTest(row)">测试</el-button>
<el-button
:type="row.isEnabled === 1 ? 'warning' : 'success'"
link
@click="handleToggleStatus(row)"
>
{{ row.isEnabled === 1 ? '禁用' : '启用' }}
</el-button>
<el-button
v-if="row.isDefault !== 1"
type="warning"
link
@click="handleSetDefault(row)"
>
设为默认
</el-button>
<el-button
v-else
type="info"
link
@click="handleUnsetDefault(row)"
>
取消默认
</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
class="pagination"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="800px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
>
<el-tabs v-model="activeTab">
<el-tab-pane label="基础配置" name="basic">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="配置名称" prop="configName">
<el-input v-model="formData.configName" placeholder="请输入配置名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="配置键值" prop="configKey">
<el-input v-model="formData.configKey" placeholder="请输入配置键值" :disabled="isEdit" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="配置类型" prop="configType">
<el-select v-model="formData.configType" style="width: 100%">
<el-option
v-for="item in CONFIG_TYPE_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务提供商" prop="provider">
<el-select v-model="formData.provider" style="width: 100%">
<el-option
v-for="item in PROVIDER_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="使用场景" prop="usageScenario">
<el-select v-model="formData.usageScenario" style="width: 100%">
<el-option
v-for="item in USAGE_SCENARIO_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="环境" prop="environment">
<el-select v-model="formData.environment" style="width: 100%">
<el-option
v-for="item in ENVIRONMENT_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="API基础URL" prop="apiBaseUrl">
<el-input v-model="formData.apiBaseUrl" placeholder="请输入API基础URL" />
</el-form-item>
<el-form-item label="API访问令牌" prop="apiToken">
<el-input
v-model="formData.apiToken"
type="password"
show-password
placeholder="请输入API访问令牌"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="API版本">
<el-input v-model="formData.apiVersion" placeholder="请输入API版本" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模型名称">
<el-input v-model="formData.modelName" placeholder="请输入模型名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="formData.configType === 'coze'">
<el-col :span="12">
<el-form-item label="Bot ID">
<el-input v-model="formData.botId" placeholder="请输入Bot ID" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Workflow ID">
<el-input v-model="formData.workflowId" placeholder="请输入Workflow ID" />
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="参数配置" name="params">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="超时时间(ms)">
<el-input-number v-model="formData.timeoutMs" :min="1000" :max="300000" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="重试次数">
<el-input-number v-model="formData.retryCount" :min="0" :max="10" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="重试延迟(ms)">
<el-input-number v-model="formData.retryDelayMs" :min="100" :max="10000" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="最大Token数">
<el-input-number v-model="formData.maxTokens" :min="1" :max="100000" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="温度参数">
<el-input-number v-model="formData.temperature" :min="0" :max="2" :step="0.1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Top-p参数">
<el-input-number v-model="formData.topP" :min="0" :max="1" :step="0.1" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="支持流式输出">
<el-switch v-model="formData.supportStream" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="支持函数调用">
<el-switch v-model="formData.supportFunctionCall" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="支持视觉理解">
<el-switch v-model="formData.supportVision" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="支持文件上传">
<el-switch v-model="formData.supportFileUpload" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="优先级">
<el-input-number v-model="formData.priority" :min="0" :max="100" style="width: 200px" />
<span class="form-tip">数值越大优先级越高</span>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="费用配置" name="pricing">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="输入Token价格">
<el-input-number v-model="formData.inputPricePer1k" :min="0" :step="0.000001" :precision="6" style="width: 100%" />
<span class="form-tip">每1K Token价格</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="输出Token价格">
<el-input-number v-model="formData.outputPricePer1k" :min="0" :step="0.000001" :precision="6" style="width: 100%" />
<span class="form-tip">每1K Token价格</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="货币单位">
<el-select v-model="formData.currency" style="width: 100%">
<el-option
v-for="item in CURRENCY_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="每分钟限制">
<el-input-number v-model="formData.rateLimitPerMinute" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="每小时限制">
<el-input-number v-model="formData.rateLimitPerHour" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="每日限制">
<el-input-number v-model="formData.rateLimitPerDay" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="其他配置" name="others">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否启用">
<el-radio-group v-model="formData.isEnabled">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否默认配置">
<el-radio-group v-model="formData.isDefault">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="自定义请求头">
<el-input
v-model="formData.customHeaders"
type="textarea"
:rows="3"
placeholder="JSON格式的自定义请求头"
/>
</el-form-item>
<el-form-item label="自定义参数">
<el-input
v-model="formData.customParams"
type="textarea"
:rows="3"
placeholder="JSON格式的自定义参数"
/>
</el-form-item>
<el-form-item label="Webhook地址">
<el-input v-model="formData.webhookUrl" placeholder="请输入Webhook回调地址" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="16">
<el-form-item label="健康检查URL">
<el-input v-model="formData.healthCheckUrl" placeholder="请输入健康检查URL" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="检查间隔(分钟)">
<el-input-number v-model="formData.healthCheckIntervalMinutes" :min="1" :max="1440" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="配置描述">
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入配置描述"
/>
</el-form-item>
<el-form-item label="使用说明">
<el-input
v-model="formData.usageNotes"
type="textarea"
:rows="3"
placeholder="请输入使用说明"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
确定
</el-button>
</template>
</el-dialog>
<!-- 查看详情对话框 -->
<el-dialog
v-model="viewDialogVisible"
title="配置详情"
width="800px"
>
<el-descriptions :column="2" border v-if="viewData">
<el-descriptions-item label="配置名称">{{ viewData.configName }}</el-descriptions-item>
<el-descriptions-item label="配置键值">{{ viewData.configKey }}</el-descriptions-item>
<el-descriptions-item label="配置类型">{{ getConfigTypeLabel(viewData.configType) }}</el-descriptions-item>
<el-descriptions-item label="服务提供商">{{ getProviderLabel(viewData.provider) }}</el-descriptions-item>
<el-descriptions-item label="使用场景">{{ getUsageScenarioLabel(viewData.usageScenario) }}</el-descriptions-item>
<el-descriptions-item label="环境">{{ getEnvironmentLabel(viewData.environment || '') }}</el-descriptions-item>
<el-descriptions-item label="API基础URL">{{ viewData.apiBaseUrl }}</el-descriptions-item>
<el-descriptions-item label="API令牌">{{ viewData.apiToken }}</el-descriptions-item>
<el-descriptions-item label="模型名称">{{ viewData.modelName || '-' }}</el-descriptions-item>
<el-descriptions-item label="优先级">{{ viewData.priority || 0 }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewData.isEnabled === 1 ? 'success' : 'danger'">
{{ viewData.isEnabled === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="默认配置">
<el-tag v-if="viewData.isDefault === 1" type="warning"></el-tag>
<span v-else></span>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ viewData.createTime }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ viewData.updateTime }}</el-descriptions-item>
<el-descriptions-item label="配置描述" :span="2">{{ viewData.description || '-' }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 接口测试对话框 -->
<el-dialog
v-model="testDialogVisible"
title="接口测试"
width="1200px"
@close="handleTestDialogClose"
>
<div class="test-container" v-if="testConfig">
<el-row :gutter="20">
<el-col :span="12">
<div class="test-section">
<h4>请求配置</h4>
<el-form label-width="100px">
<el-form-item label="请求URL">
<el-input v-model="testRequest.url" readonly />
</el-form-item>
<el-form-item label="请求头">
<el-input
v-model="testRequest.headers"
type="textarea"
:rows="6"
placeholder="JSON格式的请求头"
/>
</el-form-item>
<el-form-item label="请求体">
<el-input
v-model="testRequest.body"
type="textarea"
:rows="12"
placeholder="JSON格式的请求体"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleTestRequest" :loading="testLoading">
发送测试请求
</el-button>
<el-button @click="handleFormatRequest">格式化请求</el-button>
<el-button @click="handleResetTest">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-col>
<el-col :span="12">
<div class="test-section">
<h4>响应结果</h4>
<el-form label-width="100px">
<el-form-item label="状态码">
<el-tag :type="getStatusTagType(testResponse.status)">
{{ testResponse.status || '-' }}
</el-tag>
</el-form-item>
<el-form-item label="响应头">
<el-input
v-model="testResponse.headers"
type="textarea"
:rows="4"
readonly
placeholder="响应头信息"
/>
</el-form-item>
<el-form-item label="响应体">
<el-input
v-model="testResponse.body"
type="textarea"
:rows="16"
readonly
placeholder="响应体内容"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleFormatResponse">格式化响应</el-button>
<el-button @click="handleCopyResponse">复制响应</el-button>
</el-form-item>
</el-form>
</div>
</el-col>
</el-row>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import {
getAiConfigPage,
createAiConfig,
updateAiConfig,
deleteAiConfig,
enableAiConfig,
disableAiConfig,
setAsDefaultConfig,
unsetDefaultConfig,
countEnabledConfigs,
countDisabledConfigs,
countDefaultConfigs
} from '@/api/aiconfig'
import type { AiConfig, AiConfigPageRequest } from '@/types/aiconfig'
import {
CONFIG_TYPE_OPTIONS,
PROVIDER_OPTIONS,
USAGE_SCENARIO_OPTIONS,
ENVIRONMENT_OPTIONS,
CURRENCY_OPTIONS
} from '@/types/aiconfig'
const loading = ref(false)
const submitLoading = ref(false)
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
const testDialogVisible = ref(false)
const testLoading = ref(false)
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const activeTab = ref('basic')
const searchForm = reactive<AiConfigPageRequest>({
current: 1,
size: 10
})
const pagination = reactive({
current: 1,
size: 10,
total: 0
})
const stats = reactive({
total: 0,
enabled: 0,
disabled: 0,
default: 0
})
const tableData = ref<AiConfig[]>([])
const viewData = ref<AiConfig | null>(null)
const testConfig = ref<AiConfig | null>(null)
const formData = reactive({
id: '',
configName: '',
configKey: '',
configType: 'coze',
provider: 'coze',
apiBaseUrl: '',
apiToken: '',
apiVersion: '',
modelName: '',
botId: '',
workflowId: '',
timeoutMs: 30000,
retryCount: 3,
retryDelayMs: 1000,
maxTokens: 4000,
temperature: 0.7,
topP: 1.0,
supportStream: 1,
supportFunctionCall: 0,
supportVision: 0,
supportFileUpload: 0,
usageScenario: 'chat',
priority: 0,
inputPricePer1k: 0,
outputPricePer1k: 0,
currency: 'USD',
rateLimitPerMinute: 60,
rateLimitPerHour: 3600,
rateLimitPerDay: 86400,
isEnabled: 1,
isDefault: 0,
environment: 'production',
customHeaders: '',
customParams: '',
webhookUrl: '',
healthCheckUrl: '',
healthCheckIntervalMinutes: 5,
description: '',
usageNotes: ''
})
const formRules: FormRules = {
configName: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
configKey: [{ required: true, message: '请输入配置键值', trigger: 'blur' }],
configType: [{ required: true, message: '请选择配置类型', trigger: 'change' }],
provider: [{ required: true, message: '请选择服务提供商', trigger: 'change' }],
apiBaseUrl: [{ required: true, message: '请输入API基础URL', trigger: 'blur' }],
apiToken: [{ required: true, message: '请输入API访问令牌', trigger: 'blur' }],
usageScenario: [{ required: true, message: '请选择使用场景', trigger: 'change' }]
}
const dialogTitle = ref('新增AI配置')
// 测试相关数据
const testRequest = reactive({
url: '',
headers: '',
body: ''
})
const testResponse = reactive({
status: null as number | null,
headers: '',
body: ''
})
// 获取配置类型标签类型
const getConfigTypeTagType = (type: string) => {
const typeMap: Record<string, string> = {
coze: 'primary',
openai: 'success',
claude: 'warning',
gemini: 'info'
}
return typeMap[type] || 'info'
}
// 获取配置类型标签文本
const getConfigTypeLabel = (type: string) => {
const option = CONFIG_TYPE_OPTIONS.find(item => item.value === type)
return option?.label || type
}
// 获取服务提供商标签类型
const getProviderTagType = (provider: string) => {
const providerMap: Record<string, string> = {
coze: 'primary',
openai: 'success',
anthropic: 'warning',
google: 'info'
}
return providerMap[provider] || 'info'
}
// 获取服务提供商标签文本
const getProviderLabel = (provider: string) => {
const option = PROVIDER_OPTIONS.find(item => item.value === provider)
return option?.label || provider
}
// 获取使用场景标签文本
const getUsageScenarioLabel = (scenario: string) => {
const option = USAGE_SCENARIO_OPTIONS.find(item => item.value === scenario)
return option?.label || scenario
}
// 获取环境标签类型
const getEnvironmentTagType = (env: string) => {
const envMap: Record<string, string> = {
development: 'info',
testing: 'warning',
production: 'success'
}
return envMap[env] || 'info'
}
// 获取环境标签文本
const getEnvironmentLabel = (env: string) => {
const option = ENVIRONMENT_OPTIONS.find(item => item.value === env)
return option?.label || env
}
// 获取数据
const fetchData = async () => {
loading.value = true
try {
const params = {
...searchForm,
current: pagination.current,
size: pagination.size
}
const res = await getAiConfigPage(params)
tableData.value = res.data.records
pagination.total = res.data.total
stats.total = res.data.total
} catch (error) {
console.error('获取AI配置列表失败:', error)
} finally {
loading.value = false
}
}
// 刷新统计信息
const fetchStats = async () => {
try {
const [enabledRes, disabledRes, defaultRes] = await Promise.all([
countEnabledConfigs(),
countDisabledConfigs(),
countDefaultConfigs()
])
stats.enabled = enabledRes.data
stats.disabled = disabledRes.data
stats.default = defaultRes.data
} catch (error) {
console.error('获取统计信息失败:', error)
}
}
// 搜索
const handleSearch = () => {
pagination.current = 1
fetchData()
}
// 重置
const handleReset = () => {
Object.assign(searchForm, {
current: 1,
size: 10,
keyword: '',
configType: '',
provider: '',
usageScenario: '',
isEnabled: undefined,
environment: ''
})
fetchData()
}
// 刷新统计
const handleRefreshStats = () => {
fetchStats()
}
// 新增
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '新增AI配置'
activeTab.value = 'basic'
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: AiConfig) => {
isEdit.value = true
dialogTitle.value = '编辑AI配置'
activeTab.value = 'basic'
Object.assign(formData, row)
dialogVisible.value = true
}
// 查看详情
const handleView = (row: AiConfig) => {
viewData.value = row
viewDialogVisible.value = true
}
// 测试接口
const handleTest = (row: AiConfig) => {
testConfig.value = row
initTestData(row)
testDialogVisible.value = true
}
// 切换状态
const handleToggleStatus = async (row: AiConfig) => {
const action = row.isEnabled === 1 ? '禁用' : '启用'
try {
await ElMessageBox.confirm(`确定要${action}该配置吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
if (row.isEnabled === 1) {
await disableAiConfig(row.id)
} else {
await enableAiConfig(row.id)
}
ElMessage.success(`${action}成功`)
fetchData()
fetchStats()
} catch (error) {
if (error !== 'cancel') {
console.error(`${action}失败:`, error)
}
}
}
// 设置默认配置
const handleSetDefault = async (row: AiConfig) => {
try {
await ElMessageBox.confirm('确定要设置为默认配置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await setAsDefaultConfig(row.id)
ElMessage.success('设置成功')
fetchData()
fetchStats()
} catch (error) {
if (error !== 'cancel') {
console.error('设置默认配置失败:', error)
}
}
}
// 取消默认配置
const handleUnsetDefault = async (row: AiConfig) => {
try {
await ElMessageBox.confirm('确定要取消默认配置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await unsetDefaultConfig(row.id)
ElMessage.success('取消成功')
fetchData()
fetchStats()
} catch (error) {
if (error !== 'cancel') {
console.error('取消默认配置失败:', error)
}
}
}
// 删除
const handleDelete = (row: AiConfig) => {
ElMessageBox.confirm('确定要删除该AI配置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteAiConfig(row.id)
ElMessage.success('删除成功')
fetchData()
fetchStats()
} catch (error) {
console.error('删除失败:', error)
}
})
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (isEdit.value) {
await updateAiConfig(formData)
ElMessage.success('更新成功')
} else {
await createAiConfig(formData)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
fetchStats()
} catch (error) {
console.error('提交失败:', error)
} finally {
submitLoading.value = false
}
}
})
}
// 关闭对话框
const handleDialogClose = () => {
formRef.value?.resetFields()
Object.assign(formData, {
id: '',
configName: '',
configKey: '',
configType: 'coze',
provider: 'coze',
apiBaseUrl: '',
apiToken: '',
apiVersion: '',
modelName: '',
botId: '',
workflowId: '',
timeoutMs: 30000,
retryCount: 3,
retryDelayMs: 1000,
maxTokens: 4000,
temperature: 0.7,
topP: 1.0,
supportStream: 1,
supportFunctionCall: 0,
supportVision: 0,
supportFileUpload: 0,
usageScenario: 'chat',
priority: 0,
inputPricePer1k: 0,
outputPricePer1k: 0,
currency: 'USD',
rateLimitPerMinute: 60,
rateLimitPerHour: 3600,
rateLimitPerDay: 86400,
isEnabled: 1,
isDefault: 0,
environment: 'production',
customHeaders: '',
customParams: '',
webhookUrl: '',
healthCheckUrl: '',
healthCheckIntervalMinutes: 5,
description: '',
usageNotes: ''
})
}
// 初始化测试数据
const initTestData = (config: AiConfig) => {
// 构建请求URL
testRequest.url = config.apiBaseUrl + '/v3/chat'
// 构建请求头
const headers = {
'Authorization': `Bearer ${config.apiToken}`,
'Content-Type': 'application/json'
}
// 如果有自定义请求头,合并
if (config.customHeaders) {
try {
const customHeaders = JSON.parse(config.customHeaders)
Object.assign(headers, customHeaders)
} catch (e) {
console.warn('解析自定义请求头失败:', e)
}
}
testRequest.headers = JSON.stringify(headers, null, 2)
// 构建请求体 - 完全按照AiChatServiceImpl.buildCozeRequest的格式
const requestBody: any = {
bot_id: config.botId || '',
user_id: 'test_user_' + Date.now(),
stream: false,
additional_messages: [
{
role: 'user',
content: '你好,这是一个测试消息,请回复确认接口正常工作。',
content_type: 'text',
type: 'question'
}
],
parameters: {}
}
// 如果有workflow_id,添加到请求体
if (config.workflowId && config.workflowId.trim()) {
requestBody.workflow_id = config.workflowId
}
// 如果有自定义参数,合并
if (config.customParams) {
try {
const customParams = JSON.parse(config.customParams)
Object.assign(requestBody, customParams)
} catch (e) {
console.warn('解析自定义参数失败:', e)
}
}
testRequest.body = JSON.stringify(requestBody, null, 2)
// 清空响应数据
testResponse.status = null
testResponse.headers = ''
testResponse.body = ''
}
// 发送测试请求
const handleTestRequest = async () => {
if (!testConfig.value) return
testLoading.value = true
try {
// 解析请求头
let headers = {}
try {
headers = JSON.parse(testRequest.headers)
} catch (e) {
ElMessage.error('请求头格式错误,请检查JSON格式')
return
}
// 解析请求体
let body = {}
try {
body = JSON.parse(testRequest.body)
} catch (e) {
ElMessage.error('请求体格式错误,请检查JSON格式')
return
}
// 发送请求
const response = await fetch(testRequest.url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
// 获取响应头
const responseHeaders: any = {}
response.headers.forEach((value, key) => {
responseHeaders[key] = value
})
// 获取响应体
const responseBody = await response.text()
// 更新响应数据
testResponse.status = response.status
testResponse.headers = JSON.stringify(responseHeaders, null, 2)
testResponse.body = responseBody
// 尝试格式化响应体
try {
const jsonBody = JSON.parse(responseBody)
testResponse.body = JSON.stringify(jsonBody, null, 2)
} catch (e) {
// 如果不是JSON格式,保持原样
}
if (response.ok) {
ElMessage.success('测试请求发送成功')
} else {
ElMessage.warning(`请求返回状态码: ${response.status}`)
}
} catch (error: any) {
console.error('测试请求失败:', error)
ElMessage.error('测试请求失败: ' + (error.message || error))
// 设置错误响应
testResponse.status = 0
testResponse.headers = ''
testResponse.body = JSON.stringify({
error: 'Network Error',
message: error.message || error.toString(),
timestamp: new Date().toISOString()
}, null, 2)
} finally {
testLoading.value = false
}
}
// 格式化请求
const handleFormatRequest = () => {
try {
// 格式化请求头
if (testRequest.headers) {
const headers = JSON.parse(testRequest.headers)
testRequest.headers = JSON.stringify(headers, null, 2)
}
// 格式化请求体
if (testRequest.body) {
const body = JSON.parse(testRequest.body)
testRequest.body = JSON.stringify(body, null, 2)
}
ElMessage.success('格式化完成')
} catch (e) {
ElMessage.error('格式化失败,请检查JSON格式')
}
}
// 格式化响应
const handleFormatResponse = () => {
try {
if (testResponse.body) {
const body = JSON.parse(testResponse.body)
testResponse.body = JSON.stringify(body, null, 2)
ElMessage.success('响应格式化完成')
}
} catch (e) {
ElMessage.error('响应格式化失败,内容可能不是有效的JSON')
}
}
// 复制响应
const handleCopyResponse = async () => {
try {
await navigator.clipboard.writeText(testResponse.body)
ElMessage.success('响应内容已复制到剪贴板')
} catch (e) {
ElMessage.error('复制失败')
}
}
// 重置测试
const handleResetTest = () => {
if (testConfig.value) {
initTestData(testConfig.value)
ElMessage.success('测试数据已重置')
}
}
// 获取状态码标签类型
const getStatusTagType = (status: number | null) => {
if (!status) return 'info'
if (status >= 200 && status < 300) return 'success'
if (status >= 300 && status < 400) return 'warning'
if (status >= 400) return 'danger'
return 'info'
}
// 关闭测试对话框
const handleTestDialogClose = () => {
testConfig.value = null
testRequest.url = ''
testRequest.headers = ''
testRequest.body = ''
testResponse.status = null
testResponse.headers = ''
testResponse.body = ''
}
onMounted(() => {
fetchData()
fetchStats()
})
</script>
<style scoped lang="scss">
.ai-config-list {
.page-title {
margin-bottom: 20px;
font-size: 24px;
color: #333;
}
.search-card {
margin-bottom: 20px;
}
.table-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.header-actions {
display: flex;
gap: 10px;
}
}
.stats-row {
margin-bottom: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.pagination {
margin-top: 20px;
justify-content: flex-end;
}
}
.form-tip {
margin-left: 10px;
font-size: 12px;
color: #999;
}
}
.test-container {
.test-section {
h4 {
margin-bottom: 20px;
color: #333;
border-bottom: 2px solid #409eff;
padding-bottom: 8px;
}
.el-textarea {
:deep(.el-textarea__inner) {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
}
}
.el-input {
:deep(.el-input__inner) {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
}
}
}
}
</style>