新增:AI 接口连通性测试功能;修复:Ollama 超时问题

- 后台 AI 厂商列表新增「 测试」按钮,实时验证接口连通性
- 显示响应耗时(含冷启动)和模型返回内容
- AiChatService 请求超时从 30s 调整为 120s(兼容 Ollama 本地冷启动)
- 测试接口超时设为 60s
This commit is contained in:
2026-03-06 03:29:13 +08:00
parent 6c9db806ae
commit 318eb6f234
4 changed files with 101 additions and 4 deletions

View File

@@ -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 厂商配置
*

View File

@@ -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 数组(系统提示 + 对话上下文)

View File

@@ -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 厂商
*/

View File

@@ -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');