新增: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 厂商配置
|
* 删除 AI 厂商配置
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class AiChatService
|
|||||||
/**
|
/**
|
||||||
* AI 请求超时时间(秒)
|
* AI 请求超时时间(秒)
|
||||||
*/
|
*/
|
||||||
private const REQUEST_TIMEOUT = 30;
|
private const REQUEST_TIMEOUT = 120; // Ollama 本地模型冷启动较慢,给足时间
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis 上下文 key 前缀
|
* Redis 上下文 key 前缀
|
||||||
@@ -124,7 +124,7 @@ PROMPT;
|
|||||||
// 将用户消息加入上下文(包含发送者信息)
|
// 将用户消息加入上下文(包含发送者信息)
|
||||||
$context[] = [
|
$context[] = [
|
||||||
'role' => 'user',
|
'role' => 'user',
|
||||||
'content' => "【当前发言人:{$username}】\n" . $message
|
'content' => "【当前发言人:{$username}】\n".$message,
|
||||||
];
|
];
|
||||||
|
|
||||||
// 构建完整的 messages 数组(系统提示 + 对话上下文)
|
// 构建完整的 messages 数组(系统提示 + 对话上下文)
|
||||||
|
|||||||
@@ -148,6 +148,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
<div class="flex items-center justify-center gap-2">
|
<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() }})"
|
<button x-on:click="openEdit({{ $provider->toJson() }})"
|
||||||
class="text-indigo-600 hover:text-indigo-800 text-xs font-bold">编辑</button>
|
class="text-indigo-600 hover:text-indigo-800 text-xs font-bold">编辑</button>
|
||||||
<form action="{{ route('admin.ai-providers.destroy', $provider->id) }}" method="POST"
|
<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 厂商
|
* 设为默认 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::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}/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}/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::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');
|
Route::delete('/ai-providers/{id}', [\App\Http\Controllers\Admin\AiProviderController::class, 'destroy'])->name('ai-providers.destroy');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user