1340 lines
41 KiB
Vue
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> |