Files
IC-Coder-Plugin/src/services/creditsService.ts
XiaoFeng 7d1b8f7e26 fix: 优化登录流程和余额查询逻辑
- 添加 fetchBalanceWithToken 支持登录时直接传入 token 查询余额
- AuthProvider 会话加载改为同步方式避免时序问题
- 添加调试日志便于排查问题
2026-01-13 20:06:42 +08:00

256 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 资源点余额管理服务
* 负责缓存余额、主动查询、发送前检测
*/
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;
}
return await fetchBalanceWithToken(session.accessToken);
} catch (error) {
console.error('[CreditsService] 查询余额异常:', error);
return null;
}
}
/**
* 使用指定 token 查询余额(登录过程中使用)
*/
export async function fetchBalanceWithToken(token: string): Promise<number | null> {
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<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] 余额缓存已清除');
}