diff --git a/app/Console/Commands/AiHeartbeatCommand.php b/app/Console/Commands/AiHeartbeatCommand.php new file mode 100644 index 0000000..0d59ff1 --- /dev/null +++ b/app/Console/Commands/AiHeartbeatCommand.php @@ -0,0 +1,199 @@ +first(); + if (! $user) { + return Command::SUCCESS; + } + + // 3. 常规心跳经验与金币发放 + // (模拟前端每30-60秒发一次心跳的过程,此处每分钟跑一次,发放单人心跳奖励) + $expGain = $this->parseRewardValue(Sysparam::getValue('exp_per_heartbeat', '1')); + if ($expGain > 0) { + $expMultiplier = $this->vipService->getExpMultiplier($user); + $actualExpGain = (int) round($expGain * $expMultiplier); + $user->exp_num += $actualExpGain; + } + + $jjbGain = $this->parseRewardValue(Sysparam::getValue('jjb_per_heartbeat', '0')); + if ($jjbGain > 0) { + $jjbMultiplier = $this->vipService->getJjbMultiplier($user); + $actualJjbGain = (int) round($jjbGain * $jjbMultiplier); + $user->jjb = ($user->jjb ?? 0) + $actualJjbGain; + } + + $user->save(); + $user->refresh(); + + // 4. 重算等级(基础心跳升级) + $superLevel = (int) Sysparam::getValue('superlevel', '100'); + $leveledUp = $this->calculateNewLevel($user, $superLevel); + + // 5. 随机事件触发 + $eventChance = (int) Sysparam::getValue('auto_event_chance', '10'); + if ($eventChance > 0 && rand(1, 100) <= $eventChance) { + $autoEvent = Autoact::randomEvent(); + if ($autoEvent) { + // 执行随机事件的金钱经验惩奖 + if ($autoEvent->exp_change !== 0) { + $this->currencyService->change( + $user, 'exp', $autoEvent->exp_change, CurrencySource::AUTO_EVENT, "随机事件:{$autoEvent->text_body}", 1 + ); + } + if ($autoEvent->jjb_change !== 0) { + $this->currencyService->change( + $user, 'gold', $autoEvent->jjb_change, CurrencySource::AUTO_EVENT, "随机事件:{$autoEvent->text_body}", 1 + ); + } + + $user->refresh(); + + // 重新计算等级 + if ($this->calculateNewLevel($user, $superLevel)) { + $leveledUp = true; + } + + // 广播随机事件 + $this->broadcastSystemMessage( + '星海小博士', + $autoEvent->renderText($user->username), + match ($autoEvent->event_type) { + 'good' => '#16a34a', + 'bad' => '#dc2626', + default => '#7c3aed', + } + ); + } + } + + // 6. 如果由于心跳或事件导致了升级,广播升级消息 + if ($leveledUp) { + $this->broadcastSystemMessage( + '系统传音', + "🌟 天道酬勤!恭喜侠客【{$user->username}】挂机苦修,境界突破至 LV.{$user->user_level}!", + '#d97706', + '大声宣告' + ); + } + + return Command::SUCCESS; + } + + /** + * 计算并更新用户等级 + */ + private function calculateNewLevel(User $user, int $superLevel): bool + { + $oldLevel = $user->user_level; + + if ($oldLevel >= $superLevel) { + return false; // 管理员不自动升降级 + } + + $newLevel = Sysparam::calculateLevel($user->exp_num); + if ($newLevel !== $oldLevel && $newLevel < $superLevel) { + $user->user_level = $newLevel; + $user->save(); + + return $newLevel > $oldLevel; + } + + return false; + } + + /** + * 解析配置的奖励范围,如 "1" 或 "1-5" + */ + private function parseRewardValue(string $raw): int + { + $raw = trim($raw); + if (str_contains($raw, '-')) { + [$min, $max] = explode('-', $raw, 2); + + return rand((int) $min, (int) $max); + } + + return (int) $raw; + } + + /** + * 往所有活跃房间发送系统广播消息 + */ + private function broadcastSystemMessage(string $fromUser, string $content, string $color, string $action = ''): void + { + $activeRoomIds = $this->chatState->getAllActiveRoomIds(); + if (empty($activeRoomIds)) { + $activeRoomIds = [1]; + } + + foreach ($activeRoomIds as $roomId) { + $sysMsg = [ + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, + 'from_user' => $fromUser, + 'to_user' => '大家', + 'content' => $content, + 'is_secret' => false, + 'font_color' => $color, + 'action' => $action, + 'sent_at' => now()->toDateTimeString(), + ]; + + $this->chatState->pushMessage($roomId, $sysMsg); + broadcast(new MessageSent($roomId, $sysMsg)); + SaveMessageJob::dispatch($sysMsg); + } + } +} diff --git a/app/Enums/CurrencySource.php b/app/Enums/CurrencySource.php index 9101414..f6d88b3 100644 --- a/app/Enums/CurrencySource.php +++ b/app/Enums/CurrencySource.php @@ -44,6 +44,9 @@ enum CurrencySource: string /** AI赠送福利(用户向AI祈求获得的随机奖励) */ case AI_GIFT = 'ai_gift'; + /** 赠人玫瑰(用户或AI对外发放金币红包) */ + case GIFT_SENT = 'gift_sent'; + // ─── 以后新增活动,在这里加一行即可,数据库无需变更 ─────────── // case SIGN_IN = 'sign_in'; // 每日签到 // case TASK_REWARD = 'task_reward'; // 任务奖励 @@ -145,6 +148,7 @@ enum CurrencySource: string self::ADMIN_ADJUST => '管理员调整', self::POSITION_REWARD => '职务奖励', self::AI_GIFT => 'AI赠送', + self::GIFT_SENT => '发红包', self::MARRY_CHARM => '结婚魅力加成', self::DIVORCE_CHARM => '离婚魅力惩罚', self::RING_BUY => '购买戒指', diff --git a/app/Events/ChatBotToggled.php b/app/Events/ChatBotToggled.php new file mode 100644 index 0000000..a98753a --- /dev/null +++ b/app/Events/ChatBotToggled.php @@ -0,0 +1,34 @@ + + */ + public function broadcastOn(): array + { + return [ + new Channel('chat.system'), + ]; + } +} diff --git a/app/Http/Controllers/Admin/AiProviderController.php b/app/Http/Controllers/Admin/AiProviderController.php index 985dcfa..629ddbb 100644 --- a/app/Http/Controllers/Admin/AiProviderController.php +++ b/app/Http/Controllers/Admin/AiProviderController.php @@ -46,8 +46,40 @@ class AiProviderController extends Controller { $providers = AiProviderConfig::orderBy('sort_order')->get(); $chatbotEnabled = Sysparam::getValue('chatbot_enabled', '0') === '1'; + $chatbotMaxGold = Sysparam::getValue('chatbot_max_gold', '5000'); + $chatbotMaxDailyRewards = Sysparam::getValue('chatbot_max_daily_rewards', '1'); - return view('admin.ai-providers.index', compact('providers', 'chatbotEnabled')); + return view('admin.ai-providers.index', compact('providers', 'chatbotEnabled', 'chatbotMaxGold', 'chatbotMaxDailyRewards')); + } + + /** + * 保存全局设置 + */ + public function updateSettings(Request $request): RedirectResponse + { + $data = $request->validate([ + 'chatbot_max_gold' => 'required|integer|min:1', + 'chatbot_max_daily_rewards' => 'required|integer|min:1', + ]); + Sysparam::updateOrCreate( + ['alias' => 'chatbot_max_gold'], + [ + 'body' => (string) $data['chatbot_max_gold'], + 'guidetxt' => '单次最高发放金币金额', + ] + ); + Sysparam::clearCache('chatbot_max_gold'); + + Sysparam::updateOrCreate( + ['alias' => 'chatbot_max_daily_rewards'], + [ + 'body' => (string) $data['chatbot_max_daily_rewards'], + 'guidetxt' => '每个用户单日最多获得金币次数', + ] + ); + Sysparam::clearCache('chatbot_max_daily_rewards'); + + return back()->with('success', '全局设置保存成功!'); } /** @@ -192,11 +224,90 @@ class AiProviderController extends Controller Sysparam::clearCache('chatbot_enabled'); $status = $newValue === '1' ? '开启' : '关闭'; + $isEnabled = $newValue === '1'; + + // 确保 AI 实体账号存在 + $user = \App\Models\User::firstOrCreate( + ['username' => 'AI小班长'], + [ + 'password' => \Illuminate\Support\Facades\Hash::make(\Illuminate\Support\Str::random(16)), + 'user_level' => 10, + 'sex' => 0, // 女性 + 'usersf' => 'storage/avatars/ai_bot_cn_girl.png', + 'jjb' => 1000000, + 'sign' => '本群首席智慧小管家', + ] + ); + + // 防止后期头像变动,强制更新到最新女生头像 + if (! str_contains($user->usersf ?? '', 'ai_bot_cn_girl.png')) { + $user->update([ + 'usersf' => 'storage/avatars/ai_bot_cn_girl.png', + 'sex' => 0, + ]); + } + + $userData = [ + 'user_id' => $user->id, + 'username' => $user->username, + 'level' => $user->user_level, + 'sex' => $user->sex, + 'headface' => $user->headface, + 'vip_icon' => $user->vipIcon(), + 'vip_name' => $user->vipName(), + 'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '', + 'is_admin' => false, + 'position_icon' => '', + 'position_name' => '', + ]; + + // 广播机器人进出事件(供前端名单增删) + broadcast(new \App\Events\ChatBotToggled($userData, $isEnabled)); + + // 像真实的玩家一样,对全网活跃房间进行高调进出场播报 + $activeRoomIds = $this->chatState->getAllActiveRoomIds(); + if (empty($activeRoomIds)) { + $activeRoomIds = [1]; // 兜底 + } + + // 把 AI 实体挂名到一个主房间,即可被 app/Console/Commands/AutoSaveExp.php 扫描发经验 + $mainRoomId = $activeRoomIds[0]; + if ($isEnabled) { + $this->chatState->userJoin($mainRoomId, $user->username, $userData); + } else { + // 清理可能存在的所有房间的残留挂名 + foreach ($activeRoomIds as $rId) { + $this->chatState->userLeave($rId, $user->username); + } + } + + foreach ($activeRoomIds as $roomId) { + $content = $isEnabled + ? '🤖 【AI小班长】 迈着整齐的步伐进入了房间,随时为您服务!' + : '🤖 【AI小班长】 去休息啦,大家聊得开心!'; + + $botMsg = [ + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, + 'from_user' => '进出播报', + 'to_user' => '大家', + 'content' => $content, + 'is_secret' => false, + 'font_color' => '#9333ea', + 'action' => 'system_welcome', + 'welcome_user' => $user->username, + 'sent_at' => now()->toDateTimeString(), + ]; + + $this->chatState->pushMessage($roomId, $botMsg); + broadcast(new \App\Events\MessageSent($roomId, $botMsg)); + \App\Jobs\SaveMessageJob::dispatch($botMsg); + } return response()->json([ 'status' => 'success', 'message' => "聊天机器人已{$status}", - 'enabled' => $newValue === '1', + 'enabled' => $isEnabled, ]); } diff --git a/app/Http/Controllers/ChatBotController.php b/app/Http/Controllers/ChatBotController.php index c010b06..1e5c3c4 100644 --- a/app/Http/Controllers/ChatBotController.php +++ b/app/Http/Controllers/ChatBotController.php @@ -62,6 +62,11 @@ class ChatBotController extends Controller ], 403); } + $aiUser = \App\Models\User::where('username', 'AI小班长')->first(); + if ($aiUser) { + $aiUser->increment('exp_num', 1); + } + $user = Auth::user(); $message = $request->input('message'); $roomId = $request->input('room_id'); @@ -92,40 +97,61 @@ class ChatBotController extends Controller $reply = str_replace('[ACTION:GIVE_GOLD]', '', $reply); $reply = trim($reply); + $maxDailyRewards = (int) Sysparam::getValue('chatbot_max_daily_rewards', '1'); + $maxGold = (int) Sysparam::getValue('chatbot_max_gold', '5000'); + $redisKey = 'ai_chat:give_gold:'.date('Ymd').':'.$user->id; - // 原子操作,防止并发多次领取 - if (Redis::setnx($redisKey, 1)) { - Redis::expire($redisKey, 86400); // 缓存 24 小时 + $dailyCount = (int) Redis::get($redisKey); - // 给用户发放随机 100~5000 金币 - $goldAmount = rand(100, 5000); - $this->currencyService->change( - $user, - 'gold', - $goldAmount, - CurrencySource::AI_GIFT, - 'AI小班长发善心赠送的金币福利', - $roomId - ); + if ($dailyCount < $maxDailyRewards) { + $goldAmount = rand(100, $maxGold); - // 发送全场大广播 - $sysMsg = [ - 'id' => $this->chatState->nextMessageId($roomId), - 'room_id' => $roomId, - 'from_user' => '系统传音', - 'to_user' => '大家', - 'content' => "🤖 听闻小萌新哭穷,AI小班长看【{$user->username}】骨骼惊奇,大方地赏赐了 {$goldAmount} 枚金币福利!", - 'is_secret' => false, - 'font_color' => '#d97706', // 橙色醒目 - 'action' => '大声宣告', - 'sent_at' => now()->toDateTimeString(), - ]; - $this->chatState->pushMessage($roomId, $sysMsg); - broadcast(new MessageSent($roomId, $sysMsg)); - SaveMessageJob::dispatch($sysMsg); + if ($aiUser && $aiUser->jjb >= $goldAmount) { + Redis::incr($redisKey); + Redis::expire($redisKey, 86400); // 缓存 24 小时 + + // 真实扣除 AI 金币 + $this->currencyService->change( + $aiUser, + 'gold', + -$goldAmount, + CurrencySource::GIFT_SENT, + "赏赐给 {$user->username} 的金币福利", + $roomId + ); + + // 给用户发放金币 + $this->currencyService->change( + $user, + 'gold', + $goldAmount, + CurrencySource::AI_GIFT, + 'AI小班长发善心赠送的金币福利', + $roomId + ); + + // 发送全场大广播 + $sysMsg = [ + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, + 'from_user' => 'AI小班长', + 'to_user' => $user->username, + 'content' => "🤖 听闻小萌新哭穷,本班长看你骨骼惊奇,大方地赏赐了 {$goldAmount} 枚金币福利!", + 'is_secret' => false, + 'font_color' => '#d97706', // 橙色醒目 + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), + ]; + $this->chatState->pushMessage($roomId, $sysMsg); + broadcast(new MessageSent($roomId, $sysMsg)); + SaveMessageJob::dispatch($sysMsg); + } else { + // 如果余额不足 + $reply .= "\n\n(哎呀,我这个月的工资花光啦,没钱发金币了,大家多赏点吧~)"; + } } else { // 如果已经领过了,修改回复提醒 - $reply = $reply."\n\n(系统提示:你今天已经领过金币福利啦,每天只能领一次哦!)"; + $reply .= "\n\n(系统提示:你今天已经领过金币福利啦,把机会留给其他人吧!)"; } } diff --git a/app/Services/AiChatService.php b/app/Services/AiChatService.php index 97a7cab..f6a90ba 100644 --- a/app/Services/AiChatService.php +++ b/app/Services/AiChatService.php @@ -89,7 +89,8 @@ class AiChatService $guideRulesText = $this->getDynamicGuideRules(); return << { + console.log("机器人开关:", e); + window.dispatchEvent(new CustomEvent("chat:bot-toggled", { detail: e })); + }); + // 加入带有登录人员追踪的 Presence Channel window.Echo.join(`room.${roomId}`) // 当自己成功连接时,获取当前在这里的所有人列表 diff --git a/resources/views/admin/ai-providers/index.blade.php b/resources/views/admin/ai-providers/index.blade.php index 4167b5e..bc8e7d5 100644 --- a/resources/views/admin/ai-providers/index.blade.php +++ b/resources/views/admin/ai-providers/index.blade.php @@ -67,28 +67,56 @@ }, }"> - {{-- 全局开关 + 操作栏 --}} + {{-- 全局开关 + 高级设置 --}}
-
-
-

🤖 AI 聊天机器人

-
- 全局开关: - - - {{ $chatbotEnabled ? '已开启' : '已关闭' }} - +
+ +
+
+

🤖 AI 聊天机器人配置

+
+ 大厅状态: + + + {{ $chatbotEnabled ? '已开启' : '已关闭' }} + +
+
- + + +
+ @csrf +
+ +
+ + +
+
+
+ +
+ + +
+
+ +
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