Compare commits
25 Commits
feat/plugi
...
feat/detai
| Author | SHA1 | Date | |
|---|---|---|---|
| e61122449d | |||
| 4687c3faa6 | |||
| feff8ea4d3 | |||
| 6abec8c7b7 | |||
| f9b3699bda | |||
| 8da1177bf3 | |||
| a85a044a9b | |||
| c58e3603de | |||
| 940584e1ea | |||
| 4037e9e2d7 | |||
| 4b2f6967dc | |||
| 79ef879b97 | |||
| 1df7462778 | |||
| 0bcdc615e3 | |||
| 5577fe17bb | |||
| 820ee2f848 | |||
| be8365c8cb | |||
| b1dd2442b8 | |||
| 9281d1d724 | |||
| 226bb46094 | |||
| 251289a340 | |||
| c22081c5e9 | |||
| e4ff49bade | |||
| ada4806493 | |||
| e48e822d07 |
11
.gitignore
vendored
11
.gitignore
vendored
@ -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
29
.vscodeignore
Normal 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
1027
docs/数据流程详解.md
Normal file
File diff suppressed because it is too large
Load Diff
200
media/surfer/index.html
Normal file
200
media/surfer/index.html
Normal 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/ -->
|
||||||
65
media/surfer/integration.js
Normal file
65
media/surfer/integration.js
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
10
media/surfer/manifest.json
Normal file
10
media/surfer/manifest.json
Normal 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
2227
media/surfer/surfer.js
Normal file
File diff suppressed because it is too large
Load Diff
200
media/surfer/surfer/index.html
Normal file
200
media/surfer/surfer/index.html
Normal 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/ -->
|
||||||
52
media/surfer/surfer/integration.js
Normal file
52
media/surfer/surfer/integration.js
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
10
media/surfer/surfer/manifest.json
Normal file
10
media/surfer/surfer/manifest.json
Normal 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/surfer.js
Normal file
2227
media/surfer/surfer/surfer.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
media/surfer/surfer/surfer_bg.wasm
Normal file
BIN
media/surfer/surfer/surfer_bg.wasm
Normal file
Binary file not shown.
37
media/surfer/surfer/sw.js
Normal file
37
media/surfer/surfer/sw.js
Normal 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
BIN
media/surfer/surfer_bg.wasm
Normal file
Binary file not shown.
37
media/surfer/sw.js
Normal file
37
media/surfer/sw.js
Normal 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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
12
package.json
12
package.json
@ -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
BIN
rustup-init.exe
Normal file
Binary file not shown.
BIN
src/assets/titleIcon/PRO+.png
Normal file
BIN
src/assets/titleIcon/PRO+.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 302 KiB |
BIN
src/assets/titleIcon/PRO.png
Normal file
BIN
src/assets/titleIcon/PRO.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/titleIcon/free.png
Normal file
BIN
src/assets/titleIcon/free.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
237
src/components/codeHighlight.ts
Normal file
237
src/components/codeHighlight.ts
Normal 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();
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -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}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -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>`;
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,44 @@ 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 助手面板
|
||||||
@ -62,7 +100,7 @@ export async function showICHelperPanel(
|
|||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [
|
localResourceRoots: [
|
||||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets"),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -87,16 +125,40 @@ export async function showICHelperPanel(
|
|||||||
|
|
||||||
// 获取模型图标URI
|
// 获取模型图标URI
|
||||||
const autoIconUri = panel.webview.asWebviewUri(
|
const autoIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"model",
|
||||||
|
"Auto.png"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const liteIconUri = panel.webview.asWebviewUri(
|
const liteIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"model",
|
||||||
|
"lite.png"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const syIconUri = panel.webview.asWebviewUri(
|
const syIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"model",
|
||||||
|
"Sy.png"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const maxIconUri = panel.webview.asWebviewUri(
|
const maxIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"model",
|
||||||
|
"Max.png"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 设置HTML内容
|
// 设置HTML内容
|
||||||
@ -108,6 +170,52 @@ 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) => {
|
||||||
@ -142,13 +250,14 @@ export async function showICHelperPanel(
|
|||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
panel.webview.postMessage({ type: 'showProgress' });
|
panel.webview.postMessage({ type: "showProgress" });
|
||||||
|
|
||||||
handleUserMessage(
|
handleUserMessage(
|
||||||
panel,
|
panel,
|
||||||
message.text,
|
message.text,
|
||||||
context.extensionPath,
|
context.extensionPath,
|
||||||
message.mode
|
message.mode,
|
||||||
|
message.model // 传递服务等级
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "readFile":
|
case "readFile":
|
||||||
@ -175,10 +284,10 @@ 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(
|
||||||
context.extensionUri,
|
"ic-coder.openVCDViewer",
|
||||||
message.vcdFilePath
|
message.vcdFilePath
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -323,7 +432,11 @@ export async function showICHelperPanel(
|
|||||||
try {
|
try {
|
||||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
|
if (
|
||||||
|
item.isDirectory() &&
|
||||||
|
item.name !== "node_modules" &&
|
||||||
|
!item.name.startsWith(".")
|
||||||
|
) {
|
||||||
const fullPath = path.join(dir, item.name);
|
const fullPath = path.join(dir, item.name);
|
||||||
const relativePath = path.relative(baseDir, fullPath);
|
const relativePath = path.relative(baseDir, fullPath);
|
||||||
folders.push({ path: fullPath, relativePath });
|
folders.push({ path: fullPath, relativePath });
|
||||||
@ -352,7 +465,7 @@ export async function showICHelperPanel(
|
|||||||
canSelectMany: true,
|
canSelectMany: true,
|
||||||
openLabel: "选择图片",
|
openLabel: "选择图片",
|
||||||
filters: {
|
filters: {
|
||||||
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
图片文件: ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (imageUris && imageUris.length > 0) {
|
if (imageUris && imageUris.length > 0) {
|
||||||
@ -372,8 +485,8 @@ export async function showICHelperPanel(
|
|||||||
canSelectMany: true,
|
canSelectMany: true,
|
||||||
openLabel: "选择文档",
|
openLabel: "选择文档",
|
||||||
filters: {
|
filters: {
|
||||||
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
|
文档文件: ["pdf", "doc", "docx", "txt", "md"],
|
||||||
"所有文件": ["*"],
|
所有文件: ["*"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (docUris && docUris.length > 0) {
|
if (docUris && docUris.length > 0) {
|
||||||
@ -409,6 +522,29 @@ export async function showICHelperPanel(
|
|||||||
hasWorkspace: hasWorkspace,
|
hasWorkspace: hasWorkspace,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
// 新增:处理面板宽度不足
|
||||||
|
case "panelWidthInsufficient":
|
||||||
|
// 关闭面板
|
||||||
|
panel.dispose();
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
"聊天面板宽度不足(最小 200px),已自动关闭"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
context.subscriptions
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听面板状态变化,检查宽度
|
||||||
|
panel.onDidChangeViewState(
|
||||||
|
(e) => {
|
||||||
|
if (e.webviewPanel.visible) {
|
||||||
|
// 请求前端检查宽度
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "checkPanelWidth",
|
||||||
|
minWidth: 200,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
@ -439,8 +575,12 @@ async function getVCDFileInfo(
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
console.log(`[getVCDFileInfo] 开始解析 VCD 文件: ${vcdFilePath}`);
|
||||||
|
console.log(`[getVCDFileInfo] containerId: ${containerId}`);
|
||||||
|
|
||||||
// 检查文件是否存在
|
// 检查文件是否存在
|
||||||
if (!fs.existsSync(vcdFilePath)) {
|
if (!fs.existsSync(vcdFilePath)) {
|
||||||
|
console.error(`[getVCDFileInfo] 文件不存在: ${vcdFilePath}`);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "vcdInfo",
|
command: "vcdInfo",
|
||||||
containerId: containerId,
|
containerId: containerId,
|
||||||
@ -479,8 +619,14 @@ async function getVCDFileInfo(
|
|||||||
timeRange = `${minTime} - ${maxTime}`;
|
timeRange = `${minTime} - ${maxTime}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析前几个信号的真实数据
|
// 解析信号的真实数据
|
||||||
const signals = parseVCDSignals(content, 3); // 只解析前3个信号
|
// 增加到20个信号,以便显示更多波形(平衡性能和完整性)
|
||||||
|
const signals = parseVCDSignals(content, 20);
|
||||||
|
|
||||||
|
console.log(`[getVCDFileInfo] 解析到 ${signals.length} 个有效信号`);
|
||||||
|
signals.forEach((sig, idx) => {
|
||||||
|
console.log(`[getVCDFileInfo] 信号${idx + 1}: ${sig.name}, 值变化数: ${sig.values.length}`);
|
||||||
|
});
|
||||||
|
|
||||||
// 发送信息回前端
|
// 发送信息回前端
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -568,10 +714,13 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
// 解析信号值变化
|
// 解析信号值变化
|
||||||
// 格式1: 单比特信号 "0!" 或 "1!"
|
// 格式1: 单比特信号 "0!" 或 "1!"
|
||||||
// 格式2: 多比特信号 "b1010 !"
|
// 格式2: 多比特信号 "b1010 !"
|
||||||
|
// 转义标识符中的特殊字符
|
||||||
|
const escapedId = signalDef.identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
if (signalDef.width === 1) {
|
if (signalDef.width === 1) {
|
||||||
// 单比特信号
|
// 单比特信号
|
||||||
const singleBitMatch = trimmedLine.match(
|
const singleBitMatch = trimmedLine.match(
|
||||||
new RegExp(`^([01xz])${signalDef.identifier}$`)
|
new RegExp(`^([01xz])${escapedId}$`)
|
||||||
);
|
);
|
||||||
if (singleBitMatch) {
|
if (singleBitMatch) {
|
||||||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||||
@ -579,7 +728,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
} else {
|
} else {
|
||||||
// 多比特信号
|
// 多比特信号
|
||||||
const multiBitMatch = trimmedLine.match(
|
const multiBitMatch = trimmedLine.match(
|
||||||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`)
|
new RegExp(`^b([01xz]+)\\s+${escapedId}$`)
|
||||||
);
|
);
|
||||||
if (multiBitMatch) {
|
if (multiBitMatch) {
|
||||||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||||
@ -592,6 +741,8 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只添加有值变化数据的信号
|
||||||
|
if (values.length > 0) {
|
||||||
signals.push({
|
signals.push({
|
||||||
name: signalDef.name,
|
name: signalDef.name,
|
||||||
identifier: signalDef.identifier,
|
identifier: signalDef.identifier,
|
||||||
@ -599,6 +750,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
values: values,
|
values: values,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("解析 VCD 信号数据失败:", error);
|
console.error("解析 VCD 信号数据失败:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>`;
|
||||||
|
|||||||
@ -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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
345
src/services/userService.ts
Normal 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] 用户信息已清除');
|
||||||
|
}
|
||||||
500
src/services/vcdFileServer.ts
Normal file
500
src/services/vcdFileServer.ts
Normal 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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/types/api.ts
184
src/types/api.ts
@ -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;
|
||||||
|
|||||||
@ -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,14 @@ 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 +133,30 @@ 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(
|
||||||
console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId);
|
"[MessageHandler] 创建会话: taskId=",
|
||||||
|
lastTaskId,
|
||||||
|
"来源=",
|
||||||
|
taskIdToUse ? "historyManager" : "新生成"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
|
||||||
|
|
||||||
// 显示状态栏
|
// 显示状态栏
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -196,10 +215,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 +304,8 @@ async function handleUserMessageWithBackend(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mode
|
mode,
|
||||||
|
serviceTier // 传递服务等级
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
467
src/utils/vcdParser.ts
Normal file
467
src/utils/vcdParser.ts
Normal 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
145
src/utils/waveformTracer.ts
Normal 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}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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('');
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>');
|
.replace(/>/g, '>');
|
||||||
|
// 不再手动高亮,让 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`;
|
||||||
|
inlineCodes.push('<code>' + escapedCode + '</code>');
|
||||||
|
return placeholder;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转义其他 HTML 特殊字符
|
||||||
|
html = html
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
// 处理标题 ### 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()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
287
src/views/userInfoComponent.ts
Normal file
287
src/views/userInfoComponent.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -203,8 +218,16 @@ export function getWaveformPreviewScript(): string {
|
|||||||
* 渲染波形预览信息
|
* 渲染波形预览信息
|
||||||
*/
|
*/
|
||||||
function renderWaveformInfo(containerId, vcdInfo) {
|
function renderWaveformInfo(containerId, vcdInfo) {
|
||||||
|
console.log('[renderWaveformInfo] 开始渲染波形, containerId:', containerId);
|
||||||
|
console.log('[renderWaveformInfo] vcdInfo:', vcdInfo);
|
||||||
|
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!container) return;
|
if (!container) {
|
||||||
|
console.error('[renderWaveformInfo] 找不到容器:', containerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[renderWaveformInfo] 找到容器,信号数量:', vcdInfo.signals?.length || 0);
|
||||||
|
|
||||||
// 清空容器
|
// 清空容器
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
@ -214,6 +237,7 @@ export function getWaveformPreviewScript(): string {
|
|||||||
waveformSvg.innerHTML = drawRealWaveform(vcdInfo.signals || []);
|
waveformSvg.innerHTML = drawRealWaveform(vcdInfo.signals || []);
|
||||||
|
|
||||||
container.appendChild(waveformSvg);
|
container.appendChild(waveformSvg);
|
||||||
|
console.log('[renderWaveformInfo] 波形渲染完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -332,7 +356,7 @@ export function getWaveformPreviewScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开完整波形查看器
|
* 打开完整波形查看器(在新列中)
|
||||||
*/
|
*/
|
||||||
function openFullWaveform(vcdFilePath) {
|
function openFullWaveform(vcdFilePath) {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
|
|||||||
@ -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] 重置分段消息容器');
|
||||||
@ -609,6 +632,21 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'checkPanelWidth':
|
||||||
|
// 检查面板宽度
|
||||||
|
const minWidth = message.minWidth || 200;
|
||||||
|
const currentWidth = document.body.clientWidth;
|
||||||
|
console.log('[WebView] 检查面板宽度:', currentWidth, '最小宽度:', minWidth);
|
||||||
|
if (currentWidth < minWidth) {
|
||||||
|
// 宽度不足,通知后端关闭面板
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'panelWidthInsufficient',
|
||||||
|
currentWidth: currentWidth,
|
||||||
|
minWidth: minWidth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'vcdInfo':
|
case 'vcdInfo':
|
||||||
// 渲染迷你波形预览信息
|
// 渲染迷你波形预览信息
|
||||||
try {
|
try {
|
||||||
@ -719,6 +757,35 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听窗口大小变化,检查面板宽度
|
||||||
|
let resizeTimer;
|
||||||
|
const MIN_PANEL_WIDTH = 500;
|
||||||
|
|
||||||
|
function checkPanelWidth() {
|
||||||
|
const currentWidth = document.body.clientWidth;
|
||||||
|
if (currentWidth < MIN_PANEL_WIDTH) {
|
||||||
|
console.log('[WebView] 面板宽度不足:', currentWidth, 'px,最小要求:', MIN_PANEL_WIDTH, 'px');
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'panelWidthInsufficient',
|
||||||
|
currentWidth: currentWidth,
|
||||||
|
minWidth: MIN_PANEL_WIDTH
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
// 使用防抖,避免频繁检查
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(() => {
|
||||||
|
checkPanelWidth();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始加载时也检查一次
|
||||||
|
setTimeout(() => {
|
||||||
|
checkPanelWidth();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
${getMessageAreaScript()}
|
${getMessageAreaScript()}
|
||||||
${getAgentCardScript()}
|
${getAgentCardScript()}
|
||||||
${getWaveformPreviewScript()}
|
${getWaveformPreviewScript()}
|
||||||
|
|||||||
42
tools/waveform_trace/build.bat
Normal file
42
tools/waveform_trace/build.bat
Normal 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 ========================================
|
||||||
35
tools/waveform_trace/build.sh
Normal file
35
tools/waveform_trace/build.sh
Normal 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 "========================================"
|
||||||
115
tools/waveform_trace/src/README.md
Normal file
115
tools/waveform_trace/src/README.md
Normal 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行核心逻辑**(不含解析器)
|
||||||
455
tools/waveform_trace/src/TS_REWRITE_SPEC.md
Normal file
455
tools/waveform_trace/src/TS_REWRITE_SPEC.md
Normal 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`
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
读取VCD(Value 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. 单元测试文件
|
||||||
1403
tools/waveform_trace/src/ast_node.py
Normal file
1403
tools/waveform_trace/src/ast_node.py
Normal file
File diff suppressed because it is too large
Load Diff
70
tools/waveform_trace/src/debug_graph_analyzer.py
Normal file
70
tools/waveform_trace/src/debug_graph_analyzer.py
Normal 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))
|
||||||
144
tools/waveform_trace/src/graph_builder.py
Normal file
144
tools/waveform_trace/src/graph_builder.py
Normal 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()
|
||||||
39241
tools/waveform_trace/src/parser.out
Normal file
39241
tools/waveform_trace/src/parser.out
Normal file
File diff suppressed because it is too large
Load Diff
443
tools/waveform_trace/src/parsetab.py
Normal file
443
tools/waveform_trace/src/parsetab.py
Normal file
File diff suppressed because one or more lines are too long
8
tools/waveform_trace/src/pyverilog/Makefile
Normal file
8
tools/waveform_trace/src/pyverilog/Makefile
Normal 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
|
||||||
1
tools/waveform_trace/src/pyverilog/VERSION
Normal file
1
tools/waveform_trace/src/pyverilog/VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
1.3.0
|
||||||
7
tools/waveform_trace/src/pyverilog/__init__.py
Normal file
7
tools/waveform_trace/src/pyverilog/__init__.py
Normal 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]
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf *.pyc __pycache__ parsetab.py *.out
|
||||||
1030
tools/waveform_trace/src/pyverilog/ast_code_generator/codegen.py
Normal file
1030
tools/waveform_trace/src/pyverilog/ast_code_generator/codegen.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
always @({{ sens_list }}) {{ statement }}
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
assign {{ left }} = {{ right }};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
begin{% if scope != '' %} : {{ scope }}{% endif %}
|
||||||
|
{%- for statement in statements %}
|
||||||
|
{{ statement }}
|
||||||
|
{%- endfor %}
|
||||||
|
end
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{% if ldelay != '' %}{{ ldelay }} {% endif %}{{ left }} = {% if rdelay != '' %}{{ rdelay }} {% endif %}{{ right }};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ cond }}: {{ statement }}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
case({{ comp }})
|
||||||
|
{%- for case in caselist %}
|
||||||
|
{{ case }}
|
||||||
|
{%- endfor %}
|
||||||
|
endcase
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
casex({{ comp }})
|
||||||
|
{%- for case in caselist %}
|
||||||
|
{{ case }}
|
||||||
|
{%- endfor %}
|
||||||
|
endcase
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ {% for item in items %}{{ item }}{% if loop.index < len_items %}, {% endif %}{% endfor %} }
|
||||||
@ -0,0 +1 @@
|
|||||||
|
(({{ cond }})? {{ true_value }} : {{ false_value }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ value }}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
{%- for item in items %}{{ item }}
|
||||||
|
{%- endfor %}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
#{{ delay }}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
{% for definition in definitions %}
|
||||||
|
{{ definition }}
|
||||||
|
{% endfor %}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
diable {{ name }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
@({{ senslist }});
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ value }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
forever {{ statement }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
for({{ pre }} {{ cond }}; {{ post }}) {{ statement }}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
function {{ retwidth }} {{ name }};
|
||||||
|
{%- for s in statement %}
|
||||||
|
{{ s }}
|
||||||
|
{%- endfor %}
|
||||||
|
endfunction
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ name }}({% for arg in args %}{{ arg }}{% if loop.index < len_args %}, {% endif %}{% endfor %})
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
generate {% for item in items %}{{ item }}{% endfor %}
|
||||||
|
endgenerate
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
genvar {{ name }};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ scope }}{{ name }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{% for scope in scopes %}{{ scope }}{% endfor %}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ name }}{%- if loop != '' %}[{{ loop }}]{%- endif %}.
|
||||||
@ -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 -%}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
initial {{ statement }}
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
inout {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
input {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{{ name }}{{ array }}
|
||||||
|
({% for port in portlist %}
|
||||||
|
{{ port }}{%- if loop.index < len_portlist -%}, {%- endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
)
|
||||||
@ -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 -%};
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ value }}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user