import '../../core/utils/logger.dart'; import 'network/api.dart'; /// EMAI 会话用的 MCP 令牌。 /// /// 百炼调我们的 MCP 工具(查会议纪要、查待办…)时,需要知道「这是哪个用户」。 /// 整条链路上唯一能按用户变的通道是 Start 指令的 /// `biz_params.user_defined_params` —— 客户端连百炼只带百炼的 API Key, /// 百炼调 MCP 的 Authorization 头又是控制台里配死的、所有用户共用。 /// /// ⚠️ 这里拿的是服务端 `user_getmcptoken` 签发的**短时效、仅限 MCP 用途**的令牌, /// 不是登录 JWT —— 这条路要经过百炼(第三方),登录 JWT 等价于账号全部权限。 class McpTokenService { const McpTokenService._(); static const String _tag = 'McpToken'; static String? _token; static int _expiresAt = 0; /// 提前多久就当它过期。会话可能持续很久,卡着过期时刻去用必然中途失效。 static const int _skewSeconds = 300; static bool get _fresh { if (_token == null || _token!.isEmpty) return false; final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; return _expiresAt - _skewSeconds > now; } /// 取令牌。缓存未过期就直接用,否则重新申请。 /// /// 拿不到时返回 null 而不是抛异常 —— MCP 工具用不了只是少几项能力, /// 不该因此让整个 EMAI 会话建立不起来。 static Future get() async { if (_fresh) return _token; try { final resp = await Api.getMcpToken(); // ⚠️ AuthInterceptor 在业务码 != 0 时是把 data 置 null 后 resolve(不抛), // 所以这里必须判 null;只 catch 是接不住业务错误的。 if (resp == null) { Logger.w(_tag, '服务端未签发 MCP 令牌(多半是没配 McpTokenKey)'); return null; } final token = (resp['token'] ?? '').toString(); if (token.isEmpty) return null; _token = token; _expiresAt = (resp['expiresat'] as num?)?.toInt() ?? 0; Logger.i(_tag, 'MCP 令牌已更新,过期时刻=$_expiresAt'); return _token; } catch (e) { Logger.w(_tag, '申请 MCP 令牌失败(MCP 工具本次不可用): $e'); return null; } } /// 换账号必须清掉,否则新账号会拿着上一个人的令牌去调 MCP。 static void reset() { _token = null; _expiresAt = 0; } }