421 lines
21 KiB
PHP
421 lines
21 KiB
PHP
{{--
|
||
文件功能:AI 厂商配置管理页面
|
||
|
||
提供 AI 厂商的完整 CRUD 管理:
|
||
- 列表展示所有配置(名称、模型、状态等)
|
||
- 新增/编辑厂商配置弹窗
|
||
- 启用/禁用切换、设为默认、删除
|
||
- 全局开关控制聊天机器人是否启用
|
||
|
||
@author ChatRoom Laravel
|
||
@version 1.0.0
|
||
--}}
|
||
|
||
@extends('admin.layouts.app')
|
||
|
||
@section('title', 'AI 厂商配置')
|
||
|
||
@section('content')
|
||
<div x-data="{
|
||
showForm: false,
|
||
editId: null,
|
||
form: {
|
||
provider: '',
|
||
name: '',
|
||
api_key: '',
|
||
api_endpoint: '',
|
||
model: '',
|
||
temperature: 0.3,
|
||
max_tokens: 2048,
|
||
sort_order: 0,
|
||
},
|
||
|
||
/**
|
||
* 打开新增弹窗,重置表单
|
||
*/
|
||
openNew() {
|
||
this.editId = null;
|
||
this.form = {
|
||
provider: '',
|
||
name: '',
|
||
api_key: '',
|
||
api_endpoint: '',
|
||
model: '',
|
||
temperature: 0.3,
|
||
max_tokens: 2048,
|
||
sort_order: 0,
|
||
};
|
||
this.showForm = true;
|
||
},
|
||
|
||
/**
|
||
* 打开编辑弹窗,填充现有数据
|
||
*/
|
||
openEdit(provider) {
|
||
this.editId = provider.id;
|
||
this.form = {
|
||
provider: provider.provider,
|
||
name: provider.name,
|
||
api_key: '', // 编辑时不回填 API Key(已加密)
|
||
api_endpoint: provider.api_endpoint,
|
||
model: provider.model,
|
||
temperature: provider.temperature,
|
||
max_tokens: provider.max_tokens,
|
||
sort_order: provider.sort_order,
|
||
};
|
||
this.showForm = true;
|
||
},
|
||
}">
|
||
|
||
{{-- 全局开关 + 高级设置 --}}
|
||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6">
|
||
<div class="p-6">
|
||
<!-- 顶部栏:开关和添加按钮 -->
|
||
<div class="flex items-center justify-between border-b border-gray-100 pb-4 mb-4">
|
||
<div class="flex items-center gap-4">
|
||
<h2 class="text-lg font-bold text-gray-800">🤖 AI 聊天机器人配置</h2>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-sm text-gray-500">大厅状态:</span>
|
||
<button id="chatbot-toggle-btn" onclick="toggleChatBot()"
|
||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {{ $chatbotEnabled ? 'bg-emerald-500' : 'bg-gray-300' }}">
|
||
<span
|
||
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $chatbotEnabled ? 'translate-x-6' : 'translate-x-1' }}"></span>
|
||
</button>
|
||
<span id="chatbot-status-text"
|
||
class="text-sm font-bold {{ $chatbotEnabled ? 'text-emerald-600' : 'text-gray-400' }}">
|
||
{{ $chatbotEnabled ? '已开启' : '已关闭' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<button x-on:click="openNew()"
|
||
class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition font-bold text-sm">
|
||
+ 添加 AI 厂商
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 交互参数表单 -->
|
||
<form action="{{ route('admin.ai-providers.update-settings') }}" method="POST" class="flex flex-wrap items-end gap-6">
|
||
@csrf
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">系统打赏单次最高金币(上限)</label>
|
||
<div class="relative">
|
||
<input type="number" name="chatbot_max_gold" value="{{ $chatbotMaxGold }}" min="1" required
|
||
class="w-64 border border-gray-300 rounded-md p-2 pl-3 pr-8 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
<span class="absolute right-3 top-2.5 text-gray-400 text-sm">枚</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">每人每日最高获取红包次数限制</label>
|
||
<div class="relative">
|
||
<input type="number" name="chatbot_max_daily_rewards" value="{{ $chatbotMaxDailyRewards }}" min="1" required
|
||
class="w-64 border border-gray-300 rounded-md p-2 pl-3 pr-8 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
<span class="absolute right-3 top-2.5 text-gray-400 text-sm">次</span>
|
||
</div>
|
||
</div>
|
||
<button type="submit"
|
||
class="px-6 py-2 bg-slate-800 text-white rounded-md font-bold hover:bg-slate-900 text-sm transition h-[38px]">
|
||
保存运行参数
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 厂商列表 --}}
|
||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||
<table class="w-full text-sm">
|
||
<thead class="bg-gray-50 text-gray-600">
|
||
<tr>
|
||
<th class="px-6 py-3 text-left font-bold">厂商</th>
|
||
<th class="px-6 py-3 text-left font-bold">模型</th>
|
||
<th class="px-6 py-3 text-left font-bold">API 端点</th>
|
||
<th class="px-6 py-3 text-center font-bold">参数</th>
|
||
<th class="px-6 py-3 text-center font-bold">排序</th>
|
||
<th class="px-6 py-3 text-center font-bold">状态</th>
|
||
<th class="px-6 py-3 text-center font-bold">默认</th>
|
||
<th class="px-6 py-3 text-center font-bold">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100">
|
||
@forelse ($providers as $provider)
|
||
<tr class="hover:bg-gray-50 transition {{ !$provider->is_enabled ? 'opacity-50' : '' }}">
|
||
<td class="px-6 py-4">
|
||
<div class="font-bold text-gray-800">{{ $provider->name }}</div>
|
||
<div class="text-xs text-gray-400">{{ $provider->provider }}</div>
|
||
</td>
|
||
<td class="px-6 py-4">
|
||
<code class="bg-gray-100 px-2 py-1 rounded text-xs">{{ $provider->model }}</code>
|
||
</td>
|
||
<td class="px-6 py-4">
|
||
<span class="text-xs text-gray-500 truncate block max-w-[200px]"
|
||
title="{{ $provider->api_endpoint }}">
|
||
{{ $provider->api_endpoint }}
|
||
</span>
|
||
</td>
|
||
<td class="px-6 py-4 text-center text-xs text-gray-500">
|
||
T={{ $provider->temperature }} / {{ $provider->max_tokens }}
|
||
</td>
|
||
<td class="px-6 py-4 text-center text-gray-500">
|
||
{{ $provider->sort_order }}
|
||
</td>
|
||
<td class="px-6 py-4 text-center">
|
||
<button onclick="toggleProvider({{ $provider->id }}, this)"
|
||
class="px-3 py-1 rounded-full text-xs font-bold {{ $provider->is_enabled ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-100 text-gray-500' }}">
|
||
{{ $provider->is_enabled ? '已启用' : '已禁用' }}
|
||
</button>
|
||
</td>
|
||
<td class="px-6 py-4 text-center">
|
||
@if ($provider->is_default)
|
||
<span class="px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700">★
|
||
默认</span>
|
||
@else
|
||
<button onclick="setDefault({{ $provider->id }})"
|
||
class="px-3 py-1 rounded-full text-xs text-gray-400 hover:text-amber-600 hover:bg-amber-50 transition">
|
||
设为默认
|
||
</button>
|
||
@endif
|
||
</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"
|
||
onsubmit="return confirm('确定要删除 {{ $provider->name }} 吗?')">
|
||
@csrf
|
||
@method('DELETE')
|
||
<button type="submit"
|
||
class="text-red-500 hover:text-red-700 text-xs font-bold">删除</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="8" class="px-6 py-12 text-center text-gray-400">
|
||
暂无 AI 厂商配置,请点击上方"添加 AI 厂商"按钮。
|
||
</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{{-- 新增/编辑弹窗 --}}
|
||
<div x-show="showForm" x-cloak style="display: none;"
|
||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" x-on:click.self="showForm = false">
|
||
<div class="bg-white rounded-xl shadow-xl w-[560px] max-h-[90vh] overflow-y-auto" x-transition>
|
||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||
<h3 class="text-lg font-bold" x-text="editId ? '编辑 AI 厂商' : '添加 AI 厂商'"></h3>
|
||
<button x-on:click="showForm = false" class="text-gray-400 hover:text-gray-600 text-xl">×</button>
|
||
</div>
|
||
|
||
<form
|
||
:action="editId ? '{{ url('admin/ai-providers') }}/' + editId : '{{ route('admin.ai-providers.store') }}'"
|
||
method="POST" class="p-6 space-y-4">
|
||
@csrf
|
||
<template x-if="editId">
|
||
<input type="hidden" name="_method" value="PUT">
|
||
</template>
|
||
|
||
<div class="grid grid-cols-2 gap-4">
|
||
{{-- 厂商标识 --}}
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">厂商标识 <span
|
||
class="text-red-500">*</span></label>
|
||
<input type="text" name="provider" x-model="form.provider" required
|
||
placeholder="如:deepseek, qwen, openai"
|
||
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
</div>
|
||
{{-- 显示名称 --}}
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">显示名称 <span
|
||
class="text-red-500">*</span></label>
|
||
<input type="text" name="name" x-model="form.name" required placeholder="如:DeepSeek、通义千问"
|
||
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
</div>
|
||
</div>
|
||
|
||
{{-- API Key --}}
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">
|
||
API Key
|
||
<span x-show="!editId" class="text-red-500">*</span>
|
||
<span x-show="editId" class="text-gray-400 font-normal">(留空表示不修改)</span>
|
||
</label>
|
||
<input type="password" name="api_key" x-model="form.api_key" :required="!editId"
|
||
placeholder="sk-xxxxxxxxxxxx"
|
||
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500 font-mono">
|
||
</div>
|
||
|
||
{{-- API 端点 --}}
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">API 端点 <span
|
||
class="text-red-500">*</span></label>
|
||
<input type="url" name="api_endpoint" x-model="form.api_endpoint" required
|
||
placeholder="https://api.deepseek.com"
|
||
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
<p class="text-xs text-gray-400 mt-1">系统会自动拼接 /v1/chat/completions</p>
|
||
</div>
|
||
|
||
{{-- 模型 --}}
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">模型名称 <span
|
||
class="text-red-500">*</span></label>
|
||
<input type="text" name="model" x-model="form.model" required
|
||
placeholder="如:deepseek-chat, qwen-turbo"
|
||
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
</div>
|
||
|
||
<div class="grid grid-cols-3 gap-4">
|
||
{{-- Temperature --}}
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">Temperature</label>
|
||
<input type="number" name="temperature" x-model="form.temperature" step="0.1"
|
||
min="0" max="2"
|
||
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
</div>
|
||
{{-- Max Tokens --}}
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">最大 Tokens</label>
|
||
<input type="number" name="max_tokens" x-model="form.max_tokens" min="100"
|
||
max="32000"
|
||
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
</div>
|
||
{{-- Sort Order --}}
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-1">排序</label>
|
||
<input type="number" name="sort_order" x-model="form.sort_order" min="0"
|
||
class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||
<p class="text-xs text-gray-400 mt-1">故障转移时按此排序</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pt-4 border-t flex justify-end gap-3">
|
||
<button type="button" x-on:click="showForm = false"
|
||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md text-sm transition">取消</button>
|
||
<button type="submit"
|
||
class="px-6 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 text-sm transition">
|
||
保存
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/**
|
||
* 切换全局聊天机器人开关
|
||
*/
|
||
async function toggleChatBot() {
|
||
try {
|
||
const res = await fetch('{{ route('admin.ai-providers.toggle-chatbot') }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||
'Accept': 'application/json',
|
||
},
|
||
});
|
||
const data = await res.json();
|
||
if (data.status === 'success') {
|
||
// 更新按钮样式
|
||
const btn = document.getElementById('chatbot-toggle-btn');
|
||
const text = document.getElementById('chatbot-status-text');
|
||
if (data.enabled) {
|
||
btn.className = btn.className.replace('bg-gray-300', 'bg-emerald-500');
|
||
btn.firstElementChild.className = btn.firstElementChild.className.replace('translate-x-1',
|
||
'translate-x-6');
|
||
text.textContent = '已开启';
|
||
text.className = text.className.replace('text-gray-400', 'text-emerald-600');
|
||
} else {
|
||
btn.className = btn.className.replace('bg-emerald-500', 'bg-gray-300');
|
||
btn.firstElementChild.className = btn.firstElementChild.className.replace('translate-x-6',
|
||
'translate-x-1');
|
||
text.textContent = '已关闭';
|
||
text.className = text.className.replace('text-emerald-600', 'text-gray-400');
|
||
}
|
||
alert(data.message);
|
||
}
|
||
} catch (e) {
|
||
alert('操作失败:' + e.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换厂商启用/禁用状态
|
||
*/
|
||
async function toggleProvider(id, btn) {
|
||
try {
|
||
const res = await fetch('/admin/ai-providers/' + id + '/toggle', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||
'Accept': 'application/json',
|
||
},
|
||
});
|
||
const data = await res.json();
|
||
if (data.status === 'success') {
|
||
location.reload();
|
||
}
|
||
} catch (e) {
|
||
alert('操作失败:' + e.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 测试 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 厂商
|
||
*/
|
||
async function setDefault(id) {
|
||
try {
|
||
const res = await fetch('/admin/ai-providers/' + id + '/default', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||
'Accept': 'application/json',
|
||
},
|
||
});
|
||
const data = await res.json();
|
||
if (data.status === 'success') {
|
||
location.reload();
|
||
}
|
||
} catch (e) {
|
||
alert('操作失败:' + e.message);
|
||
}
|
||
}
|
||
</script>
|
||
@endsection
|