feat(ai): 将小班长升级为完全独立的实体用户并支持随机金币发放及持续在线刷级,设定为女兵人设并使用自定义头像

This commit is contained in:
2026-03-26 11:15:11 +08:00
parent c13bb5f35c
commit 4d60893dbe
16 changed files with 523 additions and 101 deletions
+7
View File
@@ -9,6 +9,13 @@ export function initChat(roomId) {
return;
}
// 监听全局系统事件(如 AI 机器人开关)
window.Echo.channel('chat.system')
.listen('ChatBotToggled', (e) => {
console.log("机器人开关:", e);
window.dispatchEvent(new CustomEvent("chat:bot-toggled", { detail: e }));
});
// 加入带有登录人员追踪的 Presence Channel
window.Echo.join(`room.${roomId}`)
// 当自己成功连接时,获取当前在这里的所有人列表
@@ -67,28 +67,56 @@
},
}">
{{-- 全局开关 + 操作栏 --}}
{{-- 全局开关 + 高级设置 --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6">
<div class="p-6 flex items-center justify-between">
<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 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>
<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>
<!-- 交互参数表单 -->
<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>
+24 -1
View File
@@ -45,7 +45,30 @@
fishReelUrl: "{{ route('fishing.reel', $room->id) }}",
chatBotUrl: "{{ route('chatbot.chat') }}",
chatBotClearUrl: "{{ route('chatbot.clear') }}",
chatBotEnabled: {{ \App\Models\Sysparam::getValue('chatbot_enabled', '0') === '1' ? 'true' : 'false' }},
@php
$chatbotEnabledState = \App\Models\Sysparam::getValue('chatbot_enabled', '0') === '1';
$botUserData = null;
if ($chatbotEnabledState) {
$botUser = \App\Models\User::where('username', 'AI小班长')->first();
if ($botUser) {
$botUserData = [
'user_id' => $botUser->id,
'username' => $botUser->username,
'level' => $botUser->user_level,
'sex' => $botUser->sex,
'headfaceUrl' => $botUser->headfaceUrl,
'vip_icon' => $botUser->vipIcon(),
'vip_name' => $botUser->vipName(),
'vip_color' => $botUser->isVip() ? ($botUser->vipLevel?->color ?? '') : '',
'is_admin' => false,
'position_icon' => '',
'position_name' => '',
];
}
}
@endphp
chatBotEnabled: {{ $chatbotEnabledState ? 'true' : 'false' }},
botUser: @json($botUserData),
hasPosition: {{ Auth::user()->activePosition || Auth::user()->user_level >= $superLevel ? 'true' : 'false' }},
@php
$activePos = Auth::user()->activePosition;
@@ -13,16 +13,6 @@
}
chatBotSending = true;
// 显示"思考中"提示
// 延迟显示"思考中",让广播消息先到达
const thinkDiv = document.createElement('div');
thinkDiv.className = 'msg-line';
thinkDiv.innerHTML = '<span style="color: #16a34a;">🤖 <b>AI小班长</b> 正在思考中...</span>';
setTimeout(() => {
container2.appendChild(thinkDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
}, 500);
try {
const res = await fetch(window.chatContext.chatBotUrl, {
method: 'POST',
@@ -40,9 +30,6 @@
const data = await res.json();
// 移除"思考中"提示(消息已通过广播显示)
thinkDiv.remove();
if (!res.ok || data.status !== 'success') {
const errDiv = document.createElement('div');
errDiv.className = 'msg-line';
@@ -51,7 +38,6 @@
container.appendChild(errDiv);
}
} catch (e) {
thinkDiv.remove();
const errDiv = document.createElement('div');
errDiv.className = 'msg-line';
errDiv.innerHTML = '<span style="color: #dc2626;">🤖【AI小班长】网络连接错误,请稍后重试</span>';
+33 -35
View File
@@ -257,20 +257,7 @@
};
targetContainer.appendChild(allDiv);
// ── AI 小助手(仅当全局开关开启时显示,与普通用户风格一致)──
if (window.chatContext.chatBotEnabled) {
let botDiv = document.createElement('div');
botDiv.className = 'user-item';
botDiv.innerHTML = `
<img class="user-head" src="/images/ai_bot.png" onerror="this.src='/images/headface/1.gif'">
<span class="user-name">AI小班长</span><span style="font-size:12px; margin-left:2px;" title="聊天机器人">🤖</span>
`;
botDiv.onclick = () => {
toUserSelect.value = 'AI小班长';
document.getElementById('content').focus();
};
targetContainer.appendChild(botDiv);
}
// ── AI 小助手(已在 onlineUsers 中动态维护,移除硬编码)──
// 构建用户数组并排序
let userArr = [];
@@ -374,19 +361,16 @@
// 调用核心渲染(桌面端名单容器)
_renderUserListToContainer(userList, sortBy, keyword);
// 重新填充发言对象下拉框(不过滤关键词,始终显示全部用户)
toUserSelect.innerHTML = '<option value="大家">大家</option>';
if (window.chatContext.chatBotEnabled) {
let botOption = document.createElement('option');
botOption.value = 'AI小班长';
botOption.textContent = '🤖 AI小班长';
toUserSelect.appendChild(botOption);
}
// 下拉框里如果 AI在场,可以直接选
for (let username in onlineUsers) {
if (username !== window.chatContext.username) {
let option = document.createElement('option');
option.value = username;
option.textContent = username;
let text = username;
if (username === 'AI小班长') {
text = '🤖 AI小班长';
}
option.textContent = text;
toUserSelect.appendChild(option);
}
}
@@ -445,7 +429,7 @@
let timeStrOverride = false;
// 系统用户名列表(不可被选为聊天对象)
const systemUsers = ['钓鱼播报', '星海小博士', '系统传音', '系统公告', 'AI小班长', '送花播报', '系统', '欢迎'];
const systemUsers = ['钓鱼播报', '星海小博士', '系统传音', '系统公告', '送花播报', '系统', '欢迎'];
// 动作文字映射表:情绪型(着/地,放"对"之前)和动作型(了,替换"对X说"
const actionTextMap = {
@@ -522,7 +506,7 @@
// 用户名(单击切换发言对象,双击查看资料;系统用户或游戏标签仅显示文本)
const clickableUser = (uName, color) => {
if (uName === 'AI小班长') {
return `<span class="msg-user" data-u="${uName}" style="color: ${color}; cursor: pointer;" onclick="switchTarget('${uName}')">${uName}</span>`;
return `<span class="msg-user" data-u="${uName}" style="color: ${color}; cursor: pointer;" onclick="switchTarget('${uName}')" ondblclick="openUserCard('${uName}')">${uName}</span>`;
}
if (systemUsers.includes(uName) || isGameLabel(uName)) {
return `<span class="msg-user" style="color: ${color};">${uName}</span>`;
@@ -530,14 +514,11 @@
return `<span class="msg-user" data-u="${uName}" style="color: ${color}; cursor: pointer;" onclick="switchTarget('${uName}')" ondblclick="openUserCard('${uName}')">${uName}</span>`;
};
// 系统播报用户(不包含 AI小班长)使用军号图标,AI小班长用专属图,普通用户用头像
const buggleUsers = ['钓鱼播报', '星海小博士', '送花播报', '系统传音', '系统公告'];
// 普通用户(包括 AI小班长)用数据库头像,播报类用特殊喇叭图标
const senderInfo = onlineUsers[msg.from_user];
const senderHead = ((senderInfo && senderInfo.headface) || '1.gif').toLowerCase();
let headImgSrc = senderHead.startsWith('storage/') ? '/' + senderHead : `/images/headface/${senderHead}`;
if (msg.from_user === 'AI小班长') {
headImgSrc = '/images/ai_bot.png';
} else if (buggleUsers.includes(msg.from_user)) {
if (msg.from_user.endsWith('播报') || msg.from_user === '星海小博士' || msg.from_user === '系统传音' || msg.from_user === '系统公告') {
headImgSrc = '/images/bugle.png';
}
const headImg =
@@ -733,6 +714,12 @@
users.forEach(u => {
onlineUsers[u.username] = u;
});
// 初始加载时,如果全局且开启,注入 AI
if (window.chatContext.chatBotEnabled && window.chatContext.botUser) {
onlineUsers['AI小班长'] = window.chatContext.botUser;
}
renderUserList();
// 管理员自己进房时,在本地播放烟花(服务端广播可能在 WS 连上前已发出)
@@ -743,6 +730,21 @@
}
});
// 监听机器人动态开关
window.addEventListener('chat:bot-toggled', (e) => {
const detail = e.detail;
window.chatContext.chatBotEnabled = detail.isOnline;
if (detail.isOnline && detail.user && detail.user.username) {
onlineUsers[detail.user.username] = detail.user;
window.chatContext.botUser = detail.user;
} else {
delete onlineUsers['AI小班长'];
window.chatContext.botUser = null;
}
renderUserList();
});
window.addEventListener('chat:joining', (e) => {
const user = e.detail;
onlineUsers[user.username] = user;
@@ -1192,14 +1194,10 @@
return;
}
// 如果发言对象是 AI 小助手,专用机器人 API
// 如果发言对象是 AI 小助手,也发送一份给专用机器人 API,不打断正常的发消息流程
const toUser = formData.get('to_user');
if (toUser === 'AI小班长') {
contentInput.value = '';
contentInput.focus();
_isSending = false;
sendToChatBot(content); // 异步调用,不阻塞全局发送
return;
}
// ── 神秘箱子暗号拦截 ────────────────────────────────────