Compare commits
6 Commits
merge/merg
...
9786b7141c
| Author | SHA1 | Date | |
|---|---|---|---|
| 9786b7141c | |||
| 4a7af49fea | |||
| 15a1de3a90 | |||
| 5c19be22d3 | |||
| 5546791549 | |||
| 178f3a7498 |
@ -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;
|
||||||
/** 后端服务地址(strangeLoop) */
|
/** 后端服务地址(strangeLoop) */
|
||||||
backendUrlStrongeLoop: string;
|
backendUrlStrongeLoop: string;
|
||||||
/** 请求超时时间(毫秒) */
|
/** 请求超时时间(毫秒) */
|
||||||
@ -33,6 +35,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
dev: {
|
dev: {
|
||||||
backendUrl: "http://localhost:2233",
|
backendUrl: "http://localhost:2233",
|
||||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||||
|
loginUrl: "http://localhost/login",
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max", // 默认使用 max
|
serviceTier: "max", // 默认使用 max
|
||||||
@ -41,6 +44,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
test: {
|
test: {
|
||||||
backendUrl: "http://192.168.1.108:2233",
|
backendUrl: "http://192.168.1.108:2233",
|
||||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||||
|
loginUrl: "http://192.168.1.108:2005/login",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max",
|
serviceTier: "max",
|
||||||
@ -49,6 +53,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
prod: {
|
prod: {
|
||||||
backendUrl: "https://api.iccoder.com",
|
backendUrl: "https://api.iccoder.com",
|
||||||
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
|
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
|
||||||
|
loginUrl: "https://iccoder.com/login",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "auto",
|
serviceTier: "auto",
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,6 +92,7 @@ export class DialogSession {
|
|||||||
private toolContext: ToolExecutorContext;
|
private toolContext: ToolExecutorContext;
|
||||||
private accumulatedText = '';
|
private accumulatedText = '';
|
||||||
private isActive = false;
|
private isActive = false;
|
||||||
|
private hasCompleted = false; // 标记是否已收到 complete 事件
|
||||||
private segments: MessageSegment[] = [];
|
private segments: MessageSegment[] = [];
|
||||||
private currentTextSegment: MessageSegment | null = null;
|
private currentTextSegment: MessageSegment | null = null;
|
||||||
|
|
||||||
@ -325,12 +332,34 @@ export class DialogSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.isActive = true;
|
this.isActive = true;
|
||||||
|
this.hasCompleted = false; // 重置完成标志
|
||||||
this.accumulatedText = '';
|
this.accumulatedText = '';
|
||||||
this.segments = [];
|
this.segments = [];
|
||||||
this.currentTextSegment = null;
|
this.currentTextSegment = null;
|
||||||
|
|
||||||
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 +372,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 +537,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 +563,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) => {
|
||||||
@ -556,6 +688,7 @@ export class DialogSession {
|
|||||||
|
|
||||||
onComplete: (data) => {
|
onComplete: (data) => {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
|
this.hasCompleted = true; // 标记已收到 complete 事件
|
||||||
this.finalizeTextSegment();
|
this.finalizeTextSegment();
|
||||||
|
|
||||||
// 追踪 AI 消息(用于后端重启后恢复)
|
// 追踪 AI 消息(用于后端重启后恢复)
|
||||||
@ -654,12 +787,38 @@ 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 连接已建立');
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
console.log('[DialogSession] SSE 连接已关闭');
|
console.log('[DialogSession] SSE 连接已关闭');
|
||||||
|
// 如果没有收到 complete 事件,需要补充完成逻辑
|
||||||
|
if (!this.hasCompleted && this.isActive) {
|
||||||
|
console.log('[DialogSession] 未收到 complete 事件,补充完成处理');
|
||||||
|
this.finalizeTextSegment();
|
||||||
|
if (this.accumulatedText) {
|
||||||
|
historyManager.trackAiMessage(this.accumulatedText);
|
||||||
|
}
|
||||||
|
callbacks.onComplete?.(this.segments);
|
||||||
|
}
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ 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 { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||||
|
import { getConfig } from "../config/settings";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IC Coder Authentication Provider
|
* IC Coder Authentication Provider
|
||||||
@ -13,7 +14,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;
|
||||||
|
|
||||||
@ -156,9 +156,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);
|
||||||
|
|||||||
@ -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 会在收到数据时自动重置计时器
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import * as os from 'os';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
import { createOrOverwriteFile } from '../utils/createFiles';
|
||||||
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
|
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
||||||
import { analyzeVcdFile } from '../utils/vcdParser';
|
import { analyzeVcdFile } from '../utils/vcdParser';
|
||||||
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
||||||
import {
|
import {
|
||||||
@ -285,7 +285,30 @@ async function executeSimulation(
|
|||||||
|
|
||||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||||
|
|
||||||
// 调用现有的 generateVCD 函数
|
// 检查是否有 dumpModules 参数(多 VCD 模式)
|
||||||
|
if (args.dumpModules) {
|
||||||
|
const modules = parseDumpModules(args.dumpModules);
|
||||||
|
const vcdDir = args.vcdDir || 'vcd';
|
||||||
|
|
||||||
|
const result = await generateMultiVCD(
|
||||||
|
projectPath,
|
||||||
|
context.extensionPath,
|
||||||
|
args.tbPath,
|
||||||
|
modules,
|
||||||
|
vcdDir
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const vcdList = result.vcdFiles
|
||||||
|
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`)
|
||||||
|
.join('\n');
|
||||||
|
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原有单 VCD 逻辑
|
||||||
const result = await generateVCD(projectPath, context.extensionPath);
|
const result = await generateVCD(projectPath, context.extensionPath);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@ -303,6 +326,17 @@ async function executeSimulation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 dumpModules 参数
|
||||||
|
* 格式:name:path,name:path
|
||||||
|
*/
|
||||||
|
function parseDumpModules(dumpModules: string): DumpModule[] {
|
||||||
|
return dumpModules.split(',').map(item => {
|
||||||
|
const [name, modulePath] = item.trim().split(':');
|
||||||
|
return { name: name.trim(), path: modulePath.trim() };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 waveform_summary 工具
|
* 执行 waveform_summary 工具
|
||||||
* 解析 VCD 文件并返回波形摘要
|
* 解析 VCD 文件并返回波形摘要
|
||||||
|
|||||||
@ -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 格式) ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -448,6 +523,10 @@ export interface SimulationArgs {
|
|||||||
rtlPath: string;
|
rtlPath: string;
|
||||||
tbPath: string;
|
tbPath: string;
|
||||||
duration?: string;
|
duration?: string;
|
||||||
|
/** 要dump的模块列表,格式:name:path,name:path */
|
||||||
|
dumpModules?: string;
|
||||||
|
/** VCD输出目录,默认'vcd' */
|
||||||
|
vcdDir?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** waveform_summary 工具参数 */
|
/** waveform_summary 工具参数 */
|
||||||
|
|||||||
@ -413,3 +413,193 @@ export async function checkIverilogAvailable(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 要 dump 的模块定义
|
||||||
|
*/
|
||||||
|
export interface DumpModule {
|
||||||
|
name: string; // 模块名(用于 VCD 文件名和宏名)
|
||||||
|
path: string; // 实例路径(如 dut.u_tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多 VCD 生成结果
|
||||||
|
*/
|
||||||
|
export interface MultiVCDResult {
|
||||||
|
success: boolean;
|
||||||
|
vcdFiles: Array<{
|
||||||
|
moduleName: string;
|
||||||
|
vcdPath: string;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
message: string;
|
||||||
|
stdout?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 testbench 中注入条件编译代码
|
||||||
|
* 将原有的 $dumpfile/$dumpvars 替换为条件编译版本
|
||||||
|
*/
|
||||||
|
function injectConditionalDump(
|
||||||
|
tbContent: string,
|
||||||
|
dumpModules: DumpModule[],
|
||||||
|
vcdDir: string
|
||||||
|
): string {
|
||||||
|
// 匹配 $dumpfile 和 $dumpvars 语句(可能跨多行)
|
||||||
|
const dumpPattern = /(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
|
||||||
|
|
||||||
|
// 生成条件编译代码
|
||||||
|
const conditionalCode = generateConditionalDumpCode(dumpModules, vcdDir);
|
||||||
|
|
||||||
|
// 替换原有的 dump 语句
|
||||||
|
const modified = tbContent.replace(dumpPattern, conditionalCode);
|
||||||
|
|
||||||
|
// 如果没有找到匹配,尝试单独匹配 $dumpfile
|
||||||
|
if (modified === tbContent) {
|
||||||
|
const singleDumpPattern = /\$dumpfile\s*\([^)]+\)\s*;/g;
|
||||||
|
return tbContent.replace(singleDumpPattern, conditionalCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成条件编译的 dump 代码
|
||||||
|
*/
|
||||||
|
function generateConditionalDumpCode(
|
||||||
|
dumpModules: DumpModule[],
|
||||||
|
vcdDir: string
|
||||||
|
): string {
|
||||||
|
if (dumpModules.length === 0) {
|
||||||
|
return '$dumpfile("output.vcd");\n $dumpvars(0, dut);';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
dumpModules.forEach((module, index) => {
|
||||||
|
const macroName = `DUMP_${module.name.toUpperCase()}`;
|
||||||
|
const vcdPath = `${vcdDir}/${module.name}.vcd`;
|
||||||
|
const directive = index === 0 ? '`ifdef' : '`elsif';
|
||||||
|
|
||||||
|
lines.push(`${directive} ${macroName}`);
|
||||||
|
lines.push(` $dumpfile("${vcdPath}");`);
|
||||||
|
lines.push(` $dumpvars(1, ${module.path});`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加默认分支(使用第一个模块)
|
||||||
|
lines.push('`else');
|
||||||
|
lines.push(` $dumpfile("${vcdDir}/${dumpModules[0].name}.vcd");`);
|
||||||
|
lines.push(` $dumpvars(1, ${dumpModules[0].path});`);
|
||||||
|
lines.push('`endif');
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成多个 VCD 文件(为不同子模块)
|
||||||
|
*/
|
||||||
|
export async function generateMultiVCD(
|
||||||
|
projectPath: string,
|
||||||
|
extensionPath: string,
|
||||||
|
tbPath: string,
|
||||||
|
dumpModules: DumpModule[],
|
||||||
|
vcdDir: string = 'vcd'
|
||||||
|
): Promise<MultiVCDResult> {
|
||||||
|
const results: MultiVCDResult['vcdFiles'] = [];
|
||||||
|
let allStdout = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 创建 vcd 目录
|
||||||
|
const vcdDirPath = path.join(projectPath, vcdDir);
|
||||||
|
const vcdDirUri = vscode.Uri.file(vcdDirPath);
|
||||||
|
try {
|
||||||
|
await vscode.workspace.fs.createDirectory(vcdDirUri);
|
||||||
|
} catch {
|
||||||
|
// 目录可能已存在
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 读取原始 testbench
|
||||||
|
const tbFullPath = path.isAbsolute(tbPath) ? tbPath : path.join(projectPath, tbPath);
|
||||||
|
const tbUri = vscode.Uri.file(tbFullPath);
|
||||||
|
const tbBytes = await vscode.workspace.fs.readFile(tbUri);
|
||||||
|
const originalTb = Buffer.from(tbBytes).toString('utf-8');
|
||||||
|
|
||||||
|
// 3. 注入条件编译代码
|
||||||
|
const modifiedTb = injectConditionalDump(originalTb, dumpModules, vcdDir);
|
||||||
|
await vscode.workspace.fs.writeFile(tbUri, Buffer.from(modifiedTb, 'utf-8'));
|
||||||
|
|
||||||
|
console.log('[generateMultiVCD] Testbench 已修改,开始多次仿真...');
|
||||||
|
|
||||||
|
// 4. 获取工具路径
|
||||||
|
const iverilogPath = await getIverilogPath(extensionPath);
|
||||||
|
const vvpPath = await getVvpPath(extensionPath);
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. 获取所有 Verilog 文件
|
||||||
|
const projectCheck = await checkVerilogProject(projectPath);
|
||||||
|
const outputFile = path.join(projectPath, "simulation.vvp");
|
||||||
|
|
||||||
|
// 6. 循环执行仿真
|
||||||
|
for (const module of dumpModules) {
|
||||||
|
const macroName = `DUMP_${module.name.toUpperCase()}`;
|
||||||
|
const vcdPath = path.join(vcdDirPath, `${module.name}.vcd`);
|
||||||
|
|
||||||
|
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 编译(带宏定义)
|
||||||
|
const compileArgs = [
|
||||||
|
`-D${macroName}`,
|
||||||
|
"-o", outputFile,
|
||||||
|
...projectCheck.allVerilogFiles
|
||||||
|
];
|
||||||
|
await execCommand(iverilogPath, compileArgs, { cwd: projectPath, env });
|
||||||
|
|
||||||
|
// 仿真
|
||||||
|
const simResult = await execCommand(vvpPath, [outputFile], { cwd: projectPath, env });
|
||||||
|
allStdout += `\n[${module.name}] ${simResult.stdout}`;
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
moduleName: module.name,
|
||||||
|
vcdPath: vcdPath,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[generateMultiVCD] 模块 ${module.name} 仿真失败:`, error.message);
|
||||||
|
results.push({
|
||||||
|
moduleName: module.name,
|
||||||
|
vcdPath: vcdPath,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
// 继续执行其他模块
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 清理中间文件
|
||||||
|
try {
|
||||||
|
await vscode.workspace.fs.delete(vscode.Uri.file(outputFile));
|
||||||
|
} catch {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCount = results.filter(r => r.success).length;
|
||||||
|
return {
|
||||||
|
success: successCount > 0,
|
||||||
|
vcdFiles: results,
|
||||||
|
message: `生成完成:${successCount}/${dumpModules.length} 个 VCD 文件成功`,
|
||||||
|
stdout: allStdout
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
vcdFiles: results,
|
||||||
|
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
73
src/utils/jwtUtils.ts
Normal file
73
src/utils/jwtUtils.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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 // 传递服务等级
|
||||||
|
|||||||
@ -675,6 +675,29 @@ export function getMessageAreaScript(): string {
|
|||||||
|
|
||||||
${getPlanCardScript()}
|
${getPlanCardScript()}
|
||||||
|
|
||||||
|
// 解析多 VCD 文件路径
|
||||||
|
function parseMultiVcdPaths(toolResult) {
|
||||||
|
if (!toolResult) return [];
|
||||||
|
const result = String(toolResult);
|
||||||
|
|
||||||
|
// 匹配 "- moduleName: path" 格式
|
||||||
|
const vcdListMatch = result.match(/VCD 文件列表:[\\s\\S]*?(?=\\n\\n|$)/);
|
||||||
|
if (!vcdListMatch) return [];
|
||||||
|
|
||||||
|
const paths = [];
|
||||||
|
const lineRegex = /- (\\w+): ([^\\n]+)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = lineRegex.exec(vcdListMatch[0])) !== null) {
|
||||||
|
const name = match[1];
|
||||||
|
const pathOrError = match[2].trim();
|
||||||
|
// 跳过失败的条目
|
||||||
|
if (!pathOrError.startsWith('失败')) {
|
||||||
|
paths.push({ name: name + '.vcd', path: pathOrError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取工具图标
|
// 获取工具图标
|
||||||
function getToolIcon(toolName) {
|
function getToolIcon(toolName) {
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
@ -1057,19 +1080,30 @@ export function getMessageAreaScript(): string {
|
|||||||
|
|
||||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||||
// 优先使用显式提供的路径,否则从结果文本中解析
|
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||||||
let vcdPath = segment.vcdFilePath;
|
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||||
if (!vcdPath && segment.toolResult) {
|
|
||||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
|
||||||
if (match && match[1]) {
|
|
||||||
vcdPath = match[1].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vcdPath) {
|
if (vcdPaths.length > 0) {
|
||||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
// 多 VCD 模式:为每个文件创建预览
|
||||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
vcdPaths.forEach(vcdInfo => {
|
||||||
segmentDiv.appendChild(waveformPreview);
|
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||||
|
segmentDiv.appendChild(waveformPreview);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 单 VCD 模式(兼容旧逻辑)
|
||||||
|
let vcdPath = segment.vcdFilePath;
|
||||||
|
if (!vcdPath && segment.toolResult) {
|
||||||
|
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
vcdPath = match[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vcdPath) {
|
||||||
|
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
||||||
|
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||||
|
segmentDiv.appendChild(waveformPreview);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1304,19 +1338,30 @@ export function getMessageAreaScript(): string {
|
|||||||
|
|
||||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||||
// 优先使用显式提供的路径,否则从结果文本中解析
|
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||||||
let vcdPath = segment.vcdFilePath;
|
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||||
if (!vcdPath && segment.toolResult) {
|
|
||||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
|
||||||
if (match && match[1]) {
|
|
||||||
vcdPath = match[1].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vcdPath) {
|
if (vcdPaths.length > 0) {
|
||||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
// 多 VCD 模式:为每个文件创建预览
|
||||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
vcdPaths.forEach(vcdInfo => {
|
||||||
segmentDiv.appendChild(waveformPreview);
|
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||||
|
segmentDiv.appendChild(waveformPreview);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 单 VCD 模式(兼容旧逻辑)
|
||||||
|
let vcdPath = segment.vcdFilePath;
|
||||||
|
if (!vcdPath && segment.toolResult) {
|
||||||
|
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
vcdPath = match[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vcdPath) {
|
||||||
|
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
||||||
|
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||||
|
segmentDiv.appendChild(waveformPreview);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
|
// 代码块 (\`\`\`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>
|
||||||
|
|||||||
Reference in New Issue
Block a user