fix: 修复 showPlan 工具交互逻辑和 JWT Token 问题
- 修复 pendingQuestions 缺失时无法提交回答的问题 - 添加 fallbackTaskId 参数支持直接发送到后端 - apiClient 自动获取 JWT Token - 取消按钮改为中止对话而非发送消息
This commit is contained in:
@ -128,7 +128,15 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
"ic-coder.login",
|
"ic-coder.login",
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
// 检查是否有现有 session
|
||||||
|
const existingSession = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||||
|
if (existingSession) {
|
||||||
|
// 有旧 session,使用 forceNewSession 强制创建新 session
|
||||||
|
await vscode.authentication.getSession("iccoder", [], { forceNewSession: true });
|
||||||
|
} else {
|
||||||
|
// 没有旧 session,使用 createIfNone 创建新 session
|
||||||
|
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -12,7 +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';
|
import { getUserIdFromToken, isTokenExpired } from '../utils/jwtUtils';
|
||||||
import { updateCachedBalance } from './creditsService';
|
import { updateCachedBalance } from './creditsService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -342,14 +342,29 @@ export class DialogSession {
|
|||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
// 从登录 session 获取真实 userId
|
// 从登录 session 获取真实 userId 和 token
|
||||||
let userId = config.userId; // 默认值
|
let userId = config.userId; // 默认值
|
||||||
|
let token: string | undefined;
|
||||||
try {
|
try {
|
||||||
console.log('[DialogSession] 尝试获取登录 session...');
|
console.log('[DialogSession] 尝试获取登录 session...');
|
||||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||||
console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined');
|
console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined');
|
||||||
if (session?.accessToken) {
|
if (session?.accessToken) {
|
||||||
console.log('[DialogSession] accessToken 长度:', session.accessToken.length);
|
console.log('[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);
|
const parsedUserId = getUserIdFromToken(session.accessToken);
|
||||||
console.log('[DialogSession] 解析的 userId:', parsedUserId);
|
console.log('[DialogSession] 解析的 userId:', parsedUserId);
|
||||||
if (parsedUserId) {
|
if (parsedUserId) {
|
||||||
@ -380,6 +395,7 @@ export class DialogSession {
|
|||||||
userId,
|
userId,
|
||||||
mode: mode || 'agent',
|
mode: mode || 'agent',
|
||||||
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
||||||
|
token, // JWT token 用于扣费
|
||||||
compactedData: compactedData || undefined,
|
compactedData: compactedData || undefined,
|
||||||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||||||
knowledgeData: knowledgeData || undefined
|
knowledgeData: knowledgeData || undefined
|
||||||
@ -711,6 +727,18 @@ export class DialogSession {
|
|||||||
|
|
||||||
onError: (data) => {
|
onError: (data) => {
|
||||||
this.isActive = false;
|
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);
|
callbacks.onError?.(data.message);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -895,7 +923,10 @@ export class DialogSession {
|
|||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string
|
customInput?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await userInteractionManager.receiveAnswer(askId, selected, customInput);
|
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
||||||
|
// 如果 pendingQuestions 中有问题,走正常流程
|
||||||
|
// 如果没有,receiveAnswer 会使用 fallbackTaskId 直接发送到后端
|
||||||
|
await userInteractionManager.receiveAnswer(askId, selected, customInput, this.taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,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 后立即调用用户信息接口
|
||||||
|
|||||||
@ -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 error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
|
// 检测是否是登录状态过期
|
||||||
callbacks.onError?.({ message: error.message });
|
const isLoginExpired = errorBody.includes('登录状态已过期') ||
|
||||||
reject(error);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局实例
|
// 全局实例
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export interface DialogRequest {
|
|||||||
mode: RunMode;
|
mode: RunMode;
|
||||||
/** 服务等级 */
|
/** 服务等级 */
|
||||||
serviceTier?: ServiceTier;
|
serviceTier?: ServiceTier;
|
||||||
|
/** JWT Token(用于认证和扣费) */
|
||||||
|
token?: string;
|
||||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||||
compactedData?: CompactedMemory;
|
compactedData?: CompactedMemory;
|
||||||
/** 压缩后产生的新消息 */
|
/** 压缩后产生的新消息 */
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
@ -138,7 +139,17 @@ 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;
|
if (!session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 检查 token 是否过期
|
||||||
|
const expired = isTokenExpired(session.accessToken);
|
||||||
|
// 如果已过期或无法判断(null),都认为未登录
|
||||||
|
if (expired === true || expired === null) {
|
||||||
|
console.log("Token 已过期或无法判断过期状态");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("检查登录状态失败:", error);
|
console.log("检查登录状态失败:", error);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -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,30 +728,65 @@ 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);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user