feat: 从JWT解析userId并添加资源点余额提醒
- 新增 jwtUtils.ts 解析JWT token获取user_id - dialogService 从登录session获取真实userId - 添加 credit_update 事件处理 - 余额低于5点时弹窗提醒用户充值 - settings.ts 登录URL改为可配置
This commit is contained in:
@ -8,7 +8,7 @@ import * as vscode from "vscode";
|
|||||||
type Environment = "dev" | "test" | "prod";
|
type Environment = "dev" | "test" | "prod";
|
||||||
|
|
||||||
/** 当前环境 - 修改这里切换环境 */
|
/** 当前环境 - 修改这里切换环境 */
|
||||||
const CURRENT_ENV: Environment = "test";
|
const CURRENT_ENV: Environment = "dev";
|
||||||
|
|
||||||
/** 服务等级类型 */
|
/** 服务等级类型 */
|
||||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||||
@ -17,6 +17,8 @@ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
|||||||
export interface IccoderConfig {
|
export interface IccoderConfig {
|
||||||
/** 后端服务地址 */
|
/** 后端服务地址 */
|
||||||
backendUrl: string;
|
backendUrl: string;
|
||||||
|
/** 登录页面地址 */
|
||||||
|
loginUrl: string;
|
||||||
/** 请求超时时间(毫秒) */
|
/** 请求超时时间(毫秒) */
|
||||||
timeout: number;
|
timeout: number;
|
||||||
/** 用户ID(临时使用,后续对接认证) */
|
/** 用户ID(临时使用,后续对接认证) */
|
||||||
@ -30,6 +32,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 本地开发环境 */
|
/** 本地开发环境 */
|
||||||
dev: {
|
dev: {
|
||||||
backendUrl: "http://localhost:2233",
|
backendUrl: "http://localhost:2233",
|
||||||
|
loginUrl: "http://localhost/login",
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max", // 默认使用 max
|
serviceTier: "max", // 默认使用 max
|
||||||
@ -37,6 +40,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 测试服务器环境 */
|
/** 测试服务器环境 */
|
||||||
test: {
|
test: {
|
||||||
backendUrl: "http://192.168.1.108:2233",
|
backendUrl: "http://192.168.1.108:2233",
|
||||||
|
loginUrl: "http://192.168.1.108:2005/login",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max",
|
serviceTier: "max",
|
||||||
@ -44,6 +48,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 生产环境 */
|
/** 生产环境 */
|
||||||
prod: {
|
prod: {
|
||||||
backendUrl: "https://api.iccoder.com",
|
backendUrl: "https://api.iccoder.com",
|
||||||
|
loginUrl: "https://iccoder.com/login",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "auto",
|
serviceTier: "auto",
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { getConfig } from '../config/settings';
|
|||||||
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||||
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息段落类型
|
* 消息段落类型
|
||||||
@ -331,6 +332,27 @@ export class DialogSession {
|
|||||||
|
|
||||||
const config = getConfig();
|
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 historyManager = ChatHistoryManager.getInstance();
|
||||||
const compactedData = await historyManager.loadCompactedData(this.taskId);
|
const compactedData = await historyManager.loadCompactedData(this.taskId);
|
||||||
@ -343,7 +365,7 @@ export class DialogSession {
|
|||||||
const request: DialogRequest = {
|
const request: DialogRequest = {
|
||||||
taskId: this.taskId,
|
taskId: this.taskId,
|
||||||
message,
|
message,
|
||||||
userId: config.userId,
|
userId,
|
||||||
mode: mode || 'agent',
|
mode: mode || 'agent',
|
||||||
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
||||||
compactedData: compactedData || undefined,
|
compactedData: compactedData || undefined,
|
||||||
@ -654,6 +676,23 @@ export class DialogSession {
|
|||||||
callbacks.onContextUsage?.(data);
|
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: () => {
|
onOpen: () => {
|
||||||
console.log('[DialogSession] SSE 连接已建立');
|
console.log('[DialogSession] SSE 连接已建立');
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import * as vscode from "vscode";
|
|||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import { getConfig } from "../config/settings";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IC Coder Authentication Provider
|
* IC Coder Authentication Provider
|
||||||
@ -12,7 +13,6 @@ export class ICCoderAuthenticationProvider
|
|||||||
{
|
{
|
||||||
private static readonly AUTH_TYPE = "iccoder";
|
private static readonly AUTH_TYPE = "iccoder";
|
||||||
private static readonly AUTH_NAME = "IC Coder";
|
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 loginServer: http.Server | null = null;
|
||||||
private static currentPort: number | null = null;
|
private static currentPort: number | null = null;
|
||||||
|
|
||||||
@ -149,9 +149,8 @@ export class ICCoderAuthenticationProvider
|
|||||||
|
|
||||||
// 构建登录 URL
|
// 构建登录 URL
|
||||||
const callbackUrl = `http://localhost:${port}/callback`;
|
const callbackUrl = `http://localhost:${port}/callback`;
|
||||||
const loginUrl = `${
|
const config = getConfig();
|
||||||
ICCoderAuthenticationProvider.LOGIN_URL
|
const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||||||
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
|
||||||
|
|
||||||
console.log("🔐 登录服务器已启动,监听端口:", port);
|
console.log("🔐 登录服务器已启动,监听端口:", port);
|
||||||
console.log("🌐 登录 URL:", loginUrl);
|
console.log("🌐 登录 URL:", loginUrl);
|
||||||
|
|||||||
@ -28,7 +28,8 @@ import type {
|
|||||||
AgentProgressEvent,
|
AgentProgressEvent,
|
||||||
AgentCompleteEvent,
|
AgentCompleteEvent,
|
||||||
AgentErrorEvent,
|
AgentErrorEvent,
|
||||||
ContextUsageEvent
|
ContextUsageEvent,
|
||||||
|
CreditUpdateEvent
|
||||||
} from '../types/api';
|
} from '../types/api';
|
||||||
import type { MemoryCompactedEvent } from '../types/memory';
|
import type { MemoryCompactedEvent } from '../types/memory';
|
||||||
|
|
||||||
@ -74,6 +75,8 @@ export interface SSECallbacks {
|
|||||||
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
||||||
/** 上下文使用量更新 */
|
/** 上下文使用量更新 */
|
||||||
onContextUsage?: (data: ContextUsageEvent) => void;
|
onContextUsage?: (data: ContextUsageEvent) => void;
|
||||||
|
/** 资源点余额更新 */
|
||||||
|
onCreditUpdate?: (data: CreditUpdateEvent) => void;
|
||||||
/** 连接打开 */
|
/** 连接打开 */
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
/** 连接关闭 */
|
/** 连接关闭 */
|
||||||
@ -331,6 +334,9 @@ function dispatchEvent(
|
|||||||
case 'context_usage':
|
case 'context_usage':
|
||||||
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
||||||
break;
|
break;
|
||||||
|
case 'credit_update':
|
||||||
|
callbacks.onCreditUpdate?.(data as CreditUpdateEvent);
|
||||||
|
break;
|
||||||
case 'heartbeat':
|
case 'heartbeat':
|
||||||
// 心跳事件:仅用于保持连接,不需要特殊处理
|
// 心跳事件:仅用于保持连接,不需要特殊处理
|
||||||
// Node.js req.setTimeout 会在收到数据时自动重置计时器
|
// Node.js req.setTimeout 会在收到数据时自动重置计时器
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export type SSEEventType =
|
|||||||
| "agent_error" // 子智能体错误
|
| "agent_error" // 子智能体错误
|
||||||
| "memory_compacted" // 记忆压缩完成
|
| "memory_compacted" // 记忆压缩完成
|
||||||
| "context_usage" // 上下文使用量
|
| "context_usage" // 上下文使用量
|
||||||
|
| "credit_update" // 资源点余额更新
|
||||||
| "complete" // 对话完成
|
| "complete" // 对话完成
|
||||||
| "error" // 错误
|
| "error" // 错误
|
||||||
| "warning" // 警告
|
| "warning" // 警告
|
||||||
@ -201,6 +202,12 @@ export interface ContextUsageEvent {
|
|||||||
percentage: number;
|
percentage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** credit_update 事件数据 */
|
||||||
|
export interface CreditUpdateEvent {
|
||||||
|
deductedCredits: number;
|
||||||
|
remainingCredits: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ============== 工具调用协议 (MCP 格式) ==============
|
// ============== 工具调用协议 (MCP 格式) ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
73
src/utils/jwtUtils.ts
Normal file
73
src/utils/jwtUtils.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user