From bdc55c727a58c974343b76fc47b82950e5e8390b Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Sat, 10 Jan 2026 21:45:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=89=8D=E4=BD=99=E9=A2=9D=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - creditsService.ts: 新增余额缓存和检测服务 - apiClient.ts: 新增 getCreditBalance() API 调用 - dialogService.ts: SSE credit_update 事件更新余额缓存 - messageHandler.ts: 发送消息前检测余额,低于5点阻止发送 --- src/services/apiClient.ts | 19 ++++++ src/services/creditsService.ts | 121 +++++++++++++++++++++++++++++++++ src/services/dialogService.ts | 3 + src/utils/messageHandler.ts | 22 ++++++ 4 files changed, 165 insertions(+) create mode 100644 src/services/creditsService.ts diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index c94d577..6980271 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -224,3 +224,22 @@ export async function getUserInfo(): Promise { method: 'GET' }); } + +/** 余额查询响应 */ +export interface CreditBalanceResponse { + success: boolean; + balance?: number; + error?: string; +} + +/** + * 查询用户资源点余额 + * GET /api/dialog/balance?userId=xxx + */ +export async function getCreditBalance(userId: string): Promise { + console.log('[API] 查询余额: userId=', userId); + return request(`/api/dialog/balance?userId=${userId}`, { + method: 'GET', + timeout: 5000 + }); +} diff --git a/src/services/creditsService.ts b/src/services/creditsService.ts new file mode 100644 index 0000000..d55b7f2 --- /dev/null +++ b/src/services/creditsService.ts @@ -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 { + 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 { + 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] 余额缓存已清除'); +} diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 34cb2a0..515fa4b 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -13,6 +13,7 @@ import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient'; import { ChatHistoryManager } from '../utils/chatHistoryManager'; import { getUserIdFromToken } from '../utils/jwtUtils'; +import { updateCachedBalance } from './creditsService'; /** * 消息段落类型 @@ -791,6 +792,8 @@ export class DialogSession { onCreditUpdate: (data) => { console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits); + // 更新余额缓存 + updateCachedBalance(data.remainingCredits); // 资源点余额低于阈值时弹窗提醒 const LOW_CREDIT_THRESHOLD = 5; if (data.remainingCredits < LOW_CREDIT_THRESHOLD) { diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index b916a8d..2d0c5da 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -18,6 +18,7 @@ import { ChatHistoryManager } from "./chatHistoryManager"; import { dialogManager, DialogSession } from "../services/dialogService"; import { userInteractionManager } from "../services/userInteraction"; import { healthCheck } from "../services/apiClient"; +import { checkBalanceBeforeSend } from "../services/creditsService"; import type { RunMode, ServiceTier } from "../types/api"; @@ -90,6 +91,27 @@ export async function handleUserMessage( 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) { try {