6 Commits

Author SHA1 Message Date
7d1b8f7e26 fix: 优化登录流程和余额查询逻辑
- 添加 fetchBalanceWithToken 支持登录时直接传入 token 查询余额
- AuthProvider 会话加载改为同步方式避免时序问题
- 添加调试日志便于排查问题
2026-01-13 20:06:42 +08:00
5753e120ba feat: 添加一键优化提示词功能
- 在 ICHelperPanel.ts 添加 optimizePrompt 消息处理分支
- 新增 promptOptimizeService.ts 调用后端优化 API
- 完善 WebView 端优化按钮交互逻辑
2026-01-13 19:29:17 +08:00
21a8abd5cf Merge remote-tracking branch 'origin/feat/front-end' into feat/back-to-front
# Conflicts:
#	src/services/dialogService.ts
2026-01-13 14:34:08 +08:00
4b2da8244f fix: 修复登录状态相关问题
- 修复登录时VSCode弹出"账户不一致"确认框的问题
- 添加SSE业务错误码401检测,正确触发重新登录流程
- 修复侧边栏登录状态不刷新的问题,添加onDidChangeSessions监听
2026-01-13 14:20:55 +08:00
c571cd9137 feat: 更新 Gateway 路由配置
- dev 环境: localhost:8080/iccoder
- test 环境: 192.168.1.108:2029/iccoder
- prod 环境: api.iccoder.com/iccoder
2026-01-13 12:01:35 +08:00
72a84ed9e2 fix: 修复 showPlan 工具交互逻辑和 JWT Token 问题
- 修复 pendingQuestions 缺失时无法提交回答的问题
- 添加 fallbackTaskId 参数支持直接发送到后端
- apiClient 自动获取 JWT Token
- 取消按钮改为中止对话而非发送消息
2026-01-13 10:58:33 +08:00
18 changed files with 811 additions and 481 deletions

View File

@ -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";
@ -31,28 +31,28 @@ export interface IccoderConfig {
/** 环境配置 */ /** 环境配置 */
const ENV_CONFIG: Record<Environment, IccoderConfig> = { const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 本地开发环境 */ /** 本地开发环境 - 通过 Gateway 路由 */
dev: { dev: {
backendUrl: "http://localhost:2233", backendUrl: "http://localhost:8080/iccoder",
backendUrlStrongeLoop: "http://192.168.1.108:2029", backendUrlStrongeLoop: "http://localhost:8080",
loginUrl: "http://localhost/login", loginUrl: "http://localhost/login",
timeout: 300000, timeout: 300000,
userId: "default-user", userId: "default-user",
serviceTier: "max", // 默认使用 max serviceTier: "max", // 默认使用 max
}, },
/** 测试服务器环境 */ /** 测试服务器环境 - 通过 Gateway 路由 */
test: { test: {
backendUrl: "http://192.168.1.108:2233", backendUrl: "http://192.168.1.108:2029/iccoder",
backendUrlStrongeLoop: "http://192.168.1.108:2029", backendUrlStrongeLoop: "http://192.168.1.108:2029",
loginUrl: "http://192.168.1.108:2005/login", loginUrl: "http://192.168.1.108:2005/login",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
serviceTier: "max", serviceTier: "max",
}, },
/** 生产环境 */ /** 生产环境 - 通过 Gateway 路由 */
prod: { prod: {
backendUrl: "https://api.iccoder.com", backendUrl: "https://api.iccoder.com/iccoder",
backendUrlStrongeLoop: "http://api.iccoder.com:2029", backendUrlStrongeLoop: "https://api.iccoder.com",
loginUrl: "https://iccoder.com/login", loginUrl: "https://iccoder.com/login",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",

View File

@ -132,6 +132,17 @@ export function activate(context: vscode.ExtensionContext) {
"ic-coder.login", "ic-coder.login",
async () => { async () => {
try { try {
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
try {
await vscode.authentication.getSession("iccoder", [], {
clearSessionPreference: true,
createIfNone: false
});
} catch {
// 忽略错误
}
// 创建新 session
await vscode.authentication.getSession("iccoder", [], { createIfNone: true }); await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
} catch (error) { } catch (error) {
vscode.window.showErrorMessage(`登录失败: ${error}`); vscode.window.showErrorMessage(`登录失败: ${error}`);

View File

@ -9,6 +9,7 @@ import {
handleReplaceInFile, handleReplaceInFile,
handleUserAnswer, handleUserAnswer,
abortCurrentDialog, abortCurrentDialog,
handleOptimizePrompt,
handlePlanAction, handlePlanAction,
getCurrentTaskId, getCurrentTaskId,
setLastTaskId, setLastTaskId,
@ -328,6 +329,17 @@ export async function showICHelperPanel(
} }
} }
break; break;
case "optimizePrompt":
if (typeof message.prompt === "string") {
void handleOptimizePrompt(panel, message.prompt);
} else {
panel.webview.postMessage({
command: "optimizeResult",
success: false,
error: "提示词为空或格式错误",
});
}
break;
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
case "planAction": case "planAction":
if (message.action === "confirm") { if (message.action === "confirm") {

View File

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

View File

@ -100,7 +100,18 @@ export async function fetchBalance(): Promise<number | null> {
return null; return null;
} }
const token = session.accessToken; return await fetchBalanceWithToken(session.accessToken);
} catch (error) {
console.error('[CreditsService] 查询余额异常:', error);
return null;
}
}
/**
* 使用指定 token 查询余额(登录过程中使用)
*/
export async function fetchBalanceWithToken(token: string): Promise<number | null> {
try {
console.log('[CreditsService] 开始查询余额token 长度:', token.length); console.log('[CreditsService] 开始查询余额token 长度:', token.length);
// 直接调用 StrangeLoop 的 /api/credit/balance 接口 // 直接调用 StrangeLoop 的 /api/credit/balance 接口

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,23 @@ export class ICCoderAuthenticationProvider
private _sessions: vscode.AuthenticationSession[] = []; private _sessions: vscode.AuthenticationSession[] = [];
constructor(private readonly context: vscode.ExtensionContext) { constructor(private readonly context: vscode.ExtensionContext) {
// 从存储中恢复会话 // 从存储中恢复会话(同步执行)
this.loadSessions(); this.loadSessionsSync();
}
/**
* 从存储中加载会话(同步版本)
*/
private loadSessionsSync(): void {
const storedSessions = this.context.globalState.get<
vscode.AuthenticationSession[]
>("icCoderSessions", []);
this._sessions = storedSessions;
console.log("[AuthProvider] 同步加载 sessions, 数量:", this._sessions.length);
if (this._sessions.length > 0) {
console.log("[AuthProvider] Session ID:", this._sessions[0].id);
console.log("[AuthProvider] Account:", this._sessions[0].account.label);
}
} }
/** /**
@ -42,7 +57,9 @@ export class ICCoderAuthenticationProvider
* 保存会话到存储 * 保存会话到存储
*/ */
private async saveSessions(): Promise<void> { private async saveSessions(): Promise<void> {
console.log("[AuthProvider] 保存 sessions, 数量:", this._sessions.length);
await this.context.globalState.update("icCoderSessions", this._sessions); await this.context.globalState.update("icCoderSessions", this._sessions);
console.log("[AuthProvider] sessions 已保存到 globalState");
} }
/** /**
@ -51,6 +68,7 @@ export class ICCoderAuthenticationProvider
async getSessions( async getSessions(
scopes?: readonly string[] scopes?: readonly string[]
): Promise<vscode.AuthenticationSession[]> { ): Promise<vscode.AuthenticationSession[]> {
console.log("[AuthProvider] getSessions 被调用, 当前 sessions 数量:", this._sessions.length);
return [...this._sessions]; return [...this._sessions];
} }
@ -61,6 +79,20 @@ export class ICCoderAuthenticationProvider
scopes: readonly string[] scopes: readonly string[]
): Promise<vscode.AuthenticationSession> { ): Promise<vscode.AuthenticationSession> {
try { 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(); const token = await this.login();
// 获取到 token 后立即调用用户信息接口 // 获取到 token 后立即调用用户信息接口

View File

@ -0,0 +1,103 @@
/**
* 提示词优化服务
* 调用后端 API 优化用户输入的提示词
*/
import * as vscode from 'vscode';
import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
import { getApiUrl } from '../config/settings';
/** 优化响应类型 */
interface OptimizeResponse {
success: boolean;
optimizedPrompt?: string;
error?: string;
}
/**
* 优化提示词
* @param prompt 原始提示词
* @returns 优化后的提示词
*/
export async function optimizePrompt(prompt: string): Promise<string> {
// 获取 JWT token
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
if (!session?.accessToken) {
throw new Error('未登录,请先登录');
}
const response = await callOptimizeApi(prompt, session.accessToken);
if (response.success && response.optimizedPrompt) {
return response.optimizedPrompt;
} else {
throw new Error(response.error || '优化失败');
}
}
/**
* 调用后端优化 API
*/
async function callOptimizeApi(prompt: string, token: string): Promise<OptimizeResponse> {
const urlStr = getApiUrl('/api/prompt/optimize');
const url = new URL(urlStr);
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const body = JSON.stringify({ prompt });
const requestOptions: http.RequestOptions = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
'Authorization': `Bearer ${token}`
},
timeout: 30000
};
return new Promise((resolve, reject) => {
const req = httpModule.request(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('[PromptOptimize] 响应状态码:', res.statusCode);
try {
const json = JSON.parse(data);
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve(json as OptimizeResponse);
} else if (res.statusCode === 401 || res.statusCode === 403) {
resolve({ success: false, error: '登录已过期,请重新登录' });
} else {
resolve({ success: false, error: json.error || json.message || `HTTP ${res.statusCode}` });
}
} catch (e) {
resolve({ success: false, error: `解析响应失败: ${data}` });
}
});
});
req.on('error', (error) => {
reject(error);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('请求超时'));
});
req.write(body);
req.end();
});
}

View File

@ -173,7 +173,8 @@ export async function startStreamDialog(
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'text/event-stream', 'Accept': 'text/event-stream',
'Cache-Control': 'no-cache', '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 = ''; let errorBody = '';
res.on('data', chunk => errorBody += chunk); res.on('data', chunk => errorBody += chunk);
res.on('end', () => { res.on('end', () => {
// 检测是否是登录状态过期
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}`); const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
callbacks.onError?.({ message: error.message }); callbacks.onError?.({ message: error.message });
reject(error); reject(error);
}
}); });
return; return;
} }
@ -226,6 +238,25 @@ export async function startStreamDialog(
res.on('data', (chunk: string) => { res.on('data', (chunk: string) => {
if (!controller.aborted) { if (!controller.aborted) {
console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200)); console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200));
// 检查是否是业务错误码Gateway 返回 HTTP 200 但响应体是错误 JSON
try {
const trimmed = chunk.trim();
if (trimmed.startsWith('{') && trimmed.includes('"code"')) {
const json = JSON.parse(trimmed);
if (json.code === 401 || json.msg?.includes('登录状态已过期')) {
console.log('[SSE] 检测到登录过期业务错误');
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
callbacks.onError?.({ message: error.message });
controller.abort();
reject(error);
return;
}
}
} catch {
// 不是 JSON 格式,继续正常处理
}
parser.feed(chunk); parser.feed(chunk);
} }
}); });

View File

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

View File

@ -8,7 +8,7 @@ import { URL } from 'url';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { getStrangeLoopApiUrl, getConfig } from '../config/settings'; import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api'; import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
import { fetchBalance, getCachedBalance } from './creditsService'; import { fetchBalanceWithToken, getCachedBalance } from './creditsService';
/** /**
* HTTP 请求选项 * HTTP 请求选项
@ -230,7 +230,7 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
const [userInfo, membershipInfo, credits] = await Promise.all([ const [userInfo, membershipInfo, credits] = await Promise.all([
getUserInfo(token), getUserInfo(token),
getMembershipInfo(token), getMembershipInfo(token),
fetchBalance() fetchBalanceWithToken(token)
]); ]);
if (!userInfo) { if (!userInfo) {

View File

@ -40,6 +40,8 @@ export interface DialogRequest {
mode: RunMode; mode: RunMode;
/** 服务等级 */ /** 服务等级 */
serviceTier?: ServiceTier; serviceTier?: ServiceTier;
/** JWT Token用于认证和扣费 */
token?: string;
/** 压缩后的记忆数据(用于后端重启后恢复) */ /** 压缩后的记忆数据(用于后端重启后恢复) */
compactedData?: CompactedMemory; compactedData?: CompactedMemory;
/** 压缩后产生的新消息 */ /** 压缩后产生的新消息 */

View File

@ -71,3 +71,31 @@ export function getUserIdFromToken(token: string): string | null {
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段'); console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
return null; return null;
} }
/**
* 检测 JWT token 是否已过期
* @param token JWT token
* @param bufferSeconds 提前多少秒判定为过期默认60秒
* @returns true 表示已过期false 表示未过期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 字段,无法判断过期');
return null;
}
const now = Math.floor(Date.now() / 1000);
const expTime = payload.exp - bufferSeconds;
const isExpired = now >= expTime;
if (isExpired) {
console.warn('[JWT] token 已过期exp:', payload.exp, '当前:', now);
}
return isExpired;
}

View File

@ -22,6 +22,7 @@ import {
checkBalanceBeforeSend, checkBalanceBeforeSend,
fetchBalance, fetchBalance,
} from "../services/creditsService"; } from "../services/creditsService";
import { optimizePrompt } from "../services/promptOptimizeService";
import type { RunMode, ServiceTier } from "../types/api"; import type { RunMode, ServiceTier } from "../types/api";
@ -1031,3 +1032,35 @@ async function handleVCDGeneration(
vscode.window.showErrorMessage(errorMsg); vscode.window.showErrorMessage(errorMsg);
} }
} }
/**
* 处理提示词优化请求
*/
export async function handleOptimizePrompt(
panel: vscode.WebviewPanel,
prompt: string
): Promise<void> {
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
console.log("[MessageHandler] prompt:", prompt);
console.log("[MessageHandler] prompt 长度:", prompt?.length);
try {
console.log("[MessageHandler] 开始调用 optimizePrompt...");
const optimized = await optimizePrompt(prompt);
console.log("[MessageHandler] 优化成功,结果:", optimized);
panel.webview.postMessage({
command: "optimizeResult",
success: true,
optimizedPrompt: optimized,
});
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "优化失败";
console.error("[MessageHandler] 提示词优化失败:", errorMsg);
panel.webview.postMessage({
command: "optimizeResult",
success: false,
error: errorMsg,
});
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
}
}

View File

@ -1,5 +1,6 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import { getWebviewContent } from "./webviewContent"; import { getWebviewContent } from "./webviewContent";
import { isTokenExpired } from "../utils/jwtUtils";
import { import {
handleUserMessage, handleUserMessage,
insertCodeToEditor, insertCodeToEditor,
@ -10,6 +11,7 @@ import {
handleReplaceInFile, handleReplaceInFile,
handleUserAnswer, handleUserAnswer,
abortCurrentDialog, abortCurrentDialog,
handleOptimizePrompt,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
/** /**
@ -69,6 +71,9 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
// 处理消息 // 处理消息
panel.webview.onDidReceiveMessage( panel.webview.onDidReceiveMessage(
(message) => { (message) => {
console.log("[ICViewProvider] ====== 收到 WebView 消息 ======");
console.log("[ICViewProvider] command:", message.command);
console.log("[ICViewProvider] 完整消息:", JSON.stringify(message));
switch (message.command) { switch (message.command) {
case "sendMessage": case "sendMessage":
handleUserMessage(panel, message.text, context.extensionPath, message.mode); handleUserMessage(panel, message.text, context.extensionPath, message.mode);
@ -116,6 +121,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
case "abortDialog": case "abortDialog":
void abortCurrentDialog(); void abortCurrentDialog();
break; break;
// 新增:优化提示词
case "optimizePrompt":
handleOptimizePrompt(panel, message.prompt);
break;
} }
}, },
undefined, undefined,
@ -127,10 +136,34 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
* 侧边栏视图提供者 * 侧边栏视图提供者
*/ */
export class ICViewProvider implements vscode.WebviewViewProvider { export class ICViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
constructor( constructor(
private readonly extensionUri: vscode.Uri, private readonly extensionUri: vscode.Uri,
private readonly context: vscode.ExtensionContext private readonly context: vscode.ExtensionContext
) {} ) {
// 监听认证状态变化
this.context.subscriptions.push(
vscode.authentication.onDidChangeSessions((e) => {
if (e.provider.id === "iccoder") {
this.refreshLoginStatus();
}
})
);
}
/**
* 刷新登录状态并更新视图
*/
private async refreshLoginStatus(): Promise<void> {
if (this._view) {
const isLoggedIn = await this.checkLoginStatus();
this._view.webview.html = this.getWebviewContent(
this._view.webview,
isLoggedIn
);
}
}
/** /**
* 检查登录状态(使用 Authentication API * 检查登录状态(使用 Authentication API
@ -138,14 +171,29 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
private async checkLoginStatus(): Promise<boolean> { private async checkLoginStatus(): Promise<boolean> {
try { try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false }); const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
return !!session; console.log("[ICViewProvider] 检查登录状态, session:", session ? "存在" : "不存在");
if (!session) {
return false;
}
// 检查 token 是否过期
const expired = isTokenExpired(session.accessToken);
console.log("[ICViewProvider] token 过期检查结果:", expired);
// 只有明确过期才认为未登录,无法判断时认为已登录
if (expired === true) {
console.log("[ICViewProvider] Token 已过期");
return false;
}
return true;
} catch (error) { } catch (error) {
console.log("检查登录状态失败:", error); console.log("[ICViewProvider] 检查登录状态失败:", error);
return false; return false;
} }
} }
resolveWebviewView(webviewView: vscode.WebviewView) { resolveWebviewView(webviewView: vscode.WebviewView) {
// 保存引用以便后续刷新
this._view = webviewView;
webviewView.webview.options = { webviewView.webview.options = {
enableScripts: true, enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")], localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],

View File

@ -60,35 +60,97 @@ export function getOptimizeButtonScript(): string {
return ` return `
let isOptimized = false; // 标记是否已优化 let isOptimized = false; // 标记是否已优化
let originalText = ''; // 保存原始文本用于撤回 let originalText = ''; // 保存原始文本用于撤回
let isOptimizing = false; // 标记是否正在优化中
function handleOptimize() { function handleOptimize() {
console.log('[Optimize] handleOptimize 被调用');
console.log('[Optimize] isOptimizing:', isOptimizing);
console.log('[Optimize] isOptimized:', isOptimized);
console.log('[Optimize] messageInput:', messageInput);
if (isOptimizing) {
console.log('[Optimize] 正在优化中,忽略点击');
return; // 正在优化中,忽略点击
}
if (isOptimized) { if (isOptimized) {
// 撤回操作 // 撤回操作
console.log('[Optimize] 执行撤回操作');
messageInput.value = originalText; messageInput.value = originalText;
resetOptimizeButton(); resetOptimizeButton();
} else { } else {
// 优化操作 // 优化操作
const currentText = messageInput.value.trim();
console.log('[Optimize] 当前输入内容:', currentText);
console.log('[Optimize] 内容长度:', currentText.length);
if (!currentText) {
console.log('[Optimize] 输入框为空,不执行优化');
return; // 输入框为空,不执行优化
}
originalText = messageInput.value; // 保存原始文本 originalText = messageInput.value; // 保存原始文本
isOptimizing = true;
console.log('[Optimize] 开始优化,显示加载状态');
// 使用死数据替换输入框内容 // 显示加载状态
const optimizedTexts = [ showOptimizeLoading();
'请帮我优化这段代码,提高性能和可读性',
'请分析这个问题并给出最佳解决方案',
'请帮我重构这段代码,使其更加简洁高效',
'请检查代码中的潜在问题并提供改进建议'
];
const randomText = optimizedTexts[Math.floor(Math.random() * optimizedTexts.length)];
messageInput.value = randomText;
// 切换到撤回状态 // 发送优化请求到扩展
isOptimized = true; console.log('[Optimize] 发送 optimizePrompt 消息');
updateOptimizeButton(); vscode.postMessage({
command: 'optimizePrompt',
prompt: currentText
});
console.log('[Optimize] postMessage 已发送');
} }
messageInput.focus(); messageInput.focus();
autoResizeTextarea(); autoResizeTextarea();
} }
// 处理优化结果
function handleOptimizeResult(success, optimizedPrompt, error) {
isOptimizing = false;
hideOptimizeLoading();
if (success && optimizedPrompt) {
messageInput.value = optimizedPrompt;
isOptimized = true;
updateOptimizeButton();
} else {
// 优化失败,恢复原始文本
messageInput.value = originalText;
console.error('优化失败:', error);
}
messageInput.focus();
autoResizeTextarea();
}
function showOptimizeLoading() {
const optimizeButton = document.getElementById('optimizeButton');
const optimizeIcon = document.getElementById('optimizeIcon');
if (optimizeButton && optimizeIcon) {
optimizeButton.disabled = true;
optimizeButton.style.opacity = '0.5';
// 显示加载动画
optimizeIcon.innerHTML = '<circle cx="512" cy="512" r="400" fill="none" stroke="#409eff" stroke-width="60" stroke-dasharray="1200" stroke-dashoffset="0"><animateTransform attributeName="transform" type="rotate" from="0 512 512" to="360 512 512" dur="1s" repeatCount="indefinite"/></circle>';
}
}
function hideOptimizeLoading() {
const optimizeButton = document.getElementById('optimizeButton');
if (optimizeButton) {
optimizeButton.disabled = false;
optimizeButton.style.opacity = '1';
}
// 恢复图标会在 updateOptimizeButton 或 resetOptimizeButton 中处理
if (!isOptimized) {
resetOptimizeButton();
}
}
function updateOptimizeButton() { function updateOptimizeButton() {
const optimizeIcon = document.getElementById('optimizeIcon'); const optimizeIcon = document.getElementById('optimizeIcon');
const optimizeTooltip = document.getElementById('optimizeTooltip'); const optimizeTooltip = document.getElementById('optimizeTooltip');

View File

@ -150,24 +150,50 @@ export function getPlanCardStyles(): string {
.plan-actions { .plan-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 12px;
padding: 14px 16px; padding: 14px 16px;
border-top: 1px solid var(--vscode-input-border); border-top: 1px solid var(--vscode-input-border);
background: var(--vscode-sideBar-background); background: var(--vscode-sideBar-background);
} }
.plan-actions .question-options { .plan-input-row {
display: flex; display: flex;
flex-wrap: wrap;
gap: 8px; gap: 8px;
width: 100%;
}
.plan-input {
flex: 1;
padding: 10px 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 13px;
box-sizing: border-box;
}
.plan-input:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.plan-btn-row {
display: flex;
gap: 10px;
} }
.plan-btn { .plan-btn {
padding: 8px 18px; padding: 8px 20px;
border-radius: 4px; border-radius: 4px;
border: none; border: none;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 13px;
font-weight: 500; font-weight: 500;
} }
.plan-btn-submit {
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border: 1px solid var(--vscode-input-border);
}
.plan-btn-submit:hover {
background: var(--vscode-list-hoverBackground);
}
.plan-btn-confirm { .plan-btn-confirm {
background: var(--vscode-button-background); background: var(--vscode-button-background);
color: var(--vscode-button-foreground); color: var(--vscode-button-foreground);
@ -175,41 +201,26 @@ export function getPlanCardStyles(): string {
.plan-btn-confirm:hover { .plan-btn-confirm:hover {
background: var(--vscode-button-hoverBackground); background: var(--vscode-button-hoverBackground);
} }
.plan-btn-modify {
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border: 1px solid var(--vscode-input-border);
}
.plan-btn-cancel { .plan-btn-cancel {
background: transparent; background: transparent;
color: var(--vscode-descriptionForeground); color: var(--vscode-descriptionForeground);
}
.plan-actions .custom-input-container {
display: flex;
gap: 8px;
width: 100%;
}
.plan-actions .custom-input {
flex: 1;
padding: 8px 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border); border: 1px solid var(--vscode-input-border);
border-radius: 4px; }
.plan-btn-cancel:hover {
background: var(--vscode-list-hoverBackground);
}
.plan-answered {
padding: 12px 16px;
border-top: 1px solid var(--vscode-input-border);
background: var(--vscode-sideBar-background);
font-size: 13px; font-size: 13px;
} }
.plan-actions .custom-submit { .answered-label {
padding: 8px 18px; color: var(--vscode-descriptionForeground);
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
} }
.plan-actions .custom-submit:hover { .answered-value {
background: var(--vscode-button-hoverBackground); color: var(--vscode-textLink-foreground);
font-weight: 500;
} }
/* 阶段进度条样式 */ /* 阶段进度条样式 */
@ -597,16 +608,17 @@ export function getPlanCardScript(): string {
// 兼容旧格式:渲染步骤列表 // 兼容旧格式:渲染步骤列表
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : ''; const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
// 选项按钮
const options = ['确认执行', '修改计划', '取消'];
const optionsHtml = options.map(opt => {
const isSelected = isAnswered && opt === selectedAnswer;
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join('');
// 渲染 Markdown 格式的摘要 // 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || ''); const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
// 已回答时显示用户的选择
const answeredHtml = isAnswered ? \`
<div class="plan-answered">
<span class="answered-label">已回复:</span>
<span class="answered-value">\${selectedAnswer}</span>
</div>
\` : '';
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="plan-card"> <div class="plan-card">
<div class="plan-header"> <div class="plan-header">
@ -618,59 +630,72 @@ export function getPlanCardScript(): string {
<div class="plan-summary">\${summaryHtml}</div> <div class="plan-summary">\${summaryHtml}</div>
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`} \${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
</div> </div>
<div class="plan-actions"> <div class="plan-actions" data-ask-id="\${segment.askId}" style="display: \${isAnswered ? 'none' : 'flex'};">
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div> <div class="plan-input-row">
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};"> <input type="text" class="plan-input" placeholder="输入修改建议..." />
<input type="text" class="custom-input" placeholder="输入修改建议..." /> <button class="plan-btn plan-btn-submit">提交修改</button>
<button class="custom-submit">提交</button> </div>
<div class="plan-btn-row">
<button class="plan-btn plan-btn-confirm">确认执行</button>
<button class="plan-btn plan-btn-cancel">取消</button>
</div> </div>
</div> </div>
\${answeredHtml}
</div> </div>
\`; \`;
// 只在未回答时添加事件监听 // 只在未回答时添加事件监听
if (!isAnswered) { if (!isAnswered) {
setTimeout(() => { setTimeout(() => {
const optionButtons = segmentDiv.querySelectorAll('.question-option'); const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
optionButtons.forEach(btn => { const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
btn.addEventListener('click', function() { const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
const option = this.getAttribute('data-option'); const planInput = segmentDiv.querySelector('.plan-input');
// 发送答案到后端
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
// 同时发送 planAction 用于模式切换
const actionMap = {
'确认执行': 'confirm',
'修改计划': 'modify',
'取消': 'cancel'
};
vscode.postMessage({
command: 'planAction',
action: actionMap[option] || option,
planTitle: segment.planTitle,
model: getCurrentModel()
});
});
});
const submitBtn = segmentDiv.querySelector('.custom-submit'); // 提交修改按钮
const customInput = segmentDiv.querySelector('.custom-input'); if (submitBtn && planInput) {
if (submitBtn && customInput) {
submitBtn.addEventListener('click', function() { submitBtn.addEventListener('click', function() {
const customValue = customInput.value.trim(); const inputValue = planInput.value.trim();
if (customValue) { if (inputValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
} }
}); });
customInput.addEventListener('keypress', function(e) { // 回车键提交修改
planInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
const customValue = customInput.value.trim(); const inputValue = planInput.value.trim();
if (customValue) { if (inputValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
} }
} }
}); });
} }
// 确认执行按钮
if (confirmBtn) {
confirmBtn.addEventListener('click', function() {
handleQuestionAnswerInSegment(segment.askId, '确认执行', segmentDiv);
});
}
// 取消按钮 - 直接中止对话,不发送给智能体
if (cancelBtn) {
cancelBtn.addEventListener('click', function() {
// 标记问题已回答
answeredQuestions.set(segment.askId, '取消');
segmentDiv.classList.add('answered');
// 隐藏操作按钮
const actionsDiv = segmentDiv.querySelector('.plan-actions');
if (actionsDiv) {
actionsDiv.style.display = 'none';
}
// 发送中止对话命令
vscode.postMessage({ command: 'abortDialog' });
});
}
}, 0); }, 0);
} }
} }
@ -703,28 +728,63 @@ export function getPlanCardScript(): string {
<div class="plan-summary">\${summaryHtml}</div> <div class="plan-summary">\${summaryHtml}</div>
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`} \${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
</div> </div>
<div class="plan-actions"> <div class="plan-actions" data-ask-id="\${segment.askId}">
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button> <div class="plan-input-row">
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button> <input type="text" class="plan-input" placeholder="输入修改建议..." />
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button> <button class="plan-btn plan-btn-submit">提交修改</button>
</div>
<div class="plan-btn-row">
<button class="plan-btn plan-btn-confirm">确认执行</button>
<button class="plan-btn plan-btn-cancel">取消</button>
</div>
</div> </div>
</div> </div>
\`; \`;
// 绑定按钮事件 // 绑定按钮事件(静态渲染时也需要能响应)
setTimeout(() => { setTimeout(() => {
const planCard = segmentDiv.querySelector('.plan-card'); const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
if (planCard) { const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
planCard.querySelectorAll('.plan-btn').forEach(btn => { const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
btn.addEventListener('click', (e) => { const planInput = segmentDiv.querySelector('.plan-input');
const action = e.currentTarget?.dataset?.action;
// 提交修改按钮
if (submitBtn && planInput) {
submitBtn.addEventListener('click', function() {
const inputValue = planInput.value.trim();
if (inputValue) {
vscode.postMessage({ vscode.postMessage({
command: 'planAction', command: 'submitAnswer',
action: action, askId: segment.askId,
planTitle: segment.planTitle, selected: [inputValue],
model: getCurrentModel() customInput: inputValue
});
}
});
}
// 确认执行按钮
if (confirmBtn) {
confirmBtn.addEventListener('click', function() {
vscode.postMessage({
command: 'submitAnswer',
askId: segment.askId,
selected: ['确认执行'],
customInput: '确认执行'
}); });
}); });
}
// 取消按钮 - 直接中止对话
if (cancelBtn) {
cancelBtn.addEventListener('click', function() {
// 隐藏操作按钮
const actionsDiv = segmentDiv.querySelector('.plan-actions');
if (actionsDiv) {
actionsDiv.style.display = 'none';
}
// 发送中止对话命令
vscode.postMessage({ command: 'abortDialog' });
}); });
} }
}, 0); }, 0);

View File

@ -428,6 +428,7 @@ export function getWebviewContent(
<script> <script>
console.log('[WebView] 脚本开始执行'); console.log('[WebView] 脚本开始执行');
const vscode = acquireVsCodeApi(); const vscode = acquireVsCodeApi();
window.vscode = vscode; // 确保全局可访问
console.log('[WebView] vscode API 已获取'); console.log('[WebView] vscode API 已获取');
const messageInput = document.getElementById('messageInput'); const messageInput = document.getElementById('messageInput');
const modeSelect = document.getElementById('modeSelect'); const modeSelect = document.getElementById('modeSelect');
@ -742,6 +743,13 @@ export function getWebviewContent(
} }
break; break;
case 'optimizeResult':
// 处理提示词优化结果
if (typeof handleOptimizeResult === 'function') {
handleOptimizeResult(message.success, message.optimizedPrompt, message.error);
}
break;
default: default:
console.log('[WebView] 未处理的消息类型:', message.command); console.log('[WebView] 未处理的消息类型:', message.command);
} }