Compare commits
81 Commits
feat/backe
...
feat/eda
| Author | SHA1 | Date | |
|---|---|---|---|
| ccbc40f5fb | |||
| 5adde3d40a | |||
| fa5c2cdafd | |||
| aa80088abc | |||
| 0ae627ca7c | |||
| 7732b11d37 | |||
| 81717dc84f | |||
| 11c408ce0f | |||
| c138406217 | |||
| 2a280aaa93 | |||
| 2f6eae9f2b | |||
| d0ff876ba2 | |||
| 7fe87e515b | |||
| 790110ba7e | |||
| 29e80ce296 | |||
| c244a308d7 | |||
| 7cde4fa138 | |||
| 1b7259d1c1 | |||
| 09ff812562 | |||
| e7c631d532 | |||
| 06573e37d7 | |||
| d740f4da44 | |||
| f24bd38ec7 | |||
| 45934baf0a | |||
| 4384ee53c5 | |||
| d89c326be5 | |||
| 2dccb4f871 | |||
| a9ddf3074e | |||
| db087bb184 | |||
| 5e9083041f | |||
| be0555d6bc | |||
| ea19dfcbe6 | |||
| fa55e32153 | |||
| f6b1f5c45a | |||
| 1f9a1822c9 | |||
| 63015c6bbc | |||
| 24b30df992 | |||
| 67b1003831 | |||
| 00d37bdaf0 | |||
| c5fcb1427e | |||
| 9118ebd662 | |||
| 19cdf47bed | |||
| 95bac94479 | |||
| 421a8934a7 | |||
| f7f45668d3 | |||
| c8e9a5b897 | |||
| a1bfa62796 | |||
| 64e11cbc3c | |||
| 15445aa13c | |||
| 52834047f2 | |||
| 76817675f1 | |||
| 2cce8f94c9 | |||
| 9b5f102d9f | |||
| 68de33165e | |||
| f56ad33366 | |||
| 35c63802b5 | |||
| 3458f6fe23 | |||
| 8f305033f7 | |||
| 4a18f1c418 | |||
| 373edb6d80 | |||
| 1c66e0e599 | |||
| 536e7720cb | |||
| 75eac4b1ce | |||
| 9ed0afee6b | |||
| f700473967 | |||
| 5fc0fd2a95 | |||
| 5f88c7ceac | |||
| 4c7ec65577 | |||
| 3e18299099 | |||
| 021be88880 | |||
| a479e81682 | |||
| c3e3012a94 | |||
| c9e9df3825 | |||
| 7ca2fa1bcc | |||
| 208c24682b | |||
| 316c784bde | |||
| 1467ae8a89 | |||
| 0ea3afbe70 | |||
| 4f1d7f495a | |||
| 7c4ecb013e | |||
| ed5976a22c |
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,8 +4,7 @@ node_modules
|
|||||||
.vscode-test/
|
.vscode-test/
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
# waveform_trace 打包产物(exe 太大,通过 Release 发布)
|
# waveform_trace 打包产物
|
||||||
tools/waveform_trace/bin/
|
|
||||||
tools/waveform_trace/src/build/
|
tools/waveform_trace/src/build/
|
||||||
tools/waveform_trace/src/dist/
|
tools/waveform_trace/src/dist/
|
||||||
tools/waveform_trace/src/*.spec
|
tools/waveform_trace/src/*.spec
|
||||||
|
|||||||
4
.npmrc
4
.npmrc
@ -1 +1,3 @@
|
|||||||
enable-pre-post-scripts = true
|
enable-pre-post-scripts = true
|
||||||
|
shamefully-hoist = true
|
||||||
|
public-hoist-pattern[] = *
|
||||||
33
.vscodeignore
Normal file
33
.vscodeignore
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# 开发文件
|
||||||
|
.vscode/**
|
||||||
|
.vscode-test/**
|
||||||
|
src/**
|
||||||
|
.gitignore
|
||||||
|
.yarnrc
|
||||||
|
vsc-extension-quickstart.md
|
||||||
|
**/tsconfig.json
|
||||||
|
**/.eslintrc.json
|
||||||
|
**/*.map
|
||||||
|
**/*.ts
|
||||||
|
|
||||||
|
# 测试文件
|
||||||
|
out/test/**
|
||||||
|
|
||||||
|
# 依赖
|
||||||
|
node_modules/**
|
||||||
|
|
||||||
|
# 文档(避免中文文件名打包问题)
|
||||||
|
docs/**
|
||||||
|
CLAUDE.md
|
||||||
|
|
||||||
|
# 只排除 waveform_trace 的 src/dist 目录
|
||||||
|
tools/waveform_trace/src/**
|
||||||
|
tools/iverilog/examples/**
|
||||||
|
tools/iverilog/INSTALL.md
|
||||||
|
tools/iverilog/README.md
|
||||||
|
tools/iverilog/DOWNLOAD_INSTRUCTIONS.md
|
||||||
|
|
||||||
|
|
||||||
|
# Git 相关
|
||||||
|
.git/**
|
||||||
|
.github/**
|
||||||
75
CHANGELOG.md
75
CHANGELOG.md
@ -2,22 +2,67 @@
|
|||||||
|
|
||||||
所有重要的项目变更都将记录在此文件中。
|
所有重要的项目变更都将记录在此文件中。
|
||||||
|
|
||||||
## [1.0.2] - 2026-01-13
|
## [1.0.12] - 2026-03-06
|
||||||
|
|
||||||
IC Coder插件端正式发布。
|
### 新增
|
||||||
|
|
||||||
IC Coder 插件端是一个专为 FPGA 开发设计的 VS Code 扩展,提供 AI 驱动的智能辅助功能。
|
- 支持 AskUserQuestion 多问题和多选功能
|
||||||
|
|
||||||
|
## [1.0.9] - 2026-03-04
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
|
||||||
|
- 将工具折叠图标颜色从蓝色改为灰色
|
||||||
|
- 统一使用蓝色主题色
|
||||||
|
- 优化打包配置,排除重复的 exe 文件
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 修复代码变更继续对话查找不到之前的代码变更信息的 bug
|
||||||
|
- 修复对话展示两遍的问题
|
||||||
|
|
||||||
|
## [1.0.8] - 2026-03-03
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 删除文件确认功能
|
||||||
|
- 文件路径标签显示
|
||||||
|
- 企业试用用户欢迎弹窗优化
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 修复继续对话时消息覆盖问题
|
||||||
|
- 修复试用用户欢迎弹窗显示逻辑
|
||||||
|
- 修复企业试用用户仍弹出邀请码的问题
|
||||||
|
- 修复登录过期点击重新登录失败的问题
|
||||||
|
|
||||||
|
## [1.0.7] - 2026-03-02
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 修复 AI 响应内容重复显示问题
|
||||||
|
|
||||||
|
## [1.0.6] - 2026-03-02
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- Git Diff 功能:支持查看当前文件的 Git 差异对比
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 修复添加上下文搜索选择文件不匹配的问题
|
||||||
|
- 修复过期认证状态未清除导致重新登录失败的问题
|
||||||
|
|
||||||
|
## [1.0.4] - 2026-01-28
|
||||||
|
|
||||||
|
IC Coder插件端正式上线。
|
||||||
|
|
||||||
|
IC Coder 插件端是一个是一个自主式人工智能 Verilog 编码平台,可以将芯片设计与验证的效率提升至少20倍!
|
||||||
|
|
||||||
主要功能:
|
主要功能:
|
||||||
- VCD波形解析
|
|
||||||
- 自动生成完整工程
|
- 自动搭建电路架构:够根据自然语言描述的设计需求,自动生成完整的电路架构
|
||||||
- 自动仿真
|
- AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码
|
||||||
- 自主代码迭代
|
- AI自主代码迭代:实现了真正的自主式开发循环,能够持续优化代码直到满足设计要求
|
||||||
- 智能匹配最优模型
|
- 随时可掌控:提供透明化的开发过程,让用户始终掌握AI的工作状态
|
||||||
- 多线程任务处理
|
- 多层次安全保障:将数据安全和隐私保护作为核心设计原则,提供企业级的安全保障
|
||||||
- 实时跟随
|
|
||||||
- 丰富的上下文工具
|
|
||||||
- 全双工交互
|
|
||||||
- 多层次安全保障
|
|
||||||
- 自动搭建电路架构
|
|
||||||
- 多平台支持
|
|
||||||
|
|||||||
@ -12,7 +12,6 @@ IC Coder是一款**The Agentic AI Verilog Coding Platform(自主式人工智
|
|||||||
|
|
||||||
- **多智能体架构(Multi-Agent System)**:多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节
|
- **多智能体架构(Multi-Agent System)**:多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节
|
||||||
- **增强上下文引擎**:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性
|
- **增强上下文引擎**:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性
|
||||||
- **自研EDA工具集**:完整的仿真、综合、时序分析工具链,无缝集成到AI工作流中
|
|
||||||
|
|
||||||
这些技术共同支撑着从需求分析、架构设计、代码生成到验证调试的全流程智能化开发体验。
|
这些技术共同支撑着从需求分析、架构设计、代码生成到验证调试的全流程智能化开发体验。
|
||||||
|
|
||||||
|
|||||||
261
docs/AskUserQuestion-API设计.md
Normal file
261
docs/AskUserQuestion-API设计.md
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
# AskUserQuestion 多选支持 - API 设计文档
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
当前 AI 询问用户问题时存在以下问题:
|
||||||
|
1. 后端返回的选项不准确
|
||||||
|
2. 多个问题只给几个选项
|
||||||
|
3. 不支持多选方式
|
||||||
|
|
||||||
|
## 需求
|
||||||
|
|
||||||
|
实现一个问题对应多个选项,支持多选的方式。
|
||||||
|
|
||||||
|
## 数据结构设计
|
||||||
|
|
||||||
|
### 后端返回格式
|
||||||
|
|
||||||
|
后端通过 SSE 的 `ask_user` 事件返回以下格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"askId": "ask_1234567890",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "请确认 SPI 控制器的配置需求:工作模式?",
|
||||||
|
"options": [
|
||||||
|
"Master/8位/模式0/固定分频/需要CS",
|
||||||
|
"Master/可配置位宽/可配置模式/需要CS",
|
||||||
|
"Slave模式"
|
||||||
|
],
|
||||||
|
"multiSelect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "数据位宽?",
|
||||||
|
"options": [
|
||||||
|
"8位 还是其他?"
|
||||||
|
],
|
||||||
|
"multiSelect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "时钟极性和相位?",
|
||||||
|
"options": [
|
||||||
|
"CPOL=0/CPHA=0 (模式0) 还是其他模式?"
|
||||||
|
],
|
||||||
|
"multiSelect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "时钟分频?",
|
||||||
|
"options": [
|
||||||
|
"需要可配置的分频比吗?"
|
||||||
|
],
|
||||||
|
"multiSelect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "是否需要芯片选信号 (CS) 控制?",
|
||||||
|
"options": [
|
||||||
|
"是",
|
||||||
|
"否"
|
||||||
|
],
|
||||||
|
"multiSelect": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端数据结构
|
||||||
|
|
||||||
|
#### 1. API 类型定义 (`src/types/api.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/** ask_user 事件数据 */
|
||||||
|
export interface AskUserEvent {
|
||||||
|
askId: string;
|
||||||
|
questions: QuestionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个问题项 */
|
||||||
|
export interface QuestionItem {
|
||||||
|
question: string;
|
||||||
|
options: string[];
|
||||||
|
multiSelect?: boolean; // 是否支持多选,默认 false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. MessageSegment 类型 (`src/services/dialogService.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface MessageSegment {
|
||||||
|
type: "text" | "tool" | "question" | "agent" | "plan" | "progress";
|
||||||
|
// ... 其他字段
|
||||||
|
askId?: string;
|
||||||
|
questions?: QuestionItem[]; // 改为问题数组
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 用户回答格式 (`src/types/api.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface AnswerRequest {
|
||||||
|
taskId: string;
|
||||||
|
askId: string;
|
||||||
|
answers: {
|
||||||
|
[questionIndex: number]: string[]; // 每个问题的答案数组(支持多选)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端实现要点
|
||||||
|
|
||||||
|
### 1. 显示多个问题
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 遍历 questions 数组,为每个问题生成 UI
|
||||||
|
segment.questions?.forEach((q, index) => {
|
||||||
|
// 显示问题标题
|
||||||
|
// 显示选项(单选或多选)
|
||||||
|
// 收集答案
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 多选支持
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (q.multiSelect) {
|
||||||
|
// 渲染复选框
|
||||||
|
// 允许选择多个选项
|
||||||
|
} else {
|
||||||
|
// 渲染单选按钮
|
||||||
|
// 只允许选择一个选项
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 提交答案
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const answers = {
|
||||||
|
0: ["Master/8位/模式0/固定分频/需要CS"], // 第1个问题的答案
|
||||||
|
1: ["8位 还是其他?"], // 第2个问题的答案
|
||||||
|
2: ["CPOL=0/CPHA=0 (模式0) 还是其他模式?"], // 第3个问题的答案
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'userAnswer',
|
||||||
|
askId: 'ask_1234567890',
|
||||||
|
answers: answers
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后端需要做的修改
|
||||||
|
|
||||||
|
### 1. 修改 AskUserQuestion 工具的返回格式
|
||||||
|
|
||||||
|
从:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"askId": "xxx",
|
||||||
|
"question": "单个问题",
|
||||||
|
"options": ["选项1", "选项2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
改为:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"askId": "xxx",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "问题1",
|
||||||
|
"options": ["选项1", "选项2"],
|
||||||
|
"multiSelect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "问题2",
|
||||||
|
"options": ["选项A", "选项B", "选项C"],
|
||||||
|
"multiSelect": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 接收答案的格式
|
||||||
|
|
||||||
|
从:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "xxx",
|
||||||
|
"askId": "xxx",
|
||||||
|
"selected": ["选项1"],
|
||||||
|
"customInput": "自定义输入"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
改为:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "xxx",
|
||||||
|
"askId": "xxx",
|
||||||
|
"answers": {
|
||||||
|
"0": ["选项1"], // 第1个问题的答案
|
||||||
|
"1": ["选项A", "选项B"] // 第2个问题的答案(多选)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例场景
|
||||||
|
|
||||||
|
### 场景:SPI 控制器配置
|
||||||
|
|
||||||
|
**后端发送:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"askId": "ask_spi_config",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "工作模式?",
|
||||||
|
"options": [
|
||||||
|
"Master/8位/模式0/固定分频/需要CS",
|
||||||
|
"Master/可配置位宽/可配置模式/需要CS",
|
||||||
|
"Slave模式"
|
||||||
|
],
|
||||||
|
"multiSelect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "需要哪些功能?",
|
||||||
|
"options": [
|
||||||
|
"可配置时钟分频",
|
||||||
|
"可配置数据位宽",
|
||||||
|
"支持多个CS",
|
||||||
|
"DMA支持"
|
||||||
|
],
|
||||||
|
"multiSelect": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户选择:**
|
||||||
|
- 问题1:选择 "Master/8位/模式0/固定分频/需要CS"
|
||||||
|
- 问题2:选择 "可配置时钟分频" 和 "可配置数据位宽"
|
||||||
|
|
||||||
|
**前端提交:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "task_xxx",
|
||||||
|
"askId": "ask_spi_config",
|
||||||
|
"answers": {
|
||||||
|
"0": ["Master/8位/模式0/固定分频/需要CS"],
|
||||||
|
"1": ["可配置时钟分频", "可配置数据位宽"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
这个设计方案:
|
||||||
|
1. ✅ 支持多个问题
|
||||||
|
2. ✅ 每个问题有多个选项
|
||||||
|
3. ✅ 支持单选和多选
|
||||||
|
4. ✅ 数据结构清晰,易于扩展
|
||||||
|
5. ✅ 向后兼容(可以只有一个问题)
|
||||||
526
docs/EDA联动功能需求文档.md
Normal file
526
docs/EDA联动功能需求文档.md
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
# Vivado 联动功能需求文档
|
||||||
|
|
||||||
|
## 1. 项目背景
|
||||||
|
|
||||||
|
### 1.1 当前状态
|
||||||
|
|
||||||
|
IC Coder Plugin 目前支持:
|
||||||
|
|
||||||
|
- iverilog 仿真(内置 Windows 版本)
|
||||||
|
- VCD 波形查看
|
||||||
|
- Verilog 代码生成和文件操作
|
||||||
|
|
||||||
|
### 1.2 需求来源
|
||||||
|
|
||||||
|
用户需要在 VS Code 中直接调用本地 Vivado 工具,并将产出文件自动导入到项目中,完成从仿真到 FPGA 部署的完整流程。
|
||||||
|
|
||||||
|
### 1.3 Vivado 是什么?
|
||||||
|
|
||||||
|
**Vivado** 是 Xilinx(现 AMD)的 FPGA 开发工具,用于将 Verilog 代码部署到 FPGA 硬件:
|
||||||
|
|
||||||
|
- **综合(Synthesis)**:将 RTL 代码转换为门级网表
|
||||||
|
- **实现(Implementation)**:布局布线,映射到具体 FPGA 芯片
|
||||||
|
- **生成比特流(Bitstream)**:生成 .bit 配置文件用于烧录
|
||||||
|
|
||||||
|
**与 iverilog 的区别**:
|
||||||
|
|
||||||
|
- iverilog:只做**仿真验证**(软件层面验证逻辑)
|
||||||
|
- Vivado:做**综合+实现+生成配置文件**(真正部署到硬件)
|
||||||
|
|
||||||
|
**典型开发流程**:
|
||||||
|
|
||||||
|
```
|
||||||
|
编写 Verilog → iverilog 仿真验证 → Vivado 综合 → Vivado 实现 → 生成 .bit 文件 → 烧录到 FPGA
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 功能目标
|
||||||
|
|
||||||
|
### 2.1 核心目标
|
||||||
|
|
||||||
|
- **前端提供原子工具**:前端只提供独立的 Vivado 命令工具,不控制流程
|
||||||
|
- **后端AI控制流程**:所有执行顺序、依赖检查由后端AI决策
|
||||||
|
- **工具职责单一**:每个工具只负责执行一个具体命令
|
||||||
|
- **结果透明返回**:执行结果完整返回给后端,由后端决定下一步
|
||||||
|
|
||||||
|
### 2.2 设计原则
|
||||||
|
|
||||||
|
- 前端不做流程判断,只执行命令
|
||||||
|
- 前端不检查依赖关系,由后端保证顺序
|
||||||
|
- 前端返回详细的执行结果,包括成功/失败、输出、报告等
|
||||||
|
- 后端AI根据结果智能决策是否继续
|
||||||
|
|
||||||
|
## 3. 功能详细需求
|
||||||
|
|
||||||
|
### 3.1 前端提供的工具
|
||||||
|
|
||||||
|
前端提供 4 个独立的工具,每个工具只负责执行一个命令:
|
||||||
|
|
||||||
|
#### 3.1.1 createVivadoProject - 创建工程
|
||||||
|
|
||||||
|
- **输入**:项目名称、芯片型号、源文件列表、约束文件(可选)
|
||||||
|
- **输出**:工程文件(.xpr)
|
||||||
|
- **说明**:创建 Vivado 工程,不执行任何构建操作
|
||||||
|
|
||||||
|
#### 3.1.2 runVivadoSynthesis - 综合
|
||||||
|
|
||||||
|
- **输入**:工程路径或源文件、芯片型号、顶层模块、约束文件(可选)
|
||||||
|
- **输出**:.dcp 文件、综合报告
|
||||||
|
- **说明**:执行综合,前端不检查工程是否存在。约束文件在此阶段可选,主要用于时序约束
|
||||||
|
|
||||||
|
#### 3.1.3 runVivadoImplementation - 实现
|
||||||
|
|
||||||
|
- **输入**:综合后的 .dcp 文件路径、约束文件(必需,包含管脚约束)
|
||||||
|
- **输出**:实现后的 .dcp 文件、时序报告
|
||||||
|
- **说明**:执行实现,前端不检查 .dcp 是否存在。**管脚约束是必需的**,否则无法完成布局布线
|
||||||
|
|
||||||
|
#### 3.1.4 runVivadoBitstream - 生成比特流
|
||||||
|
|
||||||
|
- **输入**:实现后的 .dcp 文件路径
|
||||||
|
- **输出**:.bit 文件(可下载到 FPGA 的配置文件)
|
||||||
|
- **核心依赖**:
|
||||||
|
1. 实现已完成
|
||||||
|
2. 工程指定目标芯片型号
|
||||||
|
3. 已完成管脚约束(无管脚约束无法生成)
|
||||||
|
- **说明**:生成比特流,前端不检查 .dcp 是否存在
|
||||||
|
|
||||||
|
### 3.2 配置管理
|
||||||
|
|
||||||
|
#### 3.2.1 配置项
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vivado": {
|
||||||
|
"enabled": true,
|
||||||
|
"executablePath": "C:/Xilinx/Vivado/2023.1/bin/vivado.bat",
|
||||||
|
"workingDir": "${workspaceFolder}/vivado_project",
|
||||||
|
"part": "xc7a35tcpg236-1", // FPGA 型号
|
||||||
|
"commands": {
|
||||||
|
"synthesis": "vivado -mode batch -source synth.tcl",
|
||||||
|
"implementation": "vivado -mode batch -source impl.tcl",
|
||||||
|
"bitstream": "vivado -mode batch -source bitstream.tcl"
|
||||||
|
},
|
||||||
|
"outputFiles": {
|
||||||
|
"synthesis": ["*.dcp", "*_synth.rpt"],
|
||||||
|
"implementation": ["*.dcp", "*_timing.rpt", "*_utilization.rpt"],
|
||||||
|
"bitstream": ["*.bit"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.2 存储位置
|
||||||
|
|
||||||
|
- 全局配置:VS Code Settings(`settings.json`)
|
||||||
|
- 项目配置:`.vscode/ic-coder-vivado.json`(优先级更高)
|
||||||
|
|
||||||
|
### 3.3 工具调用接口
|
||||||
|
|
||||||
|
#### 3.3.1 通用响应格式
|
||||||
|
|
||||||
|
所有工具返回统一的响应格式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface VivadoToolResponse {
|
||||||
|
success: boolean; // 是否成功
|
||||||
|
command: string; // 执行的命令
|
||||||
|
executionTime: number; // 执行时间(毫秒)
|
||||||
|
output: string; // 完整输出日志
|
||||||
|
error?: string; // 错误信息(如果失败)
|
||||||
|
outputFiles?: string[]; // 产出文件路径列表
|
||||||
|
reports?: {
|
||||||
|
resources?: string; // 资源使用摘要
|
||||||
|
timing?: string; // 时序信息摘要
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.2 各工具的参数定义
|
||||||
|
|
||||||
|
**createVivadoProject**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
projectName: string; // 项目名称
|
||||||
|
part: string; // 芯片型号
|
||||||
|
topModule: string; // 顶层模块
|
||||||
|
files: string[]; // 源文件列表
|
||||||
|
constraints?: string; // 约束文件(可选)
|
||||||
|
mode: 'gui' | 'batch'; // 执行模式
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**runVivadoSynthesis**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
projectPath?: string; // 工程路径(可选,如果有工程)
|
||||||
|
part: string; // 芯片型号
|
||||||
|
topModule: string; // 顶层模块
|
||||||
|
files?: string[]; // 源文件(如果没有工程)
|
||||||
|
constraints?: string; // 约束文件(可选)
|
||||||
|
mode: 'gui' | 'batch'; // 执行模式
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**runVivadoImplementation**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||||
|
constraints: string; // 约束文件(必需,包含管脚约束)
|
||||||
|
mode: 'gui' | 'batch'; // 执行模式
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**runVivadoBitstream**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||||
|
mode: 'gui' | 'batch'; // 执行模式
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 后端AI的职责
|
||||||
|
|
||||||
|
后端AI负责:
|
||||||
|
1. 询问用户必要参数(芯片型号、执行模式等)
|
||||||
|
2. 理解用户意图,决定调用哪些工具
|
||||||
|
3. 按正确顺序调用工具(遵循依赖关系)
|
||||||
|
4. 检查每步执行结果,决定是否继续
|
||||||
|
5. 汇总结果并展示给用户
|
||||||
|
|
||||||
|
#### 3.4.1 询问用户参数
|
||||||
|
|
||||||
|
后端必须询问:
|
||||||
|
- **芯片型号**(必需):"请提供 FPGA 芯片型号(例如:xc7a35tcpg236-1)"
|
||||||
|
- **执行模式**(必需):"选择执行模式:1) 图形化 2) 后端执行"
|
||||||
|
- **约束文件**(必需):"请提供约束文件(.xdc),包含管脚约束和时序约束"
|
||||||
|
|
||||||
|
#### 3.4.2 理解依赖关系
|
||||||
|
|
||||||
|
后端AI需要理解:
|
||||||
|
```
|
||||||
|
创建工程 → 综合 → 实现 → 生成比特流
|
||||||
|
```
|
||||||
|
|
||||||
|
如果用户说"做实现",后端应该:
|
||||||
|
1. 先调用 `createVivadoProject` 创建工程
|
||||||
|
2. 再调用 `runVivadoSynthesis` 执行综合
|
||||||
|
3. 最后调用 `runVivadoImplementation` 执行实现
|
||||||
|
|
||||||
|
#### 3.4.3 逐步调用工具
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤1: 调用 createVivadoProject
|
||||||
|
检查 response.success
|
||||||
|
如果失败 → 停止并报错
|
||||||
|
|
||||||
|
步骤2: 调用 runVivadoSynthesis
|
||||||
|
检查 response.success
|
||||||
|
如果失败 → 停止并报错
|
||||||
|
|
||||||
|
步骤3: 调用 runVivadoImplementation
|
||||||
|
检查 response.success
|
||||||
|
返回最终结果
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 UI 交互
|
||||||
|
|
||||||
|
#### 3.5.1 配置界面
|
||||||
|
|
||||||
|
- 在设置页面添加 "Vivado 配置" 选项
|
||||||
|
- 支持配置 Vivado 路径、FPGA 型号
|
||||||
|
- 支持测试 Vivado 可用性(点击按钮测试)
|
||||||
|
|
||||||
|
#### 3.5.2 调用界面
|
||||||
|
|
||||||
|
- 在聊天面板中,AI 可以建议使用 Vivado
|
||||||
|
- 用户确认后,显示执行进度对话框
|
||||||
|
- 实时显示日志输出(可折叠)
|
||||||
|
- 显示执行状态:准备中 → 执行中 → 完成/失败
|
||||||
|
|
||||||
|
#### 3.5.3 结果展示
|
||||||
|
|
||||||
|
- 执行成功:显示执行时间、资源使用、时序信息
|
||||||
|
- 执行失败:显示错误信息、建议解决方案
|
||||||
|
- 导入文件:高亮显示已导入的文件,支持点击打开报告
|
||||||
|
|
||||||
|
### 3.6 后端集成
|
||||||
|
|
||||||
|
#### 3.6.1 工具定义
|
||||||
|
|
||||||
|
后端注册 4 个独立工具:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "createVivadoProject",
|
||||||
|
"description": "创建 Vivado 工程。需要先询问用户芯片型号和执行模式。",
|
||||||
|
"parameters": {
|
||||||
|
"projectName": "项目名称",
|
||||||
|
"part": "芯片型号(必须从用户获取)",
|
||||||
|
"topModule": "顶层模块名",
|
||||||
|
"files": "源文件列表",
|
||||||
|
"constraints": "约束文件(可选)",
|
||||||
|
"mode": "执行模式(gui/batch)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "runVivadoSynthesis",
|
||||||
|
"description": "执行 Vivado 综合。前端不检查依赖,后端需确保工程已创建。",
|
||||||
|
"parameters": {
|
||||||
|
"projectPath": "工程路径(可选)",
|
||||||
|
"part": "芯片型号",
|
||||||
|
"topModule": "顶层模块",
|
||||||
|
"files": "源文件(如果没有工程)",
|
||||||
|
"constraints": "约束文件(可选)",
|
||||||
|
"mode": "执行模式(gui/batch)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "runVivadoImplementation",
|
||||||
|
"description": "执行 Vivado 实现。前端不检查依赖,后端需确保综合已完成且提供约束文件。",
|
||||||
|
"parameters": {
|
||||||
|
"dcpFile": "综合后的 .dcp 文件路径",
|
||||||
|
"constraints": "约束文件(必需,包含管脚约束)",
|
||||||
|
"mode": "执行模式(gui/batch)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "runVivadoBitstream",
|
||||||
|
"description": "生成比特流。前端不检查依赖,后端需确保实现已完成。",
|
||||||
|
"parameters": {
|
||||||
|
"dcpFile": "实现后的 .dcp 文件路径",
|
||||||
|
"mode": "执行模式(gui/batch)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.6.2 后端调用示例
|
||||||
|
|
||||||
|
**场景:用户要求完整流程**
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: 用 Vivado 跑完整流程
|
||||||
|
|
||||||
|
AI: 请提供芯片型号和执行模式
|
||||||
|
用户: xc7a35tcpg236-1,后端执行
|
||||||
|
|
||||||
|
AI 执行:
|
||||||
|
1. [调用] createVivadoProject({ projectName: "counter", part: "xc7a35tcpg236-1", ... })
|
||||||
|
[结果] { success: true, outputFiles: ["counter.xpr"] }
|
||||||
|
|
||||||
|
2. [调用] runVivadoSynthesis({ projectPath: "counter.xpr", ... })
|
||||||
|
[结果] { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||||
|
|
||||||
|
3. [调用] runVivadoImplementation({ dcpFile: "counter_synth.dcp", constraints: "counter.xdc", ... })
|
||||||
|
[结果] { success: true, outputFiles: ["counter_impl.dcp"], reports: {...} }
|
||||||
|
|
||||||
|
4. [调用] runVivadoBitstream({ dcpFile: "counter_impl.dcp", ... })
|
||||||
|
[结果] { success: true, outputFiles: ["counter.bit"] }
|
||||||
|
|
||||||
|
AI 回复: 完成!已生成 counter.bit 文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 用户场景
|
||||||
|
|
||||||
|
### 4.1 场景一:完整流程执行
|
||||||
|
|
||||||
|
**用户输入**:"用 Vivado 跑完整个 FPGA 流程"
|
||||||
|
|
||||||
|
**AI 交互**:
|
||||||
|
```
|
||||||
|
AI: 好的,我将使用 Vivado 执行完整的 FPGA 开发流程。请提供以下信息:
|
||||||
|
|
||||||
|
1. FPGA 芯片型号(例如:xc7a35tcpg236-1)
|
||||||
|
2. 请选择要执行的步骤(可多选):
|
||||||
|
- 综合(Synthesis)
|
||||||
|
- 实现(Implementation)
|
||||||
|
- 生成比特流(Bitstream)
|
||||||
|
3. 执行模式:
|
||||||
|
- 图形化模式:打开 Vivado GUI
|
||||||
|
- 后端执行:自动在后台执行
|
||||||
|
4. 约束文件路径(.xdc,必需)
|
||||||
|
|
||||||
|
用户: xc7a35tcpg236-1,全部步骤,后端执行,约束文件是 counter.xdc
|
||||||
|
|
||||||
|
AI: 收到!执行计划:
|
||||||
|
1. 创建工程
|
||||||
|
2. 综合
|
||||||
|
3. 实现
|
||||||
|
4. 生成比特流
|
||||||
|
|
||||||
|
开始执行...
|
||||||
|
|
||||||
|
[步骤 1/4] 创建工程...
|
||||||
|
✓ 工程创建成功(耗时 2s)
|
||||||
|
|
||||||
|
[步骤 2/4] 综合中...
|
||||||
|
✓ 综合完成(耗时 45s)
|
||||||
|
资源使用:LUT 32/20800 (0.15%), FF 8/41600 (0.02%)
|
||||||
|
|
||||||
|
[步骤 3/4] 实现中...
|
||||||
|
✓ 实现完成(耗时 120s)
|
||||||
|
时序:WNS 5.234ns, TNS 0.000ns(满足要求)
|
||||||
|
|
||||||
|
[步骤 4/4] 生成比特流...
|
||||||
|
✓ 比特流生成完成(耗时 15s)
|
||||||
|
|
||||||
|
所有步骤执行完毕!产出文件已导入到 vivado_output/ 目录。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 场景二:只执行实现(自动补充依赖)
|
||||||
|
|
||||||
|
**用户输入**:"用 Vivado 做实现"
|
||||||
|
|
||||||
|
**AI 交互**:
|
||||||
|
```
|
||||||
|
AI: 请提供以下信息:
|
||||||
|
1. FPGA 芯片型号
|
||||||
|
2. 执行模式(图形化/后端执行)
|
||||||
|
|
||||||
|
用户: xc7a35tcpg236-1,后端执行
|
||||||
|
|
||||||
|
AI: 注意:实现依赖综合,我将自动执行以下步骤:
|
||||||
|
1. 创建工程
|
||||||
|
2. 综合(前置依赖)
|
||||||
|
3. 实现(您选择的步骤)
|
||||||
|
|
||||||
|
是否继续?
|
||||||
|
|
||||||
|
用户: 是
|
||||||
|
|
||||||
|
AI: 开始执行...
|
||||||
|
[自动执行创建工程 → 综合 → 实现]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 场景三:图形化模式
|
||||||
|
|
||||||
|
**用户输入**:"打开 Vivado 图形界面做综合"
|
||||||
|
|
||||||
|
**AI 交互**:
|
||||||
|
```
|
||||||
|
AI: 请提供芯片型号:
|
||||||
|
用户: xc7a35tcpg236-1
|
||||||
|
|
||||||
|
AI: 正在创建工程并打开 Vivado GUI...
|
||||||
|
✓ 工程已创建
|
||||||
|
✓ Vivado 图形界面已启动
|
||||||
|
|
||||||
|
您现在可以在 Vivado 中手动操作。完成后,产出文件将保存在工程目录中。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 场景四:执行中断处理
|
||||||
|
|
||||||
|
**执行过程**:
|
||||||
|
```
|
||||||
|
[步骤 1/3] 创建工程...
|
||||||
|
✓ 成功
|
||||||
|
|
||||||
|
[步骤 2/3] 综合中...
|
||||||
|
✗ 综合失败
|
||||||
|
错误:ERROR: [Synth 8-439] module 'counter' not found
|
||||||
|
|
||||||
|
AI: 综合失败,发现以下问题:
|
||||||
|
- 找不到模块 'counter'
|
||||||
|
|
||||||
|
建议检查:
|
||||||
|
1. 模块名是否正确
|
||||||
|
2. 文件中是否定义了该模块
|
||||||
|
3. 是否有语法错误
|
||||||
|
|
||||||
|
执行已停止,请修复错误后重试。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 技术约束
|
||||||
|
|
||||||
|
### 5.1 平台兼容性
|
||||||
|
|
||||||
|
- Windows:支持 `.bat` 可执行文件
|
||||||
|
- Linux:支持 shell 脚本
|
||||||
|
- 路径分隔符自动适配
|
||||||
|
|
||||||
|
### 5.2 性能要求
|
||||||
|
|
||||||
|
- 命令执行不阻塞 UI
|
||||||
|
- 综合时间可能较长(分钟级),需要进度提示
|
||||||
|
- 日志输出实时更新,限制缓冲区大小
|
||||||
|
|
||||||
|
### 5.3 安全性
|
||||||
|
|
||||||
|
- 工作目录限制在项目范围内
|
||||||
|
- 许可证路径不记录到日志
|
||||||
|
|
||||||
|
## 6. 验收标准
|
||||||
|
|
||||||
|
### 6.1 功能验收
|
||||||
|
|
||||||
|
- [ ] 用户可以配置 Vivado 路径和 FPGA 型号
|
||||||
|
- [ ] AI 可以通过工具调用成功执行 Vivado 综合
|
||||||
|
- [ ] 产出文件自动导入到指定目录
|
||||||
|
- [ ] 执行过程有清晰的进度提示
|
||||||
|
- [ ] 报告文件可以正常打开查看
|
||||||
|
|
||||||
|
### 6.2 性能验收
|
||||||
|
|
||||||
|
- [ ] 小型项目综合时间 < 1 分钟
|
||||||
|
- [ ] UI 响应流畅,不卡顿
|
||||||
|
- [ ] 日志输出实时更新(延迟 < 500ms)
|
||||||
|
|
||||||
|
### 6.3 用户体验验收
|
||||||
|
|
||||||
|
- [ ] 配置界面直观易用
|
||||||
|
- [ ] 首次使用有引导提示
|
||||||
|
- [ ] 错误提示清晰,有解决建议
|
||||||
|
- [ ] 导入的文件可以直接打开查看
|
||||||
|
|
||||||
|
## 7. 风险和依赖
|
||||||
|
|
||||||
|
### 7.1 风险
|
||||||
|
|
||||||
|
- **Vivado 版本差异**:不同版本的命令行参数可能不同
|
||||||
|
- **许可证问题**:Vivado 需要许可证才能运行
|
||||||
|
- **路径问题**:Windows 路径中的空格和特殊字符
|
||||||
|
- **执行时间长**:大型项目可能需要数十分钟
|
||||||
|
|
||||||
|
### 7.2 依赖
|
||||||
|
|
||||||
|
- 用户需要自行安装 Vivado
|
||||||
|
- 用户需要配置正确的 Vivado 路径
|
||||||
|
- 需要设置环境变量(如 `XILINX_VIVADO`)
|
||||||
|
- 需要有效的 Vivado 许可证
|
||||||
|
- **需要提供 .xdc 约束文件**:
|
||||||
|
- **管脚约束**(必需):定义信号与 FPGA 引脚的映射关系,实现阶段必须提供
|
||||||
|
- **时序约束**(强烈推荐):定义时钟频率和时序要求,确保设计满足性能指标
|
||||||
|
|
||||||
|
## 8. 后续扩展
|
||||||
|
|
||||||
|
### 8.1 短期扩展
|
||||||
|
|
||||||
|
- 支持自定义 TCL 脚本模板
|
||||||
|
- 支持批量处理多个设计
|
||||||
|
- 支持时序约束编辑器
|
||||||
|
|
||||||
|
### 8.2 长期扩展
|
||||||
|
|
||||||
|
- 支持其他 FPGA 工具(Quartus)
|
||||||
|
- 云端 Vivado 服务集成
|
||||||
|
- 结果对比和版本管理
|
||||||
|
- 性能分析和优化建议
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录
|
||||||
|
|
||||||
|
### A. Vivado 命令行参考
|
||||||
|
|
||||||
|
- 官方文档:https://docs.xilinx.com/
|
||||||
|
- TCL 命令参考:UG835
|
||||||
|
- 设计流程参考:UG892
|
||||||
|
|
||||||
|
### B. 术语表
|
||||||
|
|
||||||
|
- **RTL**:Register Transfer Level,寄存器传输级
|
||||||
|
- **综合**:Synthesis,将 RTL 代码转换为门级网表
|
||||||
|
- **实现**:Implementation,布局布线
|
||||||
|
- **比特流**:Bitstream,FPGA 配置文件
|
||||||
|
- **DCP**:Design Checkpoint,Vivado 设计检查点文件
|
||||||
|
- **XDC**:Xilinx Design Constraints,约束文件
|
||||||
|
- **LUT**:Look-Up Table,查找表(FPGA 基本逻辑单元)
|
||||||
|
- **FF**:Flip-Flop,触发器
|
||||||
@ -88,6 +88,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
|||||||
5. 点击 **Create** 完成创建
|
5. 点击 **Create** 完成创建
|
||||||
|
|
||||||
**注意事项:**
|
**注意事项:**
|
||||||
|
|
||||||
- Publisher ID 一旦创建无法修改
|
- Publisher ID 一旦创建无法修改
|
||||||
- Publisher ID 必须全局唯一
|
- Publisher ID 必须全局唯一
|
||||||
- 建议使用有意义且专业的 ID
|
- 建议使用有意义且专业的 ID
|
||||||
@ -126,6 +127,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
|||||||
## [0.0.2] - 2025-12-29
|
## [0.0.2] - 2025-12-29
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|
||||||
- 添加发送和暂停按钮功能
|
- 添加发送和暂停按钮功能
|
||||||
- 添加一键优化按钮组件
|
- 添加一键优化按钮组件
|
||||||
- 添加 Plan 开关组件
|
- 添加 Plan 开关组件
|
||||||
@ -133,11 +135,13 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
|||||||
- 添加上下文压缩功能
|
- 添加上下文压缩功能
|
||||||
|
|
||||||
### 改进
|
### 改进
|
||||||
|
|
||||||
- 优化用户界面交互体验
|
- 优化用户界面交互体验
|
||||||
|
|
||||||
## [0.0.1] - 2025-12-XX
|
## [0.0.1] - 2025-12-XX
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|
||||||
- 初始版本发布
|
- 初始版本发布
|
||||||
- Verilog 代码智能生成
|
- Verilog 代码智能生成
|
||||||
- 集成 iverilog 仿真工具
|
- 集成 iverilog 仿真工具
|
||||||
@ -161,6 +165,7 @@ in the Software without restriction...
|
|||||||
### 4. 优化 README.md
|
### 4. 优化 README.md
|
||||||
|
|
||||||
确保 README 包含:
|
确保 README 包含:
|
||||||
|
|
||||||
- 清晰的功能介绍
|
- 清晰的功能介绍
|
||||||
- 使用截图或 GIF 演示
|
- 使用截图或 GIF 演示
|
||||||
- 详细的使用说明
|
- 详细的使用说明
|
||||||
@ -219,6 +224,7 @@ pnpm vsce publish
|
|||||||
**步骤:**
|
**步骤:**
|
||||||
|
|
||||||
1. 本地打包插件:
|
1. 本地打包插件:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run package
|
pnpm run package
|
||||||
pnpm vsce package[pnpm vsce package --no-dependencies]
|
pnpm vsce package[pnpm vsce package --no-dependencies]
|
||||||
@ -257,7 +263,7 @@ pnpm vsce publish major
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 发布指定版本
|
# 发布指定版本
|
||||||
pnpm vsce publish 0.0.3
|
npx vsce publish --packagePath iccoder-1.0.7.vsix
|
||||||
```
|
```
|
||||||
|
|
||||||
### 更新流程建议
|
### 更新流程建议
|
||||||
@ -268,8 +274,6 @@ pnpm vsce publish 0.0.3
|
|||||||
4. 执行发布命令
|
4. 执行发布命令
|
||||||
5. 验证市场上的插件是否正常
|
5. 验证市场上的插件是否正常
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 更新流程
|
## 更新流程
|
||||||
|
|
||||||
1. 修改版本号
|
1. 修改版本号
|
||||||
@ -281,10 +285,10 @@ pnpm vsce publish 0.0.3
|
|||||||
```bash
|
```bash
|
||||||
#补丁版本 (1.0.0 -> 1.0.1)
|
#补丁版本 (1.0.0 -> 1.0.1)
|
||||||
pnpm version patch
|
pnpm version patch
|
||||||
|
|
||||||
#次要版本 (1.0.0 -> 1.1.0)
|
#次要版本 (1.0.0 -> 1.1.0)
|
||||||
pnpm version minor
|
pnpm version minor
|
||||||
|
|
||||||
#主要版本 (1.0.0 -> 2.0.0)
|
#主要版本 (1.0.0 -> 2.0.0)
|
||||||
pnpm version major
|
pnpm version major
|
||||||
```
|
```
|
||||||
@ -292,17 +296,17 @@ pnpm vsce publish 0.0.3
|
|||||||
2. 打包
|
2. 打包
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#先build
|
#先编译
|
||||||
|
pnpm run compile
|
||||||
|
|
||||||
|
#中间build
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
#后打包成.vsix
|
#后打包成.vsix
|
||||||
pnpm vsce package --no-dependencies
|
pnpm vsce package --no-dependencies
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
3. 手动上传/命令上传
|
3. 手动上传/命令上传
|
||||||
|
|
||||||
- https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update
|
- https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update
|
||||||
- 命令上传:vsce publish
|
- 命令上传:vsce publish
|
||||||
|
|
||||||
@ -315,6 +319,7 @@ pnpm vsce publish 0.0.3
|
|||||||
**原因:** PAT Token 无效或过期
|
**原因:** PAT Token 无效或过期
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
- 重新生成 PAT Token
|
- 重新生成 PAT Token
|
||||||
- 重新登录:`pnpm vsce login ic-coder-team`
|
- 重新登录:`pnpm vsce login ic-coder-team`
|
||||||
|
|
||||||
@ -323,6 +328,7 @@ pnpm vsce publish 0.0.3
|
|||||||
**原因:** Publisher ID 不存在或不匹配
|
**原因:** Publisher ID 不存在或不匹配
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
- 检查 `package.json` 中的 `publisher` 字段
|
- 检查 `package.json` 中的 `publisher` 字段
|
||||||
- 确认已在市场创建对应的 Publisher
|
- 确认已在市场创建对应的 Publisher
|
||||||
|
|
||||||
@ -331,17 +337,20 @@ pnpm vsce publish 0.0.3
|
|||||||
**原因:** 必需文件缺失
|
**原因:** 必需文件缺失
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
- 确保 `dist/` 目录存在且包含编译后的代码
|
- 确保 `dist/` 目录存在且包含编译后的代码
|
||||||
- 运行 `pnpm run package` 重新构建
|
- 运行 `pnpm run package` 重新构建
|
||||||
|
|
||||||
### 4. 插件审核被拒
|
### 4. 插件审核被拒
|
||||||
|
|
||||||
**常见原因:**
|
**常见原因:**
|
||||||
|
|
||||||
- 插件名称或描述违反市场规则
|
- 插件名称或描述违反市场规则
|
||||||
- 图标不符合要求(建议 128x128 PNG)
|
- 图标不符合要求(建议 128x128 PNG)
|
||||||
- README 内容不完整
|
- README 内容不完整
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
- 查看审核反馈邮件
|
- 查看审核反馈邮件
|
||||||
- 修改相关内容后重新发布
|
- 修改相关内容后重新发布
|
||||||
|
|
||||||
@ -363,6 +372,7 @@ code --install-extension ic-coder-plugin-0.0.2.vsix
|
|||||||
```
|
```
|
||||||
|
|
||||||
或者在 VS Code 中:
|
或者在 VS Code 中:
|
||||||
|
|
||||||
1. 打开扩展面板
|
1. 打开扩展面板
|
||||||
2. 点击 `...` 菜单
|
2. 点击 `...` 菜单
|
||||||
3. 选择 **Install from VSIX...**
|
3. 选择 **Install from VSIX...**
|
||||||
804
docs/VSCode-Extension-API-Guide.md
Normal file
804
docs/VSCode-Extension-API-Guide.md
Normal file
@ -0,0 +1,804 @@
|
|||||||
|
# VS Code Extension API 核心知识点
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
- [1. Extension 生命周期](#1-extension-生命周期) ⭐⭐⭐
|
||||||
|
- [2. 激活事件 (Activation Events)](#2-激活事件-activation-events) ⭐⭐
|
||||||
|
- [3. 命令系统 (Commands)](#3-命令系统-commands) ⭐⭐
|
||||||
|
- [4. Webview API](#4-webview-api) ⭐⭐⭐⭐⭐ **面试重点**
|
||||||
|
- [5. TreeView 和自定义视图](#5-treeview-和自定义视图) ⭐⭐
|
||||||
|
- [6. 文件系统操作](#6-文件系统操作) ⭐⭐⭐
|
||||||
|
- [7. 配置和存储](#7-配置和存储) ⭐⭐⭐⭐ **面试重点**
|
||||||
|
- [8. 消息通知](#8-消息通知) ⭐
|
||||||
|
- [9. 语言特性支持](#9-语言特性支持) ⭐
|
||||||
|
- [10. 调试和诊断](#10-调试和诊断) ⭐
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Extension 生命周期 ⭐⭐⭐
|
||||||
|
|
||||||
|
### 1.1 核心函数 🔥必考
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// extension.ts
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
// 插件激活时调用(只调用一次)
|
||||||
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
|
console.log('Extension is now active!');
|
||||||
|
|
||||||
|
// 注册命令、视图、事件监听等
|
||||||
|
// 使用 context.subscriptions 管理资源
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件停用时调用(清理资源)
|
||||||
|
export function deactivate() {
|
||||||
|
console.log('Extension is deactivated');
|
||||||
|
// 清理资源、关闭连接等
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 ExtensionContext 重要属性 🔥必考
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ExtensionContext {
|
||||||
|
// 插件订阅管理(自动清理)
|
||||||
|
subscriptions: { dispose(): any }[];
|
||||||
|
|
||||||
|
// 工作区存储路径
|
||||||
|
storageUri: vscode.Uri | undefined;
|
||||||
|
globalStorageUri: vscode.Uri;
|
||||||
|
|
||||||
|
// 插件路径
|
||||||
|
extensionUri: vscode.Uri;
|
||||||
|
extensionPath: string;
|
||||||
|
|
||||||
|
// 状态存储
|
||||||
|
workspaceState: Memento; // 工作区级别
|
||||||
|
globalState: Memento; // 全局级别
|
||||||
|
secrets: SecretStorage; // 敏感信息存储
|
||||||
|
|
||||||
|
// 环境变量
|
||||||
|
environmentVariableCollection: EnvironmentVariableCollection;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 资源管理最佳实践 🔥必考
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
|
// ✅ 推荐:使用 context.subscriptions 自动管理
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('extension.command', () => {})
|
||||||
|
);
|
||||||
|
|
||||||
|
// ❌ 不推荐:手动管理容易忘记清理
|
||||||
|
const disposable = vscode.commands.registerCommand('extension.command', () => {});
|
||||||
|
// 需要在 deactivate 中手动调用 disposable.dispose()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 激活事件 (Activation Events) ⭐⭐
|
||||||
|
|
||||||
|
### 2.1 常用激活事件 📌重要
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json
|
||||||
|
{
|
||||||
|
"activationEvents": [
|
||||||
|
// 启动时激活
|
||||||
|
"onStartupFinished",
|
||||||
|
|
||||||
|
// 执行命令时激活
|
||||||
|
"onCommand:extension.helloWorld",
|
||||||
|
|
||||||
|
// 打开特定语言文件时激活
|
||||||
|
"onLanguage:javascript",
|
||||||
|
"onLanguage:verilog",
|
||||||
|
|
||||||
|
// 打开特定文件类型时激活
|
||||||
|
"onFileSystem:sftp",
|
||||||
|
|
||||||
|
// 打开特定视图时激活
|
||||||
|
"onView:myCustomView",
|
||||||
|
|
||||||
|
// 调试时激活
|
||||||
|
"onDebug",
|
||||||
|
|
||||||
|
// 打开特定 URI 时激活
|
||||||
|
"onUri",
|
||||||
|
|
||||||
|
// Webview 恢复时激活
|
||||||
|
"onWebviewPanel:myWebview",
|
||||||
|
|
||||||
|
// 任务执行时激活
|
||||||
|
"onTaskType:npm"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 延迟激活策略 🔥必考
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 推荐:使用 onStartupFinished 延迟激活
|
||||||
|
"activationEvents": ["onStartupFinished"]
|
||||||
|
|
||||||
|
// ❌ 不推荐:使用 * 会拖慢启动速度
|
||||||
|
"activationEvents": ["*"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 命令系统 (Commands)
|
||||||
|
|
||||||
|
### 3.1 注册命令
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 注册简单命令
|
||||||
|
const disposable = vscode.commands.registerCommand(
|
||||||
|
'extension.helloWorld',
|
||||||
|
() => {
|
||||||
|
vscode.window.showInformationMessage('Hello World!');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
context.subscriptions.push(disposable);
|
||||||
|
|
||||||
|
// 注册带参数的命令
|
||||||
|
vscode.commands.registerCommand(
|
||||||
|
'extension.openFile',
|
||||||
|
(filePath: string) => {
|
||||||
|
vscode.workspace.openTextDocument(filePath).then(doc => {
|
||||||
|
vscode.window.showTextDocument(doc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 执行命令
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 执行内置命令
|
||||||
|
await vscode.commands.executeCommand('workbench.action.files.save');
|
||||||
|
|
||||||
|
// 执行自定义命令
|
||||||
|
await vscode.commands.executeCommand('extension.openFile', '/path/to/file');
|
||||||
|
|
||||||
|
// 获取所有可用命令
|
||||||
|
const commands = await vscode.commands.getCommands();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 常用内置命令
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 文件操作
|
||||||
|
'workbench.action.files.save'
|
||||||
|
'workbench.action.files.saveAll'
|
||||||
|
'workbench.action.closeActiveEditor'
|
||||||
|
|
||||||
|
// 编辑器操作
|
||||||
|
'editor.action.formatDocument'
|
||||||
|
'editor.action.commentLine'
|
||||||
|
'editor.action.selectAll'
|
||||||
|
|
||||||
|
// 窗口操作
|
||||||
|
'workbench.action.toggleSidebarVisibility'
|
||||||
|
'workbench.action.terminal.new'
|
||||||
|
'workbench.action.quickOpen'
|
||||||
|
|
||||||
|
// Git 操作
|
||||||
|
'git.commit'
|
||||||
|
'git.push'
|
||||||
|
'git.pull'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Webview API ⭐⭐⭐⭐⭐ **面试重点**
|
||||||
|
|
||||||
|
### 4.1 创建 Webview Panel 🔥必考
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'myWebview', // viewType(唯一标识)
|
||||||
|
'My Webview', // 标题
|
||||||
|
vscode.ViewColumn.One, // 显示位置
|
||||||
|
{
|
||||||
|
enableScripts: true, // 启用 JavaScript
|
||||||
|
retainContextWhenHidden: true, // 隐藏时保留状态
|
||||||
|
localResourceRoots: [ // 允许访问的本地资源路径
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, 'media')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 设置 Webview 内容
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
panel.webview.html = getWebviewContent();
|
||||||
|
|
||||||
|
function getWebviewContent() {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>My Webview</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello from Webview!</h1>
|
||||||
|
<button onclick="sendMessage()">Send Message</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'alert',
|
||||||
|
text: 'Hello from Webview!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收来自 Extension 的消息
|
||||||
|
window.addEventListener('message', event => {
|
||||||
|
const message = event.data;
|
||||||
|
console.log('Received:', message);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Webview 消息通信 🔥必考(项目核心)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Extension → Webview
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: 'update',
|
||||||
|
data: 'some data'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Webview → Extension
|
||||||
|
panel.webview.onDidReceiveMessage(
|
||||||
|
message => {
|
||||||
|
switch (message.command) {
|
||||||
|
case 'alert':
|
||||||
|
vscode.window.showInformationMessage(message.text);
|
||||||
|
break;
|
||||||
|
case 'getData':
|
||||||
|
// 处理数据请求
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: 'dataResponse',
|
||||||
|
data: fetchData()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
context.subscriptions
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Webview 生命周期管理 📌重要
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 监听 Webview 关闭事件
|
||||||
|
panel.onDidDispose(
|
||||||
|
() => {
|
||||||
|
// 清理资源
|
||||||
|
console.log('Webview disposed');
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
context.subscriptions
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听 Webview 可见性变化
|
||||||
|
panel.onDidChangeViewState(
|
||||||
|
e => {
|
||||||
|
if (e.webviewPanel.visible) {
|
||||||
|
console.log('Webview is now visible');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
context.subscriptions
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 加载本地资源 📌重要
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取本地资源 URI
|
||||||
|
const scriptUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, 'media', 'script.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
const styleUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, 'media', 'style.css')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 在 HTML 中使用
|
||||||
|
const html = `
|
||||||
|
<link href="${styleUri}" rel="stylesheet">
|
||||||
|
<script src="${scriptUri}"></script>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 Webview 状态持久化 📌重要
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Webview 中保存状态
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
const state = vscode.getState() || { count: 0 };
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
state.count++;
|
||||||
|
vscode.setState(state);
|
||||||
|
|
||||||
|
// Extension 中序列化状态
|
||||||
|
panel.webview.options = {
|
||||||
|
enableScripts: true,
|
||||||
|
retainContextWhenHidden: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 恢复 Webview
|
||||||
|
vscode.window.registerWebviewPanelSerializer('myWebview', {
|
||||||
|
async deserializeWebviewPanel(webviewPanel, state) {
|
||||||
|
webviewPanel.webview.html = getWebviewContent();
|
||||||
|
// 恢复状态
|
||||||
|
webviewPanel.webview.postMessage({ command: 'restore', state });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. TreeView 和自定义视图
|
||||||
|
|
||||||
|
### 5.1 创建 TreeView Provider
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class MyTreeDataProvider implements vscode.TreeDataProvider<TreeItem> {
|
||||||
|
private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
|
||||||
|
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||||
|
|
||||||
|
refresh(): void {
|
||||||
|
this._onDidChangeTreeData.fire(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeItem(element: TreeItem): vscode.TreeItem {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildren(element?: TreeItem): Thenable<TreeItem[]> {
|
||||||
|
if (!element) {
|
||||||
|
// 返回根节点
|
||||||
|
return Promise.resolve([
|
||||||
|
new TreeItem('Item 1', vscode.TreeItemCollapsibleState.None),
|
||||||
|
new TreeItem('Item 2', vscode.TreeItemCollapsibleState.Collapsed)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// 返回子节点
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TreeItem extends vscode.TreeItem {
|
||||||
|
constructor(
|
||||||
|
public readonly label: string,
|
||||||
|
public readonly collapsibleState: vscode.TreeItemCollapsibleState
|
||||||
|
) {
|
||||||
|
super(label, collapsibleState);
|
||||||
|
this.tooltip = `Tooltip for ${label}`;
|
||||||
|
this.command = {
|
||||||
|
command: 'extension.itemClicked',
|
||||||
|
title: 'Click Item',
|
||||||
|
arguments: [this]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 注册 TreeView
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const treeDataProvider = new MyTreeDataProvider();
|
||||||
|
const treeView = vscode.window.createTreeView('myTreeView', {
|
||||||
|
treeDataProvider,
|
||||||
|
showCollapseAll: true
|
||||||
|
});
|
||||||
|
|
||||||
|
context.subscriptions.push(treeView);
|
||||||
|
|
||||||
|
// 刷新视图
|
||||||
|
treeDataProvider.refresh();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 WebviewView Provider(侧边栏 Webview)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class MyWebviewViewProvider implements vscode.WebviewViewProvider {
|
||||||
|
resolveWebviewView(
|
||||||
|
webviewView: vscode.WebviewView,
|
||||||
|
context: vscode.WebviewViewResolveContext,
|
||||||
|
token: vscode.CancellationToken
|
||||||
|
) {
|
||||||
|
webviewView.webview.options = {
|
||||||
|
enableScripts: true
|
||||||
|
};
|
||||||
|
|
||||||
|
webviewView.webview.html = getWebviewContent();
|
||||||
|
|
||||||
|
webviewView.webview.onDidReceiveMessage(message => {
|
||||||
|
// 处理消息
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
vscode.window.registerWebviewViewProvider(
|
||||||
|
'myWebviewView',
|
||||||
|
new MyWebviewViewProvider()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 文件系统操作 ⭐⭐⭐
|
||||||
|
|
||||||
|
### 6.1 读取文件 📌重要
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 读取文本文件
|
||||||
|
const uri = vscode.Uri.file('/path/to/file.txt');
|
||||||
|
const content = await vscode.workspace.fs.readFile(uri);
|
||||||
|
const text = Buffer.from(content).toString('utf8');
|
||||||
|
|
||||||
|
// 使用 TextDocument API
|
||||||
|
const document = await vscode.workspace.openTextDocument(uri);
|
||||||
|
const text = document.getText();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 写入文件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 写入文件
|
||||||
|
const uri = vscode.Uri.file('/path/to/file.txt');
|
||||||
|
const content = Buffer.from('Hello World', 'utf8');
|
||||||
|
await vscode.workspace.fs.writeFile(uri, content);
|
||||||
|
|
||||||
|
// 使用 WorkspaceEdit
|
||||||
|
const edit = new vscode.WorkspaceEdit();
|
||||||
|
edit.createFile(uri, { overwrite: true });
|
||||||
|
edit.insert(uri, new vscode.Position(0, 0), 'Hello World');
|
||||||
|
await vscode.workspace.applyEdit(edit);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 文件监听
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 监听文件变化
|
||||||
|
const watcher = vscode.workspace.createFileSystemWatcher('**/*.js');
|
||||||
|
|
||||||
|
watcher.onDidCreate(uri => {
|
||||||
|
console.log('File created:', uri.fsPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.onDidChange(uri => {
|
||||||
|
console.log('File changed:', uri.fsPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.onDidDelete(uri => {
|
||||||
|
console.log('File deleted:', uri.fsPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
context.subscriptions.push(watcher);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 工作区操作
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取工作区文件夹
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (workspaceFolders) {
|
||||||
|
const rootPath = workspaceFolders[0].uri.fsPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找文件
|
||||||
|
const files = await vscode.workspace.findFiles(
|
||||||
|
'**/*.ts', // include pattern
|
||||||
|
'**/node_modules/**' // exclude pattern
|
||||||
|
);
|
||||||
|
|
||||||
|
// 打开文件
|
||||||
|
const document = await vscode.workspace.openTextDocument(uri);
|
||||||
|
await vscode.window.showTextDocument(document);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 配置和存储 ⭐⭐⭐⭐ **面试重点**
|
||||||
|
|
||||||
|
### 7.1 读取配置 📌重要
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 读取配置
|
||||||
|
const config = vscode.workspace.getConfiguration('myExtension');
|
||||||
|
const value = config.get<string>('settingName', 'defaultValue');
|
||||||
|
|
||||||
|
// 监听配置变化
|
||||||
|
vscode.workspace.onDidChangeConfiguration(e => {
|
||||||
|
if (e.affectsConfiguration('myExtension.settingName')) {
|
||||||
|
console.log('Configuration changed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 更新配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const config = vscode.workspace.getConfiguration('myExtension');
|
||||||
|
|
||||||
|
// 更新用户配置(全局)
|
||||||
|
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Global);
|
||||||
|
|
||||||
|
// 更新工作区配置
|
||||||
|
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Workspace);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 状态存储 🔥必考
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 工作区状态(仅当前工作区)
|
||||||
|
await context.workspaceState.update('key', 'value');
|
||||||
|
const value = context.workspaceState.get('key');
|
||||||
|
|
||||||
|
// 全局状态(跨工作区)
|
||||||
|
await context.globalState.update('key', 'value');
|
||||||
|
const value = context.globalState.get('key');
|
||||||
|
|
||||||
|
// 存储对象
|
||||||
|
await context.globalState.update('userData', { name: 'John', age: 30 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 敏感信息存储 🔥必考(Token 管理)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 存储密码、Token 等敏感信息
|
||||||
|
await context.secrets.store('apiToken', 'secret-token-value');
|
||||||
|
|
||||||
|
// 读取
|
||||||
|
const token = await context.secrets.get('apiToken');
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
await context.secrets.delete('apiToken');
|
||||||
|
|
||||||
|
// 监听变化
|
||||||
|
context.secrets.onDidChange(e => {
|
||||||
|
console.log('Secret changed:', e.key);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 消息通知
|
||||||
|
|
||||||
|
### 8.1 信息提示
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 普通信息
|
||||||
|
vscode.window.showInformationMessage('Operation completed!');
|
||||||
|
|
||||||
|
// 警告
|
||||||
|
vscode.window.showWarningMessage('This action may cause issues');
|
||||||
|
|
||||||
|
// 错误
|
||||||
|
vscode.window.showErrorMessage('Operation failed!');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 带按钮的提示
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await vscode.window.showInformationMessage(
|
||||||
|
'Do you want to continue?',
|
||||||
|
'Yes',
|
||||||
|
'No',
|
||||||
|
'Cancel'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Yes') {
|
||||||
|
// 用户点击了 Yes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 输入框
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 简单输入
|
||||||
|
const input = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Enter your name',
|
||||||
|
placeHolder: 'John Doe',
|
||||||
|
validateInput: (value) => {
|
||||||
|
return value.length < 3 ? 'Name too short' : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 快速选择
|
||||||
|
const selected = await vscode.window.showQuickPick(
|
||||||
|
['Option 1', 'Option 2', 'Option 3'],
|
||||||
|
{
|
||||||
|
placeHolder: 'Select an option',
|
||||||
|
canPickMany: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 进度提示
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await vscode.window.withProgress(
|
||||||
|
{
|
||||||
|
location: vscode.ProgressLocation.Notification,
|
||||||
|
title: 'Processing...',
|
||||||
|
cancellable: true
|
||||||
|
},
|
||||||
|
async (progress, token) => {
|
||||||
|
token.onCancellationRequested(() => {
|
||||||
|
console.log('User canceled');
|
||||||
|
});
|
||||||
|
|
||||||
|
progress.report({ increment: 0, message: 'Starting...' });
|
||||||
|
await doWork1();
|
||||||
|
|
||||||
|
progress.report({ increment: 50, message: 'Half done...' });
|
||||||
|
await doWork2();
|
||||||
|
|
||||||
|
progress.report({ increment: 100, message: 'Complete!' });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 语言特性支持
|
||||||
|
|
||||||
|
### 9.1 代码补全
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const provider = vscode.languages.registerCompletionItemProvider(
|
||||||
|
'javascript',
|
||||||
|
{
|
||||||
|
provideCompletionItems(document, position) {
|
||||||
|
const item = new vscode.CompletionItem('myFunction');
|
||||||
|
item.kind = vscode.CompletionItemKind.Function;
|
||||||
|
item.detail = 'My custom function';
|
||||||
|
item.documentation = 'This is a custom function';
|
||||||
|
item.insertText = new vscode.SnippetString('myFunction($1)$0');
|
||||||
|
|
||||||
|
return [item];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'.' // 触发字符
|
||||||
|
);
|
||||||
|
|
||||||
|
context.subscriptions.push(provider);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 悬停提示
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const provider = vscode.languages.registerHoverProvider('javascript', {
|
||||||
|
provideHover(document, position) {
|
||||||
|
const range = document.getWordRangeAtPosition(position);
|
||||||
|
const word = document.getText(range);
|
||||||
|
|
||||||
|
return new vscode.Hover([
|
||||||
|
`**${word}**`,
|
||||||
|
'This is a hover tooltip'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 诊断(错误提示)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const diagnosticCollection = vscode.languages.createDiagnosticCollection('myExtension');
|
||||||
|
context.subscriptions.push(diagnosticCollection);
|
||||||
|
|
||||||
|
function updateDiagnostics(document: vscode.TextDocument) {
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
|
||||||
|
const text = document.getText();
|
||||||
|
const regex = /TODO/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text))) {
|
||||||
|
const range = new vscode.Range(
|
||||||
|
document.positionAt(match.index),
|
||||||
|
document.positionAt(match.index + match[0].length)
|
||||||
|
);
|
||||||
|
|
||||||
|
const diagnostic = new vscode.Diagnostic(
|
||||||
|
range,
|
||||||
|
'TODO found',
|
||||||
|
vscode.DiagnosticSeverity.Warning
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnosticCollection.set(document.uri, diagnostics);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 调试和诊断
|
||||||
|
|
||||||
|
### 10.1 输出通道
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const outputChannel = vscode.window.createOutputChannel('My Extension');
|
||||||
|
context.subscriptions.push(outputChannel);
|
||||||
|
|
||||||
|
outputChannel.appendLine('Extension activated');
|
||||||
|
outputChannel.show(); // 显示输出面板
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 日志记录
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用 LogOutputChannel(带时间戳)
|
||||||
|
const logger = vscode.window.createOutputChannel('My Extension', { log: true });
|
||||||
|
|
||||||
|
logger.trace('Trace message');
|
||||||
|
logger.debug('Debug message');
|
||||||
|
logger.info('Info message');
|
||||||
|
logger.warn('Warning message');
|
||||||
|
logger.error('Error message');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 错误处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await riskyOperation();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
vscode.window.showErrorMessage(`Error: ${error.message}`);
|
||||||
|
logger.error(error.stack || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践总结
|
||||||
|
|
||||||
|
### ✅ 推荐做法
|
||||||
|
|
||||||
|
1. **资源管理**:所有 disposable 对象都放入 `context.subscriptions`
|
||||||
|
2. **延迟激活**:使用 `onStartupFinished` 而不是 `*`
|
||||||
|
3. **异步操作**:使用 `async/await` 处理异步操作
|
||||||
|
4. **错误处理**:捕获异常并给用户友好提示
|
||||||
|
5. **类型安全**:充分利用 TypeScript 类型系统
|
||||||
|
6. **状态持久化**:使用 `globalState`/`workspaceState` 保存状态
|
||||||
|
7. **敏感信息**:使用 `secrets` API 存储 Token、密码等
|
||||||
|
|
||||||
|
### ❌ 避免做法
|
||||||
|
|
||||||
|
1. 不要在 `activate` 中执行耗时操作
|
||||||
|
2. 不要忘记清理资源(监听器、Webview 等)
|
||||||
|
3. 不要在 Webview 中直接访问文件系统
|
||||||
|
4. 不要在配置中存储敏感信息
|
||||||
|
5. 不要阻塞主线程(使用 Worker 或异步操作)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考资源
|
||||||
|
|
||||||
|
- [VS Code Extension API 官方文档](https://code.visualstudio.com/api)
|
||||||
|
- [Extension Samples](https://github.com/microsoft/vscode-extension-samples)
|
||||||
|
- [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
|
||||||
166
docs/Vivado联动前后端对接文档.md
Normal file
166
docs/Vivado联动前后端对接文档.md
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
# Vivado 联动前后端对接文档
|
||||||
|
|
||||||
|
## 1. 前端提供的工具
|
||||||
|
|
||||||
|
前端提供 4 个独立工具,每个工具执行一个 Vivado 命令。
|
||||||
|
|
||||||
|
### 1.1 createVivadoProject - 创建工程
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
projectName: string; // 项目名称
|
||||||
|
part: string; // 芯片型号(如 xc7a35tcpg236-1)
|
||||||
|
topModule: string; // 顶层模块名
|
||||||
|
files: string[]; // 源文件路径列表
|
||||||
|
constraints?: string; // 约束文件路径(可选)
|
||||||
|
mode: 'gui' | 'batch'; // gui=打开图形界面,batch=后台执行
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: boolean; // 是否成功
|
||||||
|
command: "create_project";
|
||||||
|
executionTime: number; // 执行时间(毫秒)
|
||||||
|
output: string; // 完整日志
|
||||||
|
error?: string; // 失败原因(如果失败)
|
||||||
|
outputFiles?: string[]; // 产出的工程文件路径
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 runVivadoSynthesis - 综合
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
projectPath?: string; // 工程路径(可选)
|
||||||
|
part: string; // 芯片型号
|
||||||
|
topModule: string; // 顶层模块
|
||||||
|
files?: string[]; // 源文件(如果没有工程)
|
||||||
|
constraints?: string; // 约束文件(可选)
|
||||||
|
mode: 'gui' | 'batch';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: boolean;
|
||||||
|
command: "synthesis";
|
||||||
|
executionTime: number;
|
||||||
|
output: string;
|
||||||
|
error?: string; // 失败原因
|
||||||
|
outputFiles?: string[]; // .dcp 文件等
|
||||||
|
reports?: {
|
||||||
|
resources?: string; // 资源使用摘要
|
||||||
|
timing?: string; // 时序摘要
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 runVivadoImplementation - 实现
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||||
|
mode: 'gui' | 'batch';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: boolean;
|
||||||
|
command: "implementation";
|
||||||
|
executionTime: number;
|
||||||
|
output: string;
|
||||||
|
error?: string; // 失败原因
|
||||||
|
outputFiles?: string[]; // 实现后的 .dcp 文件等
|
||||||
|
reports?: {
|
||||||
|
resources?: string;
|
||||||
|
timing?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 runVivadoBitstream - 生成比特流
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||||
|
mode: 'gui' | 'batch';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: boolean;
|
||||||
|
command: "bitstream";
|
||||||
|
executionTime: number;
|
||||||
|
output: string;
|
||||||
|
error?: string; // 失败原因
|
||||||
|
outputFiles?: string[]; // .bit 文件路径
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 前端职责
|
||||||
|
|
||||||
|
- 接收后端工具调用
|
||||||
|
- 生成对应的 TCL 脚本
|
||||||
|
- 执行 Vivado 命令
|
||||||
|
- 捕获输出日志
|
||||||
|
- 解析报告文件(提取资源和时序摘要)
|
||||||
|
- 返回执行结果
|
||||||
|
|
||||||
|
**前端不做**:
|
||||||
|
- 不检查依赖关系
|
||||||
|
- 不验证执行顺序
|
||||||
|
- 不控制流程
|
||||||
|
|
||||||
|
## 3. 后端职责
|
||||||
|
|
||||||
|
- 询问用户参数(芯片型号、执行模式等)
|
||||||
|
- 理解依赖关系(创建工程 → 综合 → 实现 → 生成比特流)
|
||||||
|
- 按正确顺序调用工具
|
||||||
|
- 检查每步的 `success` 字段
|
||||||
|
- 如果失败,读取 `error` 字段并提示用户
|
||||||
|
- 汇总结果展示给用户
|
||||||
|
|
||||||
|
## 4. 调用示例
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: 用 Vivado 做综合
|
||||||
|
|
||||||
|
后端:
|
||||||
|
1. 询问芯片型号 → xc7a35tcpg236-1
|
||||||
|
2. 询问执行模式 → batch
|
||||||
|
|
||||||
|
3. 调用 createVivadoProject(...)
|
||||||
|
返回: { success: true, outputFiles: ["counter.xpr"] }
|
||||||
|
|
||||||
|
4. 调用 runVivadoSynthesis(...)
|
||||||
|
返回: { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||||
|
|
||||||
|
5. 展示结果给用户
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 错误处理
|
||||||
|
|
||||||
|
如果某步失败:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: "ERROR: [Synth 8-439] module 'counter' not found",
|
||||||
|
output: "详细日志..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
后端应该:
|
||||||
|
1. 停止后续步骤
|
||||||
|
2. 提取 `error` 字段
|
||||||
|
3. 给用户提示和建议
|
||||||
153
docs/Vivado联动功能技术设计文档.md
Normal file
153
docs/Vivado联动功能技术设计文档.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# Vivado 联动功能技术设计文档
|
||||||
|
|
||||||
|
## 1. 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
后端 AI
|
||||||
|
↓ 调用工具
|
||||||
|
前端 Extension (messageHandler.ts)
|
||||||
|
↓ 调用
|
||||||
|
VivadoRunner (utils/vivadoRunner.ts)
|
||||||
|
↓ 生成 TCL 脚本并执行
|
||||||
|
本地 Vivado
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 核心模块
|
||||||
|
|
||||||
|
### 2.1 VivadoRunner
|
||||||
|
|
||||||
|
**职责**:执行单个 Vivado 命令
|
||||||
|
|
||||||
|
**主要方法**:
|
||||||
|
- `createProject()` - 创建工程
|
||||||
|
- `runSynthesis()` - 执行综合
|
||||||
|
- `runImplementation()` - 执行实现
|
||||||
|
- `runBitstream()` - 生成比特流
|
||||||
|
|
||||||
|
**实现要点**:
|
||||||
|
- 根据参数生成 TCL 脚本
|
||||||
|
- 启动子进程执行 Vivado
|
||||||
|
- 捕获输出日志
|
||||||
|
- 解析报告文件
|
||||||
|
- 返回结果
|
||||||
|
|
||||||
|
### 2.2 TCL 脚本生成
|
||||||
|
|
||||||
|
**创建工程**:
|
||||||
|
```tcl
|
||||||
|
create_project {projectName} {workDir} -part {part} -force
|
||||||
|
add_files -norecurse {files}
|
||||||
|
set_property top {topModule} [current_fileset]
|
||||||
|
```
|
||||||
|
|
||||||
|
**综合**:
|
||||||
|
```tcl
|
||||||
|
synth_design -part {part} -top {topModule}
|
||||||
|
report_utilization -file utilization.rpt
|
||||||
|
write_checkpoint -force synth.dcp
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现**:
|
||||||
|
```tcl
|
||||||
|
open_checkpoint {dcpFile}
|
||||||
|
opt_design
|
||||||
|
place_design
|
||||||
|
route_design
|
||||||
|
report_timing_summary -file timing.rpt
|
||||||
|
write_checkpoint -force impl.dcp
|
||||||
|
```
|
||||||
|
|
||||||
|
**生成比特流**:
|
||||||
|
```tcl
|
||||||
|
open_checkpoint {dcpFile}
|
||||||
|
write_bitstream -force output.bit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 MessageHandler 集成
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 处理工具调用
|
||||||
|
async function handleToolCall(toolName: string, params: any) {
|
||||||
|
switch(toolName) {
|
||||||
|
case 'createVivadoProject':
|
||||||
|
return await vivadoRunner.createProject(params);
|
||||||
|
case 'runVivadoSynthesis':
|
||||||
|
return await vivadoRunner.runSynthesis(params);
|
||||||
|
case 'runVivadoImplementation':
|
||||||
|
return await vivadoRunner.runImplementation(params);
|
||||||
|
case 'runVivadoBitstream':
|
||||||
|
return await vivadoRunner.runBitstream(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 配置管理
|
||||||
|
|
||||||
|
**配置文件位置**:
|
||||||
|
- 全局:`settings.json` 中的 `ic-coder.vivado`
|
||||||
|
- 项目:`.vscode/ic-coder-vivado.json`
|
||||||
|
|
||||||
|
**配置项**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vivado": {
|
||||||
|
"enabled": true,
|
||||||
|
"executablePath": "C:/Xilinx/Vivado/2023.1/bin/vivado.bat",
|
||||||
|
"workingDir": "${workspaceFolder}/vivado_project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── utils/
|
||||||
|
│ ├── vivadoRunner.ts # Vivado 执行器
|
||||||
|
│ ├── vivadoConfig.ts # 配置读取
|
||||||
|
│ └── tclGenerator.ts # TCL 脚本生成
|
||||||
|
└── utils/
|
||||||
|
└── messageHandler.ts # 工具调用处理(新增部分)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 实现要点
|
||||||
|
|
||||||
|
### 5.1 子进程执行
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const process = spawn(vivadoPath, ['-mode', 'batch', '-source', tclPath]);
|
||||||
|
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code) => {
|
||||||
|
resolve({ success: code === 0, output });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 报告解析
|
||||||
|
|
||||||
|
从 `.rpt` 文件中提取关键信息:
|
||||||
|
- 资源使用:LUT、FF、BRAM 数量
|
||||||
|
- 时序信息:WNS、TNS
|
||||||
|
|
||||||
|
### 5.3 错误处理
|
||||||
|
|
||||||
|
捕获常见错误:
|
||||||
|
- Vivado 未配置
|
||||||
|
- 文件不存在
|
||||||
|
- 综合/实现失败
|
||||||
|
- 时序不满足
|
||||||
|
|
||||||
|
返回清晰的错误信息给后端。
|
||||||
|
|
||||||
|
## 6. 测试
|
||||||
|
|
||||||
|
**单元测试**:
|
||||||
|
- TCL 脚本生成正确性
|
||||||
|
- 配置读取
|
||||||
|
|
||||||
|
**集成测试**:
|
||||||
|
- 完整流程测试(需要本地 Vivado)
|
||||||
|
- 错误处理测试
|
||||||
45
docs/code-changes-feature.md
Normal file
45
docs/code-changes-feature.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# 代码变更审查功能
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以查看所有修改并选择采纳或拒绝。
|
||||||
|
|
||||||
|
## 核心文件
|
||||||
|
|
||||||
|
### 1. 数据结构
|
||||||
|
- `src/types/fileChanges.ts` - 变更数据类型定义
|
||||||
|
|
||||||
|
### 2. 服务层
|
||||||
|
- `src/services/changeTracker.ts` - 变更追踪服务(单例)
|
||||||
|
- `trackChange()` - 记录文件变更
|
||||||
|
- `acceptChange()` - 采纳变更(保存文件)
|
||||||
|
- `rejectChange()` - 拒绝变更(恢复旧内容)
|
||||||
|
|
||||||
|
### 3. UI 组件
|
||||||
|
- `src/views/changePanel.ts` - 变更面板 UI
|
||||||
|
- `src/utils/diffRenderer.ts` - Diff 可视化渲染
|
||||||
|
|
||||||
|
### 4. 集成点
|
||||||
|
- `src/utils/messageHandler.ts` - 消息处理
|
||||||
|
- `trackFileChange()` - 记录变更
|
||||||
|
- `handleAcceptChange()` - 处理采纳
|
||||||
|
- `handleRejectChange()` - 处理拒绝
|
||||||
|
- `sendChangesToWebview()` - 发送变更到前端
|
||||||
|
|
||||||
|
- `src/services/toolExecutor.ts` - 工具执行器
|
||||||
|
- 在 `executeFileWrite()` 中记录变更
|
||||||
|
|
||||||
|
## 使用流程
|
||||||
|
|
||||||
|
1. **开始对话** - 调用 `startChangeSession(sessionId)`
|
||||||
|
2. **修改文件** - 自动调用 `trackFileChange()`
|
||||||
|
3. **对话结束** - 调用 `sendChangesToWebview()` 显示变更面板
|
||||||
|
4. **用户操作** - 点击采纳/拒绝按钮
|
||||||
|
5. **处理结果** - 保存或恢复文件内容
|
||||||
|
|
||||||
|
## 待完成工作
|
||||||
|
|
||||||
|
1. 在 ICHelperPanel 中集成消息处理(监听 acceptChange/rejectChange 命令)
|
||||||
|
2. 在对话结束时调用 `sendChangesToWebview()`
|
||||||
|
3. 在 Webview 中实现变更列表的动态渲染
|
||||||
|
4. 处理前端的采纳/拒绝响应
|
||||||
42
docs/code-to-chat-feature.md
Normal file
42
docs/code-to-chat-feature.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# 代码快速添加到对话功能
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
选中代码后,通过右键菜单/小灯泡/快捷键(Ctrl+Shift+I),将代码作为上下文添加到聊天面板输入框上方。
|
||||||
|
|
||||||
|
## 实现方式
|
||||||
|
|
||||||
|
### 1. Code Action Provider
|
||||||
|
`src/providers/codeActionProvider.ts` - 提供小灯泡菜单选项
|
||||||
|
|
||||||
|
### 2. 命令注册
|
||||||
|
`src/extension.ts` - 注册 `ic-coder.addCodeToChat` 命令,发送消息到 webview
|
||||||
|
|
||||||
|
### 3. 全局引用
|
||||||
|
`src/panels/ICHelperPanel.ts` - 保存 panel 到 `(global as any).currentICHelperPanel`
|
||||||
|
|
||||||
|
### 4. 上下文显示
|
||||||
|
`src/views/contextDisplay.ts` - 添加 `code` 类型支持和 `addCodeContext` 消息处理
|
||||||
|
|
||||||
|
### 5. 配置
|
||||||
|
`package.json` - 配置命令、右键菜单、快捷键
|
||||||
|
|
||||||
|
## 用户体验
|
||||||
|
|
||||||
|
1. 选中代码
|
||||||
|
2. 右键/小灯泡/Ctrl+Shift+I
|
||||||
|
3. 代码显示为上下文项:`文件名.v:10-25` 📄
|
||||||
|
4. 输入问题发送(代码自动作为上下文)
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
代码上下文存储为 JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fileName": "路径",
|
||||||
|
"startLine": 10,
|
||||||
|
"endLine": 25,
|
||||||
|
"code": "代码内容",
|
||||||
|
"languageId": "verilog"
|
||||||
|
}
|
||||||
|
```
|
||||||
294
docs/delete-file-confirmation.md
Normal file
294
docs/delete-file-confirmation.md
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# 删除文件确认功能实现文档
|
||||||
|
|
||||||
|
## 1. 功能概述
|
||||||
|
|
||||||
|
在 AI 返回删除文件命令时,前端拦截并弹出确认对话框,用户确认后才执行删除操作。
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 消息流程
|
||||||
|
|
||||||
|
```
|
||||||
|
AI 后端 → 删除文件工具调用 → 前端拦截 → 用户确认对话框
|
||||||
|
↓
|
||||||
|
确定/取消
|
||||||
|
↓
|
||||||
|
执行删除/返回取消结果
|
||||||
|
↓
|
||||||
|
返回 TOOL_EXECUTION_RESULT
|
||||||
|
↓
|
||||||
|
AI 后端
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 关键原则
|
||||||
|
|
||||||
|
**前端必须返回结果**:无论用户选择什么,前端都必须向后端返回 `TOOL_EXECUTION_RESULT`,否则后端会等待超时。
|
||||||
|
|
||||||
|
## 3. 实现方案
|
||||||
|
|
||||||
|
### 3.1 修改位置
|
||||||
|
|
||||||
|
文件:`src/utils/messageHandler.ts`
|
||||||
|
|
||||||
|
在处理工具调用的函数中,找到删除文件的工具处理逻辑。
|
||||||
|
|
||||||
|
### 3.2 核心代码实现
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 处理删除文件工具调用(带用户确认)
|
||||||
|
*/
|
||||||
|
async function handleDeleteFileTool(
|
||||||
|
toolCall: any,
|
||||||
|
panel: vscode.WebviewPanel
|
||||||
|
): Promise<ToolExecutionResult> {
|
||||||
|
const filePath = toolCall.arguments.filePath; // 根据实际参数名调整
|
||||||
|
|
||||||
|
// 弹出确认对话框
|
||||||
|
const confirmed = await vscode.window.showWarningMessage(
|
||||||
|
`确定要删除文件吗?\n\n${filePath}`,
|
||||||
|
{
|
||||||
|
modal: true, // 模态对话框,阻止其他操作
|
||||||
|
detail: '此操作不可撤销'
|
||||||
|
},
|
||||||
|
'确定删除',
|
||||||
|
'取消'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 用户确认删除
|
||||||
|
if (confirmed === '确定删除') {
|
||||||
|
try {
|
||||||
|
// 执行删除操作
|
||||||
|
const uri = vscode.Uri.file(filePath);
|
||||||
|
await vscode.workspace.fs.delete(uri, {
|
||||||
|
recursive: false, // 如果是目录需要设置为 true
|
||||||
|
useTrash: true // 移到回收站而非永久删除(推荐)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回成功结果
|
||||||
|
return {
|
||||||
|
type: 'TOOL_EXECUTION_RESULT',
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
result: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: `文件已删除: ${filePath}`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// 删除失败
|
||||||
|
return {
|
||||||
|
type: 'TOOL_EXECUTION_RESULT',
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
result: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: `删除失败: ${error.message}`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户取消或关闭对话框
|
||||||
|
return {
|
||||||
|
type: 'TOOL_EXECUTION_RESULT',
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
result: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: '用户取消了删除操作'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 集成到消息处理流程
|
||||||
|
|
||||||
|
在 `messageHandler.ts` 的工具调用处理逻辑中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 示例:在处理工具调用的地方
|
||||||
|
async function handleToolCall(toolCall: any, panel: vscode.WebviewPanel) {
|
||||||
|
switch (toolCall.name) {
|
||||||
|
case 'deleteFile': // 根据实际工具名称调整
|
||||||
|
return await handleDeleteFileTool(toolCall, panel);
|
||||||
|
|
||||||
|
case 'deleteDirectory': // 如果有删除目录的工具
|
||||||
|
return await handleDeleteDirectoryTool(toolCall, panel);
|
||||||
|
|
||||||
|
// ... 其他工具
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 用户体验优化
|
||||||
|
|
||||||
|
### 4.1 对话框样式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const confirmed = await vscode.window.showWarningMessage(
|
||||||
|
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
|
||||||
|
{
|
||||||
|
modal: true,
|
||||||
|
detail: '⚠️ 文件将被移到回收站,可以恢复'
|
||||||
|
},
|
||||||
|
'确定删除',
|
||||||
|
'取消'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 批量删除优化
|
||||||
|
|
||||||
|
如果 AI 一次返回多个删除操作:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 方案 1:逐个确认
|
||||||
|
for (const file of filesToDelete) {
|
||||||
|
await handleDeleteFileTool(file, panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案 2:批量确认(推荐)
|
||||||
|
const confirmed = await vscode.window.showWarningMessage(
|
||||||
|
`确定要删除以下 ${filesToDelete.length} 个文件吗?\n\n${filesToDelete.join('\n')}`,
|
||||||
|
{ modal: true },
|
||||||
|
'全部删除',
|
||||||
|
'取消'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 安全考虑
|
||||||
|
|
||||||
|
### 5.1 使用回收站
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await vscode.workspace.fs.delete(uri, {
|
||||||
|
useTrash: true // 移到回收站,可恢复
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 路径验证
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 防止删除工作区外的文件
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders) {
|
||||||
|
return { success: false, error: '未打开工作区' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInWorkspace = workspaceFolders.some(folder =>
|
||||||
|
filePath.startsWith(folder.uri.fsPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInWorkspace) {
|
||||||
|
return { success: false, error: '只能删除工作区内的文件' };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 敏感文件保护
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const protectedFiles = [
|
||||||
|
'package.json',
|
||||||
|
'tsconfig.json',
|
||||||
|
'.git',
|
||||||
|
'node_modules'
|
||||||
|
];
|
||||||
|
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
if (protectedFiles.includes(fileName)) {
|
||||||
|
vscode.window.showErrorMessage(`不允许删除系统文件: ${fileName}`);
|
||||||
|
return { success: false, error: '受保护的文件' };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 错误处理
|
||||||
|
|
||||||
|
### 6.1 常见错误
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await vscode.workspace.fs.delete(uri, { useTrash: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'FileNotFound') {
|
||||||
|
return { success: false, error: '文件不存在' };
|
||||||
|
}
|
||||||
|
if (error.code === 'NoPermissions') {
|
||||||
|
return { success: false, error: '没有删除权限' };
|
||||||
|
}
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 测试场景
|
||||||
|
|
||||||
|
### 7.1 基本测试
|
||||||
|
|
||||||
|
- [ ] 用户点击"确定删除" → 文件被删除
|
||||||
|
- [ ] 用户点击"取消" → 文件保留,返回取消消息
|
||||||
|
- [ ] 用户关闭对话框 → 文件保留,返回取消消息
|
||||||
|
- [ ] 文件不存在 → 返回错误消息
|
||||||
|
- [ ] 没有删除权限 → 返回错误消息
|
||||||
|
|
||||||
|
### 7.2 边界测试
|
||||||
|
|
||||||
|
- [ ] 删除工作区外的文件 → 拒绝
|
||||||
|
- [ ] 删除受保护文件 → 拒绝
|
||||||
|
- [ ] 批量删除 → 正确处理
|
||||||
|
- [ ] 后端收到取消消息后继续对话 → 流程正常
|
||||||
|
|
||||||
|
## 8. 配置选项(可选)
|
||||||
|
|
||||||
|
可以添加用户设置来控制行为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json
|
||||||
|
"configuration": {
|
||||||
|
"properties": {
|
||||||
|
"ic-coder.confirmDelete": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "删除文件前是否需要确认"
|
||||||
|
},
|
||||||
|
"ic-coder.useTrash": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "删除文件时移到回收站而非永久删除"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
读取配置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const config = vscode.workspace.getConfiguration('ic-coder');
|
||||||
|
const needConfirm = config.get<boolean>('confirmDelete', true);
|
||||||
|
const useTrash = config.get<boolean>('useTrash', true);
|
||||||
|
|
||||||
|
if (needConfirm) {
|
||||||
|
// 弹出确认对话框
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 总结
|
||||||
|
|
||||||
|
### 9.1 后端是否需要修改?
|
||||||
|
|
||||||
|
**不需要**。后端继续返回删除工具调用,前端负责:
|
||||||
|
1. 拦截工具调用
|
||||||
|
2. 弹出确认对话框
|
||||||
|
3. 执行或取消删除
|
||||||
|
4. **必须返回结果给后端**
|
||||||
|
|
||||||
|
### 9.2 关键要点
|
||||||
|
|
||||||
|
- ✅ 前端必须返回 `TOOL_EXECUTION_RESULT`
|
||||||
|
- ✅ 使用 `useTrash: true` 提高安全性
|
||||||
|
- ✅ 验证文件路径在工作区内
|
||||||
|
- ✅ 保护敏感文件
|
||||||
|
- ✅ 提供清晰的错误消息
|
||||||
|
|
||||||
|
### 9.3 下一步
|
||||||
|
|
||||||
|
1. 在 `messageHandler.ts` 中找到工具调用处理逻辑
|
||||||
|
2. 实现 `handleDeleteFileTool` 函数
|
||||||
|
3. 集成到现有流程
|
||||||
|
4. 测试各种场景
|
||||||
|
5. 考虑添加用户配置选项
|
||||||
50
docs/integration-guide.md
Normal file
50
docs/integration-guide.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# 代码变更审查功能 - 使用说明
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以:
|
||||||
|
- 查看所有修改的文件列表
|
||||||
|
- 点击文件查看 diff 对比
|
||||||
|
- 采纳变更(保存文件)
|
||||||
|
- 拒绝变更(恢复旧内容)
|
||||||
|
|
||||||
|
## 已完成的集成
|
||||||
|
|
||||||
|
### 1. 后端集成
|
||||||
|
- ✅ 在 `ICHelperPanel.ts` 中添加了消息监听(acceptChange/rejectChange)
|
||||||
|
- ✅ 在发送消息时启动变更追踪会话
|
||||||
|
- ✅ 在文件操作时自动记录变更(messageHandler.ts、toolExecutor.ts)
|
||||||
|
|
||||||
|
### 2. 前端集成
|
||||||
|
- ✅ 在 `webviewContent.ts` 中添加了消息处理(showChanges/changeAccepted/changeRejected)
|
||||||
|
- ✅ 在 `changePanel.ts` 中实现了完整的 UI 交互逻辑
|
||||||
|
|
||||||
|
### 3. 核心功能
|
||||||
|
- ✅ 变更追踪服务(changeTracker.ts)
|
||||||
|
- ✅ Diff 可视化渲染(diffRenderer.ts)
|
||||||
|
- ✅ 采纳/拒绝变更逻辑
|
||||||
|
|
||||||
|
## 待完成工作
|
||||||
|
|
||||||
|
需要在对话结束时调用 `sendChangesToWebview(panel)` 来显示变更面板。
|
||||||
|
|
||||||
|
建议在以下位置添加:
|
||||||
|
1. 在 `handleUserMessage` 函数中,对话流结束时
|
||||||
|
2. 或在 `dialogManager` 的对话完成回调中
|
||||||
|
|
||||||
|
示例代码:
|
||||||
|
```typescript
|
||||||
|
// 对话结束时
|
||||||
|
import { sendChangesToWebview } from '../utils/messageHandler';
|
||||||
|
|
||||||
|
// 在对话完成的地方调用
|
||||||
|
sendChangesToWebview(panel);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 启动插件(F5)
|
||||||
|
2. 发送消息让 AI 修改文件
|
||||||
|
3. 对话结束后,输入框上方应显示"代码变更"面板
|
||||||
|
4. 点击文件查看 diff
|
||||||
|
5. 点击"采纳"或"拒绝"按钮测试功能
|
||||||
161
docs/personal-rules-mvp-requirements.md
Normal file
161
docs/personal-rules-mvp-requirements.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# 个人规则功能需求文档(方案 A:本地 `.md` 注入)
|
||||||
|
|
||||||
|
## 1. 文档目标
|
||||||
|
|
||||||
|
在不改动现有核心对话模式的前提下,实现“个人规则(Personal Rules)”能力:
|
||||||
|
用户可在插件内维护个人规则文本,插件保存到本地 `.md` 文件;每次发起对话时自动读取并随请求传递给后端,由后端注入模型上下文,影响回答风格与行为。
|
||||||
|
|
||||||
|
## 2. 范围定义
|
||||||
|
|
||||||
|
### 2.1 本期范围(MVP)
|
||||||
|
|
||||||
|
1. 支持用户编辑、保存、启用/停用个人规则。
|
||||||
|
2. 本地落盘为 `.md` 文件。
|
||||||
|
3. 发消息时自动加载规则并传给后端。
|
||||||
|
4. 后端接收结构化字段并注入提示词。
|
||||||
|
5. 基础异常处理和可观测提示。
|
||||||
|
|
||||||
|
### 2.2 非本期范围
|
||||||
|
|
||||||
|
1. 云端同步、多设备同步。
|
||||||
|
2. 规则版本历史/回滚。
|
||||||
|
3. 多规则集合管理(仅单份个人规则文本)。
|
||||||
|
4. 团队共享规则。
|
||||||
|
|
||||||
|
## 3. 术语与核心概念
|
||||||
|
|
||||||
|
1. `Personal Rules`:用户个人偏好与约束文本。
|
||||||
|
2. `Rules File`:本地规则文件,Markdown 格式。
|
||||||
|
3. `Rules Enabled`:规则开关;关闭时不注入。
|
||||||
|
4. `Rules Injection`:请求时将规则传后端并参与模型上下文构建。
|
||||||
|
|
||||||
|
## 4. 用户故事
|
||||||
|
|
||||||
|
1. 作为用户,我希望在插件里写下“回答风格、代码习惯、语言偏好”等个人规则。
|
||||||
|
2. 作为用户,我希望规则保存在本地可见文件中。
|
||||||
|
3. 作为用户,我希望发消息时自动生效,无需每次重复输入。
|
||||||
|
4. 作为用户,我希望可以一键关闭规则,临时不生效。
|
||||||
|
|
||||||
|
## 5. 功能需求(前端/Webview + 扩展端)
|
||||||
|
|
||||||
|
### 5.1 规则管理界面
|
||||||
|
|
||||||
|
1. 提供“个人规则”入口。
|
||||||
|
2. 提供多行编辑框(显示当前规则内容)。
|
||||||
|
3. 提供“保存”按钮。
|
||||||
|
4. 提供“启用/停用”开关。
|
||||||
|
5. 显示当前状态:
|
||||||
|
6. 规则是否启用。
|
||||||
|
7. 规则字数/长度。
|
||||||
|
8. 最近保存时间(可选)。
|
||||||
|
|
||||||
|
### 5.2 本地文件存储
|
||||||
|
|
||||||
|
1. 规则内容保存到本地 `.md`。
|
||||||
|
2. 推荐文件名:`personal-rules.md`。
|
||||||
|
3. 推荐路径(优先):插件全局存储目录下固定子路径。
|
||||||
|
4. 文件不存在时可自动创建。
|
||||||
|
5. 用户可通过“打开规则文件”查看(可选)。
|
||||||
|
|
||||||
|
### 5.3 对话发送前处理
|
||||||
|
|
||||||
|
1. 用户点击发送消息。
|
||||||
|
2. 扩展端检查规则开关:
|
||||||
|
3. 关闭:不读取规则,不传后端。
|
||||||
|
4. 开启:读取 `.md` 内容。
|
||||||
|
5. 读取成功且非空时,将规则文本附加到对话请求结构化字段。
|
||||||
|
6. 读取失败时:提示告警,但不阻断正常对话。
|
||||||
|
|
||||||
|
### 5.4 限制与防护
|
||||||
|
|
||||||
|
1. 规则长度上限(例如 4000 字符,可配置)。
|
||||||
|
2. 超限时保存被拒绝,提示用户缩短。
|
||||||
|
3. 空白内容视为“无规则”。
|
||||||
|
4. 不允许二进制或非文本写入。
|
||||||
|
|
||||||
|
## 6. 功能需求(后端)
|
||||||
|
|
||||||
|
### 6.1 请求协议扩展
|
||||||
|
|
||||||
|
在现有对话请求结构中增加字段:
|
||||||
|
|
||||||
|
1. `personalRules`:字符串,可选。
|
||||||
|
2. `rulesEnabled`:布尔,可选(便于追踪)。
|
||||||
|
3. `rulesMeta`:可选元信息(长度、来源)。
|
||||||
|
|
||||||
|
### 6.2 注入策略
|
||||||
|
|
||||||
|
1. 后端收到 `personalRules` 后,将其注入系统提示层(而非用户消息层)。
|
||||||
|
2. 注入顺序建议:
|
||||||
|
3. 系统安全与平台策略。
|
||||||
|
4. 产品默认系统提示。
|
||||||
|
5. 用户个人规则。
|
||||||
|
6. 用户输入。
|
||||||
|
7. 若 `personalRules` 为空或开关关闭,则跳过注入。
|
||||||
|
|
||||||
|
### 6.3 风险控制
|
||||||
|
|
||||||
|
1. 规则文本不允许覆盖平台安全策略。
|
||||||
|
2. 记录本次是否注入规则(日志字段即可)。
|
||||||
|
3. 异常不应导致整次对话失败(可降级为无规则对话)。
|
||||||
|
|
||||||
|
## 7. 前后端对接设计
|
||||||
|
|
||||||
|
### 7.1 消息链路
|
||||||
|
|
||||||
|
1. Webview 触发 `sendMessage`。
|
||||||
|
2. 扩展端 `messageHandler` 统一处理发送。
|
||||||
|
3. `messageHandler` 在调用 `dialogService.sendMessage` 前读取个人规则。
|
||||||
|
4. `dialogService` 组装 `DialogRequest`,带上 `personalRules`。
|
||||||
|
5. `sseHandler` 发起流式请求。
|
||||||
|
6. 后端注入规则后进入模型推理。
|
||||||
|
7. 正常走现有 SSE 回传流程。
|
||||||
|
|
||||||
|
### 7.2 职责边界
|
||||||
|
|
||||||
|
1. Webview:展示与编辑,不直接拼接最终请求。
|
||||||
|
2. 扩展端:规则文件读写、开关状态管理、请求组装。
|
||||||
|
3. 后端:规则注入、优先级控制、审计日志。
|
||||||
|
|
||||||
|
## 8. 数据与状态设计
|
||||||
|
|
||||||
|
### 8.1 本地文件
|
||||||
|
|
||||||
|
1. 文件格式:Markdown 纯文本。
|
||||||
|
2. 内容约定:无强制模板,允许自由文本。
|
||||||
|
3. 编码:UTF-8。
|
||||||
|
|
||||||
|
### 8.2 本地配置状态
|
||||||
|
|
||||||
|
1. `personalRulesEnabled`:是否启用。
|
||||||
|
2. `personalRulesPath`:规则文件路径(可固定也可配置)。
|
||||||
|
3. `lastSavedAt`:最近保存时间(可选)。
|
||||||
|
|
||||||
|
## 9. 异常与降级
|
||||||
|
|
||||||
|
1. 文件不存在:自动创建空文件,视为无规则。
|
||||||
|
2. 文件读取失败:弹出提示,继续无规则发送。
|
||||||
|
3. 文件写入失败:保存失败提示,不更新状态。
|
||||||
|
4. 后端字段不识别:请求兼容,后端忽略新字段。
|
||||||
|
5. 后端注入失败:降级为普通对话,记录日志。
|
||||||
|
|
||||||
|
## 10. 安全与合规要求
|
||||||
|
|
||||||
|
1. 个人规则属于用户本地数据,不主动上传除非发起对话。
|
||||||
|
2. 日志中避免完整打印规则正文(最多打印长度和哈希)。
|
||||||
|
3. 后端注入时必须确保平台安全策略优先级更高。
|
||||||
|
|
||||||
|
## 11. 验收标准(UAT)
|
||||||
|
|
||||||
|
1. 用户保存规则后,本地存在 `personal-rules.md` 且内容一致。
|
||||||
|
2. 开启规则发送消息时,请求中可观测到 `personalRules` 字段。
|
||||||
|
3. 关闭规则发送消息时,请求中不含该字段或为空。
|
||||||
|
4. 规则文件损坏/读取失败时,不影响正常聊天。
|
||||||
|
5. 超过长度上限时,前端保存被拒绝且提示明确。
|
||||||
|
6. 后端日志可确认“本次是否注入个人规则”。
|
||||||
|
|
||||||
|
## 12. 迭代建议(下一阶段)
|
||||||
|
|
||||||
|
1. 规则模板(代码风格、语言风格、测试偏好)。
|
||||||
|
2. 项目规则与个人规则合并策略。
|
||||||
|
3. 云端同步(按 `userId`),多端一致。
|
||||||
55
docs/webpack-optimization.md
Normal file
55
docs/webpack-optimization.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Webpack 打包优化说明
|
||||||
|
|
||||||
|
## 优化内容
|
||||||
|
|
||||||
|
### 1. 自动模式切换
|
||||||
|
- 开发模式:保持源码可读性
|
||||||
|
- 生产模式:自动压缩代码
|
||||||
|
|
||||||
|
### 2. 性能优化
|
||||||
|
- **Tree Shaking**:移除未使用的代码
|
||||||
|
- **transpileOnly**:跳过类型检查,加快编译速度
|
||||||
|
- **自动清理**:每次打包自动删除旧文件
|
||||||
|
|
||||||
|
### 3. 体积监控
|
||||||
|
- 单文件超过 2MB 会发出警告
|
||||||
|
- 帮助及时发现打包体积问题
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 开发模式
|
||||||
|
```bash
|
||||||
|
# 编译(不压缩)
|
||||||
|
pnpm run compile
|
||||||
|
|
||||||
|
# 监听模式(自动重新编译)
|
||||||
|
pnpm run watch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产模式
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
set NODE_ENV=production && pnpm run package
|
||||||
|
|
||||||
|
# macOS/Linux
|
||||||
|
NODE_ENV=production pnpm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
## 打包结果
|
||||||
|
|
||||||
|
- **输出目录**:`dist/`
|
||||||
|
- **入口文件**:`dist/extension.js`
|
||||||
|
- **静态资源**:`dist/assets/`
|
||||||
|
|
||||||
|
## 性能对比
|
||||||
|
|
||||||
|
| 模式 | 体积 | 编译速度 | Source Map |
|
||||||
|
|------|------|----------|------------|
|
||||||
|
| 开发 | 较大 | 快 | 完整 |
|
||||||
|
| 生产 | 小 | 较慢 | 隐藏 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 开发时使用 `pnpm run watch`,修改代码自动重新编译
|
||||||
|
2. 发布前必须使用生产模式打包
|
||||||
|
3. 如果打包体积超过 2MB,检查是否引入了不必要的依赖
|
||||||
783
docs/插件试用用户功能实现方案.md
Normal file
783
docs/插件试用用户功能实现方案.md
Normal file
@ -0,0 +1,783 @@
|
|||||||
|
# 插件试用用户功能实现方案
|
||||||
|
|
||||||
|
## 1. 方案概述
|
||||||
|
|
||||||
|
**核心思路:**
|
||||||
|
- Web 登录成功后只返回 token(保持现状)
|
||||||
|
- 插件调用 `getUserInfo(token)` 时,后端返回的数据里包含标识字段
|
||||||
|
- 前端根据该字段判断是否是插件试用用户
|
||||||
|
- 插件试用用户:显示欢迎弹窗,不显示邀请码弹窗
|
||||||
|
- 正式用户:显示邀请码弹窗(现有逻辑)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 后端需要做什么
|
||||||
|
|
||||||
|
### 2.1 在用户信息接口中添加字段
|
||||||
|
|
||||||
|
**接口:** `GET /system/user/getInfo`
|
||||||
|
|
||||||
|
**现有响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userId": "xxx",
|
||||||
|
"username": "testuser",
|
||||||
|
"nickname": "测试用户",
|
||||||
|
"email": "test@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增字段:**
|
||||||
|
|
||||||
|
**方案 :添加 isPluginTrial 字段**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userId": "xxx",
|
||||||
|
"username": "testuser",
|
||||||
|
"nickname": "测试用户",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"isPluginTrial": true, // ← 新增:是否是插件试用用户
|
||||||
|
"pluginTrialExpiresAt": 1709654400000 // ← 新增:试用到期时间(毫秒时间戳)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 后端逻辑说明
|
||||||
|
|
||||||
|
**判断逻辑:**
|
||||||
|
```javascript
|
||||||
|
// 伪代码
|
||||||
|
function getUserInfo(userId) {
|
||||||
|
const user = db.users.findById(userId);
|
||||||
|
|
||||||
|
// 判断是否是插件试用用户(后端自己的逻辑)
|
||||||
|
const isPluginTrial = checkIfPluginTrialUser(user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
nickname: user.nickname,
|
||||||
|
email: user.email,
|
||||||
|
isPluginTrial: isPluginTrial,
|
||||||
|
pluginTrialExpiresAt: isPluginTrial ? user.trial_expires_at : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Token 过期时间:**
|
||||||
|
- 插件试用用户:JWT Token 设置 15 天过期
|
||||||
|
- 正式用户:JWT Token 设置 30 天过期(或现有逻辑)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 前端需要修改的地方
|
||||||
|
|
||||||
|
### 3.1 修改 UserInfo 接口
|
||||||
|
|
||||||
|
**文件:** `src/services/userService.ts`
|
||||||
|
|
||||||
|
**现有接口:**
|
||||||
|
```typescript
|
||||||
|
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?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增字段:**
|
||||||
|
```typescript
|
||||||
|
interface UserInfo {
|
||||||
|
// ... 现有字段
|
||||||
|
isPluginTrial?: boolean; // ← 新增:是否是插件试用用户
|
||||||
|
pluginTrialExpiresAt?: number; // ← 新增:试用到期时间(毫秒时间戳)
|
||||||
|
membership?: {
|
||||||
|
tierCode: string;
|
||||||
|
tierName: string;
|
||||||
|
tierLevel: number;
|
||||||
|
remainingDays?: number;
|
||||||
|
monthlyCredits?: number;
|
||||||
|
};
|
||||||
|
credits?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 3.2 修改 onTokenReceived() 方法
|
||||||
|
|
||||||
|
**文件:** `src/services/userService.ts`
|
||||||
|
|
||||||
|
**现有代码位置:** 约 200 行左右
|
||||||
|
|
||||||
|
**修改内容:**
|
||||||
|
```typescript
|
||||||
|
async onTokenReceived(token: string) {
|
||||||
|
// 现有逻辑:并行获取三类信息
|
||||||
|
const [userInfo, membershipInfo, credits] = await Promise.all([
|
||||||
|
getUserInfo(token),
|
||||||
|
getMembershipInfo(token),
|
||||||
|
fetchBalanceWithToken(token)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 合并数据
|
||||||
|
const fullUserInfo = {
|
||||||
|
...userInfo,
|
||||||
|
membership: membershipInfo,
|
||||||
|
credits: credits
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到 globalState
|
||||||
|
await this.context.globalState.update('icCoderUserInfo', fullUserInfo);
|
||||||
|
|
||||||
|
// ========== 新增逻辑 ==========
|
||||||
|
// 判断是否是插件试用用户
|
||||||
|
if (fullUserInfo.isPluginTrial === true) {
|
||||||
|
// 插件试用用户:显示欢迎弹窗,不显示邀请码弹窗
|
||||||
|
await this.showWelcomePanel();
|
||||||
|
// 标记为已显示欢迎弹窗(避免重复显示)
|
||||||
|
await this.context.globalState.update('pluginTrialWelcomed', true);
|
||||||
|
} else {
|
||||||
|
// 正式用户:显示邀请码弹窗(现有逻辑)
|
||||||
|
await this.checkAndShowInvitationModal();
|
||||||
|
}
|
||||||
|
// ========== 新增逻辑结束 ==========
|
||||||
|
|
||||||
|
return fullUserInfo;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 3.3 新增欢迎弹窗面板
|
||||||
|
|
||||||
|
**新建文件:** `src/panels/WelcomePanel.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 欢迎引导面板
|
||||||
|
* 功能:插件试用用户首次登录显示使用教程
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class WelcomePanel {
|
||||||
|
public static currentPanel: WelcomePanel | undefined;
|
||||||
|
private readonly _panel: vscode.WebviewPanel;
|
||||||
|
private _disposables: vscode.Disposable[] = [];
|
||||||
|
|
||||||
|
private constructor(panel: vscode.WebviewPanel) {
|
||||||
|
this._panel = panel;
|
||||||
|
this._panel.webview.html = this.getHtmlContent();
|
||||||
|
|
||||||
|
// 监听关闭事件
|
||||||
|
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static render(context: vscode.ExtensionContext) {
|
||||||
|
// 避免重复显示
|
||||||
|
if (WelcomePanel.currentPanel) {
|
||||||
|
WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'icCoderWelcome',
|
||||||
|
'欢迎使用 IC Coder',
|
||||||
|
vscode.ViewColumn.One,
|
||||||
|
{
|
||||||
|
enableScripts: true,
|
||||||
|
retainContextWhenHidden: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
WelcomePanel.currentPanel = new WelcomePanel(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlContent(): string {
|
||||||
|
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>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 40px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
background-color: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.welcome-message {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
.step h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
.step p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🎉 欢迎使用 IC Coder!</h1>
|
||||||
|
<p class="welcome-message">
|
||||||
|
您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>📝 步骤 1:打开聊天面板</h3>
|
||||||
|
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>💬 步骤 2:输入您的需求</h3>
|
||||||
|
<p>描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>🔬 步骤 3:运行仿真</h3>
|
||||||
|
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button" onclick="close()">开始使用</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function close() {
|
||||||
|
// 通知 VS Code 关闭面板
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
WelcomePanel.currentPanel = undefined;
|
||||||
|
this._panel.dispose();
|
||||||
|
while (this._disposables.length) {
|
||||||
|
const disposable = this._disposables.pop();
|
||||||
|
if (disposable) {
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 3.4 新增过期提醒面板
|
||||||
|
|
||||||
|
**新建文件:** `src/panels/ExpiredPanel.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 试用期到期提醒面板
|
||||||
|
* 功能:试用期到期时显示续费提示
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class ExpiredPanel {
|
||||||
|
public static render() {
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'icCoderExpired',
|
||||||
|
'试用期已到期',
|
||||||
|
vscode.ViewColumn.One,
|
||||||
|
{ enableScripts: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.webview.html = this.getHtmlContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getHtmlContent(): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 60px 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
background-color: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: var(--vscode-errorForeground);
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 15px 0;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
padding: 12px 30px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>⏰ 您的试用期已到期</h1>
|
||||||
|
<p>感谢您使用 IC Coder!您的 15 天试用期已结束。</p>
|
||||||
|
<p>如需继续使用,请联系我们获取正式版本。</p>
|
||||||
|
|
||||||
|
<button class="button" onclick="contact()">联系我们</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function contact() {
|
||||||
|
// 可以打开联系页面或发送邮件
|
||||||
|
window.open('https://iccoder.com/contact', '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 3.5 新增过期检测服务
|
||||||
|
|
||||||
|
**新建文件:** `src/services/trialExpirationService.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 试用期过期检测服务
|
||||||
|
* 功能:检查插件试用用户是否过期
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { getUserInfo } from './userService';
|
||||||
|
import { ExpiredPanel } from '../panels/ExpiredPanel';
|
||||||
|
|
||||||
|
export class TrialExpirationService {
|
||||||
|
private context: vscode.ExtensionContext;
|
||||||
|
|
||||||
|
constructor(context: vscode.ExtensionContext) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否过期
|
||||||
|
* @returns true=已过期,false=未过期
|
||||||
|
*/
|
||||||
|
public async checkExpiration(): Promise<boolean> {
|
||||||
|
const userInfo = await getUserInfo();
|
||||||
|
|
||||||
|
// 不是插件试用用户,不需要检查
|
||||||
|
if (!userInfo?.isPluginTrial) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有过期时间,不检查
|
||||||
|
if (!userInfo.pluginTrialExpiresAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
const now = Date.now();
|
||||||
|
if (now >= userInfo.pluginTrialExpiresAt) {
|
||||||
|
// 已过期
|
||||||
|
await this.handleExpired();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理过期逻辑
|
||||||
|
*/
|
||||||
|
private async handleExpired(): Promise<void> {
|
||||||
|
// 显示过期弹窗
|
||||||
|
ExpiredPanel.render();
|
||||||
|
|
||||||
|
// 清除本地数据(可选)
|
||||||
|
// await this.context.globalState.update('icCoderUserInfo', undefined);
|
||||||
|
// await this.context.globalState.update('icCoderSessions', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 3.6 在消息发送前检查过期
|
||||||
|
|
||||||
|
**文件:** `src/utils/messageHandler.ts`
|
||||||
|
|
||||||
|
**修改位置:** 在发送消息给后端之前添加过期检查
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TrialExpirationService } from '../services/trialExpirationService';
|
||||||
|
|
||||||
|
// 在 handleUserMessage 或类似的消息处理函数中添加
|
||||||
|
async function handleUserMessage(message: string, context: vscode.ExtensionContext) {
|
||||||
|
// ========== 新增:检查试用期是否过期 ==========
|
||||||
|
const trialService = new TrialExpirationService(context);
|
||||||
|
const isExpired = await trialService.checkExpiration();
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
// 已过期,禁止使用
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '您的试用期已到期,请联系我们获取正式版本'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// ========== 新增结束 ==========
|
||||||
|
|
||||||
|
// 现有的消息处理逻辑
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 完整的实现流程
|
||||||
|
|
||||||
|
### 4.1 登录流程(带过期检查)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户点击登录
|
||||||
|
↓
|
||||||
|
2. 打开浏览器,Web 端登录
|
||||||
|
↓
|
||||||
|
3. 重定向回插件:http://localhost:{port}/callback?token={token}
|
||||||
|
↓
|
||||||
|
4. 插件调用 onTokenReceived(token)
|
||||||
|
↓
|
||||||
|
5. 并行获取:getUserInfo + getMembershipInfo + Credits
|
||||||
|
↓
|
||||||
|
6. 后端返回 userInfo(包含 isPluginTrial 和 pluginTrialExpiresAt)
|
||||||
|
↓
|
||||||
|
7. 判断 isPluginTrial === true?
|
||||||
|
├─ 是:显示欢迎弹窗,不显示邀请码弹窗
|
||||||
|
└─ 否:显示邀请码弹窗(现有逻辑)
|
||||||
|
↓
|
||||||
|
8. 保存用户信息到 globalState
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 使用功能时的过期检查
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户发送消息/使用功能
|
||||||
|
↓
|
||||||
|
2. 调用 trialService.checkExpiration()
|
||||||
|
↓
|
||||||
|
3. 获取 userInfo,检查 isPluginTrial
|
||||||
|
↓
|
||||||
|
4. 如果是插件试用用户,检查 Date.now() >= pluginTrialExpiresAt?
|
||||||
|
├─ 是:显示过期弹窗,禁止使用,返回 true
|
||||||
|
└─ 否:允许使用,返回 false
|
||||||
|
↓
|
||||||
|
5. 继续正常的消息处理流程
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. ⚠️ 潜在 Bug 和注意事项
|
||||||
|
|
||||||
|
### 5.1 时间同步问题
|
||||||
|
|
||||||
|
**问题:** 前端使用 `Date.now()` 判断过期,如果用户本地时间不准确会导致误判
|
||||||
|
|
||||||
|
**场景:**
|
||||||
|
- 用户本地时间快了 1 天 → 提前显示过期弹窗
|
||||||
|
- 用户本地时间慢了 1 天 → 过期后仍可使用
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
```typescript
|
||||||
|
// 方案 1:每次使用前调用后端验证(推荐)
|
||||||
|
async checkExpiration(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// 调用后端接口验证 Token 是否过期
|
||||||
|
const response = await fetch(`${API_BASE}/auth/verify`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Token 过期
|
||||||
|
await this.handleExpired();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
// 网络错误,使用本地时间判断
|
||||||
|
const userInfo = await getUserInfo();
|
||||||
|
return Date.now() >= (userInfo?.pluginTrialExpiresAt || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 5.2 欢迎弹窗重复显示
|
||||||
|
|
||||||
|
**问题:** 每次登录都显示欢迎弹窗,用户体验不好
|
||||||
|
|
||||||
|
**场景:**
|
||||||
|
- 试用用户第一次登录显示欢迎弹窗 ✅
|
||||||
|
- 用户关闭插件后重新打开,又显示欢迎弹窗 ❌
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
```typescript
|
||||||
|
async onTokenReceived(token: string) {
|
||||||
|
// ... 获取用户信息
|
||||||
|
|
||||||
|
if (fullUserInfo.isPluginTrial === true) {
|
||||||
|
// 检查是否已经显示过欢迎弹窗
|
||||||
|
const hasWelcomed = this.context.globalState.get('pluginTrialWelcomed');
|
||||||
|
|
||||||
|
if (!hasWelcomed) {
|
||||||
|
await this.showWelcomePanel();
|
||||||
|
await this.context.globalState.update('pluginTrialWelcomed', true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.checkAndShowInvitationModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 5.3 isPluginTrial 字段类型不一致
|
||||||
|
|
||||||
|
**问题:** 后端可能返回 `true`、`false`、`null`、`undefined`,前端判断时需要严格处理
|
||||||
|
|
||||||
|
**场景:**
|
||||||
|
```typescript
|
||||||
|
// ❌ 错误写法
|
||||||
|
if (userInfo.isPluginTrial) {
|
||||||
|
// undefined 会被判断为 false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确写法
|
||||||
|
if (userInfo.isPluginTrial === true) {
|
||||||
|
// 插件试用用户
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**建议:**
|
||||||
|
- 后端统一返回 `true` 或 `false`,不要返回 `null`
|
||||||
|
- 前端使用严格相等 `===` 判断
|
||||||
|
|
||||||
|
|
||||||
|
### 5.4 Token 过期但前端未清除
|
||||||
|
|
||||||
|
**问题:** Token 在后端已过期,但前端仍保存着过期的 Token
|
||||||
|
|
||||||
|
**场景:**
|
||||||
|
- 用户 15 天后打开插件
|
||||||
|
- 前端尝试调用 API,后端返回 401
|
||||||
|
- 前端没有处理 401,导致功能异常
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
```typescript
|
||||||
|
// 在 apiClient.ts 中统一处理 401
|
||||||
|
async function apiCall(url: string, options: any) {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Token 过期,清除本地数据
|
||||||
|
await clearAllData();
|
||||||
|
ExpiredPanel.render();
|
||||||
|
throw new Error('Token expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 前后端对接清单
|
||||||
|
|
||||||
|
### 6.1 后端需要提供
|
||||||
|
|
||||||
|
**1. 修改 `GET /system/user/getInfo` 接口**
|
||||||
|
- 新增字段:`isPluginTrial` (boolean)
|
||||||
|
- 新增字段:`pluginTrialExpiresAt` (number, 毫秒时间戳)
|
||||||
|
|
||||||
|
**2. Token 过期时间设置**
|
||||||
|
- 插件试用用户:JWT Token 设置 15 天过期
|
||||||
|
- 正式用户:保持现有逻辑
|
||||||
|
|
||||||
|
**3. 测试账号**
|
||||||
|
- 提供 1-2 个插件试用用户账号用于测试
|
||||||
|
|
||||||
|
### 6.2 前端需要修改
|
||||||
|
|
||||||
|
**1. 修改文件:**
|
||||||
|
- `src/services/userService.ts` - 修改 UserInfo 接口和 onTokenReceived()
|
||||||
|
- `src/utils/messageHandler.ts` - 添加过期检查
|
||||||
|
|
||||||
|
**2. 新增文件:**
|
||||||
|
- `src/panels/WelcomePanel.ts` - 欢迎弹窗
|
||||||
|
- `src/panels/ExpiredPanel.ts` - 过期提醒弹窗
|
||||||
|
- `src/services/trialExpirationService.ts` - 过期检测服务
|
||||||
|
|
||||||
|
**3. globalState 新增存储键:**
|
||||||
|
- `pluginTrialWelcomed` (boolean) - 是否已显示欢迎弹窗
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 测试计划
|
||||||
|
|
||||||
|
### 7.1 登录流程测试
|
||||||
|
|
||||||
|
**测试用例 1:插件试用用户登录**
|
||||||
|
- 使用插件试用用户账号登录
|
||||||
|
- 验证是否显示欢迎弹窗
|
||||||
|
- 验证是否不显示邀请码弹窗
|
||||||
|
- 验证 userInfo 中 isPluginTrial === true
|
||||||
|
|
||||||
|
**测试用例 2:正式用户登录**
|
||||||
|
- 使用正式用户账号登录
|
||||||
|
- 验证是否显示邀请码弹窗
|
||||||
|
- 验证是否不显示欢迎弹窗
|
||||||
|
- 验证 userInfo 中 isPluginTrial !== true
|
||||||
|
|
||||||
|
**测试用例 3:欢迎弹窗不重复显示**
|
||||||
|
- 插件试用用户登录后显示欢迎弹窗
|
||||||
|
- 关闭插件重新打开
|
||||||
|
- 验证不再显示欢迎弹窗
|
||||||
|
|
||||||
|
|
||||||
|
### 7.2 过期检测测试
|
||||||
|
|
||||||
|
**测试用例 4:未过期用户正常使用**
|
||||||
|
- 插件试用用户登录(未过期)
|
||||||
|
- 发送消息使用功能
|
||||||
|
- 验证功能正常使用
|
||||||
|
|
||||||
|
**测试用例 5:已过期用户禁止使用**
|
||||||
|
- 修改本地时间到 15 天后(或修改 pluginTrialExpiresAt)
|
||||||
|
- 尝试发送消息
|
||||||
|
- 验证显示过期弹窗
|
||||||
|
- 验证功能被禁用
|
||||||
|
|
||||||
|
**测试用例 6:Token 过期处理**
|
||||||
|
- 使用过期的 Token 调用 API
|
||||||
|
- 验证后端返回 401
|
||||||
|
- 验证前端显示过期弹窗
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 总结
|
||||||
|
|
||||||
|
### 8.1 核心改动点
|
||||||
|
|
||||||
|
**后端(最小改动):**
|
||||||
|
1. `GET /system/user/getInfo` 接口新增 2 个字段
|
||||||
|
2. JWT Token 根据用户类型设置不同过期时间
|
||||||
|
|
||||||
|
**前端(主要改动):**
|
||||||
|
1. 修改 UserInfo 接口定义
|
||||||
|
2. 修改 onTokenReceived() 添加判断逻辑
|
||||||
|
3. 新增 3 个文件(欢迎面板、过期面板、过期检测服务)
|
||||||
|
4. 在消息发送前添加过期检查
|
||||||
|
|
||||||
|
### 8.2 关键判断逻辑
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 登录后判断
|
||||||
|
if (userInfo.isPluginTrial === true) {
|
||||||
|
showWelcomePanel(); // 显示欢迎弹窗
|
||||||
|
} else {
|
||||||
|
showInvitationModal(); // 显示邀请码弹窗
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用前判断
|
||||||
|
if (Date.now() >= userInfo.pluginTrialExpiresAt) {
|
||||||
|
showExpiredPanel(); // 显示过期弹窗
|
||||||
|
return; // 禁止使用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 8.3 必须注意的问题
|
||||||
|
|
||||||
|
1. **时间同步问题** - 用户本地时间不准确导致误判,建议调用后端验证
|
||||||
|
2. **isPluginTrial 严格判断** - 必须使用 `=== true` 判断
|
||||||
|
3. **欢迎弹窗重复显示** - 使用 globalState 标记避免重复
|
||||||
|
4. **Token 过期处理** - 在 apiClient 中统一处理 401 响应
|
||||||
|
|
||||||
|
### 8.4 实现优先级
|
||||||
|
|
||||||
|
**P0(必须实现):**
|
||||||
|
1. 后端接口新增字段
|
||||||
|
2. 前端 UserInfo 接口修改
|
||||||
|
3. 前端 onTokenReceived() 判断逻辑
|
||||||
|
4. 过期检测逻辑
|
||||||
|
|
||||||
|
**P1(重要):**
|
||||||
|
1. 欢迎弹窗
|
||||||
|
2. 过期提醒弹窗
|
||||||
|
|
||||||
|
**P2(优化):**
|
||||||
|
1. 后端验证过期(避免时间同步问题)
|
||||||
|
2. 更友好的过期提示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档完成!** 基于你现有的代码架构,这个方案改动最小,实现最简单。
|
||||||
|
|
||||||
35
package.json
35
package.json
@ -2,7 +2,7 @@
|
|||||||
"name": "iccoder",
|
"name": "iccoder",
|
||||||
"displayName": "IC Coder: Agentic Verilog Platform",
|
"displayName": "IC Coder: Agentic Verilog Platform",
|
||||||
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
||||||
"version": "1.0.3",
|
"version": "1.0.12",
|
||||||
"publisher": "ICCoderAgenticVerilogPlatform",
|
"publisher": "ICCoderAgenticVerilogPlatform",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.80.0"
|
"vscode": "^1.80.0"
|
||||||
@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onCommand:ic-coder.openPanel",
|
"onCommand:ic-coder.openPanel",
|
||||||
"onView:ic-coder-sidebar",
|
"onView:ic-coder.mainView",
|
||||||
"onLanguage:verilog",
|
"onLanguage:verilog",
|
||||||
"onLanguage:vhdl",
|
"onLanguage:vhdl",
|
||||||
"onStartupFinished"
|
"onStartupFinished"
|
||||||
@ -54,6 +54,28 @@
|
|||||||
"command": "ic-coder.testNotification",
|
"command": "ic-coder.testNotification",
|
||||||
"title": "测试系统通知",
|
"title": "测试系统通知",
|
||||||
"category": "IC Coder"
|
"category": "IC Coder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "ic-coder.addCodeToChat",
|
||||||
|
"title": "添加到 IC Coder 对话",
|
||||||
|
"category": "IC Coder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menus": {
|
||||||
|
"editor/context": [
|
||||||
|
{
|
||||||
|
"command": "ic-coder.addCodeToChat",
|
||||||
|
"when": "editorHasSelection",
|
||||||
|
"group": "9_cutcopypaste"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keybindings": [
|
||||||
|
{
|
||||||
|
"command": "ic-coder.addCodeToChat",
|
||||||
|
"key": "ctrl+l",
|
||||||
|
"mac": "cmd+l",
|
||||||
|
"when": "editorTextFocus && editorHasSelection"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"viewsContainers": {
|
"viewsContainers": {
|
||||||
@ -135,6 +157,7 @@
|
|||||||
"@vscode/test-cli": "^0.0.12",
|
"@vscode/test-cli": "^0.0.12",
|
||||||
"@vscode/test-electron": "^2.5.2",
|
"@vscode/test-electron": "^2.5.2",
|
||||||
"@vscode/vsce": "^3.7.1",
|
"@vscode/vsce": "^3.7.1",
|
||||||
|
"copy-webpack-plugin": "^14.0.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"ts-loader": "^9.5.4",
|
"ts-loader": "^9.5.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
@ -142,14 +165,6 @@
|
|||||||
"webpack": "^5.103.0",
|
"webpack": "^5.103.0",
|
||||||
"webpack-cli": "^6.0.1"
|
"webpack-cli": "^6.0.1"
|
||||||
},
|
},
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"media",
|
|
||||||
"tools",
|
|
||||||
"src/assets",
|
|
||||||
"LICENSE",
|
|
||||||
"CHANGELOG.md"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wavedrom/doppler": "^1.14.0",
|
"@wavedrom/doppler": "^1.14.0",
|
||||||
"eventsource-parser": "^3.0.6",
|
"eventsource-parser": "^3.0.6",
|
||||||
|
|||||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@ -57,6 +57,9 @@ importers:
|
|||||||
'@vscode/vsce':
|
'@vscode/vsce':
|
||||||
specifier: ^3.7.1
|
specifier: ^3.7.1
|
||||||
version: 3.7.1
|
version: 3.7.1
|
||||||
|
copy-webpack-plugin:
|
||||||
|
specifier: ^14.0.0
|
||||||
|
version: 14.0.0(webpack@5.103.0)
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.39.1
|
specifier: ^9.39.1
|
||||||
version: 9.39.1
|
version: 9.39.1
|
||||||
@ -823,6 +826,12 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
copy-webpack-plugin@14.0.0:
|
||||||
|
resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==}
|
||||||
|
engines: {node: '>= 20.9.0'}
|
||||||
|
peerDependencies:
|
||||||
|
webpack: ^5.1.0
|
||||||
|
|
||||||
core-util-is@1.0.3:
|
core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
@ -1928,6 +1937,10 @@ packages:
|
|||||||
serialize-javascript@6.0.2:
|
serialize-javascript@6.0.2:
|
||||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||||
|
|
||||||
|
serialize-javascript@7.0.4:
|
||||||
|
resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
setimmediate@1.0.5:
|
setimmediate@1.0.5:
|
||||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||||
|
|
||||||
@ -3297,6 +3310,15 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
copy-webpack-plugin@14.0.0(webpack@5.103.0):
|
||||||
|
dependencies:
|
||||||
|
glob-parent: 6.0.2
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
schema-utils: 4.3.3
|
||||||
|
serialize-javascript: 7.0.4
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
webpack: 5.103.0(webpack-cli@6.0.1)
|
||||||
|
|
||||||
core-util-is@1.0.3: {}
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
@ -4431,6 +4453,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
randombytes: 2.1.0
|
randombytes: 2.1.0
|
||||||
|
|
||||||
|
serialize-javascript@7.0.4: {}
|
||||||
|
|
||||||
setimmediate@1.0.5: {}
|
setimmediate@1.0.5: {}
|
||||||
|
|
||||||
shallow-clone@3.0.1:
|
shallow-clone@3.0.1:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
165
src/extension.ts
165
src/extension.ts
@ -10,10 +10,46 @@ import { initCreditsService } from "./services/creditsService";
|
|||||||
import { isTokenExpired } from "./utils/jwtUtils";
|
import { isTokenExpired } from "./utils/jwtUtils";
|
||||||
import { NotificationService } from "./services/notificationService";
|
import { NotificationService } from "./services/notificationService";
|
||||||
import { InvitationService } from "./services/invitationService";
|
import { InvitationService } from "./services/invitationService";
|
||||||
|
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
console.log("🎉 IC Coder 插件已激活!");
|
console.log("🎉 IC Coder 插件已激活!");
|
||||||
|
|
||||||
|
// 创建装饰类型(代码旁边的提示)
|
||||||
|
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||||
|
after: {
|
||||||
|
contentText: ' Ctrl+L 添加到 IC Coder 对话',
|
||||||
|
color: '#888',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
margin: '0 0 0 1em'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新装饰
|
||||||
|
const updateDecorations = () => {
|
||||||
|
const editor = vscode.window.activeTextEditor;
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
if (!editor.selection.isEmpty) {
|
||||||
|
// 找到选区末尾所在的行,并将提示放在该行的末尾
|
||||||
|
const { anchor, active } = editor.selection;
|
||||||
|
const endPos = anchor.isAfter(active) ? anchor : active;
|
||||||
|
const lineEndPos = editor.document.lineAt(endPos.line).range.end;
|
||||||
|
const range = new vscode.Range(lineEndPos, lineEndPos);
|
||||||
|
const decoration = { range };
|
||||||
|
editor.setDecorations(decorationType, [decoration]);
|
||||||
|
} else {
|
||||||
|
editor.setDecorations(decorationType, []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.window.onDidChangeTextEditorSelection(updateDecorations),
|
||||||
|
vscode.window.onDidChangeActiveTextEditor(updateDecorations)
|
||||||
|
);
|
||||||
|
|
||||||
|
updateDecorations();
|
||||||
|
|
||||||
// 初始化通知服务
|
// 初始化通知服务
|
||||||
const notificationService = NotificationService.getInstance(context);
|
const notificationService = NotificationService.getInstance(context);
|
||||||
console.log('[Extension] 通知服务已初始化');
|
console.log('[Extension] 通知服务已初始化');
|
||||||
@ -159,20 +195,32 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
// 注册命令:用户登录
|
// 注册命令:用户登录
|
||||||
const loginCommand = vscode.commands.registerCommand(
|
const loginCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.login",
|
"ic-coder.login",
|
||||||
async () => {
|
async (options?: { forceReauth?: boolean }) => {
|
||||||
try {
|
try {
|
||||||
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
|
const forceReauth = options?.forceReauth === true;
|
||||||
try {
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
await vscode.authentication.getSession("iccoder", [], {
|
createIfNone: false,
|
||||||
clearSessionPreference: true,
|
});
|
||||||
createIfNone: false
|
const expired = session?.accessToken
|
||||||
});
|
? isTokenExpired(session.accessToken)
|
||||||
} catch {
|
: null;
|
||||||
// 忽略错误
|
|
||||||
|
// 会话仍有效时,直接打开聊天面板
|
||||||
|
if (session && expired === false && !forceReauth) {
|
||||||
|
vscode.commands.executeCommand("ic-coder.openChat");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新 session
|
// 1) 清空当前登录状态信息
|
||||||
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
await authProvider.clearSessionsForRelogin();
|
||||||
|
await context.globalState.update("icCoderSessions", []);
|
||||||
|
await context.globalState.update("icCoderUserInfo", undefined);
|
||||||
|
|
||||||
|
// 2) 重新登录(强制新会话)
|
||||||
|
await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
clearSessionPreference: true,
|
||||||
|
forceNewSession: true,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
||||||
}
|
}
|
||||||
@ -238,6 +286,81 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 注册命令:将选中代码添加到对话
|
||||||
|
const addCodeToChat = vscode.commands.registerCommand(
|
||||||
|
"ic-coder.addCodeToChat",
|
||||||
|
async () => {
|
||||||
|
console.log('[addCodeToChat] 命令触发');
|
||||||
|
const editor = vscode.window.activeTextEditor;
|
||||||
|
if (!editor) {
|
||||||
|
console.log('[addCodeToChat] 没有活动编辑器');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = editor.selection;
|
||||||
|
const selectedText = editor.document.getText(selection);
|
||||||
|
|
||||||
|
if (!selectedText) {
|
||||||
|
vscode.window.showWarningMessage("请先选择代码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = editor.document.fileName;
|
||||||
|
const startLine = selection.start.line + 1;
|
||||||
|
const endLine = selection.end.line + 1;
|
||||||
|
|
||||||
|
// 检查是否已有打开的面板
|
||||||
|
let panel = (global as any).currentICHelperPanel;
|
||||||
|
let needCreatePanel = false;
|
||||||
|
|
||||||
|
if (!panel) {
|
||||||
|
needCreatePanel = true;
|
||||||
|
} else {
|
||||||
|
// 尝试访问 webview,如果抛出异常说明已销毁
|
||||||
|
try {
|
||||||
|
const _ = panel.webview;
|
||||||
|
} catch (e) {
|
||||||
|
needCreatePanel = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[addCodeToChat] 需要创建面板:', needCreatePanel);
|
||||||
|
|
||||||
|
if (needCreatePanel) {
|
||||||
|
console.log('[addCodeToChat] 正在打开面板...');
|
||||||
|
await showICHelperPanel(context);
|
||||||
|
panel = (global as any).currentICHelperPanel;
|
||||||
|
console.log('[addCodeToChat] 面板打开后状态:', panel ? '成功' : '失败');
|
||||||
|
|
||||||
|
// 如果面板仍未创建(如未登录),直接返回
|
||||||
|
if (!panel) {
|
||||||
|
console.log('[addCodeToChat] 面板创建失败,退出');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送代码上下文
|
||||||
|
console.log('[addCodeToChat] 准备发送代码到面板');
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
if (panel?.webview) {
|
||||||
|
console.log('[addCodeToChat] 发送 addCodeContext 消息');
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: 'addCodeContext',
|
||||||
|
fileName,
|
||||||
|
startLine,
|
||||||
|
endLine,
|
||||||
|
code: selectedText,
|
||||||
|
languageId: editor.document.languageId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[addCodeToChat] 发送消息失败:', e);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 注册命令:查看会话历史
|
// 注册命令:查看会话历史
|
||||||
// TODO: 这些命令需要根据新的任务架构重新实现
|
// TODO: 这些命令需要根据新的任务架构重新实现
|
||||||
// 暂时注释掉,等待重新实现
|
// 暂时注释掉,等待重新实现
|
||||||
@ -289,12 +412,24 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
const viewProvider = new ICViewProvider(context.extensionUri, context);
|
const viewProvider = new ICViewProvider(context.extensionUri, context);
|
||||||
const viewRegistration = vscode.window.registerWebviewViewProvider(
|
const viewRegistration = vscode.window.registerWebviewViewProvider(
|
||||||
"ic-coder.mainView",
|
"ic-coder.mainView",
|
||||||
viewProvider
|
viewProvider,
|
||||||
|
{
|
||||||
|
webviewOptions: {
|
||||||
|
retainContextWhenHidden: true
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 注册 VCD 自定义编辑器
|
// 注册 VCD 自定义编辑器
|
||||||
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
|
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
|
||||||
|
|
||||||
|
// 注册 Code Action Provider
|
||||||
|
const codeActionProvider = vscode.languages.registerCodeActionsProvider(
|
||||||
|
{ scheme: 'file' },
|
||||||
|
new ICCoderCodeActionProvider(),
|
||||||
|
{ providedCodeActionKinds: [vscode.CodeActionKind.RefactorRewrite] }
|
||||||
|
);
|
||||||
|
|
||||||
// 添加到订阅
|
// 添加到订阅
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
openPanelCommand,
|
openPanelCommand,
|
||||||
@ -305,6 +440,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
logoutCommand,
|
logoutCommand,
|
||||||
changeInvitationCodeCommand,
|
changeInvitationCodeCommand,
|
||||||
testNotificationCommand,
|
testNotificationCommand,
|
||||||
|
addCodeToChat,
|
||||||
|
// testTrialUserCommand,
|
||||||
|
// testExpiredUserCommand,
|
||||||
// TODO: 等待重新实现这些命令
|
// TODO: 等待重新实现这些命令
|
||||||
// viewHistoryCommand,
|
// viewHistoryCommand,
|
||||||
// newSessionCommand,
|
// newSessionCommand,
|
||||||
@ -313,7 +451,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
// clearHistoryCommand,
|
// clearHistoryCommand,
|
||||||
// searchSessionCommand,
|
// searchSessionCommand,
|
||||||
viewRegistration,
|
viewRegistration,
|
||||||
vcdEditorProvider
|
vcdEditorProvider,
|
||||||
|
codeActionProvider
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
78
src/panels/ExpiredPanel.ts
Normal file
78
src/panels/ExpiredPanel.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 试用期到期提醒面板
|
||||||
|
* 功能:试用期到期时显示续费提示
|
||||||
|
* 依赖:vscode
|
||||||
|
* 使用场景:试用用户到期时显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class ExpiredPanel {
|
||||||
|
public static render() {
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'icCoderExpired',
|
||||||
|
'试用期已到期',
|
||||||
|
vscode.ViewColumn.One,
|
||||||
|
{ enableScripts: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.webview.html = this.getHtmlContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getHtmlContent(): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 60px 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
background-color: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: var(--vscode-errorForeground);
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 15px 0;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
padding: 12px 30px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>⏰ 您的试用期已到期</h1>
|
||||||
|
<p>感谢您使用 IC Coder!您的 15 天试用期已结束。</p>
|
||||||
|
<p>如需继续使用,请联系我们获取正式版本。</p>
|
||||||
|
|
||||||
|
<button class="button" onclick="contact()">联系我们</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function contact() {
|
||||||
|
window.open('https://iccoder.com/contact', '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,13 @@ import { VCDFileServer } from "../services/vcdFileServer";
|
|||||||
/**
|
/**
|
||||||
* VCD 波形查看器自定义编辑器提供者
|
* VCD 波形查看器自定义编辑器提供者
|
||||||
*/
|
*/
|
||||||
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider {
|
export class VCDViewerEditorProvider
|
||||||
public static register(context: vscode.ExtensionContext, vcdFileServer: VCDFileServer): vscode.Disposable {
|
implements vscode.CustomReadonlyEditorProvider
|
||||||
|
{
|
||||||
|
public static register(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
vcdFileServer: VCDFileServer,
|
||||||
|
): vscode.Disposable {
|
||||||
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
|
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
|
||||||
const providerRegistration = vscode.window.registerCustomEditorProvider(
|
const providerRegistration = vscode.window.registerCustomEditorProvider(
|
||||||
"ic-coder.vcdViewer",
|
"ic-coder.vcdViewer",
|
||||||
@ -16,20 +21,20 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
|||||||
webviewOptions: {
|
webviewOptions: {
|
||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return providerRegistration;
|
return providerRegistration;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly context: vscode.ExtensionContext,
|
private readonly context: vscode.ExtensionContext,
|
||||||
private readonly vcdFileServer: VCDFileServer
|
private readonly vcdFileServer: VCDFileServer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async openCustomDocument(
|
async openCustomDocument(
|
||||||
uri: vscode.Uri,
|
uri: vscode.Uri,
|
||||||
openContext: vscode.CustomDocumentOpenContext,
|
openContext: vscode.CustomDocumentOpenContext,
|
||||||
token: vscode.CancellationToken
|
token: vscode.CancellationToken,
|
||||||
): Promise<vscode.CustomDocument> {
|
): Promise<vscode.CustomDocument> {
|
||||||
return {
|
return {
|
||||||
uri,
|
uri,
|
||||||
@ -40,7 +45,7 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
|||||||
async resolveCustomEditor(
|
async resolveCustomEditor(
|
||||||
document: vscode.CustomDocument,
|
document: vscode.CustomDocument,
|
||||||
webviewPanel: vscode.WebviewPanel,
|
webviewPanel: vscode.WebviewPanel,
|
||||||
token: vscode.CancellationToken
|
token: vscode.CancellationToken,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
webviewPanel.webview.options = {
|
webviewPanel.webview.options = {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
@ -52,7 +57,7 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
|||||||
webviewPanel,
|
webviewPanel,
|
||||||
this.context.extensionUri,
|
this.context.extensionUri,
|
||||||
document.uri.fsPath,
|
document.uri.fsPath,
|
||||||
this.vcdFileServer
|
this.vcdFileServer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,7 +73,11 @@ export class VCDViewerPanel {
|
|||||||
private _currentVcdPath: string | undefined;
|
private _currentVcdPath: string | undefined;
|
||||||
private _vcdFileServer: VCDFileServer | undefined;
|
private _vcdFileServer: VCDFileServer | undefined;
|
||||||
|
|
||||||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) {
|
private constructor(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
extensionUri: vscode.Uri,
|
||||||
|
vcdFileServer?: VCDFileServer,
|
||||||
|
) {
|
||||||
this._panel = panel;
|
this._panel = panel;
|
||||||
this._extensionUri = extensionUri;
|
this._extensionUri = extensionUri;
|
||||||
this._vcdFileServer = vcdFileServer;
|
this._vcdFileServer = vcdFileServer;
|
||||||
@ -91,7 +100,10 @@ export class VCDViewerPanel {
|
|||||||
break;
|
break;
|
||||||
case "loaded":
|
case "loaded":
|
||||||
// Surfer iframe 加载完成,发送 VCD 文件
|
// Surfer iframe 加载完成,发送 VCD 文件
|
||||||
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
|
console.log(
|
||||||
|
"[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:",
|
||||||
|
this._currentVcdPath,
|
||||||
|
);
|
||||||
if (this._currentVcdPath) {
|
if (this._currentVcdPath) {
|
||||||
this.sendVcdToSurfer(this._currentVcdPath);
|
this.sendVcdToSurfer(this._currentVcdPath);
|
||||||
}
|
}
|
||||||
@ -99,14 +111,18 @@ export class VCDViewerPanel {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
this._disposables
|
this._disposables,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建或显示 VCD 查看器面板
|
* 创建或显示 VCD 查看器面板
|
||||||
*/
|
*/
|
||||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
public static createOrShow(
|
||||||
|
extensionUri: vscode.Uri,
|
||||||
|
vcdFilePath?: string,
|
||||||
|
vcdFileServer?: VCDFileServer,
|
||||||
|
) {
|
||||||
// 在当前活动编辑器旁边打开新列
|
// 在当前活动编辑器旁边打开新列
|
||||||
const column = vscode.ViewColumn.Beside;
|
const column = vscode.ViewColumn.Beside;
|
||||||
|
|
||||||
@ -128,10 +144,14 @@ export class VCDViewerPanel {
|
|||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [extensionUri],
|
localResourceRoots: [extensionUri],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
VCDViewerPanel.currentPanel = new VCDViewerPanel(
|
||||||
|
panel,
|
||||||
|
extensionUri,
|
||||||
|
vcdFileServer,
|
||||||
|
);
|
||||||
|
|
||||||
// 如果提供了 VCD 文件路径,加载它
|
// 如果提供了 VCD 文件路径,加载它
|
||||||
if (vcdFilePath) {
|
if (vcdFilePath) {
|
||||||
@ -146,7 +166,7 @@ export class VCDViewerPanel {
|
|||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
extensionUri: vscode.Uri,
|
extensionUri: vscode.Uri,
|
||||||
vcdFilePath: string,
|
vcdFilePath: string,
|
||||||
vcdFileServer?: VCDFileServer
|
vcdFileServer?: VCDFileServer,
|
||||||
) {
|
) {
|
||||||
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||||
viewer.loadVCDFile(vcdFilePath);
|
viewer.loadVCDFile(vcdFilePath);
|
||||||
@ -172,14 +192,14 @@ export class VCDViewerPanel {
|
|||||||
|
|
||||||
// 更新面板标题
|
// 更新面板标题
|
||||||
const fileName = path.basename(vcdFilePath);
|
const fileName = path.basename(vcdFilePath);
|
||||||
this._panel.title = `Surfer 波形查看器 - ${fileName}`;
|
this._panel.title = `波形查看器 - ${fileName}`;
|
||||||
|
|
||||||
// 设置 HTML 内容
|
// 设置 HTML 内容
|
||||||
this._panel.webview.html = this._getWebviewContent();
|
this._panel.webview.html = this._getWebviewContent();
|
||||||
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,8 +210,8 @@ export class VCDViewerPanel {
|
|||||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||||
try {
|
try {
|
||||||
// 读取 VCD 文件
|
// 读取 VCD 文件
|
||||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
|
||||||
const lines = buffer.split('\n');
|
const lines = buffer.split("\n");
|
||||||
|
|
||||||
const scopeNames: string[] = [];
|
const scopeNames: string[] = [];
|
||||||
let scopeDepth = 0;
|
let scopeDepth = 0;
|
||||||
@ -201,7 +221,7 @@ export class VCDViewerPanel {
|
|||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
// 遇到 $enddefinitions 就停止解析
|
// 遇到 $enddefinitions 就停止解析
|
||||||
if (trimmed.startsWith('$enddefinitions')) {
|
if (trimmed.startsWith("$enddefinitions")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,22 +232,22 @@ export class VCDViewerPanel {
|
|||||||
const scopeName = scopeMatch[2];
|
const scopeName = scopeMatch[2];
|
||||||
|
|
||||||
// 记录顶层 module (depth = 0)
|
// 记录顶层 module (depth = 0)
|
||||||
if (scopeDepth === 0 && scopeType === 'module') {
|
if (scopeDepth === 0 && scopeType === "module") {
|
||||||
scopeStack.push(scopeName);
|
scopeStack.push(scopeName);
|
||||||
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
|
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
|
||||||
}
|
}
|
||||||
// 记录顶层下的直接子模块 (depth = 1)
|
// 记录顶层下的直接子模块 (depth = 1)
|
||||||
else if (scopeDepth === 1 && scopeType === 'module') {
|
else if (scopeDepth === 1 && scopeType === "module") {
|
||||||
const fullPath = [...scopeStack, scopeName];
|
const fullPath = [...scopeStack, scopeName];
|
||||||
scopeNames.push(fullPath.join('.'));
|
scopeNames.push(fullPath.join("."));
|
||||||
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.'));
|
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join("."));
|
||||||
}
|
}
|
||||||
|
|
||||||
scopeDepth++;
|
scopeDepth++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 遇到 $upscope 减少深度
|
// 遇到 $upscope 减少深度
|
||||||
if (trimmed.startsWith('$upscope')) {
|
if (trimmed.startsWith("$upscope")) {
|
||||||
scopeDepth--;
|
scopeDepth--;
|
||||||
if (scopeDepth === 0) {
|
if (scopeDepth === 0) {
|
||||||
scopeStack.pop();
|
scopeStack.pop();
|
||||||
@ -277,7 +297,7 @@ export class VCDViewerPanel {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
|
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`
|
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,13 +372,23 @@ export class VCDViewerPanel {
|
|||||||
private _getWebviewContent(): string {
|
private _getWebviewContent(): string {
|
||||||
// 获取 surfer 资源 URI
|
// 获取 surfer 资源 URI
|
||||||
const surferJsUri = this._panel.webview.asWebviewUri(
|
const surferJsUri = this._panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
|
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js"),
|
||||||
);
|
);
|
||||||
const surferWasmUri = this._panel.webview.asWebviewUri(
|
const surferWasmUri = this._panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
|
vscode.Uri.joinPath(
|
||||||
|
this._extensionUri,
|
||||||
|
"media",
|
||||||
|
"surfer",
|
||||||
|
"surfer_bg.wasm",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const integrationJsUri = this._panel.webview.asWebviewUri(
|
const integrationJsUri = this._panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js")
|
vscode.Uri.joinPath(
|
||||||
|
this._extensionUri,
|
||||||
|
"media",
|
||||||
|
"surfer",
|
||||||
|
"integration.js",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
@ -367,7 +397,7 @@ export class VCDViewerPanel {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<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:*;">
|
<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>
|
<title>波形查看器</title>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 获取 VS Code API(只能调用一次)
|
// 获取 VS Code API(只能调用一次)
|
||||||
|
|||||||
153
src/panels/WelcomePanel.ts
Normal file
153
src/panels/WelcomePanel.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* 欢迎引导面板
|
||||||
|
* 功能:插件试用用户首次登录显示使用教程
|
||||||
|
* 依赖:vscode
|
||||||
|
* 使用场景:试用用户首次登录时显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class WelcomePanel {
|
||||||
|
public static currentPanel: WelcomePanel | undefined;
|
||||||
|
private readonly _panel: vscode.WebviewPanel;
|
||||||
|
private _disposables: vscode.Disposable[] = [];
|
||||||
|
|
||||||
|
private constructor(panel: vscode.WebviewPanel) {
|
||||||
|
this._panel = panel;
|
||||||
|
this._panel.webview.html = this.getHtmlContent();
|
||||||
|
|
||||||
|
// 监听来自 webview 的消息
|
||||||
|
this._panel.webview.onDidReceiveMessage(
|
||||||
|
(message) => {
|
||||||
|
if (message.command === 'close') {
|
||||||
|
this._panel.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
this._disposables
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听关闭事件
|
||||||
|
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static render(context: vscode.ExtensionContext) {
|
||||||
|
// 避免重复显示
|
||||||
|
if (WelcomePanel.currentPanel) {
|
||||||
|
WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'icCoderWelcome',
|
||||||
|
'欢迎使用 IC Coder',
|
||||||
|
vscode.ViewColumn.One,
|
||||||
|
{
|
||||||
|
enableScripts: true,
|
||||||
|
retainContextWhenHidden: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
WelcomePanel.currentPanel = new WelcomePanel(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlContent(): string {
|
||||||
|
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>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 40px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
background-color: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.welcome-message {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
.step h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
.step p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🎉 欢迎使用 IC Coder!</h1>
|
||||||
|
<p class="welcome-message">
|
||||||
|
您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>📝 步骤 1:打开聊天面板</h3>
|
||||||
|
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>💬 步骤 2:输入您的需求</h3>
|
||||||
|
<p>描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>🔬 步骤 3:运行仿真</h3>
|
||||||
|
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button" onclick="closePanel()">开始使用</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
|
||||||
|
function closePanel() {
|
||||||
|
vscode.postMessage({ command: 'close' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
WelcomePanel.currentPanel = undefined;
|
||||||
|
this._panel.dispose();
|
||||||
|
while (this._disposables.length) {
|
||||||
|
const disposable = this._disposables.pop();
|
||||||
|
if (disposable) {
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/panels/helpers/authHelper.ts
Normal file
66
src/panels/helpers/authHelper.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 认证辅助模块
|
||||||
|
* 功能:处理用户登录状态检查和 token 验证
|
||||||
|
* 依赖:vscode, jwtUtils
|
||||||
|
* 使用场景:面板初始化时验证用户登录状态
|
||||||
|
*/
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import { isTokenExpired } from "../../utils/jwtUtils";
|
||||||
|
|
||||||
|
export async function checkAuthAndPromptLogin(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
): Promise<boolean> {
|
||||||
|
let token: string | undefined;
|
||||||
|
try {
|
||||||
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
createIfNone: false,
|
||||||
|
});
|
||||||
|
token = session?.accessToken;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[AuthHelper] 获取 session 失败:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token && isTokenExpired(token)) {
|
||||||
|
await context.globalState.update("icCoderSessions", []);
|
||||||
|
await context.globalState.update("icCoderUserInfo", undefined);
|
||||||
|
const action = await vscode.window.showWarningMessage(
|
||||||
|
"登录已过期,请重新登录",
|
||||||
|
"立即登录",
|
||||||
|
);
|
||||||
|
if (action === "立即登录") {
|
||||||
|
vscode.commands.executeCommand("ic-coder.login", { forceReauth: true });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
createIfNone: false,
|
||||||
|
});
|
||||||
|
if (!session) {
|
||||||
|
vscode.window
|
||||||
|
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||||
|
.then((selection) => {
|
||||||
|
if (selection === "立即登录") {
|
||||||
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window
|
||||||
|
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||||
|
.then((selection) => {
|
||||||
|
if (selection === "立即登录") {
|
||||||
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
104
src/panels/helpers/contextHelper.ts
Normal file
104
src/panels/helpers/contextHelper.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* 上下文管理模块
|
||||||
|
* 功能:处理文件、文件夹、图片、文档上下文添加
|
||||||
|
* 依赖:vscode, fs, path
|
||||||
|
* 使用场景:用户添加上下文项时
|
||||||
|
*/
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
|
export async function handleAddContextFile(panel: vscode.WebviewPanel) {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleAddContextFolder(panel: vscode.WebviewPanel) {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleAddContextImage(panel: vscode.WebviewPanel) {
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleAddContextDocument(panel: vscode.WebviewPanel) {
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
224
src/panels/helpers/conversationHelper.ts
Normal file
224
src/panels/helpers/conversationHelper.ts
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
/**
|
||||||
|
* 会话历史管理模块
|
||||||
|
* 功能:加载和选择会话历史
|
||||||
|
* 依赖:vscode, chatHistoryManager, messageHandler
|
||||||
|
* 使用场景:会话历史列表和切换
|
||||||
|
*/
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||||
|
import { MessageType } from "../../types/chatHistory";
|
||||||
|
import { setLastTaskId } from "../../utils/messageHandler";
|
||||||
|
|
||||||
|
export async function loadConversationHistory(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 10,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
|
||||||
|
if (!workspacePath) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
hasMore: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await historyManager.getConversationHistoryList(
|
||||||
|
workspacePath,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: result.items,
|
||||||
|
total: result.total,
|
||||||
|
hasMore: result.hasMore,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载会话历史失败:", error);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
hasMore: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectConversation(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
taskId: string,
|
||||||
|
extensionPath: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
|
||||||
|
if (!workspacePath) {
|
||||||
|
vscode.window.showErrorMessage("没有打开的工作区");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskSession = await historyManager.loadTaskSession(
|
||||||
|
workspacePath,
|
||||||
|
taskId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!taskSession) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const switched = await historyManager.switchTask(workspacePath, taskId);
|
||||||
|
if (!switched) {
|
||||||
|
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastTaskId(taskId);
|
||||||
|
|
||||||
|
const panelId = (panel as any).__uniqueId;
|
||||||
|
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||||
|
|
||||||
|
panel.webview.postMessage({ command: "clearChat" });
|
||||||
|
|
||||||
|
const segments: any[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < taskSession.messages.length) {
|
||||||
|
const message = taskSession.messages[i];
|
||||||
|
|
||||||
|
if (message.type === MessageType.USER) {
|
||||||
|
if (segments.length > 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveSegments",
|
||||||
|
segments: [...segments],
|
||||||
|
});
|
||||||
|
segments.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textContent = message.contents?.find((c) => c.type === "TEXT");
|
||||||
|
if (textContent && "text" in textContent) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "addUserMessage",
|
||||||
|
text: textContent.text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else if (message.type === MessageType.AI) {
|
||||||
|
if (message.segments && message.segments.length > 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveSegments",
|
||||||
|
segments: message.segments,
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
if (message.text) {
|
||||||
|
segments.push({ type: "text", content: message.text });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.toolExecutionRequests &&
|
||||||
|
message.toolExecutionRequests.length > 0
|
||||||
|
) {
|
||||||
|
for (const toolReq of message.toolExecutionRequests) {
|
||||||
|
let toolResult = "";
|
||||||
|
if (i + 1 < taskSession.messages.length) {
|
||||||
|
const nextMsg = taskSession.messages[i + 1];
|
||||||
|
if (
|
||||||
|
nextMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||||
|
nextMsg.id === toolReq.id
|
||||||
|
) {
|
||||||
|
toolResult = nextMsg.text;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
type: "tool",
|
||||||
|
toolName: toolReq.name,
|
||||||
|
askId: toolReq.id,
|
||||||
|
toolResult: toolResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
|
||||||
|
while (i < taskSession.messages.length) {
|
||||||
|
const nextMsg = taskSession.messages[i];
|
||||||
|
if (nextMsg.type === MessageType.USER) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextMsg.type === MessageType.AI) {
|
||||||
|
if (nextMsg.segments && nextMsg.segments.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextMsg.text) {
|
||||||
|
segments.push({ type: "text", content: nextMsg.text });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
nextMsg.toolExecutionRequests &&
|
||||||
|
nextMsg.toolExecutionRequests.length > 0
|
||||||
|
) {
|
||||||
|
for (const toolReq of nextMsg.toolExecutionRequests) {
|
||||||
|
let toolResult = "";
|
||||||
|
if (i + 1 < taskSession.messages.length) {
|
||||||
|
const resultMsg = taskSession.messages[i + 1];
|
||||||
|
if (
|
||||||
|
resultMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||||
|
resultMsg.id === toolReq.id
|
||||||
|
) {
|
||||||
|
toolResult = resultMsg.text;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segments.push({
|
||||||
|
type: "tool",
|
||||||
|
toolName: toolReq.name,
|
||||||
|
askId: toolReq.id,
|
||||||
|
toolResult: toolResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) {
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.length > 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveSegments",
|
||||||
|
segments: segments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送任务完成消息(历史记录)
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "taskCompleteHistory",
|
||||||
|
});
|
||||||
|
|
||||||
|
vscode.window.showInformationMessage(
|
||||||
|
`已加载会话: ${taskSession.meta.taskName}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("选择会话失败:", error);
|
||||||
|
vscode.window.showErrorMessage(`加载会话失败: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/panels/helpers/fileHelper.ts
Normal file
78
src/panels/helpers/fileHelper.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 文件操作辅助模块
|
||||||
|
* 功能:处理文件打开、选择等操作
|
||||||
|
* 依赖:vscode, fs, path
|
||||||
|
* 使用场景:打开文件、跳转到代码位置
|
||||||
|
*/
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
|
export async function openFile(filePath: string) {
|
||||||
|
const path = require("path");
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
const fullPath =
|
||||||
|
path.isAbsolute(filePath) || !workspaceFolder
|
||||||
|
? filePath
|
||||||
|
: vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath;
|
||||||
|
const doc = await vscode.workspace.openTextDocument(fullPath);
|
||||||
|
await vscode.window.showTextDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openFileWithSelection(
|
||||||
|
filePath: string,
|
||||||
|
startLine: number,
|
||||||
|
endLine: number,
|
||||||
|
) {
|
||||||
|
const path = require("path");
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
const fullPath =
|
||||||
|
path.isAbsolute(filePath) || !workspaceFolder
|
||||||
|
? filePath
|
||||||
|
: vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath;
|
||||||
|
const doc = await vscode.workspace.openTextDocument(fullPath);
|
||||||
|
const editor = await vscode.window.showTextDocument(doc);
|
||||||
|
const start = new vscode.Position(startLine - 1, 0);
|
||||||
|
const end = new vscode.Position(
|
||||||
|
endLine - 1,
|
||||||
|
doc.lineAt(endLine - 1).text.length,
|
||||||
|
);
|
||||||
|
editor.selection = new vscode.Selection(start, end);
|
||||||
|
editor.revealRange(new vscode.Range(start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openFilePathTag(
|
||||||
|
filePath: string,
|
||||||
|
startLine?: number,
|
||||||
|
endLine?: number,
|
||||||
|
) {
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
|
||||||
|
let fullPath = filePath;
|
||||||
|
|
||||||
|
if (!path.isAbsolute(filePath) && workspaceFolder) {
|
||||||
|
const candidatePath = vscode.Uri.joinPath(
|
||||||
|
workspaceFolder.uri,
|
||||||
|
filePath,
|
||||||
|
).fsPath;
|
||||||
|
if (fs.existsSync(candidatePath)) {
|
||||||
|
fullPath = candidatePath;
|
||||||
|
} else {
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
const files = await vscode.workspace.findFiles(
|
||||||
|
`**/${fileName}`,
|
||||||
|
"**/node_modules/**",
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
if (files.length > 0) {
|
||||||
|
fullPath = files[0].fsPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startLine && endLine) {
|
||||||
|
await openFileWithSelection(fullPath, startLine, endLine);
|
||||||
|
} else {
|
||||||
|
await openFile(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
396
src/panels/helpers/messageRouter.ts
Normal file
396
src/panels/helpers/messageRouter.ts
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
/**
|
||||||
|
* 消息路由处理模块
|
||||||
|
* 功能:处理 webview 消息的路由分发
|
||||||
|
* 依赖:各个 helper 模块和 messageHandler
|
||||||
|
* 使用场景:webview 消息接收时
|
||||||
|
*/
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import {
|
||||||
|
handleUserMessage,
|
||||||
|
insertCodeToEditor,
|
||||||
|
handleReadFile,
|
||||||
|
handleUpdateFile,
|
||||||
|
handleRenameFile,
|
||||||
|
handleReplaceInFile,
|
||||||
|
handleUserAnswer,
|
||||||
|
abortCurrentDialog,
|
||||||
|
handleOptimizePrompt,
|
||||||
|
handlePlanAction,
|
||||||
|
getCurrentTaskId,
|
||||||
|
handleAcceptChange,
|
||||||
|
handleRejectChange,
|
||||||
|
handleOpenFileDiff,
|
||||||
|
startChangeSession,
|
||||||
|
} from "../../utils/messageHandler";
|
||||||
|
import { compactDialog } from "../../services/apiClient";
|
||||||
|
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||||
|
import { getCachedUserInfo } from "../../services/userService";
|
||||||
|
import { loadConversationHistory, selectConversation } from "./conversationHelper";
|
||||||
|
import { getVCDFileInfo } from "./vcdHelper";
|
||||||
|
import {
|
||||||
|
handleAddContextFile,
|
||||||
|
handleAddContextFolder,
|
||||||
|
handleAddContextImage,
|
||||||
|
handleAddContextDocument,
|
||||||
|
} from "./contextHelper";
|
||||||
|
import { openFile, openFileWithSelection, openFilePathTag } from "./fileHelper";
|
||||||
|
|
||||||
|
export async function handleWebviewMessage(
|
||||||
|
message: any,
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
) {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const panelId = (panel as any).__uniqueId;
|
||||||
|
|
||||||
|
switch (message.command) {
|
||||||
|
case "sendMessage":
|
||||||
|
if (!historyManager.getPanelTask(panelId)) {
|
||||||
|
const workspacePath =
|
||||||
|
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
if (workspacePath) {
|
||||||
|
try {
|
||||||
|
const taskMeta = await historyManager.createTask(
|
||||||
|
workspacePath,
|
||||||
|
"新对话",
|
||||||
|
);
|
||||||
|
historyManager.setPanelTask(
|
||||||
|
panelId,
|
||||||
|
taskMeta.taskId,
|
||||||
|
workspacePath,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("创建任务失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
const sessionId = `session_${panelId}_${Date.now()}`;
|
||||||
|
startChangeSession(sessionId);
|
||||||
|
panel.webview.postMessage({ type: "showProgress" });
|
||||||
|
|
||||||
|
handleUserMessage(
|
||||||
|
panel,
|
||||||
|
message.text,
|
||||||
|
context.extensionPath,
|
||||||
|
message.mode,
|
||||||
|
message.model,
|
||||||
|
message.contextItems,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "readFile":
|
||||||
|
handleReadFile(panel, message.filePath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "updateFile":
|
||||||
|
handleUpdateFile(panel, message.filePath, message.content);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "renameFile":
|
||||||
|
handleRenameFile(panel, message.oldPath, message.newPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "replaceInFile":
|
||||||
|
handleReplaceInFile(
|
||||||
|
panel,
|
||||||
|
message.filePath,
|
||||||
|
message.searchText,
|
||||||
|
message.replaceText,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "insertCode":
|
||||||
|
insertCodeToEditor(message.code);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "showInfo":
|
||||||
|
vscode.window.showInformationMessage(message.text);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "openWaveformViewer":
|
||||||
|
if (message.vcdFilePath) {
|
||||||
|
vscode.commands.executeCommand(
|
||||||
|
"ic-coder.openVCDViewer",
|
||||||
|
message.vcdFilePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "getVCDInfo":
|
||||||
|
if (message.vcdFilePath && message.containerId) {
|
||||||
|
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "createNewConversation":
|
||||||
|
const { showICHelperPanel } = require("../ICHelperPanel");
|
||||||
|
showICHelperPanel(context, panel.viewColumn);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "loadConversationHistory":
|
||||||
|
loadConversationHistory(
|
||||||
|
panel,
|
||||||
|
message.offset || 0,
|
||||||
|
message.limit || 10,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "selectConversation":
|
||||||
|
if (message.conversationId) {
|
||||||
|
selectConversation(panel, message.conversationId, context.extensionPath);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "submitAnswer":
|
||||||
|
void handleUserAnswer(
|
||||||
|
message.askId,
|
||||||
|
message.selected,
|
||||||
|
message.customInput,
|
||||||
|
message.answers,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "abortDialog":
|
||||||
|
void abortCurrentDialog();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "compressConversation":
|
||||||
|
{
|
||||||
|
const taskId = getCurrentTaskId();
|
||||||
|
if (taskId) {
|
||||||
|
compactDialog(taskId)
|
||||||
|
.then((result) => {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveMessage",
|
||||||
|
text: result.success
|
||||||
|
? "✅ 会话压缩完成"
|
||||||
|
: `❌ 压缩失败: ${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 "openFile":
|
||||||
|
if (message.filePath) {
|
||||||
|
await openFile(message.filePath);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "openFileWithSelection":
|
||||||
|
if (message.filePath) {
|
||||||
|
await openFileWithSelection(
|
||||||
|
message.filePath,
|
||||||
|
message.startLine,
|
||||||
|
message.endLine,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "openFilePathTag":
|
||||||
|
if (message.filePath) {
|
||||||
|
await openFilePathTag(
|
||||||
|
message.filePath,
|
||||||
|
message.startLine,
|
||||||
|
message.endLine,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "acceptChange":
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleAcceptChange(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "rejectChange":
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleRejectChange(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "openFileDiff":
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleOpenFileDiff(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "checkInvitationCode":
|
||||||
|
{
|
||||||
|
const userInfo = getCachedUserInfo();
|
||||||
|
if (userInfo?.isPluginTrial === true) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "invitationCodeStatus",
|
||||||
|
verified: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { InvitationService } = require("../../services/invitationService");
|
||||||
|
const isVerified = await InvitationService.isVerified(context);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "invitationCodeStatus",
|
||||||
|
verified: isVerified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "checkWelcomeModal":
|
||||||
|
{
|
||||||
|
const userInfo = getCachedUserInfo();
|
||||||
|
if (userInfo?.isPluginTrial === true) {
|
||||||
|
if (userInfo.pluginTrialExpiresAt === undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (userInfo.pluginTrialExpiresAt !== null) {
|
||||||
|
const now = Date.now();
|
||||||
|
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||||
|
if (isExpired) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panel.webview.postMessage({ command: "showWelcomeModal" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "checkTrialExpiration":
|
||||||
|
{
|
||||||
|
const { TrialExpirationService } = require("../../services/trialExpirationService");
|
||||||
|
const trialService = new TrialExpirationService(context, panel);
|
||||||
|
await trialService.checkExpiration();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "verifyInvitationCode":
|
||||||
|
{
|
||||||
|
const { InvitationService } = require("../../services/invitationService");
|
||||||
|
const result = await InvitationService.verifyCode(message.code);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
await InvitationService.saveVerificationStatus(context, message.code);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "invitationCodeVerified",
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
panel.webview.postMessage({ command: "showNdtWelcomeModal" });
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "invitationCodeVerified",
|
||||||
|
success: false,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "openICCoder":
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "openTutorial":
|
||||||
|
vscode.env.openExternal(
|
||||||
|
vscode.Uri.parse(
|
||||||
|
"https://www.iccoder.com/guides/quick-start/first-task-plugin",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "openUserManual":
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "openUserFeedback":
|
||||||
|
panel.webview.postMessage({ command: "showFeedbackQRCode" });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "planAction":
|
||||||
|
if (message.action === "confirm") {
|
||||||
|
panel.webview.postMessage({ command: "switchMode", mode: "agent" });
|
||||||
|
} else if (message.action === "modify" || message.action === "cancel") {
|
||||||
|
void handlePlanAction(
|
||||||
|
panel,
|
||||||
|
message.action,
|
||||||
|
message.planTitle || "",
|
||||||
|
context.extensionPath,
|
||||||
|
message.model,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "addContextFile":
|
||||||
|
await handleAddContextFile(panel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "addContextFolder":
|
||||||
|
await handleAddContextFolder(panel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "addContextImage":
|
||||||
|
await handleAddContextImage(panel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "addContextDocument":
|
||||||
|
await handleAddContextDocument(panel);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/panels/helpers/userInfoHelper.ts
Normal file
116
src/panels/helpers/userInfoHelper.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 用户信息辅助模块
|
||||||
|
* 功能:管理用户信息的获取、更新和发送
|
||||||
|
* 依赖:vscode, userService, creditsService
|
||||||
|
* 使用场景:面板初始化和余额更新时
|
||||||
|
*/
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import { getCachedUserInfo } from "../../services/userService";
|
||||||
|
import { setBalanceUpdateCallback } from "../../services/creditsService";
|
||||||
|
|
||||||
|
export 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,
|
||||||
|
"dist",
|
||||||
|
"assets",
|
||||||
|
"titleIcon",
|
||||||
|
iconFile,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return iconUri.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendUserInfoToWebview(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
let userInfo = getCachedUserInfo();
|
||||||
|
|
||||||
|
if (userInfo) {
|
||||||
|
console.log("[UserInfoHelper] 使用缓存的用户信息:", userInfo);
|
||||||
|
const tierIconUrl = getTierIconUri(
|
||||||
|
panel.webview,
|
||||||
|
context,
|
||||||
|
userInfo.membership?.tierCode,
|
||||||
|
);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateUserInfo",
|
||||||
|
userInfo: {
|
||||||
|
userId: userInfo.userId,
|
||||||
|
nickname: userInfo.nickname,
|
||||||
|
username: userInfo.username,
|
||||||
|
credits: userInfo.credits,
|
||||||
|
membership: userInfo.membership,
|
||||||
|
},
|
||||||
|
tierIconUrl: tierIconUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
createIfNone: false,
|
||||||
|
});
|
||||||
|
if (session) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateUserInfo",
|
||||||
|
userInfo: {
|
||||||
|
userId: session.account.id,
|
||||||
|
nickname: session.account.label,
|
||||||
|
username: session.account.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[UserInfoHelper] 获取用户信息失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupBalanceUpdateCallback(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
) {
|
||||||
|
setBalanceUpdateCallback((balance: number) => {
|
||||||
|
const userInfo = getCachedUserInfo();
|
||||||
|
if (userInfo) {
|
||||||
|
userInfo.credits = balance;
|
||||||
|
const tierIconUrl = getTierIconUri(
|
||||||
|
panel.webview,
|
||||||
|
context,
|
||||||
|
userInfo.membership?.tierCode,
|
||||||
|
);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateUserInfo",
|
||||||
|
userInfo: {
|
||||||
|
userId: userInfo.userId,
|
||||||
|
nickname: userInfo.nickname,
|
||||||
|
username: userInfo.username,
|
||||||
|
credits: balance,
|
||||||
|
membership: userInfo.membership,
|
||||||
|
},
|
||||||
|
tierIconUrl: tierIconUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
158
src/panels/helpers/vcdHelper.ts
Normal file
158
src/panels/helpers/vcdHelper.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* VCD 文件处理模块
|
||||||
|
* 功能:VCD 文件信息获取和信号解析
|
||||||
|
* 依赖:vscode, fs
|
||||||
|
* 使用场景:波形查看器相关功能
|
||||||
|
*/
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
|
export async function getVCDFileInfo(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
vcdFilePath: string,
|
||||||
|
containerId: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
if (!fs.existsSync(vcdFilePath)) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "vcdInfo",
|
||||||
|
containerId: containerId,
|
||||||
|
vcdInfo: {
|
||||||
|
signalCount: "N/A",
|
||||||
|
timeRange: "N/A",
|
||||||
|
fileSize: "N/A",
|
||||||
|
error: "文件不存在",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(vcdFilePath);
|
||||||
|
const fileSizeKB = stats.size / 1024;
|
||||||
|
const fileSize =
|
||||||
|
fileSizeKB < 1024
|
||||||
|
? `${fileSizeKB.toFixed(2)} KB`
|
||||||
|
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
|
||||||
|
|
||||||
|
const content = fs.readFileSync(vcdFilePath, "utf-8");
|
||||||
|
const varMatches = content.match(/\$var/g);
|
||||||
|
const signalCount = varMatches ? varMatches.length : 0;
|
||||||
|
|
||||||
|
let timeRange = "N/A";
|
||||||
|
const timeMatch = content.match(/#(\d+)/g);
|
||||||
|
if (timeMatch && timeMatch.length > 0) {
|
||||||
|
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
|
||||||
|
const minTime = Math.min(...times);
|
||||||
|
const maxTime = Math.max(...times);
|
||||||
|
timeRange = `${minTime} - ${maxTime}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signals = parseVCDSignals(content, 3);
|
||||||
|
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "vcdInfo",
|
||||||
|
containerId: containerId,
|
||||||
|
vcdInfo: {
|
||||||
|
signalCount: signalCount.toString(),
|
||||||
|
timeRange: timeRange,
|
||||||
|
fileSize: fileSize,
|
||||||
|
signals: signals,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取 VCD 文件信息失败:", error);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "vcdInfo",
|
||||||
|
containerId: containerId,
|
||||||
|
vcdInfo: {
|
||||||
|
signalCount: "N/A",
|
||||||
|
timeRange: "N/A",
|
||||||
|
fileSize: "N/A",
|
||||||
|
error: error instanceof Error ? error.message : "未知错误",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||||
|
const signals: Array<{
|
||||||
|
name: string;
|
||||||
|
identifier: string;
|
||||||
|
width: number;
|
||||||
|
values: Array<{ time: number; value: string }>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
|
||||||
|
let match;
|
||||||
|
const signalDefs: Array<{
|
||||||
|
name: string;
|
||||||
|
identifier: string;
|
||||||
|
width: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
while (
|
||||||
|
(match = varRegex.exec(content)) !== null &&
|
||||||
|
signalDefs.length < maxSignals
|
||||||
|
) {
|
||||||
|
const width = parseInt(match[2]);
|
||||||
|
const identifier = match[3];
|
||||||
|
const name = match[4].trim();
|
||||||
|
signalDefs.push({ name, identifier, width });
|
||||||
|
}
|
||||||
|
|
||||||
|
const dumpvarsIndex = content.indexOf("$dumpvars");
|
||||||
|
if (dumpvarsIndex === -1) {
|
||||||
|
return signals;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSection = content.substring(dumpvarsIndex);
|
||||||
|
|
||||||
|
for (const signalDef of signalDefs) {
|
||||||
|
const values: Array<{ time: number; value: string }> = [];
|
||||||
|
let currentTime = 0;
|
||||||
|
const lines = dataSection.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
if (trimmedLine.startsWith("#")) {
|
||||||
|
currentTime = parseInt(trimmedLine.substring(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signalDef.width === 1) {
|
||||||
|
const singleBitMatch = trimmedLine.match(
|
||||||
|
new RegExp(`^([01xz])${signalDef.identifier}$`),
|
||||||
|
);
|
||||||
|
if (singleBitMatch) {
|
||||||
|
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const multiBitMatch = trimmedLine.match(
|
||||||
|
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`),
|
||||||
|
);
|
||||||
|
if (multiBitMatch) {
|
||||||
|
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.length >= 50) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signals.push({
|
||||||
|
name: signalDef.name,
|
||||||
|
identifier: signalDef.identifier,
|
||||||
|
width: signalDef.width,
|
||||||
|
values: values,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解析 VCD 信号数据失败:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return signals;
|
||||||
|
}
|
||||||
26
src/providers/codeActionProvider.ts
Normal file
26
src/providers/codeActionProvider.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Code Action Provider - 为选中代码提供快捷操作
|
||||||
|
* 功能:在小灯泡菜单中显示"添加到 IC Coder 对话"选项
|
||||||
|
*/
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class ICCoderCodeActionProvider implements vscode.CodeActionProvider {
|
||||||
|
provideCodeActions(
|
||||||
|
document: vscode.TextDocument,
|
||||||
|
range: vscode.Range
|
||||||
|
): vscode.CodeAction[] {
|
||||||
|
const selectedText = document.getText(range);
|
||||||
|
if (!selectedText) return [];
|
||||||
|
|
||||||
|
const action = new vscode.CodeAction(
|
||||||
|
'💬 添加到 IC Coder 对话',
|
||||||
|
vscode.CodeActionKind.RefactorRewrite
|
||||||
|
);
|
||||||
|
action.command = {
|
||||||
|
command: 'ic-coder.addCodeToChat',
|
||||||
|
title: '添加到对话'
|
||||||
|
};
|
||||||
|
|
||||||
|
return [action];
|
||||||
|
}
|
||||||
|
}
|
||||||
210
src/services/changeTracker.ts
Normal file
210
src/services/changeTracker.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* 文件变更追踪服务
|
||||||
|
* 功能:收集和管理 AI 修改文件的变更记录
|
||||||
|
* 依赖:types/fileChanges
|
||||||
|
* 使用场景:在文件操作时记录变更,供用户审查
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FileChange, ChangeSession } from '../types/fileChanges';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
class ChangeTrackerService {
|
||||||
|
private currentSession: ChangeSession | null = null;
|
||||||
|
private changeListeners: Array<(session: ChangeSession) => void> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始新的变更会话
|
||||||
|
*/
|
||||||
|
startSession(sessionId: string): void {
|
||||||
|
// 如果已有 session(无论状态),重用并重置为 active
|
||||||
|
if (this.currentSession) {
|
||||||
|
this.currentSession.status = 'active';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentSession = {
|
||||||
|
sessionId,
|
||||||
|
startTime: Date.now(),
|
||||||
|
changes: [],
|
||||||
|
status: 'active'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录文件变更
|
||||||
|
*/
|
||||||
|
trackChange(filePath: string, oldContent: string, newContent: string): string {
|
||||||
|
if (!this.currentSession) {
|
||||||
|
this.startSession(`session_${Date.now()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeId = `change_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
// 判断变更类型
|
||||||
|
let changeType: 'create' | 'modify' | 'delete';
|
||||||
|
if (oldContent === '' && newContent !== '') {
|
||||||
|
changeType = 'create';
|
||||||
|
} else if (oldContent !== '' && newContent === '') {
|
||||||
|
changeType = 'delete';
|
||||||
|
} else {
|
||||||
|
changeType = 'modify';
|
||||||
|
}
|
||||||
|
|
||||||
|
const change: FileChange = {
|
||||||
|
filePath,
|
||||||
|
oldContent,
|
||||||
|
newContent,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
changeType,
|
||||||
|
changeId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.currentSession!.changes.push(change);
|
||||||
|
this.notifyListeners();
|
||||||
|
|
||||||
|
return changeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束当前会话
|
||||||
|
*/
|
||||||
|
endSession(): ChangeSession | null {
|
||||||
|
if (this.currentSession && this.currentSession.changes.length > 0) {
|
||||||
|
this.currentSession.status = 'completed';
|
||||||
|
const session = this.currentSession;
|
||||||
|
this.notifyListeners();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前会话
|
||||||
|
*/
|
||||||
|
getCurrentSession(): ChangeSession | null {
|
||||||
|
return this.currentSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空当前会话
|
||||||
|
*/
|
||||||
|
clearSession(): void {
|
||||||
|
this.currentSession = null;
|
||||||
|
this.notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定的变更
|
||||||
|
*/
|
||||||
|
removeChange(changeId: string): boolean {
|
||||||
|
if (!this.currentSession) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.currentSession.changes.findIndex(c => c.changeId === changeId);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.currentSession.changes.splice(index, 1);
|
||||||
|
this.notifyListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听变更
|
||||||
|
*/
|
||||||
|
onChangeUpdate(listener: (session: ChangeSession) => void): void {
|
||||||
|
this.changeListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知所有监听器
|
||||||
|
*/
|
||||||
|
private notifyListeners(): void {
|
||||||
|
if (this.currentSession) {
|
||||||
|
this.changeListeners.forEach(listener => listener(this.currentSession!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采纳变更(保存文件)
|
||||||
|
*/
|
||||||
|
async acceptChange(changeId: string): Promise<boolean> {
|
||||||
|
if (!this.currentSession) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = this.currentSession.changes.find(c => c.changeId === changeId);
|
||||||
|
if (!change) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath);
|
||||||
|
|
||||||
|
// 如果是删除操作,删除文件
|
||||||
|
if (change.changeType === 'delete') {
|
||||||
|
if (fs.existsSync(absolutePath)) {
|
||||||
|
await fs.promises.unlink(absolutePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 创建或修改文件
|
||||||
|
await fs.promises.writeFile(absolutePath, change.newContent, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeChange(changeId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ChangeTracker] 采纳变更失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝变更(恢复旧内容)
|
||||||
|
*/
|
||||||
|
async rejectChange(changeId: string): Promise<boolean> {
|
||||||
|
if (!this.currentSession) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = this.currentSession.changes.find(c => c.changeId === changeId);
|
||||||
|
if (!change) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath);
|
||||||
|
|
||||||
|
// 如果是新建文件,删除它
|
||||||
|
if (change.changeType === 'create') {
|
||||||
|
if (fs.existsSync(absolutePath)) {
|
||||||
|
await fs.promises.unlink(absolutePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 恢复旧内容
|
||||||
|
await fs.promises.writeFile(absolutePath, change.oldContent, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeChange(changeId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ChangeTracker] 拒绝变更失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeTracker = new ChangeTrackerService();
|
||||||
@ -25,6 +25,9 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
|
|||||||
/** ExtensionContext 用于持久化存储 */
|
/** ExtensionContext 用于持久化存储 */
|
||||||
let extensionContext: vscode.ExtensionContext | null = null;
|
let extensionContext: vscode.ExtensionContext | null = null;
|
||||||
|
|
||||||
|
/** 余额更新回调函数 */
|
||||||
|
let onBalanceUpdateCallback: ((balance: number) => void) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化 Credits 服务(设置 context)
|
* 初始化 Credits 服务(设置 context)
|
||||||
*/
|
*/
|
||||||
@ -39,6 +42,13 @@ export function initCreditsService(context: vscode.ExtensionContext): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置余额更新回调
|
||||||
|
*/
|
||||||
|
export function setBalanceUpdateCallback(callback: (balance: number) => void): void {
|
||||||
|
onBalanceUpdateCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存余额到持久化存储
|
* 保存余额到持久化存储
|
||||||
*/
|
*/
|
||||||
@ -60,6 +70,10 @@ export function updateCachedBalance(balance: number): void {
|
|||||||
saveBalance(balance).catch(err => {
|
saveBalance(balance).catch(err => {
|
||||||
console.error('[CreditsService] 保存余额失败:', err);
|
console.error('[CreditsService] 保存余额失败:', err);
|
||||||
});
|
});
|
||||||
|
// 通知前端更新余额显示
|
||||||
|
if (onBalanceUpdateCallback) {
|
||||||
|
onBalanceUpdateCallback(balance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -43,8 +43,7 @@ export interface MessageSegment {
|
|||||||
toolResult?: string;
|
toolResult?: string;
|
||||||
toolDescription?: string;
|
toolDescription?: string;
|
||||||
askId?: string;
|
askId?: string;
|
||||||
question?: string;
|
questions?: import("../types/api").QuestionItem[];
|
||||||
options?: string[];
|
|
||||||
// 智能体相关字段
|
// 智能体相关字段
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
agentName?: string;
|
agentName?: string;
|
||||||
@ -97,7 +96,7 @@ export interface DialogCallbacks {
|
|||||||
summary: string
|
summary: string
|
||||||
) => void;
|
) => void;
|
||||||
/** 显示问题(ask_user) */
|
/** 显示问题(ask_user) */
|
||||||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
onQuestion?: (askId: string, questions: import("../types/api").QuestionItem[]) => void;
|
||||||
/** 实时更新段落(流式过程中) */
|
/** 实时更新段落(流式过程中) */
|
||||||
onSegmentUpdate?: (segments: MessageSegment[]) => void;
|
onSegmentUpdate?: (segments: MessageSegment[]) => void;
|
||||||
/** 对话完成,返回所有段落 */
|
/** 对话完成,返回所有段落 */
|
||||||
@ -449,7 +448,9 @@ export class DialogSession {
|
|||||||
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
if (selection === "重新登录") {
|
if (selection === "重新登录") {
|
||||||
vscode.commands.executeCommand("iccoder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
throw new Error("登录已过期,请重新登录");
|
throw new Error("登录已过期,请重新登录");
|
||||||
@ -645,8 +646,11 @@ export class DialogSession {
|
|||||||
this.segments.push({
|
this.segments.push({
|
||||||
type: "question",
|
type: "question",
|
||||||
askId: askId,
|
askId: askId,
|
||||||
question: question,
|
questions: [{
|
||||||
options: ["确认执行", "取消"],
|
question: question,
|
||||||
|
options: ["确认执行", "取消"],
|
||||||
|
multiSelect: false
|
||||||
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 实时发送段落更新
|
// 实时发送段落更新
|
||||||
@ -664,8 +668,11 @@ export class DialogSession {
|
|||||||
await userInteractionManager.handleAskUser(
|
await userInteractionManager.handleAskUser(
|
||||||
{
|
{
|
||||||
askId: askId,
|
askId: askId,
|
||||||
question: question,
|
questions: [{
|
||||||
options: ["确认执行", "取消"],
|
question: question,
|
||||||
|
options: ["确认执行", "取消"],
|
||||||
|
multiSelect: false
|
||||||
|
}]
|
||||||
} as AskUserEvent,
|
} as AskUserEvent,
|
||||||
this.taskId
|
this.taskId
|
||||||
);
|
);
|
||||||
@ -712,8 +719,11 @@ export class DialogSession {
|
|||||||
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||||||
const planEvent = {
|
const planEvent = {
|
||||||
askId: askId,
|
askId: askId,
|
||||||
question: `请确认执行计划:${data.title}`,
|
questions: [{
|
||||||
options: ["确认执行", "修改计划", "取消"],
|
question: `请确认执行计划:${data.title}`,
|
||||||
|
options: ["确认执行", "修改计划", "取消"],
|
||||||
|
multiSelect: false
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await userInteractionManager.handleAskUser(
|
await userInteractionManager.handleAskUser(
|
||||||
@ -854,13 +864,12 @@ export class DialogSession {
|
|||||||
this.segments.push({
|
this.segments.push({
|
||||||
type: "question",
|
type: "question",
|
||||||
askId: data.askId,
|
askId: data.askId,
|
||||||
question: data.question,
|
questions: data.questions,
|
||||||
options: data.options,
|
|
||||||
});
|
});
|
||||||
// 实时发送段落更新(包含问题)
|
// 实时发送段落更新(包含问题)
|
||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
// 同时调用 onQuestion 用于更新状态栏等
|
// 同时调用 onQuestion 用于更新状态栏等
|
||||||
callbacks.onQuestion?.(data.askId, data.question, data.options);
|
callbacks.onQuestion?.(data.askId, data.questions);
|
||||||
try {
|
try {
|
||||||
await userInteractionManager.handleAskUser(data, this.taskId);
|
await userInteractionManager.handleAskUser(data, this.taskId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -894,7 +903,9 @@ export class DialogSession {
|
|||||||
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
if (selection === "重新登录") {
|
if (selection === "重新登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 登录过期错误已处理,不再传递给外部
|
// 登录过期错误已处理,不再传递给外部
|
||||||
@ -1106,7 +1117,8 @@ export class DialogSession {
|
|||||||
async submitAnswer(
|
async submitAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string
|
customInput?: string,
|
||||||
|
answers?: { [questionIndex: string]: string[] }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
||||||
// 如果 pendingQuestions 中有问题,走正常流程
|
// 如果 pendingQuestions 中有问题,走正常流程
|
||||||
@ -1115,6 +1127,7 @@ export class DialogSession {
|
|||||||
askId,
|
askId,
|
||||||
selected,
|
selected,
|
||||||
customInput,
|
customInput,
|
||||||
|
answers,
|
||||||
this.taskId
|
this.taskId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,6 +176,28 @@ export class ICCoderAuthenticationProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear local authentication state without window reload.
|
||||||
|
* Used by re-login flow when session is expired.
|
||||||
|
*/
|
||||||
|
async clearSessionsForRelogin(): Promise<void> {
|
||||||
|
if (this._sessions.length === 0) {
|
||||||
|
await clearUserInfo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = [...this._sessions];
|
||||||
|
this._sessions = [];
|
||||||
|
await this.saveSessions();
|
||||||
|
await clearUserInfo();
|
||||||
|
|
||||||
|
this._onDidChangeSessions.fire({
|
||||||
|
added: [],
|
||||||
|
removed,
|
||||||
|
changed: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成会话 ID
|
* 生成会话 ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
// 使用 require 导入 node-notifier
|
// 尝试加载 node-notifier,如果失败则使用 null
|
||||||
const notifier = require('node-notifier');
|
let notifier: any = null;
|
||||||
|
try {
|
||||||
|
notifier = require('node-notifier');
|
||||||
|
console.log('[NotificationService] node-notifier 加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[NotificationService] node-notifier 加载失败,将只使用 VS Code 内置通知');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知类型枚举
|
* 通知类型枚举
|
||||||
@ -114,6 +120,13 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
console.log('[NotificationService] 通过防抖检查');
|
console.log('[NotificationService] 通过防抖检查');
|
||||||
|
|
||||||
|
// 如果 node-notifier 不可用,直接使用 VS Code 内置通知
|
||||||
|
if (!notifier) {
|
||||||
|
console.log('[NotificationService] node-notifier 不可用,使用 VS Code 内置通知');
|
||||||
|
this.showVSCodeNotification(title, message, type, onClick);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 node-notifier 发送系统通知
|
// 使用 node-notifier 发送系统通知
|
||||||
console.log('[NotificationService] 使用 node-notifier 发送系统通知');
|
console.log('[NotificationService] 使用 node-notifier 发送系统通知');
|
||||||
|
|
||||||
|
|||||||
@ -2,21 +2,31 @@
|
|||||||
* 工具执行器
|
* 工具执行器
|
||||||
* 接收后端的 tool_call 事件,执行本地工具,返回结果
|
* 接收后端的 tool_call 事件,执行本地工具,返回结果
|
||||||
*/
|
*/
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from "vscode";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
import * as os from 'os';
|
import * as os from "os";
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
import { readFileContent, readDirectory } from "../utils/readFiles";
|
||||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
import { createOrOverwriteFile } from "../utils/createFiles";
|
||||||
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
|
||||||
import { analyzeVcdFile } from '../utils/vcdParser';
|
import { changeTracker } from "./changeTracker";
|
||||||
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
import {
|
||||||
|
generateVCD,
|
||||||
|
checkIverilogAvailable,
|
||||||
|
generateMultiVCD,
|
||||||
|
DumpModule,
|
||||||
|
} from "../utils/iverilogRunner";
|
||||||
|
import { analyzeVcdFile } from "../utils/vcdParser";
|
||||||
|
import {
|
||||||
|
executeWaveformTrace,
|
||||||
|
WaveformTraceArgs,
|
||||||
|
} from "../utils/waveformTracer";
|
||||||
import {
|
import {
|
||||||
submitToolResult,
|
submitToolResult,
|
||||||
createSuccessResult,
|
createSuccessResult,
|
||||||
createBusinessErrorResult,
|
createBusinessErrorResult,
|
||||||
createSystemErrorResult
|
createSystemErrorResult,
|
||||||
} from './apiClient';
|
} from "./apiClient";
|
||||||
import type {
|
import type {
|
||||||
ToolCallRequest,
|
ToolCallRequest,
|
||||||
ToolName,
|
ToolName,
|
||||||
@ -29,8 +39,8 @@ import type {
|
|||||||
SimulationArgs,
|
SimulationArgs,
|
||||||
WaveformSummaryArgs,
|
WaveformSummaryArgs,
|
||||||
KnowledgeSaveArgs,
|
KnowledgeSaveArgs,
|
||||||
KnowledgeLoadArgs
|
KnowledgeLoadArgs,
|
||||||
} from '../types/api';
|
} from "../types/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具执行器上下文
|
* 工具执行器上下文
|
||||||
@ -49,7 +59,7 @@ export interface ToolExecutorContext {
|
|||||||
*/
|
*/
|
||||||
export async function executeToolCall(
|
export async function executeToolCall(
|
||||||
request: ToolCallRequest,
|
request: ToolCallRequest,
|
||||||
context: ToolExecutorContext
|
context: ToolExecutorContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const toolName = request.params.name as ToolName;
|
const toolName = request.params.name as ToolName;
|
||||||
const args = request.params.arguments;
|
const args = request.params.arguments;
|
||||||
@ -61,37 +71,53 @@ export async function executeToolCall(
|
|||||||
let resultText: string;
|
let resultText: string;
|
||||||
|
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case 'file_read':
|
case "file_read":
|
||||||
resultText = await executeFileRead(args as unknown as FileReadArgs);
|
resultText = await executeFileRead(args as unknown as FileReadArgs);
|
||||||
break;
|
break;
|
||||||
case 'file_write':
|
case "file_write":
|
||||||
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
||||||
break;
|
break;
|
||||||
case 'file_delete':
|
case "file_delete":
|
||||||
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
|
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
|
||||||
break;
|
break;
|
||||||
case 'file_list':
|
case "file_list":
|
||||||
resultText = await executeFileList(args as unknown as FileListArgs);
|
resultText = await executeFileList(args as unknown as FileListArgs);
|
||||||
break;
|
break;
|
||||||
case 'syntax_check':
|
case "syntax_check":
|
||||||
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
|
resultText = await executeSyntaxCheck(
|
||||||
|
args as unknown as SyntaxCheckArgs,
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'iverilog':
|
case "iverilog":
|
||||||
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
|
resultText = await executeIverilog(
|
||||||
|
args as unknown as IverilogArgs,
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'simulation':
|
case "simulation":
|
||||||
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
resultText = await executeSimulation(
|
||||||
|
args as unknown as SimulationArgs,
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'waveform_summary':
|
case "waveform_summary":
|
||||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
resultText = await executeWaveformSummary(
|
||||||
|
args as unknown as WaveformSummaryArgs,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'waveform_trace':
|
case "waveform_trace":
|
||||||
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
|
resultText = await executeWaveformTrace(
|
||||||
|
args as unknown as WaveformTraceArgs,
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'knowledge_save':
|
case "knowledge_save":
|
||||||
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
resultText = await executeKnowledgeSave(
|
||||||
|
args as unknown as KnowledgeSaveArgs,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'knowledge_load':
|
case "knowledge_load":
|
||||||
resultText = await executeKnowledgeLoad();
|
resultText = await executeKnowledgeLoad();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -102,10 +128,12 @@ export async function executeToolCall(
|
|||||||
const result = createSuccessResult(callId, resultText);
|
const result = createSuccessResult(callId, resultText);
|
||||||
await submitToolResult(result);
|
await submitToolResult(result);
|
||||||
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
|
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||||
console.error(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error);
|
console.error(
|
||||||
|
`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
// 提交错误结果
|
// 提交错误结果
|
||||||
const result = createBusinessErrorResult(callId, errorMessage);
|
const result = createBusinessErrorResult(callId, errorMessage);
|
||||||
@ -125,10 +153,21 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
|||||||
* 执行 file_write 工具
|
* 执行 file_write 工具
|
||||||
*/
|
*/
|
||||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||||
|
const absolutePath = resolveWorkspaceFilePath(args.path);
|
||||||
|
const existedBeforeWrite = fs.existsSync(absolutePath);
|
||||||
|
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : "";
|
||||||
|
|
||||||
await createOrOverwriteFile(args.path, args.content);
|
await createOrOverwriteFile(args.path, args.content);
|
||||||
|
|
||||||
|
// 记录文件变更
|
||||||
|
try {
|
||||||
|
changeTracker.trackChange(args.path, oldContent, args.content);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ToolExecutor] 记录文件变更失败:", error);
|
||||||
|
}
|
||||||
|
|
||||||
// Verilog 文件添加知识图谱提示
|
// Verilog 文件添加知识图谱提示
|
||||||
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
const isVerilogFile = args.path.endsWith(".v") || args.path.endsWith(".sv");
|
||||||
if (isVerilogFile) {
|
if (isVerilogFile) {
|
||||||
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
|
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
|
||||||
}
|
}
|
||||||
@ -138,7 +177,7 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 file_delete 工具
|
* 执行 file_delete 工具
|
||||||
* 删除指定路径的文件
|
* 删除指定路径的文件(带用户确认)
|
||||||
*/
|
*/
|
||||||
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||||
const filePath = args.path;
|
const filePath = args.path;
|
||||||
@ -146,7 +185,7 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
|||||||
// 获取工作区路径
|
// 获取工作区路径
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
@ -167,11 +206,60 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
|||||||
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除文件
|
// 验证文件路径在工作区内
|
||||||
fs.unlinkSync(absolutePath);
|
const isInWorkspace = workspaceFolders.some((folder) =>
|
||||||
|
absolutePath.startsWith(folder.uri.fsPath),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInWorkspace) {
|
||||||
|
throw new Error("只能删除工作区内的文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保护敏感文件
|
||||||
|
const protectedFiles = [
|
||||||
|
"package.json",
|
||||||
|
"tsconfig.json",
|
||||||
|
".git",
|
||||||
|
"node_modules",
|
||||||
|
];
|
||||||
|
|
||||||
|
const fileName = path.basename(absolutePath);
|
||||||
|
if (protectedFiles.includes(fileName)) {
|
||||||
|
throw new Error(`不允许删除系统文件: ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹出确认对话框
|
||||||
|
const confirmed = await vscode.window.showWarningMessage(
|
||||||
|
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
|
||||||
|
{
|
||||||
|
modal: true, // 模态对话框,阻止其他操作
|
||||||
|
detail: "⚠️ 文件将被移到回收站,可以恢复",
|
||||||
|
},
|
||||||
|
"确定删除",
|
||||||
|
"取消",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 用户取消或关闭对话框
|
||||||
|
if (confirmed !== "确定删除") {
|
||||||
|
throw new Error("用户取消了删除操作");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容用于变更追踪
|
||||||
|
const oldContent = fs.readFileSync(absolutePath, "utf-8");
|
||||||
|
|
||||||
|
// 记录删除变更
|
||||||
|
const relativePath = path.relative(workspacePath, absolutePath);
|
||||||
|
changeTracker.trackChange(relativePath, oldContent, "");
|
||||||
|
|
||||||
|
// 删除文件(移到回收站)
|
||||||
|
const uri = vscode.Uri.file(absolutePath);
|
||||||
|
await vscode.workspace.fs.delete(uri, {
|
||||||
|
recursive: false, // 不是目录,设为 false
|
||||||
|
useTrash: true, // 移到回收站而非永久删除
|
||||||
|
});
|
||||||
|
|
||||||
// Verilog 文件添加知识图谱提示
|
// Verilog 文件添加知识图谱提示
|
||||||
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
|
const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv");
|
||||||
if (isVerilogFile) {
|
if (isVerilogFile) {
|
||||||
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
||||||
}
|
}
|
||||||
@ -183,13 +271,13 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
|||||||
* 执行 file_list 工具
|
* 执行 file_list 工具
|
||||||
*/
|
*/
|
||||||
async function executeFileList(args: FileListArgs): Promise<string> {
|
async function executeFileList(args: FileListArgs): Promise<string> {
|
||||||
const dirPath = args.path || '.';
|
const dirPath = args.path || ".";
|
||||||
const extensions = args.extension ? [args.extension] : undefined;
|
const extensions = args.extension ? [args.extension] : undefined;
|
||||||
|
|
||||||
const files = await readDirectory(dirPath, extensions);
|
const files = await readDirectory(dirPath, extensions);
|
||||||
const fileList = files.map(f => f.path).join('\n');
|
const fileList = files.map((f) => f.path).join("\n");
|
||||||
|
|
||||||
return fileList || '(目录为空)';
|
return fileList || "(目录为空)";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -198,12 +286,12 @@ async function executeFileList(args: FileListArgs): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
async function executeSyntaxCheck(
|
async function executeSyntaxCheck(
|
||||||
args: SyntaxCheckArgs,
|
args: SyntaxCheckArgs,
|
||||||
context: ToolExecutorContext
|
context: ToolExecutorContext,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// 检查 iverilog 是否可用
|
// 检查 iverilog 是否可用
|
||||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||||
if (!iverilogCheck.available) {
|
if (!iverilogCheck.available) {
|
||||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建临时文件
|
// 创建临时文件
|
||||||
@ -212,33 +300,33 @@ async function executeSyntaxCheck(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 写入代码到临时文件
|
// 写入代码到临时文件
|
||||||
fs.writeFileSync(tempFile, args.code, 'utf-8');
|
fs.writeFileSync(tempFile, args.code, "utf-8");
|
||||||
|
|
||||||
// 调用 iverilog 进行语法检查
|
// 调用 iverilog 进行语法检查
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require("child_process");
|
||||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(iverilogPath, ['-t', 'null', tempFile], {
|
const child = spawn(iverilogPath, ["-t", "null", tempFile], {
|
||||||
cwd: tempDir,
|
cwd: tempDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = "";
|
||||||
let stderr = '';
|
let stderr = "";
|
||||||
|
|
||||||
child.stdout.on('data', (data: Buffer) => {
|
child.stdout.on("data", (data: Buffer) => {
|
||||||
stdout += data.toString();
|
stdout += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on('data', (data: Buffer) => {
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code: number) => {
|
child.on("close", (code: number) => {
|
||||||
// 清理临时文件
|
// 清理临时文件
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(tempFile);
|
fs.unlinkSync(tempFile);
|
||||||
@ -247,13 +335,13 @@ async function executeSyntaxCheck(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve('语法检查通过,无错误。');
|
resolve("语法检查通过,无错误。");
|
||||||
} else {
|
} else {
|
||||||
resolve(`语法检查发现错误:\n${stderr || stdout}`);
|
resolve(`语法检查发现错误:\n${stderr || stdout}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (error: Error) => {
|
child.on("error", (error: Error) => {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(tempFile);
|
fs.unlinkSync(tempFile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -262,7 +350,6 @@ async function executeSyntaxCheck(
|
|||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 确保清理临时文件
|
// 确保清理临时文件
|
||||||
try {
|
try {
|
||||||
@ -280,18 +367,18 @@ async function executeSyntaxCheck(
|
|||||||
*/
|
*/
|
||||||
async function executeIverilog(
|
async function executeIverilog(
|
||||||
args: IverilogArgs,
|
args: IverilogArgs,
|
||||||
context: ToolExecutorContext
|
context: ToolExecutorContext,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// 检查 iverilog 是否可用
|
// 检查 iverilog 是否可用
|
||||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||||
if (!iverilogCheck.available) {
|
if (!iverilogCheck.available) {
|
||||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取工作目录
|
// 获取工作目录
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
throw new Error('没有打开的工作区');
|
throw new Error("没有打开的工作区");
|
||||||
}
|
}
|
||||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||||
const workDir = args.workDir
|
const workDir = args.workDir
|
||||||
@ -300,32 +387,32 @@ async function executeIverilog(
|
|||||||
|
|
||||||
// 解析参数
|
// 解析参数
|
||||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||||
const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0);
|
const cmdArgs = args.args.split(/\s+/).filter((a) => a.length > 0);
|
||||||
|
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require("child_process");
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(iverilogPath, cmdArgs, {
|
const child = spawn(iverilogPath, cmdArgs, {
|
||||||
cwd: workDir,
|
cwd: workDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = "";
|
||||||
let stderr = '';
|
let stderr = "";
|
||||||
|
|
||||||
child.stdout.on('data', (data: Buffer) => {
|
child.stdout.on("data", (data: Buffer) => {
|
||||||
stdout += data.toString();
|
stdout += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on('data', (data: Buffer) => {
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code: number) => {
|
child.on("close", (code: number) => {
|
||||||
const output = stderr || stdout || '(无输出)';
|
const output = stderr || stdout || "(无输出)";
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve(`执行成功\n${output}`);
|
resolve(`执行成功\n${output}`);
|
||||||
} else {
|
} else {
|
||||||
@ -333,7 +420,7 @@ async function executeIverilog(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (error: Error) => {
|
child.on("error", (error: Error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -344,12 +431,12 @@ async function executeIverilog(
|
|||||||
*/
|
*/
|
||||||
async function executeSimulation(
|
async function executeSimulation(
|
||||||
args: SimulationArgs,
|
args: SimulationArgs,
|
||||||
context: ToolExecutorContext
|
context: ToolExecutorContext,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// 获取工作区路径
|
// 获取工作区路径
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||||
@ -357,21 +444,24 @@ async function executeSimulation(
|
|||||||
// 检查是否有 dumpModules 参数(多 VCD 模式)
|
// 检查是否有 dumpModules 参数(多 VCD 模式)
|
||||||
if (args.dumpModules) {
|
if (args.dumpModules) {
|
||||||
const modules = parseDumpModules(args.dumpModules);
|
const modules = parseDumpModules(args.dumpModules);
|
||||||
const vcdDir = args.vcdDir || 'vcd';
|
const vcdDir = args.vcdDir || "vcd";
|
||||||
|
|
||||||
const result = await generateMultiVCD(
|
const result = await generateMultiVCD(
|
||||||
projectPath,
|
projectPath,
|
||||||
context.extensionPath,
|
context.extensionPath,
|
||||||
args.tbPath,
|
args.tbPath,
|
||||||
modules,
|
modules,
|
||||||
vcdDir
|
vcdDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const vcdList = result.vcdFiles
|
const vcdList = result.vcdFiles
|
||||||
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`)
|
.map(
|
||||||
.join('\n');
|
(f) =>
|
||||||
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`;
|
`- ${f.moduleName}: ${f.success ? f.vcdPath : "失败 - " + f.error}`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? "\n\n仿真输出:" + result.stdout : ""}`;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message);
|
throw new Error(result.message);
|
||||||
}
|
}
|
||||||
@ -400,8 +490,8 @@ async function executeSimulation(
|
|||||||
* 格式:name:path,name:path
|
* 格式:name:path,name:path
|
||||||
*/
|
*/
|
||||||
function parseDumpModules(dumpModules: string): DumpModule[] {
|
function parseDumpModules(dumpModules: string): DumpModule[] {
|
||||||
return dumpModules.split(',').map(item => {
|
return dumpModules.split(",").map((item) => {
|
||||||
const [name, modulePath] = item.trim().split(':');
|
const [name, modulePath] = item.trim().split(":");
|
||||||
return { name: name.trim(), path: modulePath.trim() };
|
return { name: name.trim(), path: modulePath.trim() };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -410,13 +500,15 @@ function parseDumpModules(dumpModules: string): DumpModule[] {
|
|||||||
* 执行 waveform_summary 工具
|
* 执行 waveform_summary 工具
|
||||||
* 解析 VCD 文件并返回波形摘要
|
* 解析 VCD 文件并返回波形摘要
|
||||||
*/
|
*/
|
||||||
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
|
async function executeWaveformSummary(
|
||||||
|
args: WaveformSummaryArgs,
|
||||||
|
): Promise<string> {
|
||||||
const { vcdPath, signals, checkpoints } = args;
|
const { vcdPath, signals, checkpoints } = args;
|
||||||
|
|
||||||
// 获取工作区路径
|
// 获取工作区路径
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
@ -447,17 +539,20 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
|
|||||||
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||||
const workspaceFolder = getWorkspaceFolder();
|
const workspaceFolder = getWorkspaceFolder();
|
||||||
if (!workspaceFolder) {
|
if (!workspaceFolder) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
|
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, ".iccoder");
|
||||||
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
|
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, "knowledge.json");
|
||||||
|
|
||||||
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
|
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
|
||||||
await vscode.workspace.fs.createDirectory(iccoderDirUri);
|
await vscode.workspace.fs.createDirectory(iccoderDirUri);
|
||||||
|
|
||||||
// 写入知识图谱(UTF-8)
|
// 写入知识图谱(UTF-8)
|
||||||
await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8'));
|
await vscode.workspace.fs.writeFile(
|
||||||
|
knowledgeUri,
|
||||||
|
Buffer.from(args.data || "", "utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
return `知识图谱已保存: .iccoder/knowledge.json`;
|
return `知识图谱已保存: .iccoder/knowledge.json`;
|
||||||
}
|
}
|
||||||
@ -469,20 +564,33 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
|||||||
async function executeKnowledgeLoad(): Promise<string> {
|
async function executeKnowledgeLoad(): Promise<string> {
|
||||||
const workspaceFolder = getWorkspaceFolder();
|
const workspaceFolder = getWorkspaceFolder();
|
||||||
if (!workspaceFolder) {
|
if (!workspaceFolder) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json');
|
const knowledgeUri = vscode.Uri.joinPath(
|
||||||
|
workspaceFolder.uri,
|
||||||
|
".iccoder",
|
||||||
|
"knowledge.json",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
|
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
|
||||||
const content = Buffer.from(bytes).toString('utf-8');
|
const content = Buffer.from(bytes).toString("utf-8");
|
||||||
return content;
|
return content;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 文件不存在:返回空图谱
|
// 文件不存在:返回空图谱
|
||||||
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
if (
|
||||||
|
error instanceof vscode.FileSystemError &&
|
||||||
|
error.code === "FileNotFound"
|
||||||
|
) {
|
||||||
// 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段)
|
// 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段)
|
||||||
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] });
|
return JSON.stringify({
|
||||||
|
taskId: "",
|
||||||
|
version: 1,
|
||||||
|
module: null,
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -495,7 +603,9 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||||
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
const activeFolder = activeUri
|
||||||
|
? vscode.workspace.getWorkspaceFolder(activeUri)
|
||||||
|
: undefined;
|
||||||
return activeFolder ?? folders[0];
|
return activeFolder ?? folders[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -504,22 +614,24 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
|||||||
*/
|
*/
|
||||||
function getIverilogPath(extensionPath: string): string {
|
function getIverilogPath(extensionPath: string): string {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
if (platform === 'win32') {
|
if (platform === "win32") {
|
||||||
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe');
|
return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
|
||||||
} else {
|
} else {
|
||||||
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog');
|
return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建工具执行器上下文
|
* 创建工具执行器上下文
|
||||||
*/
|
*/
|
||||||
export function createToolExecutorContext(extensionPath: string): ToolExecutorContext {
|
export function createToolExecutorContext(
|
||||||
|
extensionPath: string,
|
||||||
|
): ToolExecutorContext {
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || '';
|
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
extensionPath,
|
extensionPath,
|
||||||
workspacePath
|
workspacePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
62
src/services/trialExpirationService.ts
Normal file
62
src/services/trialExpirationService.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 试用期过期检测服务
|
||||||
|
* 功能:检查插件试用用户是否过期
|
||||||
|
* 依赖:vscode, userService
|
||||||
|
* 使用场景:用户使用功能前检查是否过期
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { getCachedUserInfo } from './userService';
|
||||||
|
|
||||||
|
export class TrialExpirationService {
|
||||||
|
private context: vscode.ExtensionContext;
|
||||||
|
private panel?: vscode.WebviewPanel;
|
||||||
|
|
||||||
|
constructor(context: vscode.ExtensionContext, panel?: vscode.WebviewPanel) {
|
||||||
|
this.context = context;
|
||||||
|
this.panel = panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否过期
|
||||||
|
* @returns true=已过期,false=未过期
|
||||||
|
*/
|
||||||
|
public async checkExpiration(): Promise<boolean> {
|
||||||
|
const userInfo = getCachedUserInfo();
|
||||||
|
|
||||||
|
// 不是插件试用用户,不需要检查
|
||||||
|
if (!userInfo?.isPluginTrial) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有过期时间,不检查
|
||||||
|
if (!userInfo.pluginTrialExpiresAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
const now = Date.now();
|
||||||
|
if (now >= userInfo.pluginTrialExpiresAt) {
|
||||||
|
// 已过期
|
||||||
|
await this.handleExpired();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理过期逻辑
|
||||||
|
*/
|
||||||
|
private async handleExpired(): Promise<void> {
|
||||||
|
// 通知前端显示过期弹窗
|
||||||
|
if (this.panel) {
|
||||||
|
this.panel.webview.postMessage({
|
||||||
|
command: 'showExpiredModal'
|
||||||
|
});
|
||||||
|
console.log('[TrialExpirationService] 已通知前端显示过期弹窗');
|
||||||
|
} else {
|
||||||
|
console.warn('[TrialExpirationService] panel 未提供,无法显示过期弹窗');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { submitAnswer, submitToolConfirm } from './apiClient';
|
import { submitAnswer, submitToolConfirm } from './apiClient';
|
||||||
import type { AskUserEvent, AnswerRequest } from '../types/api';
|
import type { AskUserEvent, AnswerRequest, QuestionItem } from '../types/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 待处理的用户问题
|
* 待处理的用户问题
|
||||||
@ -12,8 +12,7 @@ import type { AskUserEvent, AnswerRequest } from '../types/api';
|
|||||||
interface PendingQuestion {
|
interface PendingQuestion {
|
||||||
askId: string;
|
askId: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
question: string;
|
questions: QuestionItem[];
|
||||||
options: string[];
|
|
||||||
resolve: (answer: string) => void;
|
resolve: (answer: string) => void;
|
||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
}
|
}
|
||||||
@ -45,9 +44,9 @@ export class UserInteractionManager {
|
|||||||
* @param taskId 当前任务ID
|
* @param taskId 当前任务ID
|
||||||
*/
|
*/
|
||||||
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
|
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
|
||||||
const { askId, question, options } = event;
|
const { askId, questions } = event;
|
||||||
|
|
||||||
console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`);
|
console.log(`[UserInteraction] 收到问题: askId=${askId}, count=${questions.length}`);
|
||||||
|
|
||||||
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
|
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
|
||||||
// 这里不再单独发送 showQuestion 命令,避免重复显示
|
// 这里不再单独发送 showQuestion 命令,避免重复显示
|
||||||
@ -57,8 +56,7 @@ export class UserInteractionManager {
|
|||||||
this.pendingQuestions.set(askId, {
|
this.pendingQuestions.set(askId, {
|
||||||
askId,
|
askId,
|
||||||
taskId,
|
taskId,
|
||||||
question,
|
questions,
|
||||||
options,
|
|
||||||
resolve: (answer: string) => {
|
resolve: (answer: string) => {
|
||||||
this.submitUserAnswer(askId, taskId, answer)
|
this.submitUserAnswer(askId, taskId, answer)
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
@ -80,24 +78,38 @@ export class UserInteractionManager {
|
|||||||
/**
|
/**
|
||||||
* 处理用户提交的回答(从 WebView 调用)
|
* 处理用户提交的回答(从 WebView 调用)
|
||||||
* @param askId 问题ID
|
* @param askId 问题ID
|
||||||
* @param selected 选中的选项
|
* @param selected 选中的选项(旧格式)
|
||||||
* @param customInput 自定义输入
|
* @param customInput 自定义输入(旧格式)
|
||||||
|
* @param answers 新格式:按问题索引的答案
|
||||||
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
||||||
*/
|
*/
|
||||||
async receiveAnswer(
|
async receiveAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string,
|
customInput?: string,
|
||||||
|
answers?: { [questionIndex: string]: string[] },
|
||||||
fallbackTaskId?: string
|
fallbackTaskId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const pending = this.pendingQuestions.get(askId);
|
const pending = this.pendingQuestions.get(askId);
|
||||||
const answer = customInput || selected?.join(', ') || '';
|
|
||||||
|
// 构建答案字符串
|
||||||
|
let answer = '';
|
||||||
|
if (answers && Object.keys(answers).length > 0) {
|
||||||
|
// 新格式:多问题答案
|
||||||
|
answer = Object.entries(answers)
|
||||||
|
.sort(([a], [b]) => parseInt(a) - parseInt(b))
|
||||||
|
.map(([_, vals]) => vals.join('; '))
|
||||||
|
.join(' | ');
|
||||||
|
} else {
|
||||||
|
// 旧格式:单问题答案
|
||||||
|
answer = customInput || selected?.join(', ') || '';
|
||||||
|
}
|
||||||
|
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
||||||
if (fallbackTaskId) {
|
if (fallbackTaskId) {
|
||||||
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
||||||
await this.submitUserAnswer(askId, fallbackTaskId, answer);
|
await this.submitUserAnswer(askId, fallbackTaskId, answer, answers);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||||||
}
|
}
|
||||||
@ -119,7 +131,8 @@ export class UserInteractionManager {
|
|||||||
private async submitUserAnswer(
|
private async submitUserAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
taskId: string,
|
taskId: string,
|
||||||
answer: string
|
answer: string,
|
||||||
|
answers?: { [questionIndex: string]: string[] }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 检查是否是工具确认类型的问题
|
// 检查是否是工具确认类型的问题
|
||||||
if (askId.startsWith('tool_confirm_')) {
|
if (askId.startsWith('tool_confirm_')) {
|
||||||
@ -148,7 +161,8 @@ export class UserInteractionManager {
|
|||||||
const request: AnswerRequest = {
|
const request: AnswerRequest = {
|
||||||
askId,
|
askId,
|
||||||
taskId,
|
taskId,
|
||||||
customInput: answer
|
answers: answers,
|
||||||
|
customInput: answers ? undefined : answer
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -117,6 +117,10 @@ export interface UserInfo {
|
|||||||
};
|
};
|
||||||
// Credits 余额
|
// Credits 余额
|
||||||
credits?: number;
|
credits?: number;
|
||||||
|
// 插件试用用户标识(从 JWT token 中提取)
|
||||||
|
isPluginTrial?: boolean;
|
||||||
|
// 试用到期时间(毫秒时间戳)
|
||||||
|
pluginTrialExpiresAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,7 +143,7 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
|||||||
// 处理响应数据 - 检查 code 是否为 200
|
// 处理响应数据 - 检查 code 是否为 200
|
||||||
if (response.code === 200 && response.user) {
|
if (response.code === 200 && response.user) {
|
||||||
const user = response.user;
|
const user = response.user;
|
||||||
return {
|
const userInfo: UserInfo = {
|
||||||
userId: String(user.userId),
|
userId: String(user.userId),
|
||||||
username: user.userName,
|
username: user.userName,
|
||||||
nickname: user.nickName,
|
nickname: user.nickName,
|
||||||
@ -151,6 +155,24 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
|||||||
createTime: user.createTime,
|
createTime: user.createTime,
|
||||||
loginDate: user.loginDate
|
loginDate: user.loginDate
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 从接口响应中获取企业试用标识
|
||||||
|
if (response.isPluginTrial === true) {
|
||||||
|
userInfo.isPluginTrial = true;
|
||||||
|
console.log('[UserService] 从 getInfo 接口获取到 isPluginTrial: true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取试用到期时间(null 表示长期有效)
|
||||||
|
if (response.enterpriseTrialExpires !== undefined) {
|
||||||
|
userInfo.pluginTrialExpiresAt = response.enterpriseTrialExpires;
|
||||||
|
if (response.enterpriseTrialExpires === null) {
|
||||||
|
console.log('[UserService] 试用长期有效');
|
||||||
|
} else {
|
||||||
|
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('[UserService] 获取用户信息失败:', response);
|
console.error('[UserService] 获取用户信息失败:', response);
|
||||||
@ -313,6 +335,44 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
|||||||
// 保存到持久化存储
|
// 保存到持久化存储
|
||||||
await saveUserInfo(userInfo);
|
await saveUserInfo(userInfo);
|
||||||
|
|
||||||
|
// 判断是否是插件试用用户
|
||||||
|
console.log('[UserService] 检查用户类型,isPluginTrial:', userInfo.isPluginTrial);
|
||||||
|
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
|
||||||
|
|
||||||
|
if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
|
||||||
|
// 检查是否过期
|
||||||
|
const now = Date.now();
|
||||||
|
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||||
|
console.log('[UserService] 试用到期时间:', new Date(userInfo.pluginTrialExpiresAt).toLocaleString());
|
||||||
|
console.log('[UserService] 当前时间:', new Date(now).toLocaleString());
|
||||||
|
console.log('[UserService] 是否过期:', isExpired);
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
// 已过期:显示邀请码弹窗
|
||||||
|
console.log('[UserService] 试用已过期,将显示邀请码弹窗');
|
||||||
|
} else {
|
||||||
|
// 未过期:显示欢迎弹窗
|
||||||
|
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
|
||||||
|
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
|
||||||
|
|
||||||
|
if (!hasWelcomed && extensionContext) {
|
||||||
|
await extensionContext.globalState.update('showWelcomeModal', true);
|
||||||
|
await extensionContext.globalState.update('pluginTrialWelcomed', true);
|
||||||
|
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
|
||||||
|
|
||||||
|
const checkMark = extensionContext.globalState.get('showWelcomeModal');
|
||||||
|
console.log('[UserService] 验证标记:', checkMark);
|
||||||
|
} else if (!extensionContext) {
|
||||||
|
console.error('[UserService] ❌ extensionContext 为 null,无法设置标记');
|
||||||
|
} else {
|
||||||
|
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// isPluginTrial=false 或 enterpriseTrialExpires 为 null:显示邀请码弹窗
|
||||||
|
console.log('[UserService] 非试用用户或无过期时间,将显示邀请码弹窗');
|
||||||
|
}
|
||||||
|
|
||||||
return userInfo;
|
return userInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[UserService] 获取用户信息失败:', error);
|
console.error('[UserService] 获取用户信息失败:', error);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import * as vscode from "vscode";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* VCD 文件 HTTP 服务器
|
* VCD 文件 HTTP 服务器
|
||||||
* 用于为 Surfer 波形查看器提供 VCD 文件访问
|
* 用于为 波形查看器提供 VCD 文件访问
|
||||||
*/
|
*/
|
||||||
export class VCDFileServer {
|
export class VCDFileServer {
|
||||||
private server: http.Server | null = null;
|
private server: http.Server | null = null;
|
||||||
@ -98,7 +98,10 @@ export class VCDFileServer {
|
|||||||
/**
|
/**
|
||||||
* 处理 HTTP 请求
|
* 处理 HTTP 请求
|
||||||
*/
|
*/
|
||||||
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
private handleRequest(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
): void {
|
||||||
const url = req.url || "";
|
const url = req.url || "";
|
||||||
console.log(`[VCDFileServer] 收到请求: ${url}`);
|
console.log(`[VCDFileServer] 收到请求: ${url}`);
|
||||||
|
|
||||||
@ -214,7 +217,12 @@ export class VCDFileServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileName = match[1];
|
const fileName = match[1];
|
||||||
const filePath = path.join(this.extensionUri.fsPath, "media", "surfer", fileName);
|
const filePath = path.join(
|
||||||
|
this.extensionUri.fsPath,
|
||||||
|
"media",
|
||||||
|
"surfer",
|
||||||
|
fileName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||||||
@ -257,8 +265,8 @@ export class VCDFileServer {
|
|||||||
*/
|
*/
|
||||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||||
try {
|
try {
|
||||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
|
||||||
const lines = buffer.split('\n');
|
const lines = buffer.split("\n");
|
||||||
|
|
||||||
const scopeNames: string[] = [];
|
const scopeNames: string[] = [];
|
||||||
let scopeDepth = 0;
|
let scopeDepth = 0;
|
||||||
@ -267,7 +275,7 @@ export class VCDFileServer {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
if (trimmed.startsWith('$enddefinitions')) {
|
if (trimmed.startsWith("$enddefinitions")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,17 +284,17 @@ export class VCDFileServer {
|
|||||||
const scopeType = scopeMatch[1];
|
const scopeType = scopeMatch[1];
|
||||||
const scopeName = scopeMatch[2];
|
const scopeName = scopeMatch[2];
|
||||||
|
|
||||||
if (scopeDepth === 0 && scopeType === 'module') {
|
if (scopeDepth === 0 && scopeType === "module") {
|
||||||
scopeStack.push(scopeName);
|
scopeStack.push(scopeName);
|
||||||
} else if (scopeDepth === 1 && scopeType === 'module') {
|
} else if (scopeDepth === 1 && scopeType === "module") {
|
||||||
const fullPath = [...scopeStack, scopeName];
|
const fullPath = [...scopeStack, scopeName];
|
||||||
scopeNames.push(fullPath.join('.'));
|
scopeNames.push(fullPath.join("."));
|
||||||
}
|
}
|
||||||
|
|
||||||
scopeDepth++;
|
scopeDepth++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimmed.startsWith('$upscope')) {
|
if (trimmed.startsWith("$upscope")) {
|
||||||
scopeDepth--;
|
scopeDepth--;
|
||||||
if (scopeDepth === 0) {
|
if (scopeDepth === 0) {
|
||||||
scopeStack.pop();
|
scopeStack.pop();
|
||||||
@ -323,7 +331,7 @@ export class VCDFileServer {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>Surfer 波形查看器 - ${fileName}</title>
|
<title>波形查看器 - ${fileName}</title>
|
||||||
<script>
|
<script>
|
||||||
window.surferReady = false;
|
window.surferReady = false;
|
||||||
window.pendingVcdData = null;
|
window.pendingVcdData = null;
|
||||||
|
|||||||
@ -194,11 +194,17 @@ export interface PlanSummaryUpdateEvent {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单个问题项 */
|
||||||
|
export interface QuestionItem {
|
||||||
|
question: string;
|
||||||
|
options: string[];
|
||||||
|
multiSelect?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** ask_user 事件数据 */
|
/** ask_user 事件数据 */
|
||||||
export interface AskUserEvent {
|
export interface AskUserEvent {
|
||||||
askId: string;
|
askId: string;
|
||||||
question: string;
|
questions: QuestionItem[];
|
||||||
options: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** complete 事件数据 */
|
/** complete 事件数据 */
|
||||||
@ -351,10 +357,12 @@ export interface AnswerRequest {
|
|||||||
askId: string;
|
askId: string;
|
||||||
/** 任务ID */
|
/** 任务ID */
|
||||||
taskId: string;
|
taskId: string;
|
||||||
/** 选中的选项列表 */
|
/** 选中的选项列表(旧格式,兼容) */
|
||||||
selected?: string[];
|
selected?: string[];
|
||||||
/** 自定义输入内容 */
|
/** 自定义输入内容(旧格式,兼容) */
|
||||||
customInput?: string;
|
customInput?: string;
|
||||||
|
/** 新格式:按问题索引的答案 */
|
||||||
|
answers?: { [questionIndex: string]: string[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 用户回答响应 */
|
/** 用户回答响应 */
|
||||||
@ -407,6 +415,10 @@ export interface UserInfoResponse {
|
|||||||
isDefaultModifyPwd: boolean;
|
isDefaultModifyPwd: boolean;
|
||||||
/** 密码是否过期 */
|
/** 密码是否过期 */
|
||||||
isPasswordExpired: boolean;
|
isPasswordExpired: boolean;
|
||||||
|
/** 是否为插件试用用户 */
|
||||||
|
isPluginTrial?: boolean;
|
||||||
|
/** 企业试用到期时间(毫秒时间戳) */
|
||||||
|
enterpriseTrialExpires?: number;
|
||||||
/** 用户信息 */
|
/** 用户信息 */
|
||||||
user: {
|
user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -419,6 +431,7 @@ export interface UserInfoResponse {
|
|||||||
status?: string;
|
status?: string;
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
loginDate?: string;
|
loginDate?: string;
|
||||||
|
remark?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/types/fileChanges.ts
Normal file
47
src/types/fileChanges.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 文件变更追踪类型定义
|
||||||
|
* 功能:定义代码变更的数据结构
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:AI 修改文件后的变更审查
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个文件的变更记录
|
||||||
|
*/
|
||||||
|
export interface FileChange {
|
||||||
|
/** 文件相对路径 */
|
||||||
|
filePath: string;
|
||||||
|
/** 修改前的内容 */
|
||||||
|
oldContent: string;
|
||||||
|
/** 修改后的内容 */
|
||||||
|
newContent: string;
|
||||||
|
/** 变更时间戳 */
|
||||||
|
timestamp: number;
|
||||||
|
/** 变更类型 */
|
||||||
|
changeType: 'create' | 'modify' | 'delete';
|
||||||
|
/** 变更 ID(唯一标识) */
|
||||||
|
changeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更会话(一次对话的所有变更)
|
||||||
|
*/
|
||||||
|
export interface ChangeSession {
|
||||||
|
/** 会话 ID */
|
||||||
|
sessionId: string;
|
||||||
|
/** 会话开始时间 */
|
||||||
|
startTime: number;
|
||||||
|
/** 所有文件变更 */
|
||||||
|
changes: FileChange[];
|
||||||
|
/** 会话状态 */
|
||||||
|
status: 'active' | 'completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更操作结果
|
||||||
|
*/
|
||||||
|
export interface ChangeActionResult {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
changeId: string;
|
||||||
|
}
|
||||||
203
src/utils/diffRenderer.ts
Normal file
203
src/utils/diffRenderer.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* Diff 渲染工具
|
||||||
|
* 功能:生成代码差异的 HTML 展示
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:在变更面板中展示文件修改的 diff
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface DiffLine {
|
||||||
|
type: 'add' | 'remove' | 'context';
|
||||||
|
content: string;
|
||||||
|
oldLineNumber?: number;
|
||||||
|
newLineNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的 diff 算法(基于行)
|
||||||
|
*/
|
||||||
|
export function generateDiff(oldContent: string, newContent: string): DiffLine[] {
|
||||||
|
const oldLines = oldContent.split('\n');
|
||||||
|
const newLines = newContent.split('\n');
|
||||||
|
const result: DiffLine[] = [];
|
||||||
|
|
||||||
|
let oldIndex = 0;
|
||||||
|
let newIndex = 0;
|
||||||
|
|
||||||
|
while (oldIndex < oldLines.length || newIndex < newLines.length) {
|
||||||
|
const oldLine = oldLines[oldIndex];
|
||||||
|
const newLine = newLines[newIndex];
|
||||||
|
|
||||||
|
if (oldIndex >= oldLines.length) {
|
||||||
|
// 只剩新行
|
||||||
|
result.push({
|
||||||
|
type: 'add',
|
||||||
|
content: newLine,
|
||||||
|
newLineNumber: newIndex + 1
|
||||||
|
});
|
||||||
|
newIndex++;
|
||||||
|
} else if (newIndex >= newLines.length) {
|
||||||
|
// 只剩旧行
|
||||||
|
result.push({
|
||||||
|
type: 'remove',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldIndex + 1
|
||||||
|
});
|
||||||
|
oldIndex++;
|
||||||
|
} else if (oldLine === newLine) {
|
||||||
|
// 相同行
|
||||||
|
result.push({
|
||||||
|
type: 'context',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldIndex + 1,
|
||||||
|
newLineNumber: newIndex + 1
|
||||||
|
});
|
||||||
|
oldIndex++;
|
||||||
|
newIndex++;
|
||||||
|
} else {
|
||||||
|
// 不同行,标记为删除和添加
|
||||||
|
result.push({
|
||||||
|
type: 'remove',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldIndex + 1
|
||||||
|
});
|
||||||
|
result.push({
|
||||||
|
type: 'add',
|
||||||
|
content: newLine,
|
||||||
|
newLineNumber: newIndex + 1
|
||||||
|
});
|
||||||
|
oldIndex++;
|
||||||
|
newIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 diff 结果渲染为 HTML
|
||||||
|
*/
|
||||||
|
export function renderDiffHtml(diffLines: DiffLine[]): string {
|
||||||
|
let html = '<div class="diff-viewer">';
|
||||||
|
|
||||||
|
for (const line of diffLines) {
|
||||||
|
const lineClass = `diff-line diff-line-${line.type}`;
|
||||||
|
const oldNum = line.oldLineNumber ? `<span class="line-num">${line.oldLineNumber}</span>` : '<span class="line-num"></span>';
|
||||||
|
const newNum = line.newLineNumber ? `<span class="line-num">${line.newLineNumber}</span>` : '<span class="line-num"></span>';
|
||||||
|
const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' ';
|
||||||
|
const escapedContent = escapeHtml(line.content);
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="${lineClass}">
|
||||||
|
${oldNum}
|
||||||
|
${newNum}
|
||||||
|
<span class="line-prefix">${prefix}</span>
|
||||||
|
<span class="line-content">${escapedContent}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转义 HTML 特殊字符
|
||||||
|
*/
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 diff 样式
|
||||||
|
*/
|
||||||
|
export function getDiffStyles(): string {
|
||||||
|
return `
|
||||||
|
.diff-viewer {
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
white-space: pre;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-add {
|
||||||
|
background: rgba(40, 167, 69, 0.2);
|
||||||
|
border-left: 3px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-add:hover {
|
||||||
|
background: rgba(40, 167, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-remove {
|
||||||
|
background: rgba(220, 53, 69, 0.2);
|
||||||
|
border-left: 3px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-remove:hover {
|
||||||
|
background: rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-context {
|
||||||
|
background: transparent;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-num {
|
||||||
|
display: inline-block;
|
||||||
|
width: 45px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 10px;
|
||||||
|
color: var(--vscode-editorLineNumber-foreground);
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-prefix {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-add .line-prefix {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-remove .line-prefix {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 12px 0 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
51
src/utils/fileDiff.ts
Normal file
51
src/utils/fileDiff.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import * as vscode from "vscode";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将相对路径解析为工作区绝对路径
|
||||||
|
*/
|
||||||
|
export function resolveWorkspaceFilePath(filePath: string): string {
|
||||||
|
if (path.isAbsolute(filePath)) {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
throw new Error("请先打开一个文件夹作为工作区,这样我就能为您修改文件了");
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 VS Code 原生 diff 视图展示文件修改前后对比
|
||||||
|
*/
|
||||||
|
export async function showFileDiff(
|
||||||
|
filePath: string,
|
||||||
|
oldContent: string,
|
||||||
|
title?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const absolutePath = resolveWorkspaceFilePath(filePath);
|
||||||
|
const fileUri = vscode.Uri.file(absolutePath);
|
||||||
|
const newBytes = await vscode.workspace.fs.readFile(fileUri);
|
||||||
|
const newContent = Buffer.from(newBytes).toString("utf-8");
|
||||||
|
|
||||||
|
if (oldContent === newContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = (await vscode.workspace.openTextDocument(fileUri)).languageId;
|
||||||
|
const oldDoc = await vscode.workspace.openTextDocument({
|
||||||
|
content: oldContent,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
|
const diffTitle = title || `${path.basename(absolutePath)} (修改前 <-> 修改后)`;
|
||||||
|
await vscode.commands.executeCommand(
|
||||||
|
"vscode.diff",
|
||||||
|
oldDoc.uri,
|
||||||
|
fileUri,
|
||||||
|
diffTitle,
|
||||||
|
{ preview: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ import { promisify } from "util";
|
|||||||
function execCommand(
|
function execCommand(
|
||||||
command: string,
|
command: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
options: { cwd: string; env?: any }
|
options: { cwd: string; env?: any },
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
): Promise<{ stdout: string; stderr: string }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 在 Windows 上,如果路径包含空格,不使用 shell,直接用 spawn
|
// 在 Windows 上,如果路径包含空格,不使用 shell,直接用 spawn
|
||||||
@ -23,25 +23,25 @@ function execCommand(
|
|||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
|
||||||
// 在 Windows 上使用 GBK 编码解码输出
|
// 在 Windows 上使用 GBK 编码解码输出
|
||||||
const encoding = process.platform === 'win32' ? 'gbk' : 'utf8';
|
const encoding = process.platform === "win32" ? "gbk" : "utf8";
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
child.stdout.on("data", (data) => {
|
||||||
try {
|
try {
|
||||||
// 尝试使用 iconv-lite 解码(如果可用)
|
// 尝试使用 iconv-lite 解码(如果可用)
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require("iconv-lite");
|
||||||
stdout += iconv.decode(data, encoding);
|
stdout += iconv.decode(data, encoding);
|
||||||
} catch {
|
} catch {
|
||||||
// 如果 iconv-lite 不可用,使用默认解码
|
// 如果 iconv-lite 不可用,使用默认解码
|
||||||
stdout += data.toString('utf8');
|
stdout += data.toString("utf8");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
child.stderr.on("data", (data) => {
|
||||||
try {
|
try {
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require("iconv-lite");
|
||||||
stderr += iconv.decode(data, encoding);
|
stderr += iconv.decode(data, encoding);
|
||||||
} catch {
|
} catch {
|
||||||
stderr += data.toString('utf8');
|
stderr += data.toString("utf8");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ export interface VCDGenerationResult {
|
|||||||
* 检查项目中的 Verilog 文件完整性
|
* 检查项目中的 Verilog 文件完整性
|
||||||
*/
|
*/
|
||||||
export async function checkVerilogProject(
|
export async function checkVerilogProject(
|
||||||
projectPath: string
|
projectPath: string,
|
||||||
): Promise<VerilogProjectCheck> {
|
): Promise<VerilogProjectCheck> {
|
||||||
const result: VerilogProjectCheck = {
|
const result: VerilogProjectCheck = {
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
@ -164,7 +164,7 @@ export async function checkVerilogProject(
|
|||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
result.errors.push(
|
result.errors.push(
|
||||||
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`
|
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -209,12 +209,30 @@ async function getIverilogPath(extensionPath: string): Promise<string> {
|
|||||||
let iverilogBin = "";
|
let iverilogBin = "";
|
||||||
|
|
||||||
if (platform === "win32") {
|
if (platform === "win32") {
|
||||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
|
iverilogBin = path.join(
|
||||||
|
extensionPath,
|
||||||
|
"tools",
|
||||||
|
"iverilog",
|
||||||
|
"bin",
|
||||||
|
"iverilog.exe",
|
||||||
|
);
|
||||||
} else if (platform === "darwin") {
|
} else if (platform === "darwin") {
|
||||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
|
iverilogBin = path.join(
|
||||||
|
extensionPath,
|
||||||
|
"tools",
|
||||||
|
"iverilog",
|
||||||
|
"bin",
|
||||||
|
"iverilog",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Linux
|
// Linux
|
||||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
|
iverilogBin = path.join(
|
||||||
|
extensionPath,
|
||||||
|
"tools",
|
||||||
|
"iverilog",
|
||||||
|
"bin",
|
||||||
|
"iverilog",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果插件包中没有,尝试使用系统安装的 iverilog
|
// 如果插件包中没有,尝试使用系统安装的 iverilog
|
||||||
@ -258,7 +276,7 @@ async function getVvpPath(extensionPath: string): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export async function generateVCD(
|
export async function generateVCD(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
extensionPath: string
|
extensionPath: string,
|
||||||
): Promise<VCDGenerationResult> {
|
): Promise<VCDGenerationResult> {
|
||||||
try {
|
try {
|
||||||
// 1. 检查项目完整性
|
// 1. 检查项目完整性
|
||||||
@ -302,12 +320,27 @@ export async function generateVCD(
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `iverilog 编译失败:\n${error.message}`,
|
message: `IC Coder编译器编译失败:\n${error.message}`,
|
||||||
stderr: error.stderr,
|
stderr: error.stderr,
|
||||||
stdout: error.stdout,
|
stdout: error.stdout,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误)
|
||||||
|
try {
|
||||||
|
const fs = require("fs");
|
||||||
|
const vvpContent = fs.readFileSync(outputFile, "utf8");
|
||||||
|
const lines = vvpContent.split("\n");
|
||||||
|
|
||||||
|
if (lines.length > 0 && lines[0].startsWith("#!")) {
|
||||||
|
const cleanedContent = lines.slice(1).join("\n");
|
||||||
|
fs.writeFileSync(outputFile, cleanedContent, "utf8");
|
||||||
|
console.log("已删除 .vvp 文件的 shebang 行");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("删除 shebang 失败,继续执行:", error);
|
||||||
|
}
|
||||||
|
|
||||||
// 7. 执行仿真生成 VCD
|
// 7. 执行仿真生成 VCD
|
||||||
const simArgs = [outputFile];
|
const simArgs = [outputFile];
|
||||||
console.log("执行仿真命令:", vvpPath, simArgs.join(" "));
|
console.log("执行仿真命令:", vvpPath, simArgs.join(" "));
|
||||||
@ -331,13 +364,17 @@ export async function generateVCD(
|
|||||||
const projectUri = vscode.Uri.file(projectPath);
|
const projectUri = vscode.Uri.file(projectPath);
|
||||||
const entries = await vscode.workspace.fs.readDirectory(projectUri);
|
const entries = await vscode.workspace.fs.readDirectory(projectUri);
|
||||||
const vcdFiles = entries
|
const vcdFiles = entries
|
||||||
.filter(([fileName, fileType]) => fileType === vscode.FileType.File && fileName.endsWith('.vcd'))
|
.filter(
|
||||||
|
([fileName, fileType]) =>
|
||||||
|
fileType === vscode.FileType.File && fileName.endsWith(".vcd"),
|
||||||
|
)
|
||||||
.map(([fileName]) => fileName);
|
.map(([fileName]) => fileName);
|
||||||
|
|
||||||
if (vcdFiles.length === 0) {
|
if (vcdFiles.length === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
|
message:
|
||||||
|
"VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
|
||||||
stdout: simResult.stdout,
|
stdout: simResult.stdout,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -373,7 +410,7 @@ export async function generateVCD(
|
|||||||
* 检查 iverilog 是否可用
|
* 检查 iverilog 是否可用
|
||||||
*/
|
*/
|
||||||
export async function checkIverilogAvailable(
|
export async function checkIverilogAvailable(
|
||||||
extensionPath: string
|
extensionPath: string,
|
||||||
): Promise<{ available: boolean; version?: string; message: string }> {
|
): Promise<{ available: boolean; version?: string; message: string }> {
|
||||||
try {
|
try {
|
||||||
const iverilogPath = await getIverilogPath(extensionPath);
|
const iverilogPath = await getIverilogPath(extensionPath);
|
||||||
@ -385,7 +422,7 @@ export async function checkIverilogAvailable(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
available: false,
|
available: false,
|
||||||
message: `iverilog 不可用。未找到文件: ${iverilogPath}`,
|
message: `IC Coder编译器不可用。未找到文件: ${iverilogPath}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,12 +441,12 @@ export async function checkIverilogAvailable(
|
|||||||
return {
|
return {
|
||||||
available: true,
|
available: true,
|
||||||
version: version,
|
version: version,
|
||||||
message: `iverilog 可用: ${version}`,
|
message: `IC Coder编译器可用: ${version}`,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
available: false,
|
available: false,
|
||||||
message: `iverilog 执行失败: ${error.message}\n${error.stderr || ""}`,
|
message: `IC Coder编译器执行失败: ${error.message}\n${error.stderr || ""}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -418,8 +455,8 @@ export async function checkIverilogAvailable(
|
|||||||
* 要 dump 的模块定义
|
* 要 dump 的模块定义
|
||||||
*/
|
*/
|
||||||
export interface DumpModule {
|
export interface DumpModule {
|
||||||
name: string; // 模块名(用于 VCD 文件名和宏名)
|
name: string; // 模块名(用于 VCD 文件名和宏名)
|
||||||
path: string; // 实例路径(如 dut.u_tx)
|
path: string; // 实例路径(如 dut.u_tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -444,10 +481,11 @@ export interface MultiVCDResult {
|
|||||||
function injectConditionalDump(
|
function injectConditionalDump(
|
||||||
tbContent: string,
|
tbContent: string,
|
||||||
dumpModules: DumpModule[],
|
dumpModules: DumpModule[],
|
||||||
vcdDir: string
|
vcdDir: string,
|
||||||
): string {
|
): string {
|
||||||
// 匹配 $dumpfile 和 $dumpvars 语句(可能跨多行)
|
// 匹配 $dumpfile 和 $dumpvars 语句(可能跨多行)
|
||||||
const dumpPattern = /(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
|
const dumpPattern =
|
||||||
|
/(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
|
||||||
|
|
||||||
// 生成条件编译代码
|
// 生成条件编译代码
|
||||||
const conditionalCode = generateConditionalDumpCode(dumpModules, vcdDir);
|
const conditionalCode = generateConditionalDumpCode(dumpModules, vcdDir);
|
||||||
@ -469,7 +507,7 @@ function injectConditionalDump(
|
|||||||
*/
|
*/
|
||||||
function generateConditionalDumpCode(
|
function generateConditionalDumpCode(
|
||||||
dumpModules: DumpModule[],
|
dumpModules: DumpModule[],
|
||||||
vcdDir: string
|
vcdDir: string,
|
||||||
): string {
|
): string {
|
||||||
if (dumpModules.length === 0) {
|
if (dumpModules.length === 0) {
|
||||||
return '$dumpfile("output.vcd");\n $dumpvars(0, dut);';
|
return '$dumpfile("output.vcd");\n $dumpvars(0, dut);';
|
||||||
@ -480,7 +518,7 @@ function generateConditionalDumpCode(
|
|||||||
dumpModules.forEach((module, index) => {
|
dumpModules.forEach((module, index) => {
|
||||||
const macroName = `DUMP_${module.name.toUpperCase()}`;
|
const macroName = `DUMP_${module.name.toUpperCase()}`;
|
||||||
const vcdPath = `${vcdDir}/${module.name}.vcd`;
|
const vcdPath = `${vcdDir}/${module.name}.vcd`;
|
||||||
const directive = index === 0 ? '`ifdef' : '`elsif';
|
const directive = index === 0 ? "`ifdef" : "`elsif";
|
||||||
|
|
||||||
lines.push(`${directive} ${macroName}`);
|
lines.push(`${directive} ${macroName}`);
|
||||||
lines.push(` $dumpfile("${vcdPath}");`);
|
lines.push(` $dumpfile("${vcdPath}");`);
|
||||||
@ -488,12 +526,12 @@ function generateConditionalDumpCode(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 添加默认分支(使用第一个模块)
|
// 添加默认分支(使用第一个模块)
|
||||||
lines.push('`else');
|
lines.push("`else");
|
||||||
lines.push(` $dumpfile("${vcdDir}/${dumpModules[0].name}.vcd");`);
|
lines.push(` $dumpfile("${vcdDir}/${dumpModules[0].name}.vcd");`);
|
||||||
lines.push(` $dumpvars(1, ${dumpModules[0].path});`);
|
lines.push(` $dumpvars(1, ${dumpModules[0].path});`);
|
||||||
lines.push('`endif');
|
lines.push("`endif");
|
||||||
|
|
||||||
return lines.join('\n');
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -504,10 +542,10 @@ export async function generateMultiVCD(
|
|||||||
extensionPath: string,
|
extensionPath: string,
|
||||||
tbPath: string,
|
tbPath: string,
|
||||||
dumpModules: DumpModule[],
|
dumpModules: DumpModule[],
|
||||||
vcdDir: string = 'vcd'
|
vcdDir: string = "vcd",
|
||||||
): Promise<MultiVCDResult> {
|
): Promise<MultiVCDResult> {
|
||||||
const results: MultiVCDResult['vcdFiles'] = [];
|
const results: MultiVCDResult["vcdFiles"] = [];
|
||||||
let allStdout = '';
|
let allStdout = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 创建 vcd 目录
|
// 1. 创建 vcd 目录
|
||||||
@ -520,16 +558,21 @@ export async function generateMultiVCD(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 读取原始 testbench
|
// 2. 读取原始 testbench
|
||||||
const tbFullPath = path.isAbsolute(tbPath) ? tbPath : path.join(projectPath, tbPath);
|
const tbFullPath = path.isAbsolute(tbPath)
|
||||||
|
? tbPath
|
||||||
|
: path.join(projectPath, tbPath);
|
||||||
const tbUri = vscode.Uri.file(tbFullPath);
|
const tbUri = vscode.Uri.file(tbFullPath);
|
||||||
const tbBytes = await vscode.workspace.fs.readFile(tbUri);
|
const tbBytes = await vscode.workspace.fs.readFile(tbUri);
|
||||||
const originalTb = Buffer.from(tbBytes).toString('utf-8');
|
const originalTb = Buffer.from(tbBytes).toString("utf-8");
|
||||||
|
|
||||||
// 3. 注入条件编译代码
|
// 3. 注入条件编译代码
|
||||||
const modifiedTb = injectConditionalDump(originalTb, dumpModules, vcdDir);
|
const modifiedTb = injectConditionalDump(originalTb, dumpModules, vcdDir);
|
||||||
await vscode.workspace.fs.writeFile(tbUri, Buffer.from(modifiedTb, 'utf-8'));
|
await vscode.workspace.fs.writeFile(
|
||||||
|
tbUri,
|
||||||
|
Buffer.from(modifiedTb, "utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
console.log('[generateMultiVCD] Testbench 已修改,开始多次仿真...');
|
console.log("[generateMultiVCD] Testbench 已修改,开始多次仿真...");
|
||||||
|
|
||||||
// 4. 获取工具路径
|
// 4. 获取工具路径
|
||||||
const iverilogPath = await getIverilogPath(extensionPath);
|
const iverilogPath = await getIverilogPath(extensionPath);
|
||||||
@ -554,27 +597,34 @@ export async function generateMultiVCD(
|
|||||||
// 编译(带宏定义)
|
// 编译(带宏定义)
|
||||||
const compileArgs = [
|
const compileArgs = [
|
||||||
`-D${macroName}`,
|
`-D${macroName}`,
|
||||||
"-o", outputFile,
|
"-o",
|
||||||
...projectCheck.allVerilogFiles
|
outputFile,
|
||||||
|
...projectCheck.allVerilogFiles,
|
||||||
];
|
];
|
||||||
await execCommand(iverilogPath, compileArgs, { cwd: projectPath, env });
|
await execCommand(iverilogPath, compileArgs, { cwd: projectPath, env });
|
||||||
|
|
||||||
// 仿真
|
// 仿真
|
||||||
const simResult = await execCommand(vvpPath, [outputFile], { cwd: projectPath, env });
|
const simResult = await execCommand(vvpPath, [outputFile], {
|
||||||
|
cwd: projectPath,
|
||||||
|
env,
|
||||||
|
});
|
||||||
allStdout += `\n[${module.name}] ${simResult.stdout}`;
|
allStdout += `\n[${module.name}] ${simResult.stdout}`;
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
moduleName: module.name,
|
moduleName: module.name,
|
||||||
vcdPath: vcdPath,
|
vcdPath: vcdPath,
|
||||||
success: true
|
success: true,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`[generateMultiVCD] 模块 ${module.name} 仿真失败:`, error.message);
|
console.error(
|
||||||
|
`[generateMultiVCD] 模块 ${module.name} 仿真失败:`,
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
results.push({
|
results.push({
|
||||||
moduleName: module.name,
|
moduleName: module.name,
|
||||||
vcdPath: vcdPath,
|
vcdPath: vcdPath,
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message
|
error: error.message,
|
||||||
});
|
});
|
||||||
// 继续执行其他模块
|
// 继续执行其他模块
|
||||||
}
|
}
|
||||||
@ -587,19 +637,18 @@ export async function generateMultiVCD(
|
|||||||
// 忽略
|
// 忽略
|
||||||
}
|
}
|
||||||
|
|
||||||
const successCount = results.filter(r => r.success).length;
|
const successCount = results.filter((r) => r.success).length;
|
||||||
return {
|
return {
|
||||||
success: successCount > 0,
|
success: successCount > 0,
|
||||||
vcdFiles: results,
|
vcdFiles: results,
|
||||||
message: `生成完成:${successCount}/${dumpModules.length} 个 VCD 文件成功`,
|
message: `生成完成:${successCount}/${dumpModules.length} 个 VCD 文件成功`,
|
||||||
stdout: allStdout
|
stdout: allStdout,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
vcdFiles: results,
|
vcdFiles: results,
|
||||||
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : '未知错误'}`
|
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export interface JwtPayload {
|
|||||||
user_id?: number; // 用户ID (下划线命名)
|
user_id?: number; // 用户ID (下划线命名)
|
||||||
exp?: number; // 过期时间
|
exp?: number; // 过期时间
|
||||||
iat?: number; // 签发时间
|
iat?: number; // 签发时间
|
||||||
|
ispluginTrial?: boolean; // 是否是插件试用用户
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,3 +103,24 @@ export function isTokenExpired(
|
|||||||
|
|
||||||
return isExpired;
|
return isExpired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 JWT token 中获取 ispluginTrial 标识
|
||||||
|
* @param token JWT token
|
||||||
|
* @returns true=插件试用用户,false=正式用户,null=无法判断
|
||||||
|
*/
|
||||||
|
export function getIsPluginTrialFromToken(token: string): boolean | null {
|
||||||
|
const payload = parseJwtPayload(token);
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 ispluginTrial 字段
|
||||||
|
if (payload.ispluginTrial !== undefined) {
|
||||||
|
console.log("[JWT] 从 token 中获取到 ispluginTrial:", payload.ispluginTrial);
|
||||||
|
return payload.ispluginTrial === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[JWT] token 中没有 ispluginTrial 字段,判定为正式用户");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
checkVerilogProject,
|
checkVerilogProject,
|
||||||
checkIverilogAvailable,
|
checkIverilogAvailable,
|
||||||
} from "./iverilogRunner";
|
} from "./iverilogRunner";
|
||||||
|
import { createVivadoProject, runVivadoSynthesis } from "./vivadoRunner";
|
||||||
import { ChatHistoryManager } from "./chatHistoryManager";
|
import { ChatHistoryManager } from "./chatHistoryManager";
|
||||||
import { dialogManager, DialogSession } from "../services/dialogService";
|
import { dialogManager, DialogSession } from "../services/dialogService";
|
||||||
import { userInteractionManager } from "../services/userInteraction";
|
import { userInteractionManager } from "../services/userInteraction";
|
||||||
@ -25,6 +26,10 @@ import {
|
|||||||
} from "../services/creditsService";
|
} from "../services/creditsService";
|
||||||
import { optimizePrompt } from "../services/promptOptimizeService";
|
import { optimizePrompt } from "../services/promptOptimizeService";
|
||||||
import { NotificationService } from "../services/notificationService";
|
import { NotificationService } from "../services/notificationService";
|
||||||
|
import { TrialExpirationService } from "../services/trialExpirationService";
|
||||||
|
import { showFileDiff } from "./fileDiff";
|
||||||
|
import { changeTracker } from "../services/changeTracker";
|
||||||
|
import { generateDiff, renderDiffHtml } from "./diffRenderer";
|
||||||
|
|
||||||
import type { RunMode, ServiceTier } from "../types/api";
|
import type { RunMode, ServiceTier } from "../types/api";
|
||||||
|
|
||||||
@ -37,6 +42,18 @@ let currentSession: DialogSession | null = null;
|
|||||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||||
let lastTaskId: string | null = null;
|
let lastTaskId: string | null = null;
|
||||||
|
|
||||||
|
async function trackFileChange(
|
||||||
|
filePath: string,
|
||||||
|
oldContent: string,
|
||||||
|
newContent: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
changeTracker.trackChange(filePath, oldContent, newContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[MessageHandler] 记录文件变更失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理用户消息
|
* 处理用户消息
|
||||||
*/
|
*/
|
||||||
@ -45,7 +62,8 @@ export async function handleUserMessage(
|
|||||||
text: string,
|
text: string,
|
||||||
extensionPath?: string,
|
extensionPath?: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
serviceTier?: ServiceTier, // 服务等级参数
|
||||||
|
contextItems?: Array<{ id: number; type: string; path: string }>, // 上下文项参数
|
||||||
) {
|
) {
|
||||||
console.log("收到用户消息:", text);
|
console.log("收到用户消息:", text);
|
||||||
|
|
||||||
@ -55,7 +73,9 @@ export async function handleUserMessage(
|
|||||||
// 从 session 中获取 token
|
// 从 session 中获取 token
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
try {
|
try {
|
||||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
createIfNone: false,
|
||||||
|
});
|
||||||
token = session?.accessToken;
|
token = session?.accessToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[MessageHandler] 获取 session 失败:", error);
|
console.warn("[MessageHandler] 获取 session 失败:", error);
|
||||||
@ -65,21 +85,23 @@ export async function handleUserMessage(
|
|||||||
console.warn("[MessageHandler] 未登录,阻止发送");
|
console.warn("[MessageHandler] 未登录,阻止发送");
|
||||||
|
|
||||||
// 保存待发送的消息
|
// 保存待发送的消息
|
||||||
await context.globalState.update('pendingMessage', {
|
await context.globalState.update("pendingMessage", {
|
||||||
text,
|
text,
|
||||||
mode,
|
mode,
|
||||||
serviceTier,
|
serviceTier,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示弹窗提示
|
// 显示弹窗提示
|
||||||
const action = await vscode.window.showWarningMessage(
|
const action = await vscode.window.showWarningMessage(
|
||||||
'请先登录后再发送消息',
|
"请先登录后再发送消息",
|
||||||
'立即登录'
|
"立即登录",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (action === '立即登录') {
|
if (action === "立即登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复输入状态
|
// 恢复输入状态
|
||||||
@ -95,25 +117,27 @@ export async function handleUserMessage(
|
|||||||
console.warn("[MessageHandler] Token 已过期,阻止发送");
|
console.warn("[MessageHandler] Token 已过期,阻止发送");
|
||||||
|
|
||||||
// 保存待发送的消息
|
// 保存待发送的消息
|
||||||
await context.globalState.update('pendingMessage', {
|
await context.globalState.update("pendingMessage", {
|
||||||
text,
|
text,
|
||||||
mode,
|
mode,
|
||||||
serviceTier,
|
serviceTier,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清除过期的 session
|
// 清除过期的 session
|
||||||
await context.globalState.update('icCoderSessions', []);
|
await context.globalState.update("icCoderSessions", []);
|
||||||
await context.globalState.update('icCoderUserInfo', undefined);
|
await context.globalState.update("icCoderUserInfo", undefined);
|
||||||
|
|
||||||
// 显示弹窗提示
|
// 显示弹窗提示
|
||||||
const action = await vscode.window.showWarningMessage(
|
const action = await vscode.window.showWarningMessage(
|
||||||
'登录已过期,请重新登录',
|
"登录已过期,请重新登录",
|
||||||
'立即登录'
|
"立即登录",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (action === '立即登录') {
|
if (action === "立即登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复输入状态
|
// 恢复输入状态
|
||||||
@ -124,6 +148,21 @@ export async function handleUserMessage(
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查试用期是否过期
|
||||||
|
const trialService = new TrialExpirationService(context, panel);
|
||||||
|
const isExpired = await trialService.checkExpiration();
|
||||||
|
if (isExpired) {
|
||||||
|
console.warn("[MessageHandler] 试用期已过期,阻止发送");
|
||||||
|
|
||||||
|
// 恢复输入状态
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateSegments",
|
||||||
|
segments: [],
|
||||||
|
isComplete: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录用户消息到历史(允许失败,不阻塞主流程)
|
// 记录用户消息到历史(允许失败,不阻塞主流程)
|
||||||
@ -158,11 +197,11 @@ export async function handleUserMessage(
|
|||||||
// 显示错误提示
|
// 显示错误提示
|
||||||
const selection = await vscode.window.showWarningMessage(
|
const selection = await vscode.window.showWarningMessage(
|
||||||
balanceCheck.message || "资源点余额不足",
|
balanceCheck.message || "资源点余额不足",
|
||||||
"去充值"
|
"去充值",
|
||||||
);
|
);
|
||||||
if (selection === "去充值") {
|
if (selection === "去充值") {
|
||||||
vscode.env.openExternal(
|
vscode.env.openExternal(
|
||||||
vscode.Uri.parse("https://iccoder.com/memberCenter")
|
vscode.Uri.parse("https://iccoder.com/memberCenter"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 恢复输入状态
|
// 恢复输入状态
|
||||||
@ -183,14 +222,15 @@ export async function handleUserMessage(
|
|||||||
extensionPath,
|
extensionPath,
|
||||||
mode,
|
mode,
|
||||||
undefined,
|
undefined,
|
||||||
serviceTier
|
serviceTier,
|
||||||
|
contextItems,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("后端服务不可用:", error);
|
console.error("当前访问人数过多,请稍后重试:", error);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "updateStatus",
|
command: "updateStatus",
|
||||||
text: "后端服务不可用",
|
text: "当前访问人数过多,请稍后重试",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
// 恢复输入状态
|
// 恢复输入状态
|
||||||
@ -220,10 +260,19 @@ async function handleUserMessageWithBackend(
|
|||||||
extensionPath: string,
|
extensionPath: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
serviceTier?: ServiceTier, // 服务等级参数
|
||||||
|
contextItems?: Array<{ id: number; type: string; path: string }>, // 上下文项参数
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
|
// 处理上下文项:在消息前附加文件/文件夹路径
|
||||||
|
let enhancedText = text;
|
||||||
|
if (contextItems && contextItems.length > 0) {
|
||||||
|
console.log("[MessageHandler] 处理上下文项:", contextItems.length);
|
||||||
|
const paths = contextItems.map((item) => item.path).join("\n");
|
||||||
|
enhancedText = `${paths}\n\n${text}`;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
||||||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||||
@ -231,7 +280,7 @@ async function handleUserMessageWithBackend(
|
|||||||
// 创建会话(dialogManager 会自动处理旧会话的中止)
|
// 创建会话(dialogManager 会自动处理旧会话的中止)
|
||||||
currentSession = dialogManager.createSession(
|
currentSession = dialogManager.createSession(
|
||||||
extensionPath,
|
extensionPath,
|
||||||
taskIdToUse || undefined
|
taskIdToUse || undefined,
|
||||||
);
|
);
|
||||||
// 保存 taskId 用于后续操作(如压缩)
|
// 保存 taskId 用于后续操作(如压缩)
|
||||||
lastTaskId = currentSession.getTaskId();
|
lastTaskId = currentSession.getTaskId();
|
||||||
@ -239,7 +288,7 @@ async function handleUserMessageWithBackend(
|
|||||||
"[MessageHandler] 创建会话: taskId=",
|
"[MessageHandler] 创建会话: taskId=",
|
||||||
lastTaskId,
|
lastTaskId,
|
||||||
"来源=",
|
"来源=",
|
||||||
taskIdToUse ? "historyManager" : "新生成"
|
taskIdToUse ? "historyManager" : "新生成",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 显示状态栏
|
// 显示状态栏
|
||||||
@ -251,17 +300,25 @@ async function handleUserMessageWithBackend(
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
currentSession!.sendMessage(
|
currentSession!.sendMessage(
|
||||||
text,
|
enhancedText,
|
||||||
{
|
{
|
||||||
onText: (fullText, isStreaming) => {
|
onText: (fullText, isStreaming) => {
|
||||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||||
},
|
},
|
||||||
|
|
||||||
onSegmentUpdate: (segments) => {
|
onSegmentUpdate: (segments) => {
|
||||||
|
// 过滤掉包含 [调用工具:xxx] 的段落
|
||||||
|
const filteredSegments = segments.filter(seg => {
|
||||||
|
if (seg.type === 'text' && typeof seg.content === 'string') {
|
||||||
|
return !/\[调用工具:.+?\]/.test(seg.content);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// 实时发送段落更新,按后端返回顺序展示
|
// 实时发送段落更新,按后端返回顺序展示
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "updateSegments",
|
command: "updateSegments",
|
||||||
segments: segments,
|
segments: filteredSegments,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -282,7 +339,10 @@ async function handleUserMessageWithBackend(
|
|||||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||||
},
|
},
|
||||||
|
|
||||||
onQuestion: (askId, question, options) => {
|
onQuestion: (
|
||||||
|
askId: string,
|
||||||
|
questions: import("../types/api").QuestionItem[],
|
||||||
|
) => {
|
||||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "updateStatus",
|
command: "updateStatus",
|
||||||
@ -327,26 +387,36 @@ async function handleUserMessageWithBackend(
|
|||||||
command: "hideStatus",
|
command: "hideStatus",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 最后一次发送完整的段落
|
// 发送完成标记(不再重复发送 segments,避免内容重复显示)
|
||||||
const result = await panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "updateSegments",
|
command: "updateSegments",
|
||||||
segments: segments,
|
segments: [],
|
||||||
isComplete: true,
|
isComplete: true,
|
||||||
});
|
});
|
||||||
console.log("[MessageHandler] postMessage 返回值:", result);
|
|
||||||
|
// 发送任务完成消息
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "taskComplete",
|
||||||
|
});
|
||||||
|
|
||||||
// 发送系统通知 - AI 响应完成
|
// 发送系统通知 - AI 响应完成
|
||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
notificationService.success(
|
notificationService.success(
|
||||||
'IC Coder - AI 响应完成',
|
"IC Coder - AI 响应完成",
|
||||||
'您的问题已得到回复,点击查看详情',
|
"您的问题已得到回复,点击查看详情",
|
||||||
() => {
|
() => {
|
||||||
// 点击通知时聚焦到面板
|
// 点击通知时聚焦到面板
|
||||||
panel.reveal();
|
panel.reveal();
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 发送代码变更到前端
|
||||||
|
sendChangesToWebview(panel);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
|
console.warn(
|
||||||
|
"[MessageHandler] 更新面板失败(面板可能已关闭):",
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
@ -414,7 +484,7 @@ async function handleUserMessageWithBackend(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mode,
|
mode,
|
||||||
serviceTier // 传递服务等级
|
serviceTier, // 传递服务等级
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -425,10 +495,11 @@ async function handleUserMessageWithBackend(
|
|||||||
export async function handleUserAnswer(
|
export async function handleUserAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string
|
customInput?: string,
|
||||||
|
answers?: { [questionIndex: string]: string[] },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (currentSession) {
|
if (currentSession) {
|
||||||
await currentSession.submitAnswer(askId, selected, customInput);
|
await currentSession.submitAnswer(askId, selected, customInput, answers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,7 +566,7 @@ export async function handlePlanAction(
|
|||||||
action: string,
|
action: string,
|
||||||
planTitle: string,
|
planTitle: string,
|
||||||
extensionPath: string,
|
extensionPath: string,
|
||||||
serviceTier?: ServiceTier
|
serviceTier?: ServiceTier,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log(
|
console.log(
|
||||||
"[handlePlanAction] action:",
|
"[handlePlanAction] action:",
|
||||||
@ -503,7 +574,7 @@ export async function handlePlanAction(
|
|||||||
"planTitle:",
|
"planTitle:",
|
||||||
planTitle,
|
planTitle,
|
||||||
"serviceTier:",
|
"serviceTier:",
|
||||||
serviceTier
|
serviceTier,
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@ -519,7 +590,7 @@ export async function handlePlanAction(
|
|||||||
`请按照刚才的计划执行:${planTitle}`,
|
`请按照刚才的计划执行:${planTitle}`,
|
||||||
extensionPath,
|
extensionPath,
|
||||||
"agent",
|
"agent",
|
||||||
serviceTier
|
serviceTier,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -536,7 +607,7 @@ export async function handlePlanAction(
|
|||||||
`请根据以下建议修改计划:${modification}`,
|
`请根据以下建议修改计划:${modification}`,
|
||||||
extensionPath,
|
extensionPath,
|
||||||
"plan",
|
"plan",
|
||||||
serviceTier
|
serviceTier,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -591,7 +662,7 @@ function parseFileOperation(text: string): {
|
|||||||
|
|
||||||
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
||||||
const renameMatch = lowerText.match(
|
const renameMatch = lowerText.match(
|
||||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
|
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/,
|
||||||
);
|
);
|
||||||
if (renameMatch) {
|
if (renameMatch) {
|
||||||
const oldPath = renameMatch[1].trim();
|
const oldPath = renameMatch[1].trim();
|
||||||
@ -608,7 +679,7 @@ function parseFileOperation(text: string): {
|
|||||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||||
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
||||||
const replaceMatch1 = lowerText.match(
|
const replaceMatch1 = lowerText.match(
|
||||||
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/,
|
||||||
);
|
);
|
||||||
if (replaceMatch1) {
|
if (replaceMatch1) {
|
||||||
const filePath = replaceMatch1[1].trim();
|
const filePath = replaceMatch1[1].trim();
|
||||||
@ -624,7 +695,7 @@ function parseFileOperation(text: string): {
|
|||||||
|
|
||||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||||
const replaceMatch2 = lowerText.match(
|
const replaceMatch2 = lowerText.match(
|
||||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/,
|
||||||
);
|
);
|
||||||
if (replaceMatch2) {
|
if (replaceMatch2) {
|
||||||
const filePath = replaceMatch2[1].trim();
|
const filePath = replaceMatch2[1].trim();
|
||||||
@ -673,7 +744,7 @@ async function handleFileOperation(
|
|||||||
newPath?: string;
|
newPath?: string;
|
||||||
searchText?: string;
|
searchText?: string;
|
||||||
replaceText?: string;
|
replaceText?: string;
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
@ -689,7 +760,7 @@ async function handleFileOperation(
|
|||||||
text: responseText,
|
text: responseText,
|
||||||
});
|
});
|
||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
`文件创建成功: ${operation.filePath}`
|
`文件创建成功: ${operation.filePath}`,
|
||||||
);
|
);
|
||||||
await historyManager.addAiMessage(responseText);
|
await historyManager.addAiMessage(responseText);
|
||||||
break;
|
break;
|
||||||
@ -702,7 +773,7 @@ async function handleFileOperation(
|
|||||||
text: responseText,
|
text: responseText,
|
||||||
});
|
});
|
||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
`文件删除成功: ${operation.filePath}`
|
`文件删除成功: ${operation.filePath}`,
|
||||||
);
|
);
|
||||||
await historyManager.addAiMessage(responseText);
|
await historyManager.addAiMessage(responseText);
|
||||||
break;
|
break;
|
||||||
@ -738,7 +809,7 @@ async function handleFileOperation(
|
|||||||
text: responseText,
|
text: responseText,
|
||||||
});
|
});
|
||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
`文件重命名成功: ${operation.filePath} → ${operation.newPath}`
|
`文件重命名成功: ${operation.filePath} → ${operation.newPath}`,
|
||||||
);
|
);
|
||||||
await historyManager.addAiMessage(responseText);
|
await historyManager.addAiMessage(responseText);
|
||||||
break;
|
break;
|
||||||
@ -747,10 +818,21 @@ async function handleFileOperation(
|
|||||||
if (!operation.searchText || !operation.replaceText) {
|
if (!operation.searchText || !operation.replaceText) {
|
||||||
throw new Error("缺少替换内容");
|
throw new Error("缺少替换内容");
|
||||||
}
|
}
|
||||||
|
const oldContentBeforeReplace = await readFileContent(
|
||||||
|
operation.filePath,
|
||||||
|
);
|
||||||
await replaceFile(
|
await replaceFile(
|
||||||
operation.filePath,
|
operation.filePath,
|
||||||
operation.searchText,
|
operation.searchText,
|
||||||
operation.replaceText
|
operation.replaceText,
|
||||||
|
);
|
||||||
|
const newContentAfterReplace = await readFileContent(
|
||||||
|
operation.filePath,
|
||||||
|
);
|
||||||
|
await trackFileChange(
|
||||||
|
operation.filePath,
|
||||||
|
oldContentBeforeReplace,
|
||||||
|
newContentAfterReplace,
|
||||||
);
|
);
|
||||||
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -758,7 +840,7 @@ async function handleFileOperation(
|
|||||||
text: responseText,
|
text: responseText,
|
||||||
});
|
});
|
||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
`文件内容替换成功: ${operation.filePath}`
|
`文件内容替换成功: ${operation.filePath}`,
|
||||||
);
|
);
|
||||||
await historyManager.addAiMessage(responseText);
|
await historyManager.addAiMessage(responseText);
|
||||||
break;
|
break;
|
||||||
@ -818,7 +900,7 @@ function getDefaultContent(filePath: string): string {
|
|||||||
*/
|
*/
|
||||||
export async function handleReadFile(
|
export async function handleReadFile(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
filePath: string
|
filePath: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const content = await readFileContent(filePath);
|
const content = await readFileContent(filePath);
|
||||||
@ -842,7 +924,7 @@ export async function handleCreateFile(
|
|||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
content: string,
|
content: string,
|
||||||
overwrite: boolean = false //是否覆盖
|
overwrite: boolean = false, //是否覆盖
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (overwrite) {
|
if (overwrite) {
|
||||||
@ -861,11 +943,14 @@ export async function handleCreateFile(
|
|||||||
// 发送系统通知
|
// 发送系统通知
|
||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
notificationService.success(
|
notificationService.success(
|
||||||
'IC Coder - 文件创建',
|
"IC Coder - 文件创建",
|
||||||
`文件已创建: ${path.basename(filePath)}`,
|
`文件已创建: ${path.basename(filePath)}`,
|
||||||
() => {
|
() => {
|
||||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
|
vscode.commands.executeCommand(
|
||||||
}
|
"vscode.open",
|
||||||
|
vscode.Uri.file(filePath),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -873,7 +958,7 @@ export async function handleCreateFile(
|
|||||||
error: error instanceof Error ? error.message : "创建文件失败",
|
error: error instanceof Error ? error.message : "创建文件失败",
|
||||||
});
|
});
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -884,10 +969,12 @@ export async function handleCreateFile(
|
|||||||
export async function handleUpdateFile(
|
export async function handleUpdateFile(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
content: string
|
content: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const oldContent = await readFileContent(filePath);
|
||||||
await updateFile(filePath, content);
|
await updateFile(filePath, content);
|
||||||
|
await trackFileChange(filePath, oldContent, content);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "fileUpdated",
|
command: "fileUpdated",
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
@ -898,8 +985,8 @@ export async function handleUpdateFile(
|
|||||||
// 发送系统通知
|
// 发送系统通知
|
||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
notificationService.info(
|
notificationService.info(
|
||||||
'IC Coder - 文件更新',
|
"IC Coder - 文件更新",
|
||||||
`文件已更新: ${path.basename(filePath)}`
|
`文件已更新: ${path.basename(filePath)}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -907,7 +994,7 @@ export async function handleUpdateFile(
|
|||||||
error: error instanceof Error ? error.message : "更新文件失败",
|
error: error instanceof Error ? error.message : "更新文件失败",
|
||||||
});
|
});
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -918,7 +1005,7 @@ export async function handleUpdateFile(
|
|||||||
export async function handleRenameFile(
|
export async function handleRenameFile(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
oldPath: string,
|
oldPath: string,
|
||||||
newPath: string
|
newPath: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await renameFile(oldPath, newPath);
|
await renameFile(oldPath, newPath);
|
||||||
@ -929,7 +1016,7 @@ export async function handleRenameFile(
|
|||||||
message: "文件重命名成功",
|
message: "文件重命名成功",
|
||||||
});
|
});
|
||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
`文件重命名成功: ${oldPath} → ${newPath}`
|
`文件重命名成功: ${oldPath} → ${newPath}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -937,7 +1024,7 @@ export async function handleRenameFile(
|
|||||||
error: error instanceof Error ? error.message : "重命名文件失败",
|
error: error instanceof Error ? error.message : "重命名文件失败",
|
||||||
});
|
});
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -949,10 +1036,13 @@ export async function handleReplaceInFile(
|
|||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
searchText: string,
|
searchText: string,
|
||||||
replaceText: string
|
replaceText: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const oldContent = await readFileContent(filePath);
|
||||||
await replaceFile(filePath, searchText, replaceText);
|
await replaceFile(filePath, searchText, replaceText);
|
||||||
|
const newContent = await readFileContent(filePath);
|
||||||
|
await trackFileChange(filePath, oldContent, newContent);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "fileReplaced",
|
command: "fileReplaced",
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
@ -965,7 +1055,7 @@ export async function handleReplaceInFile(
|
|||||||
error: error instanceof Error ? error.message : "替换文件内容失败",
|
error: error instanceof Error ? error.message : "替换文件内容失败",
|
||||||
});
|
});
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`
|
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1010,7 +1100,7 @@ function isVCDGenerationCommand(text: string): boolean {
|
|||||||
*/
|
*/
|
||||||
async function handleVCDGeneration(
|
async function handleVCDGeneration(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
extensionPath: string
|
extensionPath: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// 获取当前工作区路径
|
// 获取当前工作区路径
|
||||||
@ -1037,7 +1127,7 @@ async function handleVCDGeneration(
|
|||||||
if (!iverilogCheck.available) {
|
if (!iverilogCheck.available) {
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "receiveMessage",
|
command: "receiveMessage",
|
||||||
text: `❌ ${iverilogCheck.message}\n\n请参考插件文档安装 iverilog 工具。`,
|
text: `❌ ${iverilogCheck.message}。`,
|
||||||
});
|
});
|
||||||
vscode.window.showErrorMessage(iverilogCheck.message);
|
vscode.window.showErrorMessage(iverilogCheck.message);
|
||||||
return;
|
return;
|
||||||
@ -1112,12 +1202,15 @@ async function handleVCDGeneration(
|
|||||||
// 发送系统通知
|
// 发送系统通知
|
||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
notificationService.success(
|
notificationService.success(
|
||||||
'IC Coder - 仿真完成',
|
"IC Coder - 仿真完成",
|
||||||
`VCD 文件已生成: ${fileName}`,
|
`VCD 文件已生成: ${fileName}`,
|
||||||
() => {
|
() => {
|
||||||
// 点击通知时打开 VCD 查看器
|
// 点击通知时打开 VCD 查看器
|
||||||
vscode.commands.executeCommand('ic-coder.openVCDViewer', result.vcdFilePath);
|
vscode.commands.executeCommand(
|
||||||
}
|
"ic-coder.openVCDViewer",
|
||||||
|
result.vcdFilePath,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -1146,12 +1239,12 @@ async function handleVCDGeneration(
|
|||||||
// 发送系统通知
|
// 发送系统通知
|
||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
notificationService.error(
|
notificationService.error(
|
||||||
'IC Coder - 仿真失败',
|
"IC Coder - 仿真失败",
|
||||||
'VCD 文件生成失败,请查看错误信息',
|
"VCD 文件生成失败,请查看错误信息",
|
||||||
() => {
|
() => {
|
||||||
// 点击通知时聚焦到面板
|
// 点击通知时聚焦到面板
|
||||||
panel.reveal();
|
panel.reveal();
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -1169,11 +1262,11 @@ async function handleVCDGeneration(
|
|||||||
// 发送系统通知
|
// 发送系统通知
|
||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
notificationService.error(
|
notificationService.error(
|
||||||
'IC Coder - 仿真错误',
|
"IC Coder - 仿真错误",
|
||||||
error instanceof Error ? error.message : '生成 VCD 文件时出错',
|
error instanceof Error ? error.message : "生成 VCD 文件时出错",
|
||||||
() => {
|
() => {
|
||||||
panel.reveal();
|
panel.reveal();
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1183,7 +1276,7 @@ async function handleVCDGeneration(
|
|||||||
*/
|
*/
|
||||||
export async function handleOptimizePrompt(
|
export async function handleOptimizePrompt(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
prompt: string
|
prompt: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
|
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
|
||||||
console.log("[MessageHandler] prompt:", prompt);
|
console.log("[MessageHandler] prompt:", prompt);
|
||||||
@ -1209,3 +1302,205 @@ export async function handleOptimizePrompt(
|
|||||||
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理采纳变更
|
||||||
|
*/
|
||||||
|
export async function handleAcceptChange(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const success = await changeTracker.acceptChange(changeId);
|
||||||
|
if (success) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: "采纳变更失败",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[MessageHandler] 采纳变更失败:", error);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理拒绝变更
|
||||||
|
*/
|
||||||
|
export async function handleRejectChange(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const success = await changeTracker.rejectChange(changeId);
|
||||||
|
if (success) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: "拒绝变更失败",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[MessageHandler] 拒绝变更失败:", error);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在对话结束时发送变更列表到前端
|
||||||
|
*/
|
||||||
|
export function sendChangesToWebview(panel: vscode.WebviewPanel) {
|
||||||
|
const session = changeTracker.endSession();
|
||||||
|
if (session && session.changes.length > 0) {
|
||||||
|
const changesWithDiff = session.changes.map((change) => {
|
||||||
|
const diffLines = generateDiff(change.oldContent, change.newContent);
|
||||||
|
const diffHtml = renderDiffHtml(diffLines);
|
||||||
|
return {
|
||||||
|
...change,
|
||||||
|
diffHtml,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "showChanges",
|
||||||
|
changes: changesWithDiff,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始新的变更会话
|
||||||
|
*/
|
||||||
|
export function startChangeSession(sessionId: string) {
|
||||||
|
changeTracker.startSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开文件 diff 编辑器
|
||||||
|
*/
|
||||||
|
export async function handleOpenFileDiff(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = changeTracker.getCurrentSession();
|
||||||
|
if (!session) {
|
||||||
|
vscode.window.showErrorMessage("没有找到变更会话");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = session.changes.find((c) => c.changeId === changeId);
|
||||||
|
if (!change) {
|
||||||
|
vscode.window.showErrorMessage("没有找到该变更");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
vscode.window.showErrorMessage("没有打开的工作区");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时文件用于对比
|
||||||
|
const filePath = change.filePath;
|
||||||
|
const absolutePath = vscode.Uri.file(
|
||||||
|
path.join(workspaceFolder.uri.fsPath, filePath),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建虚拟文档显示旧内容
|
||||||
|
const oldUri = vscode.Uri.parse(
|
||||||
|
`ic-coder-diff:${filePath}.old?${changeId}`,
|
||||||
|
).with({ scheme: "ic-coder-diff" });
|
||||||
|
|
||||||
|
// 注册文档内容提供者(如果还没注册)
|
||||||
|
if (!(global as any).__diffProviderRegistered) {
|
||||||
|
const provider = new (class
|
||||||
|
implements vscode.TextDocumentContentProvider
|
||||||
|
{
|
||||||
|
provideTextDocumentContent(uri: vscode.Uri): string {
|
||||||
|
const changeId = uri.query;
|
||||||
|
const session = changeTracker.getCurrentSession();
|
||||||
|
const change = session?.changes.find((c) => c.changeId === changeId);
|
||||||
|
return change?.oldContent || "";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
vscode.workspace.registerTextDocumentContentProvider(
|
||||||
|
"ic-coder-diff",
|
||||||
|
provider,
|
||||||
|
);
|
||||||
|
(global as any).__diffProviderRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开 diff 编辑器
|
||||||
|
await vscode.commands.executeCommand(
|
||||||
|
"vscode.diff",
|
||||||
|
oldUri,
|
||||||
|
absolutePath,
|
||||||
|
`${filePath} (变更对比)`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[MessageHandler] 打开 diff 失败:", error);
|
||||||
|
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 Vivado 工具调用
|
||||||
|
*/
|
||||||
|
export async function handleVivadoToolCall(
|
||||||
|
toolName: string,
|
||||||
|
params: any
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'createVivadoProject':
|
||||||
|
return await createVivadoProject(params);
|
||||||
|
|
||||||
|
case 'runVivadoSynthesis':
|
||||||
|
return await runVivadoSynthesis(params);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
command: toolName,
|
||||||
|
executionTime: 0,
|
||||||
|
output: '',
|
||||||
|
error: `未知的工具: ${toolName}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
command: toolName,
|
||||||
|
executionTime: 0,
|
||||||
|
output: '',
|
||||||
|
error: error.message || String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
104
src/utils/tclGenerator.ts
Normal file
104
src/utils/tclGenerator.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* TCL 脚本生成器
|
||||||
|
* 功能:生成 Vivado TCL 脚本
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成创建工程的 TCL 脚本
|
||||||
|
*/
|
||||||
|
export function generateCreateProjectTcl(
|
||||||
|
projectName: string,
|
||||||
|
projectDir: string,
|
||||||
|
part: string,
|
||||||
|
topModule: string,
|
||||||
|
files: string[],
|
||||||
|
constraints?: string,
|
||||||
|
runSynthesis?: boolean
|
||||||
|
): string {
|
||||||
|
// 转换路径为 TCL 格式(正斜杠)
|
||||||
|
const tclPath = (p: string) => p.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
let tcl = `# 创建 Vivado 工程\n\n`;
|
||||||
|
|
||||||
|
tcl += `create_project ${projectName} {${tclPath(projectDir)}} -part ${part} -force\n\n`;
|
||||||
|
|
||||||
|
// 添加源文件
|
||||||
|
tcl += `# 添加源文件\n`;
|
||||||
|
files.forEach(file => {
|
||||||
|
tcl += `add_files -norecurse {${tclPath(file)}}\n`;
|
||||||
|
});
|
||||||
|
tcl += `\n`;
|
||||||
|
|
||||||
|
// 添加约束文件
|
||||||
|
if (constraints) {
|
||||||
|
tcl += `# 添加约束文件\n`;
|
||||||
|
tcl += `add_files -fileset constrs_1 -norecurse {${tclPath(constraints)}}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置顶层模块
|
||||||
|
tcl += `# 设置顶层模块\n`;
|
||||||
|
tcl += `set_property top ${topModule} [current_fileset]\n\n`;
|
||||||
|
|
||||||
|
if (runSynthesis) {
|
||||||
|
tcl += `# 执行综合\n`;
|
||||||
|
tcl += `launch_runs synth_1\n`;
|
||||||
|
tcl += `wait_on_run synth_1\n\n`;
|
||||||
|
tcl += `# 打开综合结果\n`;
|
||||||
|
tcl += `open_run synth_1\n\n`;
|
||||||
|
tcl += `# 生成报告\n`;
|
||||||
|
tcl += `report_utilization -file {${tclPath(path.join(projectDir, `${projectName}_utilization.rpt`))}}\n`;
|
||||||
|
tcl += `report_timing_summary -file {${tclPath(path.join(projectDir, `${projectName}_timing.rpt`))}}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tcl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成综合的 TCL 脚本
|
||||||
|
*/
|
||||||
|
export function generateSynthesisTcl(
|
||||||
|
projectPath: string | undefined,
|
||||||
|
part: string,
|
||||||
|
topModule: string,
|
||||||
|
files?: string[],
|
||||||
|
constraints?: string,
|
||||||
|
outputDir?: string
|
||||||
|
): string {
|
||||||
|
const tclPath = (p: string) => p.replace(/\\/g, '/');
|
||||||
|
let tcl = `# Vivado 综合\n\n`;
|
||||||
|
|
||||||
|
if (projectPath) {
|
||||||
|
// 使用现有工程
|
||||||
|
tcl += `open_project {${tclPath(projectPath)}}\n\n`;
|
||||||
|
} else {
|
||||||
|
// 无工程模式
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
throw new Error('无工程模式需要提供源文件');
|
||||||
|
}
|
||||||
|
tcl += `# 读取源文件\n`;
|
||||||
|
files.forEach(file => {
|
||||||
|
tcl += `read_verilog {${tclPath(file)}}\n`;
|
||||||
|
});
|
||||||
|
tcl += `\n`;
|
||||||
|
|
||||||
|
if (constraints) {
|
||||||
|
tcl += `read_xdc {${tclPath(constraints)}}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tcl += `# 执行综合\n`;
|
||||||
|
tcl += `synth_design -top ${topModule} -part ${part}\n\n`;
|
||||||
|
|
||||||
|
if (outputDir) {
|
||||||
|
const dcpFile = tclPath(path.join(outputDir, `${topModule}_synth.dcp`));
|
||||||
|
tcl += `# 保存检查点\n`;
|
||||||
|
tcl += `write_checkpoint -force {${dcpFile}}\n\n`;
|
||||||
|
tcl += `# 生成报告\n`;
|
||||||
|
tcl += `report_utilization -file {${tclPath(path.join(outputDir, `${topModule}_utilization.rpt`))}}\n`;
|
||||||
|
tcl += `report_timing_summary -file {${tclPath(path.join(outputDir, `${topModule}_timing.rpt`))}}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tcl;
|
||||||
|
}
|
||||||
87
src/utils/vivadoConfig.ts
Normal file
87
src/utils/vivadoConfig.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Vivado 配置管理
|
||||||
|
* 功能:读取和验证 Vivado 配置
|
||||||
|
* 依赖:vscode
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export interface VivadoConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
executablePath: string;
|
||||||
|
workingDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Vivado 配置
|
||||||
|
*/
|
||||||
|
export function getVivadoConfig(): VivadoConfig | null {
|
||||||
|
// 优先读取项目配置
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (workspaceFolder) {
|
||||||
|
const projectConfigPath = path.join(
|
||||||
|
workspaceFolder.uri.fsPath,
|
||||||
|
'.vscode',
|
||||||
|
'ic-coder-vivado.json'
|
||||||
|
);
|
||||||
|
if (fs.existsSync(projectConfigPath)) {
|
||||||
|
const content = fs.readFileSync(projectConfigPath, 'utf-8');
|
||||||
|
return JSON.parse(content).vivado;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取全局配置
|
||||||
|
const config = vscode.workspace.getConfiguration('ic-coder');
|
||||||
|
const vivadoConfig = config.get<VivadoConfig>('vivado');
|
||||||
|
|
||||||
|
if (vivadoConfig) {
|
||||||
|
return vivadoConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动检测 Vivado
|
||||||
|
return autoDetectVivado();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证配置
|
||||||
|
*/
|
||||||
|
export function validateConfig(config: VivadoConfig): string | null {
|
||||||
|
if (!config.enabled) {
|
||||||
|
return 'Vivado 未启用';
|
||||||
|
}
|
||||||
|
// 如果是完整路径,检查文件是否存在
|
||||||
|
if (path.isAbsolute(config.executablePath) && !fs.existsSync(config.executablePath)) {
|
||||||
|
return `Vivado 可执行文件不存在: ${config.executablePath}`;
|
||||||
|
}
|
||||||
|
// 环境变量命令不检查,运行时会自动报错
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析工作目录
|
||||||
|
*/
|
||||||
|
export function resolveWorkingDir(workingDir: string): string {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (workspaceFolder) {
|
||||||
|
return workingDir.replace('${workspaceFolder}', workspaceFolder.uri.fsPath);
|
||||||
|
}
|
||||||
|
return workingDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动检测 Vivado
|
||||||
|
*/
|
||||||
|
function autoDetectVivado(): VivadoConfig | null {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
|
||||||
|
// 默认使用环境变量中的 vivado 命令
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
executablePath: 'vivado',
|
||||||
|
workingDir: workspaceFolder
|
||||||
|
? path.join(workspaceFolder.uri.fsPath, 'vivado_projects')
|
||||||
|
: path.join(process.env.USERPROFILE || 'C:\\Users\\Default', 'vivado_projects')
|
||||||
|
};
|
||||||
|
}
|
||||||
246
src/utils/vivadoRunner.ts
Normal file
246
src/utils/vivadoRunner.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
/**
|
||||||
|
* Vivado 执行器
|
||||||
|
* 功能:执行 Vivado 命令
|
||||||
|
* 依赖:vivadoConfig, tclGenerator
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { getVivadoConfig, validateConfig, resolveWorkingDir } from './vivadoConfig';
|
||||||
|
import { generateCreateProjectTcl, generateSynthesisTcl } from './tclGenerator';
|
||||||
|
|
||||||
|
export interface VivadoToolResponse {
|
||||||
|
success: boolean;
|
||||||
|
command: string;
|
||||||
|
executionTime: number;
|
||||||
|
output: string;
|
||||||
|
error?: string;
|
||||||
|
outputFiles?: string[];
|
||||||
|
reports?: {
|
||||||
|
resources?: string;
|
||||||
|
timing?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Vivado 工程
|
||||||
|
*/
|
||||||
|
export async function createVivadoProject(params: {
|
||||||
|
projectName: string;
|
||||||
|
part: string;
|
||||||
|
topModule: string;
|
||||||
|
files: string[];
|
||||||
|
constraints?: string;
|
||||||
|
mode: 'gui' | 'batch';
|
||||||
|
runSynthesis?: boolean;
|
||||||
|
}): Promise<VivadoToolResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 读取配置
|
||||||
|
const config = getVivadoConfig();
|
||||||
|
if (!config) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
command: 'create_project',
|
||||||
|
executionTime: 0,
|
||||||
|
output: '',
|
||||||
|
error: 'Vivado 未配置'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证配置
|
||||||
|
const configError = validateConfig(config);
|
||||||
|
if (configError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
command: 'create_project',
|
||||||
|
executionTime: 0,
|
||||||
|
output: '',
|
||||||
|
error: configError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备工作目录
|
||||||
|
const workingDir = resolveWorkingDir(config.workingDir);
|
||||||
|
if (!fs.existsSync(workingDir)) {
|
||||||
|
fs.mkdirSync(workingDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectDir = path.join(workingDir, params.projectName);
|
||||||
|
|
||||||
|
// 生成 TCL 脚本
|
||||||
|
const tclScript = generateCreateProjectTcl(
|
||||||
|
params.projectName,
|
||||||
|
projectDir,
|
||||||
|
params.part,
|
||||||
|
params.topModule,
|
||||||
|
params.files,
|
||||||
|
params.constraints,
|
||||||
|
params.runSynthesis
|
||||||
|
);
|
||||||
|
|
||||||
|
const tclPath = path.join(workingDir, 'create_project.tcl');
|
||||||
|
fs.writeFileSync(tclPath, tclScript);
|
||||||
|
|
||||||
|
// 执行 Vivado
|
||||||
|
const result = await executeVivado(
|
||||||
|
config.executablePath,
|
||||||
|
tclPath,
|
||||||
|
workingDir,
|
||||||
|
params.mode
|
||||||
|
);
|
||||||
|
|
||||||
|
const executionTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
// 查找产出文件
|
||||||
|
const xprFile = path.join(projectDir, `${params.projectName}.xpr`);
|
||||||
|
const outputFiles = fs.existsSync(xprFile) ? [xprFile] : [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
command: 'create_project',
|
||||||
|
executionTime,
|
||||||
|
output: result.output,
|
||||||
|
error: result.error,
|
||||||
|
outputFiles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 Vivado 综合
|
||||||
|
*/
|
||||||
|
export async function runVivadoSynthesis(params: {
|
||||||
|
projectPath?: string;
|
||||||
|
part: string;
|
||||||
|
topModule: string;
|
||||||
|
files?: string[];
|
||||||
|
constraints?: string;
|
||||||
|
mode: 'gui' | 'batch';
|
||||||
|
}): Promise<VivadoToolResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const config = getVivadoConfig();
|
||||||
|
if (!config) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
command: 'synthesis',
|
||||||
|
executionTime: 0,
|
||||||
|
output: '',
|
||||||
|
error: 'Vivado 未配置'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const configError = validateConfig(config);
|
||||||
|
if (configError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
command: 'synthesis',
|
||||||
|
executionTime: 0,
|
||||||
|
output: '',
|
||||||
|
error: configError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const workingDir = resolveWorkingDir(config.workingDir);
|
||||||
|
if (!fs.existsSync(workingDir)) {
|
||||||
|
fs.mkdirSync(workingDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputDir = path.join(workingDir, 'synth_output');
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tclScript = generateSynthesisTcl(
|
||||||
|
params.projectPath,
|
||||||
|
params.part,
|
||||||
|
params.topModule,
|
||||||
|
params.files,
|
||||||
|
params.constraints,
|
||||||
|
outputDir
|
||||||
|
);
|
||||||
|
|
||||||
|
const tclPath = path.join(workingDir, 'synthesis.tcl');
|
||||||
|
fs.writeFileSync(tclPath, tclScript);
|
||||||
|
|
||||||
|
const result = await executeVivado(
|
||||||
|
config.executablePath,
|
||||||
|
tclPath,
|
||||||
|
workingDir,
|
||||||
|
params.mode
|
||||||
|
);
|
||||||
|
|
||||||
|
const executionTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
const dcpFile = path.join(outputDir, `${params.topModule}_synth.dcp`);
|
||||||
|
const utilizationRpt = path.join(outputDir, `${params.topModule}_utilization.rpt`);
|
||||||
|
const timingRpt = path.join(outputDir, `${params.topModule}_timing.rpt`);
|
||||||
|
|
||||||
|
const outputFiles = [];
|
||||||
|
if (fs.existsSync(dcpFile)) outputFiles.push(dcpFile);
|
||||||
|
if (fs.existsSync(utilizationRpt)) outputFiles.push(utilizationRpt);
|
||||||
|
if (fs.existsSync(timingRpt)) outputFiles.push(timingRpt);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
command: 'synthesis',
|
||||||
|
executionTime,
|
||||||
|
output: result.output,
|
||||||
|
error: result.error,
|
||||||
|
outputFiles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 Vivado 命令
|
||||||
|
*/
|
||||||
|
async function executeVivado(
|
||||||
|
executablePath: string,
|
||||||
|
tclPath: string,
|
||||||
|
workingDir: string,
|
||||||
|
mode: 'gui' | 'batch'
|
||||||
|
): Promise<{ success: boolean; output: string; error?: string }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let output = '';
|
||||||
|
let errorOutput = '';
|
||||||
|
|
||||||
|
const args = mode === 'gui'
|
||||||
|
? ['-source', tclPath]
|
||||||
|
: ['-mode', 'batch', '-source', tclPath];
|
||||||
|
|
||||||
|
const process = spawn(executablePath, args, {
|
||||||
|
cwd: workingDir,
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
errorOutput += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ success: true, output });
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
output,
|
||||||
|
error: errorOutput || `执行失败,退出码: ${code}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (err) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
output,
|
||||||
|
error: `启动 Vivado 失败: ${err.message}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -92,7 +92,8 @@ export async function executeWaveformTrace(
|
|||||||
|
|
||||||
child.on('close', (code: number | null) => {
|
child.on('close', (code: number | null) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve(stdout);
|
// 成功时返回 stdout,忽略 stderr 中的进度信息
|
||||||
|
resolve(stdout || stderr);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(
|
reject(new Error(
|
||||||
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`
|
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [
|
localResourceRoots: [
|
||||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
vscode.Uri.joinPath(context.extensionUri, "dist", "assets")
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -47,21 +47,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
// 获取模型图标URI
|
// 获取模型图标URI
|
||||||
const autoIconUri = panel.webview.asWebviewUri(
|
const autoIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Auto.png")
|
||||||
);
|
);
|
||||||
const liteIconUri = panel.webview.asWebviewUri(
|
const liteIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "lite.png")
|
||||||
);
|
);
|
||||||
const syIconUri = panel.webview.asWebviewUri(
|
const syIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Sy.png")
|
||||||
);
|
);
|
||||||
const maxIconUri = panel.webview.asWebviewUri(
|
const maxIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Max.png")
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取二维码图片URI
|
// 获取二维码图片URI
|
||||||
const qrCodeUri = panel.webview.asWebviewUri(
|
const qrCodeUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "QRCode", "wx.png")
|
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "QRCode", "wx.png")
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取Logo URI
|
// 获取Logo URI
|
||||||
@ -129,7 +129,8 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
handleUserAnswer(
|
handleUserAnswer(
|
||||||
message.askId,
|
message.askId,
|
||||||
message.selected,
|
message.selected,
|
||||||
message.customInput
|
message.customInput,
|
||||||
|
message.answers
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
// 新增:中止对话
|
// 新增:中止对话
|
||||||
@ -206,35 +207,46 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolveWebviewView(webviewView: vscode.WebviewView) {
|
resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||||
|
console.log('[ICViewProvider] ========== resolveWebviewView 被调用 ==========');
|
||||||
|
|
||||||
// 保存引用以便后续刷新
|
// 保存引用以便后续刷新
|
||||||
this._view = webviewView;
|
this._view = webviewView;
|
||||||
|
|
||||||
webviewView.webview.options = {
|
webviewView.webview.options = {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
|
localResourceRoots: [
|
||||||
|
vscode.Uri.joinPath(this.extensionUri, "media"),
|
||||||
|
vscode.Uri.joinPath(this.extensionUri, "src", "assets")
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 异步检查 token 是否过期并清除
|
console.log('[ICViewProvider] Webview options 已设置');
|
||||||
vscode.authentication.getSession("iccoder", [], { createIfNone: false })
|
console.log('[ICViewProvider] extensionUri:', this.extensionUri.toString());
|
||||||
.then((session) => {
|
|
||||||
const token = session?.accessToken;
|
|
||||||
if (token && isTokenExpired(token)) {
|
|
||||||
// 静默清除过期的 session
|
|
||||||
this.context.globalState.update('icCoderSessions', []);
|
|
||||||
this.context.globalState.update('icCoderUserInfo', undefined);
|
|
||||||
console.log('[ICViewProvider] Token 已过期,已清除所有登录状态');
|
|
||||||
}
|
|
||||||
}, () => {
|
|
||||||
// 忽略错误
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查是否已登录(使用 Authentication API)
|
// 【关键修复】先设置默认 HTML,避免一直加载
|
||||||
this.checkLoginStatus().then((isLoggedIn) => {
|
try {
|
||||||
webviewView.webview.html = this.getWebviewContent(
|
const html = this.getWebviewContent(webviewView.webview, false);
|
||||||
webviewView.webview,
|
console.log('[ICViewProvider] HTML 内容已生成,长度:', html.length);
|
||||||
isLoggedIn
|
webviewView.webview.html = html;
|
||||||
);
|
console.log('[ICViewProvider] HTML 已设置到 webview');
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error('[ICViewProvider] 设置 HTML 失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步检查登录状态并更新 UI
|
||||||
|
this.checkLoginStatus()
|
||||||
|
.then((isLoggedIn) => {
|
||||||
|
console.log('[ICViewProvider] 登录状态检查完成:', isLoggedIn);
|
||||||
|
webviewView.webview.html = this.getWebviewContent(
|
||||||
|
webviewView.webview,
|
||||||
|
isLoggedIn
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('[ICViewProvider] 检查登录状态失败:', error);
|
||||||
|
// 即使失败也显示未登录状态
|
||||||
|
webviewView.webview.html = this.getWebviewContent(webviewView.webview, false);
|
||||||
|
});
|
||||||
|
|
||||||
// 处理侧边栏的消息
|
// 处理侧边栏的消息
|
||||||
webviewView.webview.onDidReceiveMessage(
|
webviewView.webview.onDidReceiveMessage(
|
||||||
@ -245,7 +257,7 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login");
|
||||||
} else if (message.command === "logout") {
|
} else if (message.command === "logout") {
|
||||||
// 退出登录(前端已有确认对话框)
|
// 退出登录(前端已有确认对话框)
|
||||||
vscode.commands.executeCommand('iccoder.logout');
|
vscode.commands.executeCommand("ic-coder.logout");
|
||||||
} else if (message.command === "openICCoder") {
|
} else if (message.command === "openICCoder") {
|
||||||
// 打开 IC Coder 官网
|
// 打开 IC Coder 官网
|
||||||
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
||||||
@ -265,187 +277,88 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
webview: vscode.Webview,
|
webview: vscode.Webview,
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
): string {
|
): string {
|
||||||
|
console.log('[ICViewProvider] 开始生成 HTML 内容, isLoggedIn:', isLoggedIn);
|
||||||
|
|
||||||
const logoUri = webview.asWebviewUri(
|
const logoUri = webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this.extensionUri, "media", "icon.png")
|
vscode.Uri.joinPath(this.extensionUri, "media", "icon.png")
|
||||||
);
|
);
|
||||||
|
|
||||||
return `
|
return `<!DOCTYPE html>
|
||||||
<!DOCTYPE html>
|
<html>
|
||||||
<html>
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8">
|
||||||
<style>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
body {
|
<style>
|
||||||
margin: 0;
|
body {
|
||||||
padding: 0;
|
margin: 0;
|
||||||
font-family: var(--vscode-font-family);
|
padding: 0;
|
||||||
color: var(--vscode-foreground);
|
font-family: var(--vscode-font-family);
|
||||||
height: 100vh;
|
color: var(--vscode-foreground);
|
||||||
display: flex;
|
height: 100vh;
|
||||||
justify-content: center;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg,
|
align-items: center;
|
||||||
var(--vscode-editor-background) 0%,
|
background: linear-gradient(135deg,
|
||||||
color-mix(in srgb, var(--vscode-editor-background) 85%, var(--vscode-button-background) 15%) 50%,
|
var(--vscode-editor-background) 0%,
|
||||||
color-mix(in srgb, var(--vscode-editor-background) 90%, var(--vscode-button-background) 10%) 100%);
|
color-mix(in srgb, var(--vscode-editor-background) 85%, var(--vscode-button-background) 15%) 50%,
|
||||||
}
|
color-mix(in srgb, var(--vscode-editor-background) 90%, var(--vscode-button-background) 10%) 100%);
|
||||||
.container {
|
}
|
||||||
display: flex;
|
.container {
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
text-align: center;
|
align-items: center;
|
||||||
padding: 20px;
|
text-align: center;
|
||||||
}
|
padding: 20px;
|
||||||
.container img {
|
}
|
||||||
margin-bottom: 16px;
|
.container img {
|
||||||
}
|
margin-bottom: 16px;
|
||||||
.container h2 {
|
}
|
||||||
margin: 0 0 16px 0;
|
.container h2 {
|
||||||
}
|
margin: 0 0 16px 0;
|
||||||
.btn {
|
}
|
||||||
width: 200px;
|
.btn {
|
||||||
padding: 8px 12px;
|
width: 200px;
|
||||||
margin: 4px 0;
|
padding: 8px 12px;
|
||||||
background: var(--vscode-button-background);
|
margin: 4px 0;
|
||||||
color: var(--vscode-button-foreground);
|
background: #007ACC;
|
||||||
border: none;
|
color: #ffffff;
|
||||||
border-radius: 4px;
|
border: none;
|
||||||
cursor: pointer;
|
border-radius: 4px;
|
||||||
text-align: center;
|
cursor: pointer;
|
||||||
}
|
text-align: center;
|
||||||
.btn:hover {
|
}
|
||||||
background: var(--vscode-button-hoverBackground);
|
.btn:hover {
|
||||||
}
|
background: #005a9e;
|
||||||
h3 {
|
}
|
||||||
margin: 0 0 8px 0;
|
</style>
|
||||||
font-size: 12px;
|
</head>
|
||||||
color: var(--vscode-descriptionForeground);
|
<body>
|
||||||
}
|
<div class="container">
|
||||||
</style>
|
<img src="${logoUri}" alt="IC Coder" width="120" />
|
||||||
</head>
|
<h2>欢迎使用 IC Coder</h2>
|
||||||
<body>
|
${isLoggedIn
|
||||||
<div class="container">
|
? '<button class="btn" onclick="openChat()">开始创作</button>'
|
||||||
<img src="${logoUri}" alt="IC Coder" width="120" />
|
: '<button class="btn" onclick="login()">登录账户</button>'
|
||||||
<h2>欢迎使用 IC Coder</h2>
|
}
|
||||||
${
|
</div>
|
||||||
isLoggedIn
|
<script>
|
||||||
? '<button class="btn" onclick="openChat()">开始创作</button>'
|
console.log('[Webview] 脚本已加载');
|
||||||
: '<button class="btn" onclick="login()">登录账户</button>'
|
const vscode = acquireVsCodeApi();
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
function openChat() {
|
||||||
const vscode = acquireVsCodeApi();
|
console.log('[Webview] 点击开始创作');
|
||||||
|
vscode.postMessage({ command: 'openChat' });
|
||||||
|
}
|
||||||
|
|
||||||
function openChat() {
|
function login() {
|
||||||
vscode.postMessage({ command: 'openChat' });
|
console.log('[Webview] 点击登录');
|
||||||
}
|
vscode.postMessage({ command: 'login' });
|
||||||
|
}
|
||||||
|
|
||||||
// 登录功能
|
console.log('[Webview] 初始化完成');
|
||||||
function login() {
|
</script>
|
||||||
vscode.postMessage({ command: 'login' });
|
</body>
|
||||||
}
|
</html>`;
|
||||||
|
|
||||||
function generateCode(type) {
|
|
||||||
const code = getCodeTemplate(type);
|
|
||||||
vscode.postMessage({
|
|
||||||
command: 'insertCode',
|
|
||||||
code: code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCodeTemplate(type) {
|
|
||||||
const templates = {
|
|
||||||
counter: \`module counter #(
|
|
||||||
parameter WIDTH = 4
|
|
||||||
)(
|
|
||||||
input wire clk,
|
|
||||||
input wire rst_n,
|
|
||||||
input wire enable,
|
|
||||||
output reg [WIDTH-1:0] count
|
|
||||||
);
|
|
||||||
always @(posedge clk or negedge rst_n) begin
|
|
||||||
if (!rst_n) begin
|
|
||||||
count <= 0;
|
|
||||||
end else if (enable) begin
|
|
||||||
count <= count + 1;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
endmodule\`,
|
|
||||||
fsm: \`module fsm (
|
|
||||||
input wire clk,
|
|
||||||
input wire rst_n,
|
|
||||||
input wire start,
|
|
||||||
output reg done
|
|
||||||
);
|
|
||||||
parameter IDLE = 2'b00;
|
|
||||||
parameter STATE1 = 2'b01;
|
|
||||||
parameter STATE2 = 2'b10;
|
|
||||||
|
|
||||||
reg [1:0] state, next_state;
|
|
||||||
|
|
||||||
always @(posedge clk or negedge rst_n) begin
|
|
||||||
if (!rst_n) begin
|
|
||||||
state <= IDLE;
|
|
||||||
end else begin
|
|
||||||
state <= next_state;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
always @(*) begin
|
|
||||||
case (state)
|
|
||||||
IDLE: next_state = start ? STATE1 : IDLE;
|
|
||||||
STATE1: next_state = STATE2;
|
|
||||||
STATE2: next_state = IDLE;
|
|
||||||
default: next_state = IDLE;
|
|
||||||
endcase
|
|
||||||
end
|
|
||||||
|
|
||||||
assign done = (state == STATE2);
|
|
||||||
endmodule\`,
|
|
||||||
fifo: \`module sync_fifo #(
|
|
||||||
parameter DATA_WIDTH = 8,
|
|
||||||
parameter DEPTH = 16
|
|
||||||
)(
|
|
||||||
input wire clk,
|
|
||||||
input wire rst_n,
|
|
||||||
input wire wr_en,
|
|
||||||
input wire [DATA_WIDTH-1:0] din,
|
|
||||||
input wire rd_en,
|
|
||||||
output reg [DATA_WIDTH-1:0] dout,
|
|
||||||
output wire full,
|
|
||||||
output wire empty
|
|
||||||
);
|
|
||||||
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
|
|
||||||
reg [$clog2(DEPTH):0] wr_ptr, rd_ptr;
|
|
||||||
|
|
||||||
assign full = (wr_ptr == rd_ptr + DEPTH);
|
|
||||||
assign empty = (wr_ptr == rd_ptr);
|
|
||||||
|
|
||||||
always @(posedge clk) begin
|
|
||||||
if (!rst_n) wr_ptr <= 0;
|
|
||||||
else if (wr_en && !full) begin
|
|
||||||
mem[wr_ptr] <= din;
|
|
||||||
wr_ptr <= wr_ptr + 1;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
always @(posedge clk) begin
|
|
||||||
if (!rst_n) begin
|
|
||||||
rd_ptr <= 0;
|
|
||||||
dout <= 0;
|
|
||||||
end else if (rd_en && !empty) begin
|
|
||||||
dout <= mem[rd_ptr];
|
|
||||||
rd_ptr <= rd_ptr + 1;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
endmodule\`
|
|
||||||
};
|
|
||||||
return templates[type] || '// 代码模板';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export function getAgentCardStyles(): string {
|
|||||||
.agent-name {
|
.agent-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
font-size:14px
|
||||||
}
|
}
|
||||||
.agent-status {
|
.agent-status {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -99,14 +100,14 @@ export function getAgentCardStyles(): string {
|
|||||||
/* 低调显示的工具调用样式 */
|
/* 低调显示的工具调用样式 */
|
||||||
.agent-step.low-profile {
|
.agent-step.low-profile {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.agent-step.low-profile .step-icon {
|
.agent-step.low-profile .step-icon {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.agent-step.low-profile .step-name {
|
.agent-step.low-profile .step-name {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -115,7 +116,7 @@ export function getAgentCardStyles(): string {
|
|||||||
}
|
}
|
||||||
.agent-step.low-profile .step-result {
|
.agent-step.low-profile .step-result {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
489
src/views/changePanel.ts
Normal file
489
src/views/changePanel.ts
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
/**
|
||||||
|
* 代码变更面板组件
|
||||||
|
* 功能:显示 AI 修改的文件列表和 diff 对比
|
||||||
|
* 依赖:utils/diffRenderer
|
||||||
|
* 使用场景:对话结束后展示代码变更供用户审查
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getDiffStyles } from "../utils/diffRenderer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取变更面板的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getChangePanelContent(): string {
|
||||||
|
return `
|
||||||
|
<div class="change-panel" id="changePanel" style="display: none;">
|
||||||
|
<div class="change-panel-header" onclick="toggleChangePanel()">
|
||||||
|
<div class="change-panel-title">
|
||||||
|
<span>代码变更</span>
|
||||||
|
<span class="change-count" id="changeCount">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="change-panel-actions" onclick="event.stopPropagation()">
|
||||||
|
<button class="batch-action-btn accept-all-btn" onclick="acceptAllChanges()" title="采纳全部">
|
||||||
|
<span>✓ 全部采纳</span>
|
||||||
|
</button>
|
||||||
|
<button class="batch-action-btn reject-all-btn" onclick="rejectAllChanges()" title="拒绝全部">
|
||||||
|
<span>✕ 全部拒绝</span>
|
||||||
|
</button>
|
||||||
|
<button class="change-toggle-btn" id="changePanelToggle">
|
||||||
|
<span class="toggle-icon">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="change-panel-body" id="changePanelBody" style="display: none;">
|
||||||
|
<div class="change-list" id="changeList">
|
||||||
|
<!-- 变更列表将动态插入 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取变更面板的样式
|
||||||
|
*/
|
||||||
|
export function getChangePanelStyles(): string {
|
||||||
|
return `
|
||||||
|
.change-panel {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
user-select: none;
|
||||||
|
border-bottom: 2px solid var(--vscode-panel-border);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-header:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-count {
|
||||||
|
background: var(--vscode-badge-background);
|
||||||
|
color: var(--vscode-badge-foreground);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-action-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-action-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-all-btn {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-all-btn:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-all-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-all-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-toggle-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-toggle-btn:hover {
|
||||||
|
background: var(--vscode-toolbar-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-body {
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item {
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item:hover {
|
||||||
|
border-color: var(--vscode-focusBorder);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-header:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-type-badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-type-create {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-type-modify {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-type-delete {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-file-path {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-action-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-action-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-btn {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-btn:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-diff {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
display: none;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-diff.expanded {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
${getDiffStyles()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取变更面板的脚本
|
||||||
|
*/
|
||||||
|
export function getChangePanelScript(): string {
|
||||||
|
return `
|
||||||
|
// 切换变更面板展开/收起
|
||||||
|
function toggleChangePanel() {
|
||||||
|
const body = document.getElementById('changePanelBody');
|
||||||
|
const toggleIcon = document.querySelector('.toggle-icon');
|
||||||
|
|
||||||
|
if (body.style.display === 'none') {
|
||||||
|
body.style.display = 'block';
|
||||||
|
toggleIcon.classList.add('expanded');
|
||||||
|
} else {
|
||||||
|
body.style.display = 'none';
|
||||||
|
toggleIcon.classList.remove('expanded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全部采纳
|
||||||
|
window.acceptAllChanges = function() {
|
||||||
|
const changeList = document.getElementById('changeList');
|
||||||
|
if (!changeList) {
|
||||||
|
console.error('changeList not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.from(changeList.querySelectorAll('.change-item'));
|
||||||
|
console.log('Found items:', items.length);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
alert('没有待处理的变更');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const changeId = item.id.replace('change-item-', '');
|
||||||
|
console.log('Accepting change:', changeId);
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'acceptChange',
|
||||||
|
changeId: changeId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全部拒绝
|
||||||
|
window.rejectAllChanges = function() {
|
||||||
|
const changeList = document.getElementById('changeList');
|
||||||
|
if (!changeList) {
|
||||||
|
console.error('changeList not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.from(changeList.querySelectorAll('.change-item'));
|
||||||
|
console.log('Found items:', items.length);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
alert('没有待处理的变更');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const changeId = item.id.replace('change-item-', '');
|
||||||
|
console.log('Rejecting change:', changeId);
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'rejectChange',
|
||||||
|
changeId: changeId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开文件 diff(在 VS Code 中打开)
|
||||||
|
function openFileDiff(changeId) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openFileDiff',
|
||||||
|
changeId: changeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 采纳变更
|
||||||
|
function acceptChange(changeId) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'acceptChange',
|
||||||
|
changeId: changeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拒绝变更
|
||||||
|
function rejectChange(changeId) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'rejectChange',
|
||||||
|
changeId: changeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示变更面板(从后端接收变更列表)
|
||||||
|
window.showChangesPanel = function(changes) {
|
||||||
|
const changePanel = document.getElementById('changePanel');
|
||||||
|
const changeList = document.getElementById('changeList');
|
||||||
|
const changeCount = document.getElementById('changeCount');
|
||||||
|
|
||||||
|
if (!changePanel || !changeList || !changeCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新变更数量
|
||||||
|
changeCount.textContent = changes.length;
|
||||||
|
|
||||||
|
// 清空现有列表
|
||||||
|
changeList.innerHTML = '';
|
||||||
|
|
||||||
|
// 渲染每个变更项
|
||||||
|
changes.forEach(change => {
|
||||||
|
const changeItem = createChangeItem(change);
|
||||||
|
changeList.appendChild(changeItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示面板
|
||||||
|
changePanel.style.display = 'block';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建单个变更项的 DOM 元素
|
||||||
|
function createChangeItem(change) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'change-item';
|
||||||
|
item.id = 'change-item-' + change.changeId;
|
||||||
|
|
||||||
|
const typeLabel = change.changeType === 'create' ? '新建' :
|
||||||
|
change.changeType === 'modify' ? '修改' : '删除';
|
||||||
|
|
||||||
|
item.innerHTML = \`
|
||||||
|
<div class="change-item-header" onclick="openFileDiff('\${change.changeId}')">
|
||||||
|
<div class="change-item-info">
|
||||||
|
<span class="change-type-badge change-type-\${change.changeType}">\${typeLabel}</span>
|
||||||
|
<span class="change-file-path">\${change.filePath}</span>
|
||||||
|
</div>
|
||||||
|
<div class="change-item-actions">
|
||||||
|
<button class="change-action-btn accept-btn" onclick="event.stopPropagation(); acceptChange('\${change.changeId}')">采纳</button>
|
||||||
|
<button class="change-action-btn reject-btn" onclick="event.stopPropagation(); rejectChange('\${change.changeId}')">拒绝</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理采纳变更的响应
|
||||||
|
window.handleChangeAccepted = function(changeId, success, error) {
|
||||||
|
if (success) {
|
||||||
|
// 从列表中移除该变更项
|
||||||
|
const item = document.getElementById('change-item-' + changeId);
|
||||||
|
if (item) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新变更数量
|
||||||
|
updateChangeCount();
|
||||||
|
} else {
|
||||||
|
console.error('采纳变更失败:', error);
|
||||||
|
alert('采纳变更失败: ' + (error || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理拒绝变更的响应
|
||||||
|
window.handleChangeRejected = function(changeId, success, error) {
|
||||||
|
if (success) {
|
||||||
|
// 从列表中移除该变更项
|
||||||
|
const item = document.getElementById('change-item-' + changeId);
|
||||||
|
if (item) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新变更数量
|
||||||
|
updateChangeCount();
|
||||||
|
} else {
|
||||||
|
console.error('拒绝变更失败:', error);
|
||||||
|
alert('拒绝变更失败: ' + (error || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新变更数量
|
||||||
|
function updateChangeCount() {
|
||||||
|
const changeList = document.getElementById('changeList');
|
||||||
|
const changeCount = document.getElementById('changeCount');
|
||||||
|
const changePanel = document.getElementById('changePanel');
|
||||||
|
|
||||||
|
if (changeList && changeCount) {
|
||||||
|
const count = changeList.children.length;
|
||||||
|
changeCount.textContent = count;
|
||||||
|
|
||||||
|
// 如果没有变更了,隐藏面板
|
||||||
|
if (count === 0 && changePanel) {
|
||||||
|
changePanel.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -16,7 +16,7 @@ export function getContextButtonContent(): string {
|
|||||||
<span class="add-context-label">添加上下文</span>
|
<span class="add-context-label">添加上下文</span>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
<span class="tooltiptext">添加文件、文件夹作为上下文</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 上拉菜单 -->
|
<!-- 上拉菜单 -->
|
||||||
@ -41,18 +41,18 @@ export function getContextButtonContent(): string {
|
|||||||
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" onclick="handleAddImage()">
|
<!-- <div class="context-menu-item" onclick="handleAddImage()">
|
||||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
<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"/>
|
<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>
|
</svg>
|
||||||
<span>图片</span>
|
<span>图片</span>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="context-menu-item" onclick="handleAddDocument()">
|
<!-- <div class="context-menu-item" onclick="handleAddDocument()">
|
||||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
<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"/>
|
<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>
|
</svg>
|
||||||
<span>文档库</span>
|
<span>文档库</span>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件/文件夹列表视图 -->
|
<!-- 文件/文件夹列表视图 -->
|
||||||
@ -271,6 +271,7 @@ export function getContextButtonStyles(): string {
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-list-item label {
|
.context-menu-list-item label {
|
||||||
@ -335,6 +336,7 @@ export function getContextButtonScript(): string {
|
|||||||
return `
|
return `
|
||||||
// 上下文菜单状态
|
// 上下文菜单状态
|
||||||
let currentListData = [];
|
let currentListData = [];
|
||||||
|
let filteredListData = [];
|
||||||
let currentListType = '';
|
let currentListType = '';
|
||||||
let selectedItems = new Set();
|
let selectedItems = new Set();
|
||||||
|
|
||||||
@ -392,6 +394,15 @@ export function getContextButtonScript(): string {
|
|||||||
|
|
||||||
selectedItems.clear();
|
selectedItems.clear();
|
||||||
currentListData = [];
|
currentListData = [];
|
||||||
|
filteredListData = [];
|
||||||
|
clearContextSearchInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearContextSearchInput() {
|
||||||
|
const searchInput = document.getElementById('contextMenuSearch');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换到列表视图
|
// 切换到列表视图
|
||||||
@ -406,10 +417,12 @@ export function getContextButtonScript(): string {
|
|||||||
titleEl.textContent = title;
|
titleEl.textContent = title;
|
||||||
|
|
||||||
currentListType = type;
|
currentListType = type;
|
||||||
currentListData = data;
|
currentListData = data || [];
|
||||||
|
filteredListData = currentListData;
|
||||||
selectedItems.clear();
|
selectedItems.clear();
|
||||||
|
|
||||||
renderList(data);
|
clearContextSearchInput();
|
||||||
|
renderList(filteredListData);
|
||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,32 +432,36 @@ export function getContextButtonScript(): string {
|
|||||||
const body = document.getElementById('contextMenuListBody');
|
const body = document.getElementById('contextMenuListBody');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
body.innerHTML = data.map((item, index) => \`
|
filteredListData = data || [];
|
||||||
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
|
|
||||||
<input type="checkbox" id="item-\${index}" />
|
body.innerHTML = filteredListData.map((item, index) => \`
|
||||||
<label for="item-\${index}">\${item.relativePath}</label>
|
<div class="context-menu-list-item \${selectedItems.has(item.path) ? 'selected' : ''}" onclick="toggleItemSelection(\${index})">
|
||||||
|
<input type="checkbox" id="item-\${index}" \${selectedItems.has(item.path) ? 'checked' : ''} />
|
||||||
|
<label>\${item.relativePath || item.path}</label>
|
||||||
</div>
|
</div>
|
||||||
\`).join('');
|
\`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换项选择
|
// 切换项选择
|
||||||
function toggleItemSelection(index) {
|
function toggleItemSelection(index) {
|
||||||
|
const selectedItem = filteredListData[index];
|
||||||
|
if (!selectedItem) return;
|
||||||
|
|
||||||
|
const selectedPath = selectedItem.path;
|
||||||
const checkbox = document.getElementById('item-' + index);
|
const checkbox = document.getElementById('item-' + index);
|
||||||
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
||||||
|
|
||||||
if (checkbox && item) {
|
if (selectedItems.has(selectedPath)) {
|
||||||
checkbox.checked = !checkbox.checked;
|
selectedItems.delete(selectedPath);
|
||||||
|
if (checkbox) checkbox.checked = false;
|
||||||
if (checkbox.checked) {
|
if (item) item.classList.remove('selected');
|
||||||
selectedItems.add(index);
|
} else {
|
||||||
item.classList.add('selected');
|
selectedItems.add(selectedPath);
|
||||||
} else {
|
if (checkbox) checkbox.checked = true;
|
||||||
selectedItems.delete(index);
|
if (item) item.classList.add('selected');
|
||||||
item.classList.remove('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSelectedCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSelectedCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新选中数量
|
// 更新选中数量
|
||||||
@ -457,15 +474,25 @@ export function getContextButtonScript(): string {
|
|||||||
|
|
||||||
// 确认选择
|
// 确认选择
|
||||||
function confirmSelection() {
|
function confirmSelection() {
|
||||||
const selected = Array.from(selectedItems).map(index => currentListData[index]);
|
try {
|
||||||
|
const selected = currentListData.filter(item => selectedItems.has(item.path));
|
||||||
|
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
selected.forEach(item => {
|
selected.forEach(item => {
|
||||||
addContextItem(currentListType, item.path);
|
addContextItem(currentListType, item.path, item.relativePath || item.path);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const menu = document.getElementById('contextMenu');
|
||||||
|
const button = document.querySelector('.add-context-button');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.remove('show');
|
||||||
|
}
|
||||||
|
if (button) {
|
||||||
|
button.classList.remove('active');
|
||||||
|
}
|
||||||
|
backToMainMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleContextMenu();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加图片
|
// 添加图片
|
||||||
@ -484,9 +511,9 @@ export function getContextButtonScript(): string {
|
|||||||
const searchInput = document.getElementById('contextMenuSearch');
|
const searchInput = document.getElementById('contextMenuSearch');
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.addEventListener('input', function(e) {
|
searchInput.addEventListener('input', function(e) {
|
||||||
const keyword = e.target.value.toLowerCase();
|
const keyword = (e.target.value || '').toLowerCase().trim();
|
||||||
const filtered = currentListData.filter(item =>
|
const filtered = currentListData.filter(item =>
|
||||||
item.relativePath.toLowerCase().includes(keyword)
|
(item.relativePath || item.path || '').toLowerCase().includes(keyword)
|
||||||
);
|
);
|
||||||
renderList(filtered);
|
renderList(filtered);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -51,7 +51,11 @@ export function getContextDisplayStyles(): string {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-item:hover {
|
.context-item.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item.clickable:hover {
|
||||||
background: var(--vscode-list-hoverBackground);
|
background: var(--vscode-list-hoverBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +130,11 @@ export function getContextDisplayScript(): string {
|
|||||||
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>';
|
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 getCodeIcon() {
|
||||||
|
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z m-484.7-122.1l39.6-39.5 113.1 113.1-39.6 39.5-113.1-113.1z m226.4-290.2l113.1 113.1-39.6 39.5-113.1-113.1 39.6-39.5z" fill="currentColor"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
// 获取删除图标 SVG
|
// 获取删除图标 SVG
|
||||||
function getRemoveIcon() {
|
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>';
|
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>';
|
||||||
@ -137,9 +146,12 @@ export function getContextDisplayScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加上下文项
|
// 添加上下文项
|
||||||
function addContextItem(type, path) {
|
function addContextItem(type, path, displayPath) {
|
||||||
|
const exists = contextItems.some(item => item.type === type && item.path === path);
|
||||||
|
if (exists) return;
|
||||||
|
|
||||||
const id = Date.now() + Math.random();
|
const id = Date.now() + Math.random();
|
||||||
contextItems.push({ id, type, path });
|
contextItems.push({ id, type, path, displayPath: displayPath || '' });
|
||||||
renderContextItems();
|
renderContextItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,13 +181,17 @@ export function getContextDisplayScript(): string {
|
|||||||
case 'folder': icon = getFolderIcon(); break;
|
case 'folder': icon = getFolderIcon(); break;
|
||||||
case 'image': icon = getImageIcon(); break;
|
case 'image': icon = getImageIcon(); break;
|
||||||
case 'document': icon = getDocumentIcon(); break;
|
case 'document': icon = getDocumentIcon(); break;
|
||||||
|
case 'code': icon = getCodeIcon(); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clickable = item.type !== 'folder' ? 'clickable' : '';
|
||||||
|
const onclick = item.type !== 'folder' ? \`onclick="window.handleContextItemClick(\${item.id})"\` : '';
|
||||||
|
|
||||||
return \`
|
return \`
|
||||||
<div class="context-item" title="\${item.path}">
|
<div class="context-item \${clickable}" title="\${item.path || item.displayPath}" \${onclick}>
|
||||||
\${icon}
|
\${icon}
|
||||||
<span class="context-item-name">\${getFileName(item.path)}</span>
|
<span class="context-item-name">\${item.displayPath || getFileName(item.path)}</span>
|
||||||
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
<span class="context-item-remove" onclick="event.stopPropagation(); removeContextItem(\${item.id})">
|
||||||
\${getRemoveIcon()}
|
\${getRemoveIcon()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -183,6 +199,27 @@ export function getContextDisplayScript(): string {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全局访问函数
|
||||||
|
window.handleContextItemClick = function(id) {
|
||||||
|
const item = contextItems.find(i => i.id === id);
|
||||||
|
if (!item || item.type === 'folder') return;
|
||||||
|
|
||||||
|
if (item.type === 'code') {
|
||||||
|
const codeData = JSON.parse(item.path);
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openFileWithSelection',
|
||||||
|
filePath: codeData.fileName,
|
||||||
|
startLine: codeData.startLine,
|
||||||
|
endLine: codeData.endLine
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openFile',
|
||||||
|
filePath: item.path
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 处理后端返回的文件选择结果
|
// 处理后端返回的文件选择结果
|
||||||
window.addEventListener('message', event => {
|
window.addEventListener('message', event => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
@ -208,6 +245,18 @@ export function getContextDisplayScript(): string {
|
|||||||
message.documents.forEach(doc => addContextItem('document', doc));
|
message.documents.forEach(doc => addContextItem('document', doc));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'addCodeContext':
|
||||||
|
// 添加代码上下文
|
||||||
|
const displayName = \`\${message.fileName.split(/[\\\\/]/).pop()}:\${message.startLine}-\${message.endLine}\`;
|
||||||
|
const codeData = {
|
||||||
|
fileName: message.fileName,
|
||||||
|
startLine: message.startLine,
|
||||||
|
endLine: message.endLine,
|
||||||
|
code: message.code,
|
||||||
|
languageId: message.languageId
|
||||||
|
};
|
||||||
|
addContextItem('code', JSON.stringify(codeData), displayName);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -330,7 +330,7 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.new-conversation-button:hover {
|
.new-conversation-button:hover {
|
||||||
background: var(--vscode-toolbar-hoverBackground);
|
background: #007ACC;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,14 @@
|
|||||||
export function getExampleShowcaseContent(): string {
|
export function getExampleShowcaseContent(): string {
|
||||||
return `
|
return `
|
||||||
<div class="example-showcase" id="exampleShowcase">
|
<div class="example-showcase" id="exampleShowcase">
|
||||||
<div class="showcase-title">示例</div>
|
<div class="showcase-header">
|
||||||
|
<div class="showcase-title">示例</div>
|
||||||
|
<button class="refresh-button" onclick="refreshExamples()" title="换一批">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M21.5 2V8M21.5 8H15.5M21.5 8L18 4.5C16.7429 3.24286 15.1767 2.35596 13.4606 1.93597C11.7446 1.51598 9.94736 1.57986 8.26381 2.12059C6.58027 2.66131 5.07831 3.65985 3.91872 4.99987C2.75913 6.33989 1.98648 7.96902 1.68 9.71M2.5 22V16M2.5 16H8.5M2.5 16L6 19.5C7.25714 20.7571 8.82331 21.644 10.5394 22.064C12.2554 22.484 14.0526 22.4201 15.7362 21.8794C17.4197 21.3387 18.9217 20.3401 20.0813 19.0001C21.2409 17.6601 22.0135 16.031 22.32 14.29" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="example-cards">
|
<div class="example-cards">
|
||||||
<div class="example-card" onclick="sendExample(0)">
|
<div class="example-card" onclick="sendExample(0)">
|
||||||
<div class="example-icon">
|
<div class="example-icon">
|
||||||
@ -62,12 +69,44 @@ export function getExampleShowcaseStyles(): string {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.showcase-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.showcase-title {
|
.showcase-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
margin-bottom: 12px;
|
}
|
||||||
text-align: left;
|
|
||||||
|
.refresh-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button svg {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:active svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.example-cards {
|
.example-cards {
|
||||||
@ -220,15 +259,74 @@ export function getExampleShowcaseStyles(): string {
|
|||||||
*/
|
*/
|
||||||
export function getExampleShowcaseScript(): string {
|
export function getExampleShowcaseScript(): string {
|
||||||
return `
|
return `
|
||||||
// 示例文本数组
|
// 所有可用的示例
|
||||||
const exampleTexts = [
|
const allExamples = [
|
||||||
'生成一个SPI控制器',
|
'设计一个算术逻辑单元,完成常见运算',
|
||||||
'生成一个GMII接口的以太网UDP通信模块'
|
'实现一个优先编码器,多个输入同时有效时,只输出优先级最高的那个编号',
|
||||||
|
'实现一个译码器,把二进制编号转换成 one-hot 输出',
|
||||||
|
'实现一个移位寄存器,完成串行/并行数据移位与装载',
|
||||||
|
'实现一个按键消抖模块,解决机械按键抖动问题',
|
||||||
|
'实现一个跑马灯控制器,控制 LED 形成不同流动效果',
|
||||||
|
'实现一个序列检测器,检测串行输入中是否出现指定比特序列',
|
||||||
|
'实现一个LFSR 伪随机数发生器',
|
||||||
|
'实现一个自动售货机,模拟一个简单售货逻辑',
|
||||||
|
'实现一个交通灯控制器,控制两方向交通灯的切换',
|
||||||
|
'实现一个先进先出的数据缓冲区',
|
||||||
|
'单端口 RAM 读写控制器',
|
||||||
|
'实现一个移位加法乘法器,不用 * 运算符'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 当前显示的示例文本
|
||||||
|
let exampleTexts = ['生成一个SPI控制器', '生成一个GMII接口的以太网UDP通信模块'];
|
||||||
|
|
||||||
// 存储待发送的示例索引
|
// 存储待发送的示例索引
|
||||||
let pendingExampleIndex = -1;
|
let pendingExampleIndex = -1;
|
||||||
|
|
||||||
|
// 节流控制
|
||||||
|
let refreshing = false;
|
||||||
|
|
||||||
|
// 刷新示例
|
||||||
|
function refreshExamples() {
|
||||||
|
if (refreshing) return;
|
||||||
|
refreshing = true;
|
||||||
|
|
||||||
|
const used = new Set();
|
||||||
|
const newExamples = [];
|
||||||
|
while (newExamples.length < 2) {
|
||||||
|
const idx = Math.floor(Math.random() * allExamples.length);
|
||||||
|
if (!used.has(idx)) {
|
||||||
|
used.add(idx);
|
||||||
|
newExamples.push(allExamples[idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exampleTexts = newExamples;
|
||||||
|
updateExampleCards();
|
||||||
|
|
||||||
|
setTimeout(() => { refreshing = false; }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新示例卡片显示
|
||||||
|
function updateExampleCards() {
|
||||||
|
const container = document.querySelector('.example-cards');
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = exampleTexts.map((text, i) => \`
|
||||||
|
<div class="example-card" onclick="sendExample(\${i})">
|
||||||
|
<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">\${text}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
// 直接发送示例消息
|
// 直接发送示例消息
|
||||||
function sendExample(index) {
|
function sendExample(index) {
|
||||||
// 先检查邀请码验证状态
|
// 先检查邀请码验证状态
|
||||||
|
|||||||
218
src/views/expiredModal.ts
Normal file
218
src/views/expiredModal.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* 试用期过期弹窗
|
||||||
|
* 功能:在聊天面板内显示过期提醒模态窗口
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:试用用户过期时在聊天面板内显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过期弹窗的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getExpiredModalContent(logoUri?: string): string {
|
||||||
|
return `
|
||||||
|
<!-- 过期弹窗 -->
|
||||||
|
<div id="expiredModal" class="expired-modal" style="display: none;">
|
||||||
|
<div class="expired-modal-overlay"></div>
|
||||||
|
<div class="expired-modal-content">
|
||||||
|
${logoUri ? `<img src="${logoUri}" class="expired-logo-corner" alt="IC Coder" />` : ""}
|
||||||
|
|
||||||
|
<div class="expired-modal-header">
|
||||||
|
<div class="expired-icon">⏰</div>
|
||||||
|
<h2>您的试用期已到期</h2>
|
||||||
|
<p class="expired-modal-subtitle">感谢您使用 IC Coder!您的 15 天试用期已结束。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="expired-modal-body">
|
||||||
|
<p class="expired-message">如需继续使用,请联系我们获取正式版本。</p>
|
||||||
|
|
||||||
|
<button id="expiredContactBtn" class="expired-btn expired-btn-primary">
|
||||||
|
<span>联系我们</span>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过期弹窗的 CSS 样式
|
||||||
|
*/
|
||||||
|
export function getExpiredModalStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 过期弹窗样式 */
|
||||||
|
.expired-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-widget-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-logo-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 24px;
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-modal-header {
|
||||||
|
padding: 60px 32px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-modal-header h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-errorForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-modal-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-modal-body {
|
||||||
|
padding: 0 32px 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-message {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin: 20px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-btn:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过期弹窗的 JavaScript 逻辑
|
||||||
|
*/
|
||||||
|
export function getExpiredModalScript(): string {
|
||||||
|
return `
|
||||||
|
// 过期弹窗逻辑
|
||||||
|
(function() {
|
||||||
|
const modal = document.getElementById('expiredModal');
|
||||||
|
const contactBtn = document.getElementById('expiredContactBtn');
|
||||||
|
const overlay = modal?.querySelector('.expired-modal-overlay');
|
||||||
|
|
||||||
|
// 显示过期弹窗
|
||||||
|
window.showExpiredModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏过期弹窗
|
||||||
|
window.hideExpiredModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击"联系我们"按钮
|
||||||
|
if (contactBtn) {
|
||||||
|
contactBtn.addEventListener('click', function() {
|
||||||
|
// 可以打开联系页面
|
||||||
|
// window.open('https://iccoder.com/contact', '_blank');
|
||||||
|
hideExpiredModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击遮罩层关闭弹窗
|
||||||
|
if (overlay) {
|
||||||
|
overlay.addEventListener('click', function() {
|
||||||
|
hideExpiredModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止点击弹窗内容时关闭
|
||||||
|
const content = modal?.querySelector('.expired-modal-content');
|
||||||
|
if (content) {
|
||||||
|
content.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自后端的消息
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.command === 'showExpiredModal') {
|
||||||
|
showExpiredModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
73
src/views/filePathTag.ts
Normal file
73
src/views/filePathTag.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 文件路径标签组件
|
||||||
|
* 功能:显示可点击的文件路径标签
|
||||||
|
* 使用场景:在用户消息中显示上下文文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件路径标签的样式
|
||||||
|
*/
|
||||||
|
export function getFilePathTagStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 文件路径标签 */
|
||||||
|
.file-path-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-right: 6px;
|
||||||
|
background: rgba(0, 122, 204, 0.15);
|
||||||
|
border: 1px solid rgba(0, 122, 204, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path-tag:hover {
|
||||||
|
background: rgba(0, 122, 204, 0.25);
|
||||||
|
border-color: rgba(0, 122, 204, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path-tag svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件路径标签的脚本
|
||||||
|
*/
|
||||||
|
export function getFilePathTagScript(): string {
|
||||||
|
return `
|
||||||
|
// 处理文件路径标签点击
|
||||||
|
function handleFilePathClick(filePath) {
|
||||||
|
// 解析文件路径,支持 file.v:5-8 格式
|
||||||
|
const match = filePath.match(/^(.+?):(\\d+)-(\\d+)$/);
|
||||||
|
if (match) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openFilePathTag',
|
||||||
|
filePath: match[1],
|
||||||
|
startLine: parseInt(match[2]),
|
||||||
|
endLine: parseInt(match[3])
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openFilePathTag',
|
||||||
|
filePath: filePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文件路径标签
|
||||||
|
window.createFilePathTag = function(filePath) {
|
||||||
|
const fileIcon = '<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>';
|
||||||
|
const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
|
||||||
|
return '<span class="file-path-tag" onclick="handleFilePathClick(\\'' + escapedPath + '\\')">' + fileIcon + filePath + '</span>';
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -24,6 +24,10 @@ import {
|
|||||||
getContextCompressStyles,
|
getContextCompressStyles,
|
||||||
getContextCompressScript,
|
getContextCompressScript,
|
||||||
} from "./contextCompress";
|
} from "./contextCompress";
|
||||||
|
import {
|
||||||
|
getFilePathTagStyles,
|
||||||
|
getFilePathTagScript,
|
||||||
|
} from "./filePathTag";
|
||||||
import {
|
import {
|
||||||
getOptimizeButtonContent,
|
getOptimizeButtonContent,
|
||||||
getOptimizeButtonStyles,
|
getOptimizeButtonStyles,
|
||||||
@ -34,6 +38,11 @@ import {
|
|||||||
getExampleShowcaseStyles,
|
getExampleShowcaseStyles,
|
||||||
getExampleShowcaseScript,
|
getExampleShowcaseScript,
|
||||||
} from "./exampleShowcase";
|
} from "./exampleShowcase";
|
||||||
|
import {
|
||||||
|
getChangePanelContent,
|
||||||
|
getChangePanelStyles,
|
||||||
|
getChangePanelScript,
|
||||||
|
} from "./changePanel";
|
||||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,6 +58,8 @@ export function getInputAreaContent(
|
|||||||
<div class="input-area centered" id="inputArea">
|
<div class="input-area centered" id="inputArea">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
|
<!-- 代码变更面板 -->
|
||||||
|
${getChangePanelContent()}
|
||||||
<!-- 顶部工具栏 -->
|
<!-- 顶部工具栏 -->
|
||||||
<div class="input-top-toolbar">
|
<div class="input-top-toolbar">
|
||||||
${getContextButtonContent()}
|
${getContextButtonContent()}
|
||||||
@ -91,9 +102,11 @@ export function getInputAreaStyles(): string {
|
|||||||
${getModelSelectorStyles()}
|
${getModelSelectorStyles()}
|
||||||
${getContextButtonStyles()}
|
${getContextButtonStyles()}
|
||||||
${getContextDisplayStyles()}
|
${getContextDisplayStyles()}
|
||||||
|
${getFilePathTagStyles()}
|
||||||
${getContextCompressStyles()}
|
${getContextCompressStyles()}
|
||||||
${getOptimizeButtonStyles()}
|
${getOptimizeButtonStyles()}
|
||||||
${getExampleShowcaseStyles()}
|
${getExampleShowcaseStyles()}
|
||||||
|
${getChangePanelStyles()}
|
||||||
.input-area {
|
.input-area {
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
@ -296,10 +309,12 @@ export function getInputAreaScript(): string {
|
|||||||
return `
|
return `
|
||||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||||
${getModelSelectorScript()}
|
${getModelSelectorScript()}
|
||||||
${getContextButtonScript()}
|
|
||||||
${getContextDisplayScript()}
|
${getContextDisplayScript()}
|
||||||
|
${getContextButtonScript()}
|
||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
${getOptimizeButtonScript()}
|
${getOptimizeButtonScript()}
|
||||||
|
${getChangePanelScript()}
|
||||||
|
${getFilePathTagScript()}
|
||||||
|
|
||||||
// 对话状态管理
|
// 对话状态管理
|
||||||
let isConversationActive = false;
|
let isConversationActive = false;
|
||||||
@ -339,12 +354,14 @@ export function getInputAreaScript(): string {
|
|||||||
if (messageInput) {
|
if (messageInput) {
|
||||||
messageInput.addEventListener('input', autoResizeTextarea);
|
messageInput.addEventListener('input', autoResizeTextarea);
|
||||||
|
|
||||||
// 监听点击事件,检测工作区状态和邀请码验证状态
|
// 监听点击事件,检测工作区状态、试用期过期和邀请码验证状态
|
||||||
messageInput.addEventListener('focus', () => {
|
messageInput.addEventListener('focus', () => {
|
||||||
if (!hasCheckedWorkspace) {
|
if (!hasCheckedWorkspace) {
|
||||||
hasCheckedWorkspace = true;
|
hasCheckedWorkspace = true;
|
||||||
vscode.postMessage({ command: 'checkWorkspace' });
|
vscode.postMessage({ command: 'checkWorkspace' });
|
||||||
}
|
}
|
||||||
|
// 检查试用期是否过期
|
||||||
|
vscode.postMessage({ command: 'checkTrialExpiration' });
|
||||||
// 检查邀请码验证状态
|
// 检查邀请码验证状态
|
||||||
vscode.postMessage({ command: 'checkInvitationCode' });
|
vscode.postMessage({ command: 'checkInvitationCode' });
|
||||||
});
|
});
|
||||||
@ -415,7 +432,21 @@ export function getInputAreaScript(): string {
|
|||||||
// 获取上下文项
|
// 获取上下文项
|
||||||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||||
|
|
||||||
addMessage(text, 'user');
|
// 构建显示消息:如果有上下文项,添加路径前缀
|
||||||
|
let displayText = text;
|
||||||
|
if (contextItems.length > 0) {
|
||||||
|
const contextPaths = contextItems
|
||||||
|
.map(item => item.displayPath || item.path)
|
||||||
|
.join(' ');
|
||||||
|
if (contextPaths) {
|
||||||
|
displayText = contextPaths + ' ' + text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(displayText, 'user');
|
||||||
|
|
||||||
|
// 重置分段消息容器,强制下次创建新容器
|
||||||
|
currentSegmentedMessage = null;
|
||||||
|
|
||||||
// 标记已有消息,切换布局到底部
|
// 标记已有消息,切换布局到底部
|
||||||
hasMessages = true;
|
hasMessages = true;
|
||||||
@ -436,6 +467,11 @@ export function getInputAreaScript(): string {
|
|||||||
autoResizeTextarea(); // 重置输入框高度
|
autoResizeTextarea(); // 重置输入框高度
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
|
|
||||||
|
// 清空上下文项
|
||||||
|
if (window.clearContextItems) {
|
||||||
|
window.clearContextItems();
|
||||||
|
}
|
||||||
|
|
||||||
// 重置优化状态
|
// 重置优化状态
|
||||||
resetOptimizeButton();
|
resetOptimizeButton();
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
220
src/views/messageRenderer.ts
Normal file
220
src/views/messageRenderer.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* 消息渲染脚本模块
|
||||||
|
* 功能:消息渲染、滚动控制、工具状态显示
|
||||||
|
* 依赖:toolHelpers, textFormatter, waveformPreviewContent, agentCard, planCard, codeHighlight
|
||||||
|
* 使用场景:webview 中的消息显示逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { collapseIconSvg } from "../constants/toolIcons";
|
||||||
|
import { getWaveformPreviewScript } from "./waveformPreviewContent";
|
||||||
|
import { getAgentCardScript } from "./agentCard";
|
||||||
|
import { getPlanCardScript } from "./planCard";
|
||||||
|
import { getCodeHighlightScript } from "../components/codeHighlight";
|
||||||
|
|
||||||
|
export function getMessageRendererScript(): string {
|
||||||
|
return `
|
||||||
|
${getAgentCardScript()}
|
||||||
|
${getPlanCardScript()}
|
||||||
|
|
||||||
|
const toolCollapseStates = new Map();
|
||||||
|
let shouldAutoScroll = true;
|
||||||
|
let lastScrollHeight = 0;
|
||||||
|
|
||||||
|
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 (shouldAutoScroll) {
|
||||||
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
lastScrollHeight = messagesEl.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(text, sender) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = \`message \${sender}-message\`;
|
||||||
|
if (sender === 'bot') {
|
||||||
|
const actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'message-actions';
|
||||||
|
const messageContent = document.createElement('span');
|
||||||
|
messageContent.textContent = text;
|
||||||
|
const copyBtn = document.createElement('button');
|
||||||
|
copyBtn.className = 'action-btn';
|
||||||
|
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 = () => copyMessage(text, 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(messageContent);
|
||||||
|
actionsDiv.appendChild(copyBtn);
|
||||||
|
actionsDiv.appendChild(likeBtn);
|
||||||
|
actionsDiv.appendChild(dislikeBtn);
|
||||||
|
div.appendChild(actionsDiv);
|
||||||
|
} else {
|
||||||
|
const parts = text.split(' ');
|
||||||
|
const filePaths = [];
|
||||||
|
const textParts = [];
|
||||||
|
parts.forEach(part => {
|
||||||
|
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) {
|
||||||
|
filePaths.push(part);
|
||||||
|
} else {
|
||||||
|
textParts.push(part);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (filePaths.length > 0) {
|
||||||
|
div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
|
||||||
|
} else {
|
||||||
|
div.textContent = text;
|
||||||
|
}
|
||||||
|
hideHeaderIfNeeded();
|
||||||
|
}
|
||||||
|
messagesEl.appendChild(div);
|
||||||
|
smartScrollToBottom();
|
||||||
|
checkHeaderVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideHeaderIfNeeded() {
|
||||||
|
checkHeaderVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyMessage(text, button) {
|
||||||
|
// 从按钮的父消息元素中获取实际文本内容
|
||||||
|
const messageDiv = button.closest('.message');
|
||||||
|
const messageContent = messageDiv?.querySelector('.message-content, div:not(.message-actions)');
|
||||||
|
const textToCopy = messageContent ? messageContent.textContent : text;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||||
|
const originalHTML = button.innerHTML;
|
||||||
|
button.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1 0.4-12.8-6.3-12.8z" fill="currentColor"/></svg>\`;
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalHTML;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLike(button) {
|
||||||
|
const isActive = button.classList.contains('active');
|
||||||
|
const parent = button.parentElement;
|
||||||
|
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
if (!isActive) {
|
||||||
|
button.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDislike(button) {
|
||||||
|
const isActive = button.classList.contains('active');
|
||||||
|
const parent = button.parentElement;
|
||||||
|
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
if (!isActive) {
|
||||||
|
button.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOrCreateStreamingMessage(text) {
|
||||||
|
hideLoadingIndicator();
|
||||||
|
if (!currentStreamingMessage) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'message bot-message streaming';
|
||||||
|
const messageContent = document.createElement('div');
|
||||||
|
messageContent.className = 'message-content';
|
||||||
|
messageContent.textContent = text;
|
||||||
|
div.appendChild(messageContent);
|
||||||
|
messagesEl.appendChild(div);
|
||||||
|
currentStreamingMessage = div;
|
||||||
|
} else {
|
||||||
|
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||||
|
if (messageContent) {
|
||||||
|
messageContent.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
smartScrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeStreamingMessage(finalText) {
|
||||||
|
if (currentStreamingMessage) {
|
||||||
|
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||||
|
if (messageContent) {
|
||||||
|
messageContent.textContent = finalText;
|
||||||
|
}
|
||||||
|
currentStreamingMessage.classList.remove('streaming');
|
||||||
|
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.onclick = () => copyMessage(finalText, copyBtn);
|
||||||
|
actionsDiv.appendChild(copyBtn);
|
||||||
|
currentStreamingMessage.appendChild(actionsDiv);
|
||||||
|
currentStreamingMessage = null;
|
||||||
|
}
|
||||||
|
smartScrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoadingIndicator(text) {
|
||||||
|
hideLoadingIndicator();
|
||||||
|
loadingIndicator = document.createElement('div');
|
||||||
|
loadingIndicator.className = 'message bot-message loading-message';
|
||||||
|
loadingIndicator.innerHTML = \`
|
||||||
|
<div class="loading-dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<span class="loading-text">\${text}</span>
|
||||||
|
\`;
|
||||||
|
messagesEl.appendChild(loadingIndicator);
|
||||||
|
smartScrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoadingIndicator() {
|
||||||
|
if (loadingIndicator) {
|
||||||
|
loadingIndicator.remove();
|
||||||
|
loadingIndicator = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToolStatus(toolName, status, detail) {
|
||||||
|
const statusIcons = {
|
||||||
|
start: '🔧',
|
||||||
|
complete: '✅',
|
||||||
|
error: '❌'
|
||||||
|
};
|
||||||
|
const statusTexts = {
|
||||||
|
start: '正在执行',
|
||||||
|
complete: '执行完成',
|
||||||
|
error: '执行失败'
|
||||||
|
};
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = \`message tool-status tool-\${status}\`;
|
||||||
|
div.innerHTML = \`
|
||||||
|
<span class="tool-icon">\${statusIcons[status]}</span>
|
||||||
|
<span class="tool-name">\${getToolDisplayName(toolName)}</span>
|
||||||
|
<span class="tool-status-text">\${statusTexts[status]}</span>
|
||||||
|
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
||||||
|
\`;
|
||||||
|
messagesEl.appendChild(div);
|
||||||
|
smartScrollToBottom();
|
||||||
|
checkHeaderVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
${getWaveformPreviewScript()}
|
||||||
|
${getCodeHighlightScript()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
632
src/views/messageStyles.ts
Normal file
632
src/views/messageStyles.ts
Normal file
@ -0,0 +1,632 @@
|
|||||||
|
/**
|
||||||
|
* 消息样式模块
|
||||||
|
* 功能:提供消息区域的所有 CSS 样式
|
||||||
|
* 依赖:agentCard, planCard, codeHighlight, waveformPreviewContent
|
||||||
|
* 使用场景:webview 样式注入
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getAgentCardStyles } from "./agentCard";
|
||||||
|
import { getPlanCardStyles } from "./planCard";
|
||||||
|
import { getCodeHighlightStyles } from "../components/codeHighlight";
|
||||||
|
import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||||||
|
|
||||||
|
export function getMessageAreaStyles(): string {
|
||||||
|
return `
|
||||||
|
.messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.user-message {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
margin-left: auto;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
.bot-message {
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
max-width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.message-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: 12px;
|
||||||
|
opacity: 0.85;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
vertical-align: middle;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.message-actions > span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.message-actions:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.action-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.action-btn svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
.action-btn.active {
|
||||||
|
color: var(--vscode-button-background);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.action-btn .action-tooltip {
|
||||||
|
visibility: hidden;
|
||||||
|
width: auto;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 4px 8px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
bottom: 125%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(5px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 12px;white-space: nowrap;pointer-events: none;
|
||||||
|
}
|
||||||
|
.action-btn .action-tooltip::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #1e1e1e transparent transparent transparent;
|
||||||
|
}
|
||||||
|
.action-btn .action-tooltip::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-width: 6px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.action-btn:hover .action-tooltip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming .message-content {
|
||||||
|
border-right: 2px solid var(--vscode-focusBorder);
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 50% { border-color: var(--vscode-focusBorder); }
|
||||||
|
51%, 100% { border-color: transparent; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.loading-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;}
|
||||||
|
.loading-dots span {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--vscode-focusBorder);
|
||||||
|
animation: loadingDot 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.loading-dots span:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
@keyframes loadingDot {
|
||||||
|
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||||
|
40% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
.loading-text {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vscode-textBlockQuote-background);
|
||||||
|
}
|
||||||
|
.tool-status.tool-start {
|
||||||
|
border-left: 3px solid var(--vscode-charts-blue);
|
||||||
|
}
|
||||||
|
.tool-status.tool-complete {
|
||||||
|
border-left: 3px solid var(--vscode-charts-green);
|
||||||
|
}
|
||||||
|
.tool-status.tool-error {
|
||||||
|
border-left: 3px solid var(--vscode-charts-red);
|
||||||
|
}
|
||||||
|
.tool-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.tool-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
.tool-status-text {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.tool-detail {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-message {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.question-text {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.question-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.question-option {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #007ACC;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #007ACC;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.question-option:hover {
|
||||||
|
background: #005a9e;
|
||||||
|
border-color: #005a9e;
|
||||||
|
}
|
||||||
|
.question-option.selected {
|
||||||
|
background: #007ACC;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #007ACC;
|
||||||
|
}
|
||||||
|
.question-message.answered .question-option:not(.selected) {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.custom-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.custom-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-input-foreground);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.custom-submit {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.custom-submit:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
.question-message.answered .custom-input-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-message {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.message-segment {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.segment-text {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-text h1,
|
||||||
|
.segment-text h2,
|
||||||
|
.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,
|
||||||
|
.question-text h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
.segment-text h2,
|
||||||
|
.question-text h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
.segment-text h3,
|
||||||
|
.question-text h3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.segment-text ul,
|
||||||
|
.segment-text ol,
|
||||||
|
.question-text ul,
|
||||||
|
.question-text ol {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
.segment-text li,
|
||||||
|
.question-text li {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.segment-text strong,
|
||||||
|
.question-text strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
.segment-text em,
|
||||||
|
.question-text em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.segment-text a,
|
||||||
|
.question-text a {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.segment-text a:hover,
|
||||||
|
.question-text a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.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: 25px 0px;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.tool-segment-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tool-segment-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.tool-segment-name {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.tool-segment-result {
|
||||||
|
display: inline;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin-left: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.tool-collapse-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tool-collapse-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.icon-expanded svg path {
|
||||||
|
fill: #007ACC !important;
|
||||||
|
}
|
||||||
|
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
.tool-file-write-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-file-write-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
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;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-syntax-check-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tool-search-code-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-search-code-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.tool-segment-content.collapsed {
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
.tool-segment-description {
|
||||||
|
margin: 25px 0 0 0px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.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: 12px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.segment-question {
|
||||||
|
background: var(--vscode-textBlockQuote-background);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 12px 35px;
|
||||||
|
border-left: 3px solid var(--vscode-charts-orange);
|
||||||
|
}
|
||||||
|
.segment-question .question-text {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.segment-question .question-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.segment-question .question-option {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #3d3f41;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: 1px solid #474747;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.segment-question .question-option:hover {
|
||||||
|
background: #005a9e;
|
||||||
|
border-color: #005a9e;
|
||||||
|
}
|
||||||
|
.segment-question .question-option.selected {
|
||||||
|
background: #007ACC;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #007ACC;
|
||||||
|
}
|
||||||
|
.segment-question.answered .question-option:not(.selected) {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.segment-question .custom-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.segment-question .custom-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-input-foreground);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-left: -20px;
|
||||||
|
}
|
||||||
|
.segment-question .custom-submit {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.segment-question .custom-submit:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
.segment-question.answered .custom-input-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.question-segment .question-text {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.question-segment .question-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.question-opt {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
color: var(--vscode-button-secondaryForeground);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;}
|
||||||
|
|
||||||
|
${getAgentCardStyles()}
|
||||||
|
|
||||||
|
${getPlanCardStyles()}
|
||||||
|
|
||||||
|
${getCodeHighlightStyles()}
|
||||||
|
|
||||||
|
${getWaveformPreviewContent()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
329
src/views/ndtWelcomeModal.ts
Normal file
329
src/views/ndtWelcomeModal.ts
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
/**
|
||||||
|
* 宁德时代欢迎弹窗
|
||||||
|
* 功能:邀请码验证成功后显示欢迎信息
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:宁德时代用户首次验证邀请码成功后显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取宁德时代欢迎弹窗的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getNdtWelcomeModalContent(logoUri?: string): string {
|
||||||
|
return `
|
||||||
|
<!-- 宁德时代欢迎弹窗 -->
|
||||||
|
<div id="ndtWelcomeModal" class="ndt-welcome-modal" style="display: none;">
|
||||||
|
<div class="ndt-welcome-modal-overlay"></div>
|
||||||
|
<div class="ndt-welcome-modal-content">
|
||||||
|
${logoUri ? `<img src="${logoUri}" class="ndt-welcome-logo-corner" alt="IC Coder" />` : ""}
|
||||||
|
|
||||||
|
<div class="ndt-welcome-modal-header">
|
||||||
|
<div class="ndt-welcome-icon">🎉</div>
|
||||||
|
<h2>欢迎企业<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder!</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ndt-welcome-modal-body">
|
||||||
|
<!-- 试用期提示 -->
|
||||||
|
<div class="ndt-trial-banner">
|
||||||
|
<span>您已获得 <strong>5 天企业版试用期</strong>,企业版试用期内Credits用量无限,并可无限制使用所有功能</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IC Coder 简介 -->
|
||||||
|
<div class="ndt-intro-section">
|
||||||
|
<h3 class="ndt-section-title">关于 IC Coder</h3>
|
||||||
|
<p class="ndt-intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform(自主式人工智能 Verilog 编码平台)。我们采用全球顶尖的IC Coder自研芯片设计微调模型,为代码生成提供强大的AI能力支撑。</p>
|
||||||
|
|
||||||
|
<div class="ndt-features">
|
||||||
|
<div class="ndt-feature-item">
|
||||||
|
<span class="ndt-feature-text">多智能体架构(Multi-Agent System):多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节</span>
|
||||||
|
</div>
|
||||||
|
<div class="ndt-feature-item">
|
||||||
|
<span class="ndt-feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
|
||||||
|
</div>
|
||||||
|
<div class="ndt-feature-item">
|
||||||
|
<span class="ndt-feature-text">AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 按钮组 -->
|
||||||
|
<div class="ndt-button-group">
|
||||||
|
<button id="ndtTutorialBtn" class="ndt-welcome-btn ndt-welcome-btn-secondary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M8 2C4.7 2 2 4.7 2 8s2.7 6 6 6 6-2.7 6-6-2.7-6-6-6zm0 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z" fill="currentColor"/>
|
||||||
|
<path d="M8 5v3l2 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>查看使用教程</span>
|
||||||
|
</button>
|
||||||
|
<button id="ndtWelcomeStartBtn" class="ndt-welcome-btn ndt-welcome-btn-primary">
|
||||||
|
<span>开始使用</span>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取宁德时代欢迎弹窗的 CSS 样式
|
||||||
|
*/
|
||||||
|
export function getNdtWelcomeModalStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 宁德时代欢迎弹窗样式 */
|
||||||
|
.ndt-welcome-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-widget-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-logo-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 24px;
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-header {
|
||||||
|
padding: 60px 32px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-header h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-body {
|
||||||
|
padding: 0 32px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 试用期横幅 */
|
||||||
|
.ndt-trial-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
border-left: 3px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-trial-banner strong {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IC Coder 简介区域 */
|
||||||
|
.ndt-intro-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-section-title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-intro-text {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-features {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-feature-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border-left: 2px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-feature-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮组 */
|
||||||
|
.ndt-button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn-primary {
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn-primary:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn-secondary {
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
color: var(--vscode-button-secondaryForeground);
|
||||||
|
border: 1px solid var(--vscode-button-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn-secondary:hover {
|
||||||
|
background: var(--vscode-button-secondaryHoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取宁德时代欢迎弹窗的 JavaScript 逻辑
|
||||||
|
*/
|
||||||
|
export function getNdtWelcomeModalScript(): string {
|
||||||
|
return `
|
||||||
|
// 宁德时代欢迎弹窗逻辑
|
||||||
|
(function() {
|
||||||
|
const modal = document.getElementById('ndtWelcomeModal');
|
||||||
|
const startBtn = document.getElementById('ndtWelcomeStartBtn');
|
||||||
|
const tutorialBtn = document.getElementById('ndtTutorialBtn');
|
||||||
|
const overlay = modal?.querySelector('.ndt-welcome-modal-overlay');
|
||||||
|
|
||||||
|
// 显示宁德时代欢迎弹窗
|
||||||
|
window.showNdtWelcomeModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏宁德时代欢迎弹窗
|
||||||
|
window.hideNdtWelcomeModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击"查看使用教程"按钮
|
||||||
|
if (tutorialBtn) {
|
||||||
|
tutorialBtn.addEventListener('click', function() {
|
||||||
|
// 打开使用教程链接
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openTutorial'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击"开始使用"按钮
|
||||||
|
if (startBtn) {
|
||||||
|
startBtn.addEventListener('click', function() {
|
||||||
|
hideNdtWelcomeModal();
|
||||||
|
// 通知后端用户已查看欢迎弹窗
|
||||||
|
vscode.postMessage({ command: 'ndtWelcomeModalViewed' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击遮罩层关闭弹窗
|
||||||
|
if (overlay) {
|
||||||
|
overlay.addEventListener('click', function() {
|
||||||
|
hideNdtWelcomeModal();
|
||||||
|
vscode.postMessage({ command: 'ndtWelcomeModalViewed' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止点击弹窗内容时关闭
|
||||||
|
const content = modal?.querySelector('.ndt-welcome-modal-content');
|
||||||
|
if (content) {
|
||||||
|
content.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自后端的消息
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.command === 'showNdtWelcomeModal') {
|
||||||
|
showNdtWelcomeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -59,7 +59,7 @@ export function getPlanCardStyles(): string {
|
|||||||
.plan-summary h2 { font-size: 16px; }
|
.plan-summary h2 { font-size: 16px; }
|
||||||
.plan-summary h3 { font-size: 14px; }
|
.plan-summary h3 { font-size: 14px; }
|
||||||
.plan-summary h4 { font-size: 13px; }
|
.plan-summary h4 { font-size: 13px; }
|
||||||
.plan-summary p { margin: 8px 0; }
|
.plan-summary p { margin: 8px 0; letter-spacing: 0.5px; }
|
||||||
.plan-summary ul, .plan-summary ol {
|
.plan-summary ul, .plan-summary ol {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
@ -195,11 +195,11 @@ export function getPlanCardStyles(): string {
|
|||||||
background: var(--vscode-list-hoverBackground);
|
background: var(--vscode-list-hoverBackground);
|
||||||
}
|
}
|
||||||
.plan-btn-confirm {
|
.plan-btn-confirm {
|
||||||
background: var(--vscode-button-background);
|
background: #007ACC;
|
||||||
color: var(--vscode-button-foreground);
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
.plan-btn-confirm:hover {
|
.plan-btn-confirm:hover {
|
||||||
background: var(--vscode-button-hoverBackground);
|
background: #005a9e;
|
||||||
}
|
}
|
||||||
.plan-btn-cancel {
|
.plan-btn-cancel {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -720,7 +720,7 @@ export function getPlanCardScript(): string {
|
|||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="plan-card">
|
<div class="plan-card">
|
||||||
<div class="plan-header">
|
<div class="plan-header">
|
||||||
<span class="plan-icon">📋</span>
|
<!-- <span class="plan-icon">📋</span> -->
|
||||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||||
</div>
|
</div>
|
||||||
\${progressHtml}
|
\${progressHtml}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export function getProgressBarContent(): string {
|
|||||||
<span class="step-number">1</span>
|
<span class="step-number">1</span>
|
||||||
<span class="step-check">✓</span>
|
<span class="step-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-label">Spec</div>
|
<div class="step-label">Specification</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="progress-line"></div>
|
<div class="progress-line"></div>
|
||||||
@ -186,8 +186,8 @@ export function getProgressBarStyles(): string {
|
|||||||
|
|
||||||
/* 已完成状态 */
|
/* 已完成状态 */
|
||||||
.progress-step.completed .step-circle {
|
.progress-step.completed .step-circle {
|
||||||
background: var(--vscode-button-background);
|
background: #007ACC;
|
||||||
border-color: var(--vscode-button-background);
|
border-color: #007ACC;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-step.completed .step-number {
|
.progress-step.completed .step-number {
|
||||||
@ -204,14 +204,14 @@ export function getProgressBarStyles(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-step.completed + .progress-line {
|
.progress-step.completed + .progress-line {
|
||||||
background: var(--vscode-button-background);
|
background: #007ACC;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进行中状态 */
|
/* 进行中状态 */
|
||||||
.progress-step.active .step-circle {
|
.progress-step.active .step-circle {
|
||||||
background: var(--vscode-button-background);
|
background: #007ACC;
|
||||||
border-color: var(--vscode-button-background);
|
border-color: #007ACC;
|
||||||
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
box-shadow: 0 0 0 2px #007ACC33;
|
||||||
animation: pulse 2s infinite;
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,10 +226,10 @@ export function getProgressBarStyles(): string {
|
|||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
box-shadow: 0 0 0 2px #007ACC33;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 0 4px var(--vscode-button-background)1a;
|
box-shadow: 0 0 0 4px #007ACC1a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +351,7 @@ export function getProgressBarScript(): string {
|
|||||||
// 更新连接线
|
// 更新连接线
|
||||||
document.querySelectorAll('.progress-line').forEach((line, index) => {
|
document.querySelectorAll('.progress-line').forEach((line, index) => {
|
||||||
if (index < currentIndex) {
|
if (index < currentIndex) {
|
||||||
line.style.background = 'var(--vscode-button-background)';
|
line.style.background = '#007ACC';
|
||||||
} else {
|
} else {
|
||||||
line.style.background = 'var(--vscode-input-border)';
|
line.style.background = 'var(--vscode-input-border)';
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/views/questionHandler.ts
Normal file
118
src/views/questionHandler.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* 问题处理脚本模块
|
||||||
|
* 功能:用户问题交互逻辑
|
||||||
|
* 依赖:textFormatter
|
||||||
|
* 使用场景:webview 中的问题回答处理
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getQuestionHandlerScript(): string {
|
||||||
|
return `
|
||||||
|
const answeredQuestions = new Map();
|
||||||
|
|
||||||
|
function handleQuestionAnswer(askId, answer, questionDiv) {
|
||||||
|
console.log('[WebView] 用户选择答案:', askId, answer);
|
||||||
|
questionDiv.classList.add('answered');
|
||||||
|
const options = questionDiv.querySelectorAll('.question-option');
|
||||||
|
options.forEach(opt => {
|
||||||
|
if (opt.textContent === answer) {
|
||||||
|
opt.classList.add('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'submitAnswer',
|
||||||
|
askId: askId,
|
||||||
|
selected: [answer],
|
||||||
|
customInput: answer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuestionAnswerInSegment(askId, answer, segmentDiv) {
|
||||||
|
console.log('[WebView] 段落中用户选择答案:', askId, answer);
|
||||||
|
answeredQuestions.set(askId, answer);
|
||||||
|
segmentDiv.classList.add('answered');
|
||||||
|
const options = segmentDiv.querySelectorAll('.question-option');
|
||||||
|
options.forEach(opt => {
|
||||||
|
if (opt.getAttribute('data-option') === answer) {
|
||||||
|
opt.classList.add('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const customContainer = segmentDiv.querySelector('.custom-input-container');
|
||||||
|
if (customContainer) {
|
||||||
|
customContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'submitAnswer',
|
||||||
|
askId: askId,
|
||||||
|
selected: [answer],
|
||||||
|
customInput: answer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
|
||||||
|
console.log('[WebView] 多问题答案提交:', askId, answers);
|
||||||
|
answeredQuestions.set(askId, answers);
|
||||||
|
segmentDiv.classList.add('answered');
|
||||||
|
const inputs = segmentDiv.querySelectorAll('input');
|
||||||
|
inputs.forEach(input => {
|
||||||
|
input.disabled = true;
|
||||||
|
if (input.checked) {
|
||||||
|
const label = input.closest('.question-option');
|
||||||
|
if (label) {
|
||||||
|
label.classList.add('selected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'submitAnswer',
|
||||||
|
askId: askId,
|
||||||
|
answers: answers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showQuestion(askId, question, options) {
|
||||||
|
console.log('[WebView] showQuestion 被调用:', askId, question, options);
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'message bot-message question-message';
|
||||||
|
div.setAttribute('data-ask-id', askId);
|
||||||
|
const questionText = document.createElement('div');
|
||||||
|
questionText.className = 'question-text';
|
||||||
|
questionText.textContent = question;
|
||||||
|
div.appendChild(questionText);
|
||||||
|
const optionsContainer = document.createElement('div');
|
||||||
|
optionsContainer.className = 'question-options';
|
||||||
|
options.forEach((option, index) => {
|
||||||
|
const optionBtn = document.createElement('button');
|
||||||
|
optionBtn.className = 'question-option';
|
||||||
|
optionBtn.textContent = option;
|
||||||
|
optionBtn.onclick = () => handleQuestionAnswer(askId, option, div);
|
||||||
|
optionsContainer.appendChild(optionBtn);
|
||||||
|
});
|
||||||
|
div.appendChild(optionsContainer);
|
||||||
|
const customContainer = document.createElement('div');
|
||||||
|
customContainer.className = 'custom-input-container';
|
||||||
|
const customInput = document.createElement('input');
|
||||||
|
customInput.type = 'text';
|
||||||
|
customInput.className = 'custom-input';
|
||||||
|
customInput.placeholder = '输入其他答案...';
|
||||||
|
const customSubmit = document.createElement('button');
|
||||||
|
customSubmit.className = 'custom-submit';
|
||||||
|
customSubmit.textContent = '提交';
|
||||||
|
customSubmit.onclick = () => {
|
||||||
|
const customValue = customInput.value.trim();
|
||||||
|
if (customValue) {
|
||||||
|
handleQuestionAnswer(askId, customValue, div);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
customContainer.appendChild(customInput);
|
||||||
|
customContainer.appendChild(customSubmit);
|
||||||
|
div.appendChild(customContainer);
|
||||||
|
messagesEl.appendChild(div);
|
||||||
|
smartScrollToBottom();
|
||||||
|
checkHeaderVisibility();
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
272
src/views/segmentRenderer.ts
Normal file
272
src/views/segmentRenderer.ts
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
* 分段消息渲染脚本模块
|
||||||
|
* 功能:实时更新分段消息、工具调用展示
|
||||||
|
* 依赖:toolHelpers, textFormatter, waveformPreviewContent
|
||||||
|
* 使用场景:webview 中的分段消息渲染
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getSegmentRendererScript(): string {
|
||||||
|
return `
|
||||||
|
function updateSegmentsRealtime(segments, isComplete) {
|
||||||
|
if (!isComplete && (!segments || segments.length === 0)) return;
|
||||||
|
|
||||||
|
if (!currentSegmentedMessage) {
|
||||||
|
if (currentStreamingMessage) {
|
||||||
|
currentStreamingMessage.remove();
|
||||||
|
currentStreamingMessage = null;
|
||||||
|
}
|
||||||
|
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||||||
|
toolStatuses.forEach(el => el.remove());
|
||||||
|
const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
|
||||||
|
if (lastSegmented && !lastSegmented.querySelector('.message-actions')) {
|
||||||
|
currentSegmentedMessage = lastSegmented;
|
||||||
|
} else {
|
||||||
|
currentSegmentedMessage = document.createElement('div');
|
||||||
|
currentSegmentedMessage.className = 'message bot-message segmented-message';
|
||||||
|
messagesEl.appendChild(currentSegmentedMessage);
|
||||||
|
}
|
||||||
|
renderedSegmentCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isComplete) {
|
||||||
|
currentSegmentedMessage.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedSegments = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < (segments?.length || 0)) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (segment.type === 'text' && segment.content) {
|
||||||
|
segmentDiv.className += ' segment-text';
|
||||||
|
segmentDiv.innerHTML = formatText(segment.content);
|
||||||
|
} else if (segment.type === 'tool') {
|
||||||
|
if (segment.toolName === 'spawnExplorer') return;
|
||||||
|
segmentDiv.className += ' low-profile';
|
||||||
|
const toolResult = segment.toolResult || '';
|
||||||
|
const toolCount = segment.toolCount || 1;
|
||||||
|
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||||
|
const toolDescription = segment.toolDescription || '';
|
||||||
|
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\${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\${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>\` : ''}
|
||||||
|
\${toolDescription ? \`<p class="tool-segment-description">\${toolDescription}</p>\` : ''}
|
||||||
|
\`;
|
||||||
|
|
||||||
|
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||||
|
if (typeof createWaveformPreview === 'function') {
|
||||||
|
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||||
|
if (vcdPaths.length > 0) {
|
||||||
|
vcdPaths.forEach(vcdInfo => {
|
||||||
|
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||||
|
segmentDiv.appendChild(waveformPreview);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let vcdPath = segment.vcdFilePath;
|
||||||
|
if (!vcdPath && segment.toolResult) {
|
||||||
|
const match = String(segment.toolResult).match(/(?:路径\\s*[::]\\s*|已生成[::]\\s*)(.+\\.vcd)/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
vcdPath = match[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vcdPath) {
|
||||||
|
const fileName = segment.fileName || vcdPath.split(/[\\\\\\/]/).pop() || 'waveform.vcd';
|
||||||
|
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||||
|
segmentDiv.appendChild(waveformPreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[VCD Preview] createWaveformPreview function not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCollapse) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||||
|
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||||
|
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';
|
||||||
|
toolCollapseStates.set(toolIdx, false);
|
||||||
|
} else {
|
||||||
|
header.classList.add('collapsed');
|
||||||
|
content.classList.add('collapsed');
|
||||||
|
content.style.maxHeight = '0';
|
||||||
|
toolCollapseStates.set(toolIdx, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
} else if (segment.type === 'question') {
|
||||||
|
segmentDiv.className += ' segment-question';
|
||||||
|
const questions = segment.questions || (segment.question ? [{
|
||||||
|
question: segment.question,
|
||||||
|
options: segment.options || [],
|
||||||
|
multiSelect: false
|
||||||
|
}] : []);
|
||||||
|
const isAnswered = answeredQuestions.has(segment.askId);
|
||||||
|
const savedAnswers = answeredQuestions.get(segment.askId) || {};
|
||||||
|
if (isAnswered) {
|
||||||
|
segmentDiv.classList.add('answered');
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionsHtml = questions.map((q, qIndex) => {
|
||||||
|
const inputType = q.multiSelect ? 'checkbox' : 'radio';
|
||||||
|
const inputName = \`q\${qIndex}\`;
|
||||||
|
const selectedAnswers = savedAnswers[qIndex] || [];
|
||||||
|
let optionsHtml;
|
||||||
|
if (!q.options || q.options.length === 0) {
|
||||||
|
const savedText = selectedAnswers[0] || '';
|
||||||
|
optionsHtml = \`<textarea class="question-text-input" name="\${inputName}" placeholder="请输入您的答案..." style="width:100%;min-height:80px;padding:8px;border:1px solid var(--vscode-input-border);border-radius:4px;background:var(--vscode-input-background);color:var(--vscode-input-foreground);resize:vertical;" \${isAnswered ? 'disabled' : ''}>\${savedText}</textarea>\`;
|
||||||
|
} else {
|
||||||
|
optionsHtml = q.options.map(opt => {
|
||||||
|
const isSelected = selectedAnswers.includes(opt);
|
||||||
|
return \`<label class="question-option\${isSelected ? ' selected' : ''}" style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 5px 5px 0;">
|
||||||
|
<input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
|
||||||
|
<span>\${opt}</span>
|
||||||
|
</label>\`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
return \`
|
||||||
|
<div class="question-item" data-question-index="\${qIndex}" style="margin-bottom:12px;">
|
||||||
|
<div class="question-text" style="margin-bottom:8px;">\${formatText(q.question)}</div>
|
||||||
|
<div class="question-options">\${optionsHtml}</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
segmentDiv.innerHTML = \`
|
||||||
|
\${questionsHtml}
|
||||||
|
<button class="custom-submit" style="display:\${isAnswered ? 'none' : 'block'};margin-top:8px;padding:8px 16px;background:var(--vscode-button-background);color:var(--vscode-button-foreground);border:none;border-radius:6px;cursor:pointer;">提交答案</button>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
if (!isAnswered) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.addEventListener('click', function() {
|
||||||
|
const answers = {};
|
||||||
|
questions.forEach((q, qIndex) => {
|
||||||
|
const textarea = segmentDiv.querySelector(\`textarea[name="q\${qIndex}"]\`);
|
||||||
|
if (textarea) {
|
||||||
|
const value = textarea.value.trim();
|
||||||
|
answers[qIndex] = value ? [value] : [];
|
||||||
|
} else {
|
||||||
|
const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
|
||||||
|
answers[qIndex] = Array.from(inputs).map(input => input.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
} else if (segment.type === 'agent') {
|
||||||
|
renderAgentCard(segment, segmentDiv);
|
||||||
|
} else if (segment.type === 'plan') {
|
||||||
|
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSegmentedMessage.appendChild(segmentDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isComplete) {
|
||||||
|
currentSegmentedMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
smartScrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSegments(segments) {
|
||||||
|
console.log('[WebView] renderSegments 被调用, segments:', segments);
|
||||||
|
if (!segments || segments.length === 0) {
|
||||||
|
console.log('[WebView] segments 为空,跳过渲染');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentStreamingMessage) {
|
||||||
|
console.log('[WebView] 移除流式消息');
|
||||||
|
currentStreamingMessage.remove();
|
||||||
|
currentStreamingMessage = null;
|
||||||
|
}
|
||||||
|
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||||||
|
console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
|
||||||
|
toolStatuses.forEach(el => {
|
||||||
|
console.log('[WebView] 移除工具状态消息:', el.className);
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
updateSegmentsRealtime(segments, false);
|
||||||
|
|
||||||
|
// 历史消息渲染完成后添加操作按钮
|
||||||
|
if (currentSegmentedMessage) {
|
||||||
|
const actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'message-actions';
|
||||||
|
const copyBtn = document.createElement('button');
|
||||||
|
copyBtn.className = 'action-btn';
|
||||||
|
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).map(s => s.content).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);
|
||||||
|
currentSegmentedMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
smartScrollToBottom();
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
56
src/views/textFormatter.ts
Normal file
56
src/views/textFormatter.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 文本格式化模块
|
||||||
|
* 功能:Markdown 文本转 HTML
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:消息内容格式化显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function formatText(text: string): string {
|
||||||
|
if (!text) return "";
|
||||||
|
|
||||||
|
let html = text;
|
||||||
|
|
||||||
|
const codeBlocks: string[] = [];
|
||||||
|
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||||
|
const language = lang || "plaintext";
|
||||||
|
const escapedCode = code
|
||||||
|
.trim()
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`;
|
||||||
|
codeBlocks.push(`<pre><code class="language-${language}">${escapedCode}</code></pre>`);
|
||||||
|
return placeholder;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inlineCodes: string[] = [];
|
||||||
|
html = html.replace(/`([^`]+)`/g, (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.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
|
||||||
|
html = html.replace(/^### (.+)$/gm, "<h3>$1</h3>");
|
||||||
|
html = html.replace(/^## (.+)$/gm, "<h2>$1</h2>");
|
||||||
|
html = html.replace(/^# (.+)$/gm, "<h1>$1</h1>");
|
||||||
|
html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||||
|
html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
||||||
|
html = html.replace(/^[\-\*] (.+)$/gm, "<li>$1</li>");
|
||||||
|
html = html.replace(/(<li>.*<\/li>\n?)+/g, "<ul>$&</ul>");
|
||||||
|
html = html.replace(/^\d+\. (.+)$/gm, "<li>$1</li>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
106
src/views/toolHelpers.ts
Normal file
106
src/views/toolHelpers.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 工具辅助函数模块
|
||||||
|
* 功能:工具图标、名称映射、VCD 路径解析
|
||||||
|
* 依赖:toolIcons
|
||||||
|
* 使用场景:工具调用显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
fileWriteIconSvg,
|
||||||
|
fileReadIconSvg,
|
||||||
|
fileDeleteIconSvg,
|
||||||
|
syntaxCheckIconSvg,
|
||||||
|
SearchCode,
|
||||||
|
saveKnowledgeIconSvg,
|
||||||
|
simulationIconSvg,
|
||||||
|
waveformIconSvg,
|
||||||
|
knowledgeLoadIconSvg,
|
||||||
|
stateTransitionIconSvg,
|
||||||
|
userQuestionIconSvg,
|
||||||
|
updateStageIconSvg,
|
||||||
|
successIconSvg,
|
||||||
|
} from "../constants/toolIcons";
|
||||||
|
|
||||||
|
export function getToolIcon(toolName: string): string {
|
||||||
|
const iconMap: Record<string, string> = {
|
||||||
|
file_read: fileReadIconSvg,
|
||||||
|
file_write: fileWriteIconSvg,
|
||||||
|
file_delete: fileDeleteIconSvg,
|
||||||
|
file_list: SearchCode,
|
||||||
|
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: SearchCode,
|
||||||
|
addPlan: fileWriteIconSvg,
|
||||||
|
addEdge: fileWriteIconSvg,
|
||||||
|
showPlan: SearchCode,
|
||||||
|
addRule: fileWriteIconSvg,
|
||||||
|
updateNode: fileWriteIconSvg,
|
||||||
|
addStateTransition: stateTransitionIconSvg,
|
||||||
|
askUser: userQuestionIconSvg,
|
||||||
|
updatePhase: updateStageIconSvg,
|
||||||
|
iverilog: successIconSvg,
|
||||||
|
};
|
||||||
|
return iconMap[toolName] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToolDisplayName(toolName: string): string {
|
||||||
|
const toolNameMap: Record<string, string> = {
|
||||||
|
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: "已显示计划",
|
||||||
|
addRule: "已添加规则",
|
||||||
|
updateNode: "已更新节点",
|
||||||
|
addStateTransition: "已添加状态转换",
|
||||||
|
spawnExplorer: "代码探索",
|
||||||
|
spawnDebugger: "波形调试",
|
||||||
|
askUser: "用户提问",
|
||||||
|
updatePhase: "已更新阶段",
|
||||||
|
iverilog: "已完成编译",
|
||||||
|
};
|
||||||
|
return toolNameMap[toolName] || toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMultiVcdPaths(toolResult: string): Array<{ name: string; path: string }> {
|
||||||
|
if (!toolResult) return [];
|
||||||
|
const result = String(toolResult);
|
||||||
|
|
||||||
|
const vcdListMatch = result.match(/VCD 文件列表:[\s\S]*?(?=\n\n|$)/);
|
||||||
|
if (!vcdListMatch) return [];
|
||||||
|
|
||||||
|
const paths: Array<{ name: string; path: string }> = [];
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -26,16 +26,16 @@ export function getUserInfoComponentContent(): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 升级到Pro按钮 (仅BASIC会员显示) -->
|
<!-- 升级到Pro按钮 (仅BASIC会员显示) -->
|
||||||
<div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
|
<!-- <div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
|
||||||
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
|
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-detail-body">
|
<div class="user-detail-body">
|
||||||
<div class="user-detail-item">
|
<!-- <div class="user-detail-item">
|
||||||
<span class="detail-label">剩余 Credits</span>
|
<span class="detail-label">剩余 Credits</span>
|
||||||
<span class="detail-value" id="creditsDetail">-</span>
|
<span class="detail-value" id="creditsDetail">-</span>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="user-detail-item logout-item" id="logoutItem">
|
<div class="user-detail-item logout-item" id="logoutItem">
|
||||||
<span class="detail-label">账户管理</span>
|
<span class="detail-label">账户管理</span>
|
||||||
<span class="detail-value logout-link">退出登录</span>
|
<span class="detail-value logout-link">退出登录</span>
|
||||||
|
|||||||
@ -25,11 +25,27 @@ import {
|
|||||||
} from "./progressBar";
|
} from "./progressBar";
|
||||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||||
import { getCurrentEnv } from "../config/settings";
|
import { getCurrentEnv } from "../config/settings";
|
||||||
|
import { taskCompleteIconSvg } from "../constants/toolIcons";
|
||||||
import {
|
import {
|
||||||
getInvitationModalContent,
|
getInvitationModalContent,
|
||||||
getInvitationModalStyles,
|
getInvitationModalStyles,
|
||||||
getInvitationModalScript,
|
getInvitationModalScript,
|
||||||
} from "./invitationModal";
|
} from "./invitationModal";
|
||||||
|
import {
|
||||||
|
getWelcomeModalContent,
|
||||||
|
getWelcomeModalStyles,
|
||||||
|
getWelcomeModalScript,
|
||||||
|
} from "./welcomeModal";
|
||||||
|
import {
|
||||||
|
getNdtWelcomeModalContent,
|
||||||
|
getNdtWelcomeModalStyles,
|
||||||
|
getNdtWelcomeModalScript,
|
||||||
|
} from "./ndtWelcomeModal";
|
||||||
|
import {
|
||||||
|
getExpiredModalContent,
|
||||||
|
getExpiredModalStyles,
|
||||||
|
getExpiredModalScript,
|
||||||
|
} from "./expiredModal";
|
||||||
/**
|
/**
|
||||||
* 获取 WebView 面板的 HTML 内容
|
* 获取 WebView 面板的 HTML 内容
|
||||||
*/
|
*/
|
||||||
@ -100,6 +116,9 @@ export function getWebviewContent(
|
|||||||
${getProgressBarStyles()}
|
${getProgressBarStyles()}
|
||||||
${getInputAreaStyles()}
|
${getInputAreaStyles()}
|
||||||
${getInvitationModalStyles()}
|
${getInvitationModalStyles()}
|
||||||
|
${getWelcomeModalStyles()}
|
||||||
|
${getNdtWelcomeModalStyles()}
|
||||||
|
${getExpiredModalStyles()}
|
||||||
|
|
||||||
.file-editor-section {
|
.file-editor-section {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
@ -286,6 +305,9 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
.segment-text {
|
.segment-text {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
font-size:0.9rem;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
.segment-tool {
|
.segment-tool {
|
||||||
background: var(--vscode-textBlockQuote-background);
|
background: var(--vscode-textBlockQuote-background);
|
||||||
@ -307,7 +329,6 @@ export function getWebviewContent(
|
|||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
}
|
}
|
||||||
.tool-segment-result {
|
.tool-segment-result {
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
padding-left: 22px;
|
padding-left: 22px;
|
||||||
@ -357,16 +378,32 @@ export function getWebviewContent(
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
}
|
}
|
||||||
|
.status-bar #statusText {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--vscode-descriptionForeground) 0%,
|
||||||
|
var(--vscode-foreground) 50%,
|
||||||
|
var(--vscode-descriptionForeground) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: textShimmer 2s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes textShimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--vscode-charts-blue);
|
background: var(--vscode-charts-blue);
|
||||||
animation: statusPulse 1.5s ease-in-out infinite;
|
animation: statusPulse 1.5s ease-in-out infinite;
|
||||||
|
box-shadow: 0 0 8px currentColor;
|
||||||
}
|
}
|
||||||
@keyframes statusPulse {
|
@keyframes statusPulse {
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
0%, 100% { opacity: 1; transform: scale(1.2); }
|
||||||
50% { opacity: 0.5; transform: scale(0.8); }
|
50% { opacity: 0.3; transform: scale(0.6); }
|
||||||
}
|
}
|
||||||
.status-bar.working .status-indicator {
|
.status-bar.working .status-indicator {
|
||||||
background: var(--vscode-charts-orange);
|
background: var(--vscode-charts-orange);
|
||||||
@ -466,6 +503,9 @@ export function getWebviewContent(
|
|||||||
${getConversationHistoryBarContent()}
|
${getConversationHistoryBarContent()}
|
||||||
${getProgressBarContent()}
|
${getProgressBarContent()}
|
||||||
${getInvitationModalContent(qrCodeUri, logoUri)}
|
${getInvitationModalContent(qrCodeUri, logoUri)}
|
||||||
|
${getWelcomeModalContent(logoUri)}
|
||||||
|
${getNdtWelcomeModalContent(logoUri)}
|
||||||
|
${getExpiredModalContent(logoUri)}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div style="display: flex; align-items: center; justify-content: center;">
|
<div style="display: flex; align-items: center; justify-content: center;">
|
||||||
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
|
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
|
||||||
@ -508,6 +548,9 @@ export function getWebviewContent(
|
|||||||
const modeSelect = document.getElementById('modeSelect');
|
const modeSelect = document.getElementById('modeSelect');
|
||||||
const messagesEl = document.getElementById('messages');
|
const messagesEl = document.getElementById('messages');
|
||||||
|
|
||||||
|
// 图标常量
|
||||||
|
const taskCompleteIconSvg = ${JSON.stringify(taskCompleteIconSvg)};
|
||||||
|
|
||||||
// 全局变量
|
// 全局变量
|
||||||
let currentStreamingMessage = null;
|
let currentStreamingMessage = null;
|
||||||
let loadingIndicator = null;
|
let loadingIndicator = null;
|
||||||
@ -732,6 +775,45 @@ export function getWebviewContent(
|
|||||||
// 隐藏加载指示器
|
// 隐藏加载指示器
|
||||||
hideLoadingIndicator();
|
hideLoadingIndicator();
|
||||||
break;
|
break;
|
||||||
|
case 'taskComplete':
|
||||||
|
// 显示任务完成提示
|
||||||
|
const taskDiv = document.createElement('div');
|
||||||
|
taskDiv.className = 'message bot-message';
|
||||||
|
const taskActionsDiv = document.createElement('div');
|
||||||
|
taskActionsDiv.className = 'message-actions';
|
||||||
|
const taskMessageContent = document.createElement('span');
|
||||||
|
taskMessageContent.innerHTML = taskCompleteIconSvg + ' 任务完成';
|
||||||
|
const taskCopyBtn = document.createElement('button');
|
||||||
|
taskCopyBtn.className = 'action-btn';
|
||||||
|
taskCopyBtn.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>\`;
|
||||||
|
taskCopyBtn.onclick = () => {
|
||||||
|
// 获取前一个 AI 消息的内容
|
||||||
|
const prevMessage = taskDiv.previousElementSibling;
|
||||||
|
if (prevMessage && prevMessage.classList.contains('bot-message')) {
|
||||||
|
const textContent = prevMessage.textContent || '';
|
||||||
|
copyMessage(textContent, taskCopyBtn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const taskLikeBtn = document.createElement('button');
|
||||||
|
taskLikeBtn.className = 'action-btn';
|
||||||
|
taskLikeBtn.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>\`;
|
||||||
|
taskLikeBtn.onclick = () => toggleLike(taskLikeBtn);
|
||||||
|
const taskDislikeBtn = document.createElement('button');
|
||||||
|
taskDislikeBtn.className = 'action-btn';
|
||||||
|
taskDislikeBtn.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>\`;
|
||||||
|
taskDislikeBtn.onclick = () => toggleDislike(taskDislikeBtn);
|
||||||
|
taskActionsDiv.appendChild(taskMessageContent);
|
||||||
|
taskActionsDiv.appendChild(taskCopyBtn);
|
||||||
|
taskActionsDiv.appendChild(taskLikeBtn);
|
||||||
|
taskActionsDiv.appendChild(taskDislikeBtn);
|
||||||
|
taskDiv.appendChild(taskActionsDiv);
|
||||||
|
messagesEl.appendChild(taskDiv);
|
||||||
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'taskCompleteHistory':
|
||||||
|
// 历史记录不显示任务完成提示
|
||||||
|
break;
|
||||||
|
|
||||||
case 'workspaceStatus':
|
case 'workspaceStatus':
|
||||||
// 更新工作区状态
|
// 更新工作区状态
|
||||||
@ -861,6 +943,27 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'showChanges':
|
||||||
|
// 显示代码变更
|
||||||
|
if (typeof showChangesPanel === 'function') {
|
||||||
|
showChangesPanel(message.changes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'changeAccepted':
|
||||||
|
// 变更已采纳
|
||||||
|
if (typeof handleChangeAccepted === 'function') {
|
||||||
|
handleChangeAccepted(message.changeId, message.success, message.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'changeRejected':
|
||||||
|
// 变更已拒绝
|
||||||
|
if (typeof handleChangeRejected === 'function') {
|
||||||
|
handleChangeRejected(message.changeId, message.success, message.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||||
}
|
}
|
||||||
@ -873,6 +976,9 @@ export function getWebviewContent(
|
|||||||
${getProgressBarScript()}
|
${getProgressBarScript()}
|
||||||
${getInputAreaScript()}
|
${getInputAreaScript()}
|
||||||
${getInvitationModalScript()}
|
${getInvitationModalScript()}
|
||||||
|
${getWelcomeModalScript()}
|
||||||
|
${getNdtWelcomeModalScript()}
|
||||||
|
${getExpiredModalScript()}
|
||||||
</script></body>
|
</script></body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|||||||
310
src/views/welcomeModal.ts
Normal file
310
src/views/welcomeModal.ts
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
/**
|
||||||
|
* 欢迎弹窗(试用用户)
|
||||||
|
* 功能:在聊天面板内显示欢迎模态窗口
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:试用用户首次登录时在聊天面板内显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取欢迎弹窗的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getWelcomeModalContent(logoUri?: string): string {
|
||||||
|
return `
|
||||||
|
<!-- 欢迎弹窗 -->
|
||||||
|
<div id="welcomeModal" class="welcome-modal" style="display: none;">
|
||||||
|
<div class="welcome-modal-overlay"></div>
|
||||||
|
<div class="welcome-modal-content">
|
||||||
|
${logoUri ? `<img src="${logoUri}" class="welcome-logo-corner" alt="IC Coder" />` : ""}
|
||||||
|
|
||||||
|
<div class="welcome-modal-header">
|
||||||
|
<div class="welcome-icon">🎉</div>
|
||||||
|
<h2>欢迎使用 IC Coder!</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-modal-body">
|
||||||
|
<!-- 试用期提示 -->
|
||||||
|
<div class="trial-banner">
|
||||||
|
<span>您已获得 <strong>5 天企业版试用期</strong>,企业版试用期内Credits用量无限,并可无限制使用所有功能</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IC Coder 简介 -->
|
||||||
|
<div class="intro-section">
|
||||||
|
<h3 class="section-title">关于 IC Coder</h3>
|
||||||
|
<p class="intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform(自主式人工智能 Verilog 编码平台)。我们采用全球顶尖的IC Coder自研芯片设计微调模型,为代码生成提供强大的AI能力支撑。</p>
|
||||||
|
|
||||||
|
<div class="features">
|
||||||
|
<div class="feature-item">
|
||||||
|
<span class="feature-text">多智能体架构(Multi-Agent System):多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<span class="feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<span class="feature-text">AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 按钮组 -->
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
|
||||||
|
<span>开始使用</span>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取欢迎弹窗的 CSS 样式
|
||||||
|
*/
|
||||||
|
export function getWelcomeModalStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 欢迎弹窗样式 */
|
||||||
|
.welcome-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-widget-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-logo-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 24px;
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-header {
|
||||||
|
padding: 60px 32px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-header h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-body {
|
||||||
|
padding: 0 32px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 试用期横幅 */
|
||||||
|
.trial-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
border-left: 3px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-banner strong {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IC Coder 简介区域 */
|
||||||
|
.intro-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border-left: 2px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮组 */
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn-primary {
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn-primary:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn-secondary {
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
color: var(--vscode-button-secondaryForeground);
|
||||||
|
border: 1px solid var(--vscode-button-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn-secondary:hover {
|
||||||
|
background: var(--vscode-button-secondaryHoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取欢迎弹窗的 JavaScript 逻辑
|
||||||
|
*/
|
||||||
|
export function getWelcomeModalScript(): string {
|
||||||
|
return `
|
||||||
|
// 欢迎弹窗逻辑
|
||||||
|
(function() {
|
||||||
|
const modal = document.getElementById('welcomeModal');
|
||||||
|
const startBtn = document.getElementById('welcomeStartBtn');
|
||||||
|
const overlay = modal?.querySelector('.welcome-modal-overlay');
|
||||||
|
|
||||||
|
// 显示欢迎弹窗
|
||||||
|
window.showWelcomeModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏欢迎弹窗
|
||||||
|
window.hideWelcomeModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击"开始使用"按钮
|
||||||
|
if (startBtn) {
|
||||||
|
startBtn.addEventListener('click', function() {
|
||||||
|
hideWelcomeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击遮罩层关闭弹窗
|
||||||
|
if (overlay) {
|
||||||
|
overlay.addEventListener('click', function() {
|
||||||
|
hideWelcomeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止点击弹窗内容时关闭
|
||||||
|
const content = modal?.querySelector('.welcome-modal-content');
|
||||||
|
if (content) {
|
||||||
|
content.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自后端的消息
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.command === 'showWelcomeModal') {
|
||||||
|
showWelcomeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面加载时检查是否需要显示欢迎弹窗
|
||||||
|
vscode.postMessage({ command: 'checkWelcomeModal' });
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
BIN
tools/waveform_trace/bin/waveform_trace.exe
Normal file
BIN
tools/waveform_trace/bin/waveform_trace.exe
Normal file
Binary file not shown.
@ -3,30 +3,30 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
//@ts-check
|
//@ts-check
|
||||||
/** @typedef {import('webpack').Configuration} WebpackConfig **/
|
/** @typedef {import('webpack').Configuration} WebpackConfig **/
|
||||||
|
|
||||||
/** @type WebpackConfig */
|
/** @type WebpackConfig */
|
||||||
const extensionConfig = {
|
const extensionConfig = {
|
||||||
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
|
target: 'node',
|
||||||
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
|
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
|
||||||
|
|
||||||
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
|
entry: './src/extension.ts',
|
||||||
output: {
|
output: {
|
||||||
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
filename: 'extension.js',
|
filename: 'extension.js',
|
||||||
libraryTarget: 'commonjs2'
|
libraryTarget: 'commonjs2',
|
||||||
|
clean: true // 自动清理旧文件
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
|
vscode: 'commonjs vscode',
|
||||||
'node-notifier': 'commonjs node-notifier' // node-notifier 依赖原生模块,必须排除
|
'node-notifier': 'commonjs node-notifier'
|
||||||
// modules added here also need to be added in the .vscodeignore file
|
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
|
extensions: ['.ts', '.js'],
|
||||||
extensions: ['.ts', '.js']
|
mainFields: ['module', 'main']
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
@ -35,15 +35,37 @@ const extensionConfig = {
|
|||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'ts-loader'
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
transpileOnly: true, // 加快编译速度
|
||||||
|
compilerOptions: {
|
||||||
|
sourceMap: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
devtool: 'nosources-source-map',
|
devtool: process.env.NODE_ENV === 'production' ? 'hidden-source-map' : 'nosources-source-map',
|
||||||
infrastructureLogging: {
|
infrastructureLogging: {
|
||||||
level: "log", // enables logging required for problem matchers
|
level: "log",
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{ from: 'src/assets', to: 'assets' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
optimization: {
|
||||||
|
minimize: process.env.NODE_ENV === 'production',
|
||||||
|
usedExports: true // Tree Shaking
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
hints: 'warning',
|
||||||
|
maxAssetSize: 2 * 1024 * 1024, // 2MB
|
||||||
|
maxEntrypointSize: 2 * 1024 * 1024
|
||||||
|
}
|
||||||
};
|
};
|
||||||
module.exports = [ extensionConfig ];
|
module.exports = [ extensionConfig ];
|
||||||
Reference in New Issue
Block a user