feat:实现Token过期检查和自动清除机制
主要改动: - 在插件激活时检查Token是否过期,过期则自动清除session - 修复Token检查逻辑,从session.accessToken获取Token而非globalState - 在消息发送前检查Token有效性,过期则提示重新登录 - 优化ICHelperPanel和ICViewProvider的Token过期处理 - 修复退出登录命令名错误(iccoder.logout -> ic-coder.logout) - 添加Token过期检查文档文档
This commit is contained in:
277
docs/token-expiration-check.md
Normal file
277
docs/token-expiration-check.md
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
# Token 过期检查实现方案
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
实现三个关键时机的 Token 过期检查:
|
||||||
|
- 插件激活时
|
||||||
|
- 发起 API 请求前
|
||||||
|
- 用户交互时(打开面板/侧边栏)
|
||||||
|
|
||||||
|
## 2. 数据存储
|
||||||
|
|
||||||
|
### 2.1 存储位置
|
||||||
|
使用 VS Code 的 `globalState` 存储:
|
||||||
|
```typescript
|
||||||
|
context.globalState.update('tokenExp', exp);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 存储内容
|
||||||
|
- `token`: 用户 token
|
||||||
|
- `tokenExp`: 过期时间戳(秒)
|
||||||
|
- `userInfo`: 用户信息
|
||||||
|
|
||||||
|
## 3. 核心函数设计
|
||||||
|
|
||||||
|
### 3.1 过期检查函数
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 检查 token 是否过期
|
||||||
|
* @param exp - 过期时间戳(秒)
|
||||||
|
* @param bufferSeconds - 提前判断过期的缓冲时间(默认 60 秒)
|
||||||
|
* @returns true 表示已过期或即将过期
|
||||||
|
*/
|
||||||
|
function isTokenExpired(exp: number | undefined, bufferSeconds: number = 60): boolean {
|
||||||
|
if (!exp) {
|
||||||
|
return true; // 没有过期时间,视为已过期
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000); // 当前时间戳(秒)
|
||||||
|
return now >= (exp - bufferSeconds); // 提前 60 秒判断过期
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 清除登录状态函数
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 清除所有登录相关状态
|
||||||
|
*/
|
||||||
|
async function clearAuthState(context: vscode.ExtensionContext): Promise<void> {
|
||||||
|
await context.globalState.update('token', undefined);
|
||||||
|
await context.globalState.update('tokenExp', undefined);
|
||||||
|
await context.globalState.update('userInfo', undefined);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 统一过期处理函数
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 处理 token 过期情况
|
||||||
|
* @param context - 扩展上下文
|
||||||
|
* @param showMessage - 是否显示提示消息
|
||||||
|
*/
|
||||||
|
async function handleTokenExpired(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
showMessage: boolean = true
|
||||||
|
): Promise<void> {
|
||||||
|
await clearAuthState(context);
|
||||||
|
|
||||||
|
if (showMessage) {
|
||||||
|
const action = await vscode.window.showWarningMessage(
|
||||||
|
'登录已过期,请重新登录',
|
||||||
|
'立即登录'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (action === '立即登录') {
|
||||||
|
// 触发登录流程(打开登录面板)
|
||||||
|
vscode.commands.executeCommand('ic-coder.openPanel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 三个检查时机实现
|
||||||
|
|
||||||
|
### 4.1 插件激活时检查
|
||||||
|
|
||||||
|
**位置**: `src/extension.ts` 的 `activate` 函数
|
||||||
|
|
||||||
|
**实现**:
|
||||||
|
```typescript
|
||||||
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
|
console.log('IC Coder 插件正在激活...');
|
||||||
|
|
||||||
|
// 1. 检查 token 是否过期
|
||||||
|
const tokenExp = context.globalState.get<number>('tokenExp');
|
||||||
|
if (isTokenExpired(tokenExp)) {
|
||||||
|
// 静默清除,不显示提示(避免启动时打扰用户)
|
||||||
|
await handleTokenExpired(context, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其他激活逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 启动时静默检查,如果过期则清除状态,但不弹窗提示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 发起 API 请求前检查
|
||||||
|
|
||||||
|
**位置**: `src/utils/messageHandler.ts` 的 API 请求函数
|
||||||
|
|
||||||
|
**实现**:
|
||||||
|
```typescript
|
||||||
|
// 在发送消息到后端前检查
|
||||||
|
async function sendMessageToBackend(message: string, context: vscode.ExtensionContext) {
|
||||||
|
// 1. 检查 token 是否过期
|
||||||
|
const tokenExp = context.globalState.get<number>('tokenExp');
|
||||||
|
if (isTokenExpired(tokenExp)) {
|
||||||
|
await handleTokenExpired(context, true); // 显示提示
|
||||||
|
return; // 中断请求
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = context.globalState.get<string>('token');
|
||||||
|
if (!token) {
|
||||||
|
vscode.window.showWarningMessage('请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 继续发送请求
|
||||||
|
// ... 原有请求逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 每次 API 请求前检查,如果过期则提示用户并中断请求
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 用户交互时检查
|
||||||
|
|
||||||
|
**位置**:
|
||||||
|
- `src/panels/ICHelperPanel.ts` - 打开聊天面板时
|
||||||
|
- `src/views/ICViewProvider.ts` - 侧边栏视图加载时
|
||||||
|
|
||||||
|
**实现 - 聊天面板**:
|
||||||
|
```typescript
|
||||||
|
// ICHelperPanel.ts
|
||||||
|
public static render(extensionUri: vscode.Uri, context: vscode.ExtensionContext) {
|
||||||
|
// 1. 检查 token 是否过期
|
||||||
|
const tokenExp = context.globalState.get<number>('tokenExp');
|
||||||
|
if (isTokenExpired(tokenExp)) {
|
||||||
|
handleTokenExpired(context, true); // 显示提示
|
||||||
|
// 继续渲染面板,但会显示未登录状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建或显示面板
|
||||||
|
// ... 原有逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现 - 侧边栏视图**:
|
||||||
|
```typescript
|
||||||
|
// ICViewProvider.ts
|
||||||
|
public resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||||
|
// 1. 检查 token 是否过期
|
||||||
|
const tokenExp = this._context.globalState.get<number>('tokenExp');
|
||||||
|
if (isTokenExpired(tokenExp)) {
|
||||||
|
handleTokenExpired(this._context, false); // 静默清除
|
||||||
|
// 继续渲染,显示未登录状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 渲染视图
|
||||||
|
// ... 原有逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 打开面板时检查,聊天面板显示提示,侧边栏静默处理
|
||||||
|
|
||||||
|
## 5. 后端响应处理
|
||||||
|
|
||||||
|
### 5.1 保存 exp 字段
|
||||||
|
|
||||||
|
**位置**: `src/utils/messageHandler.ts` 处理登录响应的地方
|
||||||
|
|
||||||
|
**实现**:
|
||||||
|
```typescript
|
||||||
|
// 处理登录成功响应
|
||||||
|
if (response.data.token) {
|
||||||
|
await context.globalState.update('token', response.data.token);
|
||||||
|
|
||||||
|
// 保存过期时间
|
||||||
|
if (response.data.exp) {
|
||||||
|
await context.globalState.update('tokenExp', response.data.exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户信息
|
||||||
|
if (response.data.userInfo) {
|
||||||
|
await context.globalState.update('userInfo', response.data.userInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 处理 401 响应
|
||||||
|
|
||||||
|
**实现**:
|
||||||
|
```typescript
|
||||||
|
// API 请求错误处理
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// 后端返回 401,说明 token 无效或过期
|
||||||
|
await handleTokenExpired(context, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 工具函数位置
|
||||||
|
|
||||||
|
建议创建新文件 `src/utils/authHelper.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export function isTokenExpired(exp: number | undefined, bufferSeconds: number = 60): boolean {
|
||||||
|
if (!exp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
return now >= (exp - bufferSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearAuthState(context: vscode.ExtensionContext): Promise<void> {
|
||||||
|
await context.globalState.update('token', undefined);
|
||||||
|
await context.globalState.update('tokenExp', undefined);
|
||||||
|
await context.globalState.update('userInfo', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleTokenExpired(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
showMessage: boolean = true
|
||||||
|
): Promise<void> {
|
||||||
|
await clearAuthState(context);
|
||||||
|
|
||||||
|
if (showMessage) {
|
||||||
|
const action = await vscode.window.showWarningMessage(
|
||||||
|
'登录已过期,请重新登录',
|
||||||
|
'立即登录'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (action === '立即登录') {
|
||||||
|
vscode.commands.executeCommand('ic-coder.openPanel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 测试场景
|
||||||
|
|
||||||
|
1. **启动测试**: 设置过期的 exp,重启插件,验证状态被清除
|
||||||
|
2. **请求测试**: 设置即将过期的 exp,发送消息,验证被拦截
|
||||||
|
3. **交互测试**: 设置过期的 exp,打开面板,验证提示显示
|
||||||
|
4. **401 测试**: 模拟后端返回 401,验证状态清除
|
||||||
|
|
||||||
|
## 8. 注意事项
|
||||||
|
|
||||||
|
- 使用 60 秒缓冲时间,避免请求中途过期
|
||||||
|
- 启动和侧边栏加载时静默处理,避免打扰用户
|
||||||
|
- 主动操作(发消息、打开聊天面板)时显示提示
|
||||||
|
- 所有时间戳使用秒为单位(与后端保持一致)
|
||||||
|
- 过期检查应该在所有需要 token 的操作前执行
|
||||||
|
|
||||||
|
## 9. 修改文件清单
|
||||||
|
|
||||||
|
需要修改的文件:
|
||||||
|
1. **新建**: `src/utils/authHelper.ts` - 认证辅助工具函数
|
||||||
|
2. **修改**: `src/extension.ts` - 插件激活时检查
|
||||||
|
3. **修改**: `src/utils/messageHandler.ts` - API 请求前检查 + 保存 exp + 处理 401
|
||||||
|
4. **修改**: `src/panels/ICHelperPanel.ts` - 打开聊天面板时检查
|
||||||
|
5. **修改**: `src/views/ICViewProvider.ts` - 侧边栏加载时检查
|
||||||
|
|
||||||
@ -7,10 +7,33 @@ import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
|||||||
import { VCDFileServer } from "./services/vcdFileServer";
|
import { VCDFileServer } from "./services/vcdFileServer";
|
||||||
import { initUserService } from "./services/userService";
|
import { initUserService } from "./services/userService";
|
||||||
import { initCreditsService } from "./services/creditsService";
|
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 插件已激活!");
|
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);
|
initUserService(context);
|
||||||
|
|
||||||
@ -30,7 +53,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
dispose: () => vcdFileServer.stop()
|
dispose: () => vcdFileServer.stop()
|
||||||
});
|
});
|
||||||
|
|
||||||
// 注册 Authentication Provider
|
// 注册 Authentication Provider(此时 icCoderSessions 已经被清除)
|
||||||
const authProvider = new ICCoderAuthenticationProvider(context);
|
const authProvider = new ICCoderAuthenticationProvider(context);
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.authentication.registerAuthenticationProvider(
|
vscode.authentication.registerAuthenticationProvider(
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { VCDViewerPanel } from "./VCDViewerPanel";
|
|||||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||||
import { MessageType } from "../types/chatHistory";
|
import { MessageType } from "../types/chatHistory";
|
||||||
import { getCachedUserInfo } from "../services/userService";
|
import { getCachedUserInfo } from "../services/userService";
|
||||||
|
import { isTokenExpired } from "../utils/jwtUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会员等级图标 URI
|
* 获取会员等级图标 URI
|
||||||
@ -58,6 +59,30 @@ export async function showICHelperPanel(
|
|||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
viewColumn?: vscode.ViewColumn
|
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 {
|
try {
|
||||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
@ -104,6 +129,7 @@ export async function showICHelperPanel(
|
|||||||
.toString(36)
|
.toString(36)
|
||||||
.substr(2, 9)}`;
|
.substr(2, 9)}`;
|
||||||
(panel as any).__uniqueId = panelId;
|
(panel as any).__uniqueId = panelId;
|
||||||
|
(panel as any).__context = context;
|
||||||
|
|
||||||
// 设置标签页图标
|
// 设置标签页图标
|
||||||
panel.iconPath = vscode.Uri.joinPath(
|
panel.iconPath = vscode.Uri.joinPath(
|
||||||
@ -190,6 +216,25 @@ export async function showICHelperPanel(
|
|||||||
console.error('[ICHelperPanel] 获取用户信息失败:', error);
|
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(
|
panel.webview.onDidReceiveMessage(
|
||||||
async (message) => {
|
async (message) => {
|
||||||
@ -524,7 +569,7 @@ export async function showICHelperPanel(
|
|||||||
break;
|
break;
|
||||||
case "logout":
|
case "logout":
|
||||||
// 退出登录(前端已有确认对话框)
|
// 退出登录(前端已有确认对话框)
|
||||||
vscode.commands.executeCommand('iccoder.logout');
|
vscode.commands.executeCommand('ic-coder.logout');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,11 +6,11 @@
|
|||||||
* JWT Payload 接口
|
* JWT Payload 接口
|
||||||
*/
|
*/
|
||||||
export interface JwtPayload {
|
export interface JwtPayload {
|
||||||
sub?: string; // subject (通常是 userId)
|
sub?: string; // subject (通常是 userId)
|
||||||
userId?: number; // 用户ID (驼峰命名)
|
userId?: number; // 用户ID (驼峰命名)
|
||||||
user_id?: number; // 用户ID (下划线命名)
|
user_id?: number; // 用户ID (下划线命名)
|
||||||
exp?: number; // 过期时间
|
exp?: number; // 过期时间
|
||||||
iat?: number; // 签发时间
|
iat?: number; // 签发时间
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,9 +21,9 @@ export interface JwtPayload {
|
|||||||
*/
|
*/
|
||||||
export function parseJwtPayload(token: string): JwtPayload | null {
|
export function parseJwtPayload(token: string): JwtPayload | null {
|
||||||
try {
|
try {
|
||||||
const parts = token.split('.');
|
const parts = token.split(".");
|
||||||
if (parts.length !== 3) {
|
if (parts.length !== 3) {
|
||||||
console.warn('[JWT] token 格式不正确,期望3部分,实际:', parts.length);
|
console.warn("[JWT] token 格式不正确,期望3部分,实际:", parts.length);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,17 +31,17 @@ export function parseJwtPayload(token: string): JwtPayload | null {
|
|||||||
const payload = parts[1];
|
const payload = parts[1];
|
||||||
|
|
||||||
// base64url 转 base64
|
// 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);
|
const parsed = JSON.parse(jsonStr);
|
||||||
|
|
||||||
console.log('[JWT] 解析成功, payload 字段:', Object.keys(parsed));
|
console.log("[JWT] 解析成功, payload 字段:", Object.keys(parsed));
|
||||||
console.log('[JWT] payload 内容:', JSON.stringify(parsed));
|
console.log("[JWT] payload 内容:", JSON.stringify(parsed));
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[JWT] 解析失败:', error);
|
console.error("[JWT] 解析失败:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ export function getUserIdFromToken(token: string): string | null {
|
|||||||
return String(payload.sub);
|
return String(payload.sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
|
console.warn("[JWT] payload 中没有 user_id, userId 或 sub 字段");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,14 +78,17 @@ export function getUserIdFromToken(token: string): string | null {
|
|||||||
* @param bufferSeconds 提前多少秒判定为过期(默认60秒)
|
* @param bufferSeconds 提前多少秒判定为过期(默认60秒)
|
||||||
* @returns true 表示已过期,false 表示未过期,null 表示无法判断
|
* @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);
|
const payload = parseJwtPayload(token);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.exp === undefined) {
|
if (payload.exp === undefined) {
|
||||||
console.warn('[JWT] payload 中没有 exp 字段,无法判断过期');
|
console.warn("[JWT] payload 中没有 exp 字段,无法判断过期");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +97,7 @@ export function isTokenExpired(token: string, bufferSeconds: number = 60): boole
|
|||||||
const isExpired = now >= expTime;
|
const isExpired = now >= expTime;
|
||||||
|
|
||||||
if (isExpired) {
|
if (isExpired) {
|
||||||
console.warn('[JWT] token 已过期,exp:', payload.exp, '当前:', now);
|
console.warn("[JWT] token 已过期,exp:", payload.exp, "当前:", now);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isExpired;
|
return isExpired;
|
||||||
|
|||||||
@ -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 { isTokenExpired } from "./jwtUtils";
|
||||||
import {
|
import {
|
||||||
checkBalanceBeforeSend,
|
checkBalanceBeforeSend,
|
||||||
fetchBalance,
|
fetchBalance,
|
||||||
@ -47,6 +48,83 @@ export async function handleUserMessage(
|
|||||||
) {
|
) {
|
||||||
console.log("收到用户消息:", text);
|
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 {
|
try {
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|||||||
@ -202,6 +202,20 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
|
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)
|
// 检查是否已登录(使用 Authentication API)
|
||||||
this.checkLoginStatus().then((isLoggedIn) => {
|
this.checkLoginStatus().then((isLoggedIn) => {
|
||||||
webviewView.webview.html = this.getWebviewContent(
|
webviewView.webview.html = this.getWebviewContent(
|
||||||
|
|||||||
@ -624,6 +624,19 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
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':
|
case 'showFeedbackQRCode':
|
||||||
// 显示用户反馈二维码弹窗
|
// 显示用户反馈二维码弹窗
|
||||||
console.log('[WebView] 显示用户反馈二维码弹窗');
|
console.log('[WebView] 显示用户反馈二维码弹窗');
|
||||||
|
|||||||
Reference in New Issue
Block a user