diff --git a/src/config/settings.ts b/src/config/settings.ts index b760a4e..40eda58 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -8,7 +8,7 @@ import * as vscode from "vscode"; type Environment = "dev" | "test" | "prod"; /** 当前环境 - 修改这里切换环境 */ -const CURRENT_ENV: Environment = "test"; +const CURRENT_ENV: Environment = "dev"; /** 服务等级类型 */ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto"; @@ -17,6 +17,8 @@ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto"; export interface IccoderConfig { /** 后端服务地址 */ backendUrl: string; + /** 登录页面地址 */ + loginUrl: string; /** 请求超时时间(毫秒) */ timeout: number; /** 用户ID(临时使用,后续对接认证) */ @@ -30,6 +32,7 @@ const ENV_CONFIG: Record = { /** 本地开发环境 */ dev: { backendUrl: "http://localhost:2233", + loginUrl: "http://localhost/login", timeout: 300000, userId: "default-user", serviceTier: "max", // 默认使用 max @@ -37,6 +40,7 @@ const ENV_CONFIG: Record = { /** 测试服务器环境 */ test: { backendUrl: "http://192.168.1.108:2233", + loginUrl: "http://192.168.1.108:2005/login", timeout: 60000, userId: "default-user", serviceTier: "max", @@ -44,6 +48,7 @@ const ENV_CONFIG: Record = { /** 生产环境 */ prod: { backendUrl: "https://api.iccoder.com", + loginUrl: "https://iccoder.com/login", timeout: 60000, userId: "default-user", serviceTier: "auto", diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index c819840..262aa2f 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -12,6 +12,7 @@ import { getConfig } from '../config/settings'; import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api'; import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient'; import { ChatHistoryManager } from '../utils/chatHistoryManager'; +import { getUserIdFromToken } from '../utils/jwtUtils'; /** * 消息段落类型 @@ -331,6 +332,27 @@ export class DialogSession { const config = getConfig(); + // 从登录 session 获取真实 userId + let userId = config.userId; // 默认值 + try { + console.log('[DialogSession] 尝试获取登录 session...'); + const session = await vscode.authentication.getSession('iccoder', [], { silent: true }); + console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined'); + if (session?.accessToken) { + console.log('[DialogSession] accessToken 长度:', session.accessToken.length); + const parsedUserId = getUserIdFromToken(session.accessToken); + console.log('[DialogSession] 解析的 userId:', parsedUserId); + if (parsedUserId) { + userId = parsedUserId; + console.log('[DialogSession] 使用真实 userId:', userId); + } + } else { + console.log('[DialogSession] 未获取到 accessToken,使用默认 userId:', userId); + } + } catch (error) { + console.warn('[DialogSession] 获取登录 session 失败:', error); + } + // 获取压缩数据和新消息(用于后端重启后恢复) const historyManager = ChatHistoryManager.getInstance(); const compactedData = await historyManager.loadCompactedData(this.taskId); @@ -343,7 +365,7 @@ export class DialogSession { const request: DialogRequest = { taskId: this.taskId, message, - userId: config.userId, + userId, mode: mode || 'agent', serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数 compactedData: compactedData || undefined, @@ -654,6 +676,23 @@ export class DialogSession { callbacks.onContextUsage?.(data); }, + onCreditUpdate: (data) => { + console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits); + // 资源点余额低于阈值时弹窗提醒 + const LOW_CREDIT_THRESHOLD = 5; + if (data.remainingCredits < LOW_CREDIT_THRESHOLD) { + vscode.window.showWarningMessage( + `资源点余额不足!当前剩余 ${data.remainingCredits.toFixed(2)} 点,请及时充值。`, + '去充值' + ).then(selection => { + if (selection === '去充值') { + // 打开充值页面 + vscode.env.openExternal(vscode.Uri.parse('https://iccoder.com/recharge')); + } + }); + } + }, + onOpen: () => { console.log('[DialogSession] SSE 连接已建立'); }, diff --git a/src/services/icCoderAuthProvider.ts b/src/services/icCoderAuthProvider.ts index 05c8a3c..05157c0 100644 --- a/src/services/icCoderAuthProvider.ts +++ b/src/services/icCoderAuthProvider.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import * as http from "http"; import * as path from "path"; import * as fs from "fs"; +import { getConfig } from "../config/settings"; /** * IC Coder Authentication Provider @@ -12,7 +13,6 @@ export class ICCoderAuthenticationProvider { private static readonly AUTH_TYPE = "iccoder"; private static readonly AUTH_NAME = "IC Coder"; - private static readonly LOGIN_URL = "http://192.168.1.108:2005/login"; private static loginServer: http.Server | null = null; private static currentPort: number | null = null; @@ -149,9 +149,8 @@ export class ICCoderAuthenticationProvider // 构建登录 URL const callbackUrl = `http://localhost:${port}/callback`; - const loginUrl = `${ - ICCoderAuthenticationProvider.LOGIN_URL - }?redirect_uri=${encodeURIComponent(callbackUrl)}`; + const config = getConfig(); + const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`; console.log("🔐 登录服务器已启动,监听端口:", port); console.log("🌐 登录 URL:", loginUrl); diff --git a/src/services/sseHandler.ts b/src/services/sseHandler.ts index 4750c0a..4b1dda9 100644 --- a/src/services/sseHandler.ts +++ b/src/services/sseHandler.ts @@ -28,7 +28,8 @@ import type { AgentProgressEvent, AgentCompleteEvent, AgentErrorEvent, - ContextUsageEvent + ContextUsageEvent, + CreditUpdateEvent } from '../types/api'; import type { MemoryCompactedEvent } from '../types/memory'; @@ -74,6 +75,8 @@ export interface SSECallbacks { onMemoryCompacted?: (data: MemoryCompactedEvent) => void; /** 上下文使用量更新 */ onContextUsage?: (data: ContextUsageEvent) => void; + /** 资源点余额更新 */ + onCreditUpdate?: (data: CreditUpdateEvent) => void; /** 连接打开 */ onOpen?: () => void; /** 连接关闭 */ @@ -331,6 +334,9 @@ function dispatchEvent( case 'context_usage': callbacks.onContextUsage?.(data as ContextUsageEvent); break; + case 'credit_update': + callbacks.onCreditUpdate?.(data as CreditUpdateEvent); + break; case 'heartbeat': // 心跳事件:仅用于保持连接,不需要特殊处理 // Node.js req.setTimeout 会在收到数据时自动重置计时器 diff --git a/src/types/api.ts b/src/types/api.ts index 34bb475..2265be3 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -66,6 +66,7 @@ export type SSEEventType = | "agent_error" // 子智能体错误 | "memory_compacted" // 记忆压缩完成 | "context_usage" // 上下文使用量 + | "credit_update" // 资源点余额更新 | "complete" // 对话完成 | "error" // 错误 | "warning" // 警告 @@ -201,6 +202,12 @@ export interface ContextUsageEvent { percentage: number; } +/** credit_update 事件数据 */ +export interface CreditUpdateEvent { + deductedCredits: number; + remainingCredits: number; +} + // ============== 工具调用协议 (MCP 格式) ============== /** diff --git a/src/utils/jwtUtils.ts b/src/utils/jwtUtils.ts new file mode 100644 index 0000000..bb4f9a8 --- /dev/null +++ b/src/utils/jwtUtils.ts @@ -0,0 +1,73 @@ +/** + * JWT 工具函数 + */ + +/** + * JWT Payload 接口 + */ +export interface JwtPayload { + sub?: string; // subject (通常是 userId) + userId?: number; // 用户ID (驼峰命名) + user_id?: number; // 用户ID (下划线命名) + exp?: number; // 过期时间 + iat?: number; // 签发时间 + [key: string]: unknown; +} + +/** + * 解析 JWT token 的 payload + * @param token JWT token + * @returns 解析后的 payload,解析失败返回 null + */ +export function parseJwtPayload(token: string): JwtPayload | null { + try { + const parts = token.split('.'); + if (parts.length !== 3) { + console.warn('[JWT] token 格式不正确,期望3部分,实际:', parts.length); + return null; + } + + // payload 是第二部分,base64url 编码 + const payload = parts[1]; + + // base64url 转 base64 + const base64 = payload.replace(/-/g, '+').replace(/_/g, '/'); + + // 解码 + const jsonStr = Buffer.from(base64, 'base64').toString('utf-8'); + const parsed = JSON.parse(jsonStr); + + console.log('[JWT] 解析成功, payload 字段:', Object.keys(parsed)); + console.log('[JWT] payload 内容:', JSON.stringify(parsed)); + return parsed; + } catch (error) { + console.error('[JWT] 解析失败:', error); + return null; + } +} + +/** + * 从 JWT token 中获取用户ID + * @param token JWT token + * @returns 用户ID字符串,获取失败返回 null + */ +export function getUserIdFromToken(token: string): string | null { + const payload = parseJwtPayload(token); + if (!payload) { + return null; + } + + // 支持多种字段名:user_id, userId, sub + if (payload.user_id !== undefined) { + return String(payload.user_id); + } + if (payload.userId !== undefined) { + return String(payload.userId); + } + if (payload.sub !== undefined) { + return String(payload.sub); + } + + console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段'); + return null; +}