You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

61 lines
2.4 KiB

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<String?> 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;
}
}