Merge branch 'feat/back-to-front' into feat/front-end

This commit is contained in:
Roe-xin
2026-01-13 11:07:53 +08:00
10 changed files with 312 additions and 103 deletions

View File

@ -2,6 +2,7 @@
* API 客户端
* 封装与后端的 HTTP 通信
*/
import * as vscode from 'vscode';
import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
@ -18,6 +19,18 @@ interface RequestOptions {
timeout?: number;
}
/**
* 获取当前登录的 Token
*/
async function getAuthToken(): Promise<string | undefined> {
try {
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
return session?.accessToken;
} catch {
return undefined;
}
}
/**
* 发送 HTTP 请求
*/
@ -25,6 +38,9 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
const url = new URL(getApiUrl(path));
const { timeout } = getConfig();
// 自动获取 Token
const token = await getAuthToken();
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
@ -35,6 +51,7 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
method: options.method,
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...options.headers
},
timeout: options.timeout || timeout

View File

@ -29,7 +29,7 @@ import type {
} from "../types/api";
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { getUserIdFromToken } from "../utils/jwtUtils";
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
import { updateCachedBalance } from "./creditsService";
/**
@ -418,8 +418,9 @@ export class DialogSession {
const config = getConfig();
// 从登录 session 获取真实 userId
// 从登录 session 获取真实 userId 和 token
let userId = config.userId; // 默认值
let token: string | undefined;
try {
console.log("[DialogSession] 尝试获取登录 session...");
const session = await vscode.authentication.getSession("iccoder", [], {
@ -434,6 +435,22 @@ export class DialogSession {
"[DialogSession] accessToken 长度:",
session.accessToken.length
);
// 检测 token 是否过期
const expired = isTokenExpired(session.accessToken);
if (expired === true) {
console.error("[DialogSession] token 已过期,需要重新登录");
vscode.window
.showErrorMessage("登录已过期,请重新登录", "重新登录")
.then((selection) => {
if (selection === "重新登录") {
vscode.commands.executeCommand("iccoder.login");
}
});
throw new Error("登录已过期,请重新登录");
}
token = session.accessToken; // 保存 token 用于扣费
const parsedUserId = getUserIdFromToken(session.accessToken);
console.log("[DialogSession] 解析的 userId:", parsedUserId);
if (parsedUserId) {
@ -475,6 +492,7 @@ export class DialogSession {
userId,
mode: mode || "agent",
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
token, // JWT token 用于扣费
compactedData: compactedData || undefined,
newMessages: newMessages.length > 0 ? newMessages : undefined,
knowledgeData: knowledgeData || undefined,
@ -861,6 +879,23 @@ export class DialogSession {
onError: (data) => {
this.isActive = false;
// 检测登录状态过期(只弹一次窗,不再传递错误)
if (
data.message.includes("LOGIN_EXPIRED") ||
data.message.includes("登录状态已过期")
) {
vscode.window
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
.then((selection) => {
if (selection === "重新登录") {
vscode.commands.executeCommand("ic-coder.login");
}
});
// 登录过期错误已处理,不再传递给外部
return;
}
callbacks.onError?.(data.message);
},
@ -1068,7 +1103,15 @@ export class DialogSession {
selected?: string[],
customInput?: string
): Promise<void> {
await userInteractionManager.receiveAnswer(askId, selected, customInput);
// 直接调用 receiveAnswer传递 taskId 作为 fallbackTaskId
// 如果 pendingQuestions 中有问题,走正常流程
// 如果没有receiveAnswer 会使用 fallbackTaskId 直接发送到后端
await userInteractionManager.receiveAnswer(
askId,
selected,
customInput,
this.taskId
);
}
}

View File

@ -61,6 +61,20 @@ export class ICCoderAuthenticationProvider
scopes: readonly string[]
): Promise<vscode.AuthenticationSession> {
try {
// 先删除旧的 session静默删除不弹窗、不重载窗口
if (this._sessions.length > 0) {
const oldSession = this._sessions[0];
this._sessions = [];
await this.saveSessions();
await clearUserInfo();
this._onDidChangeSessions.fire({
added: [],
removed: [oldSession],
changed: [],
});
console.log("🔄 已清除旧的 session");
}
const token = await this.login();
// 获取到 token 后立即调用用户信息接口

View File

@ -173,7 +173,8 @@ export async function startStreamDialog(
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Content-Length': Buffer.byteLength(body)
'Content-Length': Buffer.byteLength(body),
...(request.token ? { 'Authorization': `Bearer ${request.token}` } : {})
}
};
@ -183,9 +184,20 @@ export async function startStreamDialog(
let errorBody = '';
res.on('data', chunk => errorBody += chunk);
res.on('end', () => {
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
callbacks.onError?.({ message: error.message });
reject(error);
// 检测是否是登录状态过期
const isLoginExpired = errorBody.includes('登录状态已过期') ||
errorBody.includes('token') && errorBody.includes('过期') ||
res.statusCode === 401;
if (isLoginExpired) {
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
callbacks.onError?.({ message: error.message });
reject(error);
} else {
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
callbacks.onError?.({ message: error.message });
reject(error);
}
});
return;
}

View File

@ -82,21 +82,28 @@ export class UserInteractionManager {
* @param askId 问题ID
* @param selected 选中的选项
* @param customInput 自定义输入
* @param fallbackTaskId 当问题不存在时使用的 taskId用于直接发送到后端
*/
async receiveAnswer(
askId: string,
selected?: string[],
customInput?: string
customInput?: string,
fallbackTaskId?: string
): Promise<void> {
const pending = this.pendingQuestions.get(askId);
const answer = customInput || selected?.join(', ') || '';
if (!pending) {
console.warn(`[UserInteraction] 问题不存在或已超时: askId=${askId}`);
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
if (fallbackTaskId) {
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
await this.submitUserAnswer(askId, fallbackTaskId, answer);
} else {
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
}
return;
}
// 构建答案
const answer = customInput || selected?.join(', ') || '';
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
// 移除待处理问题
@ -173,6 +180,13 @@ export class UserInteractionManager {
hasPendingQuestions(): boolean {
return this.pendingQuestions.size > 0;
}
/**
* 检查特定问题是否存在
*/
hasPendingQuestion(askId: string): boolean {
return this.pendingQuestions.has(askId);
}
}
// 全局实例