feat: 实现发送消息前余额检测

- creditsService.ts: 新增余额缓存和检测服务
- apiClient.ts: 新增 getCreditBalance() API 调用
- dialogService.ts: SSE credit_update 事件更新余额缓存
- messageHandler.ts: 发送消息前检测余额,低于5点阻止发送
This commit is contained in:
XiaoFeng
2026-01-10 21:45:41 +08:00
parent 52e4522ed0
commit bdc55c727a
4 changed files with 165 additions and 0 deletions

View File

@ -224,3 +224,22 @@ export async function getUserInfo(): Promise<UserInfoResponse> {
method: 'GET' method: 'GET'
}); });
} }
/** 余额查询响应 */
export interface CreditBalanceResponse {
success: boolean;
balance?: number;
error?: string;
}
/**
* 查询用户资源点余额
* GET /api/dialog/balance?userId=xxx
*/
export async function getCreditBalance(userId: string): Promise<CreditBalanceResponse> {
console.log('[API] 查询余额: userId=', userId);
return request<CreditBalanceResponse>(`/api/dialog/balance?userId=${userId}`, {
method: 'GET',
timeout: 5000
});
}

View File

@ -0,0 +1,121 @@
/**
* 资源点余额管理服务
* 负责缓存余额、主动查询、发送前检测
*/
import { getCreditBalance } from './apiClient';
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;
/**
* 更新缓存的余额(从 SSE credit_update 事件调用)
*/
export function updateCachedBalance(balance: number): void {
cachedBalance = balance;
lastUpdateTime = Date.now();
console.log('[CreditsService] 余额已更新:', balance);
}
/**
* 获取缓存的余额
*/
export function getCachedBalance(): number | null {
return cachedBalance;
}
/**
* 检查缓存是否有效
*/
function isCacheValid(): boolean {
if (cachedBalance === null) return false;
return Date.now() - lastUpdateTime < CACHE_TTL_MS;
}
/**
* 主动查询余额
*/
export async function fetchBalance(): Promise<number | null> {
const userInfo = getCachedUserInfo();
if (!userInfo?.userId) {
console.warn('[CreditsService] 无法查询余额:未登录');
return null;
}
try {
const response = await getCreditBalance(userInfo.userId);
if (response.success && response.balance !== undefined) {
updateCachedBalance(response.balance);
return response.balance;
} else {
console.warn('[CreditsService] 查询余额失败:', response.error);
return null;
}
} catch (error) {
console.error('[CreditsService] 查询余额异常:', error);
return null;
}
}
/**
* 获取当前余额(优先使用缓存,过期则主动查询)
*/
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 function clearBalanceCache(): void {
cachedBalance = null;
lastUpdateTime = 0;
console.log('[CreditsService] 余额缓存已清除');
}

View File

@ -13,6 +13,7 @@ import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient'; import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
import { ChatHistoryManager } from '../utils/chatHistoryManager'; import { ChatHistoryManager } from '../utils/chatHistoryManager';
import { getUserIdFromToken } from '../utils/jwtUtils'; import { getUserIdFromToken } from '../utils/jwtUtils';
import { updateCachedBalance } from './creditsService';
/** /**
* 消息段落类型 * 消息段落类型
@ -791,6 +792,8 @@ export class DialogSession {
onCreditUpdate: (data) => { onCreditUpdate: (data) => {
console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits); console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits);
// 更新余额缓存
updateCachedBalance(data.remainingCredits);
// 资源点余额低于阈值时弹窗提醒 // 资源点余额低于阈值时弹窗提醒
const LOW_CREDIT_THRESHOLD = 5; const LOW_CREDIT_THRESHOLD = 5;
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) { if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {

View File

@ -18,6 +18,7 @@ import { ChatHistoryManager } from "./chatHistoryManager";
import { dialogManager, DialogSession } from "../services/dialogService"; import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction"; import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient"; import { healthCheck } from "../services/apiClient";
import { checkBalanceBeforeSend } from "../services/creditsService";
import type { RunMode, ServiceTier } from "../types/api"; import type { RunMode, ServiceTier } from "../types/api";
@ -90,6 +91,27 @@ export async function handleUserMessage(
return; return;
} }
// 发送前检测余额
const balanceCheck = await checkBalanceBeforeSend();
if (!balanceCheck.allowed) {
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
// 显示错误提示
const selection = await vscode.window.showWarningMessage(
balanceCheck.message || "资源点余额不足",
"去充值"
);
if (selection === "去充值") {
vscode.env.openExternal(vscode.Uri.parse("https://iccoder.com/recharge"));
}
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
// 尝试使用后端服务 // 尝试使用后端服务
if (useBackendService && extensionPath) { if (useBackendService && extensionPath) {
try { try {