feat:实现Token过期检查和自动清除机制

主要改动:
   - 在插件激活时检查Token是否过期,过期则自动清除session
   - 修复Token检查逻辑,从session.accessToken获取Token而非globalState
   - 在消息发送前检查Token有效性,过期则提示重新登录
   - 优化ICHelperPanel和ICViewProvider的Token过期处理
   - 修复退出登录命令名错误(iccoder.logout -> ic-coder.logout)
   - 添加Token过期检查文档文档
This commit is contained in:
Roe-xin
2026-01-26 18:41:52 +08:00
parent 423c9ddb0e
commit 9296b10150
7 changed files with 472 additions and 19 deletions

View File

@ -7,10 +7,33 @@ import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
import { VCDFileServer } from "./services/vcdFileServer";
import { initUserService } from "./services/userService";
import { initCreditsService } from "./services/creditsService";
import { isTokenExpired } from "./utils/jwtUtils";
export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
console.log("🎉 IC Coder 插件已激活!");
// 【关键】在创建 AuthProvider 之前,先检查并清除过期的 session
const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
if (storedSessions.length > 0) {
const session = storedSessions[0];
const token = session.accessToken;
console.log('[Extension] 检查 token 是否过期...');
if (token) {
const expired = isTokenExpired(token);
console.log('[Extension] token 过期检查结果:', expired);
if (expired) {
// 必须等待清除完成后再创建 AuthProvider
await context.globalState.update('icCoderSessions', []);
await context.globalState.update('icCoderUserInfo', undefined);
console.log('[Extension] Token 已过期,已清除所有登录状态');
}
}
}
// 初始化用户服务
initUserService(context);
@ -30,7 +53,7 @@ export function activate(context: vscode.ExtensionContext) {
dispose: () => vcdFileServer.stop()
});
// 注册 Authentication Provider
// 注册 Authentication Provider(此时 icCoderSessions 已经被清除)
const authProvider = new ICCoderAuthenticationProvider(context);
context.subscriptions.push(
vscode.authentication.registerAuthenticationProvider(

View File

@ -19,6 +19,7 @@ import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory";
import { getCachedUserInfo } from "../services/userService";
import { isTokenExpired } from "../utils/jwtUtils";
/**
* 获取会员等级图标 URI
@ -58,6 +59,30 @@ export async function showICHelperPanel(
context: vscode.ExtensionContext,
viewColumn?: vscode.ViewColumn
) {
// 检查 token 是否过期
let token: string | undefined;
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
token = session?.accessToken;
} catch (error) {
console.warn('[ICHelperPanel] 获取 session 失败:', error);
}
if (token && isTokenExpired(token)) {
// 清除过期的 session
await context.globalState.update('icCoderSessions', []);
await context.globalState.update('icCoderUserInfo', undefined);
const action = await vscode.window.showWarningMessage(
'登录已过期,请重新登录',
'立即登录'
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
}
return;
}
// 检查用户是否已登录
try {
const session = await vscode.authentication.getSession("iccoder", [], {
@ -104,6 +129,7 @@ export async function showICHelperPanel(
.toString(36)
.substr(2, 9)}`;
(panel as any).__uniqueId = panelId;
(panel as any).__context = context;
// 设置标签页图标
panel.iconPath = vscode.Uri.joinPath(
@ -190,6 +216,25 @@ export async function showICHelperPanel(
console.error('[ICHelperPanel] 获取用户信息失败:', error);
}
// 检查是否有待发送的消息
const pendingMessage = context.globalState.get('pendingMessage') as any;
if (pendingMessage) {
console.log('[ICHelperPanel] 检测到待发送消息,准备自动发送');
// 清除待发送消息
await context.globalState.update('pendingMessage', undefined);
// 延迟发送,确保面板已完全初始化
setTimeout(() => {
panel.webview.postMessage({
command: 'autoSendMessage',
text: pendingMessage.text,
mode: pendingMessage.mode,
serviceTier: pendingMessage.serviceTier
});
}, 500);
}
// 处理消息
panel.webview.onDidReceiveMessage(
async (message) => {
@ -524,7 +569,7 @@ export async function showICHelperPanel(
break;
case "logout":
// 退出登录(前端已有确认对话框)
vscode.commands.executeCommand('iccoder.logout');
vscode.commands.executeCommand('ic-coder.logout');
break;
}
},

View File

@ -6,11 +6,11 @@
* JWT Payload 接口
*/
export interface JwtPayload {
sub?: string; // subject (通常是 userId)
userId?: number; // 用户ID (驼峰命名)
user_id?: number; // 用户ID (下划线命名)
exp?: number; // 过期时间
iat?: number; // 签发时间
sub?: string; // subject (通常是 userId)
userId?: number; // 用户ID (驼峰命名)
user_id?: number; // 用户ID (下划线命名)
exp?: number; // 过期时间
iat?: number; // 签发时间
[key: string]: unknown;
}
@ -21,9 +21,9 @@ export interface JwtPayload {
*/
export function parseJwtPayload(token: string): JwtPayload | null {
try {
const parts = token.split('.');
const parts = token.split(".");
if (parts.length !== 3) {
console.warn('[JWT] token 格式不正确期望3部分实际:', parts.length);
console.warn("[JWT] token 格式不正确期望3部分实际:", parts.length);
return null;
}
@ -31,17 +31,17 @@ export function parseJwtPayload(token: string): JwtPayload | null {
const payload = parts[1];
// base64url 转 base64
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
// 解码
const jsonStr = Buffer.from(base64, 'base64').toString('utf-8');
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));
console.log("[JWT] 解析成功, payload 字段:", Object.keys(parsed));
console.log("[JWT] payload 内容:", JSON.stringify(parsed));
return parsed;
} catch (error) {
console.error('[JWT] 解析失败:', error);
console.error("[JWT] 解析失败:", error);
return null;
}
}
@ -68,7 +68,7 @@ export function getUserIdFromToken(token: string): string | null {
return String(payload.sub);
}
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
console.warn("[JWT] payload 中没有 user_id, userId 或 sub 字段");
return null;
}
@ -78,14 +78,17 @@ export function getUserIdFromToken(token: string): string | null {
* @param bufferSeconds 提前多少秒判定为过期默认60秒
* @returns true 表示已过期false 表示未过期null 表示无法判断
*/
export function isTokenExpired(token: string, bufferSeconds: number = 60): boolean | null {
export function isTokenExpired(
token: string,
bufferSeconds: number = 60,
): boolean | null {
const payload = parseJwtPayload(token);
if (!payload) {
return null;
}
if (payload.exp === undefined) {
console.warn('[JWT] payload 中没有 exp 字段,无法判断过期');
console.warn("[JWT] payload 中没有 exp 字段,无法判断过期");
return null;
}
@ -94,7 +97,7 @@ export function isTokenExpired(token: string, bufferSeconds: number = 60): boole
const isExpired = now >= expTime;
if (isExpired) {
console.warn('[JWT] token 已过期exp:', payload.exp, '当前:', now);
console.warn("[JWT] token 已过期exp:", payload.exp, "当前:", now);
}
return isExpired;

View File

@ -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 { isTokenExpired } from "./jwtUtils";
import {
checkBalanceBeforeSend,
fetchBalance,
@ -47,6 +48,83 @@ export async function handleUserMessage(
) {
console.log("收到用户消息:", text);
// 检查 token 是否过期
const context = (panel as any).__context;
if (context) {
// 从 session 中获取 token
let token: string | undefined;
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
token = session?.accessToken;
} catch (error) {
console.warn("[MessageHandler] 获取 session 失败:", error);
}
if (!token) {
console.warn("[MessageHandler] 未登录,阻止发送");
// 保存待发送的消息
await context.globalState.update('pendingMessage', {
text,
mode,
serviceTier,
timestamp: Date.now()
});
// 显示弹窗提示
const action = await vscode.window.showWarningMessage(
'请先登录后再发送消息',
'立即登录'
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
}
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
if (isTokenExpired(token)) {
console.warn("[MessageHandler] Token 已过期,阻止发送");
// 保存待发送的消息
await context.globalState.update('pendingMessage', {
text,
mode,
serviceTier,
timestamp: Date.now()
});
// 清除过期的 session
await context.globalState.update('icCoderSessions', []);
await context.globalState.update('icCoderUserInfo', undefined);
// 显示弹窗提示
const action = await vscode.window.showWarningMessage(
'登录已过期,请重新登录',
'立即登录'
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
}
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
}
// 记录用户消息到历史(允许失败,不阻塞主流程)
try {
const historyManager = ChatHistoryManager.getInstance();

View File

@ -202,6 +202,20 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
};
// 异步检查 token 是否过期并清除
vscode.authentication.getSession("iccoder", [], { createIfNone: false })
.then((session) => {
const token = session?.accessToken;
if (token && isTokenExpired(token)) {
// 静默清除过期的 session
this.context.globalState.update('icCoderSessions', []);
this.context.globalState.update('icCoderUserInfo', undefined);
console.log('[ICViewProvider] Token 已过期,已清除所有登录状态');
}
}, () => {
// 忽略错误
});
// 检查是否已登录(使用 Authentication API
this.checkLoginStatus().then((isLoggedIn) => {
webviewView.webview.html = this.getWebviewContent(

View File

@ -624,6 +624,19 @@ export function getWebviewContent(
}
break;
case 'autoSendMessage':
// 自动发送待发送的消息(登录后)
console.log('[WebView] 自动发送待发送消息:', message.text);
const inputElement = document.getElementById('userInput');
if (inputElement) {
inputElement.value = message.text;
// 触发发送
if (typeof sendMessage === 'function') {
sendMessage();
}
}
break;
case 'showFeedbackQRCode':
// 显示用户反馈二维码弹窗
console.log('[WebView] 显示用户反馈二维码弹窗');