/** * 资源点余额管理服务 * 负责缓存余额、主动查询、发送前检测 */ import * as vscode from 'vscode'; import * as https from 'https'; import * as http from 'http'; import { URL } from 'url'; import { getStrangeLoopApiUrl } from '../config/settings'; import { getCachedUserInfo } from './userService'; /** 低余额阈值 */ const LOW_CREDIT_THRESHOLD = 5; /** 缓存的余额 */ let cachedBalance: number | null = null; /** 最后更新时间 */ let lastUpdateTime: number = 0; /** 缓存有效期(5分钟) */ const CACHE_TTL_MS = 5 * 60 * 1000; /** ExtensionContext 用于持久化存储 */ let extensionContext: vscode.ExtensionContext | null = null; /** * 初始化 Credits 服务(设置 context) */ export function initCreditsService(context: vscode.ExtensionContext): void { extensionContext = context; // 从持久化存储加载余额 const savedBalance = extensionContext.globalState.get('icCoderCreditsBalance'); if (savedBalance !== undefined) { cachedBalance = savedBalance; lastUpdateTime = Date.now(); console.log('[CreditsService] 从持久化存储加载余额:', savedBalance); } } /** * 保存余额到持久化存储 */ async function saveBalance(balance: number): Promise { if (extensionContext) { await extensionContext.globalState.update('icCoderCreditsBalance', balance); console.log('[CreditsService] 余额已保存到持久化存储:', balance); } } /** * 更新缓存的余额(从 SSE credit_update 事件调用) */ export function updateCachedBalance(balance: number): void { cachedBalance = balance; lastUpdateTime = Date.now(); console.log('[CreditsService] 余额已更新:', balance); // 异步保存到持久化存储 saveBalance(balance).catch(err => { console.error('[CreditsService] 保存余额失败:', err); }); } /** * 获取缓存的余额 */ export function getCachedBalance(): number | null { return cachedBalance; } /** * 检查缓存是否有效 */ function isCacheValid(): boolean { if (cachedBalance === null) return false; return Date.now() - lastUpdateTime < CACHE_TTL_MS; } /** * StrangeLoop 余额响应类型 */ interface StrangeLoopBalanceResponse { userId?: number; availableCredits?: number; totalCredits?: number; error?: string; message?: string; } /** * 主动查询余额(直接调用 StrangeLoop 接口) */ export async function fetchBalance(): Promise { try { // 获取 JWT token const session = await vscode.authentication.getSession('iccoder', [], { silent: true }); if (!session?.accessToken) { console.warn('[CreditsService] 无法查询余额:未登录'); return null; } return await fetchBalanceWithToken(session.accessToken); } catch (error) { console.error('[CreditsService] 查询余额异常:', error); return null; } } /** * 使用指定 token 查询余额(登录过程中使用) */ export async function fetchBalanceWithToken(token: string): Promise { try { console.log('[CreditsService] 开始查询余额,token 长度:', token.length); // 直接调用 StrangeLoop 的 /api/credit/balance 接口 const response = await callStrangeLoopBalance(token); if (response.availableCredits !== undefined) { const balance = response.availableCredits; updateCachedBalance(balance); console.log('[CreditsService] 余额查询成功:', balance); return balance; } else { console.warn('[CreditsService] 查询余额失败:', response.error || response.message); return null; } } catch (error) { console.error('[CreditsService] 查询余额异常:', error); return null; } } /** * 调用 StrangeLoop 余额接口 */ async function callStrangeLoopBalance(token: string): Promise { const urlStr = getStrangeLoopApiUrl('/strangeloop/api/credit/balance'); const url = new URL(urlStr); const isHttps = url.protocol === 'https:'; const httpModule = isHttps ? https : http; // 余额查询使用固定短超时,避免阻塞发送前检查 const BALANCE_TIMEOUT_MS = 5000; const requestOptions: http.RequestOptions = { hostname: url.hostname, port: url.port || (isHttps ? 443 : 80), path: url.pathname + url.search, method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, timeout: BALANCE_TIMEOUT_MS }; return new Promise((resolve, reject) => { const req = httpModule.request(requestOptions, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { console.log('[CreditsService] 响应状态码:', res.statusCode); console.log('[CreditsService] 响应内容:', data); try { const json = JSON.parse(data); if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { resolve(json as StrangeLoopBalanceResponse); } else if (res.statusCode === 401 || res.statusCode === 403) { // 登录过期或无权限 resolve({ error: '登录已过期,请重新登录' }); } else { resolve({ error: json.error || json.message || json.msg || `HTTP ${res.statusCode}` }); } } catch (e) { resolve({ error: `解析响应失败: ${data}` }); } }); }); req.on('error', (error) => { reject(error); }); req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); }); req.end(); }); } /** * 获取当前余额(优先使用缓存,过期则主动查询) */ export async function getBalance(): Promise { if (isCacheValid()) { return cachedBalance; } return await fetchBalance(); } /** * 检查余额是否足够发送消息 * @returns { allowed: boolean, balance: number | null, message?: string } */ export async function checkBalanceBeforeSend(): Promise<{ allowed: boolean; balance: number | null; message?: string; }> { const userInfo = getCachedUserInfo(); if (!userInfo) { // 未登录,允许发送(后端会处理) return { allowed: true, balance: null }; } const balance = await getBalance(); if (balance === null) { // 无法获取余额,允许发送(后端会处理) console.warn('[CreditsService] 无法获取余额,允许发送'); return { allowed: true, balance: null }; } if (balance < LOW_CREDIT_THRESHOLD) { return { allowed: false, balance, message: `资源点余额不足!当前余额 ${balance.toFixed(2)} 点,低于最低要求 ${LOW_CREDIT_THRESHOLD} 点。请充值后再试。` }; } return { allowed: true, balance }; } /** * 清除缓存(登出时调用) */ export async function clearBalanceCache(): Promise { cachedBalance = null; lastUpdateTime = 0; if (extensionContext) { await extensionContext.globalState.update('icCoderCreditsBalance', undefined); } console.log('[CreditsService] 余额缓存已清除'); }