3 Commits

Author SHA1 Message Date
5c19be22d3 feat: 实现计划管理工具和进度条实时更新
- 添加 plan_step_add/remove/update 和 plan_summary_update 事件支持
- 添加 onPhaseProgress 回调,联动独立进度条组件
- 扩展 MessageSegment 接口支持 progress 类型
- 映射 phaseId (sim -> simulation) 适配进度条
2026-01-09 19:26:55 +08:00
5546791549 feat: Plan卡片支持Markdown渲染和智能步骤解析
- 添加renderPlanMarkdown函数,支持标题、列表、表格、代码块等
- 添加renderPlanSteps函数,智能解析JSON格式步骤对象
- 步骤显示模块名、描述、输入输出、逻辑等详细信息
- 添加plan-summary和step-details样式
2026-01-09 17:02:00 +08:00
178f3a7498 feat: 从JWT解析userId并添加资源点余额提醒
- 新增 jwtUtils.ts 解析JWT token获取user_id
- dialogService 从登录session获取真实userId
- 添加 credit_update 事件处理
- 余额低于5点时弹窗提醒用户充值
- settings.ts 登录URL改为可配置
2026-01-09 15:53:54 +08:00
8 changed files with 822 additions and 24 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";
@ -17,6 +17,8 @@ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
export interface IccoderConfig { export interface IccoderConfig {
/** 后端服务地址 */ /** 后端服务地址 */
backendUrl: string; backendUrl: string;
/** 登录页面地址 */
loginUrl: string;
/** 请求超时时间(毫秒) */ /** 请求超时时间(毫秒) */
timeout: number; timeout: number;
/** 用户ID临时使用后续对接认证 */ /** 用户ID临时使用后续对接认证 */
@ -30,6 +32,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 本地开发环境 */ /** 本地开发环境 */
dev: { dev: {
backendUrl: "http://localhost:2233", backendUrl: "http://localhost:2233",
loginUrl: "http://localhost/login",
timeout: 300000, timeout: 300000,
userId: "default-user", userId: "default-user",
serviceTier: "max", // 默认使用 max serviceTier: "max", // 默认使用 max
@ -37,6 +40,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 测试服务器环境 */ /** 测试服务器环境 */
test: { test: {
backendUrl: "http://192.168.1.108:2233", backendUrl: "http://192.168.1.108:2233",
loginUrl: "http://192.168.1.108:2005/login",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
serviceTier: "max", serviceTier: "max",
@ -44,6 +48,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 生产环境 */ /** 生产环境 */
prod: { prod: {
backendUrl: "https://api.iccoder.com", backendUrl: "https://api.iccoder.com",
loginUrl: "https://iccoder.com/login",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
serviceTier: "auto", serviceTier: "auto",

View File

@ -12,12 +12,13 @@ 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';
/** /**
* 消息段落类型 * 消息段落类型
*/ */
export interface MessageSegment { export interface MessageSegment {
type: 'text' | 'tool' | 'question' | 'agent' | 'plan'; type: 'text' | 'tool' | 'question' | 'agent' | 'plan' | 'progress';
content?: string; content?: string;
toolName?: string; toolName?: string;
toolStatus?: 'running' | 'success' | 'error'; toolStatus?: 'running' | 'success' | 'error';
@ -32,8 +33,11 @@ export interface MessageSegment {
agentSteps?: AgentStep[]; agentSteps?: AgentStep[];
// 计划相关字段 // 计划相关字段
planTitle?: string; planTitle?: string;
planPhases?: import('../types/api').PlanPhase[];
planSteps?: string[]; planSteps?: string[];
planSummary?: string; planSummary?: string;
// 进度条相关字段(独立于 plan用于执行模式
progressPhases?: import('../types/api').PlanPhase[];
} }
/** /**
@ -62,7 +66,7 @@ export interface DialogCallbacks {
/** 工具确认请求Ask 模式) */ /** 工具确认请求Ask 模式) */
onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void; onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void;
/** 计划确认请求Plan 模式) */ /** 计划确认请求Plan 模式) */
onPlanConfirm?: (confirmId: number, title: string, steps: string[], summary: string) => void; onPlanConfirm?: (confirmId: number, title: string, phases: import('../types/api').PlanPhase[] | undefined, steps: string[] | undefined, summary: string) => void;
/** 显示问题ask_user */ /** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void; onQuestion?: (askId: string, question: string, options: string[]) => void;
/** 实时更新段落(流式过程中) */ /** 实时更新段落(流式过程中) */
@ -75,6 +79,8 @@ export interface DialogCallbacks {
onNotification?: (message: string) => void; onNotification?: (message: string) => void;
/** 上下文使用量更新 */ /** 上下文使用量更新 */
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void; onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
/** 阶段进度更新 */
onPhaseProgress?: (phaseId: string, status: string) => void;
} }
/** /**
@ -331,6 +337,27 @@ export class DialogSession {
const config = getConfig(); const config = getConfig();
// 从登录 session 获取真实 userId
let userId = config.userId; // 默认值
try {
console.log('[DialogSession] 尝试获取登录 session...');
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined');
if (session?.accessToken) {
console.log('[DialogSession] accessToken 长度:', session.accessToken.length);
const parsedUserId = getUserIdFromToken(session.accessToken);
console.log('[DialogSession] 解析的 userId:', parsedUserId);
if (parsedUserId) {
userId = parsedUserId;
console.log('[DialogSession] 使用真实 userId:', userId);
}
} else {
console.log('[DialogSession] 未获取到 accessToken使用默认 userId:', userId);
}
} catch (error) {
console.warn('[DialogSession] 获取登录 session 失败:', error);
}
// 获取压缩数据和新消息(用于后端重启后恢复) // 获取压缩数据和新消息(用于后端重启后恢复)
const historyManager = ChatHistoryManager.getInstance(); const historyManager = ChatHistoryManager.getInstance();
const compactedData = await historyManager.loadCompactedData(this.taskId); const compactedData = await historyManager.loadCompactedData(this.taskId);
@ -343,7 +370,7 @@ export class DialogSession {
const request: DialogRequest = { const request: DialogRequest = {
taskId: this.taskId, taskId: this.taskId,
message, message,
userId: config.userId, userId,
mode: mode || 'agent', mode: mode || 'agent',
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数 serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
compactedData: compactedData || undefined, compactedData: compactedData || undefined,
@ -508,10 +535,12 @@ export class DialogSession {
const askId = `ask_${data.confirmId}`; const askId = `ask_${data.confirmId}`;
// 添加计划段落到聊天界面(包含 askId 用于响应) // 添加计划段落到聊天界面(包含 askId 用于响应)
// 支持新格式phases和旧格式steps
this.segments.push({ this.segments.push({
type: 'plan', type: 'plan',
askId: askId, askId: askId,
planTitle: data.title, planTitle: data.title,
planPhases: data.phases,
planSteps: data.steps, planSteps: data.steps,
planSummary: data.summary planSummary: data.summary
}); });
@ -532,7 +561,108 @@ export class DialogSession {
} }
// 调用回调通知 UI // 调用回调通知 UI
callbacks.onPlanConfirm?.(data.confirmId, data.title, data.steps, data.summary); callbacks.onPlanConfirm?.(data.confirmId, data.title, data.phases, data.steps, data.summary);
},
onPhaseProgress: (data: import('../types/api').PhaseProgressEvent) => {
console.log('[DialogSession] onPhaseProgress:', data.phaseId, data.status);
// 1. 尝试更新 plan segment兼容旧逻辑
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId) {
return { ...phase, status: data.status };
}
return phase;
});
callbacks.onSegmentUpdate?.(this.segments);
break;
}
}
// 2. 通知外部更新独立进度条
callbacks.onPhaseProgress?.(data.phaseId, data.status);
},
onPlanStepAdd: (data: import('../types/api').PlanStepAddEvent) => {
console.log('[DialogSession] onPlanStepAdd:', data.phaseId, data.step);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId) {
const newSteps = [...(phase.steps || [])];
if (data.index >= 0 && data.index < newSteps.length) {
newSteps.splice(data.index, 0, data.step);
} else {
newSteps.push(data.step);
}
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanStepRemove: (data: import('../types/api').PlanStepRemoveEvent) => {
console.log('[DialogSession] onPlanStepRemove:', data.phaseId, data.stepIndex);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId && phase.steps) {
const newSteps = [...phase.steps];
newSteps.splice(data.stepIndex, 1);
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanStepUpdate: (data: import('../types/api').PlanStepUpdateEvent) => {
console.log('[DialogSession] onPlanStepUpdate:', data.phaseId, data.stepIndex);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId && phase.steps) {
const newSteps = [...phase.steps];
if (data.stepIndex >= 0 && data.stepIndex < newSteps.length) {
newSteps[data.stepIndex] = data.step;
}
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanSummaryUpdate: (data: import('../types/api').PlanSummaryUpdateEvent) => {
console.log('[DialogSession] onPlanSummaryUpdate');
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan') {
seg.planSummary = data.summary;
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
}, },
onAskUser: async (data: AskUserEvent) => { onAskUser: async (data: AskUserEvent) => {
@ -654,6 +784,23 @@ export class DialogSession {
callbacks.onContextUsage?.(data); callbacks.onContextUsage?.(data);
}, },
onCreditUpdate: (data) => {
console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits);
// 资源点余额低于阈值时弹窗提醒
const LOW_CREDIT_THRESHOLD = 5;
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
vscode.window.showWarningMessage(
`资源点余额不足!当前剩余 ${data.remainingCredits.toFixed(2)} 点,请及时充值。`,
'去充值'
).then(selection => {
if (selection === '去充值') {
// 打开充值页面
vscode.env.openExternal(vscode.Uri.parse('https://iccoder.com/recharge'));
}
});
}
},
onOpen: () => { onOpen: () => {
console.log('[DialogSession] SSE 连接已建立'); console.log('[DialogSession] SSE 连接已建立');
}, },

View File

@ -2,6 +2,7 @@ import * as vscode from "vscode";
import * as http from "http"; import * as http from "http";
import * as path from "path"; import * as path from "path";
import * as fs from "fs"; import * as fs from "fs";
import { getConfig } from "../config/settings";
/** /**
* IC Coder Authentication Provider * IC Coder Authentication Provider
@ -12,7 +13,6 @@ export class ICCoderAuthenticationProvider
{ {
private static readonly AUTH_TYPE = "iccoder"; private static readonly AUTH_TYPE = "iccoder";
private static readonly AUTH_NAME = "IC Coder"; private static readonly AUTH_NAME = "IC Coder";
private static readonly LOGIN_URL = "http://192.168.1.108:2005/login";
private static loginServer: http.Server | null = null; private static loginServer: http.Server | null = null;
private static currentPort: number | null = null; private static currentPort: number | null = null;
@ -149,9 +149,8 @@ export class ICCoderAuthenticationProvider
// 构建登录 URL // 构建登录 URL
const callbackUrl = `http://localhost:${port}/callback`; const callbackUrl = `http://localhost:${port}/callback`;
const loginUrl = `${ const config = getConfig();
ICCoderAuthenticationProvider.LOGIN_URL const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
console.log("🔐 登录服务器已启动,监听端口:", port); console.log("🔐 登录服务器已启动,监听端口:", port);
console.log("🌐 登录 URL:", loginUrl); console.log("🌐 登录 URL:", loginUrl);

View File

@ -28,7 +28,8 @@ import type {
AgentProgressEvent, AgentProgressEvent,
AgentCompleteEvent, AgentCompleteEvent,
AgentErrorEvent, AgentErrorEvent,
ContextUsageEvent ContextUsageEvent,
CreditUpdateEvent
} from '../types/api'; } from '../types/api';
import type { MemoryCompactedEvent } from '../types/memory'; import type { MemoryCompactedEvent } from '../types/memory';
@ -44,6 +45,16 @@ export interface SSECallbacks {
onToolConfirm?: (data: ToolConfirmEvent) => void; onToolConfirm?: (data: ToolConfirmEvent) => void;
/** 收到计划确认请求Plan 模式) */ /** 收到计划确认请求Plan 模式) */
onPlanConfirm?: (data: PlanConfirmEvent) => void; onPlanConfirm?: (data: PlanConfirmEvent) => void;
/** 阶段进度更新 */
onPhaseProgress?: (data: import('../types/api').PhaseProgressEvent) => void;
/** 添加计划步骤 */
onPlanStepAdd?: (data: import('../types/api').PlanStepAddEvent) => void;
/** 删除计划步骤 */
onPlanStepRemove?: (data: import('../types/api').PlanStepRemoveEvent) => void;
/** 更新计划步骤 */
onPlanStepUpdate?: (data: import('../types/api').PlanStepUpdateEvent) => void;
/** 更新计划摘要 */
onPlanSummaryUpdate?: (data: import('../types/api').PlanSummaryUpdateEvent) => void;
/** 工具开始执行 */ /** 工具开始执行 */
onToolStart?: (data: ToolStartEvent) => void; onToolStart?: (data: ToolStartEvent) => void;
/** 工具执行完成 */ /** 工具执行完成 */
@ -74,6 +85,8 @@ export interface SSECallbacks {
onMemoryCompacted?: (data: MemoryCompactedEvent) => void; onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
/** 上下文使用量更新 */ /** 上下文使用量更新 */
onContextUsage?: (data: ContextUsageEvent) => void; onContextUsage?: (data: ContextUsageEvent) => void;
/** 资源点余额更新 */
onCreditUpdate?: (data: CreditUpdateEvent) => void;
/** 连接打开 */ /** 连接打开 */
onOpen?: () => void; onOpen?: () => void;
/** 连接关闭 */ /** 连接关闭 */
@ -286,6 +299,21 @@ function dispatchEvent(
case 'plan_confirm': case 'plan_confirm':
callbacks.onPlanConfirm?.(data as PlanConfirmEvent); callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
break; break;
case 'phase_progress':
callbacks.onPhaseProgress?.(data as import('../types/api').PhaseProgressEvent);
break;
case 'plan_step_add':
callbacks.onPlanStepAdd?.(data as import('../types/api').PlanStepAddEvent);
break;
case 'plan_step_remove':
callbacks.onPlanStepRemove?.(data as import('../types/api').PlanStepRemoveEvent);
break;
case 'plan_step_update':
callbacks.onPlanStepUpdate?.(data as import('../types/api').PlanStepUpdateEvent);
break;
case 'plan_summary_update':
callbacks.onPlanSummaryUpdate?.(data as import('../types/api').PlanSummaryUpdateEvent);
break;
case 'tool_start': case 'tool_start':
callbacks.onToolStart?.(data as ToolStartEvent); callbacks.onToolStart?.(data as ToolStartEvent);
break; break;
@ -331,6 +359,9 @@ function dispatchEvent(
case 'context_usage': case 'context_usage':
callbacks.onContextUsage?.(data as ContextUsageEvent); callbacks.onContextUsage?.(data as ContextUsageEvent);
break; break;
case 'credit_update':
callbacks.onCreditUpdate?.(data as CreditUpdateEvent);
break;
case 'heartbeat': case 'heartbeat':
// 心跳事件:仅用于保持连接,不需要特殊处理 // 心跳事件:仅用于保持连接,不需要特殊处理
// Node.js req.setTimeout 会在收到数据时自动重置计时器 // Node.js req.setTimeout 会在收到数据时自动重置计时器

View File

@ -56,6 +56,11 @@ export type SSEEventType =
| "tool_call" // 客户端工具调用请求 | "tool_call" // 客户端工具调用请求
| "tool_confirm" // 工具确认请求Ask 模式) | "tool_confirm" // 工具确认请求Ask 模式)
| "plan_confirm" // 计划确认请求Plan 模式) | "plan_confirm" // 计划确认请求Plan 模式)
| "phase_progress" // 阶段进度更新
| "plan_step_add" // 添加计划步骤
| "plan_step_remove" // 删除计划步骤
| "plan_step_update" // 更新计划步骤
| "plan_summary_update" // 更新计划摘要
| "tool_start" // 工具开始执行 | "tool_start" // 工具开始执行
| "tool_complete" // 工具执行完成 | "tool_complete" // 工具执行完成
| "tool_error" // 工具执行错误 | "tool_error" // 工具执行错误
@ -66,6 +71,7 @@ export type SSEEventType =
| "agent_error" // 子智能体错误 | "agent_error" // 子智能体错误
| "memory_compacted" // 记忆压缩完成 | "memory_compacted" // 记忆压缩完成
| "context_usage" // 上下文使用量 | "context_usage" // 上下文使用量
| "credit_update" // 资源点余额更新
| "complete" // 对话完成 | "complete" // 对话完成
| "error" // 错误 | "error" // 错误
| "warning" // 警告 | "warning" // 警告
@ -108,20 +114,83 @@ export interface ToolConfirmEvent {
timestamp: number; timestamp: number;
} }
/** 计划步骤 */
export interface PlanStep {
/** 步骤名称 */
name: string;
/** 步骤描述 */
description?: string;
}
/** 计划阶段 */
export interface PlanPhase {
/** 阶段ID: spec/design/sim/done */
id: string;
/** 阶段名称 */
name: string;
/** 阶段状态: skipped/completed/current/pending */
status: string;
/** 跳过原因 */
reason?: string;
/** 阶段内的步骤 */
steps: PlanStep[];
}
/** plan_confirm 事件数据Plan 模式计划确认) */ /** plan_confirm 事件数据Plan 模式计划确认) */
export interface PlanConfirmEvent { export interface PlanConfirmEvent {
/** 确认ID */ /** 确认ID */
confirmId: number; confirmId: number;
/** 计划标题 */ /** 计划标题 */
title: string; title: string;
/** 执行步骤列表 */ /** 四阶段计划列表(新格式) */
steps: string[]; phases?: PlanPhase[];
/** 执行步骤列表(旧格式,兼容) */
steps?: string[];
/** 计划摘要 */ /** 计划摘要 */
summary: string; summary: string;
/** 时间戳 */ /** 时间戳 */
timestamp: number; timestamp: number;
} }
/** phase_progress 事件数据(阶段进度更新) */
export interface PhaseProgressEvent {
/** 阶段ID: spec/design/sim/done */
phaseId: string;
/** 状态: current/completed */
status: string;
/** 时间戳 */
timestamp: number;
}
/** plan_step_add 事件数据(添加计划步骤) */
export interface PlanStepAddEvent {
phaseId: string;
step: PlanStep;
index: number;
timestamp: number;
}
/** plan_step_remove 事件数据(删除计划步骤) */
export interface PlanStepRemoveEvent {
phaseId: string;
stepIndex: number;
timestamp: number;
}
/** plan_step_update 事件数据(更新计划步骤) */
export interface PlanStepUpdateEvent {
phaseId: string;
stepIndex: number;
step: PlanStep;
timestamp: number;
}
/** plan_summary_update 事件数据(更新计划摘要) */
export interface PlanSummaryUpdateEvent {
summary: string;
timestamp: number;
}
/** ask_user 事件数据 */ /** ask_user 事件数据 */
export interface AskUserEvent { export interface AskUserEvent {
askId: string; askId: string;
@ -201,6 +270,12 @@ export interface ContextUsageEvent {
percentage: number; percentage: number;
} }
/** credit_update 事件数据 */
export interface CreditUpdateEvent {
deductedCredits: number;
remainingCredits: number;
}
// ============== 工具调用协议 (MCP 格式) ============== // ============== 工具调用协议 (MCP 格式) ==============
/** /**

73
src/utils/jwtUtils.ts Normal file
View File

@ -0,0 +1,73 @@
/**
* JWT 工具函数
*/
/**
* JWT Payload 接口
*/
export interface JwtPayload {
sub?: string; // subject (通常是 userId)
userId?: number; // 用户ID (驼峰命名)
user_id?: number; // 用户ID (下划线命名)
exp?: number; // 过期时间
iat?: number; // 签发时间
[key: string]: unknown;
}
/**
* 解析 JWT token 的 payload
* @param token JWT token
* @returns 解析后的 payload解析失败返回 null
*/
export function parseJwtPayload(token: string): JwtPayload | null {
try {
const parts = token.split('.');
if (parts.length !== 3) {
console.warn('[JWT] token 格式不正确期望3部分实际:', parts.length);
return null;
}
// payload 是第二部分base64url 编码
const payload = parts[1];
// base64url 转 base64
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
// 解码
const jsonStr = Buffer.from(base64, 'base64').toString('utf-8');
const parsed = JSON.parse(jsonStr);
console.log('[JWT] 解析成功, payload 字段:', Object.keys(parsed));
console.log('[JWT] payload 内容:', JSON.stringify(parsed));
return parsed;
} catch (error) {
console.error('[JWT] 解析失败:', error);
return null;
}
}
/**
* 从 JWT token 中获取用户ID
* @param token JWT token
* @returns 用户ID字符串获取失败返回 null
*/
export function getUserIdFromToken(token: string): string | null {
const payload = parseJwtPayload(token);
if (!payload) {
return null;
}
// 支持多种字段名user_id, userId, sub
if (payload.user_id !== undefined) {
return String(payload.user_id);
}
if (payload.userId !== undefined) {
return String(payload.userId);
}
if (payload.sub !== undefined) {
return String(payload.sub);
}
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
return null;
}

View File

@ -288,6 +288,36 @@ async function handleUserMessageWithBackend(
percentage: data.percentage, percentage: data.percentage,
}); });
}, },
onPhaseProgress: (phaseId, status) => {
// 发送阶段进度更新到 WebView
// 映射 phaseId: sim -> simulation
const stepMap: Record<string, string> = {
spec: "spec",
design: "design",
sim: "simulation",
done: "done",
};
const step = stepMap[phaseId] || phaseId;
if (status === "current") {
// 显示进度条并更新到当前步骤
panel.webview.postMessage({ type: "showProgress" });
panel.webview.postMessage({ type: "updateProgress", step });
} else if (status === "completed") {
// 更新到下一步(或完成)
const steps = ["spec", "design", "simulation", "done"];
const currentIndex = steps.indexOf(step);
if (currentIndex < steps.length - 1) {
panel.webview.postMessage({
type: "updateProgress",
step: steps[currentIndex + 1],
});
} else {
panel.webview.postMessage({ type: "completeProgress" });
}
}
},
}, },
mode, mode,
serviceTier // 传递服务等级 serviceTier // 传递服务等级

View File

@ -4,6 +4,7 @@
* 功能说明: * 功能说明:
* - 显示执行计划的卡片界面 * - 显示执行计划的卡片界面
* - 包含计划标题、摘要和步骤列表 * - 包含计划标题、摘要和步骤列表
* - 摘要支持 Markdown 格式渲染
* - 提供确认执行、修改计划、取消等操作按钮 * - 提供确认执行、修改计划、取消等操作按钮
*/ */
@ -43,11 +44,62 @@ export function getPlanCardStyles(): string {
padding: 16px; padding: 16px;
} }
.plan-summary { .plan-summary {
color: var(--vscode-descriptionForeground); color: var(--vscode-foreground);
margin-bottom: 12px; margin-bottom: 12px;
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.6;
} }
/* Markdown 渲染样式 */
.plan-summary h1, .plan-summary h2, .plan-summary h3, .plan-summary h4 {
margin: 16px 0 8px 0;
font-weight: 600;
color: var(--vscode-foreground);
}
.plan-summary h1 { font-size: 18px; border-bottom: 1px solid var(--vscode-input-border); padding-bottom: 6px; }
.plan-summary h2 { font-size: 16px; }
.plan-summary h3 { font-size: 14px; }
.plan-summary h4 { font-size: 13px; }
.plan-summary p { margin: 8px 0; }
.plan-summary ul, .plan-summary ol {
margin: 8px 0;
padding-left: 24px;
}
.plan-summary li { margin: 4px 0; }
.plan-summary code {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
font-family: var(--vscode-editor-font-family);
font-size: 12px;
}
.plan-summary pre {
background: var(--vscode-textCodeBlock-background);
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
.plan-summary pre code {
background: none;
padding: 0;
}
.plan-summary table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
font-size: 12px;
}
.plan-summary th, .plan-summary td {
border: 1px solid var(--vscode-input-border);
padding: 6px 10px;
text-align: left;
}
.plan-summary th {
background: var(--vscode-sideBar-background);
font-weight: 600;
}
.plan-summary strong { font-weight: 600; }
.plan-summary em { font-style: italic; }
.plan-steps { .plan-steps {
font-size: 13px; font-size: 13px;
} }
@ -58,6 +110,15 @@ export function getPlanCardStyles(): string {
border-radius: 4px; border-radius: 4px;
line-height: 1.5; line-height: 1.5;
} }
.plan-step strong {
color: var(--vscode-textLink-foreground);
}
.step-details {
margin-top: 4px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
}
.plan-step:last-child { .plan-step:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -150,6 +211,168 @@ export function getPlanCardStyles(): string {
.plan-actions .custom-submit:hover { .plan-actions .custom-submit:hover {
background: var(--vscode-button-hoverBackground); background: var(--vscode-button-hoverBackground);
} }
/* 阶段进度条样式 */
.phase-progress {
display: flex;
align-items: center;
padding: 12px 16px;
background: var(--vscode-sideBar-background);
border-bottom: 1px solid var(--vscode-input-border);
}
.phase-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.phase-item.current {
color: var(--vscode-textLink-foreground);
font-weight: 600;
}
.phase-item.completed {
color: #4caf50;
}
.phase-item.skipped {
color: var(--vscode-descriptionForeground);
opacity: 0.6;
}
.phase-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vscode-input-border);
flex-shrink: 0;
}
.phase-dot.current {
background: var(--vscode-textLink-foreground);
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.2);
}
.phase-dot.completed {
background: #4caf50;
}
.phase-dot.skipped {
background: var(--vscode-descriptionForeground);
opacity: 0.5;
}
.phase-line {
flex: 1;
height: 2px;
background: var(--vscode-input-border);
margin: 0 8px;
}
.phase-line.completed {
background: #4caf50;
}
/* 阶段列表样式 */
.plan-phases {
font-size: 13px;
}
.plan-phase {
margin-bottom: 12px;
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
overflow: hidden;
}
.plan-phase:last-child {
margin-bottom: 0;
}
.phase-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--vscode-list-hoverBackground);
cursor: pointer;
user-select: none;
}
.phase-header:hover {
background: var(--vscode-list-activeSelectionBackground);
}
.phase-toggle {
font-size: 10px;
color: var(--vscode-descriptionForeground);
transition: transform 0.2s;
}
.phase-toggle.expanded {
transform: rotate(90deg);
}
.phase-name {
flex: 1;
font-weight: 500;
}
.phase-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
}
.phase-status.current {
background: var(--vscode-textLink-foreground);
color: white;
}
.phase-status.skipped {
background: var(--vscode-descriptionForeground);
opacity: 0.6;
}
.phase-status.completed {
background: #4caf50;
color: white;
}
.phase-content {
padding: 0 12px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.phase-content.expanded {
padding: 12px;
max-height: 500px;
}
.phase-reason {
font-size: 12px;
color: var(--vscode-descriptionForeground);
font-style: italic;
margin-bottom: 8px;
}
.phase-steps {
margin: 0;
padding: 0;
list-style: none;
}
.phase-step-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid var(--vscode-input-border);
}
.phase-step-item:last-child {
border-bottom: none;
}
.phase-step-checkbox {
width: 14px;
height: 14px;
border: 2px solid var(--vscode-textLink-foreground);
border-radius: 3px;
flex-shrink: 0;
margin-top: 2px;
}
.phase-step-text {
flex: 1;
}
.phase-step-name {
font-weight: 500;
color: var(--vscode-foreground);
}
.phase-step-desc {
font-size: 12px;
color: var(--vscode-descriptionForeground);
margin-top: 2px;
}
`; `;
} }
@ -158,6 +381,200 @@ export function getPlanCardStyles(): string {
*/ */
export function getPlanCardScript(): string { export function getPlanCardScript(): string {
return ` return `
// 简单的 Markdown 渲染函数
function renderPlanMarkdown(text) {
if (!text) return '';
let html = text;
// 转义 HTML 特殊字符(保留换行)
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 代码块 (\`\`\`code\`\`\`)
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
// 行内代码 (\`code\`)
html = html.replace(/\\x60([^\\x60]+)\\x60/g, '<code>$1</code>');
// 表格处理
html = html.replace(/^\\|(.+)\\|\\s*\\n\\|[-:\\s|]+\\|\\s*\\n((?:\\|.+\\|\\s*\\n?)+)/gm, function(match, header, body) {
const headers = header.split('|').map(h => h.trim()).filter(h => h);
const rows = body.trim().split('\\n').map(row =>
row.split('|').map(cell => cell.trim()).filter(cell => cell)
);
let table = '<table><thead><tr>';
headers.forEach(h => table += '<th>' + h + '</th>');
table += '</tr></thead><tbody>';
rows.forEach(row => {
table += '<tr>';
row.forEach(cell => table += '<td>' + cell + '</td>');
table += '</tr>';
});
table += '</tbody></table>';
return table;
});
// 标题
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// 粗体和斜体
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
// 无序列表
html = html.replace(/^[\\s]*[-*] (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
// 有序列表
html = html.replace(/^[\\s]*\\d+\\. (.+)$/gm, '<li>$1</li>');
// 段落(连续的非空行)
html = html.replace(/^(?!<[hupolt]|$)(.+)$/gm, '<p>$1</p>');
// 清理多余的空行
html = html.replace(/<p><\\/p>/g, '');
html = html.replace(/\\n{2,}/g, '\\n');
return html;
}
// 解析并渲染步骤列表
function renderPlanSteps(steps) {
if (!steps || steps.length === 0) return '';
// 尝试解析 JSON 格式的步骤
let parsedSteps = steps;
// 如果是单个字符串且看起来像 JSON 数组,尝试解析
if (steps.length === 1 && typeof steps[0] === 'string') {
const str = steps[0].trim();
if (str.startsWith('[') && str.endsWith(']')) {
try {
parsedSteps = JSON.parse(str);
} catch (e) {
// 解析失败,保持原样
}
}
}
return parsedSteps.map((step, i) => {
// 如果是对象,格式化显示
if (typeof step === 'object' && step !== null) {
const name = step.name || step.id || ('步骤 ' + (i + 1));
const desc = step.description || '';
const inputs = step.inputs || '';
const outputs = step.outputs || '';
const logic = step.logic || '';
let content = '<strong>' + name + '</strong>';
if (desc) content += '' + desc;
let details = [];
if (inputs) details.push('输入: ' + inputs);
if (outputs) details.push('输出: ' + outputs);
if (logic) details.push('逻辑: ' + logic);
if (details.length > 0) {
content += '<div class="step-details">' + details.join(' | ') + '</div>';
}
return '<div class="plan-step"><span class="step-checkbox"></span>' + content + '</div>';
}
// 普通字符串
return '<div class="plan-step"><span class="step-checkbox"></span> ' + step + '</div>';
}).join('');
}
// 渲染阶段进度条
function renderPhaseProgress(phases) {
if (!phases || phases.length === 0) return '';
const phaseNames = { spec: 'Spec', design: 'Design', sim: 'Sim', done: 'Done' };
let html = '<div class="phase-progress">';
phases.forEach((phase, i) => {
const name = phaseNames[phase.id] || phase.name || phase.id;
const status = phase.status || 'pending';
html += \`<div class="phase-item \${status}">
<span class="phase-dot \${status}"></span>
<span>\${name}</span>
</div>\`;
// 添加连接线(最后一个不加)
if (i < phases.length - 1) {
const lineStatus = (status === 'completed' || status === 'skipped') ? 'completed' : '';
html += \`<div class="phase-line \${lineStatus}"></div>\`;
}
});
html += '</div>';
return html;
}
// 渲染阶段列表(两级结构)
function renderPlanPhases(phases) {
if (!phases || phases.length === 0) return '';
const statusLabels = {
skipped: '跳过',
completed: '已完成',
current: '当前',
pending: '待执行'
};
return phases.map((phase, i) => {
const status = phase.status || 'pending';
const statusLabel = statusLabels[status] || status;
const isExpanded = status === 'current';
const hasSteps = phase.steps && phase.steps.length > 0;
const hasReason = phase.reason && status === 'skipped';
let stepsHtml = '';
if (phase.steps && phase.steps.length > 0) {
stepsHtml = phase.steps.map(step => \`
<li class="phase-step-item">
<span class="phase-step-checkbox"></span>
<div class="phase-step-text">
<div class="phase-step-name">\${step.name || ''}</div>
\${step.description ? \`<div class="phase-step-desc">\${step.description}</div>\` : ''}
</div>
</li>
\`).join('');
}
return \`
<div class="plan-phase" data-phase-id="\${phase.id}">
<div class="phase-header" onclick="togglePhase(this)">
<span class="phase-toggle \${isExpanded ? 'expanded' : ''}">▶</span>
<span class="phase-name">\${phase.name || phase.id}</span>
<span class="phase-status \${status}">\${statusLabel}</span>
</div>
<div class="phase-content \${isExpanded ? 'expanded' : ''}">
\${hasReason ? \`<div class="phase-reason">\${phase.reason}</div>\` : ''}
\${hasSteps ? \`<ul class="phase-steps">\${stepsHtml}</ul>\` : ''}
\${!hasSteps && !hasReason ? '<div class="phase-reason">暂无步骤</div>' : ''}
</div>
</div>
\`;
}).join('');
}
// 切换阶段展开/折叠
function togglePhase(header) {
const toggle = header.querySelector('.phase-toggle');
const content = header.nextElementSibling;
toggle.classList.toggle('expanded');
content.classList.toggle('expanded');
}
// 渲染计划卡片(在 updateSegmentsRealtime 中使用) // 渲染计划卡片(在 updateSegmentsRealtime 中使用)
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) { function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
segmentDiv.className += ' segment-plan'; segmentDiv.className += ' segment-plan';
@ -170,9 +587,15 @@ export function getPlanCardScript(): string {
segmentDiv.classList.add('answered'); segmentDiv.classList.add('answered');
} }
const stepsHtml = (segment.planSteps || []).map((step, i) => // 判断是否有 phases新格式还是 steps旧格式
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\` const hasPhases = segment.planPhases && segment.planPhases.length > 0;
).join('');
// 渲染阶段进度条和阶段列表(新格式)
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
// 兼容旧格式:渲染步骤列表
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
// 选项按钮 // 选项按钮
const options = ['确认执行', '修改计划', '取消']; const options = ['确认执行', '修改计划', '取消'];
@ -181,15 +604,19 @@ export function getPlanCardScript(): string {
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`; return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join(''); }).join('');
// 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="plan-card"> <div class="plan-card">
<div class="plan-header"> <div class="plan-header">
<span class="plan-icon">${plannerIconSvg}</span> <span class="plan-icon">${plannerIconSvg}</span>
<span class="plan-title">\${segment.planTitle || '执行计划'}</span> <span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div> </div>
\${progressHtml}
<div class="plan-body"> <div class="plan-body">
<div class="plan-summary">\${segment.planSummary || ''}</div> <div class="plan-summary">\${summaryHtml}</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">
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div> <div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
@ -250,9 +677,19 @@ export function getPlanCardScript(): string {
// 渲染计划卡片(在 renderSegments 中使用) // 渲染计划卡片(在 renderSegments 中使用)
function renderPlanCardStatic(segment, segmentDiv) { function renderPlanCardStatic(segment, segmentDiv) {
segmentDiv.className += ' segment-plan'; segmentDiv.className += ' segment-plan';
const stepsHtml = (segment.planSteps || []).map((step, i) =>
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\` // 判断是否有 phases新格式还是 steps旧格式
).join(''); const hasPhases = segment.planPhases && segment.planPhases.length > 0;
// 渲染阶段进度条和阶段列表(新格式)
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
// 兼容旧格式:渲染步骤列表
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
// 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="plan-card"> <div class="plan-card">
@ -260,9 +697,10 @@ export function getPlanCardScript(): string {
<span class="plan-icon">📋</span> <span class="plan-icon">📋</span>
<span class="plan-title">\${segment.planTitle || '执行计划'}</span> <span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div> </div>
\${progressHtml}
<div class="plan-body"> <div class="plan-body">
<div class="plan-summary">\${segment.planSummary || ''}</div> <div class="plan-summary">\${summaryHtml}</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">
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button> <button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>