1671 lines
52 KiB
Vue
1671 lines
52 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" class="search-form">
|
|
<el-form-item label="关键词">
|
|
<el-input v-model="searchForm.keyword" placeholder="配置名称/键值/描述" clearable style="width: 200px" />
|
|
</el-form-item>
|
|
<el-form-item label="配置类型">
|
|
<el-select v-model="searchForm.configType" placeholder="请选择配置类型" clearable style="width: 150px">
|
|
<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 style="width: 150px">
|
|
<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 style="width: 150px">
|
|
<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 style="width: 120px">
|
|
<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 style="width: 130px">
|
|
<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,如:https://api.coze.cn/v3/chat" />
|
|
</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">
|
|
<el-col :span="12">
|
|
<el-form-item label="Client ID">
|
|
<el-input v-model="formData.clientId" placeholder="请输入OAuth客户端ID" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="Grant Type">
|
|
<el-select v-model="formData.grantType" placeholder="请选择授权类型" clearable style="width: 100%">
|
|
<el-option label="client_credentials" value="client_credentials" />
|
|
<el-option label="authorization_code" value="authorization_code" />
|
|
<el-option label="password" value="password" />
|
|
<el-option label="refresh_token" value="refresh_token" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-form-item label="Client Secret">
|
|
<el-input
|
|
v-model="formData.clientSecret"
|
|
type="password"
|
|
show-password
|
|
placeholder="请输入OAuth客户端密钥"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<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="Client ID">{{ viewData.clientId || '-' }}</el-descriptions-item>
|
|
<el-descriptions-item label="Client Secret">{{ viewData.clientSecret ? '******' : '-' }}</el-descriptions-item>
|
|
<el-descriptions-item label="Grant Type">{{ viewData.grantType || '-' }}</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">
|
|
<div class="test-header-info" style="margin-bottom: 20px;">
|
|
<el-descriptions :column="2" border>
|
|
<el-descriptions-item label="配置名称">{{ testConfig.configName }}</el-descriptions-item>
|
|
<el-descriptions-item label="配置键值">
|
|
<el-tag type="info">{{ testConfig.configKey }}</el-tag>
|
|
</el-descriptions-item>
|
|
</el-descriptions>
|
|
</div>
|
|
<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" placeholder="请输入请求URL" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="测试选项">
|
|
<div class="test-options">
|
|
<el-checkbox
|
|
v-model="testOptions.useStream"
|
|
@change="updateTestRequestBody"
|
|
>
|
|
启用流式响应
|
|
</el-checkbox>
|
|
<el-tooltip content="启用后将测试流式返回,可以看到AI逐步生成的响应内容" placement="top">
|
|
<el-icon class="info-icon"><InfoFilled /></el-icon>
|
|
</el-tooltip>
|
|
</div>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="测试消息">
|
|
<el-input
|
|
v-model="testOptions.testMessage"
|
|
placeholder="输入测试消息内容"
|
|
@input="updateTestRequestBody"
|
|
/>
|
|
</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="10"
|
|
placeholder="JSON格式的请求体"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item>
|
|
<el-button type="primary" @click="handleTestRequest" :loading="testLoading">
|
|
{{ testOptions.useStream ? '发送流式测试' : '发送测试请求' }}
|
|
</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-button
|
|
v-if="testResponse.status === 200"
|
|
type="success"
|
|
@click="handleSaveTestConfig"
|
|
>
|
|
保存测试配置
|
|
</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 { InfoFilled } from '@element-plus/icons-vue'
|
|
import {
|
|
getAiConfigPage,
|
|
createAiConfig,
|
|
updateAiConfig,
|
|
deleteAiConfig,
|
|
enableAiConfig,
|
|
disableAiConfig,
|
|
setAsDefaultConfig,
|
|
unsetDefaultConfig,
|
|
countEnabledConfigs,
|
|
countDisabledConfigs,
|
|
countDefaultConfigs,
|
|
updateAiConfigFromTest
|
|
} 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: '',
|
|
clientId: '',
|
|
clientSecret: '',
|
|
grantType: '',
|
|
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 testOptions = reactive({
|
|
useStream: false,
|
|
testMessage: '你好,这是一个测试消息,请回复确认接口正常工作。'
|
|
})
|
|
|
|
// 获取配置类型标签类型
|
|
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: '',
|
|
clientId: '',
|
|
clientSecret: '',
|
|
grantType: '',
|
|
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) => {
|
|
// apiBaseUrl已经是完整的API URL,直接使用
|
|
testRequest.url = config.apiBaseUrl
|
|
|
|
// 构建请求头
|
|
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: testOptions.useStream,
|
|
additional_messages: [
|
|
{
|
|
role: 'user',
|
|
content: testOptions.testMessage,
|
|
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 updateTestRequestBody = () => {
|
|
if (!testConfig.value) return
|
|
|
|
try {
|
|
const body = JSON.parse(testRequest.body)
|
|
body.stream = testOptions.useStream
|
|
body.additional_messages[0].content = testOptions.testMessage
|
|
testRequest.body = JSON.stringify(body, null, 2)
|
|
} catch (e) {
|
|
console.warn('更新请求体失败:', e)
|
|
}
|
|
}
|
|
|
|
// 发送测试请求
|
|
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 isStreamRequest = (body as any).stream === true
|
|
|
|
if (isStreamRequest) {
|
|
// 处理流式请求
|
|
await handleStreamRequest(headers, body)
|
|
} else {
|
|
// 处理普通请求
|
|
await handleNormalRequest(headers, body)
|
|
}
|
|
|
|
} 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 handleNormalRequest = async (headers: any, body: any) => {
|
|
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}`)
|
|
}
|
|
}
|
|
|
|
// 处理流式请求
|
|
const handleStreamRequest = async (headers: any, body: any) => {
|
|
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
|
|
})
|
|
|
|
testResponse.status = response.status
|
|
testResponse.headers = JSON.stringify(responseHeaders, null, 2)
|
|
|
|
if (!response.ok) {
|
|
const errorBody = await response.text()
|
|
testResponse.body = errorBody
|
|
ElMessage.warning(`请求返回状态码: ${response.status}`)
|
|
return
|
|
}
|
|
|
|
if (!response.body) {
|
|
testResponse.body = 'Error: 响应体为空'
|
|
ElMessage.error('响应体为空')
|
|
return
|
|
}
|
|
|
|
// 处理流式响应
|
|
const reader = response.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let streamContent = ''
|
|
let chunks: string[] = []
|
|
|
|
// 清空响应体,准备接收流式数据
|
|
testResponse.body = '正在接收流式数据...\n\n'
|
|
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
|
|
if (done) {
|
|
break
|
|
}
|
|
|
|
// 解码数据块
|
|
const chunk = decoder.decode(value, { stream: true })
|
|
streamContent += chunk
|
|
chunks.push(chunk)
|
|
|
|
// 实时更新响应体显示
|
|
testResponse.body = `=== 流式响应数据 ===\n\n` +
|
|
`接收到 ${chunks.length} 个数据块,总长度: ${streamContent.length} 字符\n\n` +
|
|
`=== 原始数据流 ===\n${streamContent}\n\n` +
|
|
`=== 解析后的数据 ===\n${parseStreamData(streamContent)}`
|
|
}
|
|
|
|
ElMessage.success(`流式请求完成,共接收 ${chunks.length} 个数据块`)
|
|
|
|
} catch (streamError: any) {
|
|
console.error('流式数据读取失败:', streamError)
|
|
testResponse.body += `\n\n=== 流式读取错误 ===\n${streamError.message || streamError}`
|
|
ElMessage.error('流式数据读取失败: ' + (streamError.message || streamError))
|
|
} finally {
|
|
reader.releaseLock()
|
|
}
|
|
}
|
|
|
|
// 解析流式数据
|
|
const parseStreamData = (streamContent: string): string => {
|
|
try {
|
|
const lines = streamContent.split('\n')
|
|
const parsedData: any[] = []
|
|
let currentEvent = ''
|
|
let currentData = ''
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('event:')) {
|
|
currentEvent = line.substring(6).trim()
|
|
} else if (line.startsWith('data:')) {
|
|
currentData = line.substring(5).trim()
|
|
|
|
if (currentData === '[DONE]') {
|
|
parsedData.push({
|
|
event: currentEvent || 'done',
|
|
data: '[DONE]',
|
|
timestamp: new Date().toISOString()
|
|
})
|
|
} else if (currentData) {
|
|
try {
|
|
const jsonData = JSON.parse(currentData)
|
|
parsedData.push({
|
|
event: currentEvent || 'data',
|
|
data: jsonData,
|
|
timestamp: new Date().toISOString()
|
|
})
|
|
} catch (e) {
|
|
parsedData.push({
|
|
event: currentEvent || 'raw',
|
|
data: currentData,
|
|
timestamp: new Date().toISOString()
|
|
})
|
|
}
|
|
}
|
|
|
|
currentEvent = ''
|
|
currentData = ''
|
|
} else if (line.trim() === '') {
|
|
// 空行,重置状态
|
|
currentEvent = ''
|
|
currentData = ''
|
|
}
|
|
}
|
|
|
|
return JSON.stringify(parsedData, null, 2)
|
|
} catch (e) {
|
|
return `解析失败: ${e}\n\n原始内容:\n${streamContent}`
|
|
}
|
|
}
|
|
|
|
// 格式化请求
|
|
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 handleSaveTestConfig = async () => {
|
|
if (!testConfig.value) {
|
|
ElMessage.error('测试配置不存在')
|
|
return
|
|
}
|
|
|
|
try {
|
|
// 解析请求头
|
|
let headers: any = {}
|
|
try {
|
|
headers = JSON.parse(testRequest.headers)
|
|
} catch (e) {
|
|
ElMessage.error('请求头格式错误,无法解析')
|
|
return
|
|
}
|
|
|
|
// 解析请求体
|
|
let body: any = {}
|
|
try {
|
|
body = JSON.parse(testRequest.body)
|
|
} catch (e) {
|
|
ElMessage.error('请求体格式错误,无法解析')
|
|
return
|
|
}
|
|
|
|
// 提取API Token(从Authorization头中)
|
|
let apiToken = testConfig.value.apiToken
|
|
if (headers.Authorization) {
|
|
// 移除 "Bearer " 前缀
|
|
apiToken = headers.Authorization.replace(/^Bearer\s+/i, '')
|
|
delete headers.Authorization // 从自定义头中移除,因为已经保存到apiToken字段
|
|
}
|
|
|
|
// 提取Bot ID和Workflow ID(从请求体中)
|
|
const botId = body.bot_id || testConfig.value.botId
|
|
const workflowId = body.workflow_id || testConfig.value.workflowId
|
|
const supportStream = body.stream !== undefined ? (body.stream ? 1 : 0) : testConfig.value.supportStream
|
|
|
|
// 移除已经提取的字段,剩余的作为自定义参数
|
|
const customParamsBody = { ...body }
|
|
delete customParamsBody.bot_id
|
|
delete customParamsBody.workflow_id
|
|
delete customParamsBody.stream
|
|
delete customParamsBody.user_id
|
|
delete customParamsBody.additional_messages
|
|
|
|
// 构建更新请求
|
|
const updateData = {
|
|
id: testConfig.value.id,
|
|
apiBaseUrl: testRequest.url,
|
|
apiToken: apiToken,
|
|
botId: botId,
|
|
workflowId: workflowId,
|
|
customHeaders: Object.keys(headers).length > 0 ? JSON.stringify(headers) : '',
|
|
customParams: Object.keys(customParamsBody).length > 0 ? JSON.stringify(customParamsBody) : '',
|
|
supportStream: supportStream
|
|
}
|
|
|
|
// 调用更新接口
|
|
const res = await updateAiConfigFromTest(updateData) as any
|
|
|
|
if (res.code === 200) {
|
|
ElMessage.success('测试配置已保存')
|
|
// 更新testConfig
|
|
testConfig.value = res.data
|
|
// 刷新列表
|
|
await fetchData()
|
|
} else {
|
|
ElMessage.error(res.message || '保存失败')
|
|
}
|
|
} catch (error: any) {
|
|
console.error('保存测试配置失败:', error)
|
|
ElMessage.error('保存失败: ' + (error.message || 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 = ''
|
|
testOptions.useStream = false
|
|
testOptions.testMessage = '你好,这是一个测试消息,请回复确认接口正常工作。'
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchData()
|
|
fetchStats()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.ai-config-list {
|
|
.page-title {
|
|
margin-bottom: 20px;
|
|
font-size: 24px;
|
|
color: var(--ls-text);
|
|
}
|
|
|
|
.search-card {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.search-form {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
|
|
.el-form-item {
|
|
margin-bottom: 10px;
|
|
margin-right: 15px;
|
|
}
|
|
}
|
|
|
|
.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: rgba(255, 255, 255, 0.03);
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
border-radius: var(--ls-radius-lg);
|
|
}
|
|
|
|
.pagination {
|
|
margin-top: 20px;
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
|
|
.form-tip {
|
|
margin-left: 10px;
|
|
font-size: 12px;
|
|
color: rgba(226, 232, 240, 0.6);
|
|
}
|
|
}
|
|
|
|
.test-container {
|
|
.test-section {
|
|
h4 {
|
|
margin-bottom: 20px;
|
|
color: var(--ls-text);
|
|
border-bottom: 2px solid rgba(255, 171, 145, 0.6);
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.test-options {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
|
|
.info-icon {
|
|
color: rgba(226, 232, 240, 0.6);
|
|
cursor: help;
|
|
}
|
|
}
|
|
|
|
.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> |