Compare commits
18 Commits
9281d1d724
...
merge/merg
| Author | SHA1 | Date | |
|---|---|---|---|
| 4687c3faa6 | |||
| feff8ea4d3 | |||
| 6abec8c7b7 | |||
| f9b3699bda | |||
| 8da1177bf3 | |||
| a85a044a9b | |||
| c58e3603de | |||
| 940584e1ea | |||
| 4037e9e2d7 | |||
| 4b2f6967dc | |||
| 79ef879b97 | |||
| 1df7462778 | |||
| 0bcdc615e3 | |||
| 5577fe17bb | |||
| 820ee2f848 | |||
| be8365c8cb | |||
| b1dd2442b8 | |||
| 226bb46094 |
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();
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -8,15 +8,17 @@ import * as vscode from "vscode";
|
|||||||
type Environment = "dev" | "test" | "prod";
|
type Environment = "dev" | "test" | "prod";
|
||||||
|
|
||||||
/** 当前环境 - 修改这里切换环境 */
|
/** 当前环境 - 修改这里切换环境 */
|
||||||
const CURRENT_ENV: Environment = "dev";
|
const CURRENT_ENV: Environment = "test";
|
||||||
|
|
||||||
/** 服务等级类型 */
|
/** 服务等级类型 */
|
||||||
export type ServiceTier = 'lite' | 'syntaxic' | 'max' | 'auto';
|
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(临时使用,后续对接认证) */
|
||||||
@ -30,6 +32,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 本地开发环境 */
|
/** 本地开发环境 */
|
||||||
dev: {
|
dev: {
|
||||||
backendUrl: "http://localhost:2233",
|
backendUrl: "http://localhost:2233",
|
||||||
|
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max", // 默认使用 max
|
serviceTier: "max", // 默认使用 max
|
||||||
@ -37,6 +40,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 测试服务器环境 */
|
/** 测试服务器环境 */
|
||||||
test: {
|
test: {
|
||||||
backendUrl: "http://192.168.1.108:2233",
|
backendUrl: "http://192.168.1.108:2233",
|
||||||
|
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max",
|
serviceTier: "max",
|
||||||
@ -44,6 +48,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 生产环境 */
|
/** 生产环境 */
|
||||||
prod: {
|
prod: {
|
||||||
backendUrl: "https://api.iccoder.com",
|
backendUrl: "https://api.iccoder.com",
|
||||||
|
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "auto",
|
serviceTier: "auto",
|
||||||
@ -75,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,7 +250,7 @@ export async function showICHelperPanel(
|
|||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
panel.webview.postMessage({ type: 'showProgress' });
|
panel.webview.postMessage({ type: "showProgress" });
|
||||||
|
|
||||||
handleUserMessage(
|
handleUserMessage(
|
||||||
panel,
|
panel,
|
||||||
@ -176,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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -324,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 });
|
||||||
@ -353,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) {
|
||||||
@ -373,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) {
|
||||||
@ -410,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,
|
||||||
|
|||||||
@ -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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
164
src/types/api.ts
164
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,7 @@ import { CompactedMemory, CompactedMessage } from './memory';
|
|||||||
* - agent: 智能体自主(默认)
|
* - agent: 智能体自主(默认)
|
||||||
* - auto: 完全自动
|
* - auto: 完全自动
|
||||||
*/
|
*/
|
||||||
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
|
export type RunMode = "plan" | "ask" | "agent" | "auto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务等级类型
|
* 服务等级类型
|
||||||
@ -23,7 +23,7 @@ export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
|
|||||||
* - max: 最大性能
|
* - max: 最大性能
|
||||||
* - auto: 自动选择
|
* - auto: 自动选择
|
||||||
*/
|
*/
|
||||||
export type ServiceTier = 'lite' | 'syntaxic' | 'max' | 'auto';
|
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对话请求
|
* 对话请求
|
||||||
@ -52,26 +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'; // 心跳
|
| "heartbeat"; // 心跳
|
||||||
|
|
||||||
/** text_delta 事件数据 */
|
/** text_delta 事件数据 */
|
||||||
export interface TextDeltaEvent {
|
export interface TextDeltaEvent {
|
||||||
@ -173,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,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: {
|
||||||
/** 工具名称 */
|
/** 工具名称 */
|
||||||
@ -229,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互斥) */
|
||||||
@ -310,20 +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"
|
||||||
| 'waveform_trace'
|
| "waveform_trace"
|
||||||
| 'knowledge_save'
|
| "knowledge_save"
|
||||||
| 'knowledge_load';
|
| "knowledge_load";
|
||||||
|
|
||||||
/** file_read 工具参数 */
|
/** file_read 工具参数 */
|
||||||
export interface FileReadArgs {
|
export interface FileReadArgs {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -172,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;
|
||||||
@ -332,7 +347,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()}
|
||||||
|
|||||||
Reference in New Issue
Block a user