245 lines
6.7 KiB
TypeScript
245 lines
6.7 KiB
TypeScript
/**
|
||
* 资源点余额管理服务
|
||
* 负责缓存余额、主动查询、发送前检测
|
||
*/
|
||
|
||
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<number>('icCoderCreditsBalance');
|
||
if (savedBalance !== undefined) {
|
||
cachedBalance = savedBalance;
|
||
lastUpdateTime = Date.now();
|
||
console.log('[CreditsService] 从持久化存储加载余额:', savedBalance);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存余额到持久化存储
|
||
*/
|
||
async function saveBalance(balance: number): Promise<void> {
|
||
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<number | null> {
|
||
try {
|
||
// 获取 JWT token
|
||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||
if (!session?.accessToken) {
|
||
console.warn('[CreditsService] 无法查询余额:未登录');
|
||
return null;
|
||
}
|
||
|
||
const token = session.accessToken;
|
||
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<StrangeLoopBalanceResponse> {
|
||
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<number | null> {
|
||
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<void> {
|
||
cachedBalance = null;
|
||
lastUpdateTime = 0;
|
||
if (extensionContext) {
|
||
await extensionContext.globalState.update('icCoderCreditsBalance', undefined);
|
||
}
|
||
console.log('[CreditsService] 余额缓存已清除');
|
||
}
|