-
🤖 AI 聊天机器人
-
-
全局开关:
-
-
- {{ $chatbotEnabled ? '已开启' : '已关闭' }}
-
+
+
+
+
+
🤖 AI 聊天机器人配置
+
+ 大厅状态:
+
+
+ {{ $chatbotEnabled ? '已开启' : '已关闭' }}
+
+
+
-
+
+
+
diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php
index eb8a409..44139d5 100644
--- a/resources/views/chat/frame.blade.php
+++ b/resources/views/chat/frame.blade.php
@@ -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;
diff --git a/resources/views/chat/partials/ai-chatbot.blade.php b/resources/views/chat/partials/ai-chatbot.blade.php
index 158a680..5c7b56e 100644
--- a/resources/views/chat/partials/ai-chatbot.blade.php
+++ b/resources/views/chat/partials/ai-chatbot.blade.php
@@ -13,16 +13,6 @@
}
chatBotSending = true;
- // 显示"思考中"提示
- // 延迟显示"思考中",让广播消息先到达
- const thinkDiv = document.createElement('div');
- thinkDiv.className = 'msg-line';
- thinkDiv.innerHTML = '
🤖 AI小班长 正在思考中...';
- 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 = '
🤖【AI小班长】网络连接错误,请稍后重试';
diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php
index 1763b7b..4d79f4c 100644
--- a/resources/views/chat/partials/scripts.blade.php
+++ b/resources/views/chat/partials/scripts.blade.php
@@ -257,20 +257,7 @@
};
targetContainer.appendChild(allDiv);
- // ── AI 小助手(仅当全局开关开启时显示,与普通用户风格一致)──
- if (window.chatContext.chatBotEnabled) {
- let botDiv = document.createElement('div');
- botDiv.className = 'user-item';
- botDiv.innerHTML = `
-

-
AI小班长🤖
- `;
- 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 = '
';
- 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 `
${uName}`;
+ return `
${uName}`;
}
if (systemUsers.includes(uName) || isGameLabel(uName)) {
return `
${uName}`;
@@ -530,14 +514,11 @@
return `
${uName}`;
};
- // 系统播报用户(不包含 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;
}
// ── 神秘箱子暗号拦截 ────────────────────────────────────
diff --git a/routes/console.php b/routes/console.php
index 00a5095..18ae635 100644
--- a/routes/console.php
+++ b/routes/console.php
@@ -14,6 +14,9 @@ Schedule::command('messages:purge')->dailyAt('03:00');
// 每 5 分钟为所有在线用户自动存点(经验/金币/等级)
Schedule::command('chatroom:auto-save-exp')->everyFiveMinutes();
+// 每 1 分钟为 AI小班长 独立模拟一次挂机心跳,触发随机事件
+Schedule::command('chatroom:ai-heartbeat')->everyMinute();
+
// 每 15 分钟:关闭掉线用户的开放职务日志(久无心跳 = 掉线,自动写入 logout_at)
Schedule::command('duty:close-stale-logs')->everyFifteenMinutes();
diff --git a/routes/web.php b/routes/web.php
index c9a4a90..b97b216 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -528,6 +528,7 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad
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/update-settings', [\App\Http\Controllers\Admin\AiProviderController::class, 'updateSettings'])->name('ai-providers.update-settings');
Route::delete('/ai-providers/{id}', [\App\Http\Controllers\Admin\AiProviderController::class, 'destroy'])->name('ai-providers.destroy');
// 开发日志管理
diff --git a/storage/app/public/avatars/ai_bot_cn_girl.png b/storage/app/public/avatars/ai_bot_cn_girl.png
new file mode 100644
index 0000000..dd112bb
Binary files /dev/null and b/storage/app/public/avatars/ai_bot_cn_girl.png differ