23 Commits

Author SHA1 Message Date
feff8ea4d3 feat:修改进度条的文本内容 2026-01-09 19:19:53 +08:00
6abec8c7b7 feat:预览波形展开新开窗口展示完整波形 2026-01-09 19:06:34 +08:00
f9b3699bda fix:解决自动滚动遇到大的文本的时候失效的bug 2026-01-09 18:15:58 +08:00
8da1177bf3 style:解决展示不清楚的bug 2026-01-09 18:15:30 +08:00
a85a044a9b feat:用户信息和会员展示到页面上 2026-01-09 17:21:42 +08:00
c58e3603de feat:获取会员信息 并且展示title 2026-01-09 16:24:27 +08:00
940584e1ea feat/获取用户信息+展示用户名称 2026-01-09 15:26:33 +08:00
4037e9e2d7 style:调整对话样式 2026-01-08 20:25:51 +08:00
4b2f6967dc style:优化了预览波形的样式 2026-01-08 18:18:51 +08:00
79ef879b97 Merge branch 'feat/back-to-front' into feature/waveform-renderer 2026-01-08 17:26:29 +08:00
1df7462778 docs: 添加数据流程详解文档 + fix: 修复消息渲染逻辑
- 新增完整的数据流程文档,详细说明从用户输入到响应显示的全流程
   - 修复 messageArea.ts 中的消息渲染逻辑:
     - 移除用户消息时重置分段容器的逻辑
     - 移除对话完成时跳过 segments 处理的逻辑
     - 确保对话完成时正确渲染最终的 segments
2026-01-08 17:24:36 +08:00
0bcdc615e3 style:对话界面的样式优化
- 代码高亮
- 间距调整
- 工具调用的样式调整
2026-01-08 16:10:41 +08:00
5577fe17bb fix:解决用户消息错位的bug + 解决内容重复展示的bug 2026-01-08 15:27:14 +08:00
820ee2f848 feat:实现预览波形点击展开会显示完整波形 2026-01-07 19:02:00 +08:00
be8365c8cb feature: 实现点击 VCD 文件时 Surfer 显示波形
- VCDViewerEditorProvider 现在接收并持有 vcdFileServer 实例
   - createFromWebviewPanel 方法传递 vcdFileServer 参数
   - 确保自定义编辑器打开 VCD 文件时能够通过 HTTP 服务器加载波形数据
2026-01-07 17:46:09 +08:00
b1dd2442b8 feat:surfer替换vcdroom 2026-01-07 17:30:34 +08:00
9281d1d724 feat: 支持服务等级动态切换
- 添加 ServiceTier 类型定义
- 修改 dialogService 接收 serviceTier 参数
- 修改 messageHandler 传递 serviceTier 参数
- 修改 ICHelperPanel 传递 UI 选择的服务等级
2026-01-07 16:13:56 +08:00
226bb46094 feat:换到测试服务器上 2026-01-05 19:31:28 +08:00
251289a340 Merge branch 'feat/plugin-front-end' into merge/250105merge 2026-01-05 19:08:27 +08:00
c22081c5e9 feat: 处理后端 heartbeat 事件,保持 SSE 连接活跃 2026-01-05 19:04:04 +08:00
e4ff49bade chore: 添加 vcdParser.ts (未使用,保留备用)
纯 TypeScript 实现的 VCD 解析器,当前未使用。
目前使用 waveformTracer.ts 调用 Python 打包的 exe。
2026-01-05 18:29:49 +08:00
ada4806493 feat: 集成 waveform_trace 波形调试工具
新增功能:
- waveformTracer.ts: 调用 waveform_trace.exe 的工具实现
- toolExecutor.ts: 添加 waveform_trace 工具分发
- types/api.ts: 添加 WaveformTraceArgs 类型定义

工具源码 (tools/waveform_trace/src/):
- AST 解析 + BFS 信号追踪
- VCD 波形解析
- 修复通用 testbench 支持

配置文件:
- .gitignore: 排除 exe 和打包产物
- .vscodeignore: 发布时排除源码
- build.bat/build.sh: 打包脚本
2026-01-05 18:18:57 +08:00
e48e822d07 fix: 修复 taskId 不一致导致 conversation.json 找不到的问题
- messageHandler 复用 historyManager 的 taskId 而非重新生成
- 环境切换为 dev,超时时间统一为 5 分钟
- agentCard 添加调试智能体相关工具名称映射
- 移除冗余的 segments 调试日志
2026-01-05 10:15:25 +08:00
213 changed files with 66088 additions and 366 deletions

11
.gitignore vendored
View File

@ -3,3 +3,14 @@ dist
node_modules node_modules
.vscode-test/ .vscode-test/
*.vsix *.vsix
# waveform_trace 打包产物exe 太大,通过 Release 发布)
tools/waveform_trace/bin/
tools/waveform_trace/src/build/
tools/waveform_trace/src/dist/
tools/waveform_trace/src/*.spec
# Python 缓存
__pycache__/
*.pyc
*.pyo

29
.vscodeignore Normal file
View File

@ -0,0 +1,29 @@
# 排除开发文件
.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

1027
docs/数据流程详解.md Normal file

File diff suppressed because it is too large Load Diff

200
media/surfer/index.html Normal file
View File

@ -0,0 +1,200 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<!-- change this to your project name -->
<title>Surfer</title>
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<script type="module">
import init from '/surfer.js';
await init({module_or_path: '/surfer_bg.wasm'});
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/surfer.js';
window.inject_message = inject_message;
window.id_of_name = id_of_name;
window.draw_text_arrow = draw_text_arrow;
/*SURFER_SETUP_HOOKS*/
</script>
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
<base href="/" />
<script>
function on_surfer_error(msg) {
console.log("Setting error message")
document.getElementById("error_message").innerHTML = msg
document.getElementById("error_container").style.display = "block"
}
window.on_surfer_error = on_surfer_error;
</script>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}
/* Make canvas fill entire document: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#error_container {
padding: 1em;
border-radius: 0.5em;
margin: 0px auto;
max-width: 980px;
color: #ffffff;
background-color: black;
position: relative;
height: 90%;
overflow: scroll;
}
#error_container a {
color: #ff9999;
}
#error_message {
overflow: scroll;
white-space: break-spaces;
}
</style>
<link rel="modulepreload" href="/surfer.js" crossorigin="anonymous" integrity="sha384-s5jcnzgSMjwjfa1Jq5kr3vQVXGQ7D+ZdMsCBdbbcmKefqvRKw652YAYaaHZJQob6"><link rel="preload" href="/surfer_bg.wasm" crossorigin="anonymous" integrity="sha384-YzYZZQJDXiKIAVpyBMziailnMHJ/sxzBq0VNMP854yLbTd2lneCR5ZgcvB4cYMFc" as="fetch" type="application/wasm"></head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
<canvas id="the_canvas_id"></canvas>
<div id="error_container" style="display: none;">
<h1>Sorry, Surfer crashed 🔥</h1>
<p>
Something caused Surfer to crash. Please report the error on
<a href="https://gitlab.com/surfer-project/surfer/-/issues/new">
gitlab
</a>
</p>
<p>
Any report is appreciated, but it is extra helpful if you can attach the waveform that caused
the crash and/or the steps to reproduce the crash.
</p>
<h3>
Backtrace:
</h3>
<div class="error_container">
<!-- This is filled in by javascript -->
<code id="error_message"></code>
</div>
</div>
<!-- Register the message listener system -->
<script src="integration.js"></script>
<script>
register_message_listener()
</script>
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
<script>
// We disable caching during development so that we always view the latest version.
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
window.addEventListener('load', function () {
navigator.serviceWorker.register('sw.js');
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->

View File

@ -0,0 +1,65 @@
// Web apps which integrate Surfer as an iframe can give commands to surfer via
// the .postMessage [1] function on the iframe.
//
// For example, to tell Surfer to load waveforms from a URL, use
// `.postMessage({command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"})`
//
// For more complex functionality, one can also inject any `Message` defined
// in `surfer::Message` in surfer/main.rs. However, the API of these messages
// is not stable and may change at any time. If you add functionality via
// these, make sure to test the new functionality when changing Surfer version.
//
// [1] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
function register_message_listener() {
window.addEventListener("message", (event) => {
// JSON decode the message
const decoded = event.data
switch (decoded.command) {
// Load a waveform from a URL. The format is inferred from the data.
// Example: `{command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"}`
case 'LoadUrl': {
const msg = {
LoadWaveformFileFromUrl: [
decoded.url,
"Clear"
]
}
inject_message(JSON.stringify(msg))
break;
}
case 'ToggleMenu': {
const msg = "ToggleMenu"
inject_message(JSON.stringify(msg))
break;
}
// Load waveform data directly from string content
case 'LoadData': {
const msg = {
LoadFromData: [
decoded.content,
decoded.fileName || "waveform.vcd",
"Clear"
]
}
inject_message(JSON.stringify(msg))
break;
}
// Inject any other message supported by Surfer in the surfer::Message enum.
// NOTE: The API of these is unstable.
case 'InjectMessage': {
inject_message(decoded.message);
break
}
default:
console.log(`Unknown message.command ${decoded.command}`)
break;
}
});
}

View File

@ -0,0 +1,10 @@
{
"background_color": "white",
"display": "standalone",
"id": "/index.html",
"lang": "en-US",
"name": "Surfer",
"short_name": "surfer",
"start_url": "./index.html",
"theme_color": "white"
}

2227
media/surfer/surfer.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,200 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<!-- change this to your project name -->
<title>Surfer</title>
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<script type="module">
import init from '/surfer.js';
await init({module_or_path: '/surfer_bg.wasm'});
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/surfer.js';
window.inject_message = inject_message;
window.id_of_name = id_of_name;
window.draw_text_arrow = draw_text_arrow;
/*SURFER_SETUP_HOOKS*/
</script>
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
<base href="/" />
<script>
function on_surfer_error(msg) {
console.log("Setting error message")
document.getElementById("error_message").innerHTML = msg
document.getElementById("error_container").style.display = "block"
}
window.on_surfer_error = on_surfer_error;
</script>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}
/* Make canvas fill entire document: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#error_container {
padding: 1em;
border-radius: 0.5em;
margin: 0px auto;
max-width: 980px;
color: #ffffff;
background-color: black;
position: relative;
height: 90%;
overflow: scroll;
}
#error_container a {
color: #ff9999;
}
#error_message {
overflow: scroll;
white-space: break-spaces;
}
</style>
<link rel="modulepreload" href="/surfer.js" crossorigin="anonymous" integrity="sha384-s5jcnzgSMjwjfa1Jq5kr3vQVXGQ7D+ZdMsCBdbbcmKefqvRKw652YAYaaHZJQob6"><link rel="preload" href="/surfer_bg.wasm" crossorigin="anonymous" integrity="sha384-YzYZZQJDXiKIAVpyBMziailnMHJ/sxzBq0VNMP854yLbTd2lneCR5ZgcvB4cYMFc" as="fetch" type="application/wasm"></head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
<canvas id="the_canvas_id"></canvas>
<div id="error_container" style="display: none;">
<h1>Sorry, Surfer crashed 🔥</h1>
<p>
Something caused Surfer to crash. Please report the error on
<a href="https://gitlab.com/surfer-project/surfer/-/issues/new">
gitlab
</a>
</p>
<p>
Any report is appreciated, but it is extra helpful if you can attach the waveform that caused
the crash and/or the steps to reproduce the crash.
</p>
<h3>
Backtrace:
</h3>
<div class="error_container">
<!-- This is filled in by javascript -->
<code id="error_message"></code>
</div>
</div>
<!-- Register the message listener system -->
<script src="integration.js"></script>
<script>
register_message_listener()
</script>
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
<script>
// We disable caching during development so that we always view the latest version.
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
window.addEventListener('load', function () {
navigator.serviceWorker.register('sw.js');
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->

View File

@ -0,0 +1,52 @@
// Web apps which integrate Surfer as an iframe can give commands to surfer via
// the .postMessage [1] function on the iframe.
//
// For example, to tell Surfer to load waveforms from a URL, use
// `.postMessage({command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"})`
//
// For more complex functionality, one can also inject any `Message` defined
// in `surfer::Message` in surfer/main.rs. However, the API of these messages
// is not stable and may change at any time. If you add functionality via
// these, make sure to test the new functionality when changing Surfer version.
//
// [1] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
function register_message_listener() {
window.addEventListener("message", (event) => {
// JSON decode the message
const decoded = event.data
switch (decoded.command) {
// Load a waveform from a URL. The format is inferred from the data.
// Example: `{command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"}`
case 'LoadUrl': {
const msg = {
LoadWaveformFileFromUrl: [
decoded.url,
"Clear"
]
}
inject_message(JSON.stringify(msg))
break;
}
case 'ToggleMenu': {
const msg = "ToggleMenu"
inject_message(JSON.stringify(msg))
break;
}
// Inject any other message supported by Surfer in the surfer::Message enum.
// NOTE: The API of these is unstable.
case 'InjectMessage': {
inject_message(decoded.message);
break
}
default:
console.log(`Unknown message.command ${decoded.command}`)
break;
}
});
}

View File

@ -0,0 +1,10 @@
{
"background_color": "white",
"display": "standalone",
"id": "/index.html",
"lang": "en-US",
"name": "Surfer",
"short_name": "surfer",
"start_url": "./index.html",
"theme_color": "white"
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

37
media/surfer/surfer/sw.js Normal file
View File

@ -0,0 +1,37 @@
self.addEventListener("install", function () {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", function (event) {
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
return;
}
event.respondWith(
fetch(event.request)
.then(function (response) {
// It seems like we only need to set the headers for index.html
// If you want to be on the safe side, comment this out
// if (!response.url.includes("index.html")) return response;
const newHeaders = new Headers(response.headers);
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
const moddedResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
return moddedResponse;
})
.catch(function (e) {
console.error(e);
})
);
});

BIN
media/surfer/surfer_bg.wasm Normal file

Binary file not shown.

37
media/surfer/sw.js Normal file
View File

@ -0,0 +1,37 @@
self.addEventListener("install", function () {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", function (event) {
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
return;
}
event.respondWith(
fetch(event.request)
.then(function (response) {
// It seems like we only need to set the headers for index.html
// If you want to be on the safe side, comment this out
// if (!response.url.includes("index.html")) return response;
const newHeaders = new Headers(response.headers);
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
const moddedResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
return moddedResponse;
})
.catch(function (e) {
console.error(e);
})
);
});

View File

@ -70,6 +70,18 @@
"id": "iccoder", "id": "iccoder",
"label": "IC Coder" "label": "IC Coder"
} }
],
"customEditors": [
{
"viewType": "ic-coder.vcdViewer",
"displayName": "VCD 波形查看器",
"selector": [
{
"filenamePattern": "*.vcd"
}
],
"priority": "default"
}
] ]
}, },
"scripts": { "scripts": {

BIN
rustup-init.exe Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@ -0,0 +1,237 @@
/**
* 代码高亮组件
*
* 功能说明:
* - 使用 highlight.js 提供专业的代码语法高亮
* - 支持多种编程语言Verilog, JavaScript, Python 等)
* - 提供行内代码和代码块的不同样式
* - 自动检测语言类型
*/
/**
* 获取 highlight.js 的 CDN 链接
*/
export function getHighlightJsLinks(): string {
return `
<!-- Highlight.js CSS (VS Code Dark+ 主题) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css">
<!-- Highlight.js 核心库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- Verilog 语言支持 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/verilog.min.js"></script>
`;
}
/**
* 获取代码高亮的样式
*/
export function getCodeHighlightStyles(): string {
return `
/* 代码块基础样式 */
.segment-text pre {
background: var(--vscode-textCodeBlock-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 12px;
overflow-x: auto;
margin: 12px 0;
position: relative;
white-space: pre;
}
.segment-text pre code {
background: transparent !important;
padding: 0;
border: none;
display: block;
line-height: 1.5;
white-space: pre;
font-family: 'Courier New', Consolas, 'Monaco', monospace;
font-size: 0.9em;
}
/* 行内代码样式 */
.segment-text code:not(pre code) {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
color: var(--vscode-textPreformat-foreground);
border: 1px solid var(--vscode-panel-border);
font-family: 'Courier New', Consolas, 'Monaco', monospace;
font-size: 0.9em;
}
/* 覆盖 highlight.js 的背景色,使用 VSCode 主题色 */
.segment-text pre code.hljs {
background: transparent !important;
padding: 0 !important;
}
/* 代码块语言标签 */
.code-block-wrapper {
position: relative;
margin: -20px 0;
}
.code-language-label {
position: absolute;
top: 8px;
right: 8px;
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
opacity: 0.8;
z-index: 1;
}
/* 代码块复制按钮 */
.code-copy-btn {
position: absolute;
top: 8px;
right: 8px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
border-radius: 4px;
padding: 4px 8px;
font-size: 11px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.code-block-wrapper:hover .code-copy-btn {
opacity: 1;
}
.code-copy-btn:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.code-copy-btn.copied {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
/* 代码块滚动条样式 */
.segment-text pre::-webkit-scrollbar {
height: 8px;
}
.segment-text pre::-webkit-scrollbar-track {
background: var(--vscode-scrollbarSlider-background);
border-radius: 4px;
}
.segment-text pre::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-hoverBackground);
border-radius: 4px;
}
.segment-text pre::-webkit-scrollbar-thumb:hover {
background: var(--vscode-scrollbarSlider-activeBackground);
}
`;
}
/**
* 获取代码高亮的脚本
*/
export function getCodeHighlightScript(): string {
return `
/**
* 使用 highlight.js 进行代码高亮
*/
function highlightCodeBlocks() {
// 等待 highlight.js 加载完成
if (typeof hljs === 'undefined') {
setTimeout(highlightCodeBlocks, 100);
return;
}
const codeBlocks = document.querySelectorAll('.segment-text pre code:not(.hljs)');
codeBlocks.forEach((block) => {
hljs.highlightElement(block);
});
}
/**
* 为代码块添加复制按钮
*/
function enhanceCodeBlocks() {
const codeBlocks = document.querySelectorAll('.segment-text pre code');
codeBlocks.forEach((codeElement) => {
const preElement = codeElement.parentElement;
if (!preElement || preElement.classList.contains('enhanced')) {
return;
}
// 标记为已增强,避免重复处理
preElement.classList.add('enhanced');
// 应用语法高亮
if (typeof hljs !== 'undefined' && !codeElement.classList.contains('hljs')) {
hljs.highlightElement(codeElement);
}
// 创建包装器
const wrapper = document.createElement('div');
wrapper.className = 'code-block-wrapper';
preElement.parentNode.insertBefore(wrapper, preElement);
wrapper.appendChild(preElement);
// 添加复制按钮
const copyBtn = document.createElement('button');
copyBtn.className = 'code-copy-btn';
copyBtn.textContent = '复制';
copyBtn.onclick = function() {
const code = codeElement.textContent;
navigator.clipboard.writeText(code).then(() => {
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 2000);
});
};
wrapper.appendChild(copyBtn);
});
}
/**
* 监听 DOM 变化,自动增强新添加的代码块
*/
function observeCodeBlocks() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
enhanceCodeBlocks();
}
});
});
observer.observe(document.getElementById('messages'), {
childList: true,
subtree: true
});
}
// 初始化代码块增强
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
enhanceCodeBlocks();
observeCodeBlocks();
});
} else {
enhanceCodeBlocks();
observeCodeBlocks();
}
`;
}

View File

@ -10,14 +10,21 @@ type Environment = "dev" | "test" | "prod";
/** 当前环境 - 修改这里切换环境 */ /** 当前环境 - 修改这里切换环境 */
const CURRENT_ENV: Environment = "test"; const CURRENT_ENV: Environment = "test";
/** 服务等级类型 */
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
/** 配置项接口 */ /** 配置项接口 */
export interface IccoderConfig { export interface IccoderConfig {
/** 后端服务地址 */ /** 后端服务地址 */
backendUrl: string; backendUrl: string;
/** 后端服务地址strangeLoop */
backendUrlStrongeLoop: string;
/** 请求超时时间(毫秒) */ /** 请求超时时间(毫秒) */
timeout: number; timeout: number;
/** 用户ID临时使用后续对接认证 */ /** 用户ID临时使用后续对接认证 */
userId: string; userId: string;
/** 服务等级 */
serviceTier: ServiceTier;
} }
/** 环境配置 */ /** 环境配置 */
@ -25,20 +32,26 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 本地开发环境 */ /** 本地开发环境 */
dev: { dev: {
backendUrl: "http://localhost:2233", backendUrl: "http://localhost:2233",
timeout: 60000, backendUrlStrongeLoop: "http://192.168.1.108:2029",
timeout: 300000,
userId: "default-user", userId: "default-user",
serviceTier: "max", // 默认使用 max
}, },
/** 测试服务器环境 */ /** 测试服务器环境 */
test: { test: {
backendUrl: "http://192.168.1.108:2233", backendUrl: "http://192.168.1.108:2233",
backendUrlStrongeLoop: "http://192.168.1.108:2029",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
serviceTier: "max",
}, },
/** 生产环境 */ /** 生产环境 */
prod: { prod: {
backendUrl: "https://api.iccoder.com", // TODO: 替换为实际生产地址 backendUrl: "https://api.iccoder.com",
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
serviceTier: "auto",
}, },
}; };
@ -67,3 +80,15 @@ export function getApiUrl(path: string): string {
const apiPath = path.startsWith("/") ? path : `/${path}`; const apiPath = path.startsWith("/") ? path : `/${path}`;
return `${baseUrl}${apiPath}`; 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}`;
}

View File

@ -170,3 +170,13 @@ export const stateTransitionIconSvg = `
</svg> </svg>
</span> </span>
`; `;
/**
* 用户提问图标 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>`;

View File

@ -1,13 +1,31 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import { ICViewProvider } from "./views/ICViewProvider"; import { ICViewProvider } from "./views/ICViewProvider";
import { showICHelperPanel } from "./panels/ICHelperPanel"; import { showICHelperPanel } from "./panels/ICHelperPanel";
import { VCDViewerPanel } from "./panels/VCDViewerPanel"; import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
import { ChatHistoryManager } from "./utils/chatHistoryManager"; import { ChatHistoryManager } from "./utils/chatHistoryManager";
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider"; import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
import { VCDFileServer } from "./services/vcdFileServer";
import { initUserService } from "./services/userService";
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
console.log("🎉 IC Coder 插件已激活!"); console.log("🎉 IC Coder 插件已激活!");
// 初始化用户服务
initUserService(context);
// 初始化 VCD 文件服务器
const vcdFileServer = new VCDFileServer(context.extensionUri);
vcdFileServer.start().then((port) => {
console.log(`VCD 文件服务器已启动,端口: ${port}`);
}).catch((error) => {
console.error("启动 VCD 文件服务器失败:", error);
});
// 在插件停用时关闭服务器
context.subscriptions.push({
dispose: () => vcdFileServer.stop()
});
// 注册 Authentication Provider // 注册 Authentication Provider
const authProvider = new ICCoderAuthenticationProvider(context); const authProvider = new ICCoderAuthenticationProvider(context);
context.subscriptions.push( context.subscriptions.push(
@ -68,7 +86,40 @@ export function activate(context: vscode.ExtensionContext) {
} }
} }
VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath); VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath, vcdFileServer);
}
);
// 注册命令:在浏览器中打开 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(`波形查看器已在浏览器中打开`);
} }
); );
@ -160,11 +211,15 @@ export function activate(context: vscode.ExtensionContext) {
viewProvider viewProvider
); );
// 注册 VCD 自定义编辑器
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
// 添加到订阅 // 添加到订阅
context.subscriptions.push( context.subscriptions.push(
openPanelCommand, openPanelCommand,
openChatCommand, openChatCommand,
openVCDViewerCommand, openVCDViewerCommand,
openVCDViewerInBrowserCommand,
loginCommand, loginCommand,
logoutCommand, logoutCommand,
// TODO: 等待重新实现这些命令 // TODO: 等待重新实现这些命令
@ -174,7 +229,8 @@ export function activate(context: vscode.ExtensionContext) {
// deleteSessionCommand, // deleteSessionCommand,
// clearHistoryCommand, // clearHistoryCommand,
// searchSessionCommand, // searchSessionCommand,
viewRegistration viewRegistration,
vcdEditorProvider
); );
} }

View File

@ -18,6 +18,38 @@ import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel"; import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager"; import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory"; 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 助手面板 * 创建并显示 IC 助手面板
@ -108,6 +140,45 @@ export async function showICHelperPanel(
maxIconUri.toString() maxIconUri.toString()
); );
// 获取并发送用户信息到 webview
try {
// 优先使用缓存的用户信息
let userInfo = getCachedUserInfo();
if (userInfo) {
// 使用缓存的用户信息
console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo);
const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode);
panel.webview.postMessage({
command: 'updateUserInfo',
userInfo: {
userId: userInfo.userId,
nickname: userInfo.nickname,
username: userInfo.username
},
tierIconUrl: tierIconUrl
});
} 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( panel.webview.onDidReceiveMessage(
async (message) => { async (message) => {
@ -148,7 +219,8 @@ export async function showICHelperPanel(
panel, panel,
message.text, message.text,
context.extensionPath, context.extensionPath,
message.mode message.mode,
message.model // 传递服务等级
); );
break; break;
case "readFile": case "readFile":
@ -175,12 +247,9 @@ export async function showICHelperPanel(
vscode.window.showInformationMessage(message.text); vscode.window.showInformationMessage(message.text);
break; break;
case "openWaveformViewer": case "openWaveformViewer":
// 打开波形查看器 // 在新列中打开波形查看器
if (message.vcdFilePath) { if (message.vcdFilePath) {
VCDViewerPanel.createOrShow( vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath);
context.extensionUri,
message.vcdFilePath
);
} }
break; break;
case "getVCDInfo": case "getVCDInfo":

View File

@ -1,19 +1,77 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as path from "path"; import * as path from "path";
import * as fs from "fs"; import * as fs from "fs";
import { VCDFileServer } from "../services/vcdFileServer";
/** /**
* VCD 波形查看器面板 * VCD 波形查看器自定义编辑器提供者
*/
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider {
public static register(context: vscode.ExtensionContext, vcdFileServer: VCDFileServer): vscode.Disposable {
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
const providerRegistration = vscode.window.registerCustomEditorProvider(
"ic-coder.vcdViewer",
provider,
{
webviewOptions: {
retainContextWhenHidden: true,
},
}
);
return providerRegistration;
}
constructor(
private readonly context: vscode.ExtensionContext,
private readonly vcdFileServer: VCDFileServer
) {}
async openCustomDocument(
uri: vscode.Uri,
openContext: vscode.CustomDocumentOpenContext,
token: vscode.CancellationToken
): Promise<vscode.CustomDocument> {
return {
uri,
dispose: () => {},
};
}
async resolveCustomEditor(
document: vscode.CustomDocument,
webviewPanel: vscode.WebviewPanel,
token: vscode.CancellationToken
): Promise<void> {
webviewPanel.webview.options = {
enableScripts: true,
localResourceRoots: [this.context.extensionUri],
};
// 使用公共工厂方法创建 VCD 查看器实例
VCDViewerPanel.createFromWebviewPanel(
webviewPanel,
this.context.extensionUri,
document.uri.fsPath,
this.vcdFileServer
);
}
}
/**
* VCD 波形查看器面板 (使用 Surfer)
*/ */
export class VCDViewerPanel { export class VCDViewerPanel {
public static currentPanel: VCDViewerPanel | undefined; public static currentPanel: VCDViewerPanel | undefined;
private readonly _panel: vscode.WebviewPanel; private readonly _panel: vscode.WebviewPanel;
private readonly _extensionUri: vscode.Uri; private readonly _extensionUri: vscode.Uri;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
private _currentVcdPath: string | undefined;
private _vcdFileServer: VCDFileServer | undefined;
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) {
this._panel = panel; this._panel = panel;
this._extensionUri = extensionUri; this._extensionUri = extensionUri;
this._vcdFileServer = vcdFileServer;
// 设置初始 HTML 内容 // 设置初始 HTML 内容
this._panel.webview.html = this._getLoadingHtml(); this._panel.webview.html = this._getLoadingHtml();
@ -24,12 +82,20 @@ export class VCDViewerPanel {
// 监听来自 webview 的消息 // 监听来自 webview 的消息
this._panel.webview.onDidReceiveMessage( this._panel.webview.onDidReceiveMessage(
(message) => { (message) => {
console.log("[VCDViewerPanel] 收到消息:", message);
switch (message.command) { switch (message.command) {
case "loadVCD": case "loadVCD":
if (message.filePath) { if (message.filePath) {
this.loadVCDFile(message.filePath); this.loadVCDFile(message.filePath);
} }
break; break;
case "loaded":
// Surfer iframe 加载完成,发送 VCD 文件
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
if (this._currentVcdPath) {
this.sendVcdToSurfer(this._currentVcdPath);
}
break;
} }
}, },
null, null,
@ -40,8 +106,9 @@ export class VCDViewerPanel {
/** /**
* 创建或显示 VCD 查看器面板 * 创建或显示 VCD 查看器面板
*/ */
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string) { public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
const column = vscode.ViewColumn.One; // 在当前活动编辑器旁边打开新列
const column = vscode.ViewColumn.Beside;
// 如果已经有面板打开,则显示它 // 如果已经有面板打开,则显示它
if (VCDViewerPanel.currentPanel) { if (VCDViewerPanel.currentPanel) {
@ -64,7 +131,7 @@ export class VCDViewerPanel {
} }
); );
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri); VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
// 如果提供了 VCD 文件路径,加载它 // 如果提供了 VCD 文件路径,加载它
if (vcdFilePath) { if (vcdFilePath) {
@ -72,23 +139,44 @@ export class VCDViewerPanel {
} }
} }
/**
* 从已有的 webview panel 创建 VCD 查看器(用于自定义编辑器)
*/
public static createFromWebviewPanel(
panel: vscode.WebviewPanel,
extensionUri: vscode.Uri,
vcdFilePath: string,
vcdFileServer?: VCDFileServer
) {
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
viewer.loadVCDFile(vcdFilePath);
return viewer;
}
/** /**
* 加载 VCD 文件 * 加载 VCD 文件
*/ */
public loadVCDFile(vcdFilePath: string) { public loadVCDFile(vcdFilePath: string) {
try { try {
console.log("[VCDViewerPanel] 开始加载 VCD 文件:", vcdFilePath);
// 检查文件是否存在 // 检查文件是否存在
if (!fs.existsSync(vcdFilePath)) { if (!fs.existsSync(vcdFilePath)) {
vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`); vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`);
return; return;
} }
// 保存当前 VCD 路径
this._currentVcdPath = vcdFilePath;
console.log("[VCDViewerPanel] VCD 路径已保存:", this._currentVcdPath);
// 更新面板标题 // 更新面板标题
const fileName = path.basename(vcdFilePath); const fileName = path.basename(vcdFilePath);
this._panel.title = `VCD 波形查看器 - ${fileName}`; this._panel.title = `Surfer 波形查看器 - ${fileName}`;
// 设置 HTML 内容 // 设置 HTML 内容
this._panel.webview.html = this._getWebviewContent(vcdFilePath); this._panel.webview.html = this._getWebviewContent();
console.log("[VCDViewerPanel] Webview HTML 已设置");
} catch (error) { } catch (error) {
vscode.window.showErrorMessage( vscode.window.showErrorMessage(
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}` `加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
@ -96,6 +184,104 @@ export class VCDViewerPanel {
} }
} }
/**
* 解析 VCD 文件获取根模块及其直接子模块名称
*/
private parseVcdRootScope(vcdFilePath: string): string[] {
try {
// 读取 VCD 文件
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();
// 遇到 $enddefinitions 就停止解析
if (trimmed.startsWith('$enddefinitions')) {
break;
}
// 查找 $scope 定义
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
if (scopeMatch) {
const scopeType = scopeMatch[1];
const scopeName = scopeMatch[2];
// 记录顶层 module (depth = 0)
if (scopeDepth === 0 && scopeType === 'module') {
scopeStack.push(scopeName);
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
}
// 记录顶层下的直接子模块 (depth = 1)
else if (scopeDepth === 1 && scopeType === 'module') {
const fullPath = [...scopeStack, scopeName];
scopeNames.push(fullPath.join('.'));
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.'));
}
scopeDepth++;
}
// 遇到 $upscope 减少深度
if (trimmed.startsWith('$upscope')) {
scopeDepth--;
if (scopeDepth === 0) {
scopeStack.pop();
}
}
}
return scopeNames;
} catch (error) {
console.error("[VCDViewerPanel] 解析 VCD 文件失败:", error);
return [];
}
}
/**
* 发送 VCD 文件到 Surfer
*/
private sendVcdToSurfer(vcdFilePath: string) {
try {
console.log("[VCDViewerPanel] 准备发送 VCD 到 Surfer:", vcdFilePath);
if (!this._vcdFileServer) {
throw new Error("VCD 文件服务器未初始化");
}
// 解析 VCD 文件获取根模块名称
const scopeNames = this.parseVcdRootScope(vcdFilePath);
console.log("[VCDViewerPanel] 解析到的作用域名称:", scopeNames);
// 注册文件到 HTTP 服务器
const fileId = this._vcdFileServer.registerFile(vcdFilePath);
const httpUrl = this._vcdFileServer.getFileUrl(fileId);
const fileName = path.basename(vcdFilePath);
console.log("[VCDViewerPanel] 文件名:", fileName);
console.log("[VCDViewerPanel] HTTP URL:", httpUrl);
// 使用 LoadUrl 命令通过 HTTP 加载文件
this._panel.webview.postMessage({
command: "loadVcdUrl",
url: httpUrl,
fileName: fileName,
scopeNames: scopeNames, // 传递解析到的作用域名称
});
console.log("[VCDViewerPanel] 已发送 loadVcdUrl 消息到 webview");
} catch (error) {
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
vscode.window.showErrorMessage(
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`
);
}
}
/** /**
* 清理资源 * 清理资源
*/ */
@ -163,188 +349,239 @@ export class VCDViewerPanel {
/** /**
* 获取 Webview 的 HTML 内容 * 获取 Webview 的 HTML 内容
*/ */
private _getWebviewContent(vcdFilePath: string): string { private _getWebviewContent(): string {
// 获取资源 URI // 获取 surfer 资源 URI
const vcdromJsUri = this._panel.webview.asWebviewUri( const surferJsUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcdrom.js") vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
); );
const vcdWasmUri = this._panel.webview.asWebviewUri( const surferWasmUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcd.wasm") vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
); );
const fontRegularUri = this._panel.webview.asWebviewUri( const integrationJsUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Regular.woff2") vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js")
); );
const fontObliqueUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Oblique.woff2")
);
const fontItalicUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Italic.woff2")
);
// 读取 VCD 文件内容并转换为 base64
const vcdContent = fs.readFileSync(vcdFilePath, "utf-8");
const vcdBase64 = Buffer.from(vcdContent).toString("base64");
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="zh-CN"> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${this._panel.webview.cspSource}; style-src 'unsafe-inline' ${this._panel.webview.cspSource}; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; img-src ${this._panel.webview.cspSource} data:; connect-src ${this._panel.webview.cspSource};"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
<title>VCD 波形查看器</title> <title>Surfer 波形查看器</title>
<script>
// 获取 VS Code API只能调用一次
const vscode = acquireVsCodeApi();
window.vscode = vscode;
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;
// 加载 VCD URL 的函数
function loadVcdUrl(data) {
try {
console.log('[Webview] ========== 开始加载 VCD URL ==========');
console.log('[Webview] URL:', data.url);
console.log('[Webview] Scope names from VCD:', data.scopeNames);
// 使用 setTimeout 确保 Surfer 完全准备好
setTimeout(() => {
console.log('[Webview] 通过 postMessage 发送 LoadUrl 命令');
// 使用 integration.js 提供的标准 LoadUrl 命令
window.postMessage({
command: 'LoadUrl',
url: data.url
}, '*');
console.log('[Webview] ✅ 已发送 LoadUrl 命令');
// 等待文件加载完成后,自动添加所有信号
setTimeout(async () => {
try {
console.log('[Webview] Attempting to add all signals automatically');
// 使用从 VCD 文件解析出来的作用域名称
let scopeNamesToTry = [];
if (data.scopeNames && data.scopeNames.length > 0) {
// 使用解析出来的实际子模块路径(例如 "tb.dut"
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
console.log('[Webview] Using parsed scope names:', scopeNamesToTry);
} else {
// 回退到常见的根作用域名称
scopeNamesToTry = [
['top'],
['testbench'],
['tb'],
['test'],
['dut']
];
console.log('[Webview] Using fallback scope names');
}
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('[Webview] Sent AddScope for: ' + scopeName.join('.') + ' (recursive)');
} catch (e) {
console.log('[Webview] Failed for scope: ' + scopeName.join('.'), e);
}
}
// 等待信号加载完成后,自动缩放到全部时间范围
setTimeout(() => {
try {
window.inject_message(JSON.stringify("ZoomToFit"));
console.log('[Webview] Sent ZoomToFit command');
} catch (e) {
console.log('[Webview] ZoomToFit failed:', e);
}
}, 500);
} catch (e) {
console.error('[Webview] Failed to add signals:', e);
}
}, 1500);
}, 100);
} catch (error) {
console.error('[Webview] ❌ 加载 VCD 失败:', error);
on_surfer_error(error.message + '\\n' + error.stack);
}
}
window.loadVcdUrl = loadVcdUrl;
</script>
<script type="module">
console.log('[Webview] 开始初始化 Surfer...');
import init from '${surferJsUri}';
await init({module_or_path: '${surferWasmUri}'});
console.log('[Webview] Surfer WASM 已加载');
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '${surferJsUri}';
window.inject_message = inject_message;
window.id_of_name = id_of_name;
window.draw_text_arrow = draw_text_arrow;
console.log('[Webview] Surfer 函数已导入inject_message 类型:', typeof window.inject_message);
// 等待一小段时间确保 Surfer 完全初始化
await new Promise(resolve => setTimeout(resolve, 100));
window.surferReady = true;
console.log('[Webview] Surfer 已完全初始化并准备就绪');
// 关闭 Surfer 的日志面板(如果打开的话)
try {
window.inject_message(JSON.stringify("ToggleLogs"));
console.log('[Webview] 已发送关闭日志面板命令');
} catch (e) {
console.log('[Webview] 关闭日志面板失败:', e);
}
// 如果有待处理的 VCD 数据,现在加载它
if (window.pendingVcdData) {
console.log('[Webview] 发现待处理的 VCD 数据,立即加载');
loadVcdUrl(window.pendingVcdData);
window.pendingVcdData = null;
} else {
console.log('[Webview] 没有待处理的 VCD 数据');
}
// 通知 VS Code surfer 已加载完成
console.log('[Webview] 发送 loaded 消息到 VS Code');
window.vscode.postMessage({ command: 'loaded' });
</script>
<style> <style>
@font-face { html, body {
font-family: 'Iosevka Drom Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: normal;
src: url('${fontRegularUri}') format('woff2');
}
@font-face {
font-family: 'Iosevka Drom Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: oblique;
src: url('${fontObliqueUri}') format('woff2');
}
@font-face {
font-family: 'Iosevka Drom Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: italic;
src: url('${fontItalicUri}') format('woff2');
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Iosevka Drom Web', monospace;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
overflow: hidden; overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
background: var(--vscode-editor-background);
} }
#waveform-container { canvas {
width: 100vw; margin-right: auto;
height: 100vh; margin-left: auto;
overflow: auto; display: block;
} position: absolute;
top: 0;
#waveform1 { left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.loading { #error_container {
display: flex; padding: 1em;
justify-content: center; border-radius: 0.5em;
align-items: center; margin: 0px auto;
height: 100vh; max-width: 980px;
flex-direction: column;
}
.spinner {
border: 4px solid var(--vscode-progressBar-background);
border-top: 4px solid var(--vscode-progressBar-foreground);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
padding: 20px;
color: var(--vscode-errorForeground); color: var(--vscode-errorForeground);
background-color: var(--vscode-inputValidation-errorBackground); background-color: var(--vscode-inputValidation-errorBackground);
border: 1px solid var(--vscode-inputValidation-errorBorder); position: relative;
border-radius: 4px; height: 90%;
margin: 20px; overflow: scroll;
}
#error_message {
overflow: scroll;
white-space: break-spaces;
} }
</style> </style>
<script src="${vcdromJsUri}"></script>
</head> </head>
<body> <body>
<div id="waveform-container"> <canvas id="the_canvas_id"></canvas>
<div class="loading">
<div class="spinner"></div> <div id="error_container" style="display: none;">
<p>正在加载 VCD 波形...</p> <h3>❌ Surfer 加载失败</h3>
</div> <code id="error_message"></code>
<div id="waveform1"></div>
</div> </div>
<script src="${integrationJsUri}"></script>
<script> <script>
(async function() { register_message_listener();
try {
// 设置 WASM 文件路径
window.wasmBinaryFile = '${vcdWasmUri}';
// 解码 base64 VCD 内容 console.log('[Webview] 注册 VS Code 消息监听器');
const vcdBase64 = '${vcdBase64}'; // 监听来自 VS Code 扩展的消息(使用 vscode API
const vcdContent = atob(vcdBase64); window.addEventListener('message', event => {
const message = event.data;
// 隐藏加载提示 // 检查是否来自 VS Code
document.querySelector('.loading').style.display = 'none'; if (message.command === 'loadVcdUrl') {
console.log('[Webview] 收到 VS Code 消息,命令:', message.command);
console.log('[Webview] Surfer 就绪状态:', window.surferReady);
// 创建一个函数来提供 VCD 数据流 if (window.surferReady) {
const vcdProvider = async (handler) => { // Surfer 已就绪,立即加载
// 将 VCD 内容转换为 Uint8Array loadVcdUrl(message);
const encoder = new TextEncoder();
const vcdData = encoder.encode(vcdContent);
// 创建一个 ReadableStream reader
const stream = new ReadableStream({
start(controller) {
controller.enqueue(vcdData);
controller.close();
}
});
const reader = stream.getReader();
// 调用 handler 并传递 reader
await handler([{
key: 'local',
value: 'waveform.vcd',
format: 'raw',
baseName: 'waveform.vcd',
ext: 'vcd',
reader: reader
}]);
};
// 初始化 VCDrom使用函数回调方式
if (typeof VCDrom === 'function') {
await VCDrom('waveform1', vcdProvider);
} else { } else {
throw new Error('VCDrom 未正确加载'); // Surfer 未就绪,保存数据等待加载
console.log('[Webview] Surfer 未就绪,保存数据待加载');
window.pendingVcdData = message;
} }
} catch (error) {
console.error('加载 VCD 波形失败:', error);
document.getElementById('waveform-container').innerHTML =
'<div class="error-message">' +
'<h3>❌ 加载 VCD 波形失败</h3>' +
'<p>' + error.message + '</p>' +
'<p style="margin-top: 10px;">请确保 VCD 文件格式正确。</p>' +
'<pre style="margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.1); overflow: auto;">' + error.stack + '</pre>' +
'</div>';
} }
})(); }, true); // 使用捕获阶段,优先于 integration.js 的监听器
</script> </script>
</body> </body>
</html>`; </html>`;

View File

@ -6,7 +6,7 @@ import * as https from 'https';
import * as http from 'http'; import * as http from 'http';
import { URL } from 'url'; import { URL } from 'url';
import { getApiUrl, getConfig } from '../config/settings'; 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 请求选项 * HTTP 请求选项
@ -213,3 +213,14 @@ export function createSystemErrorResult(id: number, code: number, message: strin
error: { code, message } error: { code, message }
}; };
} }
/**
* 获取用户信息
* GET /system/user/getInfo
*/
export async function getUserInfo(): Promise<UserInfoResponse> {
console.log('[API] 获取用户信息');
return request<UserInfoResponse>('/system/user/getInfo', {
method: 'GET'
});
}

View File

@ -9,7 +9,7 @@ import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from '
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor'; import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
import { userInteractionManager } from './userInteraction'; import { userInteractionManager } from './userInteraction';
import { getConfig } from '../config/settings'; import { getConfig } from '../config/settings';
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api'; import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient'; import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
import { ChatHistoryManager } from '../utils/chatHistoryManager'; import { ChatHistoryManager } from '../utils/chatHistoryManager';
@ -316,7 +316,8 @@ export class DialogSession {
async sendMessage( async sendMessage(
message: string, message: string,
callbacks: DialogCallbacks, callbacks: DialogCallbacks,
mode?: RunMode mode?: RunMode,
serviceTier?: ServiceTier // 新增:服务等级参数
): Promise<void> { ): Promise<void> {
if (this.isActive) { if (this.isActive) {
callbacks.onError?.('当前有对话正在进行中'); callbacks.onError?.('当前有对话正在进行中');
@ -344,6 +345,7 @@ export class DialogSession {
message, message,
userId: config.userId, userId: config.userId,
mode: mode || 'agent', mode: mode || 'agent',
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
compactedData: compactedData || undefined, compactedData: compactedData || undefined,
newMessages: newMessages.length > 0 ? newMessages : undefined, newMessages: newMessages.length > 0 ? newMessages : undefined,
knowledgeData: knowledgeData || undefined knowledgeData: knowledgeData || undefined

View File

@ -2,6 +2,7 @@ import * as vscode from "vscode";
import * as http from "http"; import * as http from "http";
import * as path from "path"; import * as path from "path";
import * as fs from "fs"; import * as fs from "fs";
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
/** /**
* IC Coder Authentication Provider * IC Coder Authentication Provider
@ -62,13 +63,16 @@ export class ICCoderAuthenticationProvider
try { try {
const token = await this.login(); const token = await this.login();
// 获取到 token 后立即调用用户信息接口
const userInfo = await onTokenReceived(token);
// 创建会话 // 创建会话
const session: vscode.AuthenticationSession = { const session: vscode.AuthenticationSession = {
id: this.generateSessionId(), id: this.generateSessionId(),
accessToken: token, accessToken: token,
account: { account: {
id: "iccoder-user", id: userInfo?.userId || "iccoder-user",
label: "IC Coder 用户", label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
}, },
scopes: [...scopes], scopes: [...scopes],
}; };
@ -109,6 +113,9 @@ export class ICCoderAuthenticationProvider
this._sessions.splice(sessionIndex, 1); this._sessions.splice(sessionIndex, 1);
await this.saveSessions(); await this.saveSessions();
// 清除用户信息缓存
await clearUserInfo();
// 触发会话变化事件 // 触发会话变化事件
this._onDidChangeSessions.fire({ this._onDidChangeSessions.fire({
added: [], added: [],

View File

@ -331,6 +331,11 @@ function dispatchEvent(
case 'context_usage': case 'context_usage':
callbacks.onContextUsage?.(data as ContextUsageEvent); callbacks.onContextUsage?.(data as ContextUsageEvent);
break; break;
case 'heartbeat':
// 心跳事件:仅用于保持连接,不需要特殊处理
// Node.js req.setTimeout 会在收到数据时自动重置计时器
console.log('[SSE] 收到心跳');
break;
default: default:
console.log(`[SSE] 未知事件类型: ${eventType}`, data); console.log(`[SSE] 未知事件类型: ${eventType}`, data);
} }

View File

@ -9,6 +9,8 @@ import * as fs from 'fs';
import { readFileContent, readDirectory } from '../utils/readFiles'; import { readFileContent, readDirectory } from '../utils/readFiles';
import { createOrOverwriteFile } from '../utils/createFiles'; import { createOrOverwriteFile } from '../utils/createFiles';
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner'; import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
import { analyzeVcdFile } from '../utils/vcdParser';
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
import { import {
submitToolResult, submitToolResult,
createSuccessResult, createSuccessResult,
@ -79,6 +81,9 @@ export async function executeToolCall(
case 'waveform_summary': case 'waveform_summary':
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs); resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
break; break;
case 'waveform_trace':
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
break;
case 'knowledge_save': case 'knowledge_save':
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs); resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
break; break;
@ -300,12 +305,36 @@ async function executeSimulation(
/** /**
* 执行 waveform_summary 工具 * 执行 waveform_summary 工具
* TODO: 实现 VCD 波形分析 * 解析 VCD 文件并返回波形摘要
*/ */
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> { async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
// TODO: 使用 vcdrom/vcd-stream 解析 VCD 文件 const { vcdPath, signals, checkpoints } = args;
// 目前返回一个占位响应
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`; // 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
}
const workspacePath = workspaceFolders[0].uri.fsPath;
// 解析 VCD 文件路径(支持相对路径)
const absolutePath = path.isAbsolute(vcdPath)
? vcdPath
: path.join(workspacePath, vcdPath);
// 检查文件是否存在
if (!fs.existsSync(absolutePath)) {
throw new Error(`VCD 文件不存在: ${vcdPath}`);
}
// 解析检查点时间
const checkpoint = checkpoints ? parseInt(checkpoints, 10) : undefined;
// 调用 VCD 解析器
const result = analyzeVcdFile(absolutePath, signals, checkpoint);
return result;
} }
/** /**

345
src/services/userService.ts Normal file
View File

@ -0,0 +1,345 @@
/**
* 用户服务
* 管理用户信息和认证相关的 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';
/**
* 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;
};
}
/**
* 获取用户信息
* 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] = await Promise.all([
getUserInfo(token),
getMembershipInfo(token)
]);
if (!userInfo) {
console.warn('[UserService] 未能获取到用户信息');
return 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
};
}
}
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;
}
return extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
}
/**
* 清除持久化存储的用户信息
*/
export async function clearUserInfo(): Promise<void> {
if (!extensionContext) {
console.warn('[UserService] ExtensionContext 未初始化');
return;
}
await extensionContext.globalState.update('icCoderUserInfo', undefined);
console.log('[UserService] 用户信息已清除');
}

View File

@ -0,0 +1,500 @@
import * as http from "http";
import * as fs from "fs";
import * as path from "path";
import * as vscode from "vscode";
/**
* VCD 文件 HTTP 服务器
* 用于为 Surfer 波形查看器提供 VCD 文件访问
*/
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;
}
/**
* 启动服务器
*/
public async start(): Promise<number> {
if (this.server) {
return this.port;
}
return new Promise((resolve, reject) => {
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
// 监听随机端口
this.server.listen(0, "127.0.0.1", () => {
const address = this.server!.address();
if (address && typeof address === "object") {
this.port = address.port;
console.log(`[VCDFileServer] 服务器已启动,端口: ${this.port}`);
resolve(this.port);
} else {
reject(new Error("无法获取服务器端口"));
}
});
this.server.on("error", (error) => {
console.error("[VCDFileServer] 服务器错误:", error);
reject(error);
});
});
}
/**
* 停止服务器
*/
public stop(): void {
if (this.server) {
this.server.close();
this.server = null;
this.port = 0;
this.vcdFiles.clear();
console.log("[VCDFileServer] 服务器已停止");
}
}
/**
* 注册 VCD 文件
*/
public registerFile(filePath: string): string {
const fileId = this.generateFileId(filePath);
this.vcdFiles.set(fileId, filePath);
console.log(`[VCDFileServer] 注册文件: ${fileId} -> ${filePath}`);
return fileId;
}
/**
* 获取文件 URL
*/
public getFileUrl(fileId: string): string {
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
*/
private generateFileId(filePath: string): string {
const timestamp = Date.now();
const fileName = path.basename(filePath);
return `${timestamp}-${fileName}`;
}
/**
* 处理 HTTP 请求
*/
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
const url = req.url || "";
console.log(`[VCDFileServer] 收到请求: ${url}`);
// 设置 CORS 头
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
// 处理 OPTIONS 请求
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
// 路由处理
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" });
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;
}
// 检查文件是否存在
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);
res.writeHead(200, {
"Content-Type": "text/plain",
"Content-Length": fileContent.length,
});
res.end(fileContent);
console.log(`[VCDFileServer] 成功发送文件: ${filePath}`);
} catch (error) {
console.error(`[VCDFileServer] 读取文件失败:`, error);
res.writeHead(500, { "Content-Type": "text/plain" });
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>`;
}
}

View File

@ -3,7 +3,7 @@
* 对应后端 IC Coder Backend 的接口格式 * 对应后端 IC Coder Backend 的接口格式
*/ */
import { CompactedMemory, CompactedMessage } from './memory'; import { CompactedMemory, CompactedMessage } from "./memory";
// ============== 对话请求/响应 ============== // ============== 对话请求/响应 ==============
@ -14,7 +14,16 @@ import { CompactedMemory, CompactedMessage } from './memory';
* - agent: 智能体自主(默认) * - agent: 智能体自主(默认)
* - auto: 完全自动 * - auto: 完全自动
*/ */
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto'; export type RunMode = "plan" | "ask" | "agent" | "auto";
/**
* 服务等级类型
* - lite: 轻量级
* - syntaxic: 语法级
* - max: 最大性能
* - auto: 自动选择
*/
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
/** /**
* 对话请求 * 对话请求
@ -29,6 +38,8 @@ export interface DialogRequest {
userId: string; userId: string;
/** 运行模式 */ /** 运行模式 */
mode: RunMode; mode: RunMode;
/** 服务等级 */
serviceTier?: ServiceTier;
/** 压缩后的记忆数据(用于后端重启后恢复) */ /** 压缩后的记忆数据(用于后端重启后恢复) */
compactedData?: CompactedMemory; compactedData?: CompactedMemory;
/** 压缩后产生的新消息 */ /** 压缩后产生的新消息 */
@ -41,25 +52,26 @@ export interface DialogRequest {
/** SSE 事件类型枚举 */ /** SSE 事件类型枚举 */
export type SSEEventType = export type SSEEventType =
| 'text_delta' // 文本增量 | "text_delta" // 文本增量
| 'tool_call' // 客户端工具调用请求 | "tool_call" // 客户端工具调用请求
| 'tool_confirm' // 工具确认请求Ask 模式) | "tool_confirm" // 工具确认请求Ask 模式)
| 'plan_confirm' // 计划确认请求Plan 模式) | "plan_confirm" // 计划确认请求Plan 模式)
| 'tool_start' // 工具开始执行 | "tool_start" // 工具开始执行
| 'tool_complete' // 工具执行完成 | "tool_complete" // 工具执行完成
| 'tool_error' // 工具执行错误 | "tool_error" // 工具执行错误
| 'ask_user' // 向用户提问 | "ask_user" // 向用户提问
| 'agent_start' // 子智能体启动 | "agent_start" // 子智能体启动
| 'agent_progress' // 子智能体进度 | "agent_progress" // 子智能体进度
| 'agent_complete' // 子智能体完成 | "agent_complete" // 子智能体完成
| 'agent_error' // 子智能体错误 | "agent_error" // 子智能体错误
| 'memory_compacted' // 记忆压缩完成 | "memory_compacted" // 记忆压缩完成
| 'context_usage' // 上下文使用量 | "context_usage" // 上下文使用量
| 'complete' // 对话完成 | "complete" // 对话完成
| 'error' // 错误 | "error" // 错误
| 'warning' // 警告 | "warning" // 警告
| 'notification' // 通知 | "notification" // 通知
| 'depth_update'; // 深度更新 | "depth_update" // 深度更新
| "heartbeat"; // 心跳
/** text_delta 事件数据 */ /** text_delta 事件数据 */
export interface TextDeltaEvent { export interface TextDeltaEvent {
@ -161,7 +173,7 @@ export interface AgentProgressEvent {
toolName: string; toolName: string;
toolInput?: unknown; toolInput?: unknown;
toolResult?: string; toolResult?: string;
status: 'running' | 'completed' | 'error'; status: "running" | "completed" | "error";
timestamp: number; timestamp: number;
} }
@ -197,11 +209,11 @@ export interface ContextUsageEvent {
*/ */
export interface ToolCallRequest { export interface ToolCallRequest {
/** JSON-RPC版本固定为"2.0" */ /** JSON-RPC版本固定为"2.0" */
jsonrpc: '2.0'; jsonrpc: "2.0";
/** 请求ID用于匹配响应 */ /** 请求ID用于匹配响应 */
id: number; id: number;
/** 方法名,固定为"tools/call" */ /** 方法名,固定为"tools/call" */
method: 'tools/call'; method: "tools/call";
/** 调用参数 */ /** 调用参数 */
params: { params: {
/** 工具名称 */ /** 工具名称 */
@ -217,7 +229,7 @@ export interface ToolCallRequest {
*/ */
export interface ToolCallResult { export interface ToolCallResult {
/** JSON-RPC版本 */ /** JSON-RPC版本 */
jsonrpc: '2.0'; jsonrpc: "2.0";
/** 请求ID与ToolCallRequest.id对应 */ /** 请求ID与ToolCallRequest.id对应 */
id: number; id: number;
/** 执行结果与error互斥 */ /** 执行结果与error互斥 */
@ -298,19 +310,110 @@ export interface ToolConfirmResponse {
approved: boolean; 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;
}
// ============== 辅助类型 ============== // ============== 辅助类型 ==============
/** 后端工具名称 */ /** 后端工具名称 */
export type ToolName = export type ToolName =
| 'file_read' | "file_read"
| 'file_write' | "file_write"
| 'file_delete' | "file_delete"
| 'file_list' | "file_list"
| 'syntax_check' | "syntax_check"
| 'simulation' | "simulation"
| 'waveform_summary' | "waveform_summary"
| 'knowledge_save' | "waveform_trace"
| 'knowledge_load'; | "knowledge_save"
| "knowledge_load";
/** file_read 工具参数 */ /** file_read 工具参数 */
export interface FileReadArgs { export interface FileReadArgs {
@ -354,6 +457,18 @@ export interface WaveformSummaryArgs {
checkpoints?: string; checkpoints?: string;
} }
/** waveform_trace 工具参数 */
export interface WaveformTraceArgs {
/** Verilog 源文件路径(相对于项目根目录) */
verilogPath: string;
/** VCD 波形文件路径(相对于项目根目录) */
vcdPath: string;
/** 仿真工具的输出字符串(包含 mismatch 信息) */
simOutput: string;
/** BFS 回溯层数,默认 2 */
traceLevel?: number;
}
/** knowledge_save 工具参数 */ /** knowledge_save 工具参数 */
export interface KnowledgeSaveArgs { export interface KnowledgeSaveArgs {
/** 知识图谱 JSON 数据 */ /** 知识图谱 JSON 数据 */
@ -374,5 +489,6 @@ export type ToolArgs =
| SyntaxCheckArgs | SyntaxCheckArgs
| SimulationArgs | SimulationArgs
| WaveformSummaryArgs | WaveformSummaryArgs
| WaveformTraceArgs
| KnowledgeSaveArgs | KnowledgeSaveArgs
| KnowledgeLoadArgs; | KnowledgeLoadArgs;

View File

@ -19,7 +19,7 @@ import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction"; import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient"; import { healthCheck } from "../services/apiClient";
import type { RunMode } from "../types/api"; import type { RunMode, ServiceTier } from "../types/api";
/** 是否使用后端服务(可通过配置控制) */ /** 是否使用后端服务(可通过配置控制) */
let useBackendService = true; let useBackendService = true;
@ -58,7 +58,8 @@ export async function handleUserMessage(
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
text: string, text: string,
extensionPath?: string, extensionPath?: string,
mode?: RunMode mode?: RunMode,
serviceTier?: ServiceTier // 新增:服务等级参数
) { ) {
console.log("收到用户消息:", text); console.log("收到用户消息:", text);
@ -90,7 +91,7 @@ export async function handleUserMessage(
// 尝试使用后端服务 // 尝试使用后端服务
if (useBackendService && extensionPath) { if (useBackendService && extensionPath) {
try { try {
await handleUserMessageWithBackend(panel, text, extensionPath, mode); await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier);
return; return;
} catch (error) { } catch (error) {
console.error("后端服务不可用:", error); console.error("后端服务不可用:", error);
@ -125,19 +126,22 @@ async function handleUserMessageWithBackend(
text: string, text: string,
extensionPath: string, extensionPath: string,
mode?: RunMode, mode?: RunMode,
reuseTaskId?: string // 可选,复用现有 taskId用于 Plan 模式确认后继续执行) reuseTaskId?: string, // 可选,复用现有 taskId用于 Plan 模式确认后继续执行)
serviceTier?: ServiceTier // 新增:服务等级参数
): Promise<void> { ): Promise<void> {
const historyManager = ChatHistoryManager.getInstance();
// 获取 historyManager 中的 taskId由 ICHelperPanel 创建)
// 优先使用 reuseTaskId其次使用 historyManager 的 taskId
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
// 创建或复用会话 // 创建或复用会话
if (!currentSession || !currentSession.active) { if (!currentSession || !currentSession.active) {
currentSession = dialogManager.createSession(extensionPath, reuseTaskId); currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
// 保存 taskId 用于后续操作(如压缩) // 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId(); lastTaskId = currentSession.getTaskId();
if (reuseTaskId) { console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId);
} }
}
const historyManager = ChatHistoryManager.getInstance();
// 显示状态栏 // 显示状态栏
panel.webview.postMessage({ panel.webview.postMessage({
@ -196,10 +200,6 @@ async function handleUserMessageWithBackend(
// 最后一次发送完整的段落 // 最后一次发送完整的段落
console.log("[MessageHandler] 对话完成, 段落数:", segments.length); console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
console.log(
"[MessageHandler] segments 内容:",
JSON.stringify(segments)
);
const result = await panel.webview.postMessage({ const result = await panel.webview.postMessage({
command: "updateSegments", command: "updateSegments",
@ -289,7 +289,8 @@ async function handleUserMessageWithBackend(
}); });
}, },
}, },
mode mode,
serviceTier // 传递服务等级
); );
}); });
} }

467
src/utils/vcdParser.ts Normal file
View File

@ -0,0 +1,467 @@
/**
* VCD (Value Change Dump) 解析器
* 纯 TypeScript 实现,参照 VerilogCoder 项目格式
*
* @deprecated 当前未使用,保留备用
* 目前使用 waveformTracer.ts 调用 Python 打包的 waveform_trace.exe
* 未来可能用此文件替换 Python 实现
*/
import * as fs from 'fs';
import * as path from 'path';
// ==================== 类型定义 ====================
/** 信号定义 */
export interface VcdSignal {
name: string; // 完整路径名,如 "tb.top_module.data"
shortName: string; // 短名,如 "data"
symbolId: string; // VCD 符号 ID如 "!", "#"
width: number; // 位宽
varType: string; // 变量类型wire, reg
module: string; // 所属模块
}
/** 时间-值对 */
export interface TimeValue {
time: number;
value: string;
}
/** 信号波形数据 */
export interface SignalWaveform {
signal: VcdSignal;
changes: TimeValue[];
}
/** VCD 解析结果 */
export interface VcdData {
date?: string;
version?: string;
timescale: string;
endTime: number;
signals: Map<string, VcdSignal>; // symbolId -> signal
waveforms: Map<string, TimeValue[]>; // symbolId -> changes
}
/** Mismatch 信息 */
export interface MismatchInfo {
time: number;
signal: string;
dutValue: string;
refValue: string;
}
// ==================== VCD 解析器 ====================
export class VcdParser {
private signals: Map<string, VcdSignal> = new Map();
private waveforms: Map<string, TimeValue[]> = new Map();
private scopeStack: string[] = [];
private timescale: string = '1ns';
private currentTime: number = 0;
private endTime: number = 0;
private date?: string;
private version?: string;
/**
* 解析 VCD 文件
*/
parse(filePath: string): VcdData {
const content = fs.readFileSync(filePath, 'utf-8');
return this.parseContent(content);
}
/**
* 解析 VCD 内容
*/
parseContent(content: string): VcdData {
// 预处理:将多行指令合并成单行
const normalizedContent = this.normalizeVcdContent(content);
const lines = normalizedContent.split('\n');
let inDefinitions = true;
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) continue;
if (inDefinitions) {
// 解析定义区
if (line.startsWith('$enddefinitions')) {
inDefinitions = false;
continue;
}
this.parseDefinition(line);
} else {
// 解析数据区
this.parseValueChange(line);
}
}
return {
date: this.date,
version: this.version,
timescale: this.timescale,
endTime: this.endTime,
signals: this.signals,
waveforms: this.waveforms
};
}
private parseDefinition(line: string): void {
if (line.startsWith('$date')) {
this.date = this.extractValue(line);
} else if (line.startsWith('$version')) {
this.version = this.extractValue(line);
} else if (line.startsWith('$timescale')) {
this.timescale = this.extractValue(line) || '1ns';
} else if (line.startsWith('$scope')) {
const match = line.match(/\$scope\s+\w+\s+(\S+)/);
if (match) {
this.scopeStack.push(match[1]);
}
} else if (line.startsWith('$upscope')) {
this.scopeStack.pop();
} else if (line.startsWith('$var')) {
this.parseVariable(line);
}
}
private parseVariable(line: string): void {
// $var wire 8 # data [7:0] $end
// $var reg 1 ! clk $end
const match = line.match(/\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+(\S+)/);
if (!match) return;
const [, varType, widthStr, symbolId, name] = match;
const width = parseInt(widthStr, 10);
const module = this.scopeStack.join('.');
const fullName = module ? `${module}.${name}` : name;
const signal: VcdSignal = {
name: fullName,
shortName: name.replace(/\[\d+:\d+\]/, ''), // 移除位宽标注
symbolId,
width,
varType,
module
};
this.signals.set(symbolId, signal);
this.waveforms.set(symbolId, []);
}
private parseValueChange(line: string): void {
if (line.startsWith('#')) {
// 时间戳: #100
this.currentTime = parseInt(line.substring(1), 10);
this.endTime = Math.max(this.endTime, this.currentTime);
} else if (line.startsWith('b') || line.startsWith('B')) {
// 多位值: b10101010 #
const spaceIdx = line.indexOf(' ');
if (spaceIdx > 0) {
const value = line.substring(1, spaceIdx);
const symbolId = line.substring(spaceIdx + 1).trim();
this.addChange(symbolId, value);
}
} else if (line.length >= 2 && !line.startsWith('$')) {
// 单位值: 0! 或 1# 或 x$
const value = line[0];
const symbolId = line.substring(1).trim();
if (symbolId && this.signals.has(symbolId)) {
this.addChange(symbolId, value);
}
}
}
private addChange(symbolId: string, value: string): void {
const changes = this.waveforms.get(symbolId);
if (changes) {
changes.push({ time: this.currentTime, value });
}
}
private extractValue(line: string): string {
// 提取 $xxx value $end 中的 value
const match = line.match(/\$\w+\s+(.+?)\s*\$end/);
return match ? match[1].trim() : '';
}
/**
* 预处理 VCD 内容,将多行指令合并成单行
*/
private normalizeVcdContent(content: string): string {
// 将多行 $xxx ... $end 合并成单行
return content.replace(/(\$\w+)\s*\n\s*([^\$]+?)\s*\n\s*(\$end)/g, '$1 $2 $3');
}
}
// ==================== 波形分析工具 ====================
/**
* 二进制字符串转十六进制
*/
export function binaryToHex(binary: string): string {
if (binary === 'x' || binary === 'X' || binary.includes('x')) {
return 'xx';
}
if (binary === 'z' || binary === 'Z' || binary.includes('z')) {
return 'zz';
}
if (binary.length <= 1) {
return binary;
}
// 补齐到 4 的倍数
const padded = binary.padStart(Math.ceil(binary.length / 4) * 4, '0');
let hex = '';
for (let i = 0; i < padded.length; i += 4) {
hex += parseInt(padded.substring(i, i + 4), 2).toString(16);
}
return hex;
}
/**
* 获取信号在指定时间的值
*/
export function getValueAtTime(
changes: TimeValue[],
time: number
): string {
let value = 'x';
for (const change of changes) {
if (change.time <= time) {
value = change.value;
} else {
break;
}
}
return value;
}
/**
* 查找 DUT 和 REF 信号的第一个 mismatch
*/
export function findFirstMismatch(
vcdData: VcdData,
dutSignals: string[],
refSignals: string[]
): MismatchInfo | null {
// 收集所有时间点
const allTimes = new Set<number>();
for (const changes of vcdData.waveforms.values()) {
for (const c of changes) {
allTimes.add(c.time);
}
}
const sortedTimes = Array.from(allTimes).sort((a, b) => a - b);
// 按信号名匹配 DUT 和 REF
for (const time of sortedTimes) {
for (let i = 0; i < dutSignals.length; i++) {
const dutSig = findSignalByName(vcdData, dutSignals[i]);
const refSig = findSignalByName(vcdData, refSignals[i]);
if (!dutSig || !refSig) continue;
const dutChanges = vcdData.waveforms.get(dutSig.symbolId) || [];
const refChanges = vcdData.waveforms.get(refSig.symbolId) || [];
const dutVal = getValueAtTime(dutChanges, time);
const refVal = getValueAtTime(refChanges, time);
// 跳过未知值
if (dutVal.includes('x') || refVal.includes('x')) continue;
if (dutVal !== refVal) {
return {
time,
signal: dutSig.shortName,
dutValue: binaryToHex(dutVal),
refValue: binaryToHex(refVal)
};
}
}
}
return null;
}
/**
* 按名称查找信号
*/
function findSignalByName(vcdData: VcdData, name: string): VcdSignal | null {
for (const signal of vcdData.signals.values()) {
if (signal.name.endsWith(name) || signal.shortName === name) {
return signal;
}
}
return null;
}
/**
* 生成波形表格(参照 VerilogCoder 格式)
*/
export function generateWaveformTable(
vcdData: VcdData,
signalNames: string[],
startTime: number = 0,
endTime?: number,
windowSize: number = 20
): string {
const actualEndTime = endTime ?? vcdData.endTime;
// 查找信号
const signals: VcdSignal[] = [];
for (const name of signalNames) {
const sig = findSignalByName(vcdData, name);
if (sig) signals.push(sig);
}
if (signals.length === 0) {
return '未找到指定信号';
}
// 收集时间点
const times = new Set<number>();
for (const sig of signals) {
const changes = vcdData.waveforms.get(sig.symbolId) || [];
for (const c of changes) {
if (c.time >= startTime && c.time <= actualEndTime) {
times.add(c.time);
}
}
}
let sortedTimes = Array.from(times).sort((a, b) => a - b);
if (sortedTimes.length > windowSize) {
sortedTimes = sortedTimes.slice(0, windowSize);
}
// 生成表头
const headers = ['time(ns)', ...signals.map(s => s.shortName)];
const colWidths = headers.map(h => Math.max(h.length, 8));
let table = '### Waveform Trace ###\n';
table += headers.map((h, i) => h.padEnd(colWidths[i])).join(' ') + '\n';
table += colWidths.map(w => '─'.repeat(w)).join('──') + '\n';
// 生成数据行
for (const time of sortedTimes) {
const row = [time.toString()];
for (const sig of signals) {
const changes = vcdData.waveforms.get(sig.symbolId) || [];
const val = getValueAtTime(changes, time);
row.push(binaryToHex(val));
}
table += row.map((v, i) => v.padEnd(colWidths[i])).join(' ') + '\n';
}
table += '### Waveform Trace End ###\n';
return table;
}
/**
* 获取信号显示名称(模块.信号名[位宽]
*/
function getSignalDisplayName(sig: VcdSignal): string {
const moduleParts = sig.module.split('.');
const moduleShort = moduleParts[moduleParts.length - 1] || '';
const bitInfo = sig.width > 1 ? `[${sig.width - 1}:0]` : '';
return moduleShort ? `${moduleShort}.${sig.shortName}${bitInfo}` : `${sig.shortName}${bitInfo}`;
}
/**
* 生成变化日志格式(只记录信号变化)
*/
export function generateChangeLog(vcdData: VcdData): string {
// 筛选信号(排除 parameter
const signals: VcdSignal[] = [];
for (const sig of vcdData.signals.values()) {
if (sig.varType !== 'parameter') {
signals.push(sig);
}
}
// 收集所有时间点
const times = new Set<number>();
for (const sig of signals) {
const changes = vcdData.waveforms.get(sig.symbolId) || [];
for (const c of changes) {
times.add(c.time);
}
}
const sortedTimes = Array.from(times).sort((a, b) => a - b);
// 记录每个信号的上一个值
const lastValues = new Map<string, string | null>();
for (const sig of signals) {
lastValues.set(sig.symbolId, null);
}
let log = '';
for (const time of sortedTimes) {
const changes: string[] = [];
for (const sig of signals) {
const waveform = vcdData.waveforms.get(sig.symbolId) || [];
const currentVal = binaryToHex(getValueAtTime(waveform, time));
const lastVal = lastValues.get(sig.symbolId);
if (lastVal === null) {
changes.push(`${getSignalDisplayName(sig)}=${currentVal}`);
} else if (currentVal !== lastVal) {
changes.push(`${getSignalDisplayName(sig)} ${lastVal}${currentVal}`);
}
lastValues.set(sig.symbolId, currentVal);
}
if (changes.length > 0) {
log += `#${time}: ${changes.join(', ')}\n`;
}
}
return log;
}
/**
* 分析 VCD 文件(主入口)
*/
export function analyzeVcdFile(
filePath: string,
signalFilter?: string,
checkpoint?: number
): string {
// 解析 VCD
const parser = new VcdParser();
const vcdData = parser.parse(filePath);
// 解析信号过滤器
const signalNames = signalFilter
? signalFilter.split(',').map(s => s.trim())
: Array.from(vcdData.signals.values()).map(s => s.shortName);
// 生成摘要
let result = `=== VCD 波形分析 ===\n`;
result += `文件: ${path.basename(filePath)}\n`;
result += `时间单位: ${vcdData.timescale}\n`;
result += `仿真时长: 0 - ${vcdData.endTime}${vcdData.timescale}\n\n`;
// 信号列表
result += `--- 信号列表 (${vcdData.signals.size} 个) ---\n`;
let idx = 1;
for (const sig of vcdData.signals.values()) {
if (idx <= 10) {
result += `${idx}. ${sig.shortName} (${sig.width}-bit, ${sig.varType})\n`;
}
idx++;
}
if (vcdData.signals.size > 10) {
result += `... 还有 ${vcdData.signals.size - 10} 个信号\n`;
}
result += '\n';
// 变化日志
result += generateChangeLog(vcdData);
return result;
}

145
src/utils/waveformTracer.ts Normal file
View File

@ -0,0 +1,145 @@
/**
* 波形追踪工具
* 调用 PyInstaller 打包的 waveform_trace 可执行文件
*/
import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import * as vscode from 'vscode';
/**
* 波形追踪参数
*/
export interface WaveformTraceArgs {
/** Verilog 源文件路径(相对于项目根目录) */
verilogPath: string;
/** VCD 波形文件路径(相对于项目根目录) */
vcdPath: string;
/** 仿真工具的输出字符串(包含 mismatch 信息) */
simOutput: string;
/** BFS 回溯层数,默认 2 */
traceLevel?: number;
}
/**
* 执行波形追踪
* @param args 追踪参数
* @param context 执行上下文
* @returns 追踪结果字符串
*/
export async function executeWaveformTrace(
args: WaveformTraceArgs,
context: { extensionPath: string }
): Promise<string> {
// 获取可执行文件路径
const tracerPath = getWaveformTracerPath(context.extensionPath);
// 检查可执行文件是否存在
if (!fs.existsSync(tracerPath)) {
throw new Error(
`waveform_trace 工具未安装: ${tracerPath}\n` +
'请确保插件包含 tools/waveform_trace/bin/ 目录'
);
}
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
}
const workspacePath = workspaceFolders[0].uri.fsPath;
// 解析路径(支持相对路径)
const verilogAbsPath = path.isAbsolute(args.verilogPath)
? args.verilogPath
: path.join(workspacePath, args.verilogPath);
const vcdAbsPath = path.isAbsolute(args.vcdPath)
? args.vcdPath
: path.join(workspacePath, args.vcdPath);
// 验证文件存在
if (!fs.existsSync(verilogAbsPath)) {
throw new Error(`Verilog 文件不存在: ${args.verilogPath}`);
}
if (!fs.existsSync(vcdAbsPath)) {
throw new Error(`VCD 文件不存在: ${args.vcdPath}`);
}
// 调用可执行文件
return new Promise((resolve, reject) => {
const child = spawn(tracerPath, [
'--verilog', verilogAbsPath,
'--vcd', vcdAbsPath,
'--sim-output', args.simOutput,
'--trace-level', String(args.traceLevel || 2),
'--output-format', 'text'
], {
windowsHide: true,
cwd: workspacePath,
shell: false
});
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 | null) => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`
));
}
});
child.on('error', (error: Error) => {
reject(new Error(`waveform_trace 启动失败: ${error.message}`));
});
});
}
/**
* 获取 waveform_trace 可执行文件路径
*/
function getWaveformTracerPath(extensionPath: string): string {
const platform = process.platform;
let binName = 'waveform_trace';
if (platform === 'win32') {
binName = 'waveform_trace.exe';
}
return path.join(extensionPath, 'tools', 'waveform_trace', 'bin', binName);
}
/**
* 检查 waveform_trace 工具是否可用
*/
export function checkWaveformTraceAvailable(extensionPath: string): {
available: boolean;
message: string;
path?: string;
} {
const tracerPath = getWaveformTracerPath(extensionPath);
if (fs.existsSync(tracerPath)) {
return {
available: true,
message: 'waveform_trace 工具可用',
path: tracerPath
};
} else {
return {
available: false,
message: `waveform_trace 工具未找到: ${tracerPath}`
};
}
}

View File

@ -98,24 +98,24 @@ export function getAgentCardStyles(): string {
} }
/* 低调显示的工具调用样式 */ /* 低调显示的工具调用样式 */
.agent-step.low-profile { .agent-step.low-profile {
opacity: 0.5; opacity: 0.85;
font-size: 10px; font-size: 12px;
padding: 2px 6px; padding: 4px 8px;
background: transparent; background: transparent;
margin-bottom: 2px; margin-bottom: 2px;
} }
.agent-step.low-profile .step-icon { .agent-step.low-profile .step-icon {
opacity: 0.4; opacity: 0.8;
font-size: 10px; font-size: 12px;
} }
.agent-step.low-profile .step-name { .agent-step.low-profile .step-name {
font-weight: 300; font-weight: 400;
color: var(--vscode-descriptionForeground); color: var(--vscode-descriptionForeground);
opacity: 0.7; opacity: 0.9;
} }
.agent-step.low-profile .step-result { .agent-step.low-profile .step-result {
opacity: 0.6; opacity: 0.85;
font-size: 9px; font-size: 11px;
} }
`; `;
} }
@ -147,7 +147,11 @@ export function getAgentCardScript(): string {
'addPlan': '添加计划', 'addPlan': '添加计划',
'addEdge': '添加边', 'addEdge': '添加边',
'showPlan': '显示计划', 'showPlan': '显示计划',
'spawnExplorer': '代码探索' 'spawnExplorer': '代码探索',
'spawnDebugger': '波形调试',
'queryByBFS': 'BFS查询',
'queryStateTransitions': '查询状态转移',
'addStateTransition': '添加状态转移'
}; };
return toolNameMap[toolName] || toolName; return toolNameMap[toolName] || toolName;
} }
@ -168,15 +172,8 @@ export function getAgentCardScript(): string {
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄'; const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
const displayName = getAgentToolDisplayName(step.toolName); const displayName = getAgentToolDisplayName(step.toolName);
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : ''; const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
// 为技术性工具调用添加低调样式(用户看不懂的) // 所有工具调用都使用低调样式
const lowProfileTools = [ const stepClass = 'agent-step low-profile';
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'spawnExplorer'
];
const stepClass = lowProfileTools.includes(step.toolName) ? 'agent-step low-profile' : 'agent-step';
return \`<div class="\${stepClass}"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`; return \`<div class="\${stepClass}"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
}).join(''); }).join('');

View File

@ -1,3 +1,10 @@
import {
getUserInfoComponentContent,
getUserInfoComponentStyles,
getUserInfoComponentScript,
} from "./userInfoComponent";
import { userAvatarIconSvg } from "../constants/toolIcons";
/** /**
* 获取会话历史栏的 HTML 内容 * 获取会话历史栏的 HTML 内容
*/ */
@ -6,7 +13,7 @@ export function getConversationHistoryBarContent(): string {
<div class="conversation-history-bar"> <div class="conversation-history-bar">
<div class="history-dropdown-container"> <div class="history-dropdown-container">
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()"> <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"> <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"/> <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> </svg>
@ -19,11 +26,20 @@ export function getConversationHistoryBarContent(): string {
</div> </div>
</div> </div>
<div class="right-actions">
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话"> <button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/> <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
</svg> </svg>
</button> </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> </div>
`; `;
} }
@ -49,13 +65,59 @@ export function getConversationHistoryBarStyles(): string {
flex: 1; 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 { .history-dropdown-button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 8px 12px; padding: 8px 12px;
background: transparent; background: transparent;
color: var(--vscode-input-foreground); color: var(--vscode-foreground);
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@ -64,7 +126,7 @@ export function getConversationHistoryBarStyles(): string {
} }
.history-dropdown-button:hover { .history-dropdown-button:hover {
opacity: 0.8; background: var(--vscode-toolbar-hoverBackground);
} }
.dropdown-label { .dropdown-label {
@ -163,7 +225,7 @@ export function getConversationHistoryBarStyles(): string {
background: transparent; background: transparent;
color: var(--vscode-foreground); color: var(--vscode-foreground);
border: none; border: none;
border-radius: 4px; border-radius: 50%;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@ -173,11 +235,12 @@ export function getConversationHistoryBarStyles(): string {
} }
.new-conversation-button:hover { .new-conversation-button:hover {
opacity: 0.7; background: var(--vscode-toolbar-hoverBackground);
transform: scale(1.1);
} }
.new-conversation-button:active { .new-conversation-button:active {
opacity: 0.5; transform: scale(0.95);
} }
.new-conversation-button svg { .new-conversation-button svg {
@ -210,6 +273,29 @@ export function getConversationHistoryBarStyles(): string {
*/ */
export function getConversationHistoryBarScript(): string { export function getConversationHistoryBarScript(): string {
return ` 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 conversationHistory = [];
let currentConversationId = null; let currentConversationId = null;

View File

@ -23,6 +23,7 @@ import {
waveformIconSvg, waveformIconSvg,
knowledgeLoadIconSvg, knowledgeLoadIconSvg,
stateTransitionIconSvg, stateTransitionIconSvg,
userQuestionIconSvg,
} from "../constants/toolIcons"; } from "../constants/toolIcons";
import { import {
getWaveformPreviewContent, getWaveformPreviewContent,
@ -30,6 +31,10 @@ import {
} from "./waveformPreviewContent"; } from "./waveformPreviewContent";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard"; import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import { getPlanCardStyles, getPlanCardScript } from "./planCard"; import { getPlanCardStyles, getPlanCardScript } from "./planCard";
import {
getCodeHighlightStyles,
getCodeHighlightScript,
} from "../components/codeHighlight";
/** /**
* 获取消息区域的 HTML 内容 * 获取消息区域的 HTML 内容
@ -294,7 +299,7 @@ export function getMessageAreaStyles(): string {
padding: 0; padding: 0;
} }
.message-segment { .message-segment {
padding: 10px 22px; padding: 10px 0;
} }
.segment-text { .segment-text {
line-height: 1.6; line-height: 1.6;
@ -303,71 +308,69 @@ export function getMessageAreaStyles(): string {
/* Markdown 样式 */ /* Markdown 样式 */
.segment-text h1, .segment-text h1,
.segment-text h2, .segment-text h2,
.segment-text h3 { .segment-text h3,
margin: 16px 0 8px 0; .question-text h1,
.question-text h2,
.question-text h3 {
margin: 0px 0 -10px 0;
font-weight: 600; font-weight: 600;
line-height: 1.3; line-height: 1.3;
} }
.segment-text h1 { .segment-text h1,
.question-text h1 {
font-size: 1.5em; font-size: 1.5em;
border-bottom: 1px solid var(--vscode-panel-border); border-bottom: 1px solid var(--vscode-panel-border);
padding-bottom: 8px; padding-bottom: 8px;
} }
.segment-text h2 { .segment-text h2,
.question-text h2 {
font-size: 1.3em; font-size: 1.3em;
} }
.segment-text h3 { .segment-text h3,
.question-text h3 {
font-size: 1.1em; font-size: 1.1em;
} }
.segment-text pre {
background: var(--vscode-textCodeBlock-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 12px;
overflow-x: auto;
margin: 12px 0;
}
.segment-text code {
font-family: 'Courier New', Consolas, monospace;
font-size: 0.9em;
}
.segment-text pre code {
background: transparent;
padding: 0;
border: none;
}
.segment-text code:not(pre code) {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
color: var(--vscode-textPreformat-foreground);
}
.segment-text ul, .segment-text ul,
.segment-text ol { .segment-text ol,
.question-text ul,
.question-text ol {
margin: 8px 0; margin: 8px 0;
padding-left: 24px; padding-left: 24px;
} }
.segment-text li { .segment-text li,
margin: 4px 0; .question-text li {
line-height: 1.6; line-height: 1;
} }
.segment-text strong { .segment-text strong,
.question-text strong {
font-weight: 600; font-weight: 600;
color: var(--vscode-foreground); color: var(--vscode-foreground);
} }
.segment-text em { .segment-text em,
.question-text em {
font-style: italic; font-style: italic;
} }
.segment-text a { .segment-text a,
.question-text a {
color: var(--vscode-textLink-foreground); color: var(--vscode-textLink-foreground);
text-decoration: none; text-decoration: none;
} }
.segment-text a:hover { .segment-text a:hover,
.question-text a:hover {
text-decoration: underline; text-decoration: underline;
} }
.segment-text p { .segment-text p,
.question-text p {
margin: 8px 0; margin: 8px 0;
} }
.segment-text code,
.question-text code {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
font-family: var(--vscode-editor-font-family);
font-size: 0.9em;
}
.segment-tool { .segment-tool {
margin: 4px 0; margin: 4px 0;
@ -375,7 +378,7 @@ export function getMessageAreaStyles(): string {
} }
/* 低调显示的工具调用 - 移除边距和背景 */ /* 低调显示的工具调用 - 移除边距和背景 */
.segment-tool.low-profile { .segment-tool.low-profile {
margin: 2px 0; margin: 2px 0px;
padding: 0; padding: 0;
background: none; background: none;
} }
@ -541,7 +544,7 @@ export function getMessageAreaStyles(): string {
/* 低调显示的工具调用样式 */ /* 低调显示的工具调用样式 */
.segment-tool.low-profile .tool-segment-header { .segment-tool.low-profile .tool-segment-header {
opacity: 0.65; opacity: 0.65;
font-size: 11px; font-size: 12px;
} }
.segment-tool.low-profile .tool-segment-icon { .segment-tool.low-profile .tool-segment-icon {
opacity: 0.55; opacity: 0.55;
@ -559,7 +562,7 @@ export function getMessageAreaStyles(): string {
background: var(--vscode-textBlockQuote-background); background: var(--vscode-textBlockQuote-background);
border-radius: 6px; border-radius: 6px;
margin: 8px 0; margin: 8px 0;
padding: 12px 14px; padding: 12px 35px;
border-left: 3px solid var(--vscode-charts-orange); border-left: 3px solid var(--vscode-charts-orange);
} }
.segment-question .question-text { .segment-question .question-text {
@ -607,6 +610,7 @@ export function getMessageAreaStyles(): string {
border: 1px solid var(--vscode-input-border); border: 1px solid var(--vscode-input-border);
border-radius: 6px; border-radius: 6px;
font-size: 13px; font-size: 13px;
margin-left: -20px;
} }
.segment-question .custom-submit { .segment-question .custom-submit {
padding: 8px 16px; padding: 8px 16px;
@ -642,6 +646,8 @@ export function getMessageAreaStyles(): string {
${getPlanCardStyles()} ${getPlanCardStyles()}
${getCodeHighlightStyles()}
${getWaveformPreviewContent()} ${getWaveformPreviewContent()}
`; `;
} }
@ -663,6 +669,7 @@ export function getMessageAreaScript(): string {
const waveformIconSvg = \`${waveformIconSvg}\`; const waveformIconSvg = \`${waveformIconSvg}\`;
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`; const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`; const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
${getAgentCardScript()} ${getAgentCardScript()}
@ -692,7 +699,8 @@ export function getMessageAreaScript(): string {
'showPlan': searchCodeIconSvg, 'showPlan': searchCodeIconSvg,
'addRule': fileWriteIconSvg, 'addRule': fileWriteIconSvg,
'updateNode': fileWriteIconSvg, 'updateNode': fileWriteIconSvg,
'addStateTransition': stateTransitionIconSvg 'addStateTransition': stateTransitionIconSvg,
'askUser': userQuestionIconSvg,
}; };
return iconMap[toolName] || ''; return iconMap[toolName] || '';
} }
@ -722,21 +730,46 @@ export function getMessageAreaScript(): string {
'addRule': '已添加规则', 'addRule': '已添加规则',
'updateNode': '已更新节点', 'updateNode': '已更新节点',
'addStateTransition': '已添加状态转换', 'addStateTransition': '已添加状态转换',
'spawnExplorer': '代码探索' 'spawnExplorer': '代码探索',
'spawnDebugger': '波形调试',
'askUser': '用户提问',
}; };
return toolNameMap[toolName] || toolName; return toolNameMap[toolName] || toolName;
} }
// 自动滚动控制标志
let shouldAutoScroll = true;
let lastScrollHeight = 0;
// 检查用户是否在底部附近允许50px的误差 // 检查用户是否在底部附近允许50px的误差
function isUserNearBottom() { function isUserNearBottom() {
const threshold = 50; const threshold = 50;
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold; 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() { function smartScrollToBottom() {
if (isUserNearBottom()) { if (shouldAutoScroll) {
messagesEl.scrollTop = messagesEl.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
lastScrollHeight = messagesEl.scrollHeight;
} }
} }
@ -919,6 +952,7 @@ export function getMessageAreaScript(): string {
// 实时更新分段消息(按后端返回顺序) // 实时更新分段消息(按后端返回顺序)
function updateSegmentsRealtime(segments, isComplete) { function updateSegmentsRealtime(segments, isComplete) {
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete); console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
if (!segments || segments.length === 0) { if (!segments || segments.length === 0) {
console.log('[WebView] segments 为空,跳过渲染'); console.log('[WebView] segments 为空,跳过渲染');
return; return;
@ -995,17 +1029,8 @@ export function getMessageAreaScript(): string {
return; return;
} }
// 为技术性工具调用添加低调样式 // 所有工具调用都使用低调样式
const lowProfileTools = [
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
];
if (lowProfileTools.includes(segment.toolName)) {
segmentDiv.className += ' low-profile'; segmentDiv.className += ' low-profile';
}
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || ''; const toolResult = segment.toolResult || '';
@ -1098,7 +1123,7 @@ export function getMessageAreaScript(): string {
: ''; : '';
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="question-text">\${segment.question || ''}</div> <div class="question-text">\${formatText(segment.question || '')}</div>
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''} \${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};"> <div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" /> <input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
@ -1257,17 +1282,8 @@ export function getMessageAreaScript(): string {
return; return;
} }
// 为技术性工具调用添加低调样式 // 所有工具调用都使用低调样式
const lowProfileTools = [
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
];
if (lowProfileTools.includes(segment.toolName)) {
segmentDiv.className += ' low-profile'; segmentDiv.className += ' low-profile';
}
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || ''; const toolResult = segment.toolResult || '';
@ -1335,7 +1351,7 @@ export function getMessageAreaScript(): string {
} else if (segment.type === 'question') { } else if (segment.type === 'question') {
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="question-segment"> <div class="question-segment">
<div class="question-text">\${segment.question || ''}</div> <div class="question-text">\${formatText(segment.question || '')}</div>
<div class="question-options"> <div class="question-options">
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')} \${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
</div> </div>
@ -1393,20 +1409,40 @@ export function getMessageAreaScript(): string {
function formatText(text) { function formatText(text) {
if (!text) return ''; if (!text) return '';
// 先转义 HTML 特殊字符 let html = text;
let html = text
// 先提取并处理代码块(避免被转义)
const codeBlocks = [];
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
const language = lang || 'plaintext';
// 转义代码内容
const escapedCode = code.trim()
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;'); .replace(/>/g, '&gt;');
// 不再手动高亮,让 highlight.js 处理
// 处理代码块(三个反引号包裹的代码) const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`;
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) { codeBlocks.push('<pre><code class="language-' + language + '">' + escapedCode + '</code></pre>');
const language = lang || 'plaintext'; return placeholder;
return '<pre><code class="language-' + language + '">' + code.trim() + '</code></pre>';
}); });
// 处理行内代码(单个反引号包裹 // 提取行内代码(避免被转义
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>'); const inlineCodes = [];
html = html.replace(/\`([^\`]+)\`/g, function(match, code) {
const escapedCode = code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`;
inlineCodes.push('<code>' + escapedCode + '</code>');
return placeholder;
});
// 转义其他 HTML 特殊字符
html = html
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 处理标题 ### Title // 处理标题 ### Title
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>'); html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
@ -1429,9 +1465,19 @@ export function getMessageAreaScript(): string {
// 处理链接 [text](url) // 处理链接 [text](url)
html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '<a href="$2" target="_blank">$1</a>'); html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
// 处理换行 // 处理换行(在恢复代码块之前)
html = html.replace(/\\n/g, '<br>'); html = html.replace(/\\n/g, '<br>');
// 恢复代码块(在最后恢复,避免被其他处理影响)
codeBlocks.forEach((block, index) => {
html = html.replace(\`___CODE_BLOCK_\${index}___\`, block);
});
// 恢复行内代码
inlineCodes.forEach((code, index) => {
html = html.replace(\`___INLINE_CODE_\${index}___\`, code);
});
return html; return html;
} }
@ -1581,5 +1627,7 @@ export function getMessageAreaScript(): string {
} }
${getWaveformPreviewScript()} ${getWaveformPreviewScript()}
${getCodeHighlightScript()}
`; `;
} }

View File

@ -25,7 +25,7 @@ export function getProgressBarContent(): string {
<span class="step-number">1</span> <span class="step-number">1</span>
<span class="step-check">✓</span> <span class="step-check">✓</span>
</div> </div>
<div class="step-label">Spec设计文档</div> <div class="step-label">Spec</div>
</div> </div>
<div class="progress-line"></div> <div class="progress-line"></div>
@ -35,7 +35,7 @@ export function getProgressBarContent(): string {
<span class="step-number">2</span> <span class="step-number">2</span>
<span class="step-check">✓</span> <span class="step-check">✓</span>
</div> </div>
<div class="step-label">Design代码编写</div> <div class="step-label">Design</div>
</div> </div>
<div class="progress-line"></div> <div class="progress-line"></div>
@ -45,7 +45,7 @@ export function getProgressBarContent(): string {
<span class="step-number">3</span> <span class="step-number">3</span>
<span class="step-check">✓</span> <span class="step-check">✓</span>
</div> </div>
<div class="step-label">Sim仿真检查</div> <div class="step-label">Simulation</div>
</div> </div>
<div class="progress-line"></div> <div class="progress-line"></div>
@ -55,7 +55,7 @@ export function getProgressBarContent(): string {
<span class="step-number">4</span> <span class="step-number">4</span>
<span class="step-check">✓</span> <span class="step-check">✓</span>
</div> </div>
<div class="step-label">Done完成</div> <div class="step-label">Done</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,287 @@
/**
* 用户信息组件
* 包含用户头像、昵称、会员等级等信息
*/
/**
* 获取用户信息组件的 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');
if (creditsDetail) {
creditsDetail.textContent = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-';
}
}
// 更新用户信息显示
function updateUserInfoDisplay(userInfo) {
currentUserInfo = userInfo;
}
// 绑定下拉面板事件
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();
});
}
});
`;
}

View File

@ -5,65 +5,79 @@ export function getWaveformPreviewContent(): string {
return ` return `
/* 波形预览组件样式 */ /* 波形预览组件样式 */
.waveform-preview { .waveform-preview {
margin-top: 12px; margin: 16px 0;
border: 1px solid var(--vscode-panel-border); border: 1px solid var(--vscode-panel-border);
border-radius: 8px; border-radius: 12px;
overflow: hidden; overflow: hidden;
background: var(--vscode-editor-background); background: var(--vscode-editor-background);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease, transform 0.2s ease;
}
.waveform-preview:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
} }
.waveform-preview-header { .waveform-preview-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px 12px; padding: 14px 16px;
background: var(--vscode-input-background); background: linear-gradient(135deg, var(--vscode-input-background) 0%, var(--vscode-editor-background) 100%);
border-bottom: 1px solid var(--vscode-panel-border); border-bottom: 1px solid var(--vscode-panel-border);
backdrop-filter: blur(10px);
} }
.waveform-preview-title { .waveform-preview-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
font-size: 13px; font-size: 14px;
font-weight: 500; font-weight: 600;
color: var(--vscode-foreground); color: var(--vscode-foreground);
letter-spacing: 0.3px;
} }
.waveform-preview-title svg { .waveform-preview-title svg {
width: 16px; width: 18px;
height: 16px; height: 18px;
color: var(--vscode-button-background); color: var(--vscode-button-background);
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
} }
.waveform-expand-btn { .waveform-expand-btn {
padding: 4px 12px; padding: 6px 14px;
background: var(--vscode-button-background); background: var(--vscode-button-background);
color: var(--vscode-button-foreground); color: var(--vscode-button-foreground);
border: none; border: none;
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
font-weight: 500;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 6px;
transition: opacity 0.2s ease; transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.waveform-expand-btn:hover { .waveform-expand-btn:hover {
opacity: 0.9; background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.waveform-expand-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
} }
.waveform-expand-btn svg { .waveform-expand-btn svg {
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
.waveform-preview-content { .waveform-preview-content {
padding: 0; padding: 12px;
min-height: 200px;
max-height: 300px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background: var(--vscode-editor-background); background: var(--vscode-editor-background);
} }
.waveform-preview-canvas { .waveform-preview-canvas {
width: 100%; width: 100%;
height: 100%; height: auto;
min-height: 200px;
} }
.waveform-preview-placeholder { .waveform-preview-placeholder {
display: flex; display: flex;
@ -88,7 +102,8 @@ export function getWaveformPreviewContent(): string {
} }
.waveform-mini-viewer { .waveform-mini-viewer {
width: 100%; width: 100%;
height: 200px; height: auto;
min-height: 120px;
background: var(--vscode-editor-background); background: var(--vscode-editor-background);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -332,7 +347,7 @@ export function getWaveformPreviewScript(): string {
} }
/** /**
* 打开完整波形查看器 * 打开完整波形查看器(在新列中)
*/ */
function openFullWaveform(vcdFilePath) { function openFullWaveform(vcdFilePath) {
vscode.postMessage({ vscode.postMessage({

View File

@ -23,6 +23,7 @@ import {
getProgressBarStyles, getProgressBarStyles,
getProgressBarScript, getProgressBarScript,
} from "./progressBar"; } from "./progressBar";
import { getHighlightJsLinks } from "../components/codeHighlight";
import { getCurrentEnv } from "../config/settings"; import { getCurrentEnv } from "../config/settings";
/** /**
* 获取 WebView 面板的 HTML 内容 * 获取 WebView 面板的 HTML 内容
@ -44,6 +45,7 @@ export function getWebviewContent(
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IC Coder</title> <title>IC Coder</title>
${getHighlightJsLinks()}
<style> <style>
body { body {
font-family: var(--vscode-font-family); font-family: var(--vscode-font-family);
@ -269,7 +271,7 @@ export function getWebviewContent(
padding: 0; padding: 0;
} }
.message-segment { .message-segment {
padding: 10px 22px; padding: 10px 0;
} }
.segment-text { .segment-text {
line-height: 1.6; line-height: 1.6;
@ -312,7 +314,7 @@ export function getWebviewContent(
background: var(--vscode-textBlockQuote-background); background: var(--vscode-textBlockQuote-background);
border-radius: 6px; border-radius: 6px;
margin: 8px 0; margin: 8px 0;
padding: 12px 14px; padding: 12px 35px;
border-left: 3px solid var(--vscode-charts-orange); border-left: 3px solid var(--vscode-charts-orange);
} }
.question-segment .question-text { .question-segment .question-text {
@ -583,6 +585,27 @@ export function getWebviewContent(
} }
break; break;
case 'updateUserInfo':
// 更新用户信息
console.log('[WebView] 收到用户信息:', message.userInfo);
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
};
console.log('[WebView] 显示用户信息:', userInfoData);
// 调用更新用户头像图标按钮的函数
if (typeof updateUserAvatarIconButton === 'function') {
updateUserAvatarIconButton(userInfoData);
}
}
break;
case 'resetSegmentedMessage': case 'resetSegmentedMessage':
// 重置分段消息容器(停止对话时调用) // 重置分段消息容器(停止对话时调用)
console.log('[WebView] 重置分段消息容器'); console.log('[WebView] 重置分段消息容器');

View File

@ -0,0 +1,42 @@
@echo off
REM waveform_trace 打包脚本 (Windows)
REM 用法: build.bat
echo ========================================
echo waveform_trace 打包脚本
echo ========================================
cd /d "%~dp0src"
echo.
echo [1/3] 安装依赖...
pip install -r requirements.txt
if %errorlevel% neq 0 (
echo 错误: 依赖安装失败
exit /b 1
)
echo.
echo [2/3] 清理旧文件...
if exist build rmdir /s /q build
if exist dist rmdir /s /q dist
if exist waveform_trace.spec del waveform_trace.spec
echo.
echo [3/3] PyInstaller 打包...
pyinstaller --onefile --name waveform_trace --collect-all pyverilog waveform_trace_cli.py
if %errorlevel% neq 0 (
echo 错误: 打包失败
exit /b 1
)
echo.
echo [4/4] 复制到 bin 目录...
if not exist "..\bin" mkdir "..\bin"
copy /y "dist\waveform_trace.exe" "..\bin\"
echo.
echo ========================================
echo 打包完成!
echo 输出: tools/waveform_trace/bin/waveform_trace.exe
echo ========================================

View File

@ -0,0 +1,35 @@
#!/bin/bash
# waveform_trace 打包脚本 (Linux/macOS)
# 用法: ./build.sh
set -e
echo "========================================"
echo " waveform_trace 打包脚本"
echo "========================================"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR/src"
echo ""
echo "[1/4] 安装依赖..."
pip install -r requirements.txt
echo ""
echo "[2/4] 清理旧文件..."
rm -rf build dist *.spec
echo ""
echo "[3/4] PyInstaller 打包..."
pyinstaller --onefile --name waveform_trace --collect-all pyverilog waveform_trace_cli.py
echo ""
echo "[4/4] 复制到 bin 目录..."
mkdir -p ../bin
cp dist/waveform_trace ../bin/
echo ""
echo "========================================"
echo " 打包完成!"
echo " 输出: tools/waveform_trace/bin/waveform_trace"
echo "========================================"

View File

@ -0,0 +1,115 @@
# AST 波形调试核心代码
## 文件说明
| 文件 | 作用 | 核心函数 | TS重写需要 |
|------|------|----------|------------|
| `ast_node.py` | AST节点定义遍历建图 | `toplogic_tree_traverse()` | ✅ 已完成 |
| `graph_builder.py` | 入口函数,调用解析器 | `generate_top_logic_graph()` | ✅ 已完成 |
| `debug_graph_analyzer.py` | BFS回溯控制信号 | `get_k_control_signals()` | ⚠️ 需重写 |
| `vcd_waveform_analyzer.py` | VCD波形文件解析 | `parse_mismatch()`, `get_tabular()` | ⚠️ 需重写 |
| `waveform_trace_tool.py` | 完整追踪工具封装 | `waveform_trace_tool()` | ⚠️ 需重写 |
---
## 调用流程
```
Verilog代码文件
┌─────────────────────────────────────┐
│ graph_builder.py │
│ generate_top_logic_graph(filelist) │
│ │ │
│ ▼ │
│ PyVerilog.parse() → AST │
│ │ │
│ ▼ │
│ ast.toplogic_tree_traverse() │
│ │ │
│ ▼ │
│ NetworkX 有向图(信号依赖图) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ debug_graph_analyzer.py │
│ DebugGraph.get_k_control_signals() │
│ │ │
│ ▼ │
│ BFS回溯K层找到控制信号链 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ vcd_waveform_analyzer.py │
│ parse_mismatch() + get_tabular() │
│ │ │
│ ▼ │
│ 提取相关信号的波形表 │
└─────────────────────────────────────┘
```
---
## 核心代码位置
### 1. AST遍历建图 (ast_node.py:32-137)
```python
def toplogic_tree_traverse(self, network_G, rvalue=False, lvalue=False, offset=0):
"""
递归遍历AST提取信号依赖关系填充到NetworkX图中
关键逻辑:
1. 识别 Rvalue右值和 Lvalue左值
2. 递归收集子节点的信号
3. 建立边:右值信号 → 左值信号(控制关系)
"""
```
### 2. 图构建入口 (graph_builder.py:89-99)
```python
def generate_top_logic_graph(filelist: list[str]):
# 1. PyVerilog解析Verilog代码
ast, directives = parse(filelist, preprocess_include=[], preprocess_define=[])
# 2. 遍历AST构建信号依赖图
return create_graph_from_ast(ast, display=False, display_signal_only=False)
```
### 3. BFS回溯 (debug_graph_analyzer.py:20-66)
```python
def get_k_control_signals(self, target_signals: list[str], k: int, signal_only: bool = False):
"""
从出错信号出发BFS回溯K层找到所有控制信号
输入target_signals = ['out'] # 出错的信号
输出control_signals = {'out': (10,10), 'state': (5,8), 'clk': (1,1)}
signal_level_tracer = [['clk->state', 'reset->state'], ['state->out']]
"""
```
---
## 依赖库
```
pyverilog # Verilog解析生成AST
networkx # 图数据结构
pandas # 波形数据处理(可选)
```
---
## 如果要用JavaScript重写
需要重写的核心逻辑:
1. **Verilog解析器** → 用 ANTLR4 + Verilog.g4 或 tree-sitter-verilog
2. **AST遍历建图** → 约100行参考 ast_node.py:32-137
3. **BFS回溯** → 约70行参考 debug_graph_analyzer.py
总计约 **200行核心逻辑**(不含解析器)

View File

@ -0,0 +1,455 @@
# AST波形调试工具 - TypeScript重写规范
## 一、项目背景
将Python实现的Verilog AST波形调试工具重写为TypeScript用于VSCode插件。
**已完成部分**
- ✅ Verilog AST解析生成JSON格式的信号依赖图
- ✅ 图结构定义
**待重写部分**
- ⚠️ BFS信号回溯
- ⚠️ VCD波形解析
- ⚠️ 仿真输出解析
- ⚠️ 工具整合封装
---
## 二、数据结构定义
### 2.1 AST图结构已完成
```typescript
interface ASTNode {
id: string;
attributes: {
lines: [number, number]; // [起始行, 结束行]
type: string; // Input/Output/Reg/Wire/Always/Assign等
};
}
interface ASTEdge {
from: string; // 控制信号
to: string; // 被控制信号
attributes: {
lines: [number, number];
type: string; // Always/Assign/IfStatement等
};
}
interface ASTGraph {
metadata: {
moduleName: string;
nodeCount: number;
edgeCount: number;
generatedAt: string;
};
nodes: ASTNode[];
edges: ASTEdge[];
}
```
### 2.2 追踪结果结构
```typescript
interface TraceResult {
controlSignals: Map<string, [number, number]>; // 信号名 -> 代码行号
signalLevelTracer: string[][]; // 每层的控制关系链
}
```
### 2.3 波形数据结构
```typescript
interface WaveformData {
time: number; // 时间点(ns)
signals: {
[signalName: string]: string; // 信号名 -> 值(十六进制)
};
}
interface MismatchInfo {
signals: string[]; // 出错的信号列表
firstMismatchTime: number; // 第一次出错的时间
}
```
---
## 三、需要重写的模块
### 3.1 BFS信号回溯模块
**源文件**: `debug_graph_analyzer.py`
**代码行数**: ~70行
**第三方依赖**: 无
#### 功能描述
从出错信号出发BFS反向遍历图找到所有控制该信号的上游信号。
#### 输入输出
```typescript
// 输入
graph: ASTGraph // AST图JSON格式
targetSignals: string[] // 出错的信号列表,如 ['count', 'overflow']
k: number // 回溯层数
signalOnly: boolean // 是否只返回信号节点过滤Always/Assign等
// 输出
TraceResult {
controlSignals: Map<string, [number, number]>,
signalLevelTracer: string[][]
}
```
#### 核心算法(伪代码)
```
1. 构建前驱映射(反向边)
for each edge in graph.edges:
predecessorMap[edge.to].push(edge.from)
2. 初始化BFS队列
for each signal in targetSignals:
queue.push([signal, signal])
controlSignals.set(signal, node.lines)
3. BFS遍历K层
for level = 0 to k:
while queue not empty:
[curSignal, controlledSignal] = queue.pop()
记录关系: curSignal -> controlledSignal
for each predecessor of curSignal:
if not visited and not filtered:
queue.push([predecessor, curSignal])
记录本层关系到 signalLevelTracer
4. 返回结果
```
#### 过滤规则
```typescript
// 需要过滤的节点类型
const FILTERED_TYPES = ['Parameter', 'Localparam'];
// signalOnly=true时还需要过滤以下前缀
const FILTERED_PREFIXES = ['Always', 'Assign', 'Module', 'IntConst'];
```
---
### 3.2 仿真输出解析模块
**源文件**: `vcd_waveform_analyzer.py` 中的 `parse_mismatch()`
**代码行数**: ~20行
**第三方依赖**: 无
#### 功能描述
解析仿真工具的输出文本,提取出错信号名和出错时间。
#### 输入输出
```typescript
// 输入
testOutput: string // 仿真工具的输出文本
// 输出
MismatchInfo {
signals: string[], // 出错信号列表
firstMismatchTime: number // 第一次出错时间(ns)
}
```
#### 解析规则
```typescript
// 需要匹配的格式
// "First mismatch occurred at time 100. Output 'count' ..."
const pattern = /First mismatch occurred at time (\d+).*Output '(\w+)'/g;
// 提取所有匹配
// 返回信号列表和最小时间戳
```
#### 示例
```
输入:
"First mismatch occurred at time 100. Output 'count' expected 0001, got 0000
First mismatch occurred at time 150. Output 'overflow' expected 1, got 0"
输出:
{
signals: ['count', 'overflow'],
firstMismatchTime: 100
}
```
---
### 3.3 VCD波形解析模块
**源文件**: `vcd_waveform_analyzer.py` 中的 `get_tabular()``tabular_via_dataframe()`
**代码行数**: ~150行
**第三方依赖**: Python版用了 `vcdvcd`, `pandas`, `numpy`
#### 功能描述
读取VCDValue Change Dump波形文件提取指定信号的波形值生成表格。
#### VCD文件格式简介
```vcd
$timescale 1ns $end
$scope module tb $end
$var wire 1 ! clk $end
$var wire 8 " count [7:0] $end
$upscope $end
$enddefinitions $end
#0
b0 "
1!
#5
0!
#10
1!
b00000001 "
...
```
#### 输入输出
```typescript
// 输入
vcdPath: string // VCD文件路径
signalsToTrace: string[] // 需要提取的信号列表
offset: number // 时间偏移(从哪个时间点开始)
windowSize: number // 窗口大小(提取多少个时间点)
// 输出
string // 格式化的波形表格字符串
```
#### 输出格式示例
```
### First mismatched signals time(ns) Trace ###
time(ns) clk reset count_ref count_dut
0 1 1 00 00
5 0 1 00 00
10 1 0 00 00
15 0 0 00 00
20 1 0 01 00 <- mismatch
### First mismatched signals time(ns) End ###
```
#### TS实现建议
1. **方案A**: 找现有的JS VCD解析库
- 搜索: `npm vcd parser`, `vcd-stream`, `wavedrom`
2. **方案B**: 自己实现简单的VCD解析器
- VCD格式相对简单核心是解析变量定义和时间变化
- 约100-150行代码
#### VCD解析核心逻辑
```typescript
class VCDParser {
signals: Map<string, Signal>; // 信号定义
timeValues: Map<number, Map<string, string>>; // 时间 -> 信号值
parse(vcdContent: string): void {
// 1. 解析头部($var定义
// 2. 解析数据部分(#时间 和 值变化)
}
getSignalValues(signalName: string, startTime: number, endTime: number): WaveformData[] {
// 提取指定信号在时间范围内的值
}
}
```
---
### 3.4 工具整合封装模块
**源文件**: `waveform_trace_tool.py`
**代码行数**: ~150行
**第三方依赖**: 依赖上面所有模块
#### 功能描述
整合所有模块,提供统一的调试接口。
#### 输入输出
```typescript
// 输入
verilogFilePath: string // Verilog文件路径
vcdFilePath: string // VCD波形文件路径
simulationOutput: string // 仿真输出文本
traceLevel: number // 回溯层数
// 输出
string // 完整的调试报告
```
#### 调试报告格式
```
[Signal Traces] Backtrace control signal relations.
clk->count
reset->count
-count->state
--state->out (*last output port level)
[Signal Waveform]:
<signal>_ref 是期望值golden
<signal>_dut 是实际输出
[Traced Signals]: out, state, count, clk, reset
[Table Waveform in hexadecimal format]
time(ns) clk reset count_ref count_dut
...
[Verilog of DUT]:
```verilog
module counter(...);
...
endmodule
```
[Hint] ...
```
---
## 四、调用流程图
```
┌─────────────────────────────────────────────────────────────────┐
│ waveform_trace_tool() │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 检查文件是否存在 │
│ ├── verilogFilePath │
│ └── vcdFilePath │
│ │
│ 2. 加载AST图已有JSON
│ └── graph = loadASTGraph(verilogFilePath) │
│ │
│ 3. 解析仿真输出,获取出错信号 │
│ └── mismatchInfo = parseMismatch(simulationOutput) │
│ ├── signals: ['count', 'overflow'] │
│ └── firstMismatchTime: 100 │
│ │
│ 4. BFS回溯找到控制信号链 │
│ └── traceResult = getKControlSignals(graph, signals, k) │
│ ├── controlSignals: Map<信号名, 行号>
│ └── signalLevelTracer: [['clk->count'], ...] │
│ │
│ 5. 读取VCD波形提取相关信号的值 │
│ └── waveformTable = getTabular(vcdPath, signals, offset) │
│ │
│ 6. 读取Verilog源码 │
│ └── verilogCode = readFile(verilogFilePath) │
│ │
│ 7. 组装调试报告 │
│ └── return formatReport(traceResult, waveformTable, code) │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 五、参考实现
### 5.1 Python源文件位置
```
ast_debug_core/
├── ast_node.py # AST节点定义参考32-137行
├── graph_builder.py # 图构建入口
├── debug_graph_analyzer.py # BFS回溯完整文件约70行
├── vcd_waveform_analyzer.py # VCD解析参考89-285行
└── waveform_trace_tool.py # 工具封装完整文件约180行
```
### 5.2 关键函数对照表
| Python函数 | 位置 | TS函数名建议 |
|------------|------|--------------|
| `get_k_control_signals()` | debug_graph_analyzer.py:20 | `getKControlSignals()` |
| `parse_mismatch()` | vcd_waveform_analyzer.py:244 | `parseMismatch()` |
| `get_tabular()` | vcd_waveform_analyzer.py:264 | `getTabular()` |
| `tabular_via_dataframe()` | vcd_waveform_analyzer.py:95 | `generateWaveformTable()` |
| `waveform_trace_tool()` | waveform_trace_tool.py:63 | `waveformTraceTool()` |
---
## 六、测试用例
### 6.1 BFS回溯测试
```typescript
// 输入
const graph: ASTGraph = /* 加载 counter_ast_graph.json */;
const targetSignals = ['count'];
const k = 2;
// 期望输出
const expected = {
controlSignals: new Map([
['count', [6, 6]],
['next_count', [10, 10]],
['reset', [4, 4]],
['clk', [3, 3]],
['enable', [5, 5]]
]),
signalLevelTracer: [
['count->count'],
['next_count->count', 'reset->count', 'clk->count'],
['enable->next_count', 'count->next_count']
]
};
```
### 6.2 仿真输出解析测试
```typescript
// 输入
const testOutput = `
Mismatches: 2
First mismatch occurred at time 100. Output 'count' expected 0001, got 0000
First mismatch occurred at time 150. Output 'overflow' expected 1, got 0
`;
// 期望输出
const expected = {
signals: ['count', 'overflow'],
firstMismatchTime: 100
};
```
---
## 七、注意事项
1. **无第三方依赖要求**
- BFS回溯和仿真解析完全可以用原生TS实现
- VCD解析可以自己实现或找现有库
2. **性能考虑**
- 图遍历使用Map而非Object提高查找效率
- VCD文件可能很大考虑流式解析
3. **错误处理**
- 文件不存在时返回友好错误信息
- 信号不在图中时跳过而非报错
4. **兼容性**
- 信号名可能包含方括号,如 `count[7:0]`
- 时间单位统一为ns
---
## 八、交付物
1. `debugGraphAnalyzer.ts` - BFS回溯模块
2. `simulationParser.ts` - 仿真输出解析模块
3. `vcdParser.ts` - VCD波形解析模块
4. `waveformTraceTool.ts` - 工具整合封装
5. `types.ts` - 类型定义
6. 单元测试文件

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Author : Chia-Tung (Mark) Ho, NVIDIA
#
import copy
import re
from collections import deque
from graph_builder import generate_top_logic_graph
# use class
class DebugGraph:
def __init__(self, verilog_filelist: list[str]):
self.filelist = verilog_filelist
self.graph = generate_top_logic_graph(verilog_filelist)
# print(list(self.graph.nodes(data=True)))
def get_k_control_signals(self, target_signals: list[str], k:int, signal_only: bool=False) -> list[str]:
control_signals = {}
signal_level_tracer = []
# queue
q = deque()
tmp_q = deque()
for signal in target_signals:
# store (predecessors, controlled signal)
q.append((signal, signal))
control_signals[signal] = self.graph.nodes[signal]['lines']
# BFS
for l in range (k + 1):
# traverse l layers
tmp_q.clear()
level_signal_control_rels = []
while len(q) > 0:
cur_signal = q.popleft()
level_signal_control_rels.append(cur_signal[0] + "->" + cur_signal[1])
if cur_signal[0] not in control_signals:
if self.graph.has_edge(cur_signal[0], cur_signal[1]):
# must be the control signals through the edge
control_signals[cur_signal[0]] = self.graph[cur_signal[0]][cur_signal[1]]['lines']
else:
print("[Error] Edge not found! - ", cur_signal)
# find the predecessors
controls = self.graph.predecessors(cur_signal[0])
for c in controls:
if c in control_signals:
continue
# exclude the parameter
if 'type' in self.graph.nodes[c] and self.graph.nodes[c]['type'] in ["Parameter", "Localparam"]:
continue
if signal_only and (re.match('^Always', c) or re.match('^Assign', c) or re.match('^Module', c) or re.match('^IntConst', c)):
continue
# store (predecessors, controlled signal)
tmp_q.append((c, cur_signal[0]))
# swap the q
assert(len(q) == 0)
print(tmp_q)
q = copy.deepcopy(tmp_q)
# record the signal relations
signal_level_tracer.append(level_signal_control_rels)
return control_signals, signal_level_tracer
if __name__ == '__main__':
debug_graph_tracer = DebugGraph(["/home/scratch.chiatungh_nvresearch/hardware-agent-marco/hardware_agent/examples/verilog_testcases/fsm_serialdata.v"])
print(debug_graph_tracer.get_k_control_signals(['out_byte', 'done'], k=3, signal_only=True))

View File

@ -0,0 +1,144 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Author : Chia-Tung (Mark) Ho, NVIDIA
#
from __future__ import print_function
import sys
import os
from optparse import OptionParser
# 优先使用本地修改过的 pyverilog包含 toplogic_tree_traverse 方法)
_local_path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _local_path)
from pyverilog.vparser.parser import parse
from io import StringIO
import networkx as nx
# importing matplotlib.pyplot
import matplotlib.pyplot as plt
import re
# create graph from ast str
# directed graph from networkX
def create_graph_from_ast(ast, display=False, display_signal_only=False):
graph = nx.DiGraph()
ast.toplogic_tree_traverse(network_G=graph, rvalue=False, lvalue=False)
if not display and not display_signal_only:
return graph
# Print out nodes with attributes
nodes_to_display = []
edges_to_display = []
print("Nodes:")
for node, attrs in graph.nodes(data=True):
if display_signal_only and (not re.match("^Assign", node) and not re.match("^Always", node) and not re.match("^Module", node)):
nodes_to_display.append(node)
print(f"Node {node}: {attrs}")
# Print out edges with attributes
print("\nEdges:")
for src, dst, attrs in graph.edges(data=True):
if display_signal_only and src in nodes_to_display and dst in nodes_to_display:
edges_to_display.append((src, dst))
print(f"Edge {src} to {dst}: {attrs}")
# displaying graphs
plt.figure(figsize=(18, 16)) # Set the figure size
pos = nx.spring_layout(graph, k=1.0)
if display_signal_only:
subgraph = graph.subgraph(nodes_to_display)
# subgraph.add_edges_from(edges_to_display)
else:
subgraph = graph
nx.draw_networkx(subgraph, pos, with_labels=True) # Draw the graph without labels
# Add node labels
# node_labels = nx.get_node_attributes(graph, 'label')
# nx.draw_networkx_labels(graph, pos, labels=node_labels)
# edge labels
edge_labels = nx.get_edge_attributes(subgraph, 'lines')
nx.draw_networkx_edge_labels(
subgraph, pos,
edge_labels=edge_labels,
font_color='blue'
)
# plt.axis('off')
plt.show()
return graph
def get_ast_structure_str(ast):
normal_stdout = sys.stdout
# put the string output to a string buffer
result = StringIO()
sys.stdout = result
# traverse the ast
ast.show(buf=sys.stdout)
# Redirect std output to the normal mode
sys.stdout = normal_stdout
# Get the result out
ast_str = result.getvalue()
# print('ast str = ', ast_str, '\n ast end')
return ast_str
def generate_top_logic_graph(filelist: list[str]):
for f in filelist:
if not os.path.exists(f):
raise IOError("file not found: " + f)
ast, directives = parse(filelist,
preprocess_include=[],
preprocess_define=[])
# ast_str = get_ast_structure_str(ast)
return create_graph_from_ast(ast, display=False, display_signal_only=False)
def main():
INFO = "Verilog code parser"
VERSION = pyverilog.__version__
USAGE = "Usage: python example_parser.py file ..."
def showVersion():
print(INFO)
print(VERSION)
print(USAGE)
sys.exit()
optparser = OptionParser()
optparser.add_option("-v", "--version", action="store_true", dest="showversion",
default=False, help="Show the version")
optparser.add_option("-I", "--include", dest="include", action="append",
default=[], help="Include path")
optparser.add_option("-D", dest="define", action="append",
default=[], help="Macro Definition")
(options, args) = optparser.parse_args()
filelist = args
# print(filelist)
if options.showversion:
showVersion()
for f in filelist:
if not os.path.exists(f):
raise IOError("file not found: " + f)
if len(filelist) == 0:
showVersion()
ast, directives = parse(filelist,
preprocess_include=options.include,
preprocess_define=options.define)
# ast_str = get_ast_structure_str(ast)
create_graph_from_ast(ast, display_signal_only=True, display=True)
ast.show(attrnames=True)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
.PHONY: clean
clean:
make clean -C ./utils
make clean -C ./vparser
make clean -C ./dataflow
make clean -C ./controlflow
make clean -C ./ast_code_generator
rm -rf *.pyc __pycache__ *.out parsetab.py *.html

View File

@ -0,0 +1 @@
1.3.0

View File

@ -0,0 +1,7 @@
from __future__ import absolute_import
from __future__ import print_function
import os
with open(os.path.join(os.path.dirname(__file__), "VERSION")) as f:
__version__ = f.read().splitlines()[0]

View File

@ -0,0 +1,3 @@
.PHONY: clean
clean:
rm -rf *.pyc __pycache__ parsetab.py *.out

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
Source
Description
ModuleDef
Paramlist
Portlist
Port
Width
Length
Dimensions
Identifier
Value
Constant
IntConst
FloatConst
StringConst
Variable
Input
Output
Inout
Tri
Wire
Reg
Integer
Real
Genvar
Ioport
Parameter
Localparam
Decl
Concat
LConcat
Repeat
Partselect
Pointer
Lvalue
Rvalue
Operator
UnaryOperator
Uminus
Ulnot
Unot
Uand
Unand
Uor
Unor
Uxor
Uxnor
Power
Times
Divide
Mod
Plus
Minus
Sll
Srl
Sra
LessThan
GreaterThan
LessEq
GreaterEq
Eq
NotEq
Eql
NotEql
And
Xor
Xnor
Or
Land
Lor
Cond
Assign
Always
SensList
Sens
Substitution
BlockingSubstitution
NonblockingSubstitution
IfStatement
ForStatement
WhileStatement
CaseStatement
Case
Block
Initial
WaitStatement
ForeverStatement
DelayStatement
InstanceList
Instance
ParamArg
PortArg
Function
FunctionCall
Task
GenerateStatement
SystemCall
IdentifierScopeLabel
IdentifierScope
Pragma
PragmaEntry
Disable
ParallelBlock
SingleStatement

View File

@ -0,0 +1,3 @@
always @({{ sens_list }}) {{ statement }}

View File

@ -0,0 +1 @@
({{ left }} {{ op }} {{ right }})

View File

@ -0,0 +1 @@
assign {{ left }} = {{ right }};

View File

@ -0,0 +1,5 @@
begin{% if scope != '' %} : {{ scope }}{% endif %}
{%- for statement in statements %}
{{ statement }}
{%- endfor %}
end

View File

@ -0,0 +1 @@
{% if ldelay != '' %}{{ ldelay }} {% endif %}{{ left }} = {% if rdelay != '' %}{{ rdelay }} {% endif %}{{ right }};

View File

@ -0,0 +1 @@
{{ cond }}: {{ statement }}

View File

@ -0,0 +1,5 @@
case({{ comp }})
{%- for case in caselist %}
{{ case }}
{%- endfor %}
endcase

View File

@ -0,0 +1,5 @@
casex({{ comp }})
{%- for case in caselist %}
{{ case }}
{%- endfor %}
endcase

View File

@ -0,0 +1 @@
{ {% for item in items %}{{ item }}{% if loop.index < len_items %}, {% endif %}{% endfor %} }

View File

@ -0,0 +1 @@
(({{ cond }})? {{ true_value }} : {{ false_value }})

View File

@ -0,0 +1 @@
{{ value }}

View File

@ -0,0 +1,2 @@
{%- for item in items %}{{ item }}
{%- endfor %}

View File

@ -0,0 +1,3 @@
{% for definition in definitions %}
{{ definition }}
{% endfor %}

View File

@ -0,0 +1 @@
diable {{ name }}

View File

@ -0,0 +1 @@
({{ left }} {{ op }} {{ right }})

View File

@ -0,0 +1 @@
({{ left }} {{ op }} {{ right }})

View File

@ -0,0 +1 @@
({{ left }} {{ op }} {{ right }})

View File

@ -0,0 +1 @@
@({{ senslist }});

View File

@ -0,0 +1 @@
{{ value }}

View File

@ -0,0 +1 @@
forever {{ statement }}

View File

@ -0,0 +1 @@
for({{ pre }} {{ cond }}; {{ post }}) {{ statement }}

View File

@ -0,0 +1,7 @@
function {{ retwidth }} {{ name }};
{%- for s in statement %}
{{ s }}
{%- endfor %}
endfunction

View File

@ -0,0 +1 @@
{{ name }}({% for arg in args %}{{ arg }}{% if loop.index < len_args %}, {% endif %}{% endfor %})

View File

@ -0,0 +1,4 @@
generate {% for item in items %}{{ item }}{% endfor %}
endgenerate

View File

@ -0,0 +1 @@
genvar {{ name }};

View File

@ -0,0 +1 @@
({{ left }} {{ op }} {{ right }})

View File

@ -0,0 +1 @@
({{ left }} {{ op }} {{ right }})

View File

@ -0,0 +1 @@
{{ scope }}{{ name }}

View File

@ -0,0 +1 @@
{% for scope in scopes %}{{ scope }}{% endfor %}

View File

@ -0,0 +1 @@
{{ name }}{%- if loop != '' %}[{{ loop }}]{%- endif %}.

View File

@ -0,0 +1,5 @@
if({{ cond }}) {{ true_statement }}
{%- if true_statement[-1] != ' ' and true_statement[-1] != '\n' %} {% endif -%}
{%- if true_statement.count('\n') == 0 and false_statement != '' %}
{% endif -%}
{%- if false_statement != '' %}else {{ false_statement }}{% endif -%}

View File

@ -0,0 +1,3 @@
initial {{ statement }}

View File

@ -0,0 +1 @@
inout {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};

View File

@ -0,0 +1 @@
input {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};

View File

@ -0,0 +1,5 @@
{{ name }}{{ array }}
({% for port in portlist %}
{{ port }}{%- if loop.index < len_portlist -%}, {%- endif -%}
{% endfor %}
)

View File

@ -0,0 +1,12 @@
{{ module }}
{%- if len_parameterlist > 0 %}
#({% for param in parameterlist %}
{{ param }}{%- if loop.index < len_parameterlist -%},
{%- endif -%}{% endfor %}
)
{%- endif %}
{%- for instance in instances %}
{{ instance }}{%- if loop.index < len_instances -%},
{%- endif -%}{%- endfor -%};

View File

@ -0,0 +1 @@
{{ value }}

Some files were not shown because too many files have changed in this diff Show More