Compare commits
47 Commits
feature/wa
...
feat/back-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d1b8f7e26 | |||
| 5753e120ba | |||
| 21a8abd5cf | |||
| 4b2da8244f | |||
| c571cd9137 | |||
| 72a84ed9e2 | |||
| 58113fb109 | |||
| 25966bc1e2 | |||
| 3c93c07afd | |||
| 85a37b546c | |||
| 37a121c3de | |||
| 341b6540fa | |||
| 1d074e5a94 | |||
| 5a5d82eef8 | |||
| 43189e144a | |||
| fd11eadc19 | |||
| 1231ef0892 | |||
| a1e88d473b | |||
| d08f9a7366 | |||
| faa7b63aee | |||
| e440dd2773 | |||
| a02027e7c9 | |||
| 772b067202 | |||
| a3fd5df8e8 | |||
| bdc55c727a | |||
| 52e4522ed0 | |||
| d44b316c9a | |||
| 939768986c | |||
| 1e99f3cb20 | |||
| 2af79cf1dc | |||
| 5b225126f1 | |||
| 4abb979eab | |||
| 4a790b5aca | |||
| 9786b7141c | |||
| 4a7af49fea | |||
| 15a1de3a90 | |||
| 4687c3faa6 | |||
| 5c19be22d3 | |||
| feff8ea4d3 | |||
| 6abec8c7b7 | |||
| f9b3699bda | |||
| 8da1177bf3 | |||
| a85a044a9b | |||
| 5546791549 | |||
| c58e3603de | |||
| 178f3a7498 | |||
| 940584e1ea |
@ -1,29 +0,0 @@
|
||||
# 排除开发文件
|
||||
.vscode/**
|
||||
.git/**
|
||||
.gitignore
|
||||
node_modules/**
|
||||
src/**
|
||||
**/*.ts
|
||||
**/*.map
|
||||
|
||||
# 排除测试文件
|
||||
test/**
|
||||
**/*.test.js
|
||||
|
||||
# 排除文档
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# 排除 waveform_trace Python 源码(只保留 exe)
|
||||
tools/waveform_trace/src/**
|
||||
tools/waveform_trace/build/**
|
||||
tools/waveform_trace/dist/**
|
||||
tools/waveform_trace/build.bat
|
||||
tools/waveform_trace/build.sh
|
||||
|
||||
# 排除打包临时文件
|
||||
**/__pycache__/**
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
**/*.spec
|
||||
BIN
src/assets/titleIcon/PRO+.png
Normal file
BIN
src/assets/titleIcon/PRO+.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 302 KiB |
BIN
src/assets/titleIcon/PRO.png
Normal file
BIN
src/assets/titleIcon/PRO.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/titleIcon/free.png
Normal file
BIN
src/assets/titleIcon/free.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
@ -8,7 +8,7 @@ import * as vscode from "vscode";
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "test";
|
||||
const CURRENT_ENV: Environment = "dev";
|
||||
|
||||
/** 服务等级类型 */
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
@ -17,6 +17,10 @@ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
export interface IccoderConfig {
|
||||
/** 后端服务地址 */
|
||||
backendUrl: string;
|
||||
/** 登录页面地址 */
|
||||
loginUrl: string;
|
||||
/** 后端服务地址(strangeLoop) */
|
||||
backendUrlStrongeLoop: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
timeout: number;
|
||||
/** 用户ID(临时使用,后续对接认证) */
|
||||
@ -27,23 +31,29 @@ export interface IccoderConfig {
|
||||
|
||||
/** 环境配置 */
|
||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 */
|
||||
/** 本地开发环境 - 通过 Gateway 路由 */
|
||||
dev: {
|
||||
backendUrl: "http://localhost:2233",
|
||||
backendUrl: "http://localhost:8080/iccoder",
|
||||
backendUrlStrongeLoop: "http://localhost:8080",
|
||||
loginUrl: "http://localhost/login",
|
||||
timeout: 300000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max", // 默认使用 max
|
||||
},
|
||||
/** 测试服务器环境 */
|
||||
/** 测试服务器环境 - 通过 Gateway 路由 */
|
||||
test: {
|
||||
backendUrl: "http://192.168.1.108:2233",
|
||||
backendUrl: "http://192.168.1.108:2029/iccoder",
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
loginUrl: "http://192.168.1.108:2005/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max",
|
||||
},
|
||||
/** 生产环境 */
|
||||
/** 生产环境 - 通过 Gateway 路由 */
|
||||
prod: {
|
||||
backendUrl: "https://api.iccoder.com",
|
||||
backendUrl: "https://api.iccoder.com/iccoder",
|
||||
backendUrlStrongeLoop: "https://api.iccoder.com",
|
||||
loginUrl: "https://iccoder.com/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "auto",
|
||||
@ -75,3 +85,15 @@ export function getApiUrl(path: string): string {
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 StrangeLoop 服务 API 地址(用于用户信息等)
|
||||
*/
|
||||
export function getStrangeLoopApiUrl(path: string): string {
|
||||
const { backendUrlStrongeLoop } = getConfig();
|
||||
const baseUrl = backendUrlStrongeLoop.endsWith("/")
|
||||
? backendUrlStrongeLoop.slice(0, -1)
|
||||
: backendUrlStrongeLoop;
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
|
||||
@ -175,3 +175,13 @@ export const stateTransitionIconSvg = `
|
||||
* 用户提问图标 SVG
|
||||
*/
|
||||
export const userQuestionIconSvg = `<svg t="1767869230062" class="icon" viewBox="0 0 1068 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4819" width="14" height="14"><path d="M563.645217 578.782609c2.537739-35.350261 6.322087-58.189913 11.397566-68.518957 7.568696-15.449043 24.175304-34.370783 49.775304-56.631652 35.172174-30.72 58.546087-53.960348 70.121739-69.810087 11.575652-15.805217 17.408-36.418783 17.408-61.885217 0-41.939478-15.805217-76.399304-47.37113-103.379479-31.610435-26.980174-73.638957-40.470261-126.130087-40.47026-56.765217 0-101.376 15.760696-133.921392 47.282086C372.424348 256.934957 356.173913 298.562783 356.173913 350.386087h71.145739c1.335652-31.165217 6.811826-55.02887 16.384-71.590957 17.051826-29.740522 47.86087-44.610783 92.338087-44.610782 35.973565 0 61.796174 8.637217 77.378783 25.911652 15.582609 17.274435 23.373913 37.665391 23.373913 61.128348 0 16.784696-5.342609 32.990609-16.027826 48.573217-5.787826 8.904348-13.534609 17.363478-23.151305 25.555478l-31.966608 28.40487c-30.675478 27.113739-50.487652 51.155478-59.570087 72.125217-6.054957 13.979826-10.551652 41.627826-13.579131 82.899479h71.145739z m15.137392 89.043478a44.521739 44.521739 0 1 0-89.043479 0 44.521739 44.521739 0 0 0 89.043479 0z" fill="#8a8a8a" p-id="4820"></path><path d="M934.912 0h-801.391304a133.565217 133.565217 0 0 0-133.565218 133.565217v623.304348l0.222609 7.835826A133.565217 133.565217 0 0 0 133.565217 890.434783h222.608696v89.043478a44.521739 44.521739 0 0 0 64.556522 39.713391L675.661913 890.434783h259.294609a133.565217 133.565217 0 0 0 133.565217-133.565218V133.565217a133.565217 133.565217 0 0 0-133.565217-133.565217z m-801.391304 89.043478h801.391304a44.521739 44.521739 0 0 1 44.521739 44.521739v623.304348a44.521739 44.521739 0 0 1-44.521739 44.521739h-269.801739a44.521739 44.521739 0 0 0-20.034783 4.763826l-199.902608 100.930783V845.913043a44.521739 44.521739 0 0 0-44.52174-44.521739h-267.130434a44.521739 44.521739 0 0 1-44.521739-44.521739V133.565217a44.521739 44.521739 0 0 1 44.521739-44.521739z" fill="#8a8a8a" p-id="4821"></path></svg>`;
|
||||
|
||||
/**
|
||||
* 用户头像图标 SVG
|
||||
*/
|
||||
export const userAvatarIconSvg = `<svg t="1767947405083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4661" width="16" height="16"><path d="M515.541449 7.082899c-280.359429 0-508.458551 228.120391-508.458551 508.458551s228.120391 508.458551 508.458551 508.458551 508.458551-228.120391 508.458551-508.458551S795.900879 7.082899 515.541449 7.082899zM515.541449 981.864196c-257.132626 0-466.301477-209.190121-466.301477-466.322747 0-257.132626 209.168851-466.322747 466.301477-466.322747s466.301477 209.190121 466.301477 466.322747S772.674075 981.864196 515.541449 981.864196zM614.574414 524.177056 614.574414 524.177056c47.751075-31.96876 79.230625-86.398604 79.230625-148.187857 0-98.437405-79.804915-178.24232-178.24232-178.24232-98.437405 0-178.24232 79.804915-178.24232 178.24232 0 61.810523 31.479551 116.219097 79.251895 148.187857-100.266622 39.519598-171.244501 137.170014-171.244501 251.453545 0 0.23397 0 0.446669 0.02127 0.659369 0 0.04254-0.02127 0.10635-0.02127 0.14889 0 15.612155 12.65563 28.246516 28.267786 28.246516 15.590885 0 21.886796-12.63436 21.886796-28.246516 0-0.340319-0.08508-0.659369-0.10635-1.020958 0.10635-118.005774 102.159649-219.995264 220.207964-219.995264 118.112124 0 220.207964 102.095839 220.207964 220.207964 0 0.14889-1.467628 29.054774 21.971875 29.054774 15.505806 0 28.076356-12.57055 28.076356-28.055086 0-0.06381-0.02127-0.12762-0.02127-0.2127 0-0.25524 0.02127-0.510479 0.02127-0.786989C785.797645 661.34707 714.798496 563.696654 614.574414 524.177056zM515.541449 510.734437c-74.402343 0-134.723968-60.321625-134.723968-134.723968 0-74.423613 60.321625-134.723968 134.723968-134.723968 74.423613 0 134.723968 60.321625 134.723968 134.723968S589.943792 510.734437 515.541449 510.734437z" fill="currentColor" p-id="4662"></path></svg>`;
|
||||
|
||||
/**
|
||||
* 更新阶段图标 SVG
|
||||
*/
|
||||
export const updateStageIconSvg = `<svg t="1768188846282" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7848" width="14" height="14"><path d="M83.712 1024c-0.256 0-0.768 0-1.024-0.256-17.408-0.512-31.488-14.848-31.488-32.512V32.512C51.2 14.592 65.792 0 83.712 0h745.472c17.92 0 32.512 14.592 32.512 32.512v280.576c0 18.432-14.592 33.28-32.512 33.28-1.536 0-3.072 0-4.608-0.256-16.128-2.304-27.648-15.872-27.648-32V77.056c0-6.912-5.632-12.288-12.288-12.288H128.256c-6.912 0-12.288 5.632-12.288 12.288v869.632c0 6.912 5.632 12.288 12.288 12.288h238.08c9.728 0 18.944 4.096 25.344 11.52 6.144 7.168 8.96 16.384 7.68 25.6-2.304 16.128-15.872 27.648-32 27.648H83.712v0.256zM534.784 1024c-6.144 0-12.032-2.816-15.616-7.424-3.84-4.352-5.376-10.496-4.352-16.64l27.648-147.968c0-0.512 0.512-1.024 0.768-1.536L867.84 558.336c11.52-11.264 26.624-17.664 42.24-17.664 16.384 0 31.488 6.4 42.752 17.664l53.76 53.504c23.296 23.04 23.552 60.928 0.768 84.736l-0.768 0.768-323.584 294.912c-2.816 2.56-6.4 4.352-9.984 5.12l-134.656 26.368c-0.512 0-2.048 0.256-3.584 0.256z m95.488-182.528c-1.024 0-1.792 0.256-2.56 1.024L590.848 875.52c-0.512 0.512-1.024 1.28-1.28 2.048l-15.104 81.152c-0.256 1.792 0.768 3.072 0.768 3.072 0.768 0.768 1.792 1.28 2.816 1.28H578.816l73.984-14.336c0.768-0.256 1.28-0.512 1.792-0.768l38.4-34.816c0.768-0.768 1.024-1.536 1.024-2.56s-0.256-2.304-1.024-3.072l-60.416-64.768-0.256-0.256c0-0.512-1.024-1.024-2.048-1.024z m217.088-194.56c-1.024 0-1.792 0.256-2.56 1.024l-172.8 155.392c-0.768 0.768-1.28 1.536-1.28 2.816 0 1.024 0.256 2.048 1.024 2.56l60.16 64.768c0.768 0.768 1.792 1.28 2.816 1.28s1.792-0.256 2.56-1.024l173.568-157.952c0.768-0.768 1.024-1.536 1.024-2.56s-0.256-2.048-1.28-3.072L849.92 647.936c-0.512-0.256-1.536-1.024-2.56-1.024z m101.376 28.928c0.768 0.768 1.536 1.024 2.816 1.024 1.024 0 1.792-0.256 2.56-1.024l16.384-14.848 0.512-0.512c3.072-3.584 2.816-8.704-0.512-12.032L916.48 594.944c-1.536-1.536-4.096-2.56-6.144-2.56-2.56 0-4.864 1.024-6.4 2.56-0.256 0.256-0.512 0.512-0.768 0.512l-17.664 15.872 63.232 64.512z" p-id="7849" fill="#8a8a8a"></path><path d="M212.48 419.584h118.016v39.424H212.48v-39.424z m137.472-118.016h118.016v39.424h-118.016v-39.424z m137.728-118.016h196.608v39.424h-196.608V183.552z m0 0" fill="#8a8a8a" p-id="7850"></path><path d="M664.576 242.688h-157.184c-11.776 0-19.712 7.936-19.712 19.712v98.304h-118.016c-11.776 0-19.712 7.936-19.712 19.712V478.72h-117.76c-11.776 0-19.712 7.936-19.712 19.712v118.016c0 11.776 7.936 19.712 19.712 19.712h432.384c11.776 0 19.712-7.936 19.712-19.712V262.144c-0.256-11.776-7.936-19.456-19.712-19.456zM369.664 576.768h-117.76v-39.424h118.016v39.424z m137.728-118.016h-118.016v-39.424h118.016v39.424z m137.472-117.76h-118.016v-39.424h118.016v39.424z m0 0" fill="#8a8a8a" p-id="7851"></path></svg>`;
|
||||
|
||||
@ -5,12 +5,20 @@ import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel
|
||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||
import { VCDFileServer } from "./services/vcdFileServer";
|
||||
import { initUserService } from "./services/userService";
|
||||
import { initCreditsService } from "./services/creditsService";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 初始化用户服务
|
||||
initUserService(context);
|
||||
|
||||
// 初始化 Credits 服务
|
||||
initCreditsService(context);
|
||||
|
||||
// 初始化 VCD 文件服务器
|
||||
const vcdFileServer = new VCDFileServer();
|
||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||
vcdFileServer.start().then((port) => {
|
||||
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
||||
}).catch((error) => {
|
||||
@ -86,11 +94,55 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:在浏览器中打开 VCD 波形查看器
|
||||
const openVCDViewerInBrowserCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.openVCDViewerInBrowser",
|
||||
async (vcdFilePath?: string) => {
|
||||
if (!vcdFilePath) {
|
||||
const fileUri = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
filters: {
|
||||
"VCD 文件": ["vcd"],
|
||||
"所有文件": ["*"],
|
||||
},
|
||||
title: "选择 VCD 文件",
|
||||
});
|
||||
|
||||
if (fileUri && fileUri[0]) {
|
||||
vcdFilePath = fileUri[0].fsPath;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册文件到服务器
|
||||
const fileId = vcdFileServer.registerFile(vcdFilePath);
|
||||
const viewerUrl = vcdFileServer.getViewerUrl(fileId);
|
||||
|
||||
// 在默认浏览器中打开
|
||||
vscode.env.openExternal(vscode.Uri.parse(viewerUrl));
|
||||
vscode.window.showInformationMessage(`波形查看器已在浏览器中打开`);
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:用户登录
|
||||
const loginCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.login",
|
||||
async () => {
|
||||
try {
|
||||
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
|
||||
try {
|
||||
await vscode.authentication.getSession("iccoder", [], {
|
||||
clearSessionPreference: true,
|
||||
createIfNone: false
|
||||
});
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
// 创建新 session
|
||||
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
||||
@ -182,6 +234,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
openPanelCommand,
|
||||
openChatCommand,
|
||||
openVCDViewerCommand,
|
||||
openVCDViewerInBrowserCommand,
|
||||
loginCommand,
|
||||
logoutCommand,
|
||||
// TODO: 等待重新实现这些命令
|
||||
|
||||
@ -9,8 +9,8 @@ import {
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
handlePlanAction,
|
||||
setPendingPlanExecution,
|
||||
getCurrentTaskId,
|
||||
setLastTaskId,
|
||||
} from "../utils/messageHandler";
|
||||
@ -18,6 +18,38 @@ import { compactDialog } from "../services/apiClient";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { MessageType } from "../types/chatHistory";
|
||||
import { getCachedUserInfo } from "../services/userService";
|
||||
|
||||
/**
|
||||
* 获取会员等级图标 URI
|
||||
*/
|
||||
function getTierIconUri(
|
||||
webview: vscode.Webview,
|
||||
context: vscode.ExtensionContext,
|
||||
tierCode?: string
|
||||
): string | undefined {
|
||||
if (!tierCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tierIconMap: Record<string, string> = {
|
||||
'BASIC': 'free.png',
|
||||
'TRIAL': 'PRO-Try.png',
|
||||
'ADVANCED': 'PRO.png',
|
||||
'PROFESSIONAL': 'PRO+.png'
|
||||
};
|
||||
|
||||
const iconFile = tierIconMap[tierCode];
|
||||
if (!iconFile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iconUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, 'src', 'assets', 'titleIcon', iconFile)
|
||||
);
|
||||
|
||||
return iconUri.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并显示 IC 助手面板
|
||||
@ -108,6 +140,49 @@ export async function showICHelperPanel(
|
||||
maxIconUri.toString()
|
||||
);
|
||||
|
||||
// 获取并发送用户信息到 webview
|
||||
try {
|
||||
// 优先使用缓存的用户信息
|
||||
let userInfo = getCachedUserInfo();
|
||||
|
||||
if (userInfo) {
|
||||
// 使用缓存的用户信息
|
||||
console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo);
|
||||
console.log('[ICHelperPanel] Credits 余额:', userInfo.credits);
|
||||
const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode);
|
||||
const messageData = {
|
||||
command: 'updateUserInfo',
|
||||
userInfo: {
|
||||
userId: userInfo.userId,
|
||||
nickname: userInfo.nickname,
|
||||
username: userInfo.username,
|
||||
credits: userInfo.credits
|
||||
},
|
||||
tierIconUrl: tierIconUrl
|
||||
};
|
||||
console.log('[ICHelperPanel] 发送用户信息到前端:', messageData);
|
||||
panel.webview.postMessage(messageData);
|
||||
} else {
|
||||
// 如果没有缓存,从 session 中获取
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (session) {
|
||||
console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account);
|
||||
panel.webview.postMessage({
|
||||
command: 'updateUserInfo',
|
||||
userInfo: {
|
||||
userId: session.account.id,
|
||||
nickname: session.account.label,
|
||||
username: session.account.label
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ICHelperPanel] 获取用户信息失败:', error);
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (message) => {
|
||||
@ -176,10 +251,9 @@ export async function showICHelperPanel(
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "openWaveformViewer":
|
||||
// 打开波形查看器 - 使用 vscode.open 触发自定义编辑器
|
||||
// 在新列中打开波形查看器
|
||||
if (message.vcdFilePath) {
|
||||
const vcdUri = vscode.Uri.file(message.vcdFilePath);
|
||||
vscode.commands.executeCommand('vscode.open', vcdUri);
|
||||
vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath);
|
||||
}
|
||||
break;
|
||||
case "getVCDInfo":
|
||||
@ -212,7 +286,7 @@ export async function showICHelperPanel(
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
void handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput
|
||||
@ -255,29 +329,34 @@ export async function showICHelperPanel(
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "optimizePrompt":
|
||||
if (typeof message.prompt === "string") {
|
||||
void handleOptimizePrompt(panel, message.prompt);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: "提示词为空或格式错误",
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
// 确认执行:切换到 Agent 模式
|
||||
// 确认执行:切换到 Agent 模式(UI 切换)
|
||||
panel.webview.postMessage({
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 获取当前会话的 taskId,用于复用知识图谱数据
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
||||
setPendingPlanExecution(
|
||||
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
|
||||
} else if (message.action === "modify" || message.action === "cancel") {
|
||||
void handlePlanAction(
|
||||
panel,
|
||||
message.planTitle || "计划",
|
||||
message.action,
|
||||
message.planTitle || "",
|
||||
context.extensionPath,
|
||||
taskId
|
||||
message.model
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 添加文件上下文 - 显示工作区文件列表
|
||||
|
||||
@ -107,7 +107,8 @@ export class VCDViewerPanel {
|
||||
* 创建或显示 VCD 查看器面板
|
||||
*/
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||
const column = vscode.ViewColumn.One;
|
||||
// 在当前活动编辑器旁边打开新列
|
||||
const column = vscode.ViewColumn.Beside;
|
||||
|
||||
// 如果已经有面板打开,则显示它
|
||||
if (VCDViewerPanel.currentPanel) {
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
* API 客户端
|
||||
* 封装与后端的 HTTP 通信
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl, getConfig } from '../config/settings';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse, UserInfoResponse } from '../types/api';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
@ -18,6 +19,18 @@ interface RequestOptions {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录的 Token
|
||||
*/
|
||||
async function getAuthToken(): Promise<string | undefined> {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
return session?.accessToken;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*/
|
||||
@ -25,6 +38,9 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
const url = new URL(getApiUrl(path));
|
||||
const { timeout } = getConfig();
|
||||
|
||||
// 自动获取 Token
|
||||
const token = await getAuthToken();
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
@ -35,6 +51,7 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
method: options.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...options.headers
|
||||
},
|
||||
timeout: options.timeout || timeout
|
||||
@ -213,3 +230,33 @@ export function createSystemErrorResult(id: number, code: number, message: strin
|
||||
error: { code, message }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(): Promise<UserInfoResponse> {
|
||||
console.log('[API] 获取用户信息');
|
||||
return request<UserInfoResponse>('/system/user/getInfo', {
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
/** 余额查询响应 */
|
||||
export interface CreditBalanceResponse {
|
||||
success: boolean;
|
||||
balance?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户资源点余额
|
||||
* GET /api/dialog/balance?userId=xxx
|
||||
*/
|
||||
export async function getCreditBalance(userId: string): Promise<CreditBalanceResponse> {
|
||||
console.log('[API] 查询余额: userId=', userId);
|
||||
return request<CreditBalanceResponse>(`/api/dialog/balance?userId=${userId}`, {
|
||||
method: 'GET',
|
||||
timeout: 5000
|
||||
});
|
||||
}
|
||||
|
||||
255
src/services/creditsService.ts
Normal file
255
src/services/creditsService.ts
Normal file
@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 资源点余额管理服务
|
||||
* 负责缓存余额、主动查询、发送前检测
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getStrangeLoopApiUrl } from '../config/settings';
|
||||
import { getCachedUserInfo } from './userService';
|
||||
|
||||
/** 低余额阈值 */
|
||||
const LOW_CREDIT_THRESHOLD = 5;
|
||||
|
||||
/** 缓存的余额 */
|
||||
let cachedBalance: number | null = null;
|
||||
|
||||
/** 最后更新时间 */
|
||||
let lastUpdateTime: number = 0;
|
||||
|
||||
/** 缓存有效期(5分钟) */
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
/** ExtensionContext 用于持久化存储 */
|
||||
let extensionContext: vscode.ExtensionContext | null = null;
|
||||
|
||||
/**
|
||||
* 初始化 Credits 服务(设置 context)
|
||||
*/
|
||||
export function initCreditsService(context: vscode.ExtensionContext): void {
|
||||
extensionContext = context;
|
||||
// 从持久化存储加载余额
|
||||
const savedBalance = extensionContext.globalState.get<number>('icCoderCreditsBalance');
|
||||
if (savedBalance !== undefined) {
|
||||
cachedBalance = savedBalance;
|
||||
lastUpdateTime = Date.now();
|
||||
console.log('[CreditsService] 从持久化存储加载余额:', savedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存余额到持久化存储
|
||||
*/
|
||||
async function saveBalance(balance: number): Promise<void> {
|
||||
if (extensionContext) {
|
||||
await extensionContext.globalState.update('icCoderCreditsBalance', balance);
|
||||
console.log('[CreditsService] 余额已保存到持久化存储:', balance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存的余额(从 SSE credit_update 事件调用)
|
||||
*/
|
||||
export function updateCachedBalance(balance: number): void {
|
||||
cachedBalance = balance;
|
||||
lastUpdateTime = Date.now();
|
||||
console.log('[CreditsService] 余额已更新:', balance);
|
||||
// 异步保存到持久化存储
|
||||
saveBalance(balance).catch(err => {
|
||||
console.error('[CreditsService] 保存余额失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的余额
|
||||
*/
|
||||
export function getCachedBalance(): number | null {
|
||||
return cachedBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否有效
|
||||
*/
|
||||
function isCacheValid(): boolean {
|
||||
if (cachedBalance === null) return false;
|
||||
return Date.now() - lastUpdateTime < CACHE_TTL_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* StrangeLoop 余额响应类型
|
||||
*/
|
||||
interface StrangeLoopBalanceResponse {
|
||||
userId?: number;
|
||||
availableCredits?: number;
|
||||
totalCredits?: number;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动查询余额(直接调用 StrangeLoop 接口)
|
||||
*/
|
||||
export async function fetchBalance(): Promise<number | null> {
|
||||
try {
|
||||
// 获取 JWT token
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
if (!session?.accessToken) {
|
||||
console.warn('[CreditsService] 无法查询余额:未登录');
|
||||
return null;
|
||||
}
|
||||
|
||||
return await fetchBalanceWithToken(session.accessToken);
|
||||
} catch (error) {
|
||||
console.error('[CreditsService] 查询余额异常:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定 token 查询余额(登录过程中使用)
|
||||
*/
|
||||
export async function fetchBalanceWithToken(token: string): Promise<number | null> {
|
||||
try {
|
||||
console.log('[CreditsService] 开始查询余额,token 长度:', token.length);
|
||||
|
||||
// 直接调用 StrangeLoop 的 /api/credit/balance 接口
|
||||
const response = await callStrangeLoopBalance(token);
|
||||
|
||||
if (response.availableCredits !== undefined) {
|
||||
const balance = response.availableCredits;
|
||||
updateCachedBalance(balance);
|
||||
console.log('[CreditsService] 余额查询成功:', balance);
|
||||
return balance;
|
||||
} else {
|
||||
console.warn('[CreditsService] 查询余额失败:', response.error || response.message);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CreditsService] 查询余额异常:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 StrangeLoop 余额接口
|
||||
*/
|
||||
async function callStrangeLoopBalance(token: string): Promise<StrangeLoopBalanceResponse> {
|
||||
const urlStr = getStrangeLoopApiUrl('/strangeloop/api/credit/balance');
|
||||
const url = new URL(urlStr);
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
// 余额查询使用固定短超时,避免阻塞发送前检查
|
||||
const BALANCE_TIMEOUT_MS = 5000;
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: BALANCE_TIMEOUT_MS
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('[CreditsService] 响应状态码:', res.statusCode);
|
||||
console.log('[CreditsService] 响应内容:', data);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as StrangeLoopBalanceResponse);
|
||||
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
// 登录过期或无权限
|
||||
resolve({ error: '登录已过期,请重新登录' });
|
||||
} else {
|
||||
resolve({ error: json.error || json.message || json.msg || `HTTP ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({ error: `解析响应失败: ${data}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前余额(优先使用缓存,过期则主动查询)
|
||||
*/
|
||||
export async function getBalance(): Promise<number | null> {
|
||||
if (isCacheValid()) {
|
||||
return cachedBalance;
|
||||
}
|
||||
return await fetchBalance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查余额是否足够发送消息
|
||||
* @returns { allowed: boolean, balance: number | null, message?: string }
|
||||
*/
|
||||
export async function checkBalanceBeforeSend(): Promise<{
|
||||
allowed: boolean;
|
||||
balance: number | null;
|
||||
message?: string;
|
||||
}> {
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (!userInfo) {
|
||||
// 未登录,允许发送(后端会处理)
|
||||
return { allowed: true, balance: null };
|
||||
}
|
||||
|
||||
const balance = await getBalance();
|
||||
|
||||
if (balance === null) {
|
||||
// 无法获取余额,允许发送(后端会处理)
|
||||
console.warn('[CreditsService] 无法获取余额,允许发送');
|
||||
return { allowed: true, balance: null };
|
||||
}
|
||||
|
||||
if (balance < LOW_CREDIT_THRESHOLD) {
|
||||
return {
|
||||
allowed: false,
|
||||
balance,
|
||||
message: `资源点余额不足!当前余额 ${balance.toFixed(2)} 点,低于最低要求 ${LOW_CREDIT_THRESHOLD} 点。请充值后再试。`
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true, balance };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存(登出时调用)
|
||||
*/
|
||||
export async function clearBalanceCache(): Promise<void> {
|
||||
cachedBalance = null;
|
||||
lastUpdateTime = 0;
|
||||
if (extensionContext) {
|
||||
await extensionContext.globalState.update('icCoderCreditsBalance', undefined);
|
||||
}
|
||||
console.log('[CreditsService] 余额缓存已清除');
|
||||
}
|
||||
@ -12,12 +12,14 @@ import { getConfig } from '../config/settings';
|
||||
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
|
||||
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
||||
import { getUserIdFromToken, isTokenExpired } from '../utils/jwtUtils';
|
||||
import { updateCachedBalance } from './creditsService';
|
||||
|
||||
/**
|
||||
* 消息段落类型
|
||||
*/
|
||||
export interface MessageSegment {
|
||||
type: 'text' | 'tool' | 'question' | 'agent' | 'plan';
|
||||
type: 'text' | 'tool' | 'question' | 'agent' | 'plan' | 'progress';
|
||||
content?: string;
|
||||
toolName?: string;
|
||||
toolStatus?: 'running' | 'success' | 'error';
|
||||
@ -32,8 +34,11 @@ export interface MessageSegment {
|
||||
agentSteps?: AgentStep[];
|
||||
// 计划相关字段
|
||||
planTitle?: string;
|
||||
planPhases?: import('../types/api').PlanPhase[];
|
||||
planSteps?: string[];
|
||||
planSummary?: string;
|
||||
// 进度条相关字段(独立于 plan,用于执行模式)
|
||||
progressPhases?: import('../types/api').PlanPhase[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,7 +67,7 @@ export interface DialogCallbacks {
|
||||
/** 工具确认请求(Ask 模式) */
|
||||
onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void;
|
||||
/** 计划确认请求(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) */
|
||||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
||||
/** 实时更新段落(流式过程中) */
|
||||
@ -75,6 +80,8 @@ export interface DialogCallbacks {
|
||||
onNotification?: (message: string) => void;
|
||||
/** 上下文使用量更新 */
|
||||
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
|
||||
/** 阶段进度更新 */
|
||||
onPhaseProgress?: (phaseId: string, status: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -86,8 +93,10 @@ export class DialogSession {
|
||||
private toolContext: ToolExecutorContext;
|
||||
private accumulatedText = '';
|
||||
private isActive = false;
|
||||
private hasCompleted = false; // 标记是否已收到 complete 事件
|
||||
private segments: MessageSegment[] = [];
|
||||
private currentTextSegment: MessageSegment | null = null;
|
||||
private completeCallback: ((segments: MessageSegment[]) => void) | null = null; // 保存完成回调,用于 abort 时触发
|
||||
|
||||
constructor(extensionPath: string, existingTaskId?: string) {
|
||||
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
@ -325,12 +334,50 @@ export class DialogSession {
|
||||
}
|
||||
|
||||
this.isActive = true;
|
||||
this.hasCompleted = false; // 重置完成标志
|
||||
this.accumulatedText = '';
|
||||
this.segments = [];
|
||||
this.currentTextSegment = null;
|
||||
this.completeCallback = callbacks.onComplete || null; // 保存完成回调,用于 abort 时触发
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
// 从登录 session 获取真实 userId 和 token
|
||||
let userId = config.userId; // 默认值
|
||||
let token: string | undefined;
|
||||
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);
|
||||
|
||||
// 检测 token 是否过期
|
||||
const expired = isTokenExpired(session.accessToken);
|
||||
if (expired === true) {
|
||||
console.error('[DialogSession] token 已过期,需要重新登录');
|
||||
vscode.window.showErrorMessage('登录已过期,请重新登录', '重新登录').then(selection => {
|
||||
if (selection === '重新登录') {
|
||||
vscode.commands.executeCommand('iccoder.login');
|
||||
}
|
||||
});
|
||||
throw new Error('登录已过期,请重新登录');
|
||||
}
|
||||
|
||||
token = session.accessToken; // 保存 token 用于扣费
|
||||
const parsedUserId = getUserIdFromToken(session.accessToken);
|
||||
console.log('[DialogSession] 解析的 userId:', parsedUserId);
|
||||
if (parsedUserId) {
|
||||
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 compactedData = await historyManager.loadCompactedData(this.taskId);
|
||||
@ -340,12 +387,15 @@ export class DialogSession {
|
||||
const knowledgeData = await this.loadKnowledgeData();
|
||||
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
|
||||
|
||||
console.log('[DialogSession] serviceTier 参数:', serviceTier, '-> 使用:', serviceTier || config.serviceTier);
|
||||
|
||||
const request: DialogRequest = {
|
||||
taskId: this.taskId,
|
||||
message,
|
||||
userId: config.userId,
|
||||
userId,
|
||||
mode: mode || 'agent',
|
||||
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
||||
token, // JWT token 用于扣费
|
||||
compactedData: compactedData || undefined,
|
||||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||||
knowledgeData: knowledgeData || undefined
|
||||
@ -426,6 +476,8 @@ export class DialogSession {
|
||||
callbacks.onToolComplete?.(data.tool_name, data.result);
|
||||
// 实时发送段落更新
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
// 追踪工具执行结果(用于后端重启后恢复)
|
||||
historyManager.trackToolResult(data.tool_name, data.result);
|
||||
},
|
||||
|
||||
onToolError: (data) => {
|
||||
@ -433,6 +485,8 @@ export class DialogSession {
|
||||
callbacks.onToolError?.(data.tool_name, data.error);
|
||||
// 实时发送段落更新
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
// 追踪工具执行错误(用于后端重启后恢复)
|
||||
historyManager.trackToolResult(data.tool_name, `[错误] ${data.error}`);
|
||||
},
|
||||
|
||||
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||||
@ -508,10 +562,12 @@ export class DialogSession {
|
||||
const askId = `ask_${data.confirmId}`;
|
||||
|
||||
// 添加计划段落到聊天界面(包含 askId 用于响应)
|
||||
// 支持新格式(phases)和旧格式(steps)
|
||||
this.segments.push({
|
||||
type: 'plan',
|
||||
askId: askId,
|
||||
planTitle: data.title,
|
||||
planPhases: data.phases,
|
||||
planSteps: data.steps,
|
||||
planSummary: data.summary
|
||||
});
|
||||
@ -532,7 +588,108 @@ export class DialogSession {
|
||||
}
|
||||
|
||||
// 调用回调通知 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) => {
|
||||
@ -556,6 +713,7 @@ export class DialogSession {
|
||||
|
||||
onComplete: (data) => {
|
||||
this.isActive = false;
|
||||
this.hasCompleted = true; // 标记已收到 complete 事件
|
||||
this.finalizeTextSegment();
|
||||
|
||||
// 追踪 AI 消息(用于后端重启后恢复)
|
||||
@ -569,6 +727,18 @@ export class DialogSession {
|
||||
|
||||
onError: (data) => {
|
||||
this.isActive = false;
|
||||
|
||||
// 检测登录状态过期(只弹一次窗,不再传递错误)
|
||||
if (data.message.includes('LOGIN_EXPIRED') || data.message.includes('登录状态已过期')) {
|
||||
vscode.window.showErrorMessage('登录状态已过期,请重新登录', '重新登录').then(selection => {
|
||||
if (selection === '重新登录') {
|
||||
vscode.commands.executeCommand('ic-coder.login');
|
||||
}
|
||||
});
|
||||
// 登录过期错误已处理,不再传递给外部
|
||||
return;
|
||||
}
|
||||
|
||||
callbacks.onError?.(data.message);
|
||||
},
|
||||
|
||||
@ -654,12 +824,40 @@ export class DialogSession {
|
||||
callbacks.onContextUsage?.(data);
|
||||
},
|
||||
|
||||
onCreditUpdate: (data) => {
|
||||
console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits);
|
||||
// 更新余额缓存
|
||||
updateCachedBalance(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: () => {
|
||||
console.log('[DialogSession] SSE 连接已建立');
|
||||
},
|
||||
|
||||
onClose: () => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
@ -678,13 +876,25 @@ export class DialogSession {
|
||||
* 中止当前对话
|
||||
*/
|
||||
abort(): void {
|
||||
// 先标记完成,防止 onClose 重复触发
|
||||
const wasActive = this.isActive;
|
||||
this.hasCompleted = true;
|
||||
this.isActive = false;
|
||||
|
||||
if (this.sseController) {
|
||||
this.sseController.abort();
|
||||
this.sseController = null;
|
||||
}
|
||||
this.isActive = false;
|
||||
userInteractionManager.cancelAll();
|
||||
|
||||
// 如果之前是活跃状态,触发完成回调以结束 Promise
|
||||
if (wasActive && this.completeCallback) {
|
||||
this.finalizeTextSegment();
|
||||
console.log('[DialogSession] abort 触发完成回调');
|
||||
this.completeCallback(this.segments);
|
||||
this.completeCallback = null;
|
||||
}
|
||||
|
||||
// 通知后端停止处理
|
||||
stopDialog(this.taskId).catch(err => {
|
||||
console.warn('[DialogSession] 停止对话请求失败:', err);
|
||||
@ -713,7 +923,10 @@ export class DialogSession {
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
): Promise<void> {
|
||||
await userInteractionManager.receiveAnswer(askId, selected, customInput);
|
||||
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
||||
// 如果 pendingQuestions 中有问题,走正常流程
|
||||
// 如果没有,receiveAnswer 会使用 fallbackTaskId 直接发送到后端
|
||||
await userInteractionManager.receiveAnswer(askId, selected, customInput, this.taskId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -749,6 +962,7 @@ class DialogManager {
|
||||
*/
|
||||
abortCurrentSession(): void {
|
||||
this.currentSession?.abort();
|
||||
this.currentSession = null; // 清空会话,确保下次创建新会话
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ import * as vscode from "vscode";
|
||||
import * as http from "http";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||
import { getConfig } from "../config/settings";
|
||||
|
||||
/**
|
||||
* IC Coder Authentication Provider
|
||||
@ -12,7 +14,6 @@ export class ICCoderAuthenticationProvider
|
||||
{
|
||||
private static readonly AUTH_TYPE = "iccoder";
|
||||
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 currentPort: number | null = null;
|
||||
|
||||
@ -23,8 +24,23 @@ export class ICCoderAuthenticationProvider
|
||||
private _sessions: vscode.AuthenticationSession[] = [];
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
// 从存储中恢复会话
|
||||
this.loadSessions();
|
||||
// 从存储中恢复会话(同步执行)
|
||||
this.loadSessionsSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中加载会话(同步版本)
|
||||
*/
|
||||
private loadSessionsSync(): void {
|
||||
const storedSessions = this.context.globalState.get<
|
||||
vscode.AuthenticationSession[]
|
||||
>("icCoderSessions", []);
|
||||
this._sessions = storedSessions;
|
||||
console.log("[AuthProvider] 同步加载 sessions, 数量:", this._sessions.length);
|
||||
if (this._sessions.length > 0) {
|
||||
console.log("[AuthProvider] Session ID:", this._sessions[0].id);
|
||||
console.log("[AuthProvider] Account:", this._sessions[0].account.label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,7 +57,9 @@ export class ICCoderAuthenticationProvider
|
||||
* 保存会话到存储
|
||||
*/
|
||||
private async saveSessions(): Promise<void> {
|
||||
console.log("[AuthProvider] 保存 sessions, 数量:", this._sessions.length);
|
||||
await this.context.globalState.update("icCoderSessions", this._sessions);
|
||||
console.log("[AuthProvider] sessions 已保存到 globalState");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,6 +68,7 @@ export class ICCoderAuthenticationProvider
|
||||
async getSessions(
|
||||
scopes?: readonly string[]
|
||||
): Promise<vscode.AuthenticationSession[]> {
|
||||
console.log("[AuthProvider] getSessions 被调用, 当前 sessions 数量:", this._sessions.length);
|
||||
return [...this._sessions];
|
||||
}
|
||||
|
||||
@ -60,15 +79,32 @@ export class ICCoderAuthenticationProvider
|
||||
scopes: readonly string[]
|
||||
): Promise<vscode.AuthenticationSession> {
|
||||
try {
|
||||
// 先删除旧的 session(静默删除,不弹窗、不重载窗口)
|
||||
if (this._sessions.length > 0) {
|
||||
const oldSession = this._sessions[0];
|
||||
this._sessions = [];
|
||||
await this.saveSessions();
|
||||
await clearUserInfo();
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
removed: [oldSession],
|
||||
changed: [],
|
||||
});
|
||||
console.log("🔄 已清除旧的 session");
|
||||
}
|
||||
|
||||
const token = await this.login();
|
||||
|
||||
// 获取到 token 后立即调用用户信息接口
|
||||
const userInfo = await onTokenReceived(token);
|
||||
|
||||
// 创建会话
|
||||
const session: vscode.AuthenticationSession = {
|
||||
id: this.generateSessionId(),
|
||||
accessToken: token,
|
||||
account: {
|
||||
id: "iccoder-user",
|
||||
label: "IC Coder 用户",
|
||||
id: userInfo?.userId || "iccoder-user",
|
||||
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
|
||||
},
|
||||
scopes: [...scopes],
|
||||
};
|
||||
@ -109,6 +145,9 @@ export class ICCoderAuthenticationProvider
|
||||
this._sessions.splice(sessionIndex, 1);
|
||||
await this.saveSessions();
|
||||
|
||||
// 清除用户信息缓存
|
||||
await clearUserInfo();
|
||||
|
||||
// 触发会话变化事件
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
@ -149,9 +188,8 @@ export class ICCoderAuthenticationProvider
|
||||
|
||||
// 构建登录 URL
|
||||
const callbackUrl = `http://localhost:${port}/callback`;
|
||||
const loginUrl = `${
|
||||
ICCoderAuthenticationProvider.LOGIN_URL
|
||||
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||||
const config = getConfig();
|
||||
const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
console.log("🔐 登录服务器已启动,监听端口:", port);
|
||||
console.log("🌐 登录 URL:", loginUrl);
|
||||
|
||||
103
src/services/promptOptimizeService.ts
Normal file
103
src/services/promptOptimizeService.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 提示词优化服务
|
||||
* 调用后端 API 优化用户输入的提示词
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl } from '../config/settings';
|
||||
|
||||
/** 优化响应类型 */
|
||||
interface OptimizeResponse {
|
||||
success: boolean;
|
||||
optimizedPrompt?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化提示词
|
||||
* @param prompt 原始提示词
|
||||
* @returns 优化后的提示词
|
||||
*/
|
||||
export async function optimizePrompt(prompt: string): Promise<string> {
|
||||
// 获取 JWT token
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
if (!session?.accessToken) {
|
||||
throw new Error('未登录,请先登录');
|
||||
}
|
||||
|
||||
const response = await callOptimizeApi(prompt, session.accessToken);
|
||||
|
||||
if (response.success && response.optimizedPrompt) {
|
||||
return response.optimizedPrompt;
|
||||
} else {
|
||||
throw new Error(response.error || '优化失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用后端优化 API
|
||||
*/
|
||||
async function callOptimizeApi(prompt: string, token: string): Promise<OptimizeResponse> {
|
||||
const urlStr = getApiUrl('/api/prompt/optimize');
|
||||
const url = new URL(urlStr);
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const body = JSON.stringify({ prompt });
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('[PromptOptimize] 响应状态码:', res.statusCode);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as OptimizeResponse);
|
||||
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
resolve({ success: false, error: '登录已过期,请重新登录' });
|
||||
} else {
|
||||
resolve({ success: false, error: json.error || json.message || `HTTP ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({ success: false, error: `解析响应失败: ${data}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
@ -28,7 +28,8 @@ import type {
|
||||
AgentProgressEvent,
|
||||
AgentCompleteEvent,
|
||||
AgentErrorEvent,
|
||||
ContextUsageEvent
|
||||
ContextUsageEvent,
|
||||
CreditUpdateEvent
|
||||
} from '../types/api';
|
||||
import type { MemoryCompactedEvent } from '../types/memory';
|
||||
|
||||
@ -44,6 +45,16 @@ export interface SSECallbacks {
|
||||
onToolConfirm?: (data: ToolConfirmEvent) => void;
|
||||
/** 收到计划确认请求(Plan 模式) */
|
||||
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;
|
||||
/** 工具执行完成 */
|
||||
@ -74,6 +85,8 @@ export interface SSECallbacks {
|
||||
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
||||
/** 上下文使用量更新 */
|
||||
onContextUsage?: (data: ContextUsageEvent) => void;
|
||||
/** 资源点余额更新 */
|
||||
onCreditUpdate?: (data: CreditUpdateEvent) => void;
|
||||
/** 连接打开 */
|
||||
onOpen?: () => void;
|
||||
/** 连接关闭 */
|
||||
@ -160,7 +173,8 @@ export async function startStreamDialog(
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
...(request.token ? { 'Authorization': `Bearer ${request.token}` } : {})
|
||||
}
|
||||
};
|
||||
|
||||
@ -170,9 +184,20 @@ export async function startStreamDialog(
|
||||
let errorBody = '';
|
||||
res.on('data', chunk => errorBody += chunk);
|
||||
res.on('end', () => {
|
||||
// 检测是否是登录状态过期
|
||||
const isLoginExpired = errorBody.includes('登录状态已过期') ||
|
||||
errorBody.includes('token') && errorBody.includes('过期') ||
|
||||
res.statusCode === 401;
|
||||
|
||||
if (isLoginExpired) {
|
||||
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
} else {
|
||||
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -213,6 +238,25 @@ export async function startStreamDialog(
|
||||
res.on('data', (chunk: string) => {
|
||||
if (!controller.aborted) {
|
||||
console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200));
|
||||
|
||||
// 检查是否是业务错误码(Gateway 返回 HTTP 200 但响应体是错误 JSON)
|
||||
try {
|
||||
const trimmed = chunk.trim();
|
||||
if (trimmed.startsWith('{') && trimmed.includes('"code"')) {
|
||||
const json = JSON.parse(trimmed);
|
||||
if (json.code === 401 || json.msg?.includes('登录状态已过期')) {
|
||||
console.log('[SSE] 检测到登录过期业务错误');
|
||||
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
|
||||
callbacks.onError?.({ message: error.message });
|
||||
controller.abort();
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 不是 JSON 格式,继续正常处理
|
||||
}
|
||||
|
||||
parser.feed(chunk);
|
||||
}
|
||||
});
|
||||
@ -286,6 +330,21 @@ function dispatchEvent(
|
||||
case 'plan_confirm':
|
||||
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
|
||||
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':
|
||||
callbacks.onToolStart?.(data as ToolStartEvent);
|
||||
break;
|
||||
@ -331,6 +390,9 @@ function dispatchEvent(
|
||||
case 'context_usage':
|
||||
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
||||
break;
|
||||
case 'credit_update':
|
||||
callbacks.onCreditUpdate?.(data as CreditUpdateEvent);
|
||||
break;
|
||||
case 'heartbeat':
|
||||
// 心跳事件:仅用于保持连接,不需要特殊处理
|
||||
// Node.js req.setTimeout 会在收到数据时自动重置计时器
|
||||
|
||||
@ -8,7 +8,7 @@ import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||
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 { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
||||
import {
|
||||
@ -25,6 +25,7 @@ import type {
|
||||
FileDeleteArgs,
|
||||
FileListArgs,
|
||||
SyntaxCheckArgs,
|
||||
IverilogArgs,
|
||||
SimulationArgs,
|
||||
WaveformSummaryArgs,
|
||||
KnowledgeSaveArgs,
|
||||
@ -75,6 +76,9 @@ export async function executeToolCall(
|
||||
case 'syntax_check':
|
||||
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
|
||||
break;
|
||||
case 'iverilog':
|
||||
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
|
||||
break;
|
||||
case 'simulation':
|
||||
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
||||
break;
|
||||
@ -270,6 +274,71 @@ async function executeSyntaxCheck(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 iverilog 工具
|
||||
* 直接执行 iverilog 命令
|
||||
*/
|
||||
async function executeIverilog(
|
||||
args: IverilogArgs,
|
||||
context: ToolExecutorContext
|
||||
): Promise<string> {
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
if (!iverilogCheck.available) {
|
||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
||||
}
|
||||
|
||||
// 获取工作目录
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('没有打开的工作区');
|
||||
}
|
||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||
const workDir = args.workDir
|
||||
? path.join(projectPath, args.workDir)
|
||||
: projectPath;
|
||||
|
||||
// 解析参数
|
||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||
const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0);
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(iverilogPath, cmdArgs, {
|
||||
cwd: workDir,
|
||||
env: {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
||||
}
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number) => {
|
||||
const output = stderr || stdout || '(无输出)';
|
||||
if (code === 0) {
|
||||
resolve(`执行成功\n${output}`);
|
||||
} else {
|
||||
resolve(`执行失败 (exit code: ${code})\n${output}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 simulation 工具
|
||||
*/
|
||||
@ -285,7 +354,30 @@ async function executeSimulation(
|
||||
|
||||
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);
|
||||
|
||||
if (result.success) {
|
||||
@ -303,6 +395,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 工具
|
||||
* 解析 VCD 文件并返回波形摘要
|
||||
|
||||
@ -82,21 +82,28 @@ export class UserInteractionManager {
|
||||
* @param askId 问题ID
|
||||
* @param selected 选中的选项
|
||||
* @param customInput 自定义输入
|
||||
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
||||
*/
|
||||
async receiveAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
customInput?: string,
|
||||
fallbackTaskId?: string
|
||||
): Promise<void> {
|
||||
const pending = this.pendingQuestions.get(askId);
|
||||
const answer = customInput || selected?.join(', ') || '';
|
||||
|
||||
if (!pending) {
|
||||
console.warn(`[UserInteraction] 问题不存在或已超时: askId=${askId}`);
|
||||
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
||||
if (fallbackTaskId) {
|
||||
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
||||
await this.submitUserAnswer(askId, fallbackTaskId, answer);
|
||||
} else {
|
||||
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建答案
|
||||
const answer = customInput || selected?.join(', ') || '';
|
||||
|
||||
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
|
||||
|
||||
// 移除待处理问题
|
||||
@ -173,6 +180,13 @@ export class UserInteractionManager {
|
||||
hasPendingQuestions(): boolean {
|
||||
return this.pendingQuestions.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查特定问题是否存在
|
||||
*/
|
||||
hasPendingQuestion(askId: string): boolean {
|
||||
return this.pendingQuestions.has(askId);
|
||||
}
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
|
||||
378
src/services/userService.ts
Normal file
378
src/services/userService.ts
Normal file
@ -0,0 +1,378 @@
|
||||
/**
|
||||
* 用户服务
|
||||
* 管理用户信息和认证相关的 API 调用
|
||||
*/
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import * as vscode from 'vscode';
|
||||
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
|
||||
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
|
||||
import { fetchBalanceWithToken, getCachedBalance } from './creditsService';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
*/
|
||||
interface RequestOptions {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
timeout?: number;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求(带 token)
|
||||
*/
|
||||
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
const url = new URL(getStrangeLoopApiUrl(path));
|
||||
const { timeout } = getConfig();
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
// 如果有 token,添加到请求头
|
||||
if (options.token) {
|
||||
headers['Authorization'] = `Bearer ${options.token}`;
|
||||
}
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: options.method,
|
||||
headers,
|
||||
timeout: options.timeout || timeout
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log(`[HTTP] 响应状态码: ${res.statusCode}`);
|
||||
console.log(`[HTTP] 响应内容: ${data}`);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as T);
|
||||
} else {
|
||||
reject(new Error(json.error || json.message || json.msg || `HTTP ${res.statusCode}`));
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果不是 JSON,直接返回原始内容
|
||||
reject(new Error(`解析响应失败 (${res.statusCode}): ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(JSON.stringify(options.body));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息数据结构(实际返回的数据)
|
||||
*/
|
||||
export interface UserInfo {
|
||||
userId: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
phonenumber?: string;
|
||||
avatar?: string;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
createTime?: string;
|
||||
loginDate?: string;
|
||||
// 会员信息
|
||||
membership?: {
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
remainingDays?: number;
|
||||
monthlyCredits?: number;
|
||||
};
|
||||
// Credits 余额
|
||||
credits?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||||
const apiPath = '/system/user/getInfo';
|
||||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||
console.log('[UserService] 获取用户信息');
|
||||
console.log('[UserService] 请求地址:', fullUrl);
|
||||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||
|
||||
try {
|
||||
const response = await request<UserInfoResponse>(apiPath, {
|
||||
method: 'GET',
|
||||
token
|
||||
});
|
||||
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.user) {
|
||||
const user = response.user;
|
||||
return {
|
||||
userId: String(user.userId),
|
||||
username: user.userName,
|
||||
nickname: user.nickName,
|
||||
email: user.email,
|
||||
phonenumber: user.phonenumber,
|
||||
avatar: user.avatar,
|
||||
roles: response.roles,
|
||||
permissions: response.permissions,
|
||||
createTime: user.createTime,
|
||||
loginDate: user.loginDate
|
||||
};
|
||||
}
|
||||
|
||||
console.error('[UserService] 获取用户信息失败:', response);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户会员信息
|
||||
* GET /strangeloop/api/membership/current
|
||||
*/
|
||||
export async function getMembershipInfo(token: string): Promise<MultiMembershipVO | null> {
|
||||
const apiPath = '/strangeloop/api/membership/current';
|
||||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||
console.log('[UserService] 获取会员信息');
|
||||
console.log('[UserService] 请求地址:', fullUrl);
|
||||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||
|
||||
try {
|
||||
const response = await request<MembershipResponse>(apiPath, {
|
||||
method: 'GET',
|
||||
token
|
||||
});
|
||||
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.data) {
|
||||
console.log('[UserService] 会员信息获取成功:', response.data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.error('[UserService] 获取会员信息失败:', response);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 请求会员信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级映射
|
||||
*/
|
||||
const TIER_LEVEL_MAP: Record<string, number> = {
|
||||
'BASIC': 1,
|
||||
'TRIAL': 2,
|
||||
'ADVANCED': 3,
|
||||
'PROFESSIONAL': 4
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取最高等级的会员信息
|
||||
*/
|
||||
function getHighestTierMembership(allMemberships?: MembershipItemVO[]): MembershipItemVO | null {
|
||||
if (!allMemberships || allMemberships.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 按等级排序,获取最高等级
|
||||
return allMemberships.reduce((highest, current) => {
|
||||
const currentLevel = TIER_LEVEL_MAP[current.tierCode] || 0;
|
||||
const highestLevel = TIER_LEVEL_MAP[highest.tierCode] || 0;
|
||||
return currentLevel > highestLevel ? current : highest;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当获取到 token 时自动调用此函数
|
||||
* 用于在登录成功后立即获取用户信息
|
||||
*/
|
||||
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
try {
|
||||
console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...');
|
||||
|
||||
// 并行获取用户信息、会员信息和余额
|
||||
const [userInfo, membershipInfo, credits] = await Promise.all([
|
||||
getUserInfo(token),
|
||||
getMembershipInfo(token),
|
||||
fetchBalanceWithToken(token)
|
||||
]);
|
||||
|
||||
if (!userInfo) {
|
||||
console.warn('[UserService] 未能获取到用户信息');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 添加 Credits 余额到用户信息
|
||||
console.log('[UserService] 获取到的 Credits 余额:', credits);
|
||||
if (credits !== null) {
|
||||
userInfo.credits = credits;
|
||||
console.log('[UserService] Credits 已添加到用户信息');
|
||||
} else {
|
||||
console.warn('[UserService] Credits 余额为 null,未添加到用户信息');
|
||||
}
|
||||
|
||||
// 打印用户信息到控制台
|
||||
console.log('='.repeat(60));
|
||||
console.log('用户信息详情:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`用户ID: ${userInfo.userId}`);
|
||||
console.log(`用户名: ${userInfo.username}`);
|
||||
console.log(`昵称: ${userInfo.nickname}`);
|
||||
if (userInfo.email) {
|
||||
console.log(`邮箱: ${userInfo.email}`);
|
||||
}
|
||||
if (userInfo.phonenumber) {
|
||||
console.log(`手机号: ${userInfo.phonenumber}`);
|
||||
}
|
||||
if (userInfo.avatar) {
|
||||
console.log(`头像: ${userInfo.avatar}`);
|
||||
}
|
||||
if (userInfo.roles && userInfo.roles.length > 0) {
|
||||
console.log(`角色: ${userInfo.roles.join(', ')}`);
|
||||
}
|
||||
if (userInfo.permissions && userInfo.permissions.length > 0) {
|
||||
console.log(`权限: ${userInfo.permissions.join(', ')}`);
|
||||
}
|
||||
if (userInfo.createTime) {
|
||||
console.log(`创建时间: ${userInfo.createTime}`);
|
||||
}
|
||||
if (userInfo.loginDate) {
|
||||
console.log(`最后登录: ${userInfo.loginDate}`);
|
||||
}
|
||||
|
||||
// 打印会员信息 - 从 allMemberships 中获取最高等级
|
||||
if (membershipInfo && membershipInfo.allMemberships) {
|
||||
const highestTier = getHighestTierMembership(membershipInfo.allMemberships);
|
||||
|
||||
if (highestTier) {
|
||||
console.log('');
|
||||
console.log('会员信息:');
|
||||
console.log(`会员等级: ${highestTier.tierName} (${highestTier.tierCode})`);
|
||||
console.log(`等级层级: ${highestTier.tierLevel}`);
|
||||
console.log(`剩余天数: ${highestTier.remainingDays === -1 ? '永久' : highestTier.remainingDays + '天'}`);
|
||||
console.log(`月度积分: ${highestTier.monthlyCredits}`);
|
||||
|
||||
// 将最高等级会员信息合并到用户信息中
|
||||
userInfo.membership = {
|
||||
tierCode: highestTier.tierCode,
|
||||
tierName: highestTier.tierName,
|
||||
tierLevel: highestTier.tierLevel,
|
||||
remainingDays: highestTier.remainingDays,
|
||||
monthlyCredits: highestTier.monthlyCredits
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 打印 Credits 余额
|
||||
console.log('');
|
||||
console.log('资源点余额:');
|
||||
if (userInfo.credits !== undefined) {
|
||||
console.log(`当前余额: ${userInfo.credits} Credits`);
|
||||
} else {
|
||||
console.log('当前余额: 未获取到余额信息');
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 保存到持久化存储
|
||||
await saveUserInfo(userInfo);
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 获取用户信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 持久化存储 ==============
|
||||
|
||||
let extensionContext: vscode.ExtensionContext | null = null;
|
||||
|
||||
/**
|
||||
* 初始化用户服务(设置 context)
|
||||
*/
|
||||
export function initUserService(context: vscode.ExtensionContext): void {
|
||||
extensionContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户信息到持久化存储
|
||||
*/
|
||||
export async function saveUserInfo(userInfo: UserInfo): Promise<void> {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return;
|
||||
}
|
||||
await extensionContext.globalState.update('icCoderUserInfo', userInfo);
|
||||
console.log('[UserService] 用户信息已保存到持久化存储');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从持久化存储获取用户信息
|
||||
*/
|
||||
export function getCachedUserInfo(): UserInfo | null {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return null;
|
||||
}
|
||||
const userInfo = extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
||||
|
||||
// 从 creditsService 加载余额并合并到用户信息中
|
||||
if (userInfo) {
|
||||
const cachedCredits = getCachedBalance();
|
||||
if (cachedCredits !== null) {
|
||||
userInfo.credits = cachedCredits;
|
||||
console.log('[UserService] 从 creditsService 加载余额:', cachedCredits);
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除持久化存储的用户信息
|
||||
*/
|
||||
export async function clearUserInfo(): Promise<void> {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return;
|
||||
}
|
||||
await extensionContext.globalState.update('icCoderUserInfo', undefined);
|
||||
console.log('[UserService] 用户信息已清除');
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import * as http from "http";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
|
||||
/**
|
||||
* VCD 文件 HTTP 服务器
|
||||
@ -10,6 +11,11 @@ export class VCDFileServer {
|
||||
private server: http.Server | null = null;
|
||||
private port: number = 0;
|
||||
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
||||
private extensionUri: vscode.Uri;
|
||||
|
||||
constructor(extensionUri: vscode.Uri) {
|
||||
this.extensionUri = extensionUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
@ -73,6 +79,13 @@ export class VCDFileServer {
|
||||
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取波形查看器 URL
|
||||
*/
|
||||
public getViewerUrl(fileId: string): string {
|
||||
return `http://127.0.0.1:${this.port}/viewer/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件 ID
|
||||
*/
|
||||
@ -101,7 +114,53 @@ export class VCDFileServer {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 URL,提取文件 ID
|
||||
// 路由处理
|
||||
if (url.startsWith("/viewer/")) {
|
||||
this.handleViewerRequest(url, res);
|
||||
} else if (url.startsWith("/vcd/")) {
|
||||
this.handleVcdFileRequest(url, res);
|
||||
} else if (url.startsWith("/static/")) {
|
||||
this.handleStaticFileRequest(url, res);
|
||||
} else {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查看器页面请求
|
||||
*/
|
||||
private handleViewerRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/viewer\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = match[1];
|
||||
const filePath = this.vcdFiles.get(fileId);
|
||||
|
||||
if (!filePath) {
|
||||
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成 HTML 页面
|
||||
const html = this.generateViewerHtml(fileId, filePath);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Content-Length": Buffer.byteLength(html),
|
||||
});
|
||||
res.end(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 VCD 文件请求
|
||||
*/
|
||||
private handleVcdFileRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/vcd\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
@ -142,4 +201,300 @@ export class VCDFileServer {
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理静态文件请求(Surfer 资源)
|
||||
*/
|
||||
private handleStaticFileRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/static\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = match[1];
|
||||
const filePath = path.join(this.extensionUri.fsPath, "media", "surfer", fileName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
const contentType = this.getContentType(fileName);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": fileContent.length,
|
||||
});
|
||||
res.end(fileContent);
|
||||
} catch (error) {
|
||||
console.error(`[VCDFileServer] 读取静态文件失败:`, error);
|
||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 Content-Type
|
||||
*/
|
||||
private getContentType(fileName: string): string {
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
const contentTypes: { [key: string]: string } = {
|
||||
".js": "application/javascript",
|
||||
".wasm": "application/wasm",
|
||||
".html": "text/html",
|
||||
".css": "text/css",
|
||||
};
|
||||
return contentTypes[ext] || "application/octet-stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||||
*/
|
||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||
try {
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
||||
const lines = buffer.split('\n');
|
||||
|
||||
const scopeNames: string[] = [];
|
||||
let scopeDepth = 0;
|
||||
const scopeStack: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith('$enddefinitions')) {
|
||||
break;
|
||||
}
|
||||
|
||||
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||||
if (scopeMatch) {
|
||||
const scopeType = scopeMatch[1];
|
||||
const scopeName = scopeMatch[2];
|
||||
|
||||
if (scopeDepth === 0 && scopeType === 'module') {
|
||||
scopeStack.push(scopeName);
|
||||
} else if (scopeDepth === 1 && scopeType === 'module') {
|
||||
const fullPath = [...scopeStack, scopeName];
|
||||
scopeNames.push(fullPath.join('.'));
|
||||
}
|
||||
|
||||
scopeDepth++;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('$upscope')) {
|
||||
scopeDepth--;
|
||||
if (scopeDepth === 0) {
|
||||
scopeStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scopeNames;
|
||||
} catch (error) {
|
||||
console.error("[VCDFileServer] 解析 VCD 文件失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成波形查看器 HTML 页面
|
||||
*/
|
||||
private generateViewerHtml(fileId: string, vcdFilePath: string): string {
|
||||
const vcdUrl = this.getFileUrl(fileId);
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||||
const scopeNamesJson = JSON.stringify(scopeNames);
|
||||
|
||||
const htmlPart1 = this.getHtmlPart1(fileName);
|
||||
const htmlPart2 = this.getHtmlPart2(vcdUrl, scopeNamesJson);
|
||||
const htmlPart3 = this.getHtmlPart3();
|
||||
|
||||
return htmlPart1 + htmlPart2 + htmlPart3;
|
||||
}
|
||||
|
||||
private getHtmlPart1(fileName: string): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Surfer 波形查看器 - ${fileName}</title>
|
||||
<script>
|
||||
window.surferReady = false;
|
||||
window.pendingVcdData = null;
|
||||
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Surfer error:", msg);
|
||||
document.getElementById("error_message").innerHTML = msg;
|
||||
document.getElementById("error_container").style.display = "block";
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
</script>
|
||||
<script type="module">
|
||||
console.log('[Browser] 开始初始化 Surfer...');
|
||||
import init from '/static/surfer.js';
|
||||
await init({module_or_path: '/static/surfer_bg.wasm'});
|
||||
console.log('[Browser] Surfer WASM 已加载');
|
||||
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/static/surfer.js';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
window.surferReady = true;
|
||||
console.log('[Browser] Surfer 已完全初始化并准备就绪');
|
||||
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ToggleLogs"));
|
||||
console.log('[Browser] 已发送关闭日志面板命令');
|
||||
} catch (e) {
|
||||
console.log('[Browser] 关闭日志面板失败:', e);
|
||||
}
|
||||
|
||||
if (window.pendingVcdData) {
|
||||
console.log('[Browser] 发现待处理的 VCD 数据,立即加载');
|
||||
loadVcdUrl(window.pendingVcdData);
|
||||
window.pendingVcdData = null;
|
||||
}
|
||||
</script>`;
|
||||
}
|
||||
|
||||
private getHtmlPart2(vcdUrl: string, scopeNamesJson: string): string {
|
||||
return `
|
||||
<script>
|
||||
function loadVcdUrl(data) {
|
||||
try {
|
||||
console.log('[Browser] ========== 开始加载 VCD URL ==========');
|
||||
console.log('[Browser] URL:', data.url);
|
||||
console.log('[Browser] Scope names from VCD:', data.scopeNames);
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('[Browser] 通过 postMessage 发送 LoadUrl 命令');
|
||||
window.postMessage({
|
||||
command: 'LoadUrl',
|
||||
url: data.url
|
||||
}, '*');
|
||||
console.log('[Browser] ✅ 已发送 LoadUrl 命令');
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('[Browser] 尝试自动添加所有信号');
|
||||
let scopeNamesToTry = [];
|
||||
|
||||
if (data.scopeNames && data.scopeNames.length > 0) {
|
||||
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||||
console.log('[Browser] 使用解析的作用域名称:', scopeNamesToTry);
|
||||
} else {
|
||||
scopeNamesToTry = [['top'], ['testbench'], ['tb'], ['test'], ['dut']];
|
||||
console.log('[Browser] 使用回退作用域名称');
|
||||
}
|
||||
|
||||
for (let i = 0; i < scopeNamesToTry.length; i++) {
|
||||
const scopeName = scopeNamesToTry[i];
|
||||
try {
|
||||
const addScopeMsg = {
|
||||
"AddScope": [
|
||||
{"strs": scopeName, "id": {"Wellen": i + 1}},
|
||||
true
|
||||
]
|
||||
};
|
||||
window.inject_message(JSON.stringify(addScopeMsg));
|
||||
console.log('[Browser] 已发送 AddScope: ' + scopeName.join('.'));
|
||||
} catch (e) {
|
||||
console.log('[Browser] AddScope 失败: ' + scopeName.join('.'), e);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ZoomToFit"));
|
||||
console.log('[Browser] 已发送 ZoomToFit 命令');
|
||||
} catch (e) {
|
||||
console.log('[Browser] ZoomToFit 失败:', e);
|
||||
}
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
console.error('[Browser] 添加信号失败:', e);
|
||||
}
|
||||
}, 1500);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('[Browser] ❌ 加载 VCD 失败:', error);
|
||||
on_surfer_error(error.message + '\\n' + error.stack);
|
||||
}
|
||||
}
|
||||
window.loadVcdUrl = loadVcdUrl;
|
||||
|
||||
// 页面加载完成后自动加载 VCD
|
||||
window.addEventListener('load', () => {
|
||||
const vcdData = {
|
||||
url: '${vcdUrl}',
|
||||
scopeNames: ${scopeNamesJson}
|
||||
};
|
||||
if (window.surferReady) {
|
||||
loadVcdUrl(vcdData);
|
||||
} else {
|
||||
window.pendingVcdData = vcdData;
|
||||
}
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
|
||||
private getHtmlPart3(): string {
|
||||
return `
|
||||
<style>
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: #f48771;
|
||||
background-color: #5a1d1d;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
<div id="error_container" style="display: none;">
|
||||
<h3>❌ Surfer 加载失败</h3>
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
<script src="/static/integration.js"></script>
|
||||
<script>
|
||||
register_message_listener();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
183
src/types/api.ts
183
src/types/api.ts
@ -40,6 +40,8 @@ export interface DialogRequest {
|
||||
mode: RunMode;
|
||||
/** 服务等级 */
|
||||
serviceTier?: ServiceTier;
|
||||
/** JWT Token(用于认证和扣费) */
|
||||
token?: string;
|
||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||
compactedData?: CompactedMemory;
|
||||
/** 压缩后产生的新消息 */
|
||||
@ -56,6 +58,11 @@ export type SSEEventType =
|
||||
| "tool_call" // 客户端工具调用请求
|
||||
| "tool_confirm" // 工具确认请求(Ask 模式)
|
||||
| "plan_confirm" // 计划确认请求(Plan 模式)
|
||||
| "phase_progress" // 阶段进度更新
|
||||
| "plan_step_add" // 添加计划步骤
|
||||
| "plan_step_remove" // 删除计划步骤
|
||||
| "plan_step_update" // 更新计划步骤
|
||||
| "plan_summary_update" // 更新计划摘要
|
||||
| "tool_start" // 工具开始执行
|
||||
| "tool_complete" // 工具执行完成
|
||||
| "tool_error" // 工具执行错误
|
||||
@ -66,6 +73,7 @@ export type SSEEventType =
|
||||
| "agent_error" // 子智能体错误
|
||||
| "memory_compacted" // 记忆压缩完成
|
||||
| "context_usage" // 上下文使用量
|
||||
| "credit_update" // 资源点余额更新
|
||||
| "complete" // 对话完成
|
||||
| "error" // 错误
|
||||
| "warning" // 警告
|
||||
@ -108,20 +116,83 @@ export interface ToolConfirmEvent {
|
||||
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 模式计划确认) */
|
||||
export interface PlanConfirmEvent {
|
||||
/** 确认ID */
|
||||
confirmId: number;
|
||||
/** 计划标题 */
|
||||
title: string;
|
||||
/** 执行步骤列表 */
|
||||
steps: string[];
|
||||
/** 四阶段计划列表(新格式) */
|
||||
phases?: PlanPhase[];
|
||||
/** 执行步骤列表(旧格式,兼容) */
|
||||
steps?: string[];
|
||||
/** 计划摘要 */
|
||||
summary: string;
|
||||
/** 时间戳 */
|
||||
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 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
@ -201,6 +272,12 @@ export interface ContextUsageEvent {
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
/** credit_update 事件数据 */
|
||||
export interface CreditUpdateEvent {
|
||||
deductedCredits: number;
|
||||
remainingCredits: number;
|
||||
}
|
||||
|
||||
// ============== 工具调用协议 (MCP 格式) ==============
|
||||
|
||||
/**
|
||||
@ -310,6 +387,96 @@ export interface ToolConfirmResponse {
|
||||
approved: boolean;
|
||||
}
|
||||
|
||||
// ============== 用户信息 ==============
|
||||
|
||||
/**
|
||||
* 用户信息响应
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export interface UserInfoResponse {
|
||||
/** 响应消息 */
|
||||
msg: string;
|
||||
/** 响应代码 (200 表示成功) */
|
||||
code: number;
|
||||
/** 权限列表 */
|
||||
permissions: string[];
|
||||
/** 角色列表 */
|
||||
roles: string[];
|
||||
/** 是否默认修改密码 */
|
||||
isDefaultModifyPwd: boolean;
|
||||
/** 密码是否过期 */
|
||||
isPasswordExpired: boolean;
|
||||
/** 用户信息 */
|
||||
user: {
|
||||
userId: number;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
email?: string;
|
||||
phonenumber?: string;
|
||||
sex?: string;
|
||||
avatar?: string;
|
||||
status?: string;
|
||||
createTime?: string;
|
||||
loginDate?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
// ============== 会员信息 ==============
|
||||
|
||||
/**
|
||||
* 会员单条记录
|
||||
*/
|
||||
export interface MembershipItemVO {
|
||||
membershipId: number | null;
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
expireTime: string | null;
|
||||
remainingDays: number;
|
||||
permanent: boolean;
|
||||
nextGrantTime: string | null;
|
||||
lastGrantTime: string | null;
|
||||
grantCycle: number;
|
||||
totalGranted: number;
|
||||
monthlyCredits: number;
|
||||
teamSeat: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户会员信息
|
||||
*/
|
||||
export interface UserMembershipVO {
|
||||
userId: number;
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
allowedModelCombinations: string[];
|
||||
description?: string;
|
||||
createdTime?: string;
|
||||
updatedTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多会员信息响应
|
||||
*/
|
||||
export interface MultiMembershipVO extends UserMembershipVO {
|
||||
displayTier?: MembershipItemVO;
|
||||
allMemberships?: MembershipItemVO[];
|
||||
totalMonthlyCredits?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员信息响应
|
||||
* GET /strangeloop/api/membership/current
|
||||
*/
|
||||
export interface MembershipResponse {
|
||||
code: number;
|
||||
msg?: string;
|
||||
message?: string;
|
||||
data?: MultiMembershipVO;
|
||||
}
|
||||
|
||||
// ============== 辅助类型 ==============
|
||||
|
||||
/** 后端工具名称 */
|
||||
@ -319,6 +486,7 @@ export type ToolName =
|
||||
| "file_delete"
|
||||
| "file_list"
|
||||
| "syntax_check"
|
||||
| "iverilog"
|
||||
| "simulation"
|
||||
| "waveform_summary"
|
||||
| "waveform_trace"
|
||||
@ -353,11 +521,21 @@ export interface SyntaxCheckArgs {
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** iverilog 工具参数 */
|
||||
export interface IverilogArgs {
|
||||
args: string;
|
||||
workDir?: string;
|
||||
}
|
||||
|
||||
/** simulation 工具参数 */
|
||||
export interface SimulationArgs {
|
||||
rtlPath: string;
|
||||
tbPath: string;
|
||||
duration?: string;
|
||||
/** 要dump的模块列表,格式:name:path,name:path */
|
||||
dumpModules?: string;
|
||||
/** VCD输出目录,默认'vcd' */
|
||||
vcdDir?: string;
|
||||
}
|
||||
|
||||
/** waveform_summary 工具参数 */
|
||||
@ -397,6 +575,7 @@ export type ToolArgs =
|
||||
| FileDeleteArgs
|
||||
| FileListArgs
|
||||
| SyntaxCheckArgs
|
||||
| IverilogArgs
|
||||
| SimulationArgs
|
||||
| WaveformSummaryArgs
|
||||
| WaveformTraceArgs
|
||||
|
||||
@ -715,6 +715,10 @@ export class ChatHistoryManager {
|
||||
|
||||
if (!projectPath) {
|
||||
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
||||
// 通知用户压缩数据保存失败
|
||||
vscode.window.showWarningMessage(
|
||||
'对话历史压缩数据保存失败:无法确定项目路径。后端重启后可能无法恢复完整对话历史。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -731,6 +735,19 @@ export class ChatHistoryManager {
|
||||
// 文件不存在,使用空数组
|
||||
}
|
||||
|
||||
// 版本检查:防止旧版本覆盖新版本(从尾部扫描,与加载逻辑一致)
|
||||
let existingSummary: CompactionSummaryMessage | null = null;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||
existingSummary = messages[i] as CompactionSummaryMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existingSummary && existingSummary.version >= compacted.version) {
|
||||
console.log(`[ChatHistoryManager] 跳过旧版本压缩数据: 现有版本=${existingSummary.version}, 新版本=${compacted.version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建压缩摘要消息
|
||||
const summaryMessage: CompactionSummaryMessage = {
|
||||
type: MessageType.COMPACTION_SUMMARY,
|
||||
@ -893,4 +910,14 @@ export class ChatHistoryManager {
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(工具执行结果)
|
||||
*/
|
||||
public trackToolResult(toolName: string, result: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'TOOL_RESULT',
|
||||
content: `[${toolName}] ${result}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 : '未知错误'}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
101
src/utils/jwtUtils.ts
Normal file
101
src/utils/jwtUtils.ts
Normal file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测 JWT token 是否已过期
|
||||
* @param token JWT token
|
||||
* @param bufferSeconds 提前多少秒判定为过期(默认60秒)
|
||||
* @returns true 表示已过期,false 表示未过期,null 表示无法判断
|
||||
*/
|
||||
export function isTokenExpired(token: string, bufferSeconds: number = 60): boolean | null {
|
||||
const payload = parseJwtPayload(token);
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.exp === undefined) {
|
||||
console.warn('[JWT] payload 中没有 exp 字段,无法判断过期');
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const expTime = payload.exp - bufferSeconds;
|
||||
const isExpired = now >= expTime;
|
||||
|
||||
if (isExpired) {
|
||||
console.warn('[JWT] token 已过期,exp:', payload.exp, '当前:', now);
|
||||
}
|
||||
|
||||
return isExpired;
|
||||
}
|
||||
@ -18,6 +18,11 @@ import { ChatHistoryManager } from "./chatHistoryManager";
|
||||
import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
import { healthCheck } from "../services/apiClient";
|
||||
import {
|
||||
checkBalanceBeforeSend,
|
||||
fetchBalance,
|
||||
} from "../services/creditsService";
|
||||
import { optimizePrompt } from "../services/promptOptimizeService";
|
||||
|
||||
import type { RunMode, ServiceTier } from "../types/api";
|
||||
|
||||
@ -30,27 +35,6 @@ let currentSession: DialogSession | null = null;
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||
let pendingPlanExecution: {
|
||||
panel: vscode.WebviewPanel;
|
||||
planTitle: string;
|
||||
extensionPath: string;
|
||||
taskId: string; // 保存 taskId 以便复用
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* 设置待执行的计划(由 ICHelperPanel 调用)
|
||||
*/
|
||||
export function setPendingPlanExecution(
|
||||
panel: vscode.WebviewPanel,
|
||||
planTitle: string,
|
||||
extensionPath: string,
|
||||
taskId: string
|
||||
): void {
|
||||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
||||
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户消息
|
||||
*/
|
||||
@ -88,10 +72,40 @@ export async function handleUserMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送前检测余额
|
||||
const balanceCheck = await checkBalanceBeforeSend();
|
||||
if (!balanceCheck.allowed) {
|
||||
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
|
||||
// 显示错误提示
|
||||
const selection = await vscode.window.showWarningMessage(
|
||||
balanceCheck.message || "资源点余额不足",
|
||||
"去充值"
|
||||
);
|
||||
if (selection === "去充值") {
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse("https://iccoder.com/memberCenter")
|
||||
);
|
||||
}
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试使用后端服务
|
||||
if (useBackendService && extensionPath) {
|
||||
try {
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier);
|
||||
await handleUserMessageWithBackend(
|
||||
panel,
|
||||
text,
|
||||
extensionPath,
|
||||
mode,
|
||||
undefined,
|
||||
serviceTier
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("后端服务不可用:", error);
|
||||
@ -135,13 +149,19 @@ async function handleUserMessageWithBackend(
|
||||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
|
||||
// 创建会话(dialogManager 会自动处理旧会话的中止)
|
||||
currentSession = dialogManager.createSession(
|
||||
extensionPath,
|
||||
taskIdToUse || undefined
|
||||
);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
|
||||
}
|
||||
console.log(
|
||||
"[MessageHandler] 创建会话: taskId=",
|
||||
lastTaskId,
|
||||
"来源=",
|
||||
taskIdToUse ? "historyManager" : "新生成"
|
||||
);
|
||||
|
||||
// 显示状态栏
|
||||
panel.webview.postMessage({
|
||||
@ -201,6 +221,17 @@ async function handleUserMessageWithBackend(
|
||||
// 最后一次发送完整的段落
|
||||
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
||||
|
||||
// 对话完成后重新获取余额(因为已经消耗了 Credits)
|
||||
try {
|
||||
console.log("[MessageHandler] 对话完成,重新获取余额...");
|
||||
const newBalance = await fetchBalance();
|
||||
if (newBalance !== null) {
|
||||
console.log("[MessageHandler] 余额已更新:", newBalance);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[MessageHandler] 获取余额失败:", error);
|
||||
}
|
||||
|
||||
const result = await panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
@ -222,39 +253,6 @@ async function handleUserMessageWithBackend(
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||||
if (pendingPlanExecution) {
|
||||
const {
|
||||
panel: execPanel,
|
||||
planTitle,
|
||||
extensionPath: execPath,
|
||||
taskId: reuseTaskId,
|
||||
} = pendingPlanExecution;
|
||||
pendingPlanExecution = null;
|
||||
console.log(
|
||||
"[MessageHandler] 自动执行计划:",
|
||||
planTitle,
|
||||
"复用 taskId:",
|
||||
reuseTaskId
|
||||
);
|
||||
|
||||
// 延迟一小段时间确保当前对话完全结束
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||||
await handleUserMessageWithBackend(
|
||||
execPanel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
execPath,
|
||||
"agent",
|
||||
reuseTaskId // 复用 Plan 模式的 taskId
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[MessageHandler] 自动执行计划失败:", err);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
|
||||
@ -288,6 +286,36 @@ async function handleUserMessageWithBackend(
|
||||
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,
|
||||
serviceTier // 传递服务等级
|
||||
@ -370,9 +398,17 @@ export async function handlePlanAction(
|
||||
panel: vscode.WebviewPanel,
|
||||
action: string,
|
||||
planTitle: string,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
serviceTier?: ServiceTier
|
||||
): Promise<void> {
|
||||
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle);
|
||||
console.log(
|
||||
"[handlePlanAction] action:",
|
||||
action,
|
||||
"planTitle:",
|
||||
planTitle,
|
||||
"serviceTier:",
|
||||
serviceTier
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
case "confirm":
|
||||
@ -386,7 +422,8 @@ export async function handlePlanAction(
|
||||
panel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
extensionPath,
|
||||
"agent"
|
||||
"agent",
|
||||
serviceTier
|
||||
);
|
||||
break;
|
||||
|
||||
@ -402,7 +439,8 @@ export async function handlePlanAction(
|
||||
panel,
|
||||
`请根据以下建议修改计划:${modification}`,
|
||||
extensionPath,
|
||||
"plan"
|
||||
"plan",
|
||||
serviceTier
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -994,3 +1032,35 @@ async function handleVCDGeneration(
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提示词优化请求
|
||||
*/
|
||||
export async function handleOptimizePrompt(
|
||||
panel: vscode.WebviewPanel,
|
||||
prompt: string
|
||||
): Promise<void> {
|
||||
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
|
||||
console.log("[MessageHandler] prompt:", prompt);
|
||||
console.log("[MessageHandler] prompt 长度:", prompt?.length);
|
||||
|
||||
try {
|
||||
console.log("[MessageHandler] 开始调用 optimizePrompt...");
|
||||
const optimized = await optimizePrompt(prompt);
|
||||
console.log("[MessageHandler] 优化成功,结果:", optimized);
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: true,
|
||||
optimizedPrompt: optimized,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : "优化失败";
|
||||
console.error("[MessageHandler] 提示词优化失败:", errorMsg);
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: errorMsg,
|
||||
});
|
||||
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import * as vscode from "vscode";
|
||||
import { getWebviewContent } from "./webviewContent";
|
||||
import { isTokenExpired } from "../utils/jwtUtils";
|
||||
import {
|
||||
handleUserMessage,
|
||||
insertCodeToEditor,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
} from "../utils/messageHandler";
|
||||
|
||||
/**
|
||||
@ -69,6 +71,9 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
console.log("[ICViewProvider] ====== 收到 WebView 消息 ======");
|
||||
console.log("[ICViewProvider] command:", message.command);
|
||||
console.log("[ICViewProvider] 完整消息:", JSON.stringify(message));
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
||||
@ -116,6 +121,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
case "abortDialog":
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
// 新增:优化提示词
|
||||
case "optimizePrompt":
|
||||
handleOptimizePrompt(panel, message.prompt);
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
@ -127,10 +136,34 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
* 侧边栏视图提供者
|
||||
*/
|
||||
export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
private _view?: vscode.WebviewView;
|
||||
|
||||
constructor(
|
||||
private readonly extensionUri: vscode.Uri,
|
||||
private readonly context: vscode.ExtensionContext
|
||||
) {}
|
||||
) {
|
||||
// 监听认证状态变化
|
||||
this.context.subscriptions.push(
|
||||
vscode.authentication.onDidChangeSessions((e) => {
|
||||
if (e.provider.id === "iccoder") {
|
||||
this.refreshLoginStatus();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新登录状态并更新视图
|
||||
*/
|
||||
private async refreshLoginStatus(): Promise<void> {
|
||||
if (this._view) {
|
||||
const isLoggedIn = await this.checkLoginStatus();
|
||||
this._view.webview.html = this.getWebviewContent(
|
||||
this._view.webview,
|
||||
isLoggedIn
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态(使用 Authentication API)
|
||||
@ -138,14 +171,29 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
private async checkLoginStatus(): Promise<boolean> {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
return !!session;
|
||||
console.log("[ICViewProvider] 检查登录状态, session:", session ? "存在" : "不存在");
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
// 检查 token 是否过期
|
||||
const expired = isTokenExpired(session.accessToken);
|
||||
console.log("[ICViewProvider] token 过期检查结果:", expired);
|
||||
// 只有明确过期才认为未登录,无法判断时认为已登录
|
||||
if (expired === true) {
|
||||
console.log("[ICViewProvider] Token 已过期");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("检查登录状态失败:", error);
|
||||
console.log("[ICViewProvider] 检查登录状态失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||
// 保存引用以便后续刷新
|
||||
this._view = webviewView;
|
||||
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
|
||||
|
||||
@ -98,24 +98,24 @@ export function getAgentCardStyles(): string {
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
.agent-step.low-profile {
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
opacity: 0.85;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.agent-step.low-profile .step-icon {
|
||||
opacity: 0.4;
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.agent-step.low-profile .step-name {
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.7;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.agent-step.low-profile .step-result {
|
||||
opacity: 0.6;
|
||||
font-size: 9px;
|
||||
opacity: 0.85;
|
||||
font-size: 11px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -14,9 +14,7 @@ export function getContextButtonContent(): string {
|
||||
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
|
||||
</svg>
|
||||
<span class="add-context-label">添加上下文</span>
|
||||
<svg class="dropdown-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 714.666667L213.333333 416l42.666667-42.666667L512 629.333333l256-256 42.666667 42.666667z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</button>
|
||||
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
import {
|
||||
getUserInfoComponentContent,
|
||||
getUserInfoComponentStyles,
|
||||
getUserInfoComponentScript,
|
||||
} from "./userInfoComponent";
|
||||
import { userAvatarIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取会话历史栏的 HTML 内容
|
||||
*/
|
||||
@ -6,7 +13,7 @@ export function getConversationHistoryBarContent(): string {
|
||||
<div class="conversation-history-bar">
|
||||
<div class="history-dropdown-container">
|
||||
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
|
||||
<span class="dropdown-label">Past Conversations</span>
|
||||
<span class="dropdown-label">历史对话</span>
|
||||
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
|
||||
</svg>
|
||||
@ -19,11 +26,20 @@ export function getConversationHistoryBarContent(): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="user-info-container">
|
||||
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
|
||||
${userAvatarIconSvg}
|
||||
</button>
|
||||
${getUserInfoComponentContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -49,13 +65,59 @@ export function getConversationHistoryBarStyles(): string {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-info-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-avatar-icon-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar-icon-button:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button.active {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
${getUserInfoComponentStyles()}
|
||||
|
||||
.history-dropdown-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-input-foreground);
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@ -64,7 +126,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.history-dropdown-button:hover {
|
||||
opacity: 0.8;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
@ -163,7 +225,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -173,11 +235,12 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button:hover {
|
||||
opacity: 0.7;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.new-conversation-button:active {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.new-conversation-button svg {
|
||||
@ -210,6 +273,29 @@ export function getConversationHistoryBarStyles(): string {
|
||||
*/
|
||||
export function getConversationHistoryBarScript(): string {
|
||||
return `
|
||||
${getUserInfoComponentScript()}
|
||||
|
||||
// 更新用户头像图标按钮显示
|
||||
function updateUserAvatarIconButton(userInfo) {
|
||||
const userAvatarIconButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (userInfo && userInfo.nickname) {
|
||||
// 显示用户头像图标按钮
|
||||
if (userAvatarIconButton) {
|
||||
userAvatarIconButton.style.display = 'flex';
|
||||
}
|
||||
// 同时更新用户详情弹窗的数据
|
||||
if (typeof updateUserInfoDisplay === 'function') {
|
||||
updateUserInfoDisplay(userInfo);
|
||||
}
|
||||
} else {
|
||||
// 隐藏用户头像图标按钮
|
||||
if (userAvatarIconButton) {
|
||||
userAvatarIconButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 会话历史相关变量
|
||||
let conversationHistory = [];
|
||||
let currentConversationId = null;
|
||||
@ -217,6 +303,7 @@ export function getConversationHistoryBarScript(): string {
|
||||
let totalHistory = 0;
|
||||
let hasMoreHistory = false;
|
||||
let isLoadingHistory = false;
|
||||
let currentLoadRequestId = 0; // 请求 ID,用于防止并发加载
|
||||
const HISTORY_PAGE_SIZE = 10;
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
@ -260,11 +347,15 @@ export function getConversationHistoryBarScript(): string {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成新的请求 ID,用于防止并发加载
|
||||
const requestId = ++currentLoadRequestId;
|
||||
|
||||
isLoadingHistory = true;
|
||||
vscode.postMessage({
|
||||
command: 'loadConversationHistory',
|
||||
offset: currentOffset,
|
||||
limit: HISTORY_PAGE_SIZE
|
||||
limit: HISTORY_PAGE_SIZE,
|
||||
requestId: requestId
|
||||
});
|
||||
}
|
||||
|
||||
@ -276,11 +367,19 @@ export function getConversationHistoryBarScript(): string {
|
||||
return;
|
||||
}
|
||||
|
||||
// 追加新数据
|
||||
conversationHistory = conversationHistory.concat(data.items);
|
||||
// 追加新数据(去重)
|
||||
const existingIds = new Set(conversationHistory.map(item => item.id));
|
||||
const newItems = [];
|
||||
for (const item of data.items) {
|
||||
if (!existingIds.has(item.id)) {
|
||||
existingIds.add(item.id);
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
conversationHistory = conversationHistory.concat(newItems);
|
||||
totalHistory = data.total;
|
||||
hasMoreHistory = data.hasMore;
|
||||
currentOffset += data.items.length;
|
||||
currentOffset = conversationHistory.length;
|
||||
|
||||
const historyList = document.getElementById('historyList');
|
||||
if (!historyList) {
|
||||
@ -368,9 +467,10 @@ export function getConversationHistoryBarScript(): string {
|
||||
});
|
||||
}
|
||||
|
||||
// 监听下拉菜单滚动事件
|
||||
// 监听下拉菜单滚动事件(防止重复注册)
|
||||
const historyDropdownMenu = document.getElementById('historyDropdownMenu');
|
||||
if (historyDropdownMenu) {
|
||||
if (historyDropdownMenu && !historyDropdownMenu._scrollListenerAdded) {
|
||||
historyDropdownMenu._scrollListenerAdded = true;
|
||||
historyDropdownMenu.addEventListener('scroll', () => {
|
||||
const menu = historyDropdownMenu;
|
||||
const scrollTop = menu.scrollTop;
|
||||
|
||||
216
src/views/exampleShowcase.ts
Normal file
216
src/views/exampleShowcase.ts
Normal file
@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 获取展示区域的 HTML 内容
|
||||
*/
|
||||
export function getExampleShowcaseContent(): string {
|
||||
return `
|
||||
<div class="example-showcase" id="exampleShowcase">
|
||||
<div class="showcase-title">展示</div>
|
||||
<div class="example-cards">
|
||||
<div class="example-card" onclick="fillExample(0)">
|
||||
<div class="example-icon">📝</div>
|
||||
<div class="example-content">
|
||||
<div class="example-title">代码生成</div>
|
||||
<div class="example-desc">生成一个 8 位全加器的 Verilog 代码</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-card" onclick="fillExample(1)">
|
||||
<div class="example-icon">🔍</div>
|
||||
<div class="example-content">
|
||||
<div class="example-title">代码分析</div>
|
||||
<div class="example-desc">分析当前项目中的时序逻辑设计</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="web-link">
|
||||
<a href="https://iccoder.com" target="_blank" class="web-link-button">
|
||||
<span class="link-icon">🌐</span>
|
||||
<span>IC Coder Web端</span>
|
||||
<span class="link-arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取展示区域的样式
|
||||
*/
|
||||
export function getExampleShowcaseStyles(): string {
|
||||
return `
|
||||
.example-showcase {
|
||||
margin-top: 24px;
|
||||
padding: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.example-showcase.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.showcase-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin-bottom: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.example-cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.example-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.example-card:hover {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.example-icon {
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.example-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.example-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.example-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.web-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.web-link-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.web-link-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.web-link-button:hover {
|
||||
transform: translateY(-1px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.link-arrow {
|
||||
font-size: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.web-link-button:hover .link-arrow {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取展示区域的脚本
|
||||
*/
|
||||
export function getExampleShowcaseScript(): string {
|
||||
return `
|
||||
// 示例文本数组
|
||||
const exampleTexts = [
|
||||
'生成一个 8 位全加器的 Verilog 代码',
|
||||
'分析当前项目中的时序逻辑设计'
|
||||
];
|
||||
|
||||
// 填充示例到输入框
|
||||
function fillExample(index) {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
if (messageInput && exampleTexts[index]) {
|
||||
messageInput.value = exampleTexts[index];
|
||||
messageInput.focus();
|
||||
// 触发自动调整高度
|
||||
if (typeof autoResizeTextarea === 'function') {
|
||||
autoResizeTextarea();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听消息变化,自动隐藏/显示展示区域
|
||||
function updateShowcaseVisibility() {
|
||||
const showcase = document.getElementById('exampleShowcase');
|
||||
if (showcase) {
|
||||
if (hasMessages) {
|
||||
showcase.classList.add('hidden');
|
||||
} else {
|
||||
showcase.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展原有的布局更新函数
|
||||
const originalUpdateInputAreaLayout = updateInputAreaLayout;
|
||||
updateInputAreaLayout = function() {
|
||||
if (originalUpdateInputAreaLayout) {
|
||||
originalUpdateInputAreaLayout();
|
||||
}
|
||||
updateShowcaseVisibility();
|
||||
};
|
||||
`;
|
||||
}
|
||||
@ -29,16 +29,21 @@ import {
|
||||
getOptimizeButtonStyles,
|
||||
getOptimizeButtonScript,
|
||||
} from "./optimizeButton";
|
||||
import {
|
||||
getExampleShowcaseContent,
|
||||
getExampleShowcaseStyles,
|
||||
getExampleShowcaseScript,
|
||||
} from "./exampleShowcase";
|
||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取输入区域的 HTML 内容
|
||||
*/
|
||||
export function getInputAreaContent(
|
||||
autoIcon: string = '',
|
||||
liteIcon: string = '',
|
||||
syIcon: string = '',
|
||||
maxIcon: string = ''
|
||||
autoIcon: string = "",
|
||||
liteIcon: string = "",
|
||||
syIcon: string = "",
|
||||
maxIcon: string = ""
|
||||
): string {
|
||||
return `
|
||||
<div class="input-area centered" id="inputArea">
|
||||
@ -71,6 +76,8 @@ export function getInputAreaContent(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 展示区域:案例和 Web 端链接 -->
|
||||
${getExampleShowcaseContent()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -86,6 +93,7 @@ export function getInputAreaStyles(): string {
|
||||
${getContextDisplayStyles()}
|
||||
${getContextCompressStyles()}
|
||||
${getOptimizeButtonStyles()}
|
||||
${getExampleShowcaseStyles()}
|
||||
.input-area {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
padding-top: 15px;
|
||||
@ -95,7 +103,7 @@ export function getInputAreaStyles(): string {
|
||||
/* 居中模式:未发起对话时 */
|
||||
.input-area.centered {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
top: 55%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% - 40px);
|
||||
@ -292,6 +300,7 @@ export function getInputAreaScript(): string {
|
||||
${getContextDisplayScript()}
|
||||
${getContextCompressScript()}
|
||||
${getOptimizeButtonScript()}
|
||||
${getExampleShowcaseScript()}
|
||||
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
userQuestionIconSvg,
|
||||
updateStageIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
import {
|
||||
getWaveformPreviewContent,
|
||||
@ -670,11 +671,35 @@ export function getMessageAreaScript(): string {
|
||||
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
||||
const updateStageIconSvg = \`${updateStageIconSvg}\`;
|
||||
|
||||
${getAgentCardScript()}
|
||||
|
||||
${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) {
|
||||
const iconMap = {
|
||||
@ -701,6 +726,7 @@ export function getMessageAreaScript(): string {
|
||||
'updateNode': fileWriteIconSvg,
|
||||
'addStateTransition': stateTransitionIconSvg,
|
||||
'askUser': userQuestionIconSvg,
|
||||
'updatePhase': updateStageIconSvg,
|
||||
};
|
||||
return iconMap[toolName] || '';
|
||||
}
|
||||
@ -733,20 +759,45 @@ export function getMessageAreaScript(): string {
|
||||
'spawnExplorer': '代码探索',
|
||||
'spawnDebugger': '波形调试',
|
||||
'askUser': '用户提问',
|
||||
'updatePhase': '已更新阶段',
|
||||
'iverilog': '已完成编译',
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
// 自动滚动控制标志
|
||||
let shouldAutoScroll = true;
|
||||
let lastScrollHeight = 0;
|
||||
|
||||
// 检查用户是否在底部附近(允许50px的误差)
|
||||
function isUserNearBottom() {
|
||||
const threshold = 50;
|
||||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||
}
|
||||
|
||||
// 智能滚动:只有用户在底部附近时才自动滚动
|
||||
// 监听用户滚动行为
|
||||
messagesEl.addEventListener('scroll', () => {
|
||||
const isAtBottom = isUserNearBottom();
|
||||
|
||||
// 如果用户滚动到底部,恢复自动滚动
|
||||
if (isAtBottom) {
|
||||
shouldAutoScroll = true;
|
||||
} else {
|
||||
// 只有当内容高度没有变化时,才认为是用户主动滚动
|
||||
// 如果内容高度变化了,说明是因为新内容导致的位置变化,不应该停止自动滚动
|
||||
if (messagesEl.scrollHeight === lastScrollHeight) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
});
|
||||
|
||||
// 智能滚动:只有在允许自动滚动时才滚动到底部
|
||||
function smartScrollToBottom() {
|
||||
if (isUserNearBottom()) {
|
||||
if (shouldAutoScroll) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1034,7 +1085,17 @@ export function getMessageAreaScript(): string {
|
||||
|
||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
// 优先使用显式提供的路径,否则从结果文本中解析
|
||||
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
|
||||
if (vcdPaths.length > 0) {
|
||||
// 多 VCD 模式:为每个文件创建预览
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
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*(.+)/);
|
||||
@ -1049,6 +1110,7 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加折叠/展开事件监听
|
||||
if (shouldCollapse) {
|
||||
@ -1281,7 +1343,17 @@ export function getMessageAreaScript(): string {
|
||||
|
||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
// 优先使用显式提供的路径,否则从结果文本中解析
|
||||
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
|
||||
if (vcdPaths.length > 0) {
|
||||
// 多 VCD 模式:为每个文件创建预览
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
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*(.+)/);
|
||||
@ -1296,6 +1368,7 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加折叠/展开事件监听
|
||||
if (shouldCollapse) {
|
||||
|
||||
@ -60,35 +60,97 @@ export function getOptimizeButtonScript(): string {
|
||||
return `
|
||||
let isOptimized = false; // 标记是否已优化
|
||||
let originalText = ''; // 保存原始文本用于撤回
|
||||
let isOptimizing = false; // 标记是否正在优化中
|
||||
|
||||
function handleOptimize() {
|
||||
console.log('[Optimize] handleOptimize 被调用');
|
||||
console.log('[Optimize] isOptimizing:', isOptimizing);
|
||||
console.log('[Optimize] isOptimized:', isOptimized);
|
||||
console.log('[Optimize] messageInput:', messageInput);
|
||||
|
||||
if (isOptimizing) {
|
||||
console.log('[Optimize] 正在优化中,忽略点击');
|
||||
return; // 正在优化中,忽略点击
|
||||
}
|
||||
|
||||
if (isOptimized) {
|
||||
// 撤回操作
|
||||
console.log('[Optimize] 执行撤回操作');
|
||||
messageInput.value = originalText;
|
||||
resetOptimizeButton();
|
||||
} else {
|
||||
// 优化操作
|
||||
const currentText = messageInput.value.trim();
|
||||
console.log('[Optimize] 当前输入内容:', currentText);
|
||||
console.log('[Optimize] 内容长度:', currentText.length);
|
||||
|
||||
if (!currentText) {
|
||||
console.log('[Optimize] 输入框为空,不执行优化');
|
||||
return; // 输入框为空,不执行优化
|
||||
}
|
||||
|
||||
originalText = messageInput.value; // 保存原始文本
|
||||
isOptimizing = true;
|
||||
console.log('[Optimize] 开始优化,显示加载状态');
|
||||
|
||||
// 使用死数据替换输入框内容
|
||||
const optimizedTexts = [
|
||||
'请帮我优化这段代码,提高性能和可读性',
|
||||
'请分析这个问题并给出最佳解决方案',
|
||||
'请帮我重构这段代码,使其更加简洁高效',
|
||||
'请检查代码中的潜在问题并提供改进建议'
|
||||
];
|
||||
const randomText = optimizedTexts[Math.floor(Math.random() * optimizedTexts.length)];
|
||||
messageInput.value = randomText;
|
||||
// 显示加载状态
|
||||
showOptimizeLoading();
|
||||
|
||||
// 切换到撤回状态
|
||||
isOptimized = true;
|
||||
updateOptimizeButton();
|
||||
// 发送优化请求到扩展
|
||||
console.log('[Optimize] 发送 optimizePrompt 消息');
|
||||
vscode.postMessage({
|
||||
command: 'optimizePrompt',
|
||||
prompt: currentText
|
||||
});
|
||||
console.log('[Optimize] postMessage 已发送');
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
autoResizeTextarea();
|
||||
}
|
||||
|
||||
// 处理优化结果
|
||||
function handleOptimizeResult(success, optimizedPrompt, error) {
|
||||
isOptimizing = false;
|
||||
hideOptimizeLoading();
|
||||
|
||||
if (success && optimizedPrompt) {
|
||||
messageInput.value = optimizedPrompt;
|
||||
isOptimized = true;
|
||||
updateOptimizeButton();
|
||||
} else {
|
||||
// 优化失败,恢复原始文本
|
||||
messageInput.value = originalText;
|
||||
console.error('优化失败:', error);
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
autoResizeTextarea();
|
||||
}
|
||||
|
||||
function showOptimizeLoading() {
|
||||
const optimizeButton = document.getElementById('optimizeButton');
|
||||
const optimizeIcon = document.getElementById('optimizeIcon');
|
||||
if (optimizeButton && optimizeIcon) {
|
||||
optimizeButton.disabled = true;
|
||||
optimizeButton.style.opacity = '0.5';
|
||||
// 显示加载动画
|
||||
optimizeIcon.innerHTML = '<circle cx="512" cy="512" r="400" fill="none" stroke="#409eff" stroke-width="60" stroke-dasharray="1200" stroke-dashoffset="0"><animateTransform attributeName="transform" type="rotate" from="0 512 512" to="360 512 512" dur="1s" repeatCount="indefinite"/></circle>';
|
||||
}
|
||||
}
|
||||
|
||||
function hideOptimizeLoading() {
|
||||
const optimizeButton = document.getElementById('optimizeButton');
|
||||
if (optimizeButton) {
|
||||
optimizeButton.disabled = false;
|
||||
optimizeButton.style.opacity = '1';
|
||||
}
|
||||
// 恢复图标会在 updateOptimizeButton 或 resetOptimizeButton 中处理
|
||||
if (!isOptimized) {
|
||||
resetOptimizeButton();
|
||||
}
|
||||
}
|
||||
|
||||
function updateOptimizeButton() {
|
||||
const optimizeIcon = document.getElementById('optimizeIcon');
|
||||
const optimizeTooltip = document.getElementById('optimizeTooltip');
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
* 功能说明:
|
||||
* - 显示执行计划的卡片界面
|
||||
* - 包含计划标题、摘要和步骤列表
|
||||
* - 摘要支持 Markdown 格式渲染
|
||||
* - 提供确认执行、修改计划、取消等操作按钮
|
||||
*/
|
||||
|
||||
@ -43,11 +44,62 @@ export function getPlanCardStyles(): string {
|
||||
padding: 16px;
|
||||
}
|
||||
.plan-summary {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
color: var(--vscode-foreground);
|
||||
margin-bottom: 12px;
|
||||
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: 0;
|
||||
}
|
||||
.plan-summary li { margin: 4px 0 4px 27px; }
|
||||
.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 {
|
||||
font-size: 13px;
|
||||
}
|
||||
@ -58,6 +110,15 @@ export function getPlanCardStyles(): string {
|
||||
border-radius: 4px;
|
||||
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 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -89,24 +150,50 @@ export function getPlanCardStyles(): string {
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-sideBar-background);
|
||||
}
|
||||
.plan-actions .question-options {
|
||||
.plan-input-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.plan-input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.plan-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
.plan-btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.plan-btn {
|
||||
padding: 8px 18px;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.plan-btn-submit {
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-submit:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.plan-btn-confirm {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
@ -114,41 +201,188 @@ export function getPlanCardStyles(): string {
|
||||
.plan-btn-confirm:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.plan-btn-modify {
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.plan-actions .custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.plan-actions .custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.plan-btn-cancel:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.plan-answered {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-sideBar-background);
|
||||
font-size: 13px;
|
||||
}
|
||||
.plan-actions .custom-submit {
|
||||
padding: 8px 18px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
.answered-label {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.answered-value {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
.plan-actions .custom-submit:hover {
|
||||
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 +392,200 @@ export function getPlanCardStyles(): string {
|
||||
*/
|
||||
export function getPlanCardScript(): string {
|
||||
return `
|
||||
// 简单的 Markdown 渲染函数
|
||||
function renderPlanMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
let html = text;
|
||||
|
||||
// 转义 HTML 特殊字符(保留换行)
|
||||
html = html.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 标题(必须在转义之后、其他处理之前)
|
||||
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>');
|
||||
|
||||
// 代码块 (\`\`\`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(/\\*\\*(.+?)\\*\\*/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 中使用)
|
||||
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
@ -170,16 +598,26 @@ export function getPlanCardScript(): string {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||
).join('');
|
||||
// 判断是否有 phases(新格式)还是 steps(旧格式)
|
||||
const hasPhases = segment.planPhases && segment.planPhases.length > 0;
|
||||
|
||||
// 选项按钮
|
||||
const options = ['确认执行', '修改计划', '取消'];
|
||||
const optionsHtml = options.map(opt => {
|
||||
const isSelected = isAnswered && opt === selectedAnswer;
|
||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||
}).join('');
|
||||
// 渲染阶段进度条和阶段列表(新格式)
|
||||
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
|
||||
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
|
||||
|
||||
// 兼容旧格式:渲染步骤列表
|
||||
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
|
||||
|
||||
// 渲染 Markdown 格式的摘要
|
||||
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
||||
|
||||
// 已回答时显示用户的选择
|
||||
const answeredHtml = isAnswered ? \`
|
||||
<div class="plan-answered">
|
||||
<span class="answered-label">已回复:</span>
|
||||
<span class="answered-value">\${selectedAnswer}</span>
|
||||
</div>
|
||||
\` : '';
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
@ -187,62 +625,77 @@ export function getPlanCardScript(): string {
|
||||
<span class="plan-icon">${plannerIconSvg}</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
\${progressHtml}
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||
<div class="plan-steps">\${stepsHtml}</div>
|
||||
<div class="plan-summary">\${summaryHtml}</div>
|
||||
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
|
||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<input type="text" class="custom-input" placeholder="输入修改建议..." />
|
||||
<button class="custom-submit">提交</button>
|
||||
<div class="plan-actions" data-ask-id="\${segment.askId}" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<div class="plan-input-row">
|
||||
<input type="text" class="plan-input" placeholder="输入修改建议..." />
|
||||
<button class="plan-btn plan-btn-submit">提交修改</button>
|
||||
</div>
|
||||
<div class="plan-btn-row">
|
||||
<button class="plan-btn plan-btn-confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
\${answeredHtml}
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 只在未回答时添加事件监听
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||||
optionButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const option = this.getAttribute('data-option');
|
||||
// 发送答案到后端
|
||||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||||
// 同时发送 planAction 用于模式切换
|
||||
const actionMap = {
|
||||
'确认执行': 'confirm',
|
||||
'修改计划': 'modify',
|
||||
'取消': 'cancel'
|
||||
};
|
||||
vscode.postMessage({
|
||||
command: 'planAction',
|
||||
action: actionMap[option] || option,
|
||||
planTitle: segment.planTitle
|
||||
});
|
||||
});
|
||||
});
|
||||
const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
|
||||
const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
|
||||
const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
|
||||
const planInput = segmentDiv.querySelector('.plan-input');
|
||||
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
const customInput = segmentDiv.querySelector('.custom-input');
|
||||
if (submitBtn && customInput) {
|
||||
// 提交修改按钮
|
||||
if (submitBtn && planInput) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
|
||||
}
|
||||
});
|
||||
|
||||
customInput.addEventListener('keypress', function(e) {
|
||||
// 回车键提交修改
|
||||
planInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认执行按钮
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
handleQuestionAnswerInSegment(segment.askId, '确认执行', segmentDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// 取消按钮 - 直接中止对话,不发送给智能体
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
// 标记问题已回答
|
||||
answeredQuestions.set(segment.askId, '取消');
|
||||
segmentDiv.classList.add('answered');
|
||||
|
||||
// 隐藏操作按钮
|
||||
const actionsDiv = segmentDiv.querySelector('.plan-actions');
|
||||
if (actionsDiv) {
|
||||
actionsDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// 发送中止对话命令
|
||||
vscode.postMessage({ command: 'abortDialog' });
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@ -250,9 +703,19 @@ export function getPlanCardScript(): string {
|
||||
// 渲染计划卡片(在 renderSegments 中使用)
|
||||
function renderPlanCardStatic(segment, segmentDiv) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||
).join('');
|
||||
|
||||
// 判断是否有 phases(新格式)还是 steps(旧格式)
|
||||
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 = \`
|
||||
<div class="plan-card">
|
||||
@ -260,31 +723,68 @@ export function getPlanCardScript(): string {
|
||||
<span class="plan-icon">📋</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
\${progressHtml}
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||
<div class="plan-steps">\${stepsHtml}</div>
|
||||
<div class="plan-summary">\${summaryHtml}</div>
|
||||
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
||||
</div>
|
||||
<div class="plan-actions" data-ask-id="\${segment.askId}">
|
||||
<div class="plan-input-row">
|
||||
<input type="text" class="plan-input" placeholder="输入修改建议..." />
|
||||
<button class="plan-btn plan-btn-submit">提交修改</button>
|
||||
</div>
|
||||
<div class="plan-btn-row">
|
||||
<button class="plan-btn plan-btn-confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-cancel">取消</button>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
|
||||
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 绑定按钮事件
|
||||
// 绑定按钮事件(静态渲染时也需要能响应)
|
||||
setTimeout(() => {
|
||||
const planCard = segmentDiv.querySelector('.plan-card');
|
||||
if (planCard) {
|
||||
planCard.querySelectorAll('.plan-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const action = e.currentTarget?.dataset?.action;
|
||||
const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
|
||||
const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
|
||||
const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
|
||||
const planInput = segmentDiv.querySelector('.plan-input');
|
||||
|
||||
// 提交修改按钮
|
||||
if (submitBtn && planInput) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
vscode.postMessage({
|
||||
command: 'planAction',
|
||||
action: action,
|
||||
planTitle: segment.planTitle
|
||||
command: 'submitAnswer',
|
||||
askId: segment.askId,
|
||||
selected: [inputValue],
|
||||
customInput: inputValue
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认执行按钮
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: segment.askId,
|
||||
selected: ['确认执行'],
|
||||
customInput: '确认执行'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 取消按钮 - 直接中止对话
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
// 隐藏操作按钮
|
||||
const actionsDiv = segmentDiv.querySelector('.plan-actions');
|
||||
if (actionsDiv) {
|
||||
actionsDiv.style.display = 'none';
|
||||
}
|
||||
// 发送中止对话命令
|
||||
vscode.postMessage({ command: 'abortDialog' });
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
|
||||
@ -25,7 +25,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Spec设计文档</div>
|
||||
<div class="step-label">Spec</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
@ -35,7 +35,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">2</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Design代码编写</div>
|
||||
<div class="step-label">Design</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
@ -45,7 +45,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">3</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Sim仿真检查</div>
|
||||
<div class="step-label">Simulation</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
@ -55,7 +55,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">4</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Done完成</div>
|
||||
<div class="step-label">Done</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
300
src/views/userInfoComponent.ts
Normal file
300
src/views/userInfoComponent.ts
Normal file
@ -0,0 +1,300 @@
|
||||
/**
|
||||
* 用户信息组件
|
||||
* 包含用户头像、昵称、会员等级等信息
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取用户信息组件的 HTML 内容
|
||||
* 只包含用户详情下拉面板,不包含触发按钮
|
||||
*/
|
||||
export function getUserInfoComponentContent(): string {
|
||||
return `
|
||||
<div class="user-info-wrapper">
|
||||
<!-- 用户详情下拉面板 -->
|
||||
<div class="user-detail-dropdown" id="userDetailDropdown">
|
||||
<div class="user-detail-content">
|
||||
<div class="user-detail-header">
|
||||
<div class="user-avatar-small">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-name-tier">
|
||||
<div class="user-detail-name" id="userDetailName">加载中...</div>
|
||||
<img class="tier-icon-inline" id="tierIconInline" style="display: none;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-detail-body">
|
||||
<div class="user-detail-item">
|
||||
<span class="detail-label">剩余 Credits</span>
|
||||
<span class="detail-value" id="creditsDetail">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息组件的 CSS 样式
|
||||
*/
|
||||
export function getUserInfoComponentStyles(): string {
|
||||
return `
|
||||
.user-info-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 用户详情下拉面板 */
|
||||
.user-detail-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
min-width: 250px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.user-detail-dropdown.active {
|
||||
display: block;
|
||||
animation: dropdownSlideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.user-detail-content {
|
||||
background: var(--vscode-sideBar-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-detail-header {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: linear-gradient(135deg, rgba(0, 122, 204, 0.1) 0%, rgba(88, 166, 255, 0.05) 100%);
|
||||
border-bottom: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #007acc 0%, #58a6ff 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.user-avatar-small svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.user-name-tier {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-detail-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.tier-icon-inline {
|
||||
height: 26px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.user-detail-body {
|
||||
padding: 12px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
}
|
||||
|
||||
.user-detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--vscode-editor-background);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-detail-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-color: rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.user-detail-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tier-icon-large {
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.tier-icon {
|
||||
width: 110px;
|
||||
height: 35px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息组件的 JavaScript 脚本
|
||||
*/
|
||||
export function getUserInfoComponentScript(): string {
|
||||
return `
|
||||
// 用户信息数据
|
||||
let currentUserInfo = null;
|
||||
|
||||
// 切换用户详情下拉面板
|
||||
function openUserDetailModal() {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
const userButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (dropdown) {
|
||||
const isActive = dropdown.classList.contains('active');
|
||||
if (isActive) {
|
||||
dropdown.classList.remove('active');
|
||||
if (userButton) {
|
||||
userButton.classList.remove('active');
|
||||
}
|
||||
} else {
|
||||
dropdown.classList.add('active');
|
||||
if (userButton) {
|
||||
userButton.classList.add('active');
|
||||
}
|
||||
// 更新下拉面板中的用户信息
|
||||
updateUserDetailModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭用户详情下拉面板
|
||||
function closeUserDetailModal() {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
const userButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('active');
|
||||
}
|
||||
if (userButton) {
|
||||
userButton.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户详情下拉面板内容
|
||||
function updateUserDetailModal() {
|
||||
if (!currentUserInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新用户名
|
||||
const userDetailName = document.getElementById('userDetailName');
|
||||
if (userDetailName) {
|
||||
userDetailName.textContent = currentUserInfo.nickname || '未知用户';
|
||||
}
|
||||
|
||||
// 更新会员等级图标(显示在用户名旁边)
|
||||
const tierIconInline = document.getElementById('tierIconInline');
|
||||
if (tierIconInline && currentUserInfo.tierIconUrl) {
|
||||
tierIconInline.src = currentUserInfo.tierIconUrl;
|
||||
tierIconInline.style.display = 'block';
|
||||
} else if (tierIconInline) {
|
||||
tierIconInline.style.display = 'none';
|
||||
}
|
||||
|
||||
// 更新剩余 Credits
|
||||
const creditsDetail = document.getElementById('creditsDetail');
|
||||
console.log('[UserInfoComponent] 更新 Credits 显示');
|
||||
console.log('[UserInfoComponent] currentUserInfo.credits:', currentUserInfo.credits);
|
||||
console.log('[UserInfoComponent] creditsDetail 元素:', creditsDetail);
|
||||
if (creditsDetail) {
|
||||
const creditsText = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-';
|
||||
creditsDetail.textContent = creditsText;
|
||||
console.log('[UserInfoComponent] Credits 已更新为:', creditsText);
|
||||
} else {
|
||||
console.warn('[UserInfoComponent] creditsDetail 元素未找到');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息显示
|
||||
function updateUserInfoDisplay(userInfo) {
|
||||
currentUserInfo = userInfo;
|
||||
console.log('[UserInfoComponent] 更新用户信息:', userInfo);
|
||||
// 如果下拉面板已打开,立即更新显示
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
if (dropdown && dropdown.classList.contains('active')) {
|
||||
updateUserDetailModal();
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定下拉面板事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 点击页面其他地方关闭下拉面板
|
||||
document.addEventListener('click', (e) => {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
const userButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (dropdown && dropdown.classList.contains('active')) {
|
||||
// 如果点击的不是用户按钮和下拉面板内容,则关闭
|
||||
if (!userButton?.contains(e.target) && !dropdown.contains(e.target)) {
|
||||
closeUserDetailModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止下拉面板内容点击事件冒泡
|
||||
const dropdownContent = document.querySelector('.user-detail-content');
|
||||
if (dropdownContent) {
|
||||
dropdownContent.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
@ -174,7 +174,7 @@ export function getWaveformPreviewScript(): string {
|
||||
const content = document.createElement('div');
|
||||
content.className = 'waveform-preview-content';
|
||||
|
||||
const miniViewerId = 'waveform-mini-' + Date.now();
|
||||
const miniViewerId = 'waveform-mini-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
const miniViewer = document.createElement('div');
|
||||
miniViewer.id = miniViewerId;
|
||||
miniViewer.className = 'waveform-mini-viewer';
|
||||
@ -347,7 +347,7 @@ export function getWaveformPreviewScript(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开完整波形查看器
|
||||
* 打开完整波形查看器(在新列中)
|
||||
*/
|
||||
function openFullWaveform(vcdFilePath) {
|
||||
vscode.postMessage({
|
||||
|
||||
@ -428,6 +428,7 @@ export function getWebviewContent(
|
||||
<script>
|
||||
console.log('[WebView] 脚本开始执行');
|
||||
const vscode = acquireVsCodeApi();
|
||||
window.vscode = vscode; // 确保全局可访问
|
||||
console.log('[WebView] vscode API 已获取');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
@ -585,6 +586,32 @@ export function getWebviewContent(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'updateUserInfo':
|
||||
// 更新用户信息
|
||||
console.log('[WebView] 收到用户信息:', message.userInfo);
|
||||
console.log('[WebView] Credits 字段值:', message.userInfo?.credits);
|
||||
if (message.userInfo) {
|
||||
const userInfoData = {
|
||||
nickname: message.userInfo.nickname || message.userInfo.username || '用户',
|
||||
userId: message.userInfo.userId || message.userInfo.id,
|
||||
tierName: message.userInfo.tierName,
|
||||
tierIconUrl: message.tierIconUrl,
|
||||
registerTime: message.userInfo.registerTime || message.userInfo.createdAt,
|
||||
credits: message.userInfo.credits
|
||||
};
|
||||
|
||||
console.log('[WebView] 显示用户信息:', userInfoData);
|
||||
console.log('[WebView] userInfoData.credits:', userInfoData.credits);
|
||||
|
||||
// 调用更新用户头像图标按钮的函数
|
||||
if (typeof updateUserAvatarIconButton === 'function') {
|
||||
updateUserAvatarIconButton(userInfoData);
|
||||
} else {
|
||||
console.warn('[WebView] updateUserAvatarIconButton 函数不存在');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'resetSegmentedMessage':
|
||||
// 重置分段消息容器(停止对话时调用)
|
||||
console.log('[WebView] 重置分段消息容器');
|
||||
@ -716,6 +743,13 @@ export function getWebviewContent(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'optimizeResult':
|
||||
// 处理提示词优化结果
|
||||
if (typeof handleOptimizeResult === 'function') {
|
||||
handleOptimizeResult(message.success, message.optimizedPrompt, message.error);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user