新增:AI 接口连通性测试功能;修复:Ollama 超时问题
- 后台 AI 厂商列表新增「⚡ 测试」按钮,实时验证接口连通性
- 显示响应耗时(含冷启动)和模型返回内容
- AiChatService 请求超时从 30s 调整为 120s(兼容 Ollama 本地冷启动)
- 测试接口超时设为 60s
This commit is contained in:
@@ -200,6 +200,67 @@ class AiProviderController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试指定 AI 厂商的接口连通性
|
||||
*
|
||||
* 发送一条简短的测试消息,返回响应结果和耗时,用于验证配置是否正确。
|
||||
*
|
||||
* @param int $id 厂商配置 ID
|
||||
* @return JsonResponse 测试结果
|
||||
*/
|
||||
public function testConnection(int $id): JsonResponse
|
||||
{
|
||||
$provider = AiProviderConfig::findOrFail($id);
|
||||
|
||||
$apiKey = $provider->getDecryptedApiKey();
|
||||
$base = rtrim($provider->api_endpoint, '/');
|
||||
$endpoint = str_ends_with($base, '/v1')
|
||||
? $base.'/chat/completions'
|
||||
: $base.'/v1/chat/completions';
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$response = \Illuminate\Support\Facades\Http::withToken($apiKey)
|
||||
->timeout(60) // Ollama 本地模型冷启动较慢,给 60s
|
||||
->post($endpoint, [
|
||||
'model' => $provider->model,
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => '请用一句话介绍你自己。'],
|
||||
],
|
||||
'max_tokens' => 64,
|
||||
]);
|
||||
|
||||
$ms = (int) ((microtime(true) - $startTime) * 1000);
|
||||
$data = $response->json();
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => "HTTP {$response->status()}:{$response->body()}",
|
||||
'ms' => $ms,
|
||||
]);
|
||||
}
|
||||
|
||||
$reply = $data['choices'][0]['message']['content'] ?? '(无回复内容)';
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => trim($reply),
|
||||
'ms' => $ms,
|
||||
'model' => $data['model'] ?? $provider->model,
|
||||
]);
|
||||
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
||||
$ms = (int) ((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => '连接失败:'.$e->getMessage(),
|
||||
'ms' => $ms,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 AI 厂商配置
|
||||
*
|
||||
|
||||
@@ -32,7 +32,7 @@ class AiChatService
|
||||
/**
|
||||
* AI 请求超时时间(秒)
|
||||
*/
|
||||
private const REQUEST_TIMEOUT = 30;
|
||||
private const REQUEST_TIMEOUT = 120; // Ollama 本地模型冷启动较慢,给足时间
|
||||
|
||||
/**
|
||||
* Redis 上下文 key 前缀
|
||||
@@ -54,7 +54,7 @@ class AiChatService
|
||||
$charmCross = Sysparam::getValue('charm_cross_sex', '2');
|
||||
$charmSame = Sysparam::getValue('charm_same_sex', '1');
|
||||
$charmLimit = Sysparam::getValue('charm_hourly_limit', '20');
|
||||
|
||||
|
||||
$levelWarn = Sysparam::getValue('level_warn', '5');
|
||||
$levelMute = Sysparam::getValue('level_mute', '8');
|
||||
$levelKick = Sysparam::getValue('level_kick', '10');
|
||||
@@ -123,8 +123,8 @@ PROMPT;
|
||||
|
||||
// 将用户消息加入上下文(包含发送者信息)
|
||||
$context[] = [
|
||||
'role' => 'user',
|
||||
'content' => "【当前发言人:{$username}】\n" . $message
|
||||
'role' => 'user',
|
||||
'content' => "【当前发言人:{$username}】\n".$message,
|
||||
];
|
||||
|
||||
// 构建完整的 messages 数组(系统提示 + 对话上下文)
|
||||
|
||||
@@ -148,6 +148,9 @@
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onclick="testConnection({{ $provider->id }}, '{{ addslashes($provider->name) }}')"
|
||||
class="text-teal-600 hover:text-teal-800 text-xs font-bold">⚡ 测试</button>
|
||||
<button x-on:click="openEdit({{ $provider->toJson() }})"
|
||||
class="text-indigo-600 hover:text-indigo-800 text-xs font-bold">编辑</button>
|
||||
<form action="{{ route('admin.ai-providers.destroy', $provider->id) }}" method="POST"
|
||||
@@ -333,6 +336,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 AI 接口连通性
|
||||
*/
|
||||
async function testConnection(id, name) {
|
||||
const btn = event.target;
|
||||
const origText = btn.textContent;
|
||||
btn.textContent = '测试中…';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/admin/ai-providers/' + id + '/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
alert(`✅ 「${name}」 连通成功!\n⤵ 响应耗时:${data.ms}ms(包含冷启动)\n🤖 模型回复:${data.message}`);
|
||||
} else {
|
||||
alert(`❌ 「${name}」 连通失败!\n耗时:${data.ms}ms\n错误:${data.message}`);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('请求异常:' + e.message);
|
||||
} finally {
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为默认 AI 厂商
|
||||
*/
|
||||
|
||||
@@ -470,6 +470,7 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad
|
||||
Route::put('/ai-providers/{id}', [\App\Http\Controllers\Admin\AiProviderController::class, 'update'])->name('ai-providers.update');
|
||||
Route::post('/ai-providers/{id}/toggle', [\App\Http\Controllers\Admin\AiProviderController::class, 'toggleEnabled'])->name('ai-providers.toggle');
|
||||
Route::post('/ai-providers/{id}/default', [\App\Http\Controllers\Admin\AiProviderController::class, 'setDefault'])->name('ai-providers.default');
|
||||
Route::post('/ai-providers/{id}/test', [\App\Http\Controllers\Admin\AiProviderController::class, 'testConnection'])->name('ai-providers.test');
|
||||
Route::post('/ai-providers/toggle-chatbot', [\App\Http\Controllers\Admin\AiProviderController::class, 'toggleChatBot'])->name('ai-providers.toggle-chatbot');
|
||||
Route::delete('/ai-providers/{id}', [\App\Http\Controllers\Admin\AiProviderController::class, 'destroy'])->name('ai-providers.destroy');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user