can(self::PERMISSION)) { throw new McpException('当前账号没有“允许 AI 使用后台浏览器”权限(ai.mcp/console),请联系管理员在角色里勾选', 'no_console_permission', 403); } // 第三个参数 1:同一终端已有未过期的会话就沿用,不轮换令牌(不影响其他终端) AdminTokenService::setToken($identity->adminId, self::TERMINAL, 1); $session = AdminSession::where(['admin_id' => $identity->adminId, 'terminal' => self::TERMINAL])->findOrEmpty(); if ($session->isEmpty()) { throw new McpException('后台会话创建失败,请稍后再试', 'console_failed', 500); } $now = time(); $session->expire_time = $now + McpConfig::consoleTtlMinutes() * 60; $session->update_time = $now; $session->save(); // setToken 按本次请求的 IP 写了登录缓存;删掉,让浏览器第一次请求时按它自己的 IP 重建 self::forgetLogin((string) $session->token); return [ 'token' => (string) $session->token, 'expire_time' => (int) $session->expire_time, 'terminal' => self::TERMINAL, 'local_storage' => [self::STORAGE_KEY => json_encode(['expire' => '', 'value' => (string) $session->token])], 'start_path' => '/admin/', ]; } /** 作废该账号的 AI 浏览器会话;没有有效会话时返回 false */ public static function closeForAdmin(int $adminId): bool { $session = AdminSession::where(['admin_id' => $adminId, 'terminal' => self::TERMINAL])->findOrEmpty(); if ($session->isEmpty() || (int) $session->expire_time <= time()) { return false; } // 到期时间记为上一秒:setToken 只在“已过期”(expire_time < 当前秒) 时换新令牌,同一秒内重新打开也不会复用旧令牌 $session->expire_time = time() - 1; $session->update_time = time(); $session->save(); self::forgetLogin((string) $session->token); return true; } /** * 清掉后台对这个令牌的登录缓存(后台先查缓存、查不到才回表看到期时间,所以作废会话必须清缓存)。 * 文件缓存按应用分目录(config/cache.php 的 path 为空):后台请求用 runtime/adminapi/cache, * 本模块在 runtime/mcp/cache,所以文件缓存时要按后台的目录再删一次;redis 等共享缓存删一次即可。 */ private static function forgetLogin(string $token): void { (new AdminTokenCache())->deleteAdminInfo($token); if ((string) config('cache.default') !== 'file') { return; } $options = (array) config('cache.stores.file'); if (($options['path'] ?? '') !== '') { return; // 配了固定目录:所有应用共用,上面已经删过 } $options['path'] = app()->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . 'adminapi' . DIRECTORY_SEPARATOR . 'cache'; (new FileCache(app(), $options))->delete('token_admin_' . $token); } }