Compare commits
151 Commits
feat/Plugi
...
50eacdafde
| Author | SHA1 | Date | |
|---|---|---|---|
| 50eacdafde | |||
| d90cca7cef | |||
| 5347425327 | |||
| 28d93c7e75 | |||
| 5339212de9 | |||
| 73a1510de4 | |||
| 606f757699 | |||
| 342bf22f3f | |||
| d2ec73f796 | |||
| c9f597beec | |||
| e9a201ef01 | |||
| 77a89847cb | |||
| c14b7f4dbc | |||
| 64724bf48c | |||
| c9e160f2ef | |||
| 3a19cc638f | |||
| a2e8e74572 | |||
| ad96743fad | |||
| 95b1bd7678 | |||
| 94b6fb056f | |||
| a24fd71636 | |||
| 7d1b8f7e26 | |||
| 5753e120ba | |||
| f55a5bfbcb | |||
| 83b706d5be | |||
| b9e63bc9a9 | |||
| ef0c8748f7 | |||
| 430a2c4062 | |||
| f5bd35c71a | |||
| f958683f53 | |||
| 21a8abd5cf | |||
| 4b2da8244f | |||
| c571cd9137 | |||
| 8cf0e32184 | |||
| 1cbd0c5fe7 | |||
| 72a84ed9e2 | |||
| 58113fb109 | |||
| 25966bc1e2 | |||
| 3c93c07afd | |||
| 85a37b546c | |||
| 37a121c3de | |||
| 341b6540fa | |||
| 1d074e5a94 | |||
| 5a5d82eef8 | |||
| 43189e144a | |||
| fd11eadc19 | |||
| 1231ef0892 | |||
| a1e88d473b | |||
| d08f9a7366 | |||
| faa7b63aee | |||
| e440dd2773 | |||
| a02027e7c9 | |||
| 772b067202 | |||
| a3fd5df8e8 | |||
| bdc55c727a | |||
| 52e4522ed0 | |||
| d44b316c9a | |||
| 939768986c | |||
| 1e99f3cb20 | |||
| 2af79cf1dc | |||
| 5b225126f1 | |||
| 4abb979eab | |||
| 4a790b5aca | |||
| 9786b7141c | |||
| 4a7af49fea | |||
| 15a1de3a90 | |||
| 4687c3faa6 | |||
| 5c19be22d3 | |||
| feff8ea4d3 | |||
| 6abec8c7b7 | |||
| f9b3699bda | |||
| 8da1177bf3 | |||
| a85a044a9b | |||
| 5546791549 | |||
| c58e3603de | |||
| 178f3a7498 | |||
| 940584e1ea | |||
| 4037e9e2d7 | |||
| 4b2f6967dc | |||
| 79ef879b97 | |||
| 1df7462778 | |||
| 0bcdc615e3 | |||
| 5577fe17bb | |||
| 820ee2f848 | |||
| be8365c8cb | |||
| b1dd2442b8 | |||
| 9281d1d724 | |||
| 226bb46094 | |||
| 251289a340 | |||
| c22081c5e9 | |||
| cca82c7885 | |||
| e4ff49bade | |||
| ada4806493 | |||
| 3831de2849 | |||
| 0df529c4fd | |||
| 5c53d7f0e9 | |||
| ef2a0dc16e | |||
| 5ce420295b | |||
| 1d7f3d7626 | |||
| 9b0d2d5e01 | |||
| 27e3351b55 | |||
| de3e84aa4e | |||
| e48e822d07 | |||
| 8dc34ee435 | |||
| d8cd86361e | |||
| acf3f9ff37 | |||
| c27b08cccf | |||
| 9fc3c9f056 | |||
| 60d8eaf0eb | |||
| df6f983e83 | |||
| acf60f2a17 | |||
| f933d84cd1 | |||
| b794d1ceb0 | |||
| 259310a29d | |||
| 715eac5949 | |||
| c2936395d9 | |||
| 8762eacb3e | |||
| 3d535fd3e1 | |||
| ecdbe0bdc0 | |||
| c49aaf753c | |||
| 0f8674e1c7 | |||
| ef2159f1bd | |||
| b662d25c9c | |||
| 1ce1ed715c | |||
| 2587018405 | |||
| 28b75e8475 | |||
| 5287d483d8 | |||
| 16e91bd2c0 | |||
| e6b6bc3698 | |||
| d43cd610a0 | |||
| 842e5fb49b | |||
| 430581598b | |||
| 2d5b297171 | |||
| 023fdb66c3 | |||
| 42481cd314 | |||
| e77194628a | |||
| 2aff54de74 | |||
| 91fadf591f | |||
| 02b56a7031 | |||
| c42ebdfe59 | |||
| 3f0cc8ae29 | |||
| 0f458f6299 | |||
| d415d8ee4e | |||
| bd7a85b705 | |||
| 44bbcde5cf | |||
| d4c726ea9c | |||
| 082ef923b2 | |||
| 5cb68652f9 | |||
| 9bfa774336 | |||
| 009da59d38 | |||
| 5ea5ddba6e |
11
.gitignore
vendored
@ -3,3 +3,14 @@ dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
|
||||
# waveform_trace 打包产物(exe 太大,通过 Release 发布)
|
||||
tools/waveform_trace/bin/
|
||||
tools/waveform_trace/src/build/
|
||||
tools/waveform_trace/src/dist/
|
||||
tools/waveform_trace/src/*.spec
|
||||
|
||||
# Python 缓存
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
9
.idea/IC-Coder-Plugin.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
16
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,16 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
|
||||
</JavaCodeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="IF_BRACE_FORCE" value="3" />
|
||||
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||
<option name="FOR_BRACE_FORCE" value="3" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/misc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/IC-Coder-Plugin.iml" filepath="$PROJECT_DIR$/.idea/IC-Coder-Plugin.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
24
CHANGELOG.md
@ -1,9 +1,23 @@
|
||||
# Change Log
|
||||
# 更新日志
|
||||
|
||||
All notable changes to the "ic-coder" extension will be documented in this file.
|
||||
所有重要的项目变更都将记录在此文件中。
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
## [1.0.2] - 2026-01-13
|
||||
|
||||
## [Unreleased]
|
||||
IC Coder插件端正式发布。
|
||||
|
||||
- Initial release
|
||||
IC Coder 插件端是一个专为 FPGA 开发设计的 VS Code 扩展,提供 AI 驱动的智能辅助功能。
|
||||
|
||||
主要功能:
|
||||
- VCD波形解析
|
||||
- 自动生成完整工程
|
||||
- 自动仿真
|
||||
- 自主代码迭代
|
||||
- 智能匹配最优模型
|
||||
- 多线程任务处理
|
||||
- 实时跟随
|
||||
- 丰富的上下文工具
|
||||
- 全双工交互
|
||||
- 多层次安全保障
|
||||
- 自动搭建电路架构
|
||||
- 多平台支持
|
||||
|
||||
90
CLAUDE.md
Normal file
@ -0,0 +1,90 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
IC Coder Plugin 是一个 VS Code 扩展,为 Verilog/FPGA 开发提供智能辅助功能,包括代码生成、文件操作、iverilog 仿真和 VCD 波形查看。
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 编译 (开发模式)
|
||||
pnpm run compile
|
||||
|
||||
# 监听模式编译
|
||||
pnpm run watch
|
||||
|
||||
# 生产环境打包
|
||||
pnpm run package
|
||||
|
||||
# 代码检查
|
||||
pnpm run lint
|
||||
|
||||
# 运行测试
|
||||
pnpm run test
|
||||
|
||||
# 编译测试文件
|
||||
pnpm run compile-tests
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
- 按 F5 在 VS Code 中启动调试模式
|
||||
- 使用 webpack 打包,入口文件为 `src/extension.ts`
|
||||
- 输出目录为 `dist/`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── extension.ts # 插件入口,注册命令和视图
|
||||
├── panels/
|
||||
│ ├── ICHelperPanel.ts # 主聊天面板 (WebviewPanel)
|
||||
│ └── VCDViewerPanel.ts # VCD 波形查看器面板
|
||||
├── views/
|
||||
│ ├── ICViewProvider.ts # 侧边栏视图提供者
|
||||
│ └── webviewContent.ts # Webview HTML 内容 (大文件,使用搜索)
|
||||
├── utils/
|
||||
│ ├── messageHandler.ts # 消息处理核心逻辑 (大文件,使用搜索)
|
||||
│ ├── iverilogRunner.ts # iverilog 编译和仿真执行
|
||||
│ ├── chatHistoryManager.ts # 会话历史管理
|
||||
│ ├── createFiles.ts # 文件创建工具
|
||||
│ └── readFiles.ts # 文件读取工具
|
||||
├── types/
|
||||
│ └── chatHistory.ts # 消息类型定义 (LangChain4j 格式)
|
||||
└── test/ # 测试文件
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
**消息流程**: Webview -> `onDidReceiveMessage` -> `messageHandler.ts` -> 后端处理 -> `panel.webview.postMessage` -> Webview
|
||||
|
||||
**消息类型** (`src/types/chatHistory.ts`):
|
||||
- `MessageType.SYSTEM` / `USER` / `AI` / `TOOL_EXECUTION_RESULT`
|
||||
- 兼容 LangChain4j 格式
|
||||
|
||||
**iverilog 集成** (`tools/iverilog/`):
|
||||
- 内置 Windows x64 版本的 iverilog/vvp
|
||||
- 通过 `IVERILOG_ROOT` 环境变量配置库路径
|
||||
- 支持命令: "生成 VCD"、"运行仿真"、"生成波形"
|
||||
|
||||
## VS Code Extension Points
|
||||
|
||||
- 命令: `ic-coder.openPanel`, `ic-coder.openChat`, `ic-coder.openVCDViewer`
|
||||
- 侧边栏视图: `ic-coder.mainView`
|
||||
- 激活事件: `onLanguage:verilog`, `onLanguage:vhdl`, `onStartupFinished`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `vcdrom`, `vcd-stream`, `waveql` - VCD 波形处理
|
||||
- `@wavedrom/doppler`, `onml` - 波形渲染
|
||||
- `iconv-lite` - 编码转换
|
||||
|
||||
## Notes
|
||||
|
||||
- `webviewContent.ts` 和 `messageHandler.ts` 文件较大,建议使用搜索而非完整读取
|
||||
- 当前仅支持 Windows 平台的 iverilog,其他平台需用户自行安装
|
||||
@ -67,6 +67,11 @@
|
||||
CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDOVVyT
|
||||
```
|
||||
|
||||
```
|
||||
//蔡工的token
|
||||
6CB3tOZPiwNi6rrOuFHMe6QzrVWBnajW5fJsNgCWu8jtERUCCRnJJQQJ99CAACAAAAAAAAAAAAASAZDO3FnY
|
||||
```
|
||||
|
||||
### 3. 创建发布者账号
|
||||
|
||||
发布者账号是你在 VS Code 市场的身份标识。
|
||||
|
||||
51
README.md
@ -1,24 +1,45 @@
|
||||
# IC Coder Plugin
|
||||
## 什么是 IC Coder
|
||||
|
||||
IC Coder 是一个面向 Verilog/FPGA 开发的智能辅助插件。
|
||||
**IC Coder** 是一款 **专注于真实 FPGA 研发的 Verilog 智能体编程平台**。我们立志于用 AI 重塑 FPGA 研发效率,让 FPGA 开发者们,都能享受到 AI 发展所带来的科技福利!目标成为全球最好用的 **LLM 生成 Verilog**的平台!
|
||||
|
||||
## 功能特性
|
||||
从 WEB 端到插件端,IC Coder 智能体架构完成了**全新升级**,采用当前主流的**层级架构**设计,这种高内聚、低耦合的架构特性,不仅支持更多功能扩展,更预留了充足的迭代空间。当前,插件端拥有了调用本地工具的能力,不再是单纯代码生成的智能体,升级为拥有**语法校验、波形逻辑检查**等工具的**全流程 Verilog 编程智能体平台**,给用户带来更沉浸的**Vibe Verilog Coding**体验。
|
||||
|
||||
- Verilog 代码智能生成
|
||||
- 文件操作支持(创建、读取、修改、删除)
|
||||
- 集成 iverilog 仿真工具
|
||||
- VCD 波形文件生成
|
||||
- 智能对话助手
|
||||
## 输入需求 对话补充需求
|
||||
|
||||
## 使用说明
|
||||
**无需**输入完整需求,放心交给智能体补充完善。
|
||||
|
||||
安装插件后,点击侧边栏的 IC Coder 图标即可开始使用。
|
||||
## Plan 模型下确认设计文档
|
||||
|
||||
## 系统要求
|
||||
**确定**好用户需求以及相关参数后,整理并输出一份 FPGA 开发**设计文档**。Plan 模式下用户可以**进一步**与 IC Coder 沟通需求,或**直接修改**设计文档。
|
||||
|
||||
- VS Code 1.107.0 或更高版本
|
||||
- 插件已内置 iverilog 工具(Windows 平台)
|
||||
## 自动搭建电路架构
|
||||
|
||||
## 许可证
|
||||
根据需求自动搭建电路架构,并将电路信号关系结构化
|
||||
|
||||
MIT
|
||||
## 自动仿真
|
||||
|
||||
自主搭建 Testbench 仿真平台,自动运行仿真生成波形
|
||||
|
||||
## 实时跟随
|
||||
|
||||
实时展示全流程执行细节,与智能体协同随时反馈,让 AI 开发更清晰、高效
|
||||
|
||||
## VCD 波形解析
|
||||
|
||||
自动解析 VCD 波形文件,自动根据需求,检查是否存在逻辑错误
|
||||
|
||||
## 自主代码迭代
|
||||
|
||||
根据波形解析结果,自动对代码进行优化,然后重新仿真并解析波形,如此迭代,直到仿真无误
|
||||
|
||||
## 多层次安全保障
|
||||
|
||||
默认本地存储与云端即时加密保障隐私,真正做到了代码全链路加密传输、云端零存储
|
||||
|
||||
## 反馈
|
||||
|
||||
无论是想与我们深入交流还是遇到任何问题,欢迎您[进入社区](https://iccoder.com:888/community)与我们联系
|
||||
|
||||
## 服务条款和隐私协议
|
||||
|
||||
请阅读我们的[服务条款](https://iccoder.com:888/guides/legal/terms-of-service)和[隐私协议](https://iccoder.com:888/guides/legal/privacy-policy)了解更多细节。
|
||||
|
||||
1027
docs/数据流程详解.md
Normal file
BIN
media/description/auto-build-architecture-copy.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
media/description/auto-build-architecture.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
media/description/auto-simulation-1.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
media/description/auto-simulation-2.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
media/description/auto-simulation-3.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
media/description/input-requirement-1.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
media/description/input-requirement-2.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
media/description/input-requirement-3.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
media/description/plan-design-doc-1.png
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
media/description/plan-design-doc-2.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
media/description/real-time-follow-1.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
media/description/real-time-follow-2.png
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
media/description/real-time-follow-3.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
media/description/real-time-follow-4.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
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
@ -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
@ -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
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
@ -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
@ -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
BIN
media/surfer/surfer/surfer_bg.wasm
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
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);
|
||||
})
|
||||
);
|
||||
});
|
||||
46
package.json
@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "iccoder",
|
||||
"displayName": "IC Coder",
|
||||
"displayName": "IC Coder: Agentic Verilog Platform",
|
||||
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
||||
"version": "0.0.2",
|
||||
"publisher": "ICCoder",
|
||||
"version": "1.0.2",
|
||||
"publisher": "ICCoderAgenticVerilogPlatform",
|
||||
"engines": {
|
||||
"vscode": "^1.80.0"
|
||||
},
|
||||
"icon": "media/icon.png",
|
||||
"categories": [
|
||||
"Other"
|
||||
"Chat",
|
||||
"Programming Languages"
|
||||
],
|
||||
"keywords": [
|
||||
"IC",
|
||||
@ -20,6 +21,10 @@
|
||||
"assistant"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.pengyejiatu.com/pengyejiatu/IC-Coder-Plugin.git"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:ic-coder.openPanel",
|
||||
"onView:ic-coder-sidebar",
|
||||
@ -70,26 +75,18 @@
|
||||
"label": "IC Coder"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "IC Coder",
|
||||
"properties": {
|
||||
"icCoder.backendUrl": {
|
||||
"type": "string",
|
||||
"default": "http://192.168.1.108:2233",
|
||||
"description": "后端服务地址"
|
||||
},
|
||||
"icCoder.timeout": {
|
||||
"type": "number",
|
||||
"default": 60000,
|
||||
"description": "请求超时时间(毫秒)"
|
||||
},
|
||||
"icCoder.userId": {
|
||||
"type": "string",
|
||||
"default": "default-user",
|
||||
"description": "用户ID(临时配置)"
|
||||
}
|
||||
"customEditors": [
|
||||
{
|
||||
"viewType": "ic-coder.vcdViewer",
|
||||
"displayName": "VCD 波形查看器",
|
||||
"selector": [
|
||||
{
|
||||
"filenamePattern": "*.vcd"
|
||||
}
|
||||
],
|
||||
"priority": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "pnpm run package",
|
||||
@ -120,7 +117,10 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"media",
|
||||
"tools"
|
||||
"tools",
|
||||
"src/assets",
|
||||
"LICENSE",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"@wavedrom/doppler": "^1.14.0",
|
||||
|
||||
BIN
rustup-init.exe
Normal file
BIN
src/assets/QRCode/wx.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
src/assets/model/Auto.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/model/Max.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/model/Sy.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/model/lite.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
src/assets/titleIcon/PRO+.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
src/assets/titleIcon/PRO.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/titleIcon/free.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
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();
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -1,37 +1,77 @@
|
||||
/**
|
||||
* 配置管理
|
||||
* 从 VSCode 设置读取配置项
|
||||
* 支持 dev(本地开发)和 test(测试服务器)两种环境
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
/** 环境类型 */
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "prod";
|
||||
|
||||
/** 服务等级类型 */
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
|
||||
/** 配置项接口 */
|
||||
export interface IccoderConfig {
|
||||
/** 后端服务地址 */
|
||||
backendUrl: string;
|
||||
/** 登录页面地址 */
|
||||
loginUrl: string;
|
||||
/** 后端服务地址(strangeLoop) */
|
||||
backendUrlStrongeLoop: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
timeout: number;
|
||||
/** 用户ID(临时使用,后续对接认证) */
|
||||
userId: string;
|
||||
/** 服务等级 */
|
||||
serviceTier: ServiceTier;
|
||||
}
|
||||
|
||||
/** 默认配置 */
|
||||
const DEFAULT_CONFIG: IccoderConfig = {
|
||||
backendUrl: "http://localhost:8080",
|
||||
/** 环境配置 */
|
||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 - 通过 Gateway 路由 */
|
||||
dev: {
|
||||
backendUrl: "http://localhost:8080/iccoder",
|
||||
backendUrlStrongeLoop: "http://localhost:8080",
|
||||
loginUrl: "http://localhost/login",
|
||||
timeout: 300000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max", // 默认使用 max
|
||||
},
|
||||
/** 测试服务器环境 - 通过 Gateway 路由 */
|
||||
test: {
|
||||
backendUrl: "http://192.168.1.108:2029/iccoder",
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
loginUrl: "http://192.168.1.108:2005/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max",
|
||||
},
|
||||
/** 生产环境 - 通过 Gateway 路由 */
|
||||
prod: {
|
||||
backendUrl: "https://api.iccoder.com",
|
||||
backendUrlStrongeLoop: "http://192.168.1.115:2029",
|
||||
loginUrl: "https://iccoder.com/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "auto",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前环境
|
||||
*/
|
||||
export function getCurrentEnv(): Environment {
|
||||
return CURRENT_ENV;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项
|
||||
*/
|
||||
export function getConfig(): IccoderConfig {
|
||||
const config = vscode.workspace.getConfiguration("icCoder");
|
||||
|
||||
return {
|
||||
backendUrl: config.get<string>("backendUrl", DEFAULT_CONFIG.backendUrl),
|
||||
timeout: config.get<number>("timeout", DEFAULT_CONFIG.timeout),
|
||||
userId: config.get<string>("userId", DEFAULT_CONFIG.userId),
|
||||
};
|
||||
return { ...ENV_CONFIG[CURRENT_ENV] };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,10 +79,21 @@ export function getConfig(): IccoderConfig {
|
||||
*/
|
||||
export function getApiUrl(path: string): string {
|
||||
const { backendUrl } = getConfig();
|
||||
// 确保 URL 格式正确
|
||||
const baseUrl = backendUrl.endsWith("/")
|
||||
? backendUrl.slice(0, -1)
|
||||
: backendUrl;
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 StrangeLoop 服务 API 地址(用于用户信息等)
|
||||
*/
|
||||
export function getStrangeLoopApiUrl(path: string): string {
|
||||
const { backendUrlStrongeLoop } = getConfig();
|
||||
const baseUrl = backendUrlStrongeLoop.endsWith("/")
|
||||
? backendUrlStrongeLoop.slice(0, -1)
|
||||
: backendUrlStrongeLoop;
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
|
||||
@ -1,13 +1,35 @@
|
||||
import * as vscode from "vscode";
|
||||
import { ICViewProvider } from "./views/ICViewProvider";
|
||||
import { showICHelperPanel } from "./panels/ICHelperPanel";
|
||||
import { VCDViewerPanel } from "./panels/VCDViewerPanel";
|
||||
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||
import { VCDFileServer } from "./services/vcdFileServer";
|
||||
import { initUserService } from "./services/userService";
|
||||
import { initCreditsService } from "./services/creditsService";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 初始化用户服务
|
||||
initUserService(context);
|
||||
|
||||
// 初始化 Credits 服务
|
||||
initCreditsService(context);
|
||||
|
||||
// 初始化 VCD 文件服务器
|
||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||
vcdFileServer.start().then((port) => {
|
||||
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
||||
}).catch((error) => {
|
||||
console.error("启动 VCD 文件服务器失败:", error);
|
||||
});
|
||||
|
||||
// 在插件停用时关闭服务器
|
||||
context.subscriptions.push({
|
||||
dispose: () => vcdFileServer.stop()
|
||||
});
|
||||
|
||||
// 注册 Authentication Provider
|
||||
const authProvider = new ICCoderAuthenticationProvider(context);
|
||||
context.subscriptions.push(
|
||||
@ -68,7 +90,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(`波形查看器已在浏览器中打开`);
|
||||
}
|
||||
);
|
||||
|
||||
@ -77,6 +132,17 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
"ic-coder.login",
|
||||
async () => {
|
||||
try {
|
||||
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
|
||||
try {
|
||||
await vscode.authentication.getSession("iccoder", [], {
|
||||
clearSessionPreference: true,
|
||||
createIfNone: false
|
||||
});
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
// 创建新 session
|
||||
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
||||
@ -91,12 +157,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
if (session) {
|
||||
// 通过创建新会话并清除偏好来实现登出
|
||||
await vscode.authentication.getSession("iccoder", [], {
|
||||
clearSessionPreference: true,
|
||||
forceNewSession: true
|
||||
});
|
||||
vscode.window.showInformationMessage("已退出登录");
|
||||
// 调用 authProvider 的 removeSession 方法
|
||||
await authProvider.removeSession(session.id);
|
||||
} else {
|
||||
vscode.window.showInformationMessage("当前未登录");
|
||||
}
|
||||
@ -160,11 +222,15 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
viewProvider
|
||||
);
|
||||
|
||||
// 注册 VCD 自定义编辑器
|
||||
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
|
||||
|
||||
// 添加到订阅
|
||||
context.subscriptions.push(
|
||||
openPanelCommand,
|
||||
openChatCommand,
|
||||
openVCDViewerCommand,
|
||||
openVCDViewerInBrowserCommand,
|
||||
loginCommand,
|
||||
logoutCommand,
|
||||
// TODO: 等待重新实现这些命令
|
||||
@ -174,7 +240,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
// deleteSessionCommand,
|
||||
// clearHistoryCommand,
|
||||
// searchSessionCommand,
|
||||
viewRegistration
|
||||
viewRegistration,
|
||||
vcdEditorProvider
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -9,10 +9,47 @@ import {
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
handlePlanAction,
|
||||
getCurrentTaskId,
|
||||
setLastTaskId,
|
||||
} from "../utils/messageHandler";
|
||||
import { compactDialog } from "../services/apiClient";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { MessageType } from "../types/chatHistory";
|
||||
import { getCachedUserInfo } from "../services/userService";
|
||||
|
||||
/**
|
||||
* 获取会员等级图标 URI
|
||||
*/
|
||||
function getTierIconUri(
|
||||
webview: vscode.Webview,
|
||||
context: vscode.ExtensionContext,
|
||||
tierCode?: string
|
||||
): string | undefined {
|
||||
if (!tierCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tierIconMap: Record<string, string> = {
|
||||
'BASIC': 'free.png',
|
||||
'TRIAL': 'PRO-Try.png',
|
||||
'ADVANCED': 'PRO.png',
|
||||
'PROFESSIONAL': 'PRO+.png'
|
||||
};
|
||||
|
||||
const iconFile = tierIconMap[tierCode];
|
||||
if (!iconFile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iconUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, 'src', 'assets', 'titleIcon', iconFile)
|
||||
);
|
||||
|
||||
return iconUri.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并显示 IC 助手面板
|
||||
@ -23,9 +60,13 @@ export async function showICHelperPanel(
|
||||
) {
|
||||
// 检查用户是否已登录
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (!session) {
|
||||
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
@ -33,7 +74,9 @@ export async function showICHelperPanel(
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
@ -49,7 +92,10 @@ export async function showICHelperPanel(
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
@ -71,8 +117,78 @@ export async function showICHelperPanel(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||
);
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
);
|
||||
|
||||
// 获取二维码图片URI
|
||||
const qrCodeUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "QRCode", "wx.png")
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
||||
panel.webview.html = getWebviewContent(
|
||||
iconUri.toString(),
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString(),
|
||||
qrCodeUri.toString()
|
||||
);
|
||||
|
||||
// 获取并发送用户信息到 webview
|
||||
try {
|
||||
// 优先使用缓存的用户信息
|
||||
let userInfo = getCachedUserInfo();
|
||||
|
||||
if (userInfo) {
|
||||
// 使用缓存的用户信息
|
||||
console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo);
|
||||
console.log('[ICHelperPanel] Credits 余额:', userInfo.credits);
|
||||
const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode);
|
||||
const messageData = {
|
||||
command: 'updateUserInfo',
|
||||
userInfo: {
|
||||
userId: userInfo.userId,
|
||||
nickname: userInfo.nickname,
|
||||
username: userInfo.username,
|
||||
credits: userInfo.credits,
|
||||
membership: userInfo.membership
|
||||
},
|
||||
tierIconUrl: tierIconUrl
|
||||
};
|
||||
console.log('[ICHelperPanel] 发送用户信息到前端:', messageData);
|
||||
panel.webview.postMessage(messageData);
|
||||
} else {
|
||||
// 如果没有缓存,从 session 中获取
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (session) {
|
||||
console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account);
|
||||
panel.webview.postMessage({
|
||||
command: 'updateUserInfo',
|
||||
userInfo: {
|
||||
userId: session.account.id,
|
||||
nickname: session.account.label,
|
||||
username: session.account.label
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ICHelperPanel] 获取用户信息失败:', error);
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
@ -107,7 +223,16 @@ export async function showICHelperPanel(
|
||||
// 切换到当前面板的任务上下文
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
|
||||
handleUserMessage(panel, message.text, context.extensionPath);
|
||||
// 显示进度条
|
||||
panel.webview.postMessage({ type: 'showProgress' });
|
||||
|
||||
handleUserMessage(
|
||||
panel,
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode,
|
||||
message.model // 传递服务等级
|
||||
);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
@ -133,12 +258,9 @@ export async function showICHelperPanel(
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "openWaveformViewer":
|
||||
// 打开波形查看器
|
||||
// 在新列中打开波形查看器
|
||||
if (message.vcdFilePath) {
|
||||
VCDViewerPanel.createOrShow(
|
||||
context.extensionUri,
|
||||
message.vcdFilePath
|
||||
);
|
||||
vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath);
|
||||
}
|
||||
break;
|
||||
case "getVCDInfo":
|
||||
@ -171,7 +293,7 @@ export async function showICHelperPanel(
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
void handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput
|
||||
@ -179,7 +301,230 @@ export async function showICHelperPanel(
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
// 新增:压缩会话
|
||||
case "compressConversation":
|
||||
{
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
compactDialog(taskId)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "✅ 会话压缩完成",
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${result.error || "未知错误"}`,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "❌ 没有活跃的会话",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "optimizePrompt":
|
||||
if (typeof message.prompt === "string") {
|
||||
void handleOptimizePrompt(panel, message.prompt);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: "提示词为空或格式错误",
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "logout":
|
||||
// 退出登录
|
||||
vscode.commands.executeCommand("ic-coder.logout");
|
||||
break;
|
||||
case "openICCoder":
|
||||
// 跳转到 IC Coder 官网
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
case "openUserManual":
|
||||
// 打开用户手册
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
case "openUserFeedback":
|
||||
// 打开用户反馈二维码弹窗
|
||||
panel.webview.postMessage({
|
||||
command: "showFeedbackQRCode"
|
||||
});
|
||||
break;
|
||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
// 确认执行:切换到 Agent 模式(UI 切换)
|
||||
panel.webview.postMessage({
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
|
||||
} else if (message.action === "modify" || message.action === "cancel") {
|
||||
void handlePlanAction(
|
||||
panel,
|
||||
message.action,
|
||||
message.planTitle || "",
|
||||
context.extensionPath,
|
||||
message.model
|
||||
);
|
||||
}
|
||||
break;
|
||||
// 添加文件上下文 - 显示工作区文件列表
|
||||
case "addContextFile":
|
||||
{
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
break;
|
||||
}
|
||||
|
||||
// 获取工作区所有文件
|
||||
const files = await vscode.workspace.findFiles(
|
||||
"**/*",
|
||||
"**/node_modules/**"
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFileList",
|
||||
files: files.map((uri) => ({
|
||||
path: uri.fsPath,
|
||||
relativePath: vscode.workspace.asRelativePath(uri),
|
||||
})),
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 添加文件夹上下文 - 显示工作区文件夹列表
|
||||
case "addContextFolder":
|
||||
{
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
break;
|
||||
}
|
||||
|
||||
// 获取工作区所有文件夹
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const folders: Array<{ path: string; relativePath: string }> = [];
|
||||
|
||||
function scanFolders(dir: string, baseDir: string) {
|
||||
try {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const item of items) {
|
||||
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
|
||||
const fullPath = path.join(dir, item.name);
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
folders.push({ path: fullPath, relativePath });
|
||||
scanFolders(fullPath, baseDir);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("扫描文件夹失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFolderList",
|
||||
folders: folders,
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 添加图片上下文
|
||||
case "addContextImage":
|
||||
{
|
||||
const imageUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择图片",
|
||||
filters: {
|
||||
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||
},
|
||||
});
|
||||
if (imageUris && imageUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextImagesSelected",
|
||||
images: imageUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 添加文档库上下文
|
||||
case "addContextDocument":
|
||||
{
|
||||
const docUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择文档",
|
||||
filters: {
|
||||
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
|
||||
"所有文件": ["*"],
|
||||
},
|
||||
});
|
||||
if (docUris && docUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextDocumentsSelected",
|
||||
documents: docUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 新增:检查工作区状态
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
vscode.workspace.workspaceFolders &&
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
);
|
||||
if (!hasWorkspace) {
|
||||
// 弹窗提示用户需要打开工作区
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||
"打开文件夹"
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "打开文件夹") {
|
||||
vscode.commands.executeCommand("vscode.openFolder");
|
||||
}
|
||||
});
|
||||
}
|
||||
// 返回工作区状态给前端
|
||||
panel.webview.postMessage({
|
||||
command: "workspaceStatus",
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
break;
|
||||
case "openExternalUrl":
|
||||
// 打开外部链接
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
break;
|
||||
case "openICCoder":
|
||||
// 打开 IC Coder 官网
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
||||
break;
|
||||
case "logout":
|
||||
// 退出登录(前端已有确认对话框)
|
||||
vscode.commands.executeCommand('iccoder.logout');
|
||||
break;
|
||||
}
|
||||
},
|
||||
@ -464,6 +809,9 @@ async function selectConversation(
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 lastTaskId,用于压缩等操作
|
||||
setLastTaskId(taskId);
|
||||
|
||||
// 更新面板的任务映射,确保后续对话保存到正确的任务中
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||
|
||||
@ -1,19 +1,77 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
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 {
|
||||
public static currentPanel: VCDViewerPanel | undefined;
|
||||
private readonly _panel: vscode.WebviewPanel;
|
||||
private readonly _extensionUri: vscode.Uri;
|
||||
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._extensionUri = extensionUri;
|
||||
this._vcdFileServer = vcdFileServer;
|
||||
|
||||
// 设置初始 HTML 内容
|
||||
this._panel.webview.html = this._getLoadingHtml();
|
||||
@ -24,12 +82,20 @@ export class VCDViewerPanel {
|
||||
// 监听来自 webview 的消息
|
||||
this._panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
console.log("[VCDViewerPanel] 收到消息:", message);
|
||||
switch (message.command) {
|
||||
case "loadVCD":
|
||||
if (message.filePath) {
|
||||
this.loadVCDFile(message.filePath);
|
||||
}
|
||||
break;
|
||||
case "loaded":
|
||||
// Surfer iframe 加载完成,发送 VCD 文件
|
||||
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
|
||||
if (this._currentVcdPath) {
|
||||
this.sendVcdToSurfer(this._currentVcdPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
null,
|
||||
@ -40,8 +106,9 @@ export class VCDViewerPanel {
|
||||
/**
|
||||
* 创建或显示 VCD 查看器面板
|
||||
*/
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string) {
|
||||
const column = vscode.ViewColumn.One;
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||
// 在当前活动编辑器旁边打开新列
|
||||
const column = vscode.ViewColumn.Beside;
|
||||
|
||||
// 如果已经有面板打开,则显示它
|
||||
if (VCDViewerPanel.currentPanel) {
|
||||
@ -64,7 +131,7 @@ export class VCDViewerPanel {
|
||||
}
|
||||
);
|
||||
|
||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri);
|
||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||
|
||||
// 如果提供了 VCD 文件路径,加载它
|
||||
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 文件
|
||||
*/
|
||||
public loadVCDFile(vcdFilePath: string) {
|
||||
try {
|
||||
console.log("[VCDViewerPanel] 开始加载 VCD 文件:", vcdFilePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前 VCD 路径
|
||||
this._currentVcdPath = vcdFilePath;
|
||||
console.log("[VCDViewerPanel] VCD 路径已保存:", this._currentVcdPath);
|
||||
|
||||
// 更新面板标题
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
this._panel.title = `VCD 波形查看器 - ${fileName}`;
|
||||
this._panel.title = `Surfer 波形查看器 - ${fileName}`;
|
||||
|
||||
// 设置 HTML 内容
|
||||
this._panel.webview.html = this._getWebviewContent(vcdFilePath);
|
||||
this._panel.webview.html = this._getWebviewContent();
|
||||
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载 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 内容
|
||||
*/
|
||||
private _getWebviewContent(vcdFilePath: string): string {
|
||||
// 获取资源 URI
|
||||
const vcdromJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcdrom.js")
|
||||
private _getWebviewContent(): string {
|
||||
// 获取 surfer 资源 URI
|
||||
const surferJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
|
||||
);
|
||||
const vcdWasmUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcd.wasm")
|
||||
const surferWasmUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
|
||||
);
|
||||
const fontRegularUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Regular.woff2")
|
||||
const integrationJsUri = this._panel.webview.asWebviewUri(
|
||||
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>
|
||||
<html lang="zh-CN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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};">
|
||||
<title>VCD 波形查看器</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<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>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>
|
||||
@font-face {
|
||||
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);
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
#waveform-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#waveform1 {
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
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;
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: var(--vscode-errorForeground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||
border-radius: 4px;
|
||||
margin: 20px;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
<script src="${vcdromJsUri}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="waveform-container">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载 VCD 波形...</p>
|
||||
</div>
|
||||
<div id="waveform1"></div>
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<div id="error_container" style="display: none;">
|
||||
<h3>❌ Surfer 加载失败</h3>
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
|
||||
<script src="${integrationJsUri}"></script>
|
||||
<script>
|
||||
(async function() {
|
||||
try {
|
||||
// 设置 WASM 文件路径
|
||||
window.wasmBinaryFile = '${vcdWasmUri}';
|
||||
register_message_listener();
|
||||
|
||||
// 解码 base64 VCD 内容
|
||||
const vcdBase64 = '${vcdBase64}';
|
||||
const vcdContent = atob(vcdBase64);
|
||||
console.log('[Webview] 注册 VS Code 消息监听器');
|
||||
// 监听来自 VS Code 扩展的消息(使用 vscode API)
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
// 隐藏加载提示
|
||||
document.querySelector('.loading').style.display = 'none';
|
||||
// 检查是否来自 VS Code
|
||||
if (message.command === 'loadVcdUrl') {
|
||||
console.log('[Webview] 收到 VS Code 消息,命令:', message.command);
|
||||
console.log('[Webview] Surfer 就绪状态:', window.surferReady);
|
||||
|
||||
// 创建一个函数来提供 VCD 数据流
|
||||
const vcdProvider = async (handler) => {
|
||||
// 将 VCD 内容转换为 Uint8Array
|
||||
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);
|
||||
if (window.surferReady) {
|
||||
// Surfer 已就绪,立即加载
|
||||
loadVcdUrl(message);
|
||||
} 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>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
* API 客户端
|
||||
* 封装与后端的 HTTP 通信
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl, getConfig } from '../config/settings';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse } from '../types/api';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse, UserInfoResponse } from '../types/api';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
@ -18,6 +19,18 @@ interface RequestOptions {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录的 Token
|
||||
*/
|
||||
async function getAuthToken(): Promise<string | undefined> {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
return session?.accessToken;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*/
|
||||
@ -25,6 +38,9 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
const url = new URL(getApiUrl(path));
|
||||
const { timeout } = getConfig();
|
||||
|
||||
// 自动获取 Token
|
||||
const token = await getAuthToken();
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
@ -35,6 +51,7 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
method: options.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...options.headers
|
||||
},
|
||||
timeout: options.timeout || timeout
|
||||
@ -103,6 +120,18 @@ export async function submitAnswer(answer: AnswerRequest): Promise<AnswerRespons
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工具确认响应(Ask 模式)
|
||||
* POST /api/tool/confirm
|
||||
*/
|
||||
export async function submitToolConfirm(response: ToolConfirmResponse): Promise<ToolResultResponse> {
|
||||
console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`);
|
||||
return request<ToolResultResponse>('/api/tool/confirm', {
|
||||
method: 'POST',
|
||||
body: response
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* GET /api/dialog/health
|
||||
@ -114,6 +143,55 @@ export async function healthCheck(): Promise<{ status: string }> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话请求
|
||||
*/
|
||||
export interface StopDialogRequest {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话响应
|
||||
*/
|
||||
export interface StopDialogResponse {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话
|
||||
* POST /api/dialog/stop
|
||||
*/
|
||||
export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
|
||||
console.log(`[API] 停止对话: taskId=${taskId}`);
|
||||
return request<StopDialogResponse>('/api/dialog/stop', {
|
||||
method: 'POST',
|
||||
body: { taskId }
|
||||
});
|
||||
}
|
||||
|
||||
/** 压缩对话响应 */
|
||||
export interface CompactDialogResponse {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动压缩对话历史
|
||||
* POST /api/dialog/compact
|
||||
*/
|
||||
export async function compactDialog(taskId: string): Promise<CompactDialogResponse> {
|
||||
console.log(`[API] 压缩对话: taskId=${taskId}`);
|
||||
return request<CompactDialogResponse>('/api/dialog/compact', {
|
||||
method: 'POST',
|
||||
body: { taskId }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功的工具结果
|
||||
*/
|
||||
@ -152,3 +230,33 @@ export function createSystemErrorResult(id: number, code: number, message: strin
|
||||
error: { code, message }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(): Promise<UserInfoResponse> {
|
||||
console.log('[API] 获取用户信息');
|
||||
return request<UserInfoResponse>('/system/user/getInfo', {
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
/** 余额查询响应 */
|
||||
export interface CreditBalanceResponse {
|
||||
success: boolean;
|
||||
balance?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户资源点余额
|
||||
* GET /api/dialog/balance?userId=xxx
|
||||
*/
|
||||
export async function getCreditBalance(userId: string): Promise<CreditBalanceResponse> {
|
||||
console.log('[API] 查询余额: userId=', userId);
|
||||
return request<CreditBalanceResponse>(`/api/dialog/balance?userId=${userId}`, {
|
||||
method: 'GET',
|
||||
timeout: 5000
|
||||
});
|
||||
}
|
||||
|
||||
255
src/services/creditsService.ts
Normal file
@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 资源点余额管理服务
|
||||
* 负责缓存余额、主动查询、发送前检测
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getStrangeLoopApiUrl } from '../config/settings';
|
||||
import { getCachedUserInfo } from './userService';
|
||||
|
||||
/** 低余额阈值 */
|
||||
const LOW_CREDIT_THRESHOLD = 5;
|
||||
|
||||
/** 缓存的余额 */
|
||||
let cachedBalance: number | null = null;
|
||||
|
||||
/** 最后更新时间 */
|
||||
let lastUpdateTime: number = 0;
|
||||
|
||||
/** 缓存有效期(5分钟) */
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
/** ExtensionContext 用于持久化存储 */
|
||||
let extensionContext: vscode.ExtensionContext | null = null;
|
||||
|
||||
/**
|
||||
* 初始化 Credits 服务(设置 context)
|
||||
*/
|
||||
export function initCreditsService(context: vscode.ExtensionContext): void {
|
||||
extensionContext = context;
|
||||
// 从持久化存储加载余额
|
||||
const savedBalance = extensionContext.globalState.get<number>('icCoderCreditsBalance');
|
||||
if (savedBalance !== undefined) {
|
||||
cachedBalance = savedBalance;
|
||||
lastUpdateTime = Date.now();
|
||||
console.log('[CreditsService] 从持久化存储加载余额:', savedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存余额到持久化存储
|
||||
*/
|
||||
async function saveBalance(balance: number): Promise<void> {
|
||||
if (extensionContext) {
|
||||
await extensionContext.globalState.update('icCoderCreditsBalance', balance);
|
||||
console.log('[CreditsService] 余额已保存到持久化存储:', balance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存的余额(从 SSE credit_update 事件调用)
|
||||
*/
|
||||
export function updateCachedBalance(balance: number): void {
|
||||
cachedBalance = balance;
|
||||
lastUpdateTime = Date.now();
|
||||
console.log('[CreditsService] 余额已更新:', balance);
|
||||
// 异步保存到持久化存储
|
||||
saveBalance(balance).catch(err => {
|
||||
console.error('[CreditsService] 保存余额失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的余额
|
||||
*/
|
||||
export function getCachedBalance(): number | null {
|
||||
return cachedBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否有效
|
||||
*/
|
||||
function isCacheValid(): boolean {
|
||||
if (cachedBalance === null) return false;
|
||||
return Date.now() - lastUpdateTime < CACHE_TTL_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* StrangeLoop 余额响应类型
|
||||
*/
|
||||
interface StrangeLoopBalanceResponse {
|
||||
userId?: number;
|
||||
availableCredits?: number;
|
||||
totalCredits?: number;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动查询余额(直接调用 StrangeLoop 接口)
|
||||
*/
|
||||
export async function fetchBalance(): Promise<number | null> {
|
||||
try {
|
||||
// 获取 JWT token
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
if (!session?.accessToken) {
|
||||
console.warn('[CreditsService] 无法查询余额:未登录');
|
||||
return null;
|
||||
}
|
||||
|
||||
return await fetchBalanceWithToken(session.accessToken);
|
||||
} catch (error) {
|
||||
console.error('[CreditsService] 查询余额异常:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定 token 查询余额(登录过程中使用)
|
||||
*/
|
||||
export async function fetchBalanceWithToken(token: string): Promise<number | null> {
|
||||
try {
|
||||
console.log('[CreditsService] 开始查询余额,token 长度:', token.length);
|
||||
|
||||
// 直接调用 StrangeLoop 的 /api/credit/balance 接口
|
||||
const response = await callStrangeLoopBalance(token);
|
||||
|
||||
if (response.availableCredits !== undefined) {
|
||||
const balance = response.availableCredits;
|
||||
updateCachedBalance(balance);
|
||||
console.log('[CreditsService] 余额查询成功:', balance);
|
||||
return balance;
|
||||
} else {
|
||||
console.warn('[CreditsService] 查询余额失败:', response.error || response.message);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CreditsService] 查询余额异常:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 StrangeLoop 余额接口
|
||||
*/
|
||||
async function callStrangeLoopBalance(token: string): Promise<StrangeLoopBalanceResponse> {
|
||||
const urlStr = getStrangeLoopApiUrl('/strangeloop/api/credit/balance');
|
||||
const url = new URL(urlStr);
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
// 余额查询使用固定短超时,避免阻塞发送前检查
|
||||
const BALANCE_TIMEOUT_MS = 5000;
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: BALANCE_TIMEOUT_MS
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('[CreditsService] 响应状态码:', res.statusCode);
|
||||
console.log('[CreditsService] 响应内容:', data);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as StrangeLoopBalanceResponse);
|
||||
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
// 登录过期或无权限
|
||||
resolve({ error: '登录已过期,请重新登录' });
|
||||
} else {
|
||||
resolve({ error: json.error || json.message || json.msg || `HTTP ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({ error: `解析响应失败: ${data}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前余额(优先使用缓存,过期则主动查询)
|
||||
*/
|
||||
export async function getBalance(): Promise<number | null> {
|
||||
if (isCacheValid()) {
|
||||
return cachedBalance;
|
||||
}
|
||||
return await fetchBalance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查余额是否足够发送消息
|
||||
* @returns { allowed: boolean, balance: number | null, message?: string }
|
||||
*/
|
||||
export async function checkBalanceBeforeSend(): Promise<{
|
||||
allowed: boolean;
|
||||
balance: number | null;
|
||||
message?: string;
|
||||
}> {
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (!userInfo) {
|
||||
// 未登录,允许发送(后端会处理)
|
||||
return { allowed: true, balance: null };
|
||||
}
|
||||
|
||||
const balance = await getBalance();
|
||||
|
||||
if (balance === null) {
|
||||
// 无法获取余额,允许发送(后端会处理)
|
||||
console.warn('[CreditsService] 无法获取余额,允许发送');
|
||||
return { allowed: true, balance: null };
|
||||
}
|
||||
|
||||
if (balance < LOW_CREDIT_THRESHOLD) {
|
||||
return {
|
||||
allowed: false,
|
||||
balance,
|
||||
message: `资源点余额不足!当前余额 ${balance.toFixed(2)} 点,低于最低要求 ${LOW_CREDIT_THRESHOLD} 点。请充值后再试。`
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true, balance };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存(登出时调用)
|
||||
*/
|
||||
export async function clearBalanceCache(): Promise<void> {
|
||||
cachedBalance = null;
|
||||
lastUpdateTime = 0;
|
||||
if (extensionContext) {
|
||||
await extensionContext.globalState.update('icCoderCreditsBalance', undefined);
|
||||
}
|
||||
console.log('[CreditsService] 余额缓存已清除');
|
||||
}
|
||||
@ -2,6 +2,8 @@ import * as vscode from "vscode";
|
||||
import * as http from "http";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||
import { getConfig } from "../config/settings";
|
||||
|
||||
/**
|
||||
* IC Coder Authentication Provider
|
||||
@ -12,7 +14,6 @@ export class ICCoderAuthenticationProvider
|
||||
{
|
||||
private static readonly AUTH_TYPE = "iccoder";
|
||||
private static readonly AUTH_NAME = "IC Coder";
|
||||
private static readonly LOGIN_URL = "http://192.168.1.108:2005/login";
|
||||
private static loginServer: http.Server | null = null;
|
||||
private static currentPort: number | null = null;
|
||||
|
||||
@ -23,8 +24,23 @@ export class ICCoderAuthenticationProvider
|
||||
private _sessions: vscode.AuthenticationSession[] = [];
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
// 从存储中恢复会话
|
||||
this.loadSessions();
|
||||
// 从存储中恢复会话(同步执行)
|
||||
this.loadSessionsSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中加载会话(同步版本)
|
||||
*/
|
||||
private loadSessionsSync(): void {
|
||||
const storedSessions = this.context.globalState.get<
|
||||
vscode.AuthenticationSession[]
|
||||
>("icCoderSessions", []);
|
||||
this._sessions = storedSessions;
|
||||
console.log("[AuthProvider] 同步加载 sessions, 数量:", this._sessions.length);
|
||||
if (this._sessions.length > 0) {
|
||||
console.log("[AuthProvider] Session ID:", this._sessions[0].id);
|
||||
console.log("[AuthProvider] Account:", this._sessions[0].account.label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,7 +57,9 @@ export class ICCoderAuthenticationProvider
|
||||
* 保存会话到存储
|
||||
*/
|
||||
private async saveSessions(): Promise<void> {
|
||||
console.log("[AuthProvider] 保存 sessions, 数量:", this._sessions.length);
|
||||
await this.context.globalState.update("icCoderSessions", this._sessions);
|
||||
console.log("[AuthProvider] sessions 已保存到 globalState");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,6 +68,7 @@ export class ICCoderAuthenticationProvider
|
||||
async getSessions(
|
||||
scopes?: readonly string[]
|
||||
): Promise<vscode.AuthenticationSession[]> {
|
||||
console.log("[AuthProvider] getSessions 被调用, 当前 sessions 数量:", this._sessions.length);
|
||||
return [...this._sessions];
|
||||
}
|
||||
|
||||
@ -60,15 +79,32 @@ export class ICCoderAuthenticationProvider
|
||||
scopes: readonly string[]
|
||||
): Promise<vscode.AuthenticationSession> {
|
||||
try {
|
||||
// 先删除旧的 session(静默删除,不弹窗、不重载窗口)
|
||||
if (this._sessions.length > 0) {
|
||||
const oldSession = this._sessions[0];
|
||||
this._sessions = [];
|
||||
await this.saveSessions();
|
||||
await clearUserInfo();
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
removed: [oldSession],
|
||||
changed: [],
|
||||
});
|
||||
console.log("🔄 已清除旧的 session");
|
||||
}
|
||||
|
||||
const token = await this.login();
|
||||
|
||||
// 获取到 token 后立即调用用户信息接口
|
||||
const userInfo = await onTokenReceived(token);
|
||||
|
||||
// 创建会话
|
||||
const session: vscode.AuthenticationSession = {
|
||||
id: this.generateSessionId(),
|
||||
accessToken: token,
|
||||
account: {
|
||||
id: "iccoder-user",
|
||||
label: "IC Coder 用户",
|
||||
id: userInfo?.userId || "iccoder-user",
|
||||
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
|
||||
},
|
||||
scopes: [...scopes],
|
||||
};
|
||||
@ -109,6 +145,9 @@ export class ICCoderAuthenticationProvider
|
||||
this._sessions.splice(sessionIndex, 1);
|
||||
await this.saveSessions();
|
||||
|
||||
// 清除用户信息缓存
|
||||
await clearUserInfo();
|
||||
|
||||
// 触发会话变化事件
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
@ -149,9 +188,8 @@ export class ICCoderAuthenticationProvider
|
||||
|
||||
// 构建登录 URL
|
||||
const callbackUrl = `http://localhost:${port}/callback`;
|
||||
const loginUrl = `${
|
||||
ICCoderAuthenticationProvider.LOGIN_URL
|
||||
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||||
const config = getConfig();
|
||||
const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
console.log("🔐 登录服务器已启动,监听端口:", port);
|
||||
console.log("🌐 登录 URL:", loginUrl);
|
||||
|
||||
103
src/services/promptOptimizeService.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 提示词优化服务
|
||||
* 调用后端 API 优化用户输入的提示词
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl } from '../config/settings';
|
||||
|
||||
/** 优化响应类型 */
|
||||
interface OptimizeResponse {
|
||||
success: boolean;
|
||||
optimizedPrompt?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化提示词
|
||||
* @param prompt 原始提示词
|
||||
* @returns 优化后的提示词
|
||||
*/
|
||||
export async function optimizePrompt(prompt: string): Promise<string> {
|
||||
// 获取 JWT token
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
if (!session?.accessToken) {
|
||||
throw new Error('未登录,请先登录');
|
||||
}
|
||||
|
||||
const response = await callOptimizeApi(prompt, session.accessToken);
|
||||
|
||||
if (response.success && response.optimizedPrompt) {
|
||||
return response.optimizedPrompt;
|
||||
} else {
|
||||
throw new Error(response.error || '优化失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用后端优化 API
|
||||
*/
|
||||
async function callOptimizeApi(prompt: string, token: string): Promise<OptimizeResponse> {
|
||||
const urlStr = getApiUrl('/api/prompt/optimize');
|
||||
const url = new URL(urlStr);
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const body = JSON.stringify({ prompt });
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('[PromptOptimize] 响应状态码:', res.statusCode);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as OptimizeResponse);
|
||||
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
resolve({ success: false, error: '登录已过期,请重新登录' });
|
||||
} else {
|
||||
resolve({ success: false, error: json.error || json.message || `HTTP ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({ success: false, error: `解析响应失败: ${data}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
@ -13,6 +13,8 @@ import type {
|
||||
SSEEventType,
|
||||
TextDeltaEvent,
|
||||
ToolCallRequest,
|
||||
ToolConfirmEvent,
|
||||
PlanConfirmEvent,
|
||||
AskUserEvent,
|
||||
CompleteEvent,
|
||||
ErrorEvent,
|
||||
@ -21,8 +23,15 @@ import type {
|
||||
ToolErrorEvent,
|
||||
WarningEvent,
|
||||
NotificationEvent,
|
||||
DepthUpdateEvent
|
||||
DepthUpdateEvent,
|
||||
AgentStartEvent,
|
||||
AgentProgressEvent,
|
||||
AgentCompleteEvent,
|
||||
AgentErrorEvent,
|
||||
ContextUsageEvent,
|
||||
CreditUpdateEvent
|
||||
} from '../types/api';
|
||||
import type { MemoryCompactedEvent } from '../types/memory';
|
||||
|
||||
/**
|
||||
* SSE 事件回调接口
|
||||
@ -32,6 +41,20 @@ export interface SSECallbacks {
|
||||
onTextDelta?: (data: TextDeltaEvent) => void;
|
||||
/** 收到工具调用请求 */
|
||||
onToolCall?: (data: ToolCallRequest) => void;
|
||||
/** 收到工具确认请求(Ask 模式) */
|
||||
onToolConfirm?: (data: ToolConfirmEvent) => void;
|
||||
/** 收到计划确认请求(Plan 模式) */
|
||||
onPlanConfirm?: (data: PlanConfirmEvent) => void;
|
||||
/** 阶段进度更新 */
|
||||
onPhaseProgress?: (data: import('../types/api').PhaseProgressEvent) => void;
|
||||
/** 添加计划步骤 */
|
||||
onPlanStepAdd?: (data: import('../types/api').PlanStepAddEvent) => void;
|
||||
/** 删除计划步骤 */
|
||||
onPlanStepRemove?: (data: import('../types/api').PlanStepRemoveEvent) => void;
|
||||
/** 更新计划步骤 */
|
||||
onPlanStepUpdate?: (data: import('../types/api').PlanStepUpdateEvent) => void;
|
||||
/** 更新计划摘要 */
|
||||
onPlanSummaryUpdate?: (data: import('../types/api').PlanSummaryUpdateEvent) => void;
|
||||
/** 工具开始执行 */
|
||||
onToolStart?: (data: ToolStartEvent) => void;
|
||||
/** 工具执行完成 */
|
||||
@ -50,6 +73,20 @@ export interface SSECallbacks {
|
||||
onNotification?: (data: NotificationEvent) => void;
|
||||
/** 深度更新 */
|
||||
onDepthUpdate?: (data: DepthUpdateEvent) => void;
|
||||
/** 子智能体启动 */
|
||||
onAgentStart?: (data: AgentStartEvent) => void;
|
||||
/** 子智能体进度 */
|
||||
onAgentProgress?: (data: AgentProgressEvent) => void;
|
||||
/** 子智能体完成 */
|
||||
onAgentComplete?: (data: AgentCompleteEvent) => void;
|
||||
/** 子智能体错误 */
|
||||
onAgentError?: (data: AgentErrorEvent) => void;
|
||||
/** 记忆压缩完成 */
|
||||
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
||||
/** 上下文使用量更新 */
|
||||
onContextUsage?: (data: ContextUsageEvent) => void;
|
||||
/** 资源点余额更新 */
|
||||
onCreditUpdate?: (data: CreditUpdateEvent) => void;
|
||||
/** 连接打开 */
|
||||
onOpen?: () => void;
|
||||
/** 连接关闭 */
|
||||
@ -124,7 +161,7 @@ export async function startStreamDialog(
|
||||
|
||||
const body = JSON.stringify(request);
|
||||
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, url=${urlString}`);
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options: http.RequestOptions = {
|
||||
@ -136,7 +173,8 @@ export async function startStreamDialog(
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
...(request.token ? { 'Authorization': `Bearer ${request.token}` } : {})
|
||||
}
|
||||
};
|
||||
|
||||
@ -146,9 +184,20 @@ export async function startStreamDialog(
|
||||
let errorBody = '';
|
||||
res.on('data', chunk => errorBody += chunk);
|
||||
res.on('end', () => {
|
||||
// 检测是否是登录状态过期
|
||||
const isLoginExpired = errorBody.includes('登录状态已过期') ||
|
||||
errorBody.includes('token') && errorBody.includes('过期') ||
|
||||
res.statusCode === 401;
|
||||
|
||||
if (isLoginExpired) {
|
||||
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
} else {
|
||||
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -189,6 +238,25 @@ export async function startStreamDialog(
|
||||
res.on('data', (chunk: string) => {
|
||||
if (!controller.aborted) {
|
||||
console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200));
|
||||
|
||||
// 检查是否是业务错误码(Gateway 返回 HTTP 200 但响应体是错误 JSON)
|
||||
try {
|
||||
const trimmed = chunk.trim();
|
||||
if (trimmed.startsWith('{') && trimmed.includes('"code"')) {
|
||||
const json = JSON.parse(trimmed);
|
||||
if (json.code === 401 || json.msg?.includes('登录状态已过期')) {
|
||||
console.log('[SSE] 检测到登录过期业务错误');
|
||||
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
|
||||
callbacks.onError?.({ message: error.message });
|
||||
controller.abort();
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 不是 JSON 格式,继续正常处理
|
||||
}
|
||||
|
||||
parser.feed(chunk);
|
||||
}
|
||||
});
|
||||
@ -256,6 +324,27 @@ function dispatchEvent(
|
||||
case 'tool_call':
|
||||
callbacks.onToolCall?.(data as ToolCallRequest);
|
||||
break;
|
||||
case 'tool_confirm':
|
||||
callbacks.onToolConfirm?.(data as ToolConfirmEvent);
|
||||
break;
|
||||
case 'plan_confirm':
|
||||
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
|
||||
break;
|
||||
case 'phase_progress':
|
||||
callbacks.onPhaseProgress?.(data as import('../types/api').PhaseProgressEvent);
|
||||
break;
|
||||
case 'plan_step_add':
|
||||
callbacks.onPlanStepAdd?.(data as import('../types/api').PlanStepAddEvent);
|
||||
break;
|
||||
case 'plan_step_remove':
|
||||
callbacks.onPlanStepRemove?.(data as import('../types/api').PlanStepRemoveEvent);
|
||||
break;
|
||||
case 'plan_step_update':
|
||||
callbacks.onPlanStepUpdate?.(data as import('../types/api').PlanStepUpdateEvent);
|
||||
break;
|
||||
case 'plan_summary_update':
|
||||
callbacks.onPlanSummaryUpdate?.(data as import('../types/api').PlanSummaryUpdateEvent);
|
||||
break;
|
||||
case 'tool_start':
|
||||
callbacks.onToolStart?.(data as ToolStartEvent);
|
||||
break;
|
||||
@ -283,6 +372,32 @@ function dispatchEvent(
|
||||
case 'depth_update':
|
||||
callbacks.onDepthUpdate?.(data as DepthUpdateEvent);
|
||||
break;
|
||||
case 'agent_start':
|
||||
callbacks.onAgentStart?.(data as AgentStartEvent);
|
||||
break;
|
||||
case 'agent_progress':
|
||||
callbacks.onAgentProgress?.(data as AgentProgressEvent);
|
||||
break;
|
||||
case 'agent_complete':
|
||||
callbacks.onAgentComplete?.(data as AgentCompleteEvent);
|
||||
break;
|
||||
case 'agent_error':
|
||||
callbacks.onAgentError?.(data as AgentErrorEvent);
|
||||
break;
|
||||
case 'memory_compacted':
|
||||
callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent);
|
||||
break;
|
||||
case 'context_usage':
|
||||
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
||||
break;
|
||||
case 'credit_update':
|
||||
callbacks.onCreditUpdate?.(data as CreditUpdateEvent);
|
||||
break;
|
||||
case 'heartbeat':
|
||||
// 心跳事件:仅用于保持连接,不需要特殊处理
|
||||
// Node.js req.setTimeout 会在收到数据时自动重置计时器
|
||||
console.log('[SSE] 收到心跳');
|
||||
break;
|
||||
default:
|
||||
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
||||
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
|
||||
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
||||
import { analyzeVcdFile } from '../utils/vcdParser';
|
||||
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
||||
import {
|
||||
submitToolResult,
|
||||
createSuccessResult,
|
||||
@ -20,10 +22,14 @@ import type {
|
||||
ToolName,
|
||||
FileReadArgs,
|
||||
FileWriteArgs,
|
||||
FileDeleteArgs,
|
||||
FileListArgs,
|
||||
SyntaxCheckArgs,
|
||||
IverilogArgs,
|
||||
SimulationArgs,
|
||||
WaveformSummaryArgs
|
||||
WaveformSummaryArgs,
|
||||
KnowledgeSaveArgs,
|
||||
KnowledgeLoadArgs
|
||||
} from '../types/api';
|
||||
|
||||
/**
|
||||
@ -61,18 +67,33 @@ export async function executeToolCall(
|
||||
case 'file_write':
|
||||
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
||||
break;
|
||||
case 'file_delete':
|
||||
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
|
||||
break;
|
||||
case 'file_list':
|
||||
resultText = await executeFileList(args as unknown as FileListArgs);
|
||||
break;
|
||||
case 'syntax_check':
|
||||
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
|
||||
break;
|
||||
case 'iverilog':
|
||||
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
|
||||
break;
|
||||
case 'simulation':
|
||||
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
||||
break;
|
||||
case 'waveform_summary':
|
||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
||||
break;
|
||||
case 'waveform_trace':
|
||||
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
|
||||
break;
|
||||
case 'knowledge_save':
|
||||
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
||||
break;
|
||||
case 'knowledge_load':
|
||||
resultText = await executeKnowledgeLoad();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`未知工具: ${toolName}`);
|
||||
}
|
||||
@ -105,9 +126,59 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
||||
*/
|
||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||
await createOrOverwriteFile(args.path, args.content);
|
||||
|
||||
// Verilog 文件添加知识图谱提示
|
||||
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
||||
if (isVerilogFile) {
|
||||
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
|
||||
}
|
||||
|
||||
return `文件已写入: ${args.path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 file_delete 工具
|
||||
* 删除指定路径的文件
|
||||
*/
|
||||
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||
const filePath = args.path;
|
||||
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 解析文件路径(支持相对路径和绝对路径)
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(workspacePath, filePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
throw new Error(`文件不存在: ${filePath}`);
|
||||
}
|
||||
|
||||
// 检查是否为文件(不允许删除目录)
|
||||
const stat = fs.statSync(absolutePath);
|
||||
if (stat.isDirectory()) {
|
||||
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
fs.unlinkSync(absolutePath);
|
||||
|
||||
// Verilog 文件添加知识图谱提示
|
||||
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
|
||||
if (isVerilogFile) {
|
||||
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
||||
}
|
||||
|
||||
return `文件已删除: ${filePath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 file_list 工具
|
||||
*/
|
||||
@ -203,6 +274,71 @@ async function executeSyntaxCheck(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 iverilog 工具
|
||||
* 直接执行 iverilog 命令
|
||||
*/
|
||||
async function executeIverilog(
|
||||
args: IverilogArgs,
|
||||
context: ToolExecutorContext
|
||||
): Promise<string> {
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
if (!iverilogCheck.available) {
|
||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
||||
}
|
||||
|
||||
// 获取工作目录
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('没有打开的工作区');
|
||||
}
|
||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||
const workDir = args.workDir
|
||||
? path.join(projectPath, args.workDir)
|
||||
: projectPath;
|
||||
|
||||
// 解析参数
|
||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||
const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0);
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(iverilogPath, cmdArgs, {
|
||||
cwd: workDir,
|
||||
env: {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
||||
}
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number) => {
|
||||
const output = stderr || stdout || '(无输出)';
|
||||
if (code === 0) {
|
||||
resolve(`执行成功\n${output}`);
|
||||
} else {
|
||||
resolve(`执行失败 (exit code: ${code})\n${output}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 simulation 工具
|
||||
*/
|
||||
@ -218,7 +354,30 @@ async function executeSimulation(
|
||||
|
||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 调用现有的 generateVCD 函数
|
||||
// 检查是否有 dumpModules 参数(多 VCD 模式)
|
||||
if (args.dumpModules) {
|
||||
const modules = parseDumpModules(args.dumpModules);
|
||||
const vcdDir = args.vcdDir || 'vcd';
|
||||
|
||||
const result = await generateMultiVCD(
|
||||
projectPath,
|
||||
context.extensionPath,
|
||||
args.tbPath,
|
||||
modules,
|
||||
vcdDir
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const vcdList = result.vcdFiles
|
||||
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`)
|
||||
.join('\n');
|
||||
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`;
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 原有单 VCD 逻辑
|
||||
const result = await generateVCD(projectPath, context.extensionPath);
|
||||
|
||||
if (result.success) {
|
||||
@ -236,14 +395,108 @@ async function executeSimulation(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 dumpModules 参数
|
||||
* 格式:name:path,name:path
|
||||
*/
|
||||
function parseDumpModules(dumpModules: string): DumpModule[] {
|
||||
return dumpModules.split(',').map(item => {
|
||||
const [name, modulePath] = item.trim().split(':');
|
||||
return { name: name.trim(), path: modulePath.trim() };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 waveform_summary 工具
|
||||
* TODO: 实现 VCD 波形分析
|
||||
* 解析 VCD 文件并返回波形摘要
|
||||
*/
|
||||
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
|
||||
// TODO: 使用 vcdrom/vcd-stream 解析 VCD 文件
|
||||
// 目前返回一个占位响应
|
||||
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`;
|
||||
const { vcdPath, signals, checkpoints } = args;
|
||||
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 解析 VCD 文件路径(支持相对路径)
|
||||
const absolutePath = path.isAbsolute(vcdPath)
|
||||
? vcdPath
|
||||
: path.join(workspacePath, vcdPath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
throw new Error(`VCD 文件不存在: ${vcdPath}`);
|
||||
}
|
||||
|
||||
// 解析检查点时间
|
||||
const checkpoint = checkpoints ? parseInt(checkpoints, 10) : undefined;
|
||||
|
||||
// 调用 VCD 解析器
|
||||
const result = analyzeVcdFile(absolutePath, signals, checkpoint);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 knowledge_save 工具
|
||||
* 保存知识图谱到 .iccoder/knowledge.json
|
||||
*/
|
||||
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||
const workspaceFolder = getWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
|
||||
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
|
||||
|
||||
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
|
||||
await vscode.workspace.fs.createDirectory(iccoderDirUri);
|
||||
|
||||
// 写入知识图谱(UTF-8)
|
||||
await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8'));
|
||||
|
||||
return `知识图谱已保存: .iccoder/knowledge.json`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 knowledge_load 工具
|
||||
* 从 .iccoder/knowledge.json 加载知识图谱
|
||||
*/
|
||||
async function executeKnowledgeLoad(): Promise<string> {
|
||||
const workspaceFolder = getWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json');
|
||||
|
||||
try {
|
||||
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
|
||||
const content = Buffer.from(bytes).toString('utf-8');
|
||||
return content;
|
||||
} catch (error) {
|
||||
// 文件不存在:返回空图谱
|
||||
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
||||
// 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段)
|
||||
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||
return activeFolder ?? folders[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import { submitAnswer } from './apiClient';
|
||||
import { submitAnswer, submitToolConfirm } from './apiClient';
|
||||
import type { AskUserEvent, AnswerRequest } from '../types/api';
|
||||
|
||||
/**
|
||||
@ -32,6 +32,13 @@ export class UserInteractionManager {
|
||||
this.webviewPanel = panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebView 面板
|
||||
*/
|
||||
getWebviewPanel(): vscode.WebviewPanel | null {
|
||||
return this.webviewPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ask_user 事件
|
||||
* @param event ask_user 事件数据
|
||||
@ -60,13 +67,13 @@ export class UserInteractionManager {
|
||||
reject
|
||||
});
|
||||
|
||||
// 设置超时(5分钟)
|
||||
// 设置超时(2小时)
|
||||
setTimeout(() => {
|
||||
if (this.pendingQuestions.has(askId)) {
|
||||
this.pendingQuestions.delete(askId);
|
||||
reject(new Error('用户回答超时'));
|
||||
}
|
||||
}, 300000);
|
||||
}, 7200000);
|
||||
});
|
||||
}
|
||||
|
||||
@ -75,21 +82,28 @@ export class UserInteractionManager {
|
||||
* @param askId 问题ID
|
||||
* @param selected 选中的选项
|
||||
* @param customInput 自定义输入
|
||||
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
||||
*/
|
||||
async receiveAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
customInput?: string,
|
||||
fallbackTaskId?: string
|
||||
): Promise<void> {
|
||||
const pending = this.pendingQuestions.get(askId);
|
||||
const answer = customInput || selected?.join(', ') || '';
|
||||
|
||||
if (!pending) {
|
||||
console.warn(`[UserInteraction] 问题不存在或已超时: askId=${askId}`);
|
||||
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
||||
if (fallbackTaskId) {
|
||||
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
||||
await this.submitUserAnswer(askId, fallbackTaskId, answer);
|
||||
} else {
|
||||
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建答案
|
||||
const answer = customInput || selected?.join(', ') || '';
|
||||
|
||||
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
|
||||
|
||||
// 移除待处理问题
|
||||
@ -107,6 +121,30 @@ export class UserInteractionManager {
|
||||
taskId: string,
|
||||
answer: string
|
||||
): Promise<void> {
|
||||
// 检查是否是工具确认类型的问题
|
||||
if (askId.startsWith('tool_confirm_')) {
|
||||
// 提取 confirmId
|
||||
const confirmId = parseInt(askId.replace('tool_confirm_', ''));
|
||||
const approved = answer === '确认执行';
|
||||
|
||||
console.log(`[UserInteraction] 提交工具确认: confirmId=${confirmId}, approved=${approved}`);
|
||||
|
||||
try {
|
||||
const response = await submitToolConfirm({
|
||||
confirmId,
|
||||
taskId,
|
||||
approved
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || '提交工具确认失败');
|
||||
}
|
||||
console.log(`[UserInteraction] 工具确认已提交: confirmId=${confirmId}`);
|
||||
} catch (error) {
|
||||
console.error(`[UserInteraction] 提交工具确认失败: confirmId=${confirmId}`, error);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// 普通问题回答
|
||||
const request: AnswerRequest = {
|
||||
askId,
|
||||
taskId,
|
||||
@ -124,6 +162,7 @@ export class UserInteractionManager {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有待处理的问题
|
||||
@ -141,6 +180,13 @@ export class UserInteractionManager {
|
||||
hasPendingQuestions(): boolean {
|
||||
return this.pendingQuestions.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查特定问题是否存在
|
||||
*/
|
||||
hasPendingQuestion(askId: string): boolean {
|
||||
return this.pendingQuestions.has(askId);
|
||||
}
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
|
||||
378
src/services/userService.ts
Normal file
@ -0,0 +1,378 @@
|
||||
/**
|
||||
* 用户服务
|
||||
* 管理用户信息和认证相关的 API 调用
|
||||
*/
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import * as vscode from 'vscode';
|
||||
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
|
||||
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
|
||||
import { fetchBalanceWithToken, getCachedBalance } from './creditsService';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
*/
|
||||
interface RequestOptions {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
timeout?: number;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求(带 token)
|
||||
*/
|
||||
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
const url = new URL(getStrangeLoopApiUrl(path));
|
||||
const { timeout } = getConfig();
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
// 如果有 token,添加到请求头
|
||||
if (options.token) {
|
||||
headers['Authorization'] = `Bearer ${options.token}`;
|
||||
}
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: options.method,
|
||||
headers,
|
||||
timeout: options.timeout || timeout
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log(`[HTTP] 响应状态码: ${res.statusCode}`);
|
||||
console.log(`[HTTP] 响应内容: ${data}`);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as T);
|
||||
} else {
|
||||
reject(new Error(json.error || json.message || json.msg || `HTTP ${res.statusCode}`));
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果不是 JSON,直接返回原始内容
|
||||
reject(new Error(`解析响应失败 (${res.statusCode}): ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(JSON.stringify(options.body));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息数据结构(实际返回的数据)
|
||||
*/
|
||||
export interface UserInfo {
|
||||
userId: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
phonenumber?: string;
|
||||
avatar?: string;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
createTime?: string;
|
||||
loginDate?: string;
|
||||
// 会员信息
|
||||
membership?: {
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
remainingDays?: number;
|
||||
monthlyCredits?: number;
|
||||
};
|
||||
// Credits 余额
|
||||
credits?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||||
const apiPath = '/system/user/getInfo';
|
||||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||
console.log('[UserService] 获取用户信息');
|
||||
console.log('[UserService] 请求地址:', fullUrl);
|
||||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||
|
||||
try {
|
||||
const response = await request<UserInfoResponse>(apiPath, {
|
||||
method: 'GET',
|
||||
token
|
||||
});
|
||||
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.user) {
|
||||
const user = response.user;
|
||||
return {
|
||||
userId: String(user.userId),
|
||||
username: user.userName,
|
||||
nickname: user.nickName,
|
||||
email: user.email,
|
||||
phonenumber: user.phonenumber,
|
||||
avatar: user.avatar,
|
||||
roles: response.roles,
|
||||
permissions: response.permissions,
|
||||
createTime: user.createTime,
|
||||
loginDate: user.loginDate
|
||||
};
|
||||
}
|
||||
|
||||
console.error('[UserService] 获取用户信息失败:', response);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户会员信息
|
||||
* GET /strangeloop/api/membership/current
|
||||
*/
|
||||
export async function getMembershipInfo(token: string): Promise<MultiMembershipVO | null> {
|
||||
const apiPath = '/strangeloop/api/membership/current';
|
||||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||
console.log('[UserService] 获取会员信息');
|
||||
console.log('[UserService] 请求地址:', fullUrl);
|
||||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||
|
||||
try {
|
||||
const response = await request<MembershipResponse>(apiPath, {
|
||||
method: 'GET',
|
||||
token
|
||||
});
|
||||
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.data) {
|
||||
console.log('[UserService] 会员信息获取成功:', response.data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.error('[UserService] 获取会员信息失败:', response);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 请求会员信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级映射
|
||||
*/
|
||||
const TIER_LEVEL_MAP: Record<string, number> = {
|
||||
'BASIC': 1,
|
||||
'TRIAL': 2,
|
||||
'ADVANCED': 3,
|
||||
'PROFESSIONAL': 4
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取最高等级的会员信息
|
||||
*/
|
||||
function getHighestTierMembership(allMemberships?: MembershipItemVO[]): MembershipItemVO | null {
|
||||
if (!allMemberships || allMemberships.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 按等级排序,获取最高等级
|
||||
return allMemberships.reduce((highest, current) => {
|
||||
const currentLevel = TIER_LEVEL_MAP[current.tierCode] || 0;
|
||||
const highestLevel = TIER_LEVEL_MAP[highest.tierCode] || 0;
|
||||
return currentLevel > highestLevel ? current : highest;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当获取到 token 时自动调用此函数
|
||||
* 用于在登录成功后立即获取用户信息
|
||||
*/
|
||||
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
try {
|
||||
console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...');
|
||||
|
||||
// 并行获取用户信息、会员信息和余额
|
||||
const [userInfo, membershipInfo, credits] = await Promise.all([
|
||||
getUserInfo(token),
|
||||
getMembershipInfo(token),
|
||||
fetchBalanceWithToken(token)
|
||||
]);
|
||||
|
||||
if (!userInfo) {
|
||||
console.warn('[UserService] 未能获取到用户信息');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 添加 Credits 余额到用户信息
|
||||
console.log('[UserService] 获取到的 Credits 余额:', credits);
|
||||
if (credits !== null) {
|
||||
userInfo.credits = credits;
|
||||
console.log('[UserService] Credits 已添加到用户信息');
|
||||
} else {
|
||||
console.warn('[UserService] Credits 余额为 null,未添加到用户信息');
|
||||
}
|
||||
|
||||
// 打印用户信息到控制台
|
||||
console.log('='.repeat(60));
|
||||
console.log('用户信息详情:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`用户ID: ${userInfo.userId}`);
|
||||
console.log(`用户名: ${userInfo.username}`);
|
||||
console.log(`昵称: ${userInfo.nickname}`);
|
||||
if (userInfo.email) {
|
||||
console.log(`邮箱: ${userInfo.email}`);
|
||||
}
|
||||
if (userInfo.phonenumber) {
|
||||
console.log(`手机号: ${userInfo.phonenumber}`);
|
||||
}
|
||||
if (userInfo.avatar) {
|
||||
console.log(`头像: ${userInfo.avatar}`);
|
||||
}
|
||||
if (userInfo.roles && userInfo.roles.length > 0) {
|
||||
console.log(`角色: ${userInfo.roles.join(', ')}`);
|
||||
}
|
||||
if (userInfo.permissions && userInfo.permissions.length > 0) {
|
||||
console.log(`权限: ${userInfo.permissions.join(', ')}`);
|
||||
}
|
||||
if (userInfo.createTime) {
|
||||
console.log(`创建时间: ${userInfo.createTime}`);
|
||||
}
|
||||
if (userInfo.loginDate) {
|
||||
console.log(`最后登录: ${userInfo.loginDate}`);
|
||||
}
|
||||
|
||||
// 打印会员信息 - 从 allMemberships 中获取最高等级
|
||||
if (membershipInfo && membershipInfo.allMemberships) {
|
||||
const highestTier = getHighestTierMembership(membershipInfo.allMemberships);
|
||||
|
||||
if (highestTier) {
|
||||
console.log('');
|
||||
console.log('会员信息:');
|
||||
console.log(`会员等级: ${highestTier.tierName} (${highestTier.tierCode})`);
|
||||
console.log(`等级层级: ${highestTier.tierLevel}`);
|
||||
console.log(`剩余天数: ${highestTier.remainingDays === -1 ? '永久' : highestTier.remainingDays + '天'}`);
|
||||
console.log(`月度积分: ${highestTier.monthlyCredits}`);
|
||||
|
||||
// 将最高等级会员信息合并到用户信息中
|
||||
userInfo.membership = {
|
||||
tierCode: highestTier.tierCode,
|
||||
tierName: highestTier.tierName,
|
||||
tierLevel: highestTier.tierLevel,
|
||||
remainingDays: highestTier.remainingDays,
|
||||
monthlyCredits: highestTier.monthlyCredits
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 打印 Credits 余额
|
||||
console.log('');
|
||||
console.log('资源点余额:');
|
||||
if (userInfo.credits !== undefined) {
|
||||
console.log(`当前余额: ${userInfo.credits} Credits`);
|
||||
} else {
|
||||
console.log('当前余额: 未获取到余额信息');
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 保存到持久化存储
|
||||
await saveUserInfo(userInfo);
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 获取用户信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 持久化存储 ==============
|
||||
|
||||
let extensionContext: vscode.ExtensionContext | null = null;
|
||||
|
||||
/**
|
||||
* 初始化用户服务(设置 context)
|
||||
*/
|
||||
export function initUserService(context: vscode.ExtensionContext): void {
|
||||
extensionContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户信息到持久化存储
|
||||
*/
|
||||
export async function saveUserInfo(userInfo: UserInfo): Promise<void> {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return;
|
||||
}
|
||||
await extensionContext.globalState.update('icCoderUserInfo', userInfo);
|
||||
console.log('[UserService] 用户信息已保存到持久化存储');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从持久化存储获取用户信息
|
||||
*/
|
||||
export function getCachedUserInfo(): UserInfo | null {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return null;
|
||||
}
|
||||
const userInfo = extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
||||
|
||||
// 从 creditsService 加载余额并合并到用户信息中
|
||||
if (userInfo) {
|
||||
const cachedCredits = getCachedBalance();
|
||||
if (cachedCredits !== null) {
|
||||
userInfo.credits = cachedCredits;
|
||||
console.log('[UserService] 从 creditsService 加载余额:', cachedCredits);
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除持久化存储的用户信息
|
||||
*/
|
||||
export async function clearUserInfo(): Promise<void> {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return;
|
||||
}
|
||||
await extensionContext.globalState.update('icCoderUserInfo', undefined);
|
||||
console.log('[UserService] 用户信息已清除');
|
||||
}
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
386
src/types/api.ts
@ -3,8 +3,28 @@
|
||||
* 对应后端 IC Coder Backend 的接口格式
|
||||
*/
|
||||
|
||||
import { CompactedMemory, CompactedMessage } from "./memory";
|
||||
|
||||
// ============== 对话请求/响应 ==============
|
||||
|
||||
/**
|
||||
* 运行模式类型
|
||||
* - plan: 只读模式,只能查询分析
|
||||
* - ask: 逐个确认,每个写操作需确认
|
||||
* - agent: 智能体自主(默认)
|
||||
* - auto: 完全自动
|
||||
*/
|
||||
export type RunMode = "plan" | "ask" | "agent" | "auto";
|
||||
|
||||
/**
|
||||
* 服务等级类型
|
||||
* - lite: 轻量级
|
||||
* - syntaxic: 语法级
|
||||
* - max: 最大性能
|
||||
* - auto: 自动选择
|
||||
*/
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
|
||||
/**
|
||||
* 对话请求
|
||||
* POST /api/dialog/stream
|
||||
@ -16,25 +36,50 @@ export interface DialogRequest {
|
||||
message: string;
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 工具模式 */
|
||||
toolMode: 'ASK' | 'AGENT';
|
||||
/** 运行模式 */
|
||||
mode: RunMode;
|
||||
/** 服务等级 */
|
||||
serviceTier?: ServiceTier;
|
||||
/** JWT Token(用于认证和扣费) */
|
||||
token?: string;
|
||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||
compactedData?: CompactedMemory;
|
||||
/** 压缩后产生的新消息 */
|
||||
newMessages?: CompactedMessage[];
|
||||
/** 知识图谱数据(JSON 字符串,用于恢复知识图谱) */
|
||||
knowledgeData?: string;
|
||||
}
|
||||
|
||||
// ============== SSE 事件类型 ==============
|
||||
|
||||
/** SSE 事件类型枚举 */
|
||||
export type SSEEventType =
|
||||
| 'text_delta' // 文本增量
|
||||
| 'tool_call' // 客户端工具调用请求
|
||||
| 'tool_start' // 工具开始执行
|
||||
| 'tool_complete' // 工具执行完成
|
||||
| 'tool_error' // 工具执行错误
|
||||
| 'ask_user' // 向用户提问
|
||||
| 'complete' // 对话完成
|
||||
| 'error' // 错误
|
||||
| 'warning' // 警告
|
||||
| 'notification' // 通知
|
||||
| 'depth_update'; // 深度更新
|
||||
| "text_delta" // 文本增量
|
||||
| "tool_call" // 客户端工具调用请求
|
||||
| "tool_confirm" // 工具确认请求(Ask 模式)
|
||||
| "plan_confirm" // 计划确认请求(Plan 模式)
|
||||
| "phase_progress" // 阶段进度更新
|
||||
| "plan_step_add" // 添加计划步骤
|
||||
| "plan_step_remove" // 删除计划步骤
|
||||
| "plan_step_update" // 更新计划步骤
|
||||
| "plan_summary_update" // 更新计划摘要
|
||||
| "tool_start" // 工具开始执行
|
||||
| "tool_complete" // 工具执行完成
|
||||
| "tool_error" // 工具执行错误
|
||||
| "ask_user" // 向用户提问
|
||||
| "agent_start" // 子智能体启动
|
||||
| "agent_progress" // 子智能体进度
|
||||
| "agent_complete" // 子智能体完成
|
||||
| "agent_error" // 子智能体错误
|
||||
| "memory_compacted" // 记忆压缩完成
|
||||
| "context_usage" // 上下文使用量
|
||||
| "credit_update" // 资源点余额更新
|
||||
| "complete" // 对话完成
|
||||
| "error" // 错误
|
||||
| "warning" // 警告
|
||||
| "notification" // 通知
|
||||
| "depth_update" // 深度更新
|
||||
| "heartbeat"; // 心跳
|
||||
|
||||
/** text_delta 事件数据 */
|
||||
export interface TextDeltaEvent {
|
||||
@ -59,6 +104,95 @@ export interface ToolErrorEvent {
|
||||
error: string;
|
||||
}
|
||||
|
||||
/** tool_confirm 事件数据(Ask 模式确认请求) */
|
||||
export interface ToolConfirmEvent {
|
||||
/** 确认ID,用于匹配响应 */
|
||||
confirmId: number;
|
||||
/** 工具名称 */
|
||||
toolName: string;
|
||||
/** 工具输入参数 */
|
||||
toolInput: Record<string, unknown>;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** 计划步骤 */
|
||||
export interface PlanStep {
|
||||
/** 步骤名称 */
|
||||
name: string;
|
||||
/** 步骤描述 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 计划阶段 */
|
||||
export interface PlanPhase {
|
||||
/** 阶段ID: spec/design/sim/done */
|
||||
id: string;
|
||||
/** 阶段名称 */
|
||||
name: string;
|
||||
/** 阶段状态: skipped/completed/current/pending */
|
||||
status: string;
|
||||
/** 跳过原因 */
|
||||
reason?: string;
|
||||
/** 阶段内的步骤 */
|
||||
steps: PlanStep[];
|
||||
}
|
||||
|
||||
/** plan_confirm 事件数据(Plan 模式计划确认) */
|
||||
export interface PlanConfirmEvent {
|
||||
/** 确认ID */
|
||||
confirmId: number;
|
||||
/** 计划标题 */
|
||||
title: string;
|
||||
/** 四阶段计划列表(新格式) */
|
||||
phases?: PlanPhase[];
|
||||
/** 执行步骤列表(旧格式,兼容) */
|
||||
steps?: string[];
|
||||
/** 计划摘要 */
|
||||
summary: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** phase_progress 事件数据(阶段进度更新) */
|
||||
export interface PhaseProgressEvent {
|
||||
/** 阶段ID: spec/design/sim/done */
|
||||
phaseId: string;
|
||||
/** 状态: current/completed */
|
||||
status: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_step_add 事件数据(添加计划步骤) */
|
||||
export interface PlanStepAddEvent {
|
||||
phaseId: string;
|
||||
step: PlanStep;
|
||||
index: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_step_remove 事件数据(删除计划步骤) */
|
||||
export interface PlanStepRemoveEvent {
|
||||
phaseId: string;
|
||||
stepIndex: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_step_update 事件数据(更新计划步骤) */
|
||||
export interface PlanStepUpdateEvent {
|
||||
phaseId: string;
|
||||
stepIndex: number;
|
||||
step: PlanStep;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_summary_update 事件数据(更新计划摘要) */
|
||||
export interface PlanSummaryUpdateEvent {
|
||||
summary: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** ask_user 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
@ -92,6 +226,58 @@ export interface DepthUpdateEvent {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
// ============== 智能体事件类型 ==============
|
||||
|
||||
/** agent_start 事件数据 */
|
||||
export interface AgentStartEvent {
|
||||
agentId: string;
|
||||
agentType: string;
|
||||
agentName: string;
|
||||
instruction: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** agent_progress 事件数据 */
|
||||
export interface AgentProgressEvent {
|
||||
agentId: string;
|
||||
step: number;
|
||||
toolName: string;
|
||||
toolInput?: unknown;
|
||||
toolResult?: string;
|
||||
status: "running" | "completed" | "error";
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** agent_complete 事件数据 */
|
||||
export interface AgentCompleteEvent {
|
||||
agentId: string;
|
||||
agentType: string;
|
||||
summary: string;
|
||||
stats: Record<string, unknown>;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** agent_error 事件数据 */
|
||||
export interface AgentErrorEvent {
|
||||
agentId: string;
|
||||
agentType: string;
|
||||
error: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** context_usage 事件数据 */
|
||||
export interface ContextUsageEvent {
|
||||
currentTokens: number;
|
||||
maxTokens: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
/** credit_update 事件数据 */
|
||||
export interface CreditUpdateEvent {
|
||||
deductedCredits: number;
|
||||
remainingCredits: number;
|
||||
}
|
||||
|
||||
// ============== 工具调用协议 (MCP 格式) ==============
|
||||
|
||||
/**
|
||||
@ -100,11 +286,11 @@ export interface DepthUpdateEvent {
|
||||
*/
|
||||
export interface ToolCallRequest {
|
||||
/** JSON-RPC版本,固定为"2.0" */
|
||||
jsonrpc: '2.0';
|
||||
jsonrpc: "2.0";
|
||||
/** 请求ID,用于匹配响应 */
|
||||
id: number;
|
||||
/** 方法名,固定为"tools/call" */
|
||||
method: 'tools/call';
|
||||
method: "tools/call";
|
||||
/** 调用参数 */
|
||||
params: {
|
||||
/** 工具名称 */
|
||||
@ -120,7 +306,7 @@ export interface ToolCallRequest {
|
||||
*/
|
||||
export interface ToolCallResult {
|
||||
/** JSON-RPC版本 */
|
||||
jsonrpc: '2.0';
|
||||
jsonrpc: "2.0";
|
||||
/** 请求ID,与ToolCallRequest.id对应 */
|
||||
id: number;
|
||||
/** 执行结果(与error互斥) */
|
||||
@ -186,16 +372,126 @@ export interface ToolResultResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ============== 工具确认响应 ==============
|
||||
|
||||
/**
|
||||
* 工具确认响应请求
|
||||
* POST /api/tool/confirm
|
||||
*/
|
||||
export interface ToolConfirmResponse {
|
||||
/** 确认ID,与 ToolConfirmEvent.confirmId 对应 */
|
||||
confirmId: number;
|
||||
/** 任务ID */
|
||||
taskId: string;
|
||||
/** 是否批准执行 */
|
||||
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 =
|
||||
| 'file_read'
|
||||
| 'file_write'
|
||||
| 'file_list'
|
||||
| 'syntax_check'
|
||||
| 'simulation'
|
||||
| 'waveform_summary';
|
||||
| "file_read"
|
||||
| "file_write"
|
||||
| "file_delete"
|
||||
| "file_list"
|
||||
| "syntax_check"
|
||||
| "iverilog"
|
||||
| "simulation"
|
||||
| "waveform_summary"
|
||||
| "waveform_trace"
|
||||
| "knowledge_save"
|
||||
| "knowledge_load";
|
||||
|
||||
/** file_read 工具参数 */
|
||||
export interface FileReadArgs {
|
||||
@ -208,6 +504,12 @@ export interface FileWriteArgs {
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** file_delete 工具参数 */
|
||||
export interface FileDeleteArgs {
|
||||
/** 要删除的文件路径 */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/** file_list 工具参数 */
|
||||
export interface FileListArgs {
|
||||
path?: string;
|
||||
@ -219,11 +521,21 @@ export interface SyntaxCheckArgs {
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** iverilog 工具参数 */
|
||||
export interface IverilogArgs {
|
||||
args: string;
|
||||
workDir?: string;
|
||||
}
|
||||
|
||||
/** simulation 工具参数 */
|
||||
export interface SimulationArgs {
|
||||
rtlPath: string;
|
||||
tbPath: string;
|
||||
duration?: string;
|
||||
/** 要dump的模块列表,格式:name:path,name:path */
|
||||
dumpModules?: string;
|
||||
/** VCD输出目录,默认'vcd' */
|
||||
vcdDir?: string;
|
||||
}
|
||||
|
||||
/** waveform_summary 工具参数 */
|
||||
@ -233,11 +545,39 @@ export interface WaveformSummaryArgs {
|
||||
checkpoints?: string;
|
||||
}
|
||||
|
||||
/** waveform_trace 工具参数 */
|
||||
export interface WaveformTraceArgs {
|
||||
/** Verilog 源文件路径(相对于项目根目录) */
|
||||
verilogPath: string;
|
||||
/** VCD 波形文件路径(相对于项目根目录) */
|
||||
vcdPath: string;
|
||||
/** 仿真工具的输出字符串(包含 mismatch 信息) */
|
||||
simOutput: string;
|
||||
/** BFS 回溯层数,默认 2 */
|
||||
traceLevel?: number;
|
||||
}
|
||||
|
||||
/** knowledge_save 工具参数 */
|
||||
export interface KnowledgeSaveArgs {
|
||||
/** 知识图谱 JSON 数据 */
|
||||
data: string;
|
||||
}
|
||||
|
||||
/** knowledge_load 工具参数 */
|
||||
export interface KnowledgeLoadArgs {
|
||||
// 无参数,直接读取 .iccoder/knowledge.json
|
||||
}
|
||||
|
||||
/** 工具参数联合类型 */
|
||||
export type ToolArgs =
|
||||
| FileReadArgs
|
||||
| FileWriteArgs
|
||||
| FileDeleteArgs
|
||||
| FileListArgs
|
||||
| SyntaxCheckArgs
|
||||
| IverilogArgs
|
||||
| SimulationArgs
|
||||
| WaveformSummaryArgs;
|
||||
| WaveformSummaryArgs
|
||||
| WaveformTraceArgs
|
||||
| KnowledgeSaveArgs
|
||||
| KnowledgeLoadArgs;
|
||||
|
||||
@ -5,7 +5,8 @@ export enum MessageType {
|
||||
SYSTEM = "SYSTEM",
|
||||
USER = "USER",
|
||||
AI = "AI",
|
||||
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT"
|
||||
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT",
|
||||
COMPACTION_SUMMARY = "COMPACTION_SUMMARY" // 压缩摘要
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,10 +70,22 @@ export interface ToolExecutionResultMessage extends BaseMessage {
|
||||
text: string; // JSON字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩摘要消息
|
||||
*/
|
||||
export interface CompactionSummaryMessage extends BaseMessage {
|
||||
type: MessageType.COMPACTION_SUMMARY;
|
||||
summary: string;
|
||||
version: number;
|
||||
compactedAt: string;
|
||||
originalMessageCount: number;
|
||||
compactedMessageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 联合消息类型
|
||||
*/
|
||||
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage;
|
||||
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage | CompactionSummaryMessage;
|
||||
|
||||
/**
|
||||
* 对话轮次元数据
|
||||
|
||||
42
src/types/memory.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 压缩记忆相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 压缩后的记忆数据
|
||||
*/
|
||||
export interface CompactedMemory {
|
||||
taskId: string;
|
||||
version: number;
|
||||
compactedAt: string;
|
||||
summary: string;
|
||||
recentMessages: CompactedMessage[];
|
||||
originalMessageCount: number;
|
||||
compactedMessageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩消息格式
|
||||
*/
|
||||
export interface CompactedMessage {
|
||||
type: 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT';
|
||||
content: string;
|
||||
toolCall?: ToolCallInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用信息
|
||||
*/
|
||||
export interface ToolCallInfo {
|
||||
toolName: string;
|
||||
toolInput: string;
|
||||
toolOutput?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记忆压缩 SSE 事件
|
||||
*/
|
||||
export interface MemoryCompactedEvent {
|
||||
taskId: string;
|
||||
compactedData: CompactedMemory;
|
||||
}
|
||||
@ -9,8 +9,10 @@ import {
|
||||
UserMessage,
|
||||
AiMessage,
|
||||
SystemMessage,
|
||||
ToolExecutionResultMessage
|
||||
ToolExecutionResultMessage,
|
||||
CompactionSummaryMessage
|
||||
} from '../types/chatHistory';
|
||||
import { CompactedMemory, CompactedMessage } from '../types/memory';
|
||||
|
||||
/**
|
||||
* 会话历史管理器
|
||||
@ -23,6 +25,8 @@ export class ChatHistoryManager {
|
||||
private currentProjectPath: string | null = null;
|
||||
// 存储每个面板的任务信息(taskId 和 projectPath)
|
||||
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
|
||||
// 追踪压缩后产生的新消息
|
||||
private newMessagesSinceCompaction: CompactedMessage[] = [];
|
||||
|
||||
private constructor() {
|
||||
// 设置存储路径: ~/.iccoder
|
||||
@ -690,4 +694,230 @@ export class ChatHistoryManager {
|
||||
hasMore: end < total
|
||||
};
|
||||
}
|
||||
|
||||
// ========== 压缩数据相关方法 ==========
|
||||
|
||||
/**
|
||||
* 保存压缩数据(存入 conversation.json 作为压缩摘要消息)
|
||||
*/
|
||||
public async saveCompactedData(compacted: CompactedMemory): Promise<void> {
|
||||
// 尝试从多个来源获取 projectPath
|
||||
let projectPath = this.currentProjectPath;
|
||||
|
||||
if (!projectPath) {
|
||||
for (const [, taskInfo] of this.panelTaskMap) {
|
||||
if (taskInfo.taskId === compacted.taskId) {
|
||||
projectPath = taskInfo.projectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
||||
// 通知用户压缩数据保存失败
|
||||
vscode.window.showWarningMessage(
|
||||
'对话历史压缩数据保存失败:无法确定项目路径。后端重启后可能无法恢复完整对话历史。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取现有对话历史
|
||||
const taskDir = this.getTaskDir(projectPath, compacted.taskId);
|
||||
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||
let messages: ChatMessage[] = [];
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
messages = JSON.parse(Buffer.from(content).toString('utf-8'));
|
||||
} catch {
|
||||
// 文件不存在,使用空数组
|
||||
}
|
||||
|
||||
// 版本检查:防止旧版本覆盖新版本(从尾部扫描,与加载逻辑一致)
|
||||
let existingSummary: CompactionSummaryMessage | null = null;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||
existingSummary = messages[i] as CompactionSummaryMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existingSummary && existingSummary.version >= compacted.version) {
|
||||
console.log(`[ChatHistoryManager] 跳过旧版本压缩数据: 现有版本=${existingSummary.version}, 新版本=${compacted.version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建压缩摘要消息
|
||||
const summaryMessage: CompactionSummaryMessage = {
|
||||
type: MessageType.COMPACTION_SUMMARY,
|
||||
summary: compacted.summary,
|
||||
version: compacted.version,
|
||||
compactedAt: compacted.compactedAt,
|
||||
originalMessageCount: compacted.originalMessageCount,
|
||||
compactedMessageCount: compacted.compactedMessageCount
|
||||
};
|
||||
|
||||
// 添加到对话历史
|
||||
messages.push(summaryMessage);
|
||||
|
||||
// 保存
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = Buffer.from(JSON.stringify(messages, null, 2), 'utf-8');
|
||||
await vscode.workspace.fs.writeFile(uri, content);
|
||||
|
||||
// 重置新消息追踪
|
||||
this.newMessagesSinceCompaction = [];
|
||||
|
||||
console.log(`[ChatHistoryManager] 压缩摘要已保存到 conversation.json: taskId=${compacted.taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载压缩数据(从 conversation.json 构建)
|
||||
*/
|
||||
public async loadCompactedData(taskId: string): Promise<CompactedMemory | null> {
|
||||
// 尝试从多个来源获取 projectPath
|
||||
let projectPath = this.currentProjectPath;
|
||||
|
||||
if (!projectPath) {
|
||||
for (const [, taskInfo] of this.panelTaskMap) {
|
||||
if (taskInfo.taskId === taskId) {
|
||||
projectPath = taskInfo.projectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.log('[ChatHistoryManager] loadCompactedData: projectPath 为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取 conversation.json
|
||||
const taskDir = this.getTaskDir(projectPath, taskId);
|
||||
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
const messages: ChatMessage[] = JSON.parse(Buffer.from(content).toString('utf-8'));
|
||||
|
||||
if (messages.length === 0) {
|
||||
console.log('[ChatHistoryManager] conversation.json 为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从 conversation.json 构建 CompactedMemory
|
||||
return this.buildCompactedMemoryFromConversation(taskId, messages);
|
||||
} catch {
|
||||
console.log('[ChatHistoryManager] conversation.json 不存在:', conversationPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 conversation.json 构建 CompactedMemory
|
||||
*/
|
||||
private buildCompactedMemoryFromConversation(taskId: string, messages: ChatMessage[]): CompactedMemory {
|
||||
// 查找最后一个压缩摘要消息
|
||||
let lastSummary: CompactionSummaryMessage | null = null;
|
||||
let summaryIndex = -1;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||
lastSummary = messages[i] as CompactionSummaryMessage;
|
||||
summaryIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取摘要后的消息(或全部消息)
|
||||
const recentMessages = summaryIndex >= 0
|
||||
? messages.slice(summaryIndex + 1)
|
||||
: messages;
|
||||
|
||||
// 转换为 CompactedMessage 格式
|
||||
const compactedMessages: CompactedMessage[] = recentMessages.map(msg => ({
|
||||
type: this.mapMessageType(msg.type),
|
||||
content: this.extractMessageContent(msg)
|
||||
}));
|
||||
|
||||
return {
|
||||
taskId,
|
||||
version: lastSummary?.version || Date.now(),
|
||||
compactedAt: lastSummary?.compactedAt || new Date().toISOString(),
|
||||
summary: lastSummary?.summary || '',
|
||||
recentMessages: compactedMessages,
|
||||
originalMessageCount: messages.length,
|
||||
compactedMessageCount: compactedMessages.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射消息类型
|
||||
*/
|
||||
private mapMessageType(type: MessageType): 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT' {
|
||||
switch (type) {
|
||||
case MessageType.USER: return 'USER';
|
||||
case MessageType.AI: return 'AI';
|
||||
case MessageType.SYSTEM: return 'SYSTEM';
|
||||
case MessageType.TOOL_EXECUTION_RESULT: return 'TOOL_RESULT';
|
||||
default: return 'USER';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取消息内容
|
||||
*/
|
||||
private extractMessageContent(msg: ChatMessage): string {
|
||||
switch (msg.type) {
|
||||
case MessageType.USER:
|
||||
return (msg as UserMessage).contents?.[0]?.text || '';
|
||||
case MessageType.AI:
|
||||
return (msg as AiMessage).text || '';
|
||||
case MessageType.SYSTEM:
|
||||
return (msg as SystemMessage).text || '';
|
||||
case MessageType.TOOL_EXECUTION_RESULT:
|
||||
return (msg as ToolExecutionResultMessage).text || '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取压缩后产生的新消息
|
||||
*/
|
||||
public getNewMessagesSinceCompaction(): CompactedMessage[] {
|
||||
return this.newMessagesSinceCompaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(用户消息)
|
||||
*/
|
||||
public trackUserMessage(text: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'USER',
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(AI消息)
|
||||
*/
|
||||
public trackAiMessage(text: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'AI',
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(工具执行结果)
|
||||
*/
|
||||
public trackToolResult(toolName: string, result: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'TOOL_RESULT',
|
||||
content: `[${toolName}] ${result}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,9 @@ export async function createFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +30,7 @@ export async function createFile(
|
||||
throw new Error(`文件已存在: ${absolutePath}`);
|
||||
} catch (error: any) {
|
||||
// 如果文件不存在,继续创建
|
||||
if (error.code !== 'FileNotFound') {
|
||||
if (error.code !== "FileNotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -65,7 +67,9 @@ export async function createOrOverwriteFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +103,9 @@ export async function createDirectory(dirPath: string): Promise<void> {
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的目录");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建目录了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +121,7 @@ export async function createDirectory(dirPath: string): Promise<void> {
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 如果目录不存在,继续创建
|
||||
if (error.code !== 'FileNotFound') {
|
||||
if (error.code !== "FileNotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -161,7 +167,9 @@ export async function deleteFile(filePath: string): Promise<void> {
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法删除相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您删除文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +205,9 @@ export async function updateFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,7 +246,9 @@ export async function appendToFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法追加相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您追加文件内容了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,7 +286,9 @@ export async function replaceFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,14 +305,17 @@ export async function replaceFile(
|
||||
|
||||
// 转义特殊字符,将字符串作为字面量处理
|
||||
const escapeRegExp = (str: string) => {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
// 替换内容 - 如果是字符串,先转义特殊字符
|
||||
let newContent: string;
|
||||
if (typeof searchValue === 'string') {
|
||||
if (typeof searchValue === "string") {
|
||||
const escapedSearch = escapeRegExp(searchValue);
|
||||
newContent = fileContent.replace(new RegExp(escapedSearch, "g"), replaceValue);
|
||||
newContent = fileContent.replace(
|
||||
new RegExp(escapedSearch, "g"),
|
||||
replaceValue
|
||||
);
|
||||
} else {
|
||||
newContent = fileContent.replace(searchValue, replaceValue);
|
||||
}
|
||||
@ -330,7 +347,9 @@ export async function insertAtLine(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -382,7 +401,9 @@ export async function renameFile(
|
||||
absoluteNewPath = path.join(workspaceRoot, newPath);
|
||||
}
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法重命名相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您重命名文件了"
|
||||
);
|
||||
}
|
||||
|
||||
const oldUri = vscode.Uri.file(absoluteOldPath);
|
||||
@ -401,10 +422,13 @@ export async function renameFile(
|
||||
throw new Error(`目标文件已存在: ${absoluteNewPath}`);
|
||||
} catch (error: any) {
|
||||
// 如果文件不存在,继续重命名
|
||||
if (error.code !== 'FileNotFound' && !error.message.includes('目标文件已存在')) {
|
||||
if (
|
||||
error.code !== "FileNotFound" &&
|
||||
!error.message.includes("目标文件已存在")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (error.message.includes('目标文件已存在')) {
|
||||
if (error.message.includes("目标文件已存在")) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -413,3 +413,193 @@ export async function checkIverilogAvailable(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 要 dump 的模块定义
|
||||
*/
|
||||
export interface DumpModule {
|
||||
name: string; // 模块名(用于 VCD 文件名和宏名)
|
||||
path: string; // 实例路径(如 dut.u_tx)
|
||||
}
|
||||
|
||||
/**
|
||||
* 多 VCD 生成结果
|
||||
*/
|
||||
export interface MultiVCDResult {
|
||||
success: boolean;
|
||||
vcdFiles: Array<{
|
||||
moduleName: string;
|
||||
vcdPath: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
message: string;
|
||||
stdout?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 testbench 中注入条件编译代码
|
||||
* 将原有的 $dumpfile/$dumpvars 替换为条件编译版本
|
||||
*/
|
||||
function injectConditionalDump(
|
||||
tbContent: string,
|
||||
dumpModules: DumpModule[],
|
||||
vcdDir: string
|
||||
): string {
|
||||
// 匹配 $dumpfile 和 $dumpvars 语句(可能跨多行)
|
||||
const dumpPattern = /(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
|
||||
|
||||
// 生成条件编译代码
|
||||
const conditionalCode = generateConditionalDumpCode(dumpModules, vcdDir);
|
||||
|
||||
// 替换原有的 dump 语句
|
||||
const modified = tbContent.replace(dumpPattern, conditionalCode);
|
||||
|
||||
// 如果没有找到匹配,尝试单独匹配 $dumpfile
|
||||
if (modified === tbContent) {
|
||||
const singleDumpPattern = /\$dumpfile\s*\([^)]+\)\s*;/g;
|
||||
return tbContent.replace(singleDumpPattern, conditionalCode);
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成条件编译的 dump 代码
|
||||
*/
|
||||
function generateConditionalDumpCode(
|
||||
dumpModules: DumpModule[],
|
||||
vcdDir: string
|
||||
): string {
|
||||
if (dumpModules.length === 0) {
|
||||
return '$dumpfile("output.vcd");\n $dumpvars(0, dut);';
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
dumpModules.forEach((module, index) => {
|
||||
const macroName = `DUMP_${module.name.toUpperCase()}`;
|
||||
const vcdPath = `${vcdDir}/${module.name}.vcd`;
|
||||
const directive = index === 0 ? '`ifdef' : '`elsif';
|
||||
|
||||
lines.push(`${directive} ${macroName}`);
|
||||
lines.push(` $dumpfile("${vcdPath}");`);
|
||||
lines.push(` $dumpvars(1, ${module.path});`);
|
||||
});
|
||||
|
||||
// 添加默认分支(使用第一个模块)
|
||||
lines.push('`else');
|
||||
lines.push(` $dumpfile("${vcdDir}/${dumpModules[0].name}.vcd");`);
|
||||
lines.push(` $dumpvars(1, ${dumpModules[0].path});`);
|
||||
lines.push('`endif');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成多个 VCD 文件(为不同子模块)
|
||||
*/
|
||||
export async function generateMultiVCD(
|
||||
projectPath: string,
|
||||
extensionPath: string,
|
||||
tbPath: string,
|
||||
dumpModules: DumpModule[],
|
||||
vcdDir: string = 'vcd'
|
||||
): Promise<MultiVCDResult> {
|
||||
const results: MultiVCDResult['vcdFiles'] = [];
|
||||
let allStdout = '';
|
||||
|
||||
try {
|
||||
// 1. 创建 vcd 目录
|
||||
const vcdDirPath = path.join(projectPath, vcdDir);
|
||||
const vcdDirUri = vscode.Uri.file(vcdDirPath);
|
||||
try {
|
||||
await vscode.workspace.fs.createDirectory(vcdDirUri);
|
||||
} catch {
|
||||
// 目录可能已存在
|
||||
}
|
||||
|
||||
// 2. 读取原始 testbench
|
||||
const tbFullPath = path.isAbsolute(tbPath) ? tbPath : path.join(projectPath, tbPath);
|
||||
const tbUri = vscode.Uri.file(tbFullPath);
|
||||
const tbBytes = await vscode.workspace.fs.readFile(tbUri);
|
||||
const originalTb = Buffer.from(tbBytes).toString('utf-8');
|
||||
|
||||
// 3. 注入条件编译代码
|
||||
const modifiedTb = injectConditionalDump(originalTb, dumpModules, vcdDir);
|
||||
await vscode.workspace.fs.writeFile(tbUri, Buffer.from(modifiedTb, 'utf-8'));
|
||||
|
||||
console.log('[generateMultiVCD] Testbench 已修改,开始多次仿真...');
|
||||
|
||||
// 4. 获取工具路径
|
||||
const iverilogPath = await getIverilogPath(extensionPath);
|
||||
const vvpPath = await getVvpPath(extensionPath);
|
||||
const env = {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
|
||||
};
|
||||
|
||||
// 5. 获取所有 Verilog 文件
|
||||
const projectCheck = await checkVerilogProject(projectPath);
|
||||
const outputFile = path.join(projectPath, "simulation.vvp");
|
||||
|
||||
// 6. 循环执行仿真
|
||||
for (const module of dumpModules) {
|
||||
const macroName = `DUMP_${module.name.toUpperCase()}`;
|
||||
const vcdPath = path.join(vcdDirPath, `${module.name}.vcd`);
|
||||
|
||||
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
|
||||
|
||||
try {
|
||||
// 编译(带宏定义)
|
||||
const compileArgs = [
|
||||
`-D${macroName}`,
|
||||
"-o", outputFile,
|
||||
...projectCheck.allVerilogFiles
|
||||
];
|
||||
await execCommand(iverilogPath, compileArgs, { cwd: projectPath, env });
|
||||
|
||||
// 仿真
|
||||
const simResult = await execCommand(vvpPath, [outputFile], { cwd: projectPath, env });
|
||||
allStdout += `\n[${module.name}] ${simResult.stdout}`;
|
||||
|
||||
results.push({
|
||||
moduleName: module.name,
|
||||
vcdPath: vcdPath,
|
||||
success: true
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(`[generateMultiVCD] 模块 ${module.name} 仿真失败:`, error.message);
|
||||
results.push({
|
||||
moduleName: module.name,
|
||||
vcdPath: vcdPath,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
// 继续执行其他模块
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 清理中间文件
|
||||
try {
|
||||
await vscode.workspace.fs.delete(vscode.Uri.file(outputFile));
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
return {
|
||||
success: successCount > 0,
|
||||
vcdFiles: results,
|
||||
message: `生成完成:${successCount}/${dumpModules.length} 个 VCD 文件成功`,
|
||||
stdout: allStdout
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
vcdFiles: results,
|
||||
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
101
src/utils/jwtUtils.ts
Normal file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* JWT 工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* JWT Payload 接口
|
||||
*/
|
||||
export interface JwtPayload {
|
||||
sub?: string; // subject (通常是 userId)
|
||||
userId?: number; // 用户ID (驼峰命名)
|
||||
user_id?: number; // 用户ID (下划线命名)
|
||||
exp?: number; // 过期时间
|
||||
iat?: number; // 签发时间
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JWT token 的 payload
|
||||
* @param token JWT token
|
||||
* @returns 解析后的 payload,解析失败返回 null
|
||||
*/
|
||||
export function parseJwtPayload(token: string): JwtPayload | null {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
console.warn('[JWT] token 格式不正确,期望3部分,实际:', parts.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
// payload 是第二部分,base64url 编码
|
||||
const payload = parts[1];
|
||||
|
||||
// base64url 转 base64
|
||||
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
// 解码
|
||||
const jsonStr = Buffer.from(base64, 'base64').toString('utf-8');
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
|
||||
console.log('[JWT] 解析成功, payload 字段:', Object.keys(parsed));
|
||||
console.log('[JWT] payload 内容:', JSON.stringify(parsed));
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error('[JWT] 解析失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JWT token 中获取用户ID
|
||||
* @param token JWT token
|
||||
* @returns 用户ID字符串,获取失败返回 null
|
||||
*/
|
||||
export function getUserIdFromToken(token: string): string | null {
|
||||
const payload = parseJwtPayload(token);
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 支持多种字段名:user_id, userId, sub
|
||||
if (payload.user_id !== undefined) {
|
||||
return String(payload.user_id);
|
||||
}
|
||||
if (payload.userId !== undefined) {
|
||||
return String(payload.userId);
|
||||
}
|
||||
if (payload.sub !== undefined) {
|
||||
return String(payload.sub);
|
||||
}
|
||||
|
||||
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测 JWT token 是否已过期
|
||||
* @param token JWT token
|
||||
* @param bufferSeconds 提前多少秒判定为过期(默认60秒)
|
||||
* @returns true 表示已过期,false 表示未过期,null 表示无法判断
|
||||
*/
|
||||
export function isTokenExpired(token: string, bufferSeconds: number = 60): boolean | null {
|
||||
const payload = parseJwtPayload(token);
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.exp === undefined) {
|
||||
console.warn('[JWT] payload 中没有 exp 字段,无法判断过期');
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const expTime = payload.exp - bufferSeconds;
|
||||
const isExpired = now >= expTime;
|
||||
|
||||
if (isExpired) {
|
||||
console.warn('[JWT] token 已过期,exp:', payload.exp, '当前:', now);
|
||||
}
|
||||
|
||||
return isExpired;
|
||||
}
|
||||
@ -18,6 +18,13 @@ import { ChatHistoryManager } from "./chatHistoryManager";
|
||||
import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
import { healthCheck } from "../services/apiClient";
|
||||
import {
|
||||
checkBalanceBeforeSend,
|
||||
fetchBalance,
|
||||
} from "../services/creditsService";
|
||||
import { optimizePrompt } from "../services/promptOptimizeService";
|
||||
|
||||
import type { RunMode, ServiceTier } from "../types/api";
|
||||
|
||||
/** 是否使用后端服务(可通过配置控制) */
|
||||
let useBackendService = true;
|
||||
@ -25,13 +32,18 @@ let useBackendService = true;
|
||||
/** 当前对话会话 */
|
||||
let currentSession: DialogSession | null = null;
|
||||
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
/**
|
||||
* 处理用户消息
|
||||
*/
|
||||
export async function handleUserMessage(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string,
|
||||
extensionPath?: string
|
||||
extensionPath?: string,
|
||||
mode?: RunMode,
|
||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||
) {
|
||||
console.log("收到用户消息:", text);
|
||||
|
||||
@ -60,35 +72,64 @@ export async function handleUserMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送前检测余额
|
||||
const balanceCheck = await checkBalanceBeforeSend();
|
||||
if (!balanceCheck.allowed) {
|
||||
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
|
||||
// 显示错误提示
|
||||
const selection = await vscode.window.showWarningMessage(
|
||||
balanceCheck.message || "资源点余额不足",
|
||||
"去充值"
|
||||
);
|
||||
if (selection === "去充值") {
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse("https://iccoder.com/memberCenter")
|
||||
);
|
||||
}
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试使用后端服务
|
||||
if (useBackendService && extensionPath) {
|
||||
try {
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath);
|
||||
await handleUserMessageWithBackend(
|
||||
panel,
|
||||
text,
|
||||
extensionPath,
|
||||
mode,
|
||||
undefined,
|
||||
serviceTier
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("后端服务不可用,回退到本地模式:", error);
|
||||
// 后端不可用时,使用本地模拟回复
|
||||
}
|
||||
}
|
||||
|
||||
// 本地模拟回复(后端不可用时的 fallback)
|
||||
console.log("使用本地模拟回复");
|
||||
const reply = getMockReply(text);
|
||||
|
||||
// 记录AI回复到历史(允许失败)
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
await historyManager.addAiMessage(reply);
|
||||
} catch (error) {
|
||||
console.warn("记录AI回复历史失败:", error);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
console.error("后端服务不可用:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: reply,
|
||||
command: "updateStatus",
|
||||
text: "后端服务不可用",
|
||||
type: "error",
|
||||
});
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有 extensionPath,显示错误
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "无法处理消息:缺少必要参数",
|
||||
type: "error",
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,15 +138,31 @@ export async function handleUserMessage(
|
||||
async function handleUserMessageWithBackend(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
mode?: RunMode,
|
||||
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||
): Promise<void> {
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(extensionPath);
|
||||
}
|
||||
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
|
||||
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
||||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||
|
||||
// 创建会话(dialogManager 会自动处理旧会话的中止)
|
||||
currentSession = dialogManager.createSession(
|
||||
extensionPath,
|
||||
taskIdToUse || undefined
|
||||
);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
console.log(
|
||||
"[MessageHandler] 创建会话: taskId=",
|
||||
lastTaskId,
|
||||
"来源=",
|
||||
taskIdToUse ? "historyManager" : "新生成"
|
||||
);
|
||||
|
||||
// 显示状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
@ -114,7 +171,9 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
currentSession!.sendMessage(text, {
|
||||
currentSession!.sendMessage(
|
||||
text,
|
||||
{
|
||||
onText: (fullText, isStreaming) => {
|
||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||
},
|
||||
@ -160,24 +219,34 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
|
||||
// 最后一次发送完整的段落
|
||||
console.log('[MessageHandler] 对话完成, 段落数:', segments.length);
|
||||
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments));
|
||||
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
||||
|
||||
// 对话完成后重新获取余额(因为已经消耗了 Credits)
|
||||
try {
|
||||
console.log("[MessageHandler] 对话完成,重新获取余额...");
|
||||
const newBalance = await fetchBalance();
|
||||
if (newBalance !== null) {
|
||||
console.log("[MessageHandler] 余额已更新:", newBalance);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[MessageHandler] 获取余额失败:", error);
|
||||
}
|
||||
|
||||
const result = await panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
isComplete: true,
|
||||
});
|
||||
console.log('[MessageHandler] postMessage 返回值:', result);
|
||||
console.log("[MessageHandler] postMessage 返回值:", result);
|
||||
|
||||
// 保存完整的 segments 到历史记录
|
||||
try {
|
||||
// 将完整的 segments 保存到一条 AI 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
.map(s => s.content)
|
||||
.join('\n');
|
||||
.filter((s) => s.type === "text" && s.content)
|
||||
.map((s) => s.content)
|
||||
.join("\n");
|
||||
|
||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||
} catch (error) {
|
||||
@ -195,13 +264,62 @@ async function handleUserMessageWithBackend(
|
||||
command: "receiveMessage",
|
||||
text: `❌ 错误: ${message}`,
|
||||
});
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
reject(new Error(message));
|
||||
},
|
||||
|
||||
onNotification: (message) => {
|
||||
vscode.window.showInformationMessage(message);
|
||||
},
|
||||
|
||||
onContextUsage: (data) => {
|
||||
// 发送上下文使用量到 WebView
|
||||
panel.webview.postMessage({
|
||||
command: "contextUsage",
|
||||
currentTokens: data.currentTokens,
|
||||
maxTokens: data.maxTokens,
|
||||
percentage: data.percentage,
|
||||
});
|
||||
},
|
||||
|
||||
onPhaseProgress: (phaseId, status) => {
|
||||
// 发送阶段进度更新到 WebView
|
||||
// 映射 phaseId: sim -> simulation
|
||||
const stepMap: Record<string, string> = {
|
||||
spec: "spec",
|
||||
design: "design",
|
||||
sim: "simulation",
|
||||
done: "done",
|
||||
};
|
||||
const step = stepMap[phaseId] || phaseId;
|
||||
|
||||
if (status === "current") {
|
||||
// 显示进度条并更新到当前步骤
|
||||
panel.webview.postMessage({ type: "showProgress" });
|
||||
panel.webview.postMessage({ type: "updateProgress", step });
|
||||
} else if (status === "completed") {
|
||||
// 更新到下一步(或完成)
|
||||
const steps = ["spec", "design", "simulation", "done"];
|
||||
const currentIndex = steps.indexOf(step);
|
||||
if (currentIndex < steps.length - 1) {
|
||||
panel.webview.postMessage({
|
||||
type: "updateProgress",
|
||||
step: steps[currentIndex + 1],
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({ type: "completeProgress" });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mode,
|
||||
serviceTier // 传递服务等级
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -221,11 +339,126 @@ export async function handleUserAnswer(
|
||||
/**
|
||||
* 中止当前对话
|
||||
*/
|
||||
export function abortCurrentDialog(): void {
|
||||
export async function abortCurrentDialog(): Promise<void> {
|
||||
if (currentSession) {
|
||||
// 保存当前已有的对话内容
|
||||
const segments = currentSession.getSegments();
|
||||
if (segments && segments.length > 0) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const textContent = segments
|
||||
.filter((s) => s.type === "text" && s.content)
|
||||
.map((s) => s.content)
|
||||
.join("\n");
|
||||
|
||||
// 添加中止标记
|
||||
const abortedContent = textContent + "\n\n[对话已被用户中止]";
|
||||
await historyManager.addAiMessage(abortedContent, undefined, segments);
|
||||
console.log("[MessageHandler] 已保存中止前的对话内容");
|
||||
} catch (error) {
|
||||
console.warn("[MessageHandler] 保存中止对话失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知 WebView 重置分段消息容器
|
||||
const panel = userInteractionManager.getWebviewPanel();
|
||||
if (panel) {
|
||||
panel.webview.postMessage({ command: "resetSegmentedMessage" });
|
||||
console.log("[MessageHandler] 已发送重置分段消息命令");
|
||||
}
|
||||
|
||||
dialogManager.abortCurrentSession();
|
||||
currentSession = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话的 taskId
|
||||
*/
|
||||
export function getCurrentTaskId(): string | null {
|
||||
return currentSession?.getTaskId() || lastTaskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最后的 taskId(加载历史会话时调用)
|
||||
*/
|
||||
export function setLastTaskId(taskId: string): void {
|
||||
lastTaskId = taskId;
|
||||
console.log("[MessageHandler] 设置 lastTaskId:", taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理计划操作(Plan 模式)
|
||||
* @param panel WebView 面板
|
||||
* @param action 操作类型:confirm/modify/cancel
|
||||
* @param planTitle 计划标题
|
||||
* @param extensionPath 扩展路径
|
||||
*/
|
||||
export async function handlePlanAction(
|
||||
panel: vscode.WebviewPanel,
|
||||
action: string,
|
||||
planTitle: string,
|
||||
extensionPath: string,
|
||||
serviceTier?: ServiceTier
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
"[handlePlanAction] action:",
|
||||
action,
|
||||
"planTitle:",
|
||||
planTitle,
|
||||
"serviceTier:",
|
||||
serviceTier
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
case "confirm":
|
||||
// 确认执行:切换到 Agent 模式并发送执行消息
|
||||
panel.webview.postMessage({
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 发送执行消息
|
||||
await handleUserMessage(
|
||||
panel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
extensionPath,
|
||||
"agent",
|
||||
serviceTier
|
||||
);
|
||||
break;
|
||||
|
||||
case "modify":
|
||||
// 修改计划:提示用户输入修改建议
|
||||
const modification = await vscode.window.showInputBox({
|
||||
prompt: "请输入您对计划的修改建议",
|
||||
placeHolder: "例如:第2步需要先检查文件是否存在...",
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (modification) {
|
||||
await handleUserMessage(
|
||||
panel,
|
||||
`请根据以下建议修改计划:${modification}`,
|
||||
extensionPath,
|
||||
"plan",
|
||||
serviceTier
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "cancel":
|
||||
// 取消计划:通知用户
|
||||
panel.webview.postMessage({
|
||||
command: "addMessage",
|
||||
text: "计划已取消。",
|
||||
sender: "bot",
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("[handlePlanAction] 未知操作:", action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件操作命令
|
||||
*/
|
||||
@ -261,7 +494,9 @@ function parseFileOperation(text: string): {
|
||||
}
|
||||
|
||||
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
||||
const renameMatch = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/);
|
||||
const renameMatch = lowerText.match(
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
|
||||
);
|
||||
if (renameMatch) {
|
||||
const oldPath = renameMatch[1].trim();
|
||||
const newPath = renameMatch[2].trim();
|
||||
@ -276,7 +511,9 @@ function parseFileOperation(text: string): {
|
||||
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
|
||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
||||
const replaceMatch1 = lowerText.match(/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
|
||||
const replaceMatch1 = lowerText.match(
|
||||
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||
);
|
||||
if (replaceMatch1) {
|
||||
const filePath = replaceMatch1[1].trim();
|
||||
const searchText = replaceMatch1[2].trim();
|
||||
@ -290,7 +527,9 @@ function parseFileOperation(text: string): {
|
||||
}
|
||||
|
||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||
const replaceMatch2 = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
|
||||
const replaceMatch2 = lowerText.match(
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||
);
|
||||
if (replaceMatch2) {
|
||||
const filePath = replaceMatch2[1].trim();
|
||||
const searchText = replaceMatch2[2].trim();
|
||||
@ -618,41 +857,6 @@ export async function handleReplaceInFile(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模拟回复
|
||||
*/
|
||||
function getMockReply(question: string): string {
|
||||
const replies = [
|
||||
`已收到您的问题:"${question}"
|
||||
|
||||
这是一个演示版本,实际需要连接AI服务。
|
||||
|
||||
示例回复:这是一个计数器模板:
|
||||
\`\`\`verilog
|
||||
module counter (
|
||||
input clk,
|
||||
input rst_n,
|
||||
output reg [3:0] count
|
||||
);
|
||||
always @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) count <= 0;
|
||||
else count <= count + 1;
|
||||
end
|
||||
endmodule
|
||||
\`\`\``,
|
||||
|
||||
`感谢提问!关于"${question}",在真实版本中我会:
|
||||
1. 分析您的代码上下文
|
||||
2. 提供优化建议
|
||||
3. 生成完整代码
|
||||
4. 解释设计原理
|
||||
|
||||
当前是演示版,请点击侧边栏按钮快速生成代码。`,
|
||||
];
|
||||
|
||||
return replies[Math.floor(Math.random() * replies.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将代码插入到编辑器
|
||||
*/
|
||||
@ -745,7 +949,8 @@ async function handleVCDGeneration(
|
||||
|
||||
if (!projectCheck.hasTestbench) {
|
||||
errorMsg += "• ❌ 缺少 testbench 文件\n";
|
||||
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||||
errorMsg +=
|
||||
"\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||||
} else {
|
||||
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
|
||||
}
|
||||
@ -789,9 +994,7 @@ async function handleVCDGeneration(
|
||||
fileName: fileName,
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`VCD 文件生成成功: ${fileName}`
|
||||
);
|
||||
vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
@ -829,3 +1032,35 @@ async function handleVCDGeneration(
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提示词优化请求
|
||||
*/
|
||||
export async function handleOptimizePrompt(
|
||||
panel: vscode.WebviewPanel,
|
||||
prompt: string
|
||||
): Promise<void> {
|
||||
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
|
||||
console.log("[MessageHandler] prompt:", prompt);
|
||||
console.log("[MessageHandler] prompt 长度:", prompt?.length);
|
||||
|
||||
try {
|
||||
console.log("[MessageHandler] 开始调用 optimizePrompt...");
|
||||
const optimized = await optimizePrompt(prompt);
|
||||
console.log("[MessageHandler] 优化成功,结果:", optimized);
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: true,
|
||||
optimizedPrompt: optimized,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : "优化失败";
|
||||
console.error("[MessageHandler] 提示词优化失败:", errorMsg);
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: errorMsg,
|
||||
});
|
||||
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +94,13 @@ export async function readDirectory(
|
||||
const results = [];
|
||||
|
||||
for (const [fileName, fileType] of entries) {
|
||||
// 处理目录
|
||||
if (fileType === vscode.FileType.Directory) {
|
||||
results.push({ path: fileName + '/', content: '[目录]', isDirectory: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理文件
|
||||
if (fileType === vscode.FileType.File) {
|
||||
// 如果指定了扩展名过滤
|
||||
if (extensions && extensions.length > 0) {
|
||||
@ -108,7 +115,7 @@ export async function readDirectory(
|
||||
const fileUri = vscode.Uri.file(filePath);
|
||||
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
|
||||
const content = Buffer.from(contentBytes).toString("utf-8");
|
||||
results.push({ path: fileName, content });
|
||||
results.push({ path: fileName, content, isDirectory: false });
|
||||
} catch (error) {
|
||||
// 跳过无法读取的文件
|
||||
continue;
|
||||
|
||||
467
src/utils/vcdParser.ts
Normal file
@ -0,0 +1,467 @@
|
||||
/**
|
||||
* VCD (Value Change Dump) 解析器
|
||||
* 纯 TypeScript 实现,参照 VerilogCoder 项目格式
|
||||
*
|
||||
* @deprecated 当前未使用,保留备用
|
||||
* 目前使用 waveformTracer.ts 调用 Python 打包的 waveform_trace.exe
|
||||
* 未来可能用此文件替换 Python 实现
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/** 信号定义 */
|
||||
export interface VcdSignal {
|
||||
name: string; // 完整路径名,如 "tb.top_module.data"
|
||||
shortName: string; // 短名,如 "data"
|
||||
symbolId: string; // VCD 符号 ID,如 "!", "#"
|
||||
width: number; // 位宽
|
||||
varType: string; // 变量类型:wire, reg
|
||||
module: string; // 所属模块
|
||||
}
|
||||
|
||||
/** 时间-值对 */
|
||||
export interface TimeValue {
|
||||
time: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** 信号波形数据 */
|
||||
export interface SignalWaveform {
|
||||
signal: VcdSignal;
|
||||
changes: TimeValue[];
|
||||
}
|
||||
|
||||
/** VCD 解析结果 */
|
||||
export interface VcdData {
|
||||
date?: string;
|
||||
version?: string;
|
||||
timescale: string;
|
||||
endTime: number;
|
||||
signals: Map<string, VcdSignal>; // symbolId -> signal
|
||||
waveforms: Map<string, TimeValue[]>; // symbolId -> changes
|
||||
}
|
||||
|
||||
/** Mismatch 信息 */
|
||||
export interface MismatchInfo {
|
||||
time: number;
|
||||
signal: string;
|
||||
dutValue: string;
|
||||
refValue: string;
|
||||
}
|
||||
|
||||
// ==================== VCD 解析器 ====================
|
||||
|
||||
export class VcdParser {
|
||||
private signals: Map<string, VcdSignal> = new Map();
|
||||
private waveforms: Map<string, TimeValue[]> = new Map();
|
||||
private scopeStack: string[] = [];
|
||||
private timescale: string = '1ns';
|
||||
private currentTime: number = 0;
|
||||
private endTime: number = 0;
|
||||
private date?: string;
|
||||
private version?: string;
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件
|
||||
*/
|
||||
parse(filePath: string): VcdData {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return this.parseContent(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 内容
|
||||
*/
|
||||
parseContent(content: string): VcdData {
|
||||
// 预处理:将多行指令合并成单行
|
||||
const normalizedContent = this.normalizeVcdContent(content);
|
||||
const lines = normalizedContent.split('\n');
|
||||
let inDefinitions = true;
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
|
||||
if (inDefinitions) {
|
||||
// 解析定义区
|
||||
if (line.startsWith('$enddefinitions')) {
|
||||
inDefinitions = false;
|
||||
continue;
|
||||
}
|
||||
this.parseDefinition(line);
|
||||
} else {
|
||||
// 解析数据区
|
||||
this.parseValueChange(line);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
date: this.date,
|
||||
version: this.version,
|
||||
timescale: this.timescale,
|
||||
endTime: this.endTime,
|
||||
signals: this.signals,
|
||||
waveforms: this.waveforms
|
||||
};
|
||||
}
|
||||
|
||||
private parseDefinition(line: string): void {
|
||||
if (line.startsWith('$date')) {
|
||||
this.date = this.extractValue(line);
|
||||
} else if (line.startsWith('$version')) {
|
||||
this.version = this.extractValue(line);
|
||||
} else if (line.startsWith('$timescale')) {
|
||||
this.timescale = this.extractValue(line) || '1ns';
|
||||
} else if (line.startsWith('$scope')) {
|
||||
const match = line.match(/\$scope\s+\w+\s+(\S+)/);
|
||||
if (match) {
|
||||
this.scopeStack.push(match[1]);
|
||||
}
|
||||
} else if (line.startsWith('$upscope')) {
|
||||
this.scopeStack.pop();
|
||||
} else if (line.startsWith('$var')) {
|
||||
this.parseVariable(line);
|
||||
}
|
||||
}
|
||||
|
||||
private parseVariable(line: string): void {
|
||||
// $var wire 8 # data [7:0] $end
|
||||
// $var reg 1 ! clk $end
|
||||
const match = line.match(/\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+(\S+)/);
|
||||
if (!match) return;
|
||||
|
||||
const [, varType, widthStr, symbolId, name] = match;
|
||||
const width = parseInt(widthStr, 10);
|
||||
const module = this.scopeStack.join('.');
|
||||
const fullName = module ? `${module}.${name}` : name;
|
||||
|
||||
const signal: VcdSignal = {
|
||||
name: fullName,
|
||||
shortName: name.replace(/\[\d+:\d+\]/, ''), // 移除位宽标注
|
||||
symbolId,
|
||||
width,
|
||||
varType,
|
||||
module
|
||||
};
|
||||
|
||||
this.signals.set(symbolId, signal);
|
||||
this.waveforms.set(symbolId, []);
|
||||
}
|
||||
|
||||
private parseValueChange(line: string): void {
|
||||
if (line.startsWith('#')) {
|
||||
// 时间戳: #100
|
||||
this.currentTime = parseInt(line.substring(1), 10);
|
||||
this.endTime = Math.max(this.endTime, this.currentTime);
|
||||
} else if (line.startsWith('b') || line.startsWith('B')) {
|
||||
// 多位值: b10101010 #
|
||||
const spaceIdx = line.indexOf(' ');
|
||||
if (spaceIdx > 0) {
|
||||
const value = line.substring(1, spaceIdx);
|
||||
const symbolId = line.substring(spaceIdx + 1).trim();
|
||||
this.addChange(symbolId, value);
|
||||
}
|
||||
} else if (line.length >= 2 && !line.startsWith('$')) {
|
||||
// 单位值: 0! 或 1# 或 x$
|
||||
const value = line[0];
|
||||
const symbolId = line.substring(1).trim();
|
||||
if (symbolId && this.signals.has(symbolId)) {
|
||||
this.addChange(symbolId, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addChange(symbolId: string, value: string): void {
|
||||
const changes = this.waveforms.get(symbolId);
|
||||
if (changes) {
|
||||
changes.push({ time: this.currentTime, value });
|
||||
}
|
||||
}
|
||||
|
||||
private extractValue(line: string): string {
|
||||
// 提取 $xxx value $end 中的 value
|
||||
const match = line.match(/\$\w+\s+(.+?)\s*\$end/);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 预处理 VCD 内容,将多行指令合并成单行
|
||||
*/
|
||||
private normalizeVcdContent(content: string): string {
|
||||
// 将多行 $xxx ... $end 合并成单行
|
||||
return content.replace(/(\$\w+)\s*\n\s*([^\$]+?)\s*\n\s*(\$end)/g, '$1 $2 $3');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 波形分析工具 ====================
|
||||
|
||||
/**
|
||||
* 二进制字符串转十六进制
|
||||
*/
|
||||
export function binaryToHex(binary: string): string {
|
||||
if (binary === 'x' || binary === 'X' || binary.includes('x')) {
|
||||
return 'xx';
|
||||
}
|
||||
if (binary === 'z' || binary === 'Z' || binary.includes('z')) {
|
||||
return 'zz';
|
||||
}
|
||||
if (binary.length <= 1) {
|
||||
return binary;
|
||||
}
|
||||
// 补齐到 4 的倍数
|
||||
const padded = binary.padStart(Math.ceil(binary.length / 4) * 4, '0');
|
||||
let hex = '';
|
||||
for (let i = 0; i < padded.length; i += 4) {
|
||||
hex += parseInt(padded.substring(i, i + 4), 2).toString(16);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取信号在指定时间的值
|
||||
*/
|
||||
export function getValueAtTime(
|
||||
changes: TimeValue[],
|
||||
time: number
|
||||
): string {
|
||||
let value = 'x';
|
||||
for (const change of changes) {
|
||||
if (change.time <= time) {
|
||||
value = change.value;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找 DUT 和 REF 信号的第一个 mismatch
|
||||
*/
|
||||
export function findFirstMismatch(
|
||||
vcdData: VcdData,
|
||||
dutSignals: string[],
|
||||
refSignals: string[]
|
||||
): MismatchInfo | null {
|
||||
// 收集所有时间点
|
||||
const allTimes = new Set<number>();
|
||||
for (const changes of vcdData.waveforms.values()) {
|
||||
for (const c of changes) {
|
||||
allTimes.add(c.time);
|
||||
}
|
||||
}
|
||||
const sortedTimes = Array.from(allTimes).sort((a, b) => a - b);
|
||||
|
||||
// 按信号名匹配 DUT 和 REF
|
||||
for (const time of sortedTimes) {
|
||||
for (let i = 0; i < dutSignals.length; i++) {
|
||||
const dutSig = findSignalByName(vcdData, dutSignals[i]);
|
||||
const refSig = findSignalByName(vcdData, refSignals[i]);
|
||||
|
||||
if (!dutSig || !refSig) continue;
|
||||
|
||||
const dutChanges = vcdData.waveforms.get(dutSig.symbolId) || [];
|
||||
const refChanges = vcdData.waveforms.get(refSig.symbolId) || [];
|
||||
|
||||
const dutVal = getValueAtTime(dutChanges, time);
|
||||
const refVal = getValueAtTime(refChanges, time);
|
||||
|
||||
// 跳过未知值
|
||||
if (dutVal.includes('x') || refVal.includes('x')) continue;
|
||||
|
||||
if (dutVal !== refVal) {
|
||||
return {
|
||||
time,
|
||||
signal: dutSig.shortName,
|
||||
dutValue: binaryToHex(dutVal),
|
||||
refValue: binaryToHex(refVal)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称查找信号
|
||||
*/
|
||||
function findSignalByName(vcdData: VcdData, name: string): VcdSignal | null {
|
||||
for (const signal of vcdData.signals.values()) {
|
||||
if (signal.name.endsWith(name) || signal.shortName === name) {
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成波形表格(参照 VerilogCoder 格式)
|
||||
*/
|
||||
export function generateWaveformTable(
|
||||
vcdData: VcdData,
|
||||
signalNames: string[],
|
||||
startTime: number = 0,
|
||||
endTime?: number,
|
||||
windowSize: number = 20
|
||||
): string {
|
||||
const actualEndTime = endTime ?? vcdData.endTime;
|
||||
|
||||
// 查找信号
|
||||
const signals: VcdSignal[] = [];
|
||||
for (const name of signalNames) {
|
||||
const sig = findSignalByName(vcdData, name);
|
||||
if (sig) signals.push(sig);
|
||||
}
|
||||
|
||||
if (signals.length === 0) {
|
||||
return '未找到指定信号';
|
||||
}
|
||||
|
||||
// 收集时间点
|
||||
const times = new Set<number>();
|
||||
for (const sig of signals) {
|
||||
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||
for (const c of changes) {
|
||||
if (c.time >= startTime && c.time <= actualEndTime) {
|
||||
times.add(c.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sortedTimes = Array.from(times).sort((a, b) => a - b);
|
||||
if (sortedTimes.length > windowSize) {
|
||||
sortedTimes = sortedTimes.slice(0, windowSize);
|
||||
}
|
||||
|
||||
// 生成表头
|
||||
const headers = ['time(ns)', ...signals.map(s => s.shortName)];
|
||||
const colWidths = headers.map(h => Math.max(h.length, 8));
|
||||
|
||||
let table = '### Waveform Trace ###\n';
|
||||
table += headers.map((h, i) => h.padEnd(colWidths[i])).join(' ') + '\n';
|
||||
table += colWidths.map(w => '─'.repeat(w)).join('──') + '\n';
|
||||
|
||||
// 生成数据行
|
||||
for (const time of sortedTimes) {
|
||||
const row = [time.toString()];
|
||||
for (const sig of signals) {
|
||||
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||
const val = getValueAtTime(changes, time);
|
||||
row.push(binaryToHex(val));
|
||||
}
|
||||
table += row.map((v, i) => v.padEnd(colWidths[i])).join(' ') + '\n';
|
||||
}
|
||||
|
||||
table += '### Waveform Trace End ###\n';
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取信号显示名称(模块.信号名[位宽])
|
||||
*/
|
||||
function getSignalDisplayName(sig: VcdSignal): string {
|
||||
const moduleParts = sig.module.split('.');
|
||||
const moduleShort = moduleParts[moduleParts.length - 1] || '';
|
||||
const bitInfo = sig.width > 1 ? `[${sig.width - 1}:0]` : '';
|
||||
return moduleShort ? `${moduleShort}.${sig.shortName}${bitInfo}` : `${sig.shortName}${bitInfo}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成变化日志格式(只记录信号变化)
|
||||
*/
|
||||
export function generateChangeLog(vcdData: VcdData): string {
|
||||
// 筛选信号(排除 parameter)
|
||||
const signals: VcdSignal[] = [];
|
||||
for (const sig of vcdData.signals.values()) {
|
||||
if (sig.varType !== 'parameter') {
|
||||
signals.push(sig);
|
||||
}
|
||||
}
|
||||
|
||||
// 收集所有时间点
|
||||
const times = new Set<number>();
|
||||
for (const sig of signals) {
|
||||
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||
for (const c of changes) {
|
||||
times.add(c.time);
|
||||
}
|
||||
}
|
||||
const sortedTimes = Array.from(times).sort((a, b) => a - b);
|
||||
|
||||
// 记录每个信号的上一个值
|
||||
const lastValues = new Map<string, string | null>();
|
||||
for (const sig of signals) {
|
||||
lastValues.set(sig.symbolId, null);
|
||||
}
|
||||
|
||||
let log = '';
|
||||
|
||||
for (const time of sortedTimes) {
|
||||
const changes: string[] = [];
|
||||
|
||||
for (const sig of signals) {
|
||||
const waveform = vcdData.waveforms.get(sig.symbolId) || [];
|
||||
const currentVal = binaryToHex(getValueAtTime(waveform, time));
|
||||
const lastVal = lastValues.get(sig.symbolId);
|
||||
|
||||
if (lastVal === null) {
|
||||
changes.push(`${getSignalDisplayName(sig)}=${currentVal}`);
|
||||
} else if (currentVal !== lastVal) {
|
||||
changes.push(`${getSignalDisplayName(sig)} ${lastVal}→${currentVal}`);
|
||||
}
|
||||
|
||||
lastValues.set(sig.symbolId, currentVal);
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
log += `#${time}: ${changes.join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析 VCD 文件(主入口)
|
||||
*/
|
||||
export function analyzeVcdFile(
|
||||
filePath: string,
|
||||
signalFilter?: string,
|
||||
checkpoint?: number
|
||||
): string {
|
||||
// 解析 VCD
|
||||
const parser = new VcdParser();
|
||||
const vcdData = parser.parse(filePath);
|
||||
|
||||
// 解析信号过滤器
|
||||
const signalNames = signalFilter
|
||||
? signalFilter.split(',').map(s => s.trim())
|
||||
: Array.from(vcdData.signals.values()).map(s => s.shortName);
|
||||
|
||||
// 生成摘要
|
||||
let result = `=== VCD 波形分析 ===\n`;
|
||||
result += `文件: ${path.basename(filePath)}\n`;
|
||||
result += `时间单位: ${vcdData.timescale}\n`;
|
||||
result += `仿真时长: 0 - ${vcdData.endTime}${vcdData.timescale}\n\n`;
|
||||
|
||||
// 信号列表
|
||||
result += `--- 信号列表 (${vcdData.signals.size} 个) ---\n`;
|
||||
let idx = 1;
|
||||
for (const sig of vcdData.signals.values()) {
|
||||
if (idx <= 10) {
|
||||
result += `${idx}. ${sig.shortName} (${sig.width}-bit, ${sig.varType})\n`;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
if (vcdData.signals.size > 10) {
|
||||
result += `... 还有 ${vcdData.signals.size - 10} 个信号\n`;
|
||||
}
|
||||
result += '\n';
|
||||
|
||||
// 变化日志
|
||||
result += generateChangeLog(vcdData);
|
||||
|
||||
return result;
|
||||
}
|
||||
145
src/utils/waveformTracer.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 波形追踪工具
|
||||
* 调用 PyInstaller 打包的 waveform_trace 可执行文件
|
||||
*/
|
||||
import { spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* 波形追踪参数
|
||||
*/
|
||||
export interface WaveformTraceArgs {
|
||||
/** Verilog 源文件路径(相对于项目根目录) */
|
||||
verilogPath: string;
|
||||
/** VCD 波形文件路径(相对于项目根目录) */
|
||||
vcdPath: string;
|
||||
/** 仿真工具的输出字符串(包含 mismatch 信息) */
|
||||
simOutput: string;
|
||||
/** BFS 回溯层数,默认 2 */
|
||||
traceLevel?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行波形追踪
|
||||
* @param args 追踪参数
|
||||
* @param context 执行上下文
|
||||
* @returns 追踪结果字符串
|
||||
*/
|
||||
export async function executeWaveformTrace(
|
||||
args: WaveformTraceArgs,
|
||||
context: { extensionPath: string }
|
||||
): Promise<string> {
|
||||
// 获取可执行文件路径
|
||||
const tracerPath = getWaveformTracerPath(context.extensionPath);
|
||||
|
||||
// 检查可执行文件是否存在
|
||||
if (!fs.existsSync(tracerPath)) {
|
||||
throw new Error(
|
||||
`waveform_trace 工具未安装: ${tracerPath}\n` +
|
||||
'请确保插件包含 tools/waveform_trace/bin/ 目录'
|
||||
);
|
||||
}
|
||||
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 解析路径(支持相对路径)
|
||||
const verilogAbsPath = path.isAbsolute(args.verilogPath)
|
||||
? args.verilogPath
|
||||
: path.join(workspacePath, args.verilogPath);
|
||||
const vcdAbsPath = path.isAbsolute(args.vcdPath)
|
||||
? args.vcdPath
|
||||
: path.join(workspacePath, args.vcdPath);
|
||||
|
||||
// 验证文件存在
|
||||
if (!fs.existsSync(verilogAbsPath)) {
|
||||
throw new Error(`Verilog 文件不存在: ${args.verilogPath}`);
|
||||
}
|
||||
if (!fs.existsSync(vcdAbsPath)) {
|
||||
throw new Error(`VCD 文件不存在: ${args.vcdPath}`);
|
||||
}
|
||||
|
||||
// 调用可执行文件
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(tracerPath, [
|
||||
'--verilog', verilogAbsPath,
|
||||
'--vcd', vcdAbsPath,
|
||||
'--sim-output', args.simOutput,
|
||||
'--trace-level', String(args.traceLevel || 2),
|
||||
'--output-format', 'text'
|
||||
], {
|
||||
windowsHide: true,
|
||||
cwd: workspacePath,
|
||||
shell: false
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
reject(new Error(
|
||||
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
reject(new Error(`waveform_trace 启动失败: ${error.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 waveform_trace 可执行文件路径
|
||||
*/
|
||||
function getWaveformTracerPath(extensionPath: string): string {
|
||||
const platform = process.platform;
|
||||
let binName = 'waveform_trace';
|
||||
|
||||
if (platform === 'win32') {
|
||||
binName = 'waveform_trace.exe';
|
||||
}
|
||||
|
||||
return path.join(extensionPath, 'tools', 'waveform_trace', 'bin', binName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 waveform_trace 工具是否可用
|
||||
*/
|
||||
export function checkWaveformTraceAvailable(extensionPath: string): {
|
||||
available: boolean;
|
||||
message: string;
|
||||
path?: string;
|
||||
} {
|
||||
const tracerPath = getWaveformTracerPath(extensionPath);
|
||||
|
||||
if (fs.existsSync(tracerPath)) {
|
||||
return {
|
||||
available: true,
|
||||
message: 'waveform_trace 工具可用',
|
||||
path: tracerPath
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
available: false,
|
||||
message: `waveform_trace 工具未找到: ${tracerPath}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import * as vscode from "vscode";
|
||||
import { getWebviewContent } from "./webviewContent";
|
||||
import { isTokenExpired } from "../utils/jwtUtils";
|
||||
import {
|
||||
handleUserMessage,
|
||||
insertCodeToEditor,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
} from "../utils/messageHandler";
|
||||
|
||||
/**
|
||||
@ -24,7 +26,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
@ -39,15 +44,39 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
const iconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||
);
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
||||
panel.webview.html = getWebviewContent(
|
||||
iconUri.toString(),
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString()
|
||||
);
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
console.log("[ICViewProvider] ====== 收到 WebView 消息 ======");
|
||||
console.log("[ICViewProvider] command:", message.command);
|
||||
console.log("[ICViewProvider] 完整消息:", JSON.stringify(message));
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
handleUserMessage(panel, message.text, context.extensionPath);
|
||||
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
@ -80,6 +109,9 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "showWarning":
|
||||
vscode.window.showWarningMessage(message.message);
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
@ -90,7 +122,11 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
// 新增:优化提示词
|
||||
case "optimizePrompt":
|
||||
handleOptimizePrompt(panel, message.prompt);
|
||||
break;
|
||||
}
|
||||
},
|
||||
@ -103,10 +139,34 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
* 侧边栏视图提供者
|
||||
*/
|
||||
export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
private _view?: vscode.WebviewView;
|
||||
|
||||
constructor(
|
||||
private readonly extensionUri: vscode.Uri,
|
||||
private readonly context: vscode.ExtensionContext
|
||||
) {}
|
||||
) {
|
||||
// 监听认证状态变化
|
||||
this.context.subscriptions.push(
|
||||
vscode.authentication.onDidChangeSessions((e) => {
|
||||
if (e.provider.id === "iccoder") {
|
||||
this.refreshLoginStatus();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新登录状态并更新视图
|
||||
*/
|
||||
private async refreshLoginStatus(): Promise<void> {
|
||||
if (this._view) {
|
||||
const isLoggedIn = await this.checkLoginStatus();
|
||||
this._view.webview.html = this.getWebviewContent(
|
||||
this._view.webview,
|
||||
isLoggedIn
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态(使用 Authentication API)
|
||||
@ -114,14 +174,29 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
private async checkLoginStatus(): Promise<boolean> {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
return !!session;
|
||||
console.log("[ICViewProvider] 检查登录状态, session:", session ? "存在" : "不存在");
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
// 检查 token 是否过期
|
||||
const expired = isTokenExpired(session.accessToken);
|
||||
console.log("[ICViewProvider] token 过期检查结果:", expired);
|
||||
// 只有明确过期才认为未登录,无法判断时认为已登录
|
||||
if (expired === true) {
|
||||
console.log("[ICViewProvider] Token 已过期");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("检查登录状态失败:", error);
|
||||
console.log("[ICViewProvider] 检查登录状态失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||
// 保存引用以便后续刷新
|
||||
this._view = webviewView;
|
||||
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
|
||||
@ -136,13 +211,28 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
});
|
||||
|
||||
// 处理侧边栏的消息
|
||||
webviewView.webview.onDidReceiveMessage((message) => {
|
||||
webviewView.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
if (message.command === "openChat") {
|
||||
vscode.commands.executeCommand("ic-coder.openChat");
|
||||
} else if (message.command === "login") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
} else if (message.command === "logout") {
|
||||
// 退出登录(前端已有确认对话框)
|
||||
vscode.commands.executeCommand('iccoder.logout');
|
||||
} else if (message.command === "openICCoder") {
|
||||
// 打开 IC Coder 官网
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
||||
} else if (message.command === "openExternalUrl") {
|
||||
// 打开外部链接
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
this.context.subscriptions
|
||||
);
|
||||
}
|
||||
|
||||
private getWebviewContent(
|
||||
|
||||
204
src/views/agentCard.ts
Normal file
@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 智能体卡片组件
|
||||
*
|
||||
* 功能说明:
|
||||
* - 提供智能体执行状态的可视化展示
|
||||
* - 显示智能体名称、状态和执行步骤
|
||||
* - 支持实时更新步骤信息
|
||||
*/
|
||||
|
||||
import { agentIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取智能体卡片的样式
|
||||
*/
|
||||
export function getAgentCardStyles(): string {
|
||||
return `
|
||||
/* 智能体卡片样式 */
|
||||
.segment-agent {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.agent-card {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.agent-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-bottom: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.agent-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
.agent-name {
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
.agent-status {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.agent-status.running {
|
||||
background: var(--vscode-inputValidation-infoBackground);
|
||||
color: var(--vscode-inputValidation-infoForeground);
|
||||
}
|
||||
.agent-status.completed {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.agent-status.error {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.agent-body {
|
||||
padding: 8px;
|
||||
}
|
||||
.agent-steps-container {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.agent-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.agent-step:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.step-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.step-name {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.step-result {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.agent-step-placeholder {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
.agent-step.low-profile {
|
||||
opacity: 0.85;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.agent-step.low-profile .step-icon {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.agent-step.low-profile .step-name {
|
||||
font-weight: 400;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.9;
|
||||
}
|
||||
.agent-step.low-profile .step-result {
|
||||
opacity: 0.85;
|
||||
font-size: 11px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体卡片的脚本
|
||||
*/
|
||||
export function getAgentCardScript(): string {
|
||||
return `
|
||||
// 工具名称中文映射
|
||||
function getAgentToolDisplayName(toolName) {
|
||||
const toolNameMap = {
|
||||
'file_read': '文件读取',
|
||||
'file_write': '文件写入',
|
||||
'file_delete': '文件删除',
|
||||
'file_list': '检索文件',
|
||||
'syntax_check': '语法检查',
|
||||
'simulation': '仿真',
|
||||
'waveform_summary': '波形分析',
|
||||
'knowledge_save': '保存知识库',
|
||||
'knowledge_load': '加载知识库',
|
||||
'queryKnowledgeSummary': '查询知识摘要',
|
||||
'queryRules': '查询规则',
|
||||
'setModule': '设置模块',
|
||||
'addSignal': '正在分析信号定义',
|
||||
'addSignalExample': '正在处理信号示例',
|
||||
'validateKnowledgeGraph': '验证知识图谱',
|
||||
'querySignals': '查询信号',
|
||||
'addPlan': '添加计划',
|
||||
'addEdge': '添加边',
|
||||
'showPlan': '显示计划',
|
||||
'spawnExplorer': '代码探索',
|
||||
'spawnDebugger': '波形调试',
|
||||
'queryByBFS': 'BFS查询',
|
||||
'queryStateTransitions': '查询状态转移',
|
||||
'addStateTransition': '添加状态转移'
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染智能体卡片
|
||||
* @param {Object} segment - 智能体段落数据
|
||||
* @param {HTMLElement} segmentDiv - 段落容器元素
|
||||
*/
|
||||
function renderAgentCard(segment, segmentDiv) {
|
||||
segmentDiv.className += ' segment-agent';
|
||||
|
||||
const statusText = segment.agentStatus === 'completed' ? '完成'
|
||||
: segment.agentStatus === 'error' ? '错误' : '执行中';
|
||||
const statusClass = segment.agentStatus || 'running';
|
||||
|
||||
const stepsHtml = (segment.agentSteps || []).map(step => {
|
||||
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
||||
const displayName = getAgentToolDisplayName(step.toolName);
|
||||
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
|
||||
// 所有工具调用都使用低调样式
|
||||
const stepClass = 'agent-step low-profile';
|
||||
return \`<div class="\${stepClass}"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
|
||||
}).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="agent-card">
|
||||
<div class="agent-header">
|
||||
<span class="agent-icon">${agentIconSvg}</span>
|
||||
<span class="agent-name">\${segment.agentName || '智能体'}</span>
|
||||
<span class="agent-status \${statusClass}">\${statusText}</span>
|
||||
</div>
|
||||
<div class="agent-body">
|
||||
<div class="agent-steps-container">
|
||||
\${stepsHtml || '<div class="agent-step-placeholder">等待执行...</div>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 自动滚动到最新步骤
|
||||
setTimeout(() => {
|
||||
const container = segmentDiv.querySelector('.agent-steps-container');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -1,8 +1,19 @@
|
||||
/**
|
||||
* 模式选择器组件
|
||||
* 提供 Agent/Ask/Auto 三种模式的选择功能
|
||||
* 提供 Plan/Ask/Agent 四种模式的选择功能
|
||||
*
|
||||
* 模式说明:
|
||||
* - Plan: 只读模式,只能查询分析,不能写文件
|
||||
* - Ask: 逐个确认,每个写操作需用户确认
|
||||
* - Agent: 智能体自主,自动执行大部分操作
|
||||
*/
|
||||
|
||||
import {
|
||||
plannerIconSvg,
|
||||
askIconSvg,
|
||||
agentIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取模式选择器的 HTML 内容
|
||||
*/
|
||||
@ -17,12 +28,30 @@ export function getModeSelectorContent(): string {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mode-dropdown" id="modeDropdown">
|
||||
<div class="mode-option" data-value="agent" onclick="selectMode('agent', 'Agent')">Agent</div>
|
||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">Ask</div>
|
||||
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">Auto</div>
|
||||
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
|
||||
<div class="mode-option-header">
|
||||
<span class="mode-option-icon">${plannerIconSvg}</span>
|
||||
<span class="mode-option-label">Plan</span>
|
||||
</div>
|
||||
<span class="mode-option-desc">仅根据需求生成设计文档,之后由用户决定下一步,可以提高工程质量</span>
|
||||
</div>
|
||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
|
||||
<div class="mode-option-header">
|
||||
<span class="mode-option-icon">${askIconSvg}</span>
|
||||
<span class="mode-option-label">Ask</span>
|
||||
</div>
|
||||
<span class="mode-option-desc">仅给与智能体读权限,用于依据项目回答用户问题,或者与用户进行探讨</span>
|
||||
</div>
|
||||
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
|
||||
<div class="mode-option-header">
|
||||
<span class="mode-option-icon">${agentIconSvg}</span>
|
||||
<span class="mode-option-label">Agent</span>
|
||||
</div>
|
||||
<span class="mode-option-desc">用于快速生成工程、调试修改现有代码</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="tooltiptext">切换模式</span>
|
||||
</div>
|
||||
<span class="tooltiptext" id="modeTooltip">智能体自主模式</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -69,7 +98,8 @@ export function getModeSelectorStyles(): string {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 2px);
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 4px;
|
||||
@ -83,18 +113,45 @@ export function getModeSelectorStyles(): string {
|
||||
}
|
||||
/* 模式选择器的选项样式 */
|
||||
.mode-option {
|
||||
padding: 6px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mode-option:hover {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
.mode-option.selected {
|
||||
background: rgba(128, 128, 128, 0.5);
|
||||
color: var(--vscode-foreground);
|
||||
background: rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
.mode-option-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.mode-option-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mode-option-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
.mode-option-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.mode-option-desc {
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -124,10 +181,22 @@ export function getModeSelectorScript(): string {
|
||||
function selectMode(value, label) {
|
||||
currentMode = value;
|
||||
const modeValue = document.getElementById('modeValue');
|
||||
const modeTooltip = document.getElementById('modeTooltip');
|
||||
|
||||
if (modeValue) {
|
||||
modeValue.textContent = label;
|
||||
}
|
||||
|
||||
// 更新 tooltip
|
||||
if (modeTooltip) {
|
||||
const tooltipMap = {
|
||||
'plan': 'plan模式',
|
||||
'ask': 'ask模式',
|
||||
'agent': 'agent模式'
|
||||
};
|
||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||
}
|
||||
|
||||
// 更新选中状态
|
||||
const options = document.querySelectorAll('.mode-option');
|
||||
options.forEach(option => {
|
||||
|
||||
@ -7,14 +7,76 @@
|
||||
*/
|
||||
export function getContextButtonContent(): string {
|
||||
return `
|
||||
<div class="context-selector-wrapper">
|
||||
<div class="tooltip">
|
||||
<button class="add-context-button" onclick="handleAddContext()">
|
||||
<button class="add-context-button" onclick="toggleContextMenu()">
|
||||
<svg t="1766915545722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4994" width="200" height="200">
|
||||
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
|
||||
</svg>
|
||||
<span class="add-context-label">添加上下文</span>
|
||||
|
||||
</button>
|
||||
<span class="tooltiptext">添加文件或代码片段作为上下文</span>
|
||||
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
||||
</div>
|
||||
|
||||
<!-- 上拉菜单 -->
|
||||
<div class="context-menu" id="contextMenu">
|
||||
<!-- 主菜单 -->
|
||||
<div class="context-menu-main" id="contextMenuMain">
|
||||
<div class="context-menu-item" onclick="showFileList()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326z m1.8 562H232V136h302v216c0 23.2 18.8 42 42 42h216v494z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>文件</span>
|
||||
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="showFolderList()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32zM840 768H184V256h188.5l119.6 114.4H840V768z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>文件夹</span>
|
||||
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="handleAddImage()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>图片</span>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="handleAddDocument()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>文档库</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件/文件夹列表视图 -->
|
||||
<div class="context-menu-list" id="contextMenuList" style="display: none;">
|
||||
<div class="context-menu-list-header">
|
||||
<button class="context-menu-back" onclick="backToMainMenu()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8c-16.4 12.8-16.4 37.5 0 50.3l450.8 352.1c5.3 4.1 12.9 0.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="contextMenuListTitle">选择文件</span>
|
||||
</div>
|
||||
<div class="context-menu-list-body" id="contextMenuListBody">
|
||||
<!-- 动态加载列表 -->
|
||||
</div>
|
||||
<div class="context-menu-list-footer">
|
||||
<input type="text" id="contextMenuSearch" placeholder="搜索..." />
|
||||
<div class="context-menu-list-actions">
|
||||
<span id="contextMenuListCount">已选择 0 项</span>
|
||||
<button class="primary" onclick="confirmSelection()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -24,6 +86,12 @@ export function getContextButtonContent(): string {
|
||||
*/
|
||||
export function getContextButtonStyles(): string {
|
||||
return `
|
||||
/* 上下文选择器容器 */
|
||||
.context-selector-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 添加上下文按钮样式 */
|
||||
.add-context-button {
|
||||
display: flex;
|
||||
@ -45,15 +113,218 @@ export function getContextButtonStyles(): string {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.add-context-button svg {
|
||||
.add-context-button svg.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.add-context-button .dropdown-arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.add-context-button.active .dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.add-context-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 上拉菜单样式 */
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 0;
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
min-width: 180px;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.context-menu.show {
|
||||
display: block;
|
||||
animation: slideUp 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.context-menu-item svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.context-menu-item span {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.context-menu-item .arrow-right {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0.6;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 列表视图样式 */
|
||||
.context-menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
.context-menu-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.context-menu-back {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.context-menu-back:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.context-menu-back svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.context-menu-list-header span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.context-menu-list-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.context-menu-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.context-menu-list-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.context-menu-list-item.selected {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
|
||||
.context-menu-list-item input[type="checkbox"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-menu-list-item label {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context-menu-list-footer {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.context-menu-list-footer input {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--vscode-input-foreground);
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.context-menu-list-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.context-menu-list-footer span {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.context-menu-list-footer button {
|
||||
padding: 4px 12px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.context-menu-list-footer button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -62,10 +333,174 @@ export function getContextButtonStyles(): string {
|
||||
*/
|
||||
export function getContextButtonScript(): string {
|
||||
return `
|
||||
// 添加上下文处理函数
|
||||
function handleAddContext() {
|
||||
// 发送添加上下文请求到扩展
|
||||
vscode.postMessage({ command: 'addContext' });
|
||||
// 上下文菜单状态
|
||||
let currentListData = [];
|
||||
let currentListType = '';
|
||||
let selectedItems = new Set();
|
||||
|
||||
// 切换上下文菜单显示/隐藏
|
||||
function toggleContextMenu() {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const button = document.querySelector('.add-context-button');
|
||||
|
||||
if (menu && button) {
|
||||
const isShown = menu.classList.contains('show');
|
||||
|
||||
if (isShown) {
|
||||
menu.classList.remove('show');
|
||||
button.classList.remove('active');
|
||||
backToMainMenu(); // 关闭时回到主菜单
|
||||
} else {
|
||||
menu.classList.add('show');
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭菜单
|
||||
document.addEventListener('click', function(event) {
|
||||
const wrapper = document.querySelector('.context-selector-wrapper');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const button = document.querySelector('.add-context-button');
|
||||
|
||||
if (wrapper && menu && button && !wrapper.contains(event.target)) {
|
||||
menu.classList.remove('show');
|
||||
button.classList.remove('active');
|
||||
backToMainMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// 显示文件列表
|
||||
function showFileList() {
|
||||
vscode.postMessage({ command: 'addContextFile' });
|
||||
}
|
||||
|
||||
// 显示文件夹列表
|
||||
function showFolderList() {
|
||||
vscode.postMessage({ command: 'addContextFolder' });
|
||||
}
|
||||
|
||||
// 返回主菜单
|
||||
function backToMainMenu() {
|
||||
const mainMenu = document.getElementById('contextMenuMain');
|
||||
const listView = document.getElementById('contextMenuList');
|
||||
|
||||
if (mainMenu && listView) {
|
||||
mainMenu.style.display = 'block';
|
||||
listView.style.display = 'none';
|
||||
}
|
||||
|
||||
selectedItems.clear();
|
||||
currentListData = [];
|
||||
}
|
||||
|
||||
// 切换到列表视图
|
||||
function switchToListView(title, type, data) {
|
||||
const mainMenu = document.getElementById('contextMenuMain');
|
||||
const listView = document.getElementById('contextMenuList');
|
||||
const titleEl = document.getElementById('contextMenuListTitle');
|
||||
|
||||
if (mainMenu && listView && titleEl) {
|
||||
mainMenu.style.display = 'none';
|
||||
listView.style.display = 'flex';
|
||||
titleEl.textContent = title;
|
||||
|
||||
currentListType = type;
|
||||
currentListData = data;
|
||||
selectedItems.clear();
|
||||
|
||||
renderList(data);
|
||||
updateSelectedCount();
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染列表
|
||||
function renderList(data) {
|
||||
const body = document.getElementById('contextMenuListBody');
|
||||
if (!body) return;
|
||||
|
||||
body.innerHTML = data.map((item, index) => \`
|
||||
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
|
||||
<input type="checkbox" id="item-\${index}" />
|
||||
<label for="item-\${index}">\${item.relativePath}</label>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 切换项选择
|
||||
function toggleItemSelection(index) {
|
||||
const checkbox = document.getElementById('item-' + index);
|
||||
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
||||
|
||||
if (checkbox && item) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
|
||||
if (checkbox.checked) {
|
||||
selectedItems.add(index);
|
||||
item.classList.add('selected');
|
||||
} else {
|
||||
selectedItems.delete(index);
|
||||
item.classList.remove('selected');
|
||||
}
|
||||
|
||||
updateSelectedCount();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新选中数量
|
||||
function updateSelectedCount() {
|
||||
const countEl = document.getElementById('contextMenuListCount');
|
||||
if (countEl) {
|
||||
countEl.textContent = '已选择 ' + selectedItems.size + ' 项';
|
||||
}
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
function confirmSelection() {
|
||||
const selected = Array.from(selectedItems).map(index => currentListData[index]);
|
||||
|
||||
if (selected.length > 0) {
|
||||
selected.forEach(item => {
|
||||
addContextItem(currentListType, item.path);
|
||||
});
|
||||
}
|
||||
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 添加图片
|
||||
function handleAddImage() {
|
||||
vscode.postMessage({ command: 'addContextImage' });
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 添加文档
|
||||
function handleAddDocument() {
|
||||
vscode.postMessage({ command: 'addContextDocument' });
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const searchInput = document.getElementById('contextMenuSearch');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const keyword = e.target.value.toLowerCase();
|
||||
const filtered = currentListData.filter(item =>
|
||||
item.relativePath.toLowerCase().includes(keyword)
|
||||
);
|
||||
renderList(filtered);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理后端消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
if (message.command === 'showWorkspaceFileList') {
|
||||
switchToListView('选择文件', 'file', message.files);
|
||||
} else if (message.command === 'showWorkspaceFolderList') {
|
||||
switchToListView('选择文件夹', 'folder', message.folders);
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
225
src/views/contextDisplay.ts
Normal file
@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 上下文显示组件
|
||||
* 用于显示已选择的文件、文件夹、图片和文档
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取上下文显示区域的 HTML 内容
|
||||
*/
|
||||
export function getContextDisplayContent(): string {
|
||||
return `
|
||||
<div class="context-display-area" id="contextDisplayArea" style="display: none;">
|
||||
<div class="context-items-container" id="contextItemsContainer">
|
||||
<!-- 动态添加的上下文项将显示在这里 -->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上下文显示区域的样式
|
||||
*/
|
||||
export function getContextDisplayStyles(): string {
|
||||
return `
|
||||
/* 上下文显示区域 */
|
||||
.context-display-area {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
|
||||
.context-items-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 上下文项样式 */
|
||||
.context-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
max-width: 300px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.context-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.context-item svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.context-item-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context-item-remove {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-item-remove:hover {
|
||||
opacity: 1;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 图片预览样式 */
|
||||
.context-item.image-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.context-item-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上下文显示区域的脚本
|
||||
*/
|
||||
export function getContextDisplayScript(): string {
|
||||
return `
|
||||
// 存储上下文项
|
||||
let contextItems = [];
|
||||
|
||||
// 获取文件图标 SVG
|
||||
function getFileIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取文件夹图标 SVG
|
||||
function getFolderIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取图片图标 SVG
|
||||
function getImageIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取文档图标 SVG
|
||||
function getDocumentIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取删除图标 SVG
|
||||
function getRemoveIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 提取文件名
|
||||
function getFileName(path) {
|
||||
return path.split(/[\\\\/]/).pop();
|
||||
}
|
||||
|
||||
// 添加上下文项
|
||||
function addContextItem(type, path) {
|
||||
const id = Date.now() + Math.random();
|
||||
contextItems.push({ id, type, path });
|
||||
renderContextItems();
|
||||
}
|
||||
|
||||
// 删除上下文项
|
||||
function removeContextItem(id) {
|
||||
contextItems = contextItems.filter(item => item.id !== id);
|
||||
renderContextItems();
|
||||
}
|
||||
|
||||
// 渲染上下文项
|
||||
function renderContextItems() {
|
||||
const container = document.getElementById('contextItemsContainer');
|
||||
const displayArea = document.getElementById('contextDisplayArea');
|
||||
|
||||
if (!container || !displayArea) return;
|
||||
|
||||
if (contextItems.length === 0) {
|
||||
displayArea.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
displayArea.style.display = 'block';
|
||||
container.innerHTML = contextItems.map(item => {
|
||||
let icon = '';
|
||||
switch(item.type) {
|
||||
case 'file': icon = getFileIcon(); break;
|
||||
case 'folder': icon = getFolderIcon(); break;
|
||||
case 'image': icon = getImageIcon(); break;
|
||||
case 'document': icon = getDocumentIcon(); break;
|
||||
}
|
||||
|
||||
return \`
|
||||
<div class="context-item" title="\${item.path}">
|
||||
\${icon}
|
||||
<span class="context-item-name">\${getFileName(item.path)}</span>
|
||||
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
||||
\${getRemoveIcon()}
|
||||
</span>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 处理后端返回的文件选择结果
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
switch(message.command) {
|
||||
case 'contextFilesSelected':
|
||||
if (message.files && message.files.length > 0) {
|
||||
message.files.forEach(file => addContextItem('file', file));
|
||||
}
|
||||
break;
|
||||
case 'contextFoldersSelected':
|
||||
if (message.folders && message.folders.length > 0) {
|
||||
message.folders.forEach(folder => addContextItem('folder', folder));
|
||||
}
|
||||
break;
|
||||
case 'contextImagesSelected':
|
||||
if (message.images && message.images.length > 0) {
|
||||
message.images.forEach(image => addContextItem('image', image));
|
||||
}
|
||||
break;
|
||||
case 'contextDocumentsSelected':
|
||||
if (message.documents && message.documents.length > 0) {
|
||||
message.documents.forEach(doc => addContextItem('document', doc));
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有上下文项(供发送消息时使用)
|
||||
window.getContextItems = function() {
|
||||
return contextItems;
|
||||
};
|
||||
|
||||
// 清空上下文项(供清空对话时使用)
|
||||
window.clearContextItems = function() {
|
||||
contextItems = [];
|
||||
renderContextItems();
|
||||
};
|
||||
`;
|
||||
}
|
||||
@ -1,3 +1,24 @@
|
||||
import {
|
||||
getUserInfoComponentContent,
|
||||
getUserInfoComponentStyles,
|
||||
getUserInfoComponentScript,
|
||||
} from "./userInfoComponent";
|
||||
import {
|
||||
getMoreOptionsComponentContent,
|
||||
getMoreOptionsComponentStyles,
|
||||
getMoreOptionsComponentScript,
|
||||
} from "./moreOptionsComponent";
|
||||
import {
|
||||
getSettingsComponentContent,
|
||||
getSettingsComponentStyles,
|
||||
getSettingsComponentScript,
|
||||
} from "./settingsComponent";
|
||||
import {
|
||||
userAvatarIconSvg,
|
||||
moreIconSvg,
|
||||
setting,
|
||||
} from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取会话历史栏的 HTML 内容
|
||||
*/
|
||||
@ -6,7 +27,7 @@ export function getConversationHistoryBarContent(): string {
|
||||
<div class="conversation-history-bar">
|
||||
<div class="history-dropdown-container">
|
||||
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
|
||||
<span class="dropdown-label">Past Conversations</span>
|
||||
<span class="dropdown-label">历史对话</span>
|
||||
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
|
||||
</svg>
|
||||
@ -19,12 +40,36 @@ export function getConversationHistoryBarContent(): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="user-info-container">
|
||||
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
|
||||
${userAvatarIconSvg}
|
||||
</button>
|
||||
${getUserInfoComponentContent()}
|
||||
</div>
|
||||
|
||||
<div class='setting'>
|
||||
<button class="setting-btn" title="设置" onclick="openSettingsModal()">
|
||||
${setting}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class='more-container'>
|
||||
<button class="more-button" title="更多选项" onclick="toggleMoreOptionsDropdown()">
|
||||
${moreIconSvg}
|
||||
</button>
|
||||
${getMoreOptionsComponentContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${getSettingsComponentContent()}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -49,13 +94,125 @@ export function getConversationHistoryBarStyles(): string {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-info-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-avatar-icon-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
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()}
|
||||
|
||||
${getSettingsComponentStyles()}
|
||||
|
||||
.setting {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.setting-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
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;
|
||||
}
|
||||
|
||||
.setting-btn:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.setting-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.more-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.more-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
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;
|
||||
}
|
||||
|
||||
.more-button:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.more-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.more-button.active {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
${getMoreOptionsComponentStyles()}
|
||||
|
||||
.history-dropdown-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-input-foreground);
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@ -64,7 +221,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.history-dropdown-button:hover {
|
||||
opacity: 0.8;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
@ -157,13 +314,13 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -173,11 +330,12 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button:hover {
|
||||
opacity: 0.7;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.new-conversation-button:active {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.new-conversation-button svg {
|
||||
@ -210,6 +368,33 @@ export function getConversationHistoryBarStyles(): string {
|
||||
*/
|
||||
export function getConversationHistoryBarScript(): string {
|
||||
return `
|
||||
${getUserInfoComponentScript()}
|
||||
|
||||
${getMoreOptionsComponentScript()}
|
||||
|
||||
${getSettingsComponentScript()}
|
||||
|
||||
// 更新用户头像图标按钮显示
|
||||
function updateUserAvatarIconButton(userInfo) {
|
||||
const userAvatarIconButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (userInfo && userInfo.nickname) {
|
||||
// 显示用户头像图标按钮
|
||||
if (userAvatarIconButton) {
|
||||
userAvatarIconButton.style.display = 'flex';
|
||||
}
|
||||
// 同时更新用户详情弹窗的数据
|
||||
if (typeof updateUserInfoDisplay === 'function') {
|
||||
updateUserInfoDisplay(userInfo);
|
||||
}
|
||||
} else {
|
||||
// 隐藏用户头像图标按钮
|
||||
if (userAvatarIconButton) {
|
||||
userAvatarIconButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 会话历史相关变量
|
||||
let conversationHistory = [];
|
||||
let currentConversationId = null;
|
||||
@ -217,6 +402,7 @@ export function getConversationHistoryBarScript(): string {
|
||||
let totalHistory = 0;
|
||||
let hasMoreHistory = false;
|
||||
let isLoadingHistory = false;
|
||||
let currentLoadRequestId = 0; // 请求 ID,用于防止并发加载
|
||||
const HISTORY_PAGE_SIZE = 10;
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
@ -260,11 +446,15 @@ export function getConversationHistoryBarScript(): string {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成新的请求 ID,用于防止并发加载
|
||||
const requestId = ++currentLoadRequestId;
|
||||
|
||||
isLoadingHistory = true;
|
||||
vscode.postMessage({
|
||||
command: 'loadConversationHistory',
|
||||
offset: currentOffset,
|
||||
limit: HISTORY_PAGE_SIZE
|
||||
limit: HISTORY_PAGE_SIZE,
|
||||
requestId: requestId
|
||||
});
|
||||
}
|
||||
|
||||
@ -276,11 +466,19 @@ export function getConversationHistoryBarScript(): string {
|
||||
return;
|
||||
}
|
||||
|
||||
// 追加新数据
|
||||
conversationHistory = conversationHistory.concat(data.items);
|
||||
// 追加新数据(去重)
|
||||
const existingIds = new Set(conversationHistory.map(item => item.id));
|
||||
const newItems = [];
|
||||
for (const item of data.items) {
|
||||
if (!existingIds.has(item.id)) {
|
||||
existingIds.add(item.id);
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
conversationHistory = conversationHistory.concat(newItems);
|
||||
totalHistory = data.total;
|
||||
hasMoreHistory = data.hasMore;
|
||||
currentOffset += data.items.length;
|
||||
currentOffset = conversationHistory.length;
|
||||
|
||||
const historyList = document.getElementById('historyList');
|
||||
if (!historyList) {
|
||||
@ -368,9 +566,10 @@ export function getConversationHistoryBarScript(): string {
|
||||
});
|
||||
}
|
||||
|
||||
// 监听下拉菜单滚动事件
|
||||
// 监听下拉菜单滚动事件(防止重复注册)
|
||||
const historyDropdownMenu = document.getElementById('historyDropdownMenu');
|
||||
if (historyDropdownMenu) {
|
||||
if (historyDropdownMenu && !historyDropdownMenu._scrollListenerAdded) {
|
||||
historyDropdownMenu._scrollListenerAdded = true;
|
||||
historyDropdownMenu.addEventListener('scroll', () => {
|
||||
const menu = historyDropdownMenu;
|
||||
const scrollTop = menu.scrollTop;
|
||||
|
||||
284
src/views/exampleShowcase.ts
Normal file
@ -0,0 +1,284 @@
|
||||
/**
|
||||
* 获取展示区域的 HTML 内容
|
||||
*/
|
||||
export function getExampleShowcaseContent(): string {
|
||||
return `
|
||||
<div class="example-showcase" id="exampleShowcase">
|
||||
<div class="showcase-title">示例</div>
|
||||
<div class="example-cards">
|
||||
<div class="example-card" onclick="sendExample(0)">
|
||||
<div class="example-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 9H9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="example-content">
|
||||
<div class="example-title">生成一个SPI控制器</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-card" onclick="sendExample(1)">
|
||||
<div class="example-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="example-content">
|
||||
<div class="example-title">生成一个GMII接口的以太网UDP通信模块</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="web-link">
|
||||
<a href="https://iccoder.com" target="_blank" class="web-link-button">
|
||||
<span class="link-icon">🌐</span>
|
||||
<span>IC Coder Web端</span>
|
||||
<span class="link-arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取展示区域的样式
|
||||
*/
|
||||
export function getExampleShowcaseStyles(): string {
|
||||
return `
|
||||
.example-showcase {
|
||||
margin-top: 24px;
|
||||
padding: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.example-showcase.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.showcase-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin-bottom: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.example-cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.example-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.example-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(79, 172, 254, 0.1) 0%, rgba(0, 242, 254, 0.1) 50%, rgba(168, 85, 247, 0.1) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.example-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.example-card:hover {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.example-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.example-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.example-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.example-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.example-card:hover .example-title {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.example-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.web-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.web-link-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.web-link-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.web-link-button:hover {
|
||||
transform: translateY(-1px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.link-arrow {
|
||||
font-size: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.web-link-button:hover .link-arrow {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取展示区域的脚本
|
||||
*/
|
||||
export function getExampleShowcaseScript(): string {
|
||||
return `
|
||||
// 示例文本数组
|
||||
const exampleTexts = [
|
||||
'生成一个SPI控制器',
|
||||
'生成一个GMII接口的以太网UDP通信模块'
|
||||
];
|
||||
|
||||
// 存储待发送的示例索引
|
||||
let pendingExampleIndex = -1;
|
||||
|
||||
// 直接发送示例消息
|
||||
function sendExample(index) {
|
||||
// 先检查工作区
|
||||
pendingExampleIndex = index;
|
||||
vscode.postMessage({
|
||||
command: 'checkWorkspace'
|
||||
});
|
||||
}
|
||||
|
||||
// 实际发送示例消息
|
||||
function doSendExample(index) {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const sendButton = document.getElementById('sendButton');
|
||||
|
||||
if (messageInput && exampleTexts[index]) {
|
||||
messageInput.value = exampleTexts[index];
|
||||
|
||||
// 触发自动调整高度
|
||||
if (typeof autoResizeTextarea === 'function') {
|
||||
autoResizeTextarea();
|
||||
}
|
||||
|
||||
// 直接触发发送
|
||||
if (sendButton && typeof sendButton.click === 'function') {
|
||||
sendButton.click();
|
||||
} else if (typeof sendMessage === 'function') {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听消息变化,自动隐藏/显示展示区域
|
||||
function updateShowcaseVisibility() {
|
||||
const showcase = document.getElementById('exampleShowcase');
|
||||
if (showcase) {
|
||||
if (hasMessages) {
|
||||
showcase.classList.add('hidden');
|
||||
} else {
|
||||
showcase.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展原有的布局更新函数
|
||||
const originalUpdateInputAreaLayout = updateInputAreaLayout;
|
||||
updateInputAreaLayout = function() {
|
||||
if (originalUpdateInputAreaLayout) {
|
||||
originalUpdateInputAreaLayout();
|
||||
}
|
||||
updateShowcaseVisibility();
|
||||
};
|
||||
`;
|
||||
}
|
||||
327
src/views/generalSettingsComponent.ts
Normal file
@ -0,0 +1,327 @@
|
||||
/**
|
||||
* 获取通用设置组件的 HTML 内容
|
||||
*/
|
||||
export function getGeneralSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="general-settings">
|
||||
<h3 class="settings-section-title">通用设置</h3>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">主题</label>
|
||||
<span class="settings-item-description">选择界面主题</span>
|
||||
</div>
|
||||
<select class="settings-select" id="themeSelect">
|
||||
<option value="auto">跟随系统</option>
|
||||
<option value="light">浅色</option>
|
||||
<option value="dark">深色</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">语言</label>
|
||||
<span class="settings-item-description">选择界面语言</span>
|
||||
</div>
|
||||
<select class="settings-select" id="languageSelect">
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="en-US">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">自动保存</label>
|
||||
<span class="settings-item-description">自动保存会话历史</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="autoSaveCheckbox" checked>
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">显示时间戳</label>
|
||||
<span class="settings-item-description">在消息中显示时间戳</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="showTimestampCheckbox">
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">编辑器设置</h4>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">字体大小</label>
|
||||
<span class="settings-item-description">设置编辑器字体大小</span>
|
||||
</div>
|
||||
<input type="number" class="settings-input" id="fontSizeInput" value="14" min="10" max="24">
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">代码高亮</label>
|
||||
<span class="settings-item-description">启用代码语法高亮</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="syntaxHighlightCheckbox" checked>
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="saveGeneralSettings()">
|
||||
保存设置
|
||||
</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="resetGeneralSettings()">
|
||||
重置为默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用设置组件的 CSS 样式
|
||||
*/
|
||||
export function getGeneralSettingsComponentStyles(): string {
|
||||
return `
|
||||
.general-settings {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.settings-subsection-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.settings-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-item-header {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.settings-item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.settings-item-description {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.settings-select {
|
||||
padding: 6px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-select:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.settings-input {
|
||||
width: 80px;
|
||||
padding: 6px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-input:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.settings-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.settings-switch-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 24px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-switch-slider:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: var(--vscode-foreground);
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-switch input:checked + .settings-switch-slider {
|
||||
background: var(--vscode-button-background);
|
||||
border-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.settings-switch input:checked + .settings-switch-slider:before {
|
||||
transform: translateX(20px);
|
||||
background: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-button-primary {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.settings-button-primary:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.settings-button-secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.settings-button-secondary:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用设置组件的 JavaScript 脚本
|
||||
*/
|
||||
export function getGeneralSettingsComponentScript(): string {
|
||||
return `
|
||||
// 保存通用设置
|
||||
function saveGeneralSettings() {
|
||||
const settings = {
|
||||
theme: document.getElementById('themeSelect').value,
|
||||
language: document.getElementById('languageSelect').value,
|
||||
autoSave: document.getElementById('autoSaveCheckbox').checked,
|
||||
showTimestamp: document.getElementById('showTimestampCheckbox').checked,
|
||||
fontSize: document.getElementById('fontSizeInput').value,
|
||||
syntaxHighlight: document.getElementById('syntaxHighlightCheckbox').checked,
|
||||
};
|
||||
|
||||
// 发送消息到扩展
|
||||
vscode.postMessage({
|
||||
command: 'saveGeneralSettings',
|
||||
settings: settings
|
||||
});
|
||||
|
||||
// 显示保存成功提示
|
||||
console.log('通用设置已保存', settings);
|
||||
}
|
||||
|
||||
// 重置通用设置
|
||||
function resetGeneralSettings() {
|
||||
document.getElementById('themeSelect').value = 'auto';
|
||||
document.getElementById('languageSelect').value = 'zh-CN';
|
||||
document.getElementById('autoSaveCheckbox').checked = true;
|
||||
document.getElementById('showTimestampCheckbox').checked = false;
|
||||
document.getElementById('fontSizeInput').value = '14';
|
||||
document.getElementById('syntaxHighlightCheckbox').checked = true;
|
||||
|
||||
console.log('通用设置已重置为默认值');
|
||||
}
|
||||
|
||||
// 加载通用设置
|
||||
function loadGeneralSettings(settings) {
|
||||
if (!settings) return;
|
||||
|
||||
if (settings.theme) {
|
||||
document.getElementById('themeSelect').value = settings.theme;
|
||||
}
|
||||
if (settings.language) {
|
||||
document.getElementById('languageSelect').value = settings.language;
|
||||
}
|
||||
if (settings.autoSave !== undefined) {
|
||||
document.getElementById('autoSaveCheckbox').checked = settings.autoSave;
|
||||
}
|
||||
if (settings.showTimestamp !== undefined) {
|
||||
document.getElementById('showTimestampCheckbox').checked = settings.showTimestamp;
|
||||
}
|
||||
if (settings.fontSize) {
|
||||
document.getElementById('fontSizeInput').value = settings.fontSize;
|
||||
}
|
||||
if (settings.syntaxHighlight !== undefined) {
|
||||
document.getElementById('syntaxHighlightCheckbox').checked = settings.syntaxHighlight;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -2,60 +2,68 @@ import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||||
import {
|
||||
getModelSelectorContent,
|
||||
getModelSelectorStyles,
|
||||
getModelSelectorScript
|
||||
getModelSelectorScript,
|
||||
} from "./modelSelector";
|
||||
import {
|
||||
getModeSelectorContent,
|
||||
getModeSelectorStyles,
|
||||
getModeSelectorScript
|
||||
getModeSelectorScript,
|
||||
} from "./agentModeSelector";
|
||||
import {
|
||||
getContextButtonContent,
|
||||
getContextButtonStyles,
|
||||
getContextButtonScript
|
||||
getContextButtonScript,
|
||||
} from "./contextButton";
|
||||
import {
|
||||
getContextDisplayContent,
|
||||
getContextDisplayStyles,
|
||||
getContextDisplayScript,
|
||||
} from "./contextDisplay";
|
||||
import {
|
||||
getContextCompressContent,
|
||||
getContextCompressStyles,
|
||||
getContextCompressScript
|
||||
getContextCompressScript,
|
||||
} from "./contextCompress";
|
||||
import {
|
||||
getPlanToggleContent,
|
||||
getPlanToggleStyles,
|
||||
getPlanToggleScript
|
||||
} from "./planToggle";
|
||||
import {
|
||||
getOptimizeButtonContent,
|
||||
getOptimizeButtonStyles,
|
||||
getOptimizeButtonScript
|
||||
getOptimizeButtonScript,
|
||||
} from "./optimizeButton";
|
||||
import {
|
||||
sendIconSvg,
|
||||
stopIconSvg
|
||||
} from "../constants/toolIcons";
|
||||
getExampleShowcaseContent,
|
||||
getExampleShowcaseStyles,
|
||||
getExampleShowcaseScript,
|
||||
} from "./exampleShowcase";
|
||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取输入区域的 HTML 内容
|
||||
*/
|
||||
export function getInputAreaContent(): string {
|
||||
export function getInputAreaContent(
|
||||
autoIcon: string = "",
|
||||
liteIcon: string = "",
|
||||
syIcon: string = "",
|
||||
maxIcon: string = ""
|
||||
): string {
|
||||
return `
|
||||
<div class="input-area">
|
||||
<div class="input-area centered" id="inputArea">
|
||||
<div class="input-group">
|
||||
<div class="input-wrapper">
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="input-top-toolbar">
|
||||
${getContextButtonContent()}
|
||||
${getPlanToggleContent()}
|
||||
</div>
|
||||
<!-- 上下文显示区域 -->
|
||||
${getContextDisplayContent()}
|
||||
<textarea
|
||||
id="messageInput"
|
||||
placeholder="输入您的问题..."
|
||||
placeholder="输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
||||
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
|
||||
></textarea>
|
||||
<div class="input-bottom-row">
|
||||
<div class="mode-selector">
|
||||
${getModeSelectorContent()}
|
||||
${getModelSelectorContent()}
|
||||
${getModelSelectorContent(autoIcon, liteIcon, syIcon, maxIcon)}
|
||||
</div>
|
||||
<div class="input-actions">
|
||||
${getContextCompressContent()}
|
||||
@ -68,6 +76,8 @@ export function getInputAreaContent(): string {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 展示区域:案例和 Web 端链接 -->
|
||||
${getExampleShowcaseContent()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -80,13 +90,31 @@ export function getInputAreaStyles(): string {
|
||||
${getModeSelectorStyles()}
|
||||
${getModelSelectorStyles()}
|
||||
${getContextButtonStyles()}
|
||||
${getContextDisplayStyles()}
|
||||
${getContextCompressStyles()}
|
||||
${getPlanToggleStyles()}
|
||||
${getOptimizeButtonStyles()}
|
||||
${getExampleShowcaseStyles()}
|
||||
.input-area {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
padding-top: 15px;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
/* 居中模式:未发起对话时 */
|
||||
.input-area.centered {
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% - 40px);
|
||||
max-width: 800px;
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
/* 底部模式:发起对话后 */
|
||||
.input-area.bottom {
|
||||
position: relative;
|
||||
transform: none;
|
||||
}
|
||||
.input-group {
|
||||
display: flex;
|
||||
@ -207,6 +235,11 @@ export function getInputAreaStyles(): string {
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
textarea:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
/* 简洁的滚动条样式 */
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@ -261,15 +294,38 @@ export function getInputAreaStyles(): string {
|
||||
*/
|
||||
export function getInputAreaScript(): string {
|
||||
return `
|
||||
${getModeSelectorScript()}
|
||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||
${getModelSelectorScript()}
|
||||
${getContextButtonScript()}
|
||||
${getContextDisplayScript()}
|
||||
${getContextCompressScript()}
|
||||
${getPlanToggleScript()}
|
||||
${getOptimizeButtonScript()}
|
||||
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
let hasMessages = false; // 是否已有消息
|
||||
|
||||
// 工作区检测状态
|
||||
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
||||
let hasWorkspace = true; // 工作区状态
|
||||
|
||||
${getExampleShowcaseScript()}
|
||||
|
||||
// 切换输入框布局模式
|
||||
function updateInputAreaLayout() {
|
||||
const inputArea = document.getElementById('inputArea');
|
||||
if (!inputArea) return;
|
||||
|
||||
if (hasMessages) {
|
||||
// 有消息时,移到底部
|
||||
inputArea.classList.remove('centered');
|
||||
inputArea.classList.add('bottom');
|
||||
} else {
|
||||
// 无消息时,居中显示
|
||||
inputArea.classList.add('centered');
|
||||
inputArea.classList.remove('bottom');
|
||||
}
|
||||
}
|
||||
|
||||
// 自动调整 textarea 高度
|
||||
function autoResizeTextarea() {
|
||||
@ -283,11 +339,16 @@ export function getInputAreaScript(): string {
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('input', autoResizeTextarea);
|
||||
|
||||
// 监听点击事件,检测工作区状态
|
||||
messageInput.addEventListener('focus', () => {
|
||||
if (!hasCheckedWorkspace) {
|
||||
hasCheckedWorkspace = true;
|
||||
vscode.postMessage({ command: 'checkWorkspace' });
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化时调整一次高度
|
||||
autoResizeTextarea();
|
||||
|
||||
// 聚焦到输入框
|
||||
messageInput.focus();
|
||||
}
|
||||
|
||||
// 切换发送按钮状态
|
||||
@ -302,11 +363,17 @@ export function getInputAreaScript(): string {
|
||||
sendIconContainer.style.display = 'none';
|
||||
stopIconContainer.style.display = 'block';
|
||||
isConversationActive = true;
|
||||
// 禁用输入框
|
||||
messageInput.disabled = true;
|
||||
messageInput.placeholder = '正在处理中,请稍候...';
|
||||
} else {
|
||||
sendButton.classList.remove('sending');
|
||||
sendIconContainer.style.display = 'block';
|
||||
stopIconContainer.style.display = 'none';
|
||||
isConversationActive = false;
|
||||
// 启用输入框
|
||||
messageInput.disabled = false;
|
||||
messageInput.placeholder = '输入您的问题,按 Enter 发送,Shift + Enter 换行...';
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,15 +393,43 @@ export function getInputAreaScript(): string {
|
||||
const text = messageInput.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
// 如果正在对话中,阻止发送新消息
|
||||
if (isConversationActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查工作区状态
|
||||
if (!hasWorkspace) {
|
||||
// 如果没有工作区,阻止发送并清空输入框
|
||||
messageInput.value = '';
|
||||
autoResizeTextarea();
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
|
||||
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
||||
const planMode = document.getElementById('planToggle')?.checked || false;
|
||||
|
||||
// 获取上下文项
|
||||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||
|
||||
addMessage(text, 'user');
|
||||
|
||||
// 标记已有消息,切换布局到底部
|
||||
hasMessages = true;
|
||||
updateInputAreaLayout();
|
||||
|
||||
// 切换按钮为暂停状态
|
||||
setSendButtonState(true);
|
||||
|
||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model });
|
||||
vscode.postMessage({
|
||||
command: 'sendMessage',
|
||||
text: text,
|
||||
mode: mode,
|
||||
model: model,
|
||||
planMode: planMode,
|
||||
contextItems: contextItems
|
||||
});
|
||||
messageInput.value = '';
|
||||
autoResizeTextarea(); // 重置输入框高度
|
||||
messageInput.focus();
|
||||
@ -342,5 +437,28 @@ export function getInputAreaScript(): string {
|
||||
// 重置优化状态
|
||||
resetOptimizeButton();
|
||||
}
|
||||
|
||||
// 全局函数:重置输入框布局(用于清空对话时)
|
||||
window.resetInputAreaLayout = function() {
|
||||
hasMessages = false;
|
||||
updateInputAreaLayout();
|
||||
};
|
||||
|
||||
// 全局函数:检查是否有消息(用于页面加载时)
|
||||
window.checkMessagesAndUpdateLayout = function() {
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
if (messagesContainer) {
|
||||
const messageElements = messagesContainer.querySelectorAll('.message');
|
||||
hasMessages = messageElements.length > 0;
|
||||
updateInputAreaLayout();
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载时检查消息状态
|
||||
setTimeout(() => {
|
||||
if (window.checkMessagesAndUpdateLayout) {
|
||||
window.checkMessagesAndUpdateLayout();
|
||||
}
|
||||
}, 100);
|
||||
`;
|
||||
}
|
||||
|
||||
@ -13,13 +13,29 @@
|
||||
import {
|
||||
collapseIconSvg,
|
||||
fileWriteIconSvg,
|
||||
fileReadIconSvg,
|
||||
fileDeleteIconSvg,
|
||||
syntaxCheckIconSvg,
|
||||
SearchCode,
|
||||
agentIconSvg,
|
||||
saveKnowledgeIconSvg,
|
||||
simulationIconSvg,
|
||||
waveformIconSvg,
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
userQuestionIconSvg,
|
||||
updateStageIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
import {
|
||||
getWaveformPreviewContent,
|
||||
getWaveformPreviewScript,
|
||||
} from "./waveformPreviewContent";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
|
||||
import {
|
||||
getCodeHighlightStyles,
|
||||
getCodeHighlightScript,
|
||||
} from "../components/codeHighlight";
|
||||
|
||||
/**
|
||||
* 获取消息区域的 HTML 内容
|
||||
@ -284,7 +300,7 @@ export function getMessageAreaStyles(): string {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 22px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
@ -293,76 +309,80 @@ export function getMessageAreaStyles(): string {
|
||||
/* Markdown 样式 */
|
||||
.segment-text h1,
|
||||
.segment-text h2,
|
||||
.segment-text h3 {
|
||||
margin: 16px 0 8px 0;
|
||||
.segment-text h3,
|
||||
.question-text h1,
|
||||
.question-text h2,
|
||||
.question-text h3 {
|
||||
margin: 0px 0 -10px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.segment-text h1 {
|
||||
.segment-text h1,
|
||||
.question-text h1 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.segment-text h2 {
|
||||
.segment-text h2,
|
||||
.question-text h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.segment-text h3 {
|
||||
.segment-text h3,
|
||||
.question-text h3 {
|
||||
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 ol {
|
||||
.segment-text ol,
|
||||
.question-text ul,
|
||||
.question-text ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.segment-text li {
|
||||
margin: 4px 0;
|
||||
line-height: 1.6;
|
||||
.segment-text li,
|
||||
.question-text li {
|
||||
line-height: 1;
|
||||
}
|
||||
.segment-text strong {
|
||||
.segment-text strong,
|
||||
.question-text strong {
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.segment-text em {
|
||||
.segment-text em,
|
||||
.question-text em {
|
||||
font-style: italic;
|
||||
}
|
||||
.segment-text a {
|
||||
.segment-text a,
|
||||
.question-text a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.segment-text a:hover {
|
||||
.segment-text a:hover,
|
||||
.question-text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.segment-text p {
|
||||
.segment-text p,
|
||||
.question-text p {
|
||||
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 {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
/* 低调显示的工具调用 - 移除边距和背景 */
|
||||
.segment-tool.low-profile {
|
||||
margin: 2px 0px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
.tool-segment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -400,7 +420,7 @@ export function getMessageAreaStyles(): string {
|
||||
display: block;
|
||||
}
|
||||
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
@ -416,6 +436,28 @@ export function getMessageAreaStyles(): string {
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-read-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-read-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-delete-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-syntax-check-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@ -438,6 +480,61 @@ export function getMessageAreaStyles(): string {
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-save-knowledge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-save-knowledge-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-simulation-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-simulation-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-waveform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-waveform-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-knowledge-load-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-knowledge-load-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-state-transition-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-state-transition-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-segment-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
@ -445,11 +542,28 @@ export function getMessageAreaStyles(): string {
|
||||
.tool-segment-content.collapsed {
|
||||
max-height: 0;
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
.segment-tool.low-profile .tool-segment-header {
|
||||
opacity: 0.65;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-icon {
|
||||
opacity: 0.55;
|
||||
font-size: 11px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-name {
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-result {
|
||||
opacity: 0.7;
|
||||
font-size: 10px;
|
||||
}
|
||||
.segment-question {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 14px;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.segment-question .question-text {
|
||||
@ -497,6 +611,7 @@ export function getMessageAreaStyles(): string {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.segment-question .custom-submit {
|
||||
padding: 8px 16px;
|
||||
@ -528,6 +643,12 @@ export function getMessageAreaStyles(): string {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;}
|
||||
|
||||
${getAgentCardStyles()}
|
||||
|
||||
${getPlanCardStyles()}
|
||||
|
||||
${getCodeHighlightStyles()}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||
@ -540,32 +661,143 @@ export function getMessageAreaScript(): string {
|
||||
// 工具图标定义
|
||||
const collapseIconSvg = \`${collapseIconSvg}\`;
|
||||
const fileWriteIconSvg = \`${fileWriteIconSvg}\`;
|
||||
const fileReadIconSvg = \`${fileReadIconSvg}\`;
|
||||
const fileDeleteIconSvg = \`${fileDeleteIconSvg}\`;
|
||||
const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`;
|
||||
const searchCodeIconSvg = \`${SearchCode}\`;
|
||||
const saveKnowledgeIconSvg = \`${saveKnowledgeIconSvg}\`;
|
||||
const simulationIconSvg = \`${simulationIconSvg}\`;
|
||||
const waveformIconSvg = \`${waveformIconSvg}\`;
|
||||
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
||||
const updateStageIconSvg = \`${updateStageIconSvg}\`;
|
||||
|
||||
${getAgentCardScript()}
|
||||
|
||||
${getPlanCardScript()}
|
||||
|
||||
// 解析多 VCD 文件路径
|
||||
function parseMultiVcdPaths(toolResult) {
|
||||
if (!toolResult) return [];
|
||||
const result = String(toolResult);
|
||||
|
||||
// 匹配 "- moduleName: path" 格式
|
||||
const vcdListMatch = result.match(/VCD 文件列表:[\\s\\S]*?(?=\\n\\n|$)/);
|
||||
if (!vcdListMatch) return [];
|
||||
|
||||
const paths = [];
|
||||
const lineRegex = /- (\\w+): ([^\\n]+)/g;
|
||||
let match;
|
||||
while ((match = lineRegex.exec(vcdListMatch[0])) !== null) {
|
||||
const name = match[1];
|
||||
const pathOrError = match[2].trim();
|
||||
// 跳过失败的条目
|
||||
if (!pathOrError.startsWith('失败')) {
|
||||
paths.push({ name: name + '.vcd', path: pathOrError });
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
// 获取工具图标
|
||||
function getToolIcon(toolName) {
|
||||
const iconMap = {
|
||||
'file_read': fileReadIconSvg,
|
||||
'file_write': fileWriteIconSvg,
|
||||
'file_delete': fileDeleteIconSvg,
|
||||
'file_list': searchCodeIconSvg,
|
||||
'syntax_check': syntaxCheckIconSvg,
|
||||
'simulation': simulationIconSvg,
|
||||
'waveform_summary': waveformIconSvg,
|
||||
'knowledge_save': saveKnowledgeIconSvg,
|
||||
'knowledge_load': knowledgeLoadIconSvg,
|
||||
'queryKnowledgeSummary': knowledgeLoadIconSvg,
|
||||
'queryRules': knowledgeLoadIconSvg,
|
||||
'setModule': fileWriteIconSvg,
|
||||
'addSignal': fileWriteIconSvg,
|
||||
'addSignalExample': fileWriteIconSvg,
|
||||
'validateKnowledgeGraph': syntaxCheckIconSvg,
|
||||
'querySignals': searchCodeIconSvg,
|
||||
'addPlan': fileWriteIconSvg,
|
||||
'addEdge': fileWriteIconSvg,
|
||||
'showPlan': searchCodeIconSvg,
|
||||
'addRule': fileWriteIconSvg,
|
||||
'updateNode': fileWriteIconSvg,
|
||||
'addStateTransition': stateTransitionIconSvg,
|
||||
'askUser': userQuestionIconSvg,
|
||||
'updatePhase': updateStageIconSvg,
|
||||
};
|
||||
return iconMap[toolName] || '';
|
||||
}
|
||||
|
||||
// 工具名称映射
|
||||
function getToolDisplayName(toolName) {
|
||||
const toolNameMap = {
|
||||
'file_read': '已完成文件读取',
|
||||
'file_write': '已完成文件写入',
|
||||
'file_delete': '已完成文件删除',
|
||||
'file_list': '已检索代码文件',
|
||||
'syntax_check': '已完成语法检查',
|
||||
'simulation': '已完成仿真',
|
||||
'waveform_summary': '已完成波形分析'
|
||||
'waveform_summary': '已完成波形分析',
|
||||
'knowledge_save': '已保存知识库',
|
||||
'knowledge_load': '已加载知识库',
|
||||
'queryKnowledgeSummary': '已查询知识摘要',
|
||||
'queryRules': '已查询规则',
|
||||
'setModule': '已设置模块',
|
||||
'addSignal': '信号分析完成',
|
||||
'addSignalExample': '信号示例处理完成',
|
||||
'validateKnowledgeGraph': '已验证知识图谱',
|
||||
'querySignals': '已查询信号',
|
||||
'addPlan': '已添加计划',
|
||||
'addEdge': '已添加边',
|
||||
'showPlan': '已显示计划',
|
||||
'addRule': '已添加规则',
|
||||
'updateNode': '已更新节点',
|
||||
'addStateTransition': '已添加状态转换',
|
||||
'spawnExplorer': '代码探索',
|
||||
'spawnDebugger': '波形调试',
|
||||
'askUser': '用户提问',
|
||||
'updatePhase': '已更新阶段',
|
||||
'iverilog': '已完成编译',
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
// 自动滚动控制标志
|
||||
let shouldAutoScroll = true;
|
||||
let lastScrollHeight = 0;
|
||||
|
||||
// 检查用户是否在底部附近(允许50px的误差)
|
||||
function isUserNearBottom() {
|
||||
const threshold = 50;
|
||||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||
}
|
||||
|
||||
// 智能滚动:只有用户在底部附近时才自动滚动
|
||||
// 监听用户滚动行为
|
||||
messagesEl.addEventListener('scroll', () => {
|
||||
const isAtBottom = isUserNearBottom();
|
||||
|
||||
// 如果用户滚动到底部,恢复自动滚动
|
||||
if (isAtBottom) {
|
||||
shouldAutoScroll = true;
|
||||
} else {
|
||||
// 只有当内容高度没有变化时,才认为是用户主动滚动
|
||||
// 如果内容高度变化了,说明是因为新内容导致的位置变化,不应该停止自动滚动
|
||||
if (messagesEl.scrollHeight === lastScrollHeight) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
});
|
||||
|
||||
// 智能滚动:只有在允许自动滚动时才滚动到底部
|
||||
function smartScrollToBottom() {
|
||||
if (isUserNearBottom()) {
|
||||
if (shouldAutoScroll) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@ -742,9 +974,13 @@ export function getMessageAreaScript(): string {
|
||||
// 存储已回答问题的状态
|
||||
const answeredQuestions = new Map(); // askId -> answer
|
||||
|
||||
// 存储工具展开/折叠状态
|
||||
const toolCollapseStates = new Map(); // index -> isCollapsed
|
||||
|
||||
// 实时更新分段消息(按后端返回顺序)
|
||||
function updateSegmentsRealtime(segments, isComplete) {
|
||||
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
||||
|
||||
if (!segments || segments.length === 0) {
|
||||
console.log('[WebView] segments 为空,跳过渲染');
|
||||
return;
|
||||
@ -773,10 +1009,42 @@ export function getMessageAreaScript(): string {
|
||||
messagesEl.appendChild(currentSegmentedMessage);
|
||||
}
|
||||
|
||||
// 保存当前所有工具的展开/折叠状态
|
||||
if (currentSegmentedMessage) {
|
||||
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
|
||||
toolHeaders.forEach((header, idx) => {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
toolCollapseStates.set(idx, isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
// 清空容器并重新渲染所有段落
|
||||
currentSegmentedMessage.innerHTML = '';
|
||||
|
||||
segments.forEach((segment, index) => {
|
||||
// 合并连续相同的工具调用
|
||||
const mergedSegments = [];
|
||||
let i = 0;
|
||||
while (i < segments.length) {
|
||||
const segment = segments[i];
|
||||
if (segment.type === 'tool') {
|
||||
// 统计连续相同的工具调用
|
||||
let count = 1;
|
||||
while (i + count < segments.length &&
|
||||
segments[i + count].type === 'tool' &&
|
||||
segments[i + count].toolName === segment.toolName) {
|
||||
count++;
|
||||
}
|
||||
// 添加合并后的段落(带计数)
|
||||
mergedSegments.push({ ...segment, toolCount: count });
|
||||
i += count;
|
||||
} else {
|
||||
mergedSegments.push(segment);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
let toolIndex = 0; // 用于跟踪工具段落的索引
|
||||
mergedSegments.forEach((segment, index) => {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||
|
||||
@ -784,27 +1052,50 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.className += ' segment-text';
|
||||
segmentDiv.innerHTML = formatText(segment.content);
|
||||
} else if (segment.type === 'tool') {
|
||||
// 过滤掉不需要显示的工具
|
||||
if (segment.toolName === 'spawnExplorer') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有工具调用都使用低调样式
|
||||
segmentDiv.className += ' low-profile';
|
||||
|
||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
|
||||
// 检查工具结果是否过长(超过一行显示不下)
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
|
||||
// 恢复之前保存的展开/折叠状态
|
||||
const savedState = toolCollapseStates.get(toolIndex);
|
||||
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
|
||||
const currentToolIndex = toolIndex;
|
||||
toolIndex++; // 递增工具索引
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
||||
\${shouldCollapse ? collapseIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
|
||||
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
||||
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\`;
|
||||
|
||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
// 优先使用显式提供的路径,否则从结果文本中解析
|
||||
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
|
||||
if (vcdPaths.length > 0) {
|
||||
// 多 VCD 模式:为每个文件创建预览
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
});
|
||||
} else {
|
||||
// 单 VCD 模式(兼容旧逻辑)
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||
@ -819,33 +1110,31 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加折叠/展开事件监听
|
||||
if (shouldCollapse) {
|
||||
setTimeout(() => {
|
||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
|
||||
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
|
||||
|
||||
if (header && content) {
|
||||
header.addEventListener('click', function() {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||
|
||||
if (isCollapsed) {
|
||||
// 展开
|
||||
header.classList.remove('collapsed');
|
||||
content.classList.remove('collapsed');
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
if (iconCollapsed) iconCollapsed.style.display = 'none';
|
||||
if (iconExpanded) iconExpanded.style.display = 'block';
|
||||
toolCollapseStates.set(toolIdx, false);
|
||||
} else {
|
||||
// 折叠
|
||||
header.classList.add('collapsed');
|
||||
content.classList.add('collapsed');
|
||||
content.style.maxHeight = '0';
|
||||
if (iconCollapsed) iconCollapsed.style.display = 'block';
|
||||
if (iconExpanded) iconExpanded.style.display = 'none';
|
||||
toolCollapseStates.set(toolIdx, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -873,7 +1162,7 @@ export function getMessageAreaScript(): string {
|
||||
: '';
|
||||
|
||||
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>\` : ''}
|
||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
|
||||
@ -916,6 +1205,12 @@ export function getMessageAreaScript(): string {
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'agent') {
|
||||
// 智能体卡片渲染
|
||||
renderAgentCard(segment, segmentDiv);
|
||||
} else if (segment.type === 'plan') {
|
||||
// 计划卡片渲染(使用独立组件)
|
||||
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||||
}
|
||||
|
||||
currentSegmentedMessage.appendChild(segmentDiv);
|
||||
@ -926,9 +1221,11 @@ export function getMessageAreaScript(): string {
|
||||
console.log('[WebView] 对话完成,添加操作按钮');
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
|
||||
// 复制按钮
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
@ -936,7 +1233,22 @@ export function getMessageAreaScript(): string {
|
||||
.join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
|
||||
// 点赞按钮
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
|
||||
// 点踩按钮
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
currentSegmentedMessage.appendChild(actionsDiv);
|
||||
|
||||
// 重置当前分段消息容器
|
||||
@ -974,7 +1286,29 @@ export function getMessageAreaScript(): string {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'message bot-message segmented-message';
|
||||
|
||||
segments.forEach((segment, index) => {
|
||||
// 合并连续相同的工具调用
|
||||
const mergedSegments = [];
|
||||
let i = 0;
|
||||
while (i < segments.length) {
|
||||
const segment = segments[i];
|
||||
if (segment.type === 'tool') {
|
||||
// 统计连续相同的工具调用
|
||||
let count = 1;
|
||||
while (i + count < segments.length &&
|
||||
segments[i + count].type === 'tool' &&
|
||||
segments[i + count].toolName === segment.toolName) {
|
||||
count++;
|
||||
}
|
||||
// 添加合并后的段落(带计数)
|
||||
mergedSegments.push({ ...segment, toolCount: count });
|
||||
i += count;
|
||||
} else {
|
||||
mergedSegments.push(segment);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
mergedSegments.forEach((segment, index) => {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||
|
||||
@ -982,19 +1316,26 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.className += ' segment-text';
|
||||
segmentDiv.innerHTML = formatText(segment.content);
|
||||
} else if (segment.type === 'tool') {
|
||||
// 过滤掉不需要显示的工具
|
||||
if (segment.toolName === 'spawnExplorer') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有工具调用都使用低调样式
|
||||
segmentDiv.className += ' low-profile';
|
||||
|
||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
|
||||
// 检查工具结果是否过长(超过一行显示不下)
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
||||
\${shouldCollapse ? collapseIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
|
||||
\${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
@ -1002,7 +1343,17 @@ export function getMessageAreaScript(): string {
|
||||
|
||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
// 优先使用显式提供的路径,否则从结果文本中解析
|
||||
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
|
||||
if (vcdPaths.length > 0) {
|
||||
// 多 VCD 模式:为每个文件创建预览
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
});
|
||||
} else {
|
||||
// 单 VCD 模式(兼容旧逻辑)
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||
@ -1017,33 +1368,31 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加折叠/展开事件监听
|
||||
if (shouldCollapse) {
|
||||
setTimeout(() => {
|
||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
|
||||
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
|
||||
|
||||
if (header && content) {
|
||||
header.addEventListener('click', function() {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||
|
||||
if (isCollapsed) {
|
||||
// 展开
|
||||
header.classList.remove('collapsed');
|
||||
content.classList.remove('collapsed');
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
if (iconCollapsed) iconCollapsed.style.display = 'none';
|
||||
if (iconExpanded) iconExpanded.style.display = 'block';
|
||||
toolCollapseStates.set(toolIdx, false);
|
||||
} else {
|
||||
// 折叠
|
||||
header.classList.add('collapsed');
|
||||
content.classList.add('collapsed');
|
||||
content.style.maxHeight = '0';
|
||||
if (iconCollapsed) iconCollapsed.style.display = 'block';
|
||||
if (iconExpanded) iconExpanded.style.display = 'none';
|
||||
toolCollapseStates.set(toolIdx, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1052,12 +1401,18 @@ export function getMessageAreaScript(): string {
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="question-segment">
|
||||
<div class="question-text">\${segment.question || ''}</div>
|
||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||||
<div class="question-options">
|
||||
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
} else if (segment.type === 'agent') {
|
||||
// 智能体卡片渲染
|
||||
renderAgentCard(segment, segmentDiv);
|
||||
} else if (segment.type === 'plan') {
|
||||
// 计划卡片渲染(使用独立组件)
|
||||
renderPlanCardStatic(segment, segmentDiv);
|
||||
}
|
||||
|
||||
container.appendChild(segmentDiv);
|
||||
@ -1066,9 +1421,11 @@ export function getMessageAreaScript(): string {
|
||||
// 添加操作按钮
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
|
||||
// 复制按钮
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
@ -1076,7 +1433,22 @@ export function getMessageAreaScript(): string {
|
||||
.join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
|
||||
// 点赞按钮
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
|
||||
// 点踩按钮
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
container.appendChild(actionsDiv);
|
||||
|
||||
messagesEl.appendChild(container);
|
||||
@ -1087,20 +1459,40 @@ export function getMessageAreaScript(): string {
|
||||
function formatText(text) {
|
||||
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, '>');
|
||||
|
||||
// 处理代码块(三个反引号包裹的代码)
|
||||
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
|
||||
const language = lang || 'plaintext';
|
||||
return '<pre><code class="language-' + language + '">' + code.trim() + '</code></pre>';
|
||||
// 不再手动高亮,让 highlight.js 处理
|
||||
const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`;
|
||||
codeBlocks.push('<pre><code class="language-' + language + '">' + escapedCode + '</code></pre>');
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 处理行内代码(单个反引号包裹)
|
||||
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
|
||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||
@ -1123,9 +1515,19 @@ export function getMessageAreaScript(): string {
|
||||
// 处理链接 [text](url)
|
||||
html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
// 处理换行
|
||||
// 处理换行(在恢复代码块之前)
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1275,5 +1677,7 @@ export function getMessageAreaScript(): string {
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
|
||||
${getCodeHighlightScript()}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -5,7 +5,12 @@
|
||||
/**
|
||||
* 获取模型选择器的 HTML 内容
|
||||
*/
|
||||
export function getModelSelectorContent(): string {
|
||||
export function getModelSelectorContent(
|
||||
autoIcon: string = "",
|
||||
liteIcon: string = "",
|
||||
syIcon: string = "",
|
||||
maxIcon: string = ""
|
||||
): string {
|
||||
return `
|
||||
<!-- 模型选择 -->
|
||||
<div class="tooltip">
|
||||
@ -17,13 +22,51 @@ export function getModelSelectorContent(): string {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="select-dropdown" id="modelDropdown">
|
||||
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">Lite</div>
|
||||
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">Auto</div>
|
||||
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">Syntaxic</div>
|
||||
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">Max</div>
|
||||
<div class="select-option selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
|
||||
${
|
||||
autoIcon
|
||||
? `<img src="${autoIcon}" class="model-icon" alt="Auto">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Auto</span>
|
||||
<span class="option-desc">智能匹配最优模型</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
|
||||
${
|
||||
liteIcon
|
||||
? `<img src="${liteIcon}" class="model-icon" alt="Lite">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Lite</span>
|
||||
<span class="option-desc">基础模型,快速相应,适合简单任务</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
|
||||
${
|
||||
syIcon
|
||||
? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Syntaxic</span>
|
||||
<span class="option-desc">均衡成本和性能,节省credits同时保持可靠输出</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
|
||||
${
|
||||
maxIcon
|
||||
? `<img src="${maxIcon}" class="model-icon" alt="Max">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Max</span>
|
||||
<span class="option-desc">最强性能,质量优先,适合复杂任务</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型选择器的 tooltip 容器 -->
|
||||
<div id="modelTooltip" class="model-tooltip"></div>
|
||||
</div>
|
||||
<span class="tooltiptext">选择模型</span>
|
||||
</div>
|
||||
@ -87,11 +130,13 @@ export function getModelSelectorStyles(): string {
|
||||
/* 模型选择器的选项样式 */
|
||||
#modelDropdown .select-option {
|
||||
position: relative;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
#modelDropdown .select-option:hover {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
@ -100,49 +145,28 @@ export function getModelSelectorStyles(): string {
|
||||
background: rgba(128, 128, 128, 0.5);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
/* 模型选择器的 tooltip 样式 */
|
||||
.model-tooltip {
|
||||
position: fixed;
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
.model-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
.option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
.option-label {
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
}
|
||||
.model-tooltip.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
/* tooltip 箭头 */
|
||||
.model-tooltip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 7px;
|
||||
border-style: solid;
|
||||
border-color: transparent rgba(255, 255, 255, 0.2) transparent transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
.model-tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent #1e1e1e transparent transparent;
|
||||
margin-right: 1px;
|
||||
.option-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -205,46 +229,5 @@ export function getModelSelectorScript(): string {
|
||||
function getCurrentModel() {
|
||||
return currentModel;
|
||||
}
|
||||
|
||||
// 模型选择器 tooltip 功能
|
||||
(function initModelTooltip() {
|
||||
const modelDropdown = document.getElementById('modelDropdown');
|
||||
const modelTooltip = document.getElementById('modelTooltip');
|
||||
|
||||
if (!modelDropdown || !modelTooltip) return;
|
||||
|
||||
// 为每个选项添加鼠标事件
|
||||
const options = modelDropdown.querySelectorAll('.select-option');
|
||||
|
||||
options.forEach(option => {
|
||||
option.addEventListener('mouseenter', function(e) {
|
||||
const tooltipText = this.getAttribute('data-tooltip');
|
||||
if (!tooltipText) return;
|
||||
|
||||
// 设置 tooltip 内容
|
||||
modelTooltip.textContent = tooltipText;
|
||||
|
||||
// 获取选项的位置
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
// 计算 tooltip 位置(在选项右侧)
|
||||
const tooltipRect = modelTooltip.getBoundingClientRect();
|
||||
const left = rect.right + 12;
|
||||
const top = rect.top + (rect.height / 2) - (tooltipRect.height / 2);
|
||||
|
||||
// 设置位置
|
||||
modelTooltip.style.left = left + 'px';
|
||||
modelTooltip.style.top = top + 'px';
|
||||
|
||||
// 显示 tooltip
|
||||
modelTooltip.classList.add('show');
|
||||
});
|
||||
|
||||
option.addEventListener('mouseleave', function() {
|
||||
// 隐藏 tooltip
|
||||
modelTooltip.classList.remove('show');
|
||||
});
|
||||
});
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
394
src/views/moreOptionsComponent.ts
Normal file
@ -0,0 +1,394 @@
|
||||
/**
|
||||
* 更多选项组件
|
||||
* 包含用户手册和用户反馈入口
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取更多选项组件的 HTML 内容
|
||||
*/
|
||||
export function getMoreOptionsComponentContent(): string {
|
||||
return `
|
||||
<div class="more-options-wrapper">
|
||||
<!-- 更多选项下拉面板 -->
|
||||
<div class="more-options-dropdown" id="moreOptionsDropdown">
|
||||
<div class="more-options-content">
|
||||
<div class="more-options-header">
|
||||
<span class="more-options-title">更多选项</span>
|
||||
</div>
|
||||
|
||||
<div class="more-options-body">
|
||||
<div class="more-option-item" id="userManualOption">
|
||||
<div class="option-icon">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<div class="option-label">用户手册</div>
|
||||
<div class="option-desc">查看使用文档和帮助</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="more-option-item" id="userFeedbackOption">
|
||||
<div class="option-icon">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<div class="option-label">用户反馈</div>
|
||||
<div class="option-desc">提交问题和建议</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户反馈二维码弹窗 -->
|
||||
<div class="feedback-qrcode-modal" id="feedbackQRCodeModal">
|
||||
<div class="feedback-qrcode-overlay" onclick="closeFeedbackQRCode()"></div>
|
||||
<div class="feedback-qrcode-content">
|
||||
<div class="feedback-qrcode-header">
|
||||
<span class="feedback-qrcode-title">用户反馈</span>
|
||||
<button class="feedback-qrcode-close" onclick="closeFeedbackQRCode()">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="feedback-qrcode-body">
|
||||
<img class="feedback-qrcode-image" id="feedbackQRCodeImage" alt="微信二维码" />
|
||||
<p class="feedback-qrcode-text">扫描二维码添加微信反馈</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更多选项组件的 CSS 样式
|
||||
*/
|
||||
export function getMoreOptionsComponentStyles(): string {
|
||||
return `
|
||||
.more-options-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 更多选项下拉面板 */
|
||||
.more-options-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.more-options-dropdown.active {
|
||||
display: block;
|
||||
animation: dropdownSlideIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.more-options-content {
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.more-options-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.more-options-body {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.more-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.more-option-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.more-option-item:active {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.option-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 用户反馈二维码弹窗 */
|
||||
.feedback-qrcode-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feedback-qrcode-modal.active {
|
||||
display: flex;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-qrcode-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.feedback-qrcode-content {
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
animation: slideUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-qrcode-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.feedback-qrcode-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.feedback-qrcode-close {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.feedback-qrcode-close:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.feedback-qrcode-close svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.feedback-qrcode-body {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.feedback-qrcode-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.feedback-qrcode-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更多选项组件的 JavaScript 脚本
|
||||
*/
|
||||
export function getMoreOptionsComponentScript(): string {
|
||||
return `
|
||||
// 切换更多选项下拉面板
|
||||
function toggleMoreOptionsDropdown() {
|
||||
const dropdown = document.getElementById('moreOptionsDropdown');
|
||||
const moreButton = document.querySelector('.more-button');
|
||||
|
||||
if (dropdown) {
|
||||
const isActive = dropdown.classList.contains('active');
|
||||
if (isActive) {
|
||||
dropdown.classList.remove('active');
|
||||
if (moreButton) {
|
||||
moreButton.classList.remove('active');
|
||||
}
|
||||
} else {
|
||||
dropdown.classList.add('active');
|
||||
if (moreButton) {
|
||||
moreButton.classList.add('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭更多选项下拉面板
|
||||
function closeMoreOptionsDropdown() {
|
||||
const dropdown = document.getElementById('moreOptionsDropdown');
|
||||
const moreButton = document.querySelector('.more-button');
|
||||
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('active');
|
||||
}
|
||||
if (moreButton) {
|
||||
moreButton.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 打开用户手册
|
||||
function openUserManual() {
|
||||
console.log('打开用户手册');
|
||||
vscode.postMessage({ command: 'openUserManual' });
|
||||
closeMoreOptionsDropdown();
|
||||
}
|
||||
|
||||
// 打开用户反馈
|
||||
function openUserFeedback() {
|
||||
console.log('打开用户反馈');
|
||||
vscode.postMessage({ command: 'openUserFeedback' });
|
||||
closeMoreOptionsDropdown();
|
||||
}
|
||||
|
||||
// 显示用户反馈二维码弹窗
|
||||
function showFeedbackQRCode() {
|
||||
const modal = document.getElementById('feedbackQRCodeModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭用户反馈二维码弹窗
|
||||
function closeFeedbackQRCode() {
|
||||
const modal = document.getElementById('feedbackQRCodeModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定更多选项事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 绑定用户手册选项
|
||||
const userManualOption = document.getElementById('userManualOption');
|
||||
if (userManualOption) {
|
||||
userManualOption.addEventListener('click', openUserManual);
|
||||
}
|
||||
|
||||
// 绑定用户反馈选项
|
||||
const userFeedbackOption = document.getElementById('userFeedbackOption');
|
||||
if (userFeedbackOption) {
|
||||
userFeedbackOption.addEventListener('click', openUserFeedback);
|
||||
}
|
||||
|
||||
// 点击页面其他地方关闭下拉面板
|
||||
document.addEventListener('click', (e) => {
|
||||
const dropdown = document.getElementById('moreOptionsDropdown');
|
||||
const moreButton = document.querySelector('.more-button');
|
||||
const moreContainer = document.querySelector('.more-container');
|
||||
|
||||
if (dropdown && dropdown.classList.contains('active')) {
|
||||
// 如果点击的不是更多按钮和下拉面板内容,则关闭
|
||||
if (!moreContainer?.contains(e.target)) {
|
||||
closeMoreOptionsDropdown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止下拉面板内容点击事件冒泡
|
||||
const dropdownContent = document.querySelector('.more-options-content');
|
||||
if (dropdownContent) {
|
||||
dropdownContent.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
@ -60,35 +60,97 @@ export function getOptimizeButtonScript(): string {
|
||||
return `
|
||||
let isOptimized = false; // 标记是否已优化
|
||||
let originalText = ''; // 保存原始文本用于撤回
|
||||
let isOptimizing = false; // 标记是否正在优化中
|
||||
|
||||
function handleOptimize() {
|
||||
console.log('[Optimize] handleOptimize 被调用');
|
||||
console.log('[Optimize] isOptimizing:', isOptimizing);
|
||||
console.log('[Optimize] isOptimized:', isOptimized);
|
||||
console.log('[Optimize] messageInput:', messageInput);
|
||||
|
||||
if (isOptimizing) {
|
||||
console.log('[Optimize] 正在优化中,忽略点击');
|
||||
return; // 正在优化中,忽略点击
|
||||
}
|
||||
|
||||
if (isOptimized) {
|
||||
// 撤回操作
|
||||
console.log('[Optimize] 执行撤回操作');
|
||||
messageInput.value = originalText;
|
||||
resetOptimizeButton();
|
||||
} else {
|
||||
// 优化操作
|
||||
const currentText = messageInput.value.trim();
|
||||
console.log('[Optimize] 当前输入内容:', currentText);
|
||||
console.log('[Optimize] 内容长度:', currentText.length);
|
||||
|
||||
if (!currentText) {
|
||||
console.log('[Optimize] 输入框为空,不执行优化');
|
||||
return; // 输入框为空,不执行优化
|
||||
}
|
||||
|
||||
originalText = messageInput.value; // 保存原始文本
|
||||
isOptimizing = true;
|
||||
console.log('[Optimize] 开始优化,显示加载状态');
|
||||
|
||||
// 使用死数据替换输入框内容
|
||||
const optimizedTexts = [
|
||||
'请帮我优化这段代码,提高性能和可读性',
|
||||
'请分析这个问题并给出最佳解决方案',
|
||||
'请帮我重构这段代码,使其更加简洁高效',
|
||||
'请检查代码中的潜在问题并提供改进建议'
|
||||
];
|
||||
const randomText = optimizedTexts[Math.floor(Math.random() * optimizedTexts.length)];
|
||||
messageInput.value = randomText;
|
||||
// 显示加载状态
|
||||
showOptimizeLoading();
|
||||
|
||||
// 切换到撤回状态
|
||||
isOptimized = true;
|
||||
updateOptimizeButton();
|
||||
// 发送优化请求到扩展
|
||||
console.log('[Optimize] 发送 optimizePrompt 消息');
|
||||
vscode.postMessage({
|
||||
command: 'optimizePrompt',
|
||||
prompt: currentText
|
||||
});
|
||||
console.log('[Optimize] postMessage 已发送');
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
autoResizeTextarea();
|
||||
}
|
||||
|
||||
// 处理优化结果
|
||||
function handleOptimizeResult(success, optimizedPrompt, error) {
|
||||
isOptimizing = false;
|
||||
hideOptimizeLoading();
|
||||
|
||||
if (success && optimizedPrompt) {
|
||||
messageInput.value = optimizedPrompt;
|
||||
isOptimized = true;
|
||||
updateOptimizeButton();
|
||||
} else {
|
||||
// 优化失败,恢复原始文本
|
||||
messageInput.value = originalText;
|
||||
console.error('优化失败:', error);
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
autoResizeTextarea();
|
||||
}
|
||||
|
||||
function showOptimizeLoading() {
|
||||
const optimizeButton = document.getElementById('optimizeButton');
|
||||
const optimizeIcon = document.getElementById('optimizeIcon');
|
||||
if (optimizeButton && optimizeIcon) {
|
||||
optimizeButton.disabled = true;
|
||||
optimizeButton.style.opacity = '0.5';
|
||||
// 显示加载动画
|
||||
optimizeIcon.innerHTML = '<circle cx="512" cy="512" r="400" fill="none" stroke="#409eff" stroke-width="60" stroke-dasharray="1200" stroke-dashoffset="0"><animateTransform attributeName="transform" type="rotate" from="0 512 512" to="360 512 512" dur="1s" repeatCount="indefinite"/></circle>';
|
||||
}
|
||||
}
|
||||
|
||||
function hideOptimizeLoading() {
|
||||
const optimizeButton = document.getElementById('optimizeButton');
|
||||
if (optimizeButton) {
|
||||
optimizeButton.disabled = false;
|
||||
optimizeButton.style.opacity = '1';
|
||||
}
|
||||
// 恢复图标会在 updateOptimizeButton 或 resetOptimizeButton 中处理
|
||||
if (!isOptimized) {
|
||||
resetOptimizeButton();
|
||||
}
|
||||
}
|
||||
|
||||
function updateOptimizeButton() {
|
||||
const optimizeIcon = document.getElementById('optimizeIcon');
|
||||
const optimizeTooltip = document.getElementById('optimizeTooltip');
|
||||
|
||||
793
src/views/planCard.ts
Normal file
@ -0,0 +1,793 @@
|
||||
/**
|
||||
* 计划卡片组件
|
||||
*
|
||||
* 功能说明:
|
||||
* - 显示执行计划的卡片界面
|
||||
* - 包含计划标题、摘要和步骤列表
|
||||
* - 摘要支持 Markdown 格式渲染
|
||||
* - 提供确认执行、修改计划、取消等操作按钮
|
||||
*/
|
||||
|
||||
import { plannerIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取计划卡片的样式
|
||||
*/
|
||||
export function getPlanCardStyles(): string {
|
||||
return `
|
||||
/* 计划卡片样式 */
|
||||
.segment-plan {
|
||||
margin: 12px 0;
|
||||
}
|
||||
.plan-card {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.plan-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-bottom: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
.plan-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.plan-body {
|
||||
padding: 16px;
|
||||
}
|
||||
.plan-summary {
|
||||
color: var(--vscode-foreground);
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* Markdown 渲染样式 */
|
||||
.plan-summary h1, .plan-summary h2, .plan-summary h3, .plan-summary h4 {
|
||||
margin: 16px 0 8px 0;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.plan-summary h1 { font-size: 18px; border-bottom: 1px solid var(--vscode-input-border); padding-bottom: 6px; }
|
||||
.plan-summary h2 { font-size: 16px; }
|
||||
.plan-summary h3 { font-size: 14px; }
|
||||
.plan-summary h4 { font-size: 13px; }
|
||||
.plan-summary p { margin: 8px 0; }
|
||||
.plan-summary ul, .plan-summary ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.plan-summary li { margin: 4px 0 4px 27px; }
|
||||
.plan-summary code {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 12px;
|
||||
}
|
||||
.plan-summary pre {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.plan-summary pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.plan-summary table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.plan-summary th, .plan-summary td {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.plan-summary th {
|
||||
background: var(--vscode-sideBar-background);
|
||||
font-weight: 600;
|
||||
}
|
||||
.plan-summary strong { font-weight: 600; }
|
||||
.plan-summary em { font-style: italic; }
|
||||
.plan-steps {
|
||||
font-size: 13px;
|
||||
}
|
||||
.plan-step {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.plan-step strong {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
.step-details {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.plan-step:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.step-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
border: 2px solid var(--vscode-textLink-foreground);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.step-checkbox.completed {
|
||||
background: var(--vscode-textLink-foreground);
|
||||
border-color: var(--vscode-textLink-foreground);
|
||||
opacity: 1;
|
||||
}
|
||||
.step-checkbox.completed::after {
|
||||
content: '✓';
|
||||
color: var(--vscode-editor-background);
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-sideBar-background);
|
||||
}
|
||||
.plan-input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.plan-input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.plan-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
.plan-btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.plan-btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.plan-btn-submit {
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-submit:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.plan-btn-confirm {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
.plan-btn-confirm:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.plan-btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-cancel:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.plan-answered {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-sideBar-background);
|
||||
font-size: 13px;
|
||||
}
|
||||
.answered-label {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.answered-value {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 阶段进度条样式 */
|
||||
.phase-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-bottom: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.phase-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.phase-item.current {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
.phase-item.completed {
|
||||
color: #4caf50;
|
||||
}
|
||||
.phase-item.skipped {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.phase-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-input-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.phase-dot.current {
|
||||
background: var(--vscode-textLink-foreground);
|
||||
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.2);
|
||||
}
|
||||
.phase-dot.completed {
|
||||
background: #4caf50;
|
||||
}
|
||||
.phase-dot.skipped {
|
||||
background: var(--vscode-descriptionForeground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.phase-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: var(--vscode-input-border);
|
||||
margin: 0 8px;
|
||||
}
|
||||
.phase-line.completed {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
/* 阶段列表样式 */
|
||||
.plan-phases {
|
||||
font-size: 13px;
|
||||
}
|
||||
.plan-phase {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.plan-phase:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.phase-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.phase-header:hover {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
.phase-toggle {
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.phase-toggle.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.phase-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
.phase-status {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
}
|
||||
.phase-status.current {
|
||||
background: var(--vscode-textLink-foreground);
|
||||
color: white;
|
||||
}
|
||||
.phase-status.skipped {
|
||||
background: var(--vscode-descriptionForeground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.phase-status.completed {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
.phase-content {
|
||||
padding: 0 12px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
}
|
||||
.phase-content.expanded {
|
||||
padding: 12px;
|
||||
max-height: 500px;
|
||||
}
|
||||
.phase-reason {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.phase-steps {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.phase-step-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.phase-step-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.phase-step-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid var(--vscode-textLink-foreground);
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.phase-step-text {
|
||||
flex: 1;
|
||||
}
|
||||
.phase-step-name {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.phase-step-desc {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-top: 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计划卡片的脚本
|
||||
*/
|
||||
export function getPlanCardScript(): string {
|
||||
return `
|
||||
// 简单的 Markdown 渲染函数
|
||||
function renderPlanMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
let html = text;
|
||||
|
||||
// 转义 HTML 特殊字符(保留换行)
|
||||
html = html.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 标题(必须在转义之后、其他处理之前)
|
||||
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
||||
|
||||
// 代码块 (\`\`\`code\`\`\`)
|
||||
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
|
||||
|
||||
// 行内代码 (\`code\`)
|
||||
html = html.replace(/\\x60([^\\x60]+)\\x60/g, '<code>$1</code>');
|
||||
|
||||
// 表格处理
|
||||
html = html.replace(/^\\|(.+)\\|\\s*\\n\\|[-:\\s|]+\\|\\s*\\n((?:\\|.+\\|\\s*\\n?)+)/gm, function(match, header, body) {
|
||||
const headers = header.split('|').map(h => h.trim()).filter(h => h);
|
||||
const rows = body.trim().split('\\n').map(row =>
|
||||
row.split('|').map(cell => cell.trim()).filter(cell => cell)
|
||||
);
|
||||
|
||||
let table = '<table><thead><tr>';
|
||||
headers.forEach(h => table += '<th>' + h + '</th>');
|
||||
table += '</tr></thead><tbody>';
|
||||
rows.forEach(row => {
|
||||
table += '<tr>';
|
||||
row.forEach(cell => table += '<td>' + cell + '</td>');
|
||||
table += '</tr>';
|
||||
});
|
||||
table += '</tbody></table>';
|
||||
return table;
|
||||
});
|
||||
|
||||
// 粗体和斜体
|
||||
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
||||
|
||||
// 无序列表
|
||||
html = html.replace(/^[\\s]*[-*] (.+)$/gm, '<li>$1</li>');
|
||||
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
|
||||
|
||||
// 有序列表
|
||||
html = html.replace(/^[\\s]*\\d+\\. (.+)$/gm, '<li>$1</li>');
|
||||
|
||||
// 段落(连续的非空行)
|
||||
html = html.replace(/^(?!<[hupolt]|$)(.+)$/gm, '<p>$1</p>');
|
||||
|
||||
// 清理多余的空行
|
||||
html = html.replace(/<p><\\/p>/g, '');
|
||||
html = html.replace(/\\n{2,}/g, '\\n');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 解析并渲染步骤列表
|
||||
function renderPlanSteps(steps) {
|
||||
if (!steps || steps.length === 0) return '';
|
||||
|
||||
// 尝试解析 JSON 格式的步骤
|
||||
let parsedSteps = steps;
|
||||
|
||||
// 如果是单个字符串且看起来像 JSON 数组,尝试解析
|
||||
if (steps.length === 1 && typeof steps[0] === 'string') {
|
||||
const str = steps[0].trim();
|
||||
if (str.startsWith('[') && str.endsWith(']')) {
|
||||
try {
|
||||
parsedSteps = JSON.parse(str);
|
||||
} catch (e) {
|
||||
// 解析失败,保持原样
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsedSteps.map((step, i) => {
|
||||
// 如果是对象,格式化显示
|
||||
if (typeof step === 'object' && step !== null) {
|
||||
const name = step.name || step.id || ('步骤 ' + (i + 1));
|
||||
const desc = step.description || '';
|
||||
const inputs = step.inputs || '';
|
||||
const outputs = step.outputs || '';
|
||||
const logic = step.logic || '';
|
||||
|
||||
let content = '<strong>' + name + '</strong>';
|
||||
if (desc) content += ':' + desc;
|
||||
|
||||
let details = [];
|
||||
if (inputs) details.push('输入: ' + inputs);
|
||||
if (outputs) details.push('输出: ' + outputs);
|
||||
if (logic) details.push('逻辑: ' + logic);
|
||||
|
||||
if (details.length > 0) {
|
||||
content += '<div class="step-details">' + details.join(' | ') + '</div>';
|
||||
}
|
||||
|
||||
return '<div class="plan-step"><span class="step-checkbox"></span>' + content + '</div>';
|
||||
}
|
||||
|
||||
// 普通字符串
|
||||
return '<div class="plan-step"><span class="step-checkbox"></span> ' + step + '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 渲染阶段进度条
|
||||
function renderPhaseProgress(phases) {
|
||||
if (!phases || phases.length === 0) return '';
|
||||
|
||||
const phaseNames = { spec: 'Spec', design: 'Design', sim: 'Sim', done: 'Done' };
|
||||
let html = '<div class="phase-progress">';
|
||||
|
||||
phases.forEach((phase, i) => {
|
||||
const name = phaseNames[phase.id] || phase.name || phase.id;
|
||||
const status = phase.status || 'pending';
|
||||
|
||||
html += \`<div class="phase-item \${status}">
|
||||
<span class="phase-dot \${status}"></span>
|
||||
<span>\${name}</span>
|
||||
</div>\`;
|
||||
|
||||
// 添加连接线(最后一个不加)
|
||||
if (i < phases.length - 1) {
|
||||
const lineStatus = (status === 'completed' || status === 'skipped') ? 'completed' : '';
|
||||
html += \`<div class="phase-line \${lineStatus}"></div>\`;
|
||||
}
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// 渲染阶段列表(两级结构)
|
||||
function renderPlanPhases(phases) {
|
||||
if (!phases || phases.length === 0) return '';
|
||||
|
||||
const statusLabels = {
|
||||
skipped: '跳过',
|
||||
completed: '已完成',
|
||||
current: '当前',
|
||||
pending: '待执行'
|
||||
};
|
||||
|
||||
return phases.map((phase, i) => {
|
||||
const status = phase.status || 'pending';
|
||||
const statusLabel = statusLabels[status] || status;
|
||||
const isExpanded = status === 'current';
|
||||
const hasSteps = phase.steps && phase.steps.length > 0;
|
||||
const hasReason = phase.reason && status === 'skipped';
|
||||
|
||||
let stepsHtml = '';
|
||||
if (phase.steps && phase.steps.length > 0) {
|
||||
stepsHtml = phase.steps.map(step => \`
|
||||
<li class="phase-step-item">
|
||||
<span class="phase-step-checkbox"></span>
|
||||
<div class="phase-step-text">
|
||||
<div class="phase-step-name">\${step.name || ''}</div>
|
||||
\${step.description ? \`<div class="phase-step-desc">\${step.description}</div>\` : ''}
|
||||
</div>
|
||||
</li>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
return \`
|
||||
<div class="plan-phase" data-phase-id="\${phase.id}">
|
||||
<div class="phase-header" onclick="togglePhase(this)">
|
||||
<span class="phase-toggle \${isExpanded ? 'expanded' : ''}">▶</span>
|
||||
<span class="phase-name">\${phase.name || phase.id}</span>
|
||||
<span class="phase-status \${status}">\${statusLabel}</span>
|
||||
</div>
|
||||
<div class="phase-content \${isExpanded ? 'expanded' : ''}">
|
||||
\${hasReason ? \`<div class="phase-reason">\${phase.reason}</div>\` : ''}
|
||||
\${hasSteps ? \`<ul class="phase-steps">\${stepsHtml}</ul>\` : ''}
|
||||
\${!hasSteps && !hasReason ? '<div class="phase-reason">暂无步骤</div>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 切换阶段展开/折叠
|
||||
function togglePhase(header) {
|
||||
const toggle = header.querySelector('.phase-toggle');
|
||||
const content = header.nextElementSibling;
|
||||
toggle.classList.toggle('expanded');
|
||||
content.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
// 渲染计划卡片(在 updateSegmentsRealtime 中使用)
|
||||
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
|
||||
// 检查是否已回答
|
||||
const isAnswered = answeredQuestions.has(segment.askId);
|
||||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||||
|
||||
if (isAnswered) {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
// 判断是否有 phases(新格式)还是 steps(旧格式)
|
||||
const hasPhases = segment.planPhases && segment.planPhases.length > 0;
|
||||
|
||||
// 渲染阶段进度条和阶段列表(新格式)
|
||||
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
|
||||
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
|
||||
|
||||
// 兼容旧格式:渲染步骤列表
|
||||
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
|
||||
|
||||
// 渲染 Markdown 格式的摘要
|
||||
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
||||
|
||||
// 已回答时显示用户的选择
|
||||
const answeredHtml = isAnswered ? \`
|
||||
<div class="plan-answered">
|
||||
<span class="answered-label">已回复:</span>
|
||||
<span class="answered-value">\${selectedAnswer}</span>
|
||||
</div>
|
||||
\` : '';
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
<div class="plan-header">
|
||||
<span class="plan-icon">${plannerIconSvg}</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
\${progressHtml}
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${summaryHtml}</div>
|
||||
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
||||
</div>
|
||||
<div class="plan-actions" data-ask-id="\${segment.askId}" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<div class="plan-input-row">
|
||||
<input type="text" class="plan-input" placeholder="输入修改建议..." />
|
||||
<button class="plan-btn plan-btn-submit">提交修改</button>
|
||||
</div>
|
||||
<div class="plan-btn-row">
|
||||
<button class="plan-btn plan-btn-confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
\${answeredHtml}
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 只在未回答时添加事件监听
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
|
||||
const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
|
||||
const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
|
||||
const planInput = segmentDiv.querySelector('.plan-input');
|
||||
|
||||
// 提交修改按钮
|
||||
if (submitBtn && planInput) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
|
||||
}
|
||||
});
|
||||
|
||||
// 回车键提交修改
|
||||
planInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认执行按钮
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
handleQuestionAnswerInSegment(segment.askId, '确认执行', segmentDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// 取消按钮 - 直接中止对话,不发送给智能体
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
// 标记问题已回答
|
||||
answeredQuestions.set(segment.askId, '取消');
|
||||
segmentDiv.classList.add('answered');
|
||||
|
||||
// 隐藏操作按钮
|
||||
const actionsDiv = segmentDiv.querySelector('.plan-actions');
|
||||
if (actionsDiv) {
|
||||
actionsDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// 发送中止对话命令
|
||||
vscode.postMessage({ command: 'abortDialog' });
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染计划卡片(在 renderSegments 中使用)
|
||||
function renderPlanCardStatic(segment, segmentDiv) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
|
||||
// 判断是否有 phases(新格式)还是 steps(旧格式)
|
||||
const hasPhases = segment.planPhases && segment.planPhases.length > 0;
|
||||
|
||||
// 渲染阶段进度条和阶段列表(新格式)
|
||||
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
|
||||
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
|
||||
|
||||
// 兼容旧格式:渲染步骤列表
|
||||
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
|
||||
|
||||
// 渲染 Markdown 格式的摘要
|
||||
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
<div class="plan-header">
|
||||
<span class="plan-icon">📋</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
\${progressHtml}
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${summaryHtml}</div>
|
||||
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
||||
</div>
|
||||
<div class="plan-actions" data-ask-id="\${segment.askId}">
|
||||
<div class="plan-input-row">
|
||||
<input type="text" class="plan-input" placeholder="输入修改建议..." />
|
||||
<button class="plan-btn plan-btn-submit">提交修改</button>
|
||||
</div>
|
||||
<div class="plan-btn-row">
|
||||
<button class="plan-btn plan-btn-confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 绑定按钮事件(静态渲染时也需要能响应)
|
||||
setTimeout(() => {
|
||||
const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
|
||||
const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
|
||||
const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
|
||||
const planInput = segmentDiv.querySelector('.plan-input');
|
||||
|
||||
// 提交修改按钮
|
||||
if (submitBtn && planInput) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: segment.askId,
|
||||
selected: [inputValue],
|
||||
customInput: inputValue
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认执行按钮
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: segment.askId,
|
||||
selected: ['确认执行'],
|
||||
customInput: '确认执行'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 取消按钮 - 直接中止对话
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
// 隐藏操作按钮
|
||||
const actionsDiv = segmentDiv.querySelector('.plan-actions');
|
||||
if (actionsDiv) {
|
||||
actionsDiv.style.display = 'none';
|
||||
}
|
||||
// 发送中止对话命令
|
||||
vscode.postMessage({ command: 'abortDialog' });
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -1,19 +1,21 @@
|
||||
/**
|
||||
* Plan 开关组件
|
||||
* 注意:功能已移至模式选择器,此组件仅保留样式(已禁用)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取 Plan 开关的 HTML 内容
|
||||
* 已禁用,仅保留样式展示
|
||||
*/
|
||||
export function getPlanToggleContent(): string {
|
||||
return `
|
||||
<div class="tooltip">
|
||||
<label class="plan-toggle">
|
||||
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
|
||||
<label class="plan-toggle plan-toggle-disabled">
|
||||
<input type="checkbox" id="planToggle" disabled>
|
||||
<span class="plan-toggle-slider"></span>
|
||||
<span class="plan-toggle-label">Plan</span>
|
||||
</label>
|
||||
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span>
|
||||
<span class="tooltiptext" id="planTooltip">请使用模式选择器切换 Plan 模式</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -73,6 +75,17 @@ export function getPlanToggleStyles(): string {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
/* 禁用状态样式 */
|
||||
.plan-toggle-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.plan-toggle-disabled .plan-toggle-slider {
|
||||
background: var(--vscode-input-background);
|
||||
border-color: var(--vscode-input-border);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
411
src/views/progressBar.ts
Normal file
@ -0,0 +1,411 @@
|
||||
/**
|
||||
* 进度条模块
|
||||
*
|
||||
* 功能说明:
|
||||
* - 显示开发流程进度: Spec -> Design代码编写 -> 仿真检查 -> AST -> Done
|
||||
* - 支持动态更新当前进度状态
|
||||
* - 提供视觉反馈显示已完成和进行中的步骤
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取进度条的 HTML 内容
|
||||
*/
|
||||
export function getProgressBarContent(): string {
|
||||
return `
|
||||
<div class="progress-bar-container" style="display: none;">
|
||||
<div class="progress-bar-header">
|
||||
<span class="progress-bar-title">开发流程</span>
|
||||
<button class="progress-bar-toggle" title="收起/展开">
|
||||
<span class="toggle-icon">▼</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step" data-step="spec">
|
||||
<div class="step-circle">
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Spec</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
|
||||
<div class="progress-step" data-step="design">
|
||||
<div class="step-circle">
|
||||
<span class="step-number">2</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Design</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
|
||||
<div class="progress-step" data-step="simulation">
|
||||
<div class="step-circle">
|
||||
<span class="step-number">3</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Simulation</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
|
||||
<div class="progress-step" data-step="done">
|
||||
<div class="step-circle">
|
||||
<span class="step-number">4</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Done</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进度条的样式
|
||||
*/
|
||||
export function getProgressBarStyles(): string {
|
||||
return `
|
||||
.progress-bar-container {
|
||||
background: var(--vscode-editor-background);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.progress-bar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.progress-bar-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.progress-bar-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.progress-bar-toggle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 10px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-bar-container.collapsed .toggle-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px 10px 20px;
|
||||
max-height: 60px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-bar-container.collapsed .progress-steps {
|
||||
max-height: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-input-background);
|
||||
border: 2px solid var(--vscode-input-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.step-check {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.step-label {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: var(--vscode-input-border);
|
||||
margin: 0 6px;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
/* 已完成状态 */
|
||||
.progress-step.completed .step-circle {
|
||||
background: var(--vscode-button-background);
|
||||
border-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.progress-step.completed .step-number {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-check {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-label {
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-step.completed + .progress-line {
|
||||
background: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
/* 进行中状态 */
|
||||
.progress-step.active .step-circle {
|
||||
background: var(--vscode-button-background);
|
||||
border-color: var(--vscode-button-background);
|
||||
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.progress-step.active .step-number {
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.progress-step.active .step-label {
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 4px var(--vscode-button-background)1a;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.progress-steps {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进度条的脚本
|
||||
*/
|
||||
export function getProgressBarScript(): string {
|
||||
return `
|
||||
// 进度条管理
|
||||
const ProgressBar = {
|
||||
steps: ['spec', 'design', 'simulation', 'done'],
|
||||
currentStep: 'spec',
|
||||
isCollapsed: false,
|
||||
|
||||
/**
|
||||
* 初始化进度条
|
||||
*/
|
||||
init() {
|
||||
this.updateProgress('spec');
|
||||
this.initToggle();
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化收起/展开功能
|
||||
*/
|
||||
initToggle() {
|
||||
const container = document.querySelector('.progress-bar-container');
|
||||
const header = document.querySelector('.progress-bar-header');
|
||||
const toggle = document.querySelector('.progress-bar-toggle');
|
||||
|
||||
if (!container || !header || !toggle) return;
|
||||
|
||||
// 点击头部或按钮都可以切换
|
||||
const handleToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
if (this.isCollapsed) {
|
||||
container.classList.add('collapsed');
|
||||
} else {
|
||||
container.classList.remove('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
header.addEventListener('click', handleToggle);
|
||||
toggle.addEventListener('click', handleToggle);
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示进度条
|
||||
*/
|
||||
show() {
|
||||
const container = document.querySelector('.progress-bar-container');
|
||||
if (container) {
|
||||
container.style.display = 'block';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 隐藏进度条
|
||||
*/
|
||||
hide() {
|
||||
const container = document.querySelector('.progress-bar-container');
|
||||
if (container) {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新进度到指定步骤
|
||||
* @param {string} stepName - 步骤名称
|
||||
*/
|
||||
updateProgress(stepName) {
|
||||
if (!this.steps.includes(stepName)) {
|
||||
console.warn('Invalid step name:', stepName);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentStep = stepName;
|
||||
const currentIndex = this.steps.indexOf(stepName);
|
||||
|
||||
// 更新所有步骤的状态
|
||||
document.querySelectorAll('.progress-step').forEach((step, index) => {
|
||||
step.classList.remove('completed', 'active');
|
||||
|
||||
if (index < currentIndex) {
|
||||
step.classList.add('completed');
|
||||
} else if (index === currentIndex) {
|
||||
step.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 更新连接线
|
||||
document.querySelectorAll('.progress-line').forEach((line, index) => {
|
||||
if (index < currentIndex) {
|
||||
line.style.background = 'var(--vscode-button-background)';
|
||||
} else {
|
||||
line.style.background = 'var(--vscode-input-border)';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 前进到下一步
|
||||
*/
|
||||
nextStep() {
|
||||
const currentIndex = this.steps.indexOf(this.currentStep);
|
||||
if (currentIndex < this.steps.length - 1) {
|
||||
this.updateProgress(this.steps[currentIndex + 1]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置进度条
|
||||
*/
|
||||
reset() {
|
||||
this.updateProgress('spec');
|
||||
},
|
||||
|
||||
/**
|
||||
* 完成所有步骤
|
||||
*/
|
||||
complete() {
|
||||
this.updateProgress('done');
|
||||
// 将最后一步也标记为完成
|
||||
const lastStep = document.querySelector('.progress-step[data-step="done"]');
|
||||
if (lastStep) {
|
||||
lastStep.classList.remove('active');
|
||||
lastStep.classList.add('completed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化进度条
|
||||
ProgressBar.init();
|
||||
|
||||
// 监听来自扩展的消息以更新进度
|
||||
window.addEventListener('message', (event) => {
|
||||
const message = event.data;
|
||||
if (message.type === 'updateProgress') {
|
||||
ProgressBar.updateProgress(message.step);
|
||||
} else if (message.type === 'resetProgress') {
|
||||
ProgressBar.reset();
|
||||
} else if (message.type === 'completeProgress') {
|
||||
ProgressBar.complete();
|
||||
} else if (message.type === 'showProgress') {
|
||||
ProgressBar.show();
|
||||
} else if (message.type === 'hideProgress') {
|
||||
ProgressBar.hide();
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
177
src/views/rulesSettingsComponent.ts
Normal file
@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 获取规则设置组件的 HTML 内容
|
||||
*/
|
||||
export function getRulesSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="rules-settings">
|
||||
<h3 class="settings-section-title">规则设置</h3>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">启用自定义规则</label>
|
||||
<span class="settings-item-description">使用自定义规则来控制 AI 行为</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="enableCustomRulesCheckbox" checked>
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">系统规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="systemRulesTextarea"
|
||||
placeholder="在此输入系统规则,例如: - 始终使用中文回复 - 代码注释要详细 - 遵循项目编码规范"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
系统规则会在每次对话开始时应用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">代码生成规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="codeRulesTextarea"
|
||||
placeholder="在此输入代码生成规则,例如: - 使用 TypeScript 严格模式 - 函数命名使用驼峰命名法 - 添加必要的错误处理"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
这些规则会在生成代码时应用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">Verilog 规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="verilogRulesTextarea"
|
||||
placeholder="在此输入 Verilog 代码规则,例如: - 使用非阻塞赋值 (<=) 在时序逻辑中 - 模块命名使用小写加下划线 - 添加详细的端口注释"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
这些规则会在生成 Verilog 代码时应用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="saveRulesSettings()">
|
||||
保存规则
|
||||
</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="resetRulesSettings()">
|
||||
重置为默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则设置组件的 CSS 样式
|
||||
*/
|
||||
export function getRulesSettingsComponentStyles(): string {
|
||||
return `
|
||||
.rules-settings {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.rules-textarea-container {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.rules-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rules-textarea:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.rules-textarea::placeholder {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.rules-textarea-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则设置组件的 JavaScript 脚本
|
||||
*/
|
||||
export function getRulesSettingsComponentScript(): string {
|
||||
return `
|
||||
// 保存规则设置
|
||||
function saveRulesSettings() {
|
||||
const settings = {
|
||||
enableCustomRules: document.getElementById('enableCustomRulesCheckbox').checked,
|
||||
systemRules: document.getElementById('systemRulesTextarea').value,
|
||||
codeRules: document.getElementById('codeRulesTextarea').value,
|
||||
verilogRules: document.getElementById('verilogRulesTextarea').value,
|
||||
};
|
||||
|
||||
// 发送消息到扩展
|
||||
vscode.postMessage({
|
||||
command: 'saveRulesSettings',
|
||||
settings: settings
|
||||
});
|
||||
|
||||
// 显示保存成功提示
|
||||
console.log('规则设置已保存', settings);
|
||||
}
|
||||
|
||||
// 重置规则设置
|
||||
function resetRulesSettings() {
|
||||
document.getElementById('enableCustomRulesCheckbox').checked = true;
|
||||
document.getElementById('systemRulesTextarea').value = '';
|
||||
document.getElementById('codeRulesTextarea').value = '';
|
||||
document.getElementById('verilogRulesTextarea').value = '';
|
||||
|
||||
console.log('规则设置已重置为默认值');
|
||||
}
|
||||
|
||||
// 加载规则设置
|
||||
function loadRulesSettings(settings) {
|
||||
if (!settings) return;
|
||||
|
||||
if (settings.enableCustomRules !== undefined) {
|
||||
document.getElementById('enableCustomRulesCheckbox').checked = settings.enableCustomRules;
|
||||
}
|
||||
if (settings.systemRules) {
|
||||
document.getElementById('systemRulesTextarea').value = settings.systemRules;
|
||||
}
|
||||
if (settings.codeRules) {
|
||||
document.getElementById('codeRulesTextarea').value = settings.codeRules;
|
||||
}
|
||||
if (settings.verilogRules) {
|
||||
document.getElementById('verilogRulesTextarea').value = settings.verilogRules;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
248
src/views/settingsComponent.ts
Normal file
@ -0,0 +1,248 @@
|
||||
import {
|
||||
getGeneralSettingsComponentContent,
|
||||
getGeneralSettingsComponentStyles,
|
||||
getGeneralSettingsComponentScript,
|
||||
} from "./generalSettingsComponent";
|
||||
import {
|
||||
getRulesSettingsComponentContent,
|
||||
getRulesSettingsComponentStyles,
|
||||
getRulesSettingsComponentScript,
|
||||
} from "./rulesSettingsComponent";
|
||||
|
||||
/**
|
||||
* 获取设置面板的 HTML 内容
|
||||
*/
|
||||
export function getSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="settings-modal" id="settingsModal">
|
||||
<div class="settings-modal-overlay" onclick="closeSettingsModal()"></div>
|
||||
<div class="settings-modal-content">
|
||||
<div class="settings-modal-header">
|
||||
<h2 class="settings-modal-title">设置</h2>
|
||||
<button class="settings-modal-close" onclick="closeSettingsModal()" title="关闭">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-modal-body">
|
||||
<div class="settings-nav">
|
||||
<button class="settings-nav-item active" data-tab="general" onclick="switchSettingsTab('general')">
|
||||
通用
|
||||
</button>
|
||||
<button class="settings-nav-item" data-tab="rules" onclick="switchSettingsTab('rules')">
|
||||
规则
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="settings-tab-content active" id="generalSettings">
|
||||
${getGeneralSettingsComponentContent()}
|
||||
</div>
|
||||
<div class="settings-tab-content" id="rulesSettings">
|
||||
${getRulesSettingsComponentContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设置面板的 CSS 样式
|
||||
*/
|
||||
export function getSettingsComponentStyles(): string {
|
||||
return `
|
||||
.settings-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.settings-modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.settings-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.settings-modal-content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
height: 80%;
|
||||
max-height: 600px;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.settings-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-modal-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.settings-modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-foreground);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-modal-close:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.settings-modal-close svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.settings-modal-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
width: 180px;
|
||||
padding: 16px 0;
|
||||
border-right: 1px solid var(--vscode-panel-border);
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.settings-nav-item {
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.settings-nav-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.settings-nav-item.active {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
border-left-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
${getGeneralSettingsComponentStyles()}
|
||||
${getRulesSettingsComponentStyles()}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设置面板的 JavaScript 脚本
|
||||
*/
|
||||
export function getSettingsComponentScript(): string {
|
||||
return `
|
||||
${getGeneralSettingsComponentScript()}
|
||||
${getRulesSettingsComponentScript()}
|
||||
|
||||
// 打开设置面板
|
||||
function openSettingsModal() {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭设置面板
|
||||
function closeSettingsModal() {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换设置标签页
|
||||
function switchSettingsTab(tabName) {
|
||||
// 更新导航项状态
|
||||
const navItems = document.querySelectorAll('.settings-nav-item');
|
||||
navItems.forEach(item => {
|
||||
if (item.dataset.tab === tabName) {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 更新内容区域
|
||||
const tabContents = document.querySelectorAll('.settings-tab-content');
|
||||
tabContents.forEach(content => {
|
||||
if (content.id === tabName + 'Settings') {
|
||||
content.classList.add('active');
|
||||
} else {
|
||||
content.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 阻止点击模态框内容时关闭
|
||||
document.addEventListener('click', (event) => {
|
||||
const modalContent = document.querySelector('.settings-modal-content');
|
||||
if (modalContent && modalContent.contains(event.target)) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
178
src/views/thinkingProcess.ts
Normal file
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 思考过程组件
|
||||
*
|
||||
* 功能说明:
|
||||
* - 显示 AI 的思考过程
|
||||
* - 支持展开/折叠功能
|
||||
* - 提供打字机效果的流式显示
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取思考过程组件的 HTML 内容
|
||||
* @param thinking - 思考内容
|
||||
* @param isExpanded - 是否默认展开
|
||||
*/
|
||||
export function getThinkingProcessContent(
|
||||
thinking: string = "",
|
||||
isExpanded: boolean = false
|
||||
): string {
|
||||
return `
|
||||
<div class="thinking-process-container ${isExpanded ? "expanded" : ""}">
|
||||
<div class="thinking-header">
|
||||
<div class="thinking-icon-wrapper">
|
||||
<svg class="thinking-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="thinking-title">思考过程</span>
|
||||
</div>
|
||||
<div class="thinking-content">
|
||||
<div class="thinking-text">${thinking || "正在思考中..."}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取思考过程组件的样式
|
||||
*/
|
||||
export function getThinkingProcessStyles(): string {
|
||||
return `
|
||||
.thinking-process-container {
|
||||
margin: 12px 0;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-editor-background);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.thinking-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--vscode-input-background);
|
||||
transition: background 0.2s ease;
|
||||
width: 85px;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thinking-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.1),
|
||||
transparent
|
||||
);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.thinking-process-container.typing .thinking-header::before {
|
||||
animation: shine 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
50%, 100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.thinking-header:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.thinking-icon-wrapper {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.thinking-process-container.expanded .thinking-icon-wrapper {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.thinking-icon {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.thinking-title {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.thinking-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.thinking-process-container.expanded .thinking-content {
|
||||
max-height: 500px;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.thinking-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
padding-left: 12px;
|
||||
border-left: 2px solid var(--vscode-textBlockQuote-border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 打字机效果 */
|
||||
.thinking-text.typing::after {
|
||||
content: '▋';
|
||||
animation: blink 1s step-end infinite;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% {
|
||||
opacity: 1;
|
||||
}
|
||||
51%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.thinking-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.thinking-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.thinking-content::-webkit-scrollbar-thumb {
|
||||
background: var(--vscode-scrollbarSlider-background);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.thinking-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
612
src/views/userInfoComponent.ts
Normal file
@ -0,0 +1,612 @@
|
||||
/**
|
||||
* 用户信息组件
|
||||
* 包含用户头像、昵称、会员等级等信息
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取用户信息组件的 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-info-row">
|
||||
<div class="user-avatar-small clickable" id="userAvatarClickable">
|
||||
<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 clickable" id="userDetailName">加载中...</div>
|
||||
<img class="tier-icon-inline" id="tierIconInline" style="display: none;" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 升级到Pro按钮 (仅BASIC会员显示) -->
|
||||
<div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
|
||||
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
|
||||
</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 class="user-detail-item logout-item" id="logoutItem">
|
||||
<span class="detail-label">账户管理</span>
|
||||
<span class="detail-value logout-link">退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 退出登录确认对话框 -->
|
||||
<div class="logout-confirm-modal" id="logoutConfirmModal">
|
||||
<div class="logout-confirm-overlay"></div>
|
||||
<div class="logout-confirm-content">
|
||||
<div class="logout-confirm-header">
|
||||
<h3>确认退出</h3>
|
||||
</div>
|
||||
<div class="logout-confirm-body">
|
||||
<p>确定要退出登录吗?</p>
|
||||
</div>
|
||||
<div class="logout-confirm-footer">
|
||||
<button class="logout-confirm-btn logout-cancel-btn" id="logoutCancelBtn">取消</button>
|
||||
<button class="logout-confirm-btn logout-ok-btn" id="logoutOkBtn">确定</button>
|
||||
</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;
|
||||
flex-direction: column;
|
||||
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-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.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);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-avatar-small.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-avatar-small.clickable:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.5);
|
||||
}
|
||||
|
||||
.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);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-detail-name.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-detail-name.clickable:hover {
|
||||
color: #007acc;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.logout-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logout-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-color: rgba(204, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.logout-item:hover .logout-link {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
color: var(--vscode-foreground);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tier-icon-large {
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.tier-icon {
|
||||
width: 110px;
|
||||
height: 35px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.upgrade-pro-wrapper {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.upgrade-pro-btn {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(135deg, #007acc 0%, #58a6ff 100%);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 4px rgba(0, 122, 204, 0.2);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.upgrade-pro-btn:hover {
|
||||
background: linear-gradient(135deg, #0098ff 0%, #6bb6ff 100%);
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.upgrade-pro-btn:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
/* 退出登录确认对话框 */
|
||||
.logout-confirm-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
.logout-confirm-modal.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logout-confirm-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.logout-confirm-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 8px;
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.logout-confirm-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.logout-confirm-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.logout-confirm-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.logout-confirm-body p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logout-confirm-footer {
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
border-top: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.logout-confirm-btn {
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-cancel-btn {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.logout-cancel-btn:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.logout-ok-btn {
|
||||
background: #f48771;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.logout-ok-btn:hover {
|
||||
background: #e67361;
|
||||
}
|
||||
|
||||
.logout-confirm-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息组件的 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 logout() {
|
||||
console.log("显示退出登录确认对话框");
|
||||
// 显示确认对话框
|
||||
const modal = document.getElementById('logoutConfirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 确认退出登录
|
||||
function confirmLogout() {
|
||||
console.log("确认退出登录");
|
||||
vscode.postMessage({ command: 'logout' });
|
||||
// 关闭确认对话框
|
||||
closeLogoutConfirmModal();
|
||||
}
|
||||
|
||||
// 关闭退出登录确认对话框
|
||||
function closeLogoutConfirmModal() {
|
||||
const modal = document.getElementById('logoutConfirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到 IC Coder 官网
|
||||
function openICCoder() {
|
||||
console.log("跳转到 IC Coder 官网");
|
||||
vscode.postMessage({ command: 'openICCoder' });
|
||||
}
|
||||
|
||||
// 升级到Pro
|
||||
function upgradeToPro() {
|
||||
console.log("升级到 Pro - 跳转到 IC Coder 官网");
|
||||
vscode.postMessage({ command: 'openExternalUrl', url: 'https://www.iccoder.com' });
|
||||
}
|
||||
|
||||
// 关闭用户详情下拉面板
|
||||
function closeUserDetailModal() {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
const userButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('active');
|
||||
}
|
||||
if (userButton) {
|
||||
userButton.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户详情下拉面板内容
|
||||
function updateUserDetailModal() {
|
||||
if (!currentUserInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新用户名
|
||||
const userDetailName = document.getElementById('userDetailName');
|
||||
if (userDetailName) {
|
||||
userDetailName.textContent = currentUserInfo.nickname || '未知用户';
|
||||
}
|
||||
|
||||
// 更新会员等级图标(显示在用户名旁边)
|
||||
const tierIconInline = document.getElementById('tierIconInline');
|
||||
if (tierIconInline && currentUserInfo.tierIconUrl) {
|
||||
tierIconInline.src = currentUserInfo.tierIconUrl;
|
||||
tierIconInline.style.display = 'block';
|
||||
} else if (tierIconInline) {
|
||||
tierIconInline.style.display = 'none';
|
||||
}
|
||||
|
||||
// 更新剩余 Credits
|
||||
const creditsDetail = document.getElementById('creditsDetail');
|
||||
console.log('[UserInfoComponent] 更新 Credits 显示');
|
||||
console.log('[UserInfoComponent] currentUserInfo.credits:', currentUserInfo.credits);
|
||||
console.log('[UserInfoComponent] creditsDetail 元素:', creditsDetail);
|
||||
if (creditsDetail) {
|
||||
const creditsText = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-';
|
||||
creditsDetail.textContent = creditsText;
|
||||
console.log('[UserInfoComponent] Credits 已更新为:', creditsText);
|
||||
} else {
|
||||
console.warn('[UserInfoComponent] creditsDetail 元素未找到');
|
||||
}
|
||||
|
||||
// 显示或隐藏升级到Pro按钮 (仅BASIC会员显示)
|
||||
const upgradeProWrapper = document.getElementById('upgradeProWrapper');
|
||||
const tierCode = currentUserInfo.membership?.tierCode;
|
||||
if (upgradeProWrapper) {
|
||||
if (tierCode === 'BASIC') {
|
||||
upgradeProWrapper.style.display = 'block';
|
||||
} else {
|
||||
upgradeProWrapper.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
console.warn('[UserInfoComponent] upgradeProWrapper 元素未找到');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息显示
|
||||
function updateUserInfoDisplay(userInfo) {
|
||||
currentUserInfo = userInfo;
|
||||
console.log('[UserInfoComponent] 更新用户信息:', userInfo);
|
||||
// 如果下拉面板已打开,立即更新显示
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
if (dropdown && dropdown.classList.contains('active')) {
|
||||
updateUserDetailModal();
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定下拉面板事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 绑定退出登录卡片点击事件
|
||||
const logoutItem = document.getElementById('logoutItem');
|
||||
if (logoutItem) {
|
||||
logoutItem.addEventListener('click', () => {
|
||||
logout();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定退出登录确认对话框按钮
|
||||
const logoutOkBtn = document.getElementById('logoutOkBtn');
|
||||
if (logoutOkBtn) {
|
||||
logoutOkBtn.addEventListener('click', () => {
|
||||
confirmLogout();
|
||||
});
|
||||
}
|
||||
|
||||
const logoutCancelBtn = document.getElementById('logoutCancelBtn');
|
||||
if (logoutCancelBtn) {
|
||||
logoutCancelBtn.addEventListener('click', () => {
|
||||
closeLogoutConfirmModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 点击遮罩层关闭对话框
|
||||
const logoutConfirmModal = document.getElementById('logoutConfirmModal');
|
||||
if (logoutConfirmModal) {
|
||||
const overlay = logoutConfirmModal.querySelector('.logout-confirm-overlay');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', () => {
|
||||
closeLogoutConfirmModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定升级到Pro按钮
|
||||
const upgradeProBtn = document.getElementById('upgradeProBtn');
|
||||
if (upgradeProBtn) {
|
||||
upgradeProBtn.addEventListener('click', () => {
|
||||
upgradeToPro();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定头像点击事件
|
||||
const userAvatar = document.getElementById('userAvatarClickable');
|
||||
if (userAvatar) {
|
||||
userAvatar.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
openICCoder();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定用户名点击事件
|
||||
const userName = document.getElementById('userDetailName');
|
||||
if (userName) {
|
||||
userName.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
openICCoder();
|
||||
});
|
||||
}
|
||||
|
||||
// 点击页面其他地方关闭下拉面板
|
||||
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 `
|
||||
/* 波形预览组件样式 */
|
||||
.waveform-preview {
|
||||
margin-top: 12px;
|
||||
margin: 16px 0;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, var(--vscode-input-background) 0%, var(--vscode-editor-background) 100%);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.waveform-preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.waveform-preview-title svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--vscode-button-background);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
.waveform-expand-btn {
|
||||
padding: 4px 12px;
|
||||
padding: 6px 14px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: opacity 0.2s ease;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.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 {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.waveform-preview-content {
|
||||
padding: 0;
|
||||
min-height: 200px;
|
||||
max-height: 300px;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.waveform-preview-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
height: auto;
|
||||
}
|
||||
.waveform-preview-placeholder {
|
||||
display: flex;
|
||||
@ -88,7 +102,8 @@ export function getWaveformPreviewContent(): string {
|
||||
}
|
||||
.waveform-mini-viewer {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: auto;
|
||||
min-height: 120px;
|
||||
background: var(--vscode-editor-background);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@ -159,7 +174,7 @@ export function getWaveformPreviewScript(): string {
|
||||
const content = document.createElement('div');
|
||||
content.className = 'waveform-preview-content';
|
||||
|
||||
const miniViewerId = 'waveform-mini-' + Date.now();
|
||||
const miniViewerId = 'waveform-mini-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
const miniViewer = document.createElement('div');
|
||||
miniViewer.id = miniViewerId;
|
||||
miniViewer.className = 'waveform-mini-viewer';
|
||||
@ -263,61 +278,63 @@ export function getWaveformPreviewScript(): string {
|
||||
const timeRange = maxTime - minTime || 1;
|
||||
|
||||
// 绘制波形
|
||||
if (signal.width === 1) {
|
||||
// 单比特信号 - 绘制数字波形
|
||||
let pathData = '';
|
||||
let lastX = leftMargin;
|
||||
let lastValue = signal.values[0].value;
|
||||
const yHigh = y;
|
||||
const yLow = y + signalHeight;
|
||||
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
const value = point.value;
|
||||
|
||||
if (signal.width === 1) {
|
||||
// 单比特信号 - 绘制数字波形
|
||||
const yHigh = y;
|
||||
const yLow = y + signalHeight;
|
||||
const currentY = (value === '1') ? yHigh : yLow;
|
||||
const currentY = (point.value === '1') ? yHigh : yLow;
|
||||
|
||||
if (i === 0) {
|
||||
pathData = \`M \${x} \${currentY}\`;
|
||||
} else {
|
||||
// 绘制垂直跳变
|
||||
const prevY = (lastValue === '1') ? yHigh : yLow;
|
||||
const prevValue = signal.values[i - 1].value;
|
||||
const prevY = (prevValue === '1') ? yHigh : yLow;
|
||||
if (prevY !== currentY) {
|
||||
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
|
||||
} else {
|
||||
pathData += \` L \${x} \${currentY}\`;
|
||||
}
|
||||
}
|
||||
|
||||
lastValue = value;
|
||||
lastX = x;
|
||||
} else {
|
||||
// 多比特信号 - 绘制总线波形(梯形)
|
||||
const yTop = y + 5;
|
||||
const yBottom = y + signalHeight - 5;
|
||||
const transitionWidth = 5;
|
||||
|
||||
if (i === 0) {
|
||||
pathData = \`M \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
||||
} else {
|
||||
// 绘制梯形过渡
|
||||
pathData += \` L \${x - transitionWidth} \${yTop} L \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
}
|
||||
});
|
||||
|
||||
// 延伸到右边界
|
||||
if (signal.width === 1) {
|
||||
const lastY = (lastValue === '1') ? y : (y + signalHeight);
|
||||
const lastValue = signal.values[signal.values.length - 1].value;
|
||||
const lastY = (lastValue === '1') ? yHigh : yLow;
|
||||
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`;
|
||||
} else {
|
||||
const yMid = y + signalHeight / 2;
|
||||
pathData += \` L \${leftMargin + waveformWidth} \${yMid}\`;
|
||||
}
|
||||
|
||||
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
} else {
|
||||
// 多比特信号 - 绘制总线波形(上下双线)
|
||||
const yTop = y + 5;
|
||||
const yBottom = y + signalHeight - 5;
|
||||
const transitionWidth = 4;
|
||||
|
||||
let topPath = \`M \${leftMargin} \${yTop}\`;
|
||||
let bottomPath = \`M \${leftMargin} \${yBottom}\`;
|
||||
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
|
||||
// 上线和下线都延伸到变化点
|
||||
topPath += \` L \${x} \${yTop}\`;
|
||||
bottomPath += \` L \${x} \${yBottom}\`;
|
||||
|
||||
// 绘制梯形过渡
|
||||
topPath += \` L \${x + transitionWidth} \${yBottom} L \${x + transitionWidth} \${yTop}\`;
|
||||
bottomPath += \` L \${x + transitionWidth} \${yTop} L \${x + transitionWidth} \${yBottom}\`;
|
||||
});
|
||||
|
||||
// 延伸到右边界
|
||||
topPath += \` L \${leftMargin + waveformWidth} \${yTop}\`;
|
||||
bottomPath += \` L \${leftMargin + waveformWidth} \${yBottom}\`;
|
||||
|
||||
svgContent += \`<path d="\${topPath}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
svgContent += \`<path d="\${bottomPath}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
}
|
||||
});
|
||||
|
||||
// 绘制时间轴
|
||||
@ -330,7 +347,7 @@ export function getWaveformPreviewScript(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开完整波形查看器
|
||||
* 打开完整波形查看器(在新列中)
|
||||
*/
|
||||
function openFullWaveform(vcdFilePath) {
|
||||
vscode.postMessage({
|
||||
|
||||
@ -17,16 +17,36 @@ import {
|
||||
getMessageAreaStyles,
|
||||
getMessageAreaScript,
|
||||
} from "./messageArea";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
import {
|
||||
getProgressBarContent,
|
||||
getProgressBarStyles,
|
||||
getProgressBarScript,
|
||||
} from "./progressBar";
|
||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||
import { getCurrentEnv } from "../config/settings";
|
||||
/**
|
||||
* 获取 WebView 面板的 HTML 内容
|
||||
*/
|
||||
export function getWebviewContent(iconUri?: string): string {
|
||||
export function getWebviewContent(
|
||||
iconUri?: string,
|
||||
autoIconUri?: string,
|
||||
liteIconUri?: string,
|
||||
syIconUri?: string,
|
||||
maxIconUri?: string,
|
||||
qrCodeUri?: string
|
||||
): string {
|
||||
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
|
||||
const currentEnv = getCurrentEnv();
|
||||
const showQuickActions = currentEnv === "dev" || currentEnv === "test";
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IC Coder</title>
|
||||
${getHighlightJsLinks()}
|
||||
<style>
|
||||
body {
|
||||
font-family: var(--vscode-font-family);
|
||||
@ -53,7 +73,10 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
display: none;
|
||||
}
|
||||
.header h1 {
|
||||
color: var(--vscode-button-background);
|
||||
background: linear-gradient(to right, #4A9EFF, #7CB8FF, #A8D0FF);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.chat-container {
|
||||
@ -65,8 +88,10 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
${getMessageAreaStyles()}
|
||||
${getAgentCardStyles()}
|
||||
${getWaveformPreviewContent()}
|
||||
${getConversationHistoryBarStyles()}
|
||||
${getProgressBarStyles()}
|
||||
${getInputAreaStyles()}
|
||||
|
||||
.file-editor-section {
|
||||
@ -250,7 +275,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 22 px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
@ -293,7 +318,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 14px;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.question-segment .question-text {
|
||||
@ -372,12 +397,13 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
</head>
|
||||
<body>
|
||||
${getConversationHistoryBarContent()}
|
||||
${getProgressBarContent()}
|
||||
<div class="header">
|
||||
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
|
||||
<h1 style="margin: 0;">IC Coder</h1>
|
||||
<div style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
||||
<img src="${iconUri}" alt="IC Coder" style="width: 48px; height: 48px;" />
|
||||
<h1 style="margin: 0; font-size: 36px;">IC Coder</h1>
|
||||
</div>
|
||||
<p>专注于真实FPGA研发的Verilog智能体编程平台</p>
|
||||
<p style="font-size: 16px; margin-top: 12px;">专注于真实FPGA研发的Verilog智能体编程平台</p>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
@ -389,18 +415,24 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
<span id="statusText">思考中...</span>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
${
|
||||
showQuickActions
|
||||
? `<div class="quick-actions">
|
||||
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
|
||||
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
|
||||
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
|
||||
</div>
|
||||
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${getInputAreaContent()}
|
||||
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('[WebView] 脚本开始执行');
|
||||
const vscode = acquireVsCodeApi();
|
||||
window.vscode = vscode; // 确保全局可访问
|
||||
console.log('[WebView] vscode API 已获取');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
@ -411,11 +443,73 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
let loadingIndicator = null;
|
||||
let currentSegmentedMessage = null; // 当前分段消息容器
|
||||
|
||||
// 设置二维码图片
|
||||
const feedbackQRCodeImage = document.getElementById('feedbackQRCodeImage');
|
||||
if (feedbackQRCodeImage && '${qrCodeUri}') {
|
||||
feedbackQRCodeImage.src = '${qrCodeUri}';
|
||||
}
|
||||
|
||||
// ========== 模式选择器脚本(直接内联,避免模板字符串嵌套问题)==========
|
||||
let currentMode = 'agent';
|
||||
|
||||
function toggleModeDropdown() {
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
const modelSelectEl = document.getElementById('modelSelect');
|
||||
if (modeSelectEl) {
|
||||
modeSelectEl.classList.toggle('active');
|
||||
if (modelSelectEl) {
|
||||
modelSelectEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectMode(value, label) {
|
||||
currentMode = value;
|
||||
const modeValue = document.getElementById('modeValue');
|
||||
const modeTooltip = document.getElementById('modeTooltip');
|
||||
if (modeValue) {
|
||||
modeValue.textContent = label;
|
||||
}
|
||||
if (modeTooltip) {
|
||||
const tooltipMap = {
|
||||
'plan': 'plan模式',
|
||||
'ask': 'ask模式',
|
||||
'agent': 'agent模式'
|
||||
};
|
||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||
}
|
||||
const options = document.querySelectorAll('.mode-option');
|
||||
options.forEach(option => {
|
||||
if (option.getAttribute('data-value') === value) {
|
||||
option.classList.add('selected');
|
||||
} else {
|
||||
option.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
if (modeSelectEl) {
|
||||
modeSelectEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentMode() {
|
||||
return currentMode;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
if (modeSelectEl && !modeSelectEl.contains(event.target)) {
|
||||
modeSelectEl.classList.remove('active');
|
||||
}
|
||||
});
|
||||
// ========== 模式选择器脚本结束 ==========
|
||||
|
||||
function quickAction(type) {
|
||||
const questions = {
|
||||
counter: '生成一个4位同步计数器',
|
||||
fsm: '生成一个状态机',
|
||||
testbench: '生成测试平台'
|
||||
testbench: '生成测试平台',
|
||||
explore: '请启动知识探索智能体,分析当前项目结构'
|
||||
};
|
||||
if (questions[type]) {
|
||||
messageInput.value = questions[type];
|
||||
@ -436,8 +530,6 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
|
||||
// 监听来自插件的消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
@ -504,11 +596,76 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'updateUserInfo':
|
||||
// 更新用户信息
|
||||
console.log('[WebView] 收到用户信息:', message.userInfo);
|
||||
console.log('[WebView] Credits 字段值:', message.userInfo?.credits);
|
||||
if (message.userInfo) {
|
||||
const userInfoData = {
|
||||
nickname: message.userInfo.nickname || message.userInfo.username || '用户',
|
||||
userId: message.userInfo.userId || message.userInfo.id,
|
||||
tierName: message.userInfo.tierName,
|
||||
tierIconUrl: message.tierIconUrl,
|
||||
registerTime: message.userInfo.registerTime || message.userInfo.createdAt,
|
||||
credits: message.userInfo.credits,
|
||||
membership: message.userInfo.membership
|
||||
};
|
||||
|
||||
console.log('[WebView] 显示用户信息:', userInfoData);
|
||||
console.log('[WebView] userInfoData.credits:', userInfoData.credits);
|
||||
console.log('[WebView] userInfoData.membership:', userInfoData.membership);
|
||||
|
||||
// 调用更新用户头像图标按钮的函数
|
||||
if (typeof updateUserAvatarIconButton === 'function') {
|
||||
updateUserAvatarIconButton(userInfoData);
|
||||
} else {
|
||||
console.warn('[WebView] updateUserAvatarIconButton 函数不存在');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'showFeedbackQRCode':
|
||||
// 显示用户反馈二维码弹窗
|
||||
console.log('[WebView] 显示用户反馈二维码弹窗');
|
||||
if (typeof showFeedbackQRCode === 'function') {
|
||||
showFeedbackQRCode();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'resetSegmentedMessage':
|
||||
// 重置分段消息容器(停止对话时调用)
|
||||
console.log('[WebView] 重置分段消息容器');
|
||||
currentSegmentedMessage = null;
|
||||
break;
|
||||
|
||||
case 'contextUsage':
|
||||
// 更新上下文使用量显示
|
||||
if (typeof updateContextDisplay === 'function') {
|
||||
updateContextDisplay(message.currentTokens, message.maxTokens);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'hideLoading':
|
||||
// 隐藏加载指示器
|
||||
hideLoadingIndicator();
|
||||
break;
|
||||
|
||||
case 'workspaceStatus':
|
||||
// 更新工作区状态
|
||||
if (typeof hasWorkspace !== 'undefined') {
|
||||
hasWorkspace = message.hasWorkspace;
|
||||
console.log('[WebView] 工作区状态:', hasWorkspace);
|
||||
|
||||
// 如果有待发送的示例,且工作区存在,则发送
|
||||
if (hasWorkspace && typeof pendingExampleIndex !== 'undefined' && pendingExampleIndex >= 0) {
|
||||
if (typeof doSendExample === 'function') {
|
||||
doSendExample(pendingExampleIndex);
|
||||
pendingExampleIndex = -1; // 重置
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'vcdInfo':
|
||||
// 渲染迷你波形预览信息
|
||||
try {
|
||||
@ -565,6 +722,10 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
if (messagesContainer) {
|
||||
messagesContainer.innerHTML = '';
|
||||
}
|
||||
// 重置输入框布局到居中
|
||||
if (typeof window.resetInputAreaLayout === 'function') {
|
||||
window.resetInputAreaLayout();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addUserMessage':
|
||||
@ -572,6 +733,10 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
if (message.text) {
|
||||
addMessage(message.text, 'user');
|
||||
}
|
||||
// 检查并更新输入框布局
|
||||
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
|
||||
window.checkMessagesAndUpdateLayout();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addAiMessage':
|
||||
@ -579,6 +744,38 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
if (message.text) {
|
||||
addMessage(message.text, 'bot');
|
||||
}
|
||||
// 检查并更新输入框布局
|
||||
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
|
||||
window.checkMessagesAndUpdateLayout();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'switchMode':
|
||||
// 切换运行模式(Plan 确认后自动切换到 Agent)
|
||||
if (message.mode && typeof selectMode === 'function') {
|
||||
const labelMap = {
|
||||
'plan': 'Plan',
|
||||
'ask': 'Ask',
|
||||
'agent': 'Agent',
|
||||
'auto': 'Auto'
|
||||
};
|
||||
selectMode(message.mode, labelMap[message.mode] || message.mode);
|
||||
console.log('[WebView] 模式已切换到:', message.mode);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addMessage':
|
||||
// 添加消息(通用)
|
||||
if (message.text && message.sender) {
|
||||
addMessage(message.text, message.sender);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'optimizeResult':
|
||||
// 处理提示词优化结果
|
||||
if (typeof handleOptimizeResult === 'function') {
|
||||
handleOptimizeResult(message.success, message.optimizedPrompt, message.error);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -587,8 +784,10 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
});
|
||||
|
||||
${getMessageAreaScript()}
|
||||
${getAgentCardScript()}
|
||||
${getWaveformPreviewScript()}
|
||||
${getConversationHistoryBarScript()}
|
||||
${getProgressBarScript()}
|
||||
${getInputAreaScript()}
|
||||
</script></body>
|
||||
</html>`;
|
||||
|
||||