Compare commits
20 Commits
feat/eda
...
feat/Knowl
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a91513031 | |||
| 69f86dbc0d | |||
| 24512c61e6 | |||
| 1207d2b91a | |||
| cc4583e2cc | |||
| 4ba096d898 | |||
| d2cd7b0bc8 | |||
| 686aaebc26 | |||
| 894479e252 | |||
| cab8960159 | |||
| 46233d2ac3 | |||
| 79a6ff4c99 | |||
| ada3a3bffd | |||
| 1d64310607 | |||
| 6acec9fcb5 | |||
| 9d273fff83 | |||
| 64cce80a70 | |||
| 76c1af6e7e | |||
| eb345e3e1f | |||
| 8751944053 |
44
CHANGELOG.md
44
CHANGELOG.md
@ -2,6 +2,50 @@
|
||||
|
||||
所有重要的项目变更都将记录在此文件中。
|
||||
|
||||
## [1.13.5] - 2026-03-17
|
||||
|
||||
### 新增
|
||||
|
||||
- 个人规则管理功能
|
||||
- 代码选择添加到对话(Ctrl+L / Cmd+L)
|
||||
|
||||
### 优化
|
||||
|
||||
- 消息处理模块重构
|
||||
- 提示信息优化
|
||||
|
||||
## [1.13.0] - 2026-03-15
|
||||
|
||||
### 新增
|
||||
|
||||
- Vivado 联动功能文档
|
||||
- 示例刷新按钮
|
||||
|
||||
### 优化
|
||||
|
||||
- 界面样式和交互体验
|
||||
|
||||
## [1.12.0] - 2026-03-12
|
||||
|
||||
### 新增
|
||||
|
||||
- 对话结束提示
|
||||
|
||||
### 优化
|
||||
|
||||
- 面板组件架构优化
|
||||
|
||||
## [1.11.0] - 2026-03-10
|
||||
|
||||
### 优化
|
||||
|
||||
- 代码选择交互优化
|
||||
|
||||
### 修复
|
||||
|
||||
- AI 提问输入问题
|
||||
- Windows 平台兼容性问题
|
||||
|
||||
## [1.0.12] - 2026-03-06
|
||||
|
||||
### 新增
|
||||
|
||||
@ -37,51 +37,38 @@ IC Coder Plugin 目前支持:
|
||||
|
||||
### 2.1 核心目标
|
||||
|
||||
- **前端提供原子工具**:前端只提供独立的 Vivado 命令工具,不控制流程
|
||||
- **后端AI控制流程**:所有执行顺序、依赖检查由后端AI决策
|
||||
- **工具职责单一**:每个工具只负责执行一个具体命令
|
||||
- **结果透明返回**:执行结果完整返回给后端,由后端决定下一步
|
||||
- **前端工具封装**:在插件前端实现 Vivado 调用的完整逻辑
|
||||
- **后端简化调用**:后端只需调用一个工具接口
|
||||
- **文件自动导入**:Vivado 执行完成后,自动将产出文件导入到项目
|
||||
- **流程可视化**:执行进度、日志实时显示
|
||||
|
||||
### 2.2 设计原则
|
||||
### 2.2 非功能目标
|
||||
|
||||
- 前端不做流程判断,只执行命令
|
||||
- 前端不检查依赖关系,由后端保证顺序
|
||||
- 前端返回详细的执行结果,包括成功/失败、输出、报告等
|
||||
- 后端AI根据结果智能决策是否继续
|
||||
- 配置简单,用户友好
|
||||
- 执行过程可视化(进度、日志)
|
||||
- 错误处理完善,提示清晰
|
||||
|
||||
## 3. 功能详细需求
|
||||
|
||||
### 3.1 前端提供的工具
|
||||
### 3.1 Vivado 支持的操作
|
||||
|
||||
前端提供 4 个独立的工具,每个工具只负责执行一个命令:
|
||||
#### 3.1.1 综合(Synthesis)
|
||||
|
||||
#### 3.1.1 createVivadoProject - 创建工程
|
||||
- **输入**:Verilog/VHDL 源文件、约束文件(.xdc)
|
||||
- **输出**:设计检查点(.dcp)、综合报告(.rpt)
|
||||
- **用途**:将 RTL 代码转换为门级网表,检查资源使用情况
|
||||
|
||||
- **输入**:项目名称、芯片型号、源文件列表、约束文件(可选)
|
||||
- **输出**:工程文件(.xpr)
|
||||
- **说明**:创建 Vivado 工程,不执行任何构建操作
|
||||
#### 3.1.2 实现(Implementation)
|
||||
|
||||
#### 3.1.2 runVivadoSynthesis - 综合
|
||||
- **输入**:综合后的 .dcp 文件
|
||||
- **输出**:实现后的 .dcp 文件、时序报告、布局布线报告
|
||||
- **用途**:完成布局布线,检查时序是否满足要求
|
||||
|
||||
- **输入**:工程路径或源文件、芯片型号、顶层模块、约束文件(可选)
|
||||
- **输出**:.dcp 文件、综合报告
|
||||
- **说明**:执行综合,前端不检查工程是否存在。约束文件在此阶段可选,主要用于时序约束
|
||||
#### 3.1.3 生成比特流(Generate Bitstream)
|
||||
|
||||
#### 3.1.3 runVivadoImplementation - 实现
|
||||
|
||||
- **输入**:综合后的 .dcp 文件路径、约束文件(必需,包含管脚约束)
|
||||
- **输出**:实现后的 .dcp 文件、时序报告
|
||||
- **说明**:执行实现,前端不检查 .dcp 是否存在。**管脚约束是必需的**,否则无法完成布局布线
|
||||
|
||||
#### 3.1.4 runVivadoBitstream - 生成比特流
|
||||
|
||||
- **输入**:实现后的 .dcp 文件路径
|
||||
- **输出**:.bit 文件(可下载到 FPGA 的配置文件)
|
||||
- **核心依赖**:
|
||||
1. 实现已完成
|
||||
2. 工程指定目标芯片型号
|
||||
3. 已完成管脚约束(无管脚约束无法生成)
|
||||
- **说明**:生成比特流,前端不检查 .dcp 是否存在
|
||||
- **输入**:实现后的 .dcp 文件
|
||||
- **输出**:比特流文件(.bit)
|
||||
- **用途**:生成可烧录到 FPGA 的配置文件
|
||||
|
||||
### 3.2 配置管理
|
||||
|
||||
@ -115,111 +102,90 @@ IC Coder Plugin 目前支持:
|
||||
|
||||
### 3.3 工具调用接口
|
||||
|
||||
#### 3.3.1 通用响应格式
|
||||
|
||||
所有工具返回统一的响应格式:
|
||||
#### 3.3.1 接口定义
|
||||
|
||||
```typescript
|
||||
interface VivadoToolRequest {
|
||||
command: string; // 命令类型:synthesis | implementation | bitstream
|
||||
parameters?: {
|
||||
topModule?: string; // 顶层模块名
|
||||
files?: string[]; // 输入文件列表
|
||||
part?: string; // FPGA 型号(可选,使用配置中的默认值)
|
||||
constraints?: string; // 约束文件路径(.xdc)
|
||||
outputDir?: string; // 输出目录
|
||||
};
|
||||
importOutput?: {
|
||||
enabled: boolean; // 是否自动导入
|
||||
targetDir: string; // 目标目录
|
||||
};
|
||||
}
|
||||
|
||||
interface VivadoToolResponse {
|
||||
success: boolean; // 是否成功
|
||||
command: string; // 执行的命令
|
||||
success: boolean;
|
||||
command: string;
|
||||
executionTime: number; // 执行时间(毫秒)
|
||||
output: string; // 完整输出日志
|
||||
error?: string; // 错误信息(如果失败)
|
||||
outputFiles?: string[]; // 产出文件路径列表
|
||||
output: string; // 标准输出
|
||||
error?: string; // 错误信息
|
||||
importedFiles?: string[]; // 已导入的文件列表
|
||||
reports?: {
|
||||
resources?: string; // 资源使用摘要
|
||||
timing?: string; // 时序信息摘要
|
||||
// 报告摘要
|
||||
resources?: string; // 资源使用情况
|
||||
timing?: string; // 时序信息
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.2 各工具的参数定义
|
||||
### 3.4 执行流程
|
||||
|
||||
**createVivadoProject**
|
||||
```typescript
|
||||
{
|
||||
projectName: string; // 项目名称
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files: string[]; // 源文件列表
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
#### 3.4.1 参数验证
|
||||
|
||||
- 检查 Vivado 是否已配置
|
||||
- 检查可执行文件是否存在
|
||||
- 检查输入文件是否存在
|
||||
- 检查工作目录是否存在
|
||||
|
||||
#### 3.4.2 TCL 脚本生成
|
||||
|
||||
根据命令类型自动生成 TCL 脚本:
|
||||
|
||||
**综合脚本示例(synth.tcl)**:
|
||||
|
||||
```tcl
|
||||
# 读取源文件
|
||||
read_verilog counter.v
|
||||
read_xdc constraints.xdc
|
||||
|
||||
# 设置顶层模块
|
||||
set_property top counter [current_fileset]
|
||||
|
||||
# 综合
|
||||
synth_design -part xc7a35tcpg236-1 -top counter
|
||||
|
||||
# 生成报告
|
||||
report_utilization -file utilization_synth.rpt
|
||||
report_timing -file timing_synth.rpt
|
||||
|
||||
# 保存检查点
|
||||
write_checkpoint -force counter_synth.dcp
|
||||
```
|
||||
|
||||
**runVivadoSynthesis**
|
||||
```typescript
|
||||
{
|
||||
projectPath?: string; // 工程路径(可选,如果有工程)
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files?: string[]; // 源文件(如果没有工程)
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
#### 3.4.3 命令执行
|
||||
|
||||
**runVivadoImplementation**
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||
constraints: string; // 约束文件(必需,包含管脚约束)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
- 启动子进程执行 Vivado 命令
|
||||
- 实时捕获标准输出和错误输出
|
||||
- 向前端推送进度信息(解析日志中的进度标记)
|
||||
|
||||
**runVivadoBitstream**
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
#### 3.4.4 结果处理
|
||||
|
||||
### 3.4 后端AI的职责
|
||||
- 检查执行结果(退出码)
|
||||
- 解析报告文件,提取关键信息(资源使用、时序)
|
||||
- 查找产出文件
|
||||
|
||||
后端AI负责:
|
||||
1. 询问用户必要参数(芯片型号、执行模式等)
|
||||
2. 理解用户意图,决定调用哪些工具
|
||||
3. 按正确顺序调用工具(遵循依赖关系)
|
||||
4. 检查每步执行结果,决定是否继续
|
||||
5. 汇总结果并展示给用户
|
||||
#### 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 交互
|
||||
|
||||
@ -246,187 +212,90 @@ interface VivadoToolResponse {
|
||||
|
||||
#### 3.6.1 工具定义
|
||||
|
||||
后端注册 4 个独立工具:
|
||||
后端在工具列表中添加 Vivado 工具:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "createVivadoProject",
|
||||
"description": "创建 Vivado 工程。需要先询问用户芯片型号和执行模式。",
|
||||
"name": "runVivado",
|
||||
"description": "调用 Vivado 执行综合、实现或生成比特流。使用前必须先询问用户芯片型号等必要参数。",
|
||||
"parameters": {
|
||||
"projectName": "项目名称",
|
||||
"part": "芯片型号(必须从用户获取)",
|
||||
"command": "命令类型(synthesis/implementation/bitstream)",
|
||||
"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)"
|
||||
"files": "输入文件列表",
|
||||
"part": "FPGA 芯片型号(必须从用户获取)",
|
||||
"constraints": "约束文件路径(可选)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.6.2 后端调用示例
|
||||
#### 3.6.2 后端交互流程
|
||||
|
||||
**场景:用户要求完整流程**
|
||||
**关键点**:后端必须先收集必要参数,再调用工具
|
||||
|
||||
1. **用户发起请求**:"打开 Vivado" 或 "用 Vivado 综合"
|
||||
2. **后端识别意图**:需要调用 runVivado 工具
|
||||
3. **后端询问参数**:
|
||||
- FPGA 芯片型号(必须)
|
||||
- 约束文件(可选)
|
||||
- 确认顶层模块名
|
||||
4. **用户提供参数**
|
||||
5. **后端调用工具**:传递完整参数给前端
|
||||
6. **前端执行**:VivadoRunner 执行命令
|
||||
7. **返回结果**:后端接收结果并展示给用户
|
||||
|
||||
#### 3.6.3 调用示例(完整交互)
|
||||
|
||||
```
|
||||
用户: 用 Vivado 跑完整流程
|
||||
用户:帮我用 Vivado 综合一下 counter.v
|
||||
|
||||
AI: 请提供芯片型号和执行模式
|
||||
用户: xc7a35tcpg236-1,后端执行
|
||||
AI:好的,我将使用 Vivado 进行综合。请提供以下信息:
|
||||
1. FPGA 芯片型号(例如:xc7a35tcpg236-1)
|
||||
2. 是否有约束文件(.xdc)?
|
||||
|
||||
AI 执行:
|
||||
1. [调用] createVivadoProject({ projectName: "counter", part: "xc7a35tcpg236-1", ... })
|
||||
[结果] { success: true, outputFiles: ["counter.xpr"] }
|
||||
用户:xc7a35tcpg236-1,没有约束文件
|
||||
|
||||
2. [调用] runVivadoSynthesis({ projectPath: "counter.xpr", ... })
|
||||
[结果] { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||
AI:收到,开始综合...
|
||||
[调用工具] runVivado
|
||||
参数:
|
||||
- command: synthesis
|
||||
- topModule: counter
|
||||
- files: ["counter.v"]
|
||||
- part: "xc7a35tcpg236-1"
|
||||
|
||||
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 文件
|
||||
[执行中...]
|
||||
Vivado 综合完成!
|
||||
- 芯片型号:xc7a35tcpg236-1
|
||||
- 执行时间:45 秒
|
||||
- 资源使用:LUT: 32/20800 (0.15%), FF: 8/41600 (0.02%)
|
||||
- 产出文件:counter_synth.dcp, utilization_synth.rpt
|
||||
- 已自动导入到:vivado_output/
|
||||
```
|
||||
|
||||
## 4. 用户场景
|
||||
|
||||
### 4.1 场景一:完整流程执行
|
||||
### 4.1 场景一:单步综合
|
||||
|
||||
**用户输入**:"用 Vivado 跑完整个 FPGA 流程"
|
||||
1. 用户编写完 Verilog 代码
|
||||
2. 在聊天中输入:"用 Vivado 综合一下 counter.v"
|
||||
3. AI 调用 `runVivado` 工具
|
||||
4. 插件执行 Vivado 综合
|
||||
5. 综合完成后,显示资源使用情况,自动导入报告文件
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 好的,我将使用 Vivado 执行完整的 FPGA 开发流程。请提供以下信息:
|
||||
### 4.2 场景二:完整流程
|
||||
|
||||
1. FPGA 芯片型号(例如:xc7a35tcpg236-1)
|
||||
2. 请选择要执行的步骤(可多选):
|
||||
1. 用户输入:"用 Vivado 跑完整个流程"
|
||||
2. AI 依次调用:
|
||||
- 综合(Synthesis)
|
||||
- 实现(Implementation)
|
||||
- 生成比特流(Bitstream)
|
||||
3. 执行模式:
|
||||
- 图形化模式:打开 Vivado GUI
|
||||
- 后端执行:自动在后台执行
|
||||
4. 约束文件路径(.xdc,必需)
|
||||
3. 每个步骤完成后显示结果
|
||||
4. 最终生成 .bit 文件,用户可以烧录到 FPGA
|
||||
|
||||
用户: xc7a35tcpg236-1,全部步骤,后端执行,约束文件是 counter.xdc
|
||||
### 4.3 场景三:查看报告
|
||||
|
||||
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. 是否有语法错误
|
||||
|
||||
执行已停止,请修复错误后重试。
|
||||
```
|
||||
1. Vivado 执行完成后
|
||||
2. 用户点击导入的报告文件
|
||||
3. 在编辑器中查看资源使用、时序分析等信息
|
||||
|
||||
## 5. 技术约束
|
||||
|
||||
@ -485,9 +354,6 @@ AI: 综合失败,发现以下问题:
|
||||
- 用户需要配置正确的 Vivado 路径
|
||||
- 需要设置环境变量(如 `XILINX_VIVADO`)
|
||||
- 需要有效的 Vivado 许可证
|
||||
- **需要提供 .xdc 约束文件**:
|
||||
- **管脚约束**(必需):定义信号与 FPGA 引脚的映射关系,实现阶段必须提供
|
||||
- **时序约束**(强烈推荐):定义时钟频率和时序要求,确保设计满足性能指标
|
||||
|
||||
## 8. 后续扩展
|
||||
|
||||
|
||||
@ -1,166 +1,637 @@
|
||||
# Vivado 联动前后端对接文档
|
||||
|
||||
## 1. 前端提供的工具
|
||||
## 1. 概述
|
||||
|
||||
前端提供 4 个独立工具,每个工具执行一个 Vivado 命令。
|
||||
本文档描述后端 AI 服务如何调用前端的 Vivado 工具,以及前端如何响应和返回结果。
|
||||
|
||||
### 1.1 createVivadoProject - 创建工程
|
||||
### 1.1 调用流程
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
```
|
||||
后端 AI 服务
|
||||
↓ (1) 发送工具调用请求
|
||||
前端 Extension (MessageHandler)
|
||||
↓ (2) 解析请求,调用 VivadoRunner
|
||||
VivadoRunner
|
||||
↓ (3) 执行 Vivado,实时推送进度
|
||||
前端 Webview
|
||||
↓ (4) 显示进度和结果
|
||||
前端 Extension
|
||||
↓ (5) 返回执行结果给后端
|
||||
后端 AI 服务
|
||||
```
|
||||
|
||||
## 2. 工具定义(后端)
|
||||
|
||||
### 2.1 工具注册
|
||||
|
||||
后端需要在工具列表中注册 `runVivado` 工具:
|
||||
|
||||
```json
|
||||
{
|
||||
projectName: string; // 项目名称
|
||||
part: string; // 芯片型号(如 xc7a35tcpg236-1)
|
||||
topModule: string; // 顶层模块名
|
||||
files: string[]; // 源文件路径列表
|
||||
constraints?: string; // 约束文件路径(可选)
|
||||
mode: 'gui' | 'batch'; // gui=打开图形界面,batch=后台执行
|
||||
"name": "runVivado",
|
||||
"description": "调用本地 Vivado 工具执行 FPGA 综合、实现或生成比特流。用于将 Verilog 代码部署到 FPGA 硬件。使用前必须先询问用户必要的参数(如芯片型号、执行模式)。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"enum": ["synthesis", "implementation", "bitstream"],
|
||||
"description": "要执行的命令类型:synthesis(综合)、implementation(实现)、bitstream(生成比特流)"
|
||||
},
|
||||
"topModule": {
|
||||
"type": "string",
|
||||
"description": "顶层模块名称"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "输入的 Verilog 文件路径列表"
|
||||
},
|
||||
"constraints": {
|
||||
"type": "string",
|
||||
"description": "约束文件路径(.xdc 文件),可选"
|
||||
},
|
||||
"part": {
|
||||
"type": "string",
|
||||
"description": "FPGA 芯片型号(如 xc7a35tcpg236-1),必须从用户处获取"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["batch", "gui"],
|
||||
"description": "执行模式:batch(后台批处理)、gui(打开图形界面),必须询问用户"
|
||||
}
|
||||
},
|
||||
"required": ["command", "topModule", "files", "part", "mode"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
### 2.2 后端调用前的准备工作
|
||||
|
||||
**重要**:后端在调用 `runVivado` 工具前,必须先向用户询问必要参数:
|
||||
|
||||
1. **芯片型号(part)**:必须询问,例如 "xc7a35tcpg236-1"
|
||||
2. **执行模式(mode)**:必须询问用户选择
|
||||
- `batch`:后台批处理执行,自动完成
|
||||
- `gui`:打开 Vivado 图形界面,用户手动操作
|
||||
3. **顶层模块名**:可从文件名推断,但建议确认
|
||||
4. **约束文件**:询问是否有时序约束文件(.xdc)
|
||||
|
||||
**询问示例**:
|
||||
```
|
||||
AI: 我将使用 Vivado 进行综合。请提供以下信息:
|
||||
1. FPGA 芯片型号(例如:xc7a35tcpg236-1)
|
||||
2. 执行模式:
|
||||
- 批处理模式:后台自动执行,完成后返回结果
|
||||
- 图形界面:打开 Vivado GUI,您可以手动操作
|
||||
3. 是否有约束文件(.xdc)?
|
||||
|
||||
用户: xc7a35tcpg236-1,批处理模式,没有约束文件
|
||||
|
||||
AI: 好的,开始后台综合...
|
||||
[调用 runVivado 工具]
|
||||
```
|
||||
|
||||
### 2.3 调用示例
|
||||
|
||||
#### 示例 1:综合单个文件(批处理模式)
|
||||
|
||||
```json
|
||||
{
|
||||
success: boolean; // 是否成功
|
||||
command: "create_project";
|
||||
executionTime: number; // 执行时间(毫秒)
|
||||
output: string; // 完整日志
|
||||
error?: string; // 失败原因(如果失败)
|
||||
outputFiles?: string[]; // 产出的工程文件路径
|
||||
"tool": "runVivado",
|
||||
"parameters": {
|
||||
"command": "synthesis",
|
||||
"topModule": "counter",
|
||||
"files": ["counter.v"],
|
||||
"part": "xc7a35tcpg236-1",
|
||||
"mode": "batch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 runVivadoSynthesis - 综合
|
||||
#### 示例 2:综合带约束文件(图形界面模式)
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
```json
|
||||
{
|
||||
projectPath?: string; // 工程路径(可选)
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files?: string[]; // 源文件(如果没有工程)
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch';
|
||||
"tool": "runVivado",
|
||||
"parameters": {
|
||||
"command": "synthesis",
|
||||
"topModule": "uart_top",
|
||||
"files": ["uart_tx.v", "uart_rx.v", "uart_top.v"],
|
||||
"constraints": "constraints.xdc",
|
||||
"part": "xc7k325tffg900-2",
|
||||
"mode": "gui"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
#### 示例 3:实现(批处理模式)
|
||||
|
||||
```json
|
||||
{
|
||||
success: boolean;
|
||||
command: "synthesis";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // .dcp 文件等
|
||||
reports?: {
|
||||
resources?: string; // 资源使用摘要
|
||||
timing?: string; // 时序摘要
|
||||
};
|
||||
"tool": "runVivado",
|
||||
"parameters": {
|
||||
"command": "implementation",
|
||||
"topModule": "counter",
|
||||
"part": "xc7a35tcpg236-1",
|
||||
"mode": "batch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 runVivadoImplementation - 实现
|
||||
#### 示例 4:生成比特流(图形界面)
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
```json
|
||||
{
|
||||
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch';
|
||||
"tool": "runVivado",
|
||||
"parameters": {
|
||||
"command": "bitstream",
|
||||
"topModule": "counter",
|
||||
"part": "xc7a35tcpg236-1",
|
||||
"mode": "gui"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
## 3. 前端接收和处理
|
||||
|
||||
### 3.1 后端如何控制前端
|
||||
|
||||
**核心机制**:后端通过调用 `runVivado` 工具来控制前端执行 Vivado 命令。
|
||||
|
||||
**控制流程**:
|
||||
1. 后端识别用户意图(如"打开 Vivado"、"开始仿真")
|
||||
2. 后端向用户询问必要参数(芯片型号等)
|
||||
3. 后端调用 `runVivado` 工具,传递参数
|
||||
4. 前端接收工具调用,执行相应操作
|
||||
5. 前端返回执行结果给后端
|
||||
6. 后端将结果展示给用户
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
用户输入:"打开 Vivado 进行综合"
|
||||
|
||||
后端处理:
|
||||
1. 识别意图 → 需要调用 runVivado 工具
|
||||
2. 检查参数 → 缺少芯片型号
|
||||
3. 询问用户 → "请提供 FPGA 芯片型号"
|
||||
4. 用户回复 → "xc7a35tcpg236-1"
|
||||
5. 调用工具 → runVivado({ command: "synthesis", part: "xc7a35tcpg236-1", ... })
|
||||
6. 前端执行 → VivadoRunner 启动 Vivado
|
||||
7. 返回结果 → { success: true, ... }
|
||||
8. 展示结果 → "综合完成,耗时 45 秒"
|
||||
```
|
||||
|
||||
### 3.2 MessageHandler 处理逻辑
|
||||
|
||||
前端在 `messageHandler.ts` 中添加工具处理:
|
||||
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "implementation";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // 实现后的 .dcp 文件等
|
||||
reports?: {
|
||||
resources?: string;
|
||||
timing?: string;
|
||||
};
|
||||
// src/utils/messageHandler.ts
|
||||
|
||||
export async function handleToolExecution(
|
||||
panel: vscode.WebviewPanel,
|
||||
toolName: string,
|
||||
parameters: any
|
||||
): Promise<any> {
|
||||
|
||||
if (toolName === 'runVivado') {
|
||||
return await handleVivadoTool(panel, parameters);
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 runVivadoBitstream - 生成比特流
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch';
|
||||
// 其他工具处理...
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "bitstream";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // .bit 文件路径
|
||||
}
|
||||
```
|
||||
async function handleVivadoTool(
|
||||
panel: vscode.WebviewPanel,
|
||||
parameters: any
|
||||
): Promise<VivadoToolResponse> {
|
||||
|
||||
## 2. 前端职责
|
||||
const { command, topModule, files, constraints, part, mode } = parameters;
|
||||
|
||||
- 接收后端工具调用
|
||||
- 生成对应的 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
|
||||
{
|
||||
// 验证必需参数
|
||||
if (!part) {
|
||||
return {
|
||||
success: false,
|
||||
error: "ERROR: [Synth 8-439] module 'counter' not found",
|
||||
output: "详细日志..."
|
||||
command,
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: '缺少必需参数:芯片型号(part)。后端应该先询问用户。'
|
||||
};
|
||||
}
|
||||
|
||||
if (!mode) {
|
||||
return {
|
||||
success: false,
|
||||
command,
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: '缺少必需参数:执行模式(mode)。后端应该询问用户选择 batch 或 gui。'
|
||||
};
|
||||
}
|
||||
|
||||
// 构建请求
|
||||
const request: VivadoToolRequest = {
|
||||
command,
|
||||
parameters: {
|
||||
topModule,
|
||||
files,
|
||||
constraints,
|
||||
part,
|
||||
mode
|
||||
},
|
||||
importOutput: {
|
||||
enabled: mode === 'batch', // 只有批处理模式才自动导入
|
||||
targetDir: path.join(
|
||||
vscode.workspace.workspaceFolders![0].uri.fsPath,
|
||||
'vivado_output'
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// 向前端发送开始消息
|
||||
panel.webview.postMessage({
|
||||
type: 'vivado-start',
|
||||
command
|
||||
});
|
||||
|
||||
// 执行 Vivado
|
||||
const response = await runVivado(request, (progress) => {
|
||||
// 实时推送进度到前端
|
||||
panel.webview.postMessage({
|
||||
type: 'vivado-progress',
|
||||
progress
|
||||
});
|
||||
});
|
||||
|
||||
// 向前端发送完成消息
|
||||
panel.webview.postMessage({
|
||||
type: 'vivado-complete',
|
||||
response
|
||||
});
|
||||
|
||||
// 返回结果给后端
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
后端应该:
|
||||
1. 停止后续步骤
|
||||
2. 提取 `error` 字段
|
||||
3. 给用户提示和建议
|
||||
## 4. 响应格式
|
||||
|
||||
### 4.1 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"command": "synthesis",
|
||||
"executionTime": 45230,
|
||||
"output": "Vivado 执行日志...",
|
||||
"importedFiles": [
|
||||
"/path/to/vivado_output/counter_synth.dcp",
|
||||
"/path/to/vivado_output/counter_utilization_synth.rpt"
|
||||
],
|
||||
"reports": {
|
||||
"resources": "LUT: 32/20800 (0.15%)\nFF: 8/41600 (0.02%)",
|
||||
"timing": "WNS: 5.234ns, TNS: 0.000ns"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 失败响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"command": "synthesis",
|
||||
"executionTime": 1250,
|
||||
"output": "部分执行日志...",
|
||||
"error": "ERROR: [Synth 8-439] module 'counter' not found"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 后端使用指南
|
||||
|
||||
### 5.1 AI 对话流程(完整版)
|
||||
|
||||
```
|
||||
用户:帮我用 Vivado 综合一下 counter.v
|
||||
|
||||
AI 分析:
|
||||
1. 用户想要综合 Verilog 文件
|
||||
2. 需要调用 runVivado 工具
|
||||
3. 命令类型是 synthesis
|
||||
4. 顶层模块名从文件名推断为 counter
|
||||
5. 输入文件是 counter.v
|
||||
6. ⚠️ 缺少必要参数:芯片型号
|
||||
|
||||
AI 回复用户:
|
||||
"好的,我将使用 Vivado 进行综合。请提供以下信息:
|
||||
1. FPGA 芯片型号(例如:xc7a35tcpg236-1、xc7k325tffg900-2)
|
||||
2. 是否有约束文件(.xdc)?"
|
||||
|
||||
用户:xc7a35tcpg236-1,没有约束文件
|
||||
|
||||
AI 调用工具:
|
||||
{
|
||||
"tool": "runVivado",
|
||||
"parameters": {
|
||||
"command": "synthesis",
|
||||
"topModule": "counter",
|
||||
"files": ["counter.v"],
|
||||
"part": "xc7a35tcpg236-1"
|
||||
}
|
||||
}
|
||||
|
||||
前端执行并返回结果
|
||||
|
||||
AI 回复用户:
|
||||
"Vivado 综合完成!
|
||||
- 执行时间:45.2 秒
|
||||
- 芯片型号:xc7a35tcpg236-1
|
||||
- 资源使用:LUT: 32/20800 (0.15%), FF: 8/41600 (0.02%)
|
||||
- 产出文件已导入到 vivado_output 目录"
|
||||
```
|
||||
|
||||
### 5.2 完整流程示例
|
||||
|
||||
```
|
||||
用户:用 Vivado 跑完整个流程
|
||||
|
||||
AI:好的,我将依次执行综合、实现和生成比特流。请提供:
|
||||
1. FPGA 芯片型号
|
||||
2. 顶层模块名
|
||||
3. 是否有约束文件
|
||||
|
||||
用户:xc7a35tcpg236-1,顶层模块是 counter,没有约束文件
|
||||
|
||||
AI:收到,开始执行...
|
||||
|
||||
步骤 1:综合
|
||||
[调用] runVivado { command: "synthesis", topModule: "counter", files: ["counter.v"], part: "xc7a35tcpg236-1" }
|
||||
[结果] 综合成功,耗时 45s
|
||||
|
||||
步骤 2:实现
|
||||
[调用] runVivado { command: "implementation", topModule: "counter", part: "xc7a35tcpg236-1" }
|
||||
[结果] 实现成功,耗时 120s,时序满足要求
|
||||
|
||||
步骤 3:生成比特流
|
||||
[调用] runVivado { command: "bitstream", topModule: "counter", part: "xc7a35tcpg236-1" }
|
||||
[结果] 比特流生成成功,文件:counter.bit
|
||||
|
||||
完成!所有文件已导入到 vivado_output 目录。
|
||||
```
|
||||
|
||||
## 6. 错误处理
|
||||
|
||||
### 6.1 常见错误
|
||||
|
||||
#### 错误 1:Vivado 未配置
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Vivado 未配置,请在设置中配置 Vivado 路径"
|
||||
}
|
||||
```
|
||||
|
||||
**AI 应该回复**:
|
||||
"Vivado 尚未配置,请先在插件设置中配置 Vivado 的安装路径。"
|
||||
|
||||
#### 错误 2:文件不存在
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "输入文件不存在: counter.v"
|
||||
}
|
||||
```
|
||||
|
||||
**AI 应该回复**:
|
||||
"找不到文件 counter.v,请确认文件路径是否正确。"
|
||||
|
||||
#### 错误 3:综合失败
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "ERROR: [Synth 8-439] module 'counter' not found",
|
||||
"output": "详细日志..."
|
||||
}
|
||||
```
|
||||
|
||||
**AI 应该回复**:
|
||||
"综合失败,错误信息:找不到模块 'counter'。请检查:
|
||||
1. 模块名是否正确
|
||||
2. 文件中是否定义了该模块
|
||||
3. 是否有语法错误"
|
||||
|
||||
### 6.2 错误处理建议
|
||||
|
||||
后端收到 `success: false` 时:
|
||||
1. 提取 `error` 字段中的错误信息
|
||||
2. 分析错误类型(配置问题、文件问题、语法问题等)
|
||||
3. 给用户提供具体的解决建议
|
||||
4. 必要时可以查看 `output` 字段获取详细日志
|
||||
|
||||
## 7. 进度推送(可选)
|
||||
|
||||
前端会实时推送进度信息到 Webview,后端无需处理,但可以了解进度格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "vivado-progress",
|
||||
"progress": {
|
||||
"stage": "synthesis",
|
||||
"percentage": 45,
|
||||
"message": "正在综合模块 counter..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 测试建议
|
||||
|
||||
### 8.1 后端测试用例
|
||||
|
||||
```javascript
|
||||
// 测试用例 1:基本综合
|
||||
test('综合单个文件', async () => {
|
||||
const result = await callTool('runVivado', {
|
||||
command: 'synthesis',
|
||||
topModule: 'counter',
|
||||
files: ['counter.v']
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.importedFiles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// 测试用例 2:错误处理
|
||||
test('文件不存在', async () => {
|
||||
const result = await callTool('runVivado', {
|
||||
command: 'synthesis',
|
||||
topModule: 'test',
|
||||
files: ['not_exist.v']
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('不存在');
|
||||
});
|
||||
```
|
||||
|
||||
## 9. 注意事项
|
||||
|
||||
### 9.1 后端必须询问的参数
|
||||
|
||||
**关键要求**:后端在调用 `runVivado` 前,必须先向用户收集以下参数:
|
||||
|
||||
| 参数 | 是否必须 | 询问方式 | 示例 |
|
||||
|------|---------|---------|------|
|
||||
| part(芯片型号) | ✅ 必须 | 直接询问 | "请提供 FPGA 芯片型号(如 xc7a35tcpg236-1)" |
|
||||
| mode(执行模式) | ✅ 必须 | 提供选项 | "选择执行模式:1.批处理 2.图形界面" |
|
||||
| topModule | ✅ 必须 | 可推断或询问 | 从文件名推断,或询问"顶层模块名是什么?" |
|
||||
| files | ✅ 必须(综合时) | 可推断或询问 | 从当前文件或工作区推断 |
|
||||
| constraints | ❌ 可选 | 询问 | "是否有约束文件(.xdc)?" |
|
||||
|
||||
### 9.2 常见用户输入场景
|
||||
|
||||
#### 场景 1:用户说"打开 Vivado"
|
||||
```
|
||||
用户:打开 Vivado
|
||||
|
||||
后端分析:用户想使用 Vivado,但没有指定具体操作
|
||||
|
||||
后端回复:
|
||||
"好的,请问您想执行什么操作?
|
||||
1. 综合(Synthesis)- 将 Verilog 转换为门级网表
|
||||
2. 实现(Implementation)- 布局布线
|
||||
3. 生成比特流(Bitstream)- 生成 .bit 文件
|
||||
|
||||
另外,请提供:
|
||||
- FPGA 芯片型号(例如:xc7a35tcpg236-1)
|
||||
- 执行模式:批处理(后台自动)或 图形界面(手动操作)"
|
||||
```
|
||||
|
||||
#### 场景 2:用户说"开始仿真"
|
||||
```
|
||||
用户:开始仿真
|
||||
|
||||
后端分析:用户可能混淆了仿真和综合
|
||||
|
||||
后端回复:
|
||||
"您是想:
|
||||
1. 使用 iverilog 进行功能仿真(验证逻辑)
|
||||
2. 使用 Vivado 进行综合(部署到 FPGA)
|
||||
|
||||
请选择您需要的操作。"
|
||||
```
|
||||
|
||||
#### 场景 3:用户提供完整信息
|
||||
```
|
||||
用户:用 Vivado 综合 counter.v,芯片是 xc7a35tcpg236-1,批处理模式
|
||||
|
||||
后端分析:信息完整,可以直接调用
|
||||
|
||||
后端操作:
|
||||
[调用] runVivado({
|
||||
command: "synthesis",
|
||||
topModule: "counter",
|
||||
files: ["counter.v"],
|
||||
part: "xc7a35tcpg236-1",
|
||||
mode: "batch"
|
||||
})
|
||||
```
|
||||
|
||||
#### 场景 4:用户选择图形界面
|
||||
```
|
||||
用户:打开 Vivado 图形界面做综合
|
||||
|
||||
后端分析:用户明确要求 GUI 模式
|
||||
|
||||
后端询问:
|
||||
"好的,请提供:
|
||||
1. FPGA 芯片型号
|
||||
2. 顶层模块名"
|
||||
|
||||
用户:xc7a35tcpg236-1, counter
|
||||
|
||||
后端操作:
|
||||
[调用] runVivado({
|
||||
command: "synthesis",
|
||||
topModule: "counter",
|
||||
files: ["counter.v"],
|
||||
part: "xc7a35tcpg236-1",
|
||||
mode: "gui"
|
||||
})
|
||||
|
||||
前端执行:
|
||||
- 生成 TCL 脚本和项目文件
|
||||
- 执行: vivado counter_project.xpr (打开图形界面)
|
||||
- 返回: { success: true, message: "Vivado GUI 已启动" }
|
||||
|
||||
后端回复:
|
||||
"Vivado 图形界面已打开,您可以在界面中手动操作。"
|
||||
```
|
||||
|
||||
### 9.3 执行时间
|
||||
- 综合:小型设计 30s-2min,大型设计 5-30min
|
||||
- 实现:通常是综合时间的 2-3 倍
|
||||
- 生成比特流:通常 10-30s
|
||||
|
||||
后端应该设置合理的超时时间(建议 10 分钟)。
|
||||
|
||||
### 9.4 依赖关系
|
||||
- `implementation` 需要先执行 `synthesis`
|
||||
- `bitstream` 需要先执行 `implementation`
|
||||
|
||||
后端 AI 应该理解这个依赖关系,按顺序调用。
|
||||
|
||||
### 9.5 文件路径
|
||||
- 所有文件路径都是相对于工作区根目录
|
||||
- 前端会自动解析为绝对路径
|
||||
- 支持相对路径和绝对路径
|
||||
|
||||
## 10. 参数传递详细说明
|
||||
|
||||
### 10.1 必需参数
|
||||
|
||||
| 参数 | 类型 | 说明 | 获取方式 |
|
||||
|------|------|------|----------|
|
||||
| command | string | 命令类型 | 从用户意图推断 |
|
||||
| topModule | string | 顶层模块名 | 从文件名推断或询问用户 |
|
||||
| files | string[] | 源文件列表 | 从工作区查找或用户指定 |
|
||||
| part | string | 芯片型号 | **必须询问用户** |
|
||||
|
||||
### 10.2 可选参数
|
||||
|
||||
| 参数 | 类型 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| constraints | string | 约束文件路径 | 无 |
|
||||
|
||||
### 10.3 参数验证规则
|
||||
|
||||
后端在调用前应验证:
|
||||
- `part` 格式正确(如 xc7a35tcpg236-1)
|
||||
- `files` 数组不为空
|
||||
- `topModule` 不为空
|
||||
- `command` 在枚举值内
|
||||
|
||||
## 11. 快速集成清单
|
||||
|
||||
后端开发者需要做的事情:
|
||||
|
||||
- [ ] 在工具列表中注册 `runVivado` 工具
|
||||
- [ ] **实现参数询问逻辑(芯片型号等)**
|
||||
- [ ] 实现工具调用逻辑(发送请求到前端)
|
||||
- [ ] 处理返回结果(success/error)
|
||||
- [ ] 实现错误处理和用户提示
|
||||
- [ ] 理解三个命令的依赖关系
|
||||
- [ ] 设置合理的超时时间(建议 10 分钟)
|
||||
- [ ] 编写测试用例
|
||||
|
||||
前端开发者需要做的事情:
|
||||
|
||||
- [ ] 实现 `handleVivadoTool` 函数
|
||||
- [ ] 集成 VivadoRunner
|
||||
- [ ] 实现进度推送
|
||||
- [ ] 实现结果展示
|
||||
- [ ] 处理各种错误情况
|
||||
- [ ] 验证传入的参数完整性
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
247
docs/personal-rules-backend-integration.md
Normal file
247
docs/personal-rules-backend-integration.md
Normal file
@ -0,0 +1,247 @@
|
||||
# 个人规则功能 - 后端对接文档
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
个人规则功能允许用户创建多条自定义规则,这些规则会在每次对话时自动传递给后端,由后端注入到 AI 的系统提示词中,从而影响 AI 的回答风格和行为。
|
||||
|
||||
## 2. 前端实现说明
|
||||
|
||||
### 2.1 用户界面
|
||||
- 用户可以在设置页面创建、修改、删除多条规则
|
||||
- 每条规则包含:规则名称 + 规则内容
|
||||
- 全局开关:启用/禁用所有规则
|
||||
|
||||
### 2.2 规则存储
|
||||
- 存储位置:`C:\Users\{用户名}\.iccoder\rules\`
|
||||
- 文件格式:每条规则一个独立的 `.md` 文件
|
||||
- 文件命名:`rule-{时间戳}.md`
|
||||
- 文件内容格式:
|
||||
```markdown
|
||||
# 规则名称
|
||||
|
||||
规则内容详细描述...
|
||||
```
|
||||
|
||||
### 2.3 规则传输逻辑
|
||||
- **开关开启**:所有规则内容合并后通过 `personalRules` 字段传给后端
|
||||
- **开关关闭**:`personalRules` 字段为 `undefined`,不传给后端
|
||||
|
||||
## 3. 后端接口变更
|
||||
|
||||
### 3.1 DialogRequest 接口新增字段
|
||||
|
||||
在现有的 `DialogRequest` 接口中新增 `personalRules` 字段:
|
||||
|
||||
```typescript
|
||||
export interface DialogRequest {
|
||||
taskId: string;
|
||||
message: string;
|
||||
userId: string;
|
||||
mode: RunMode;
|
||||
serviceTier?: ServiceTier;
|
||||
token?: string;
|
||||
compactedData?: CompactedMemory;
|
||||
newMessages?: CompactedMessage[];
|
||||
knowledgeData?: string;
|
||||
personalRules?: string; // 新增:个人规则内容
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 字段说明
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `personalRules` | `string` | 否 | 用户的个人规则内容,多条规则用 `\n\n` 分隔 |
|
||||
|
||||
### 3.3 字段示例
|
||||
|
||||
**单条规则:**
|
||||
```json
|
||||
{
|
||||
"message": "帮我写一个排序函数",
|
||||
"personalRules": "始终使用中文回复,代码注释要详细"
|
||||
}
|
||||
```
|
||||
|
||||
**多条规则(合并后):**
|
||||
```json
|
||||
{
|
||||
"message": "帮我写一个排序函数",
|
||||
"personalRules": "始终使用中文回复,代码注释要详细\n\n使用 TypeScript 严格模式\n\n遵循项目编码规范"
|
||||
}
|
||||
```
|
||||
|
||||
**规则关闭:**
|
||||
```json
|
||||
{
|
||||
"message": "帮我写一个排序函数",
|
||||
"personalRules": undefined
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 后端处理要求
|
||||
|
||||
### 4.1 接收处理
|
||||
|
||||
```typescript
|
||||
// 伪代码示例
|
||||
function handleDialogRequest(request: DialogRequest) {
|
||||
const { message, personalRules, ...otherFields } = request;
|
||||
|
||||
// 检查是否有个人规则
|
||||
if (personalRules && personalRules.trim()) {
|
||||
// 有规则:注入到系统提示词
|
||||
return processWithRules(message, personalRules, otherFields);
|
||||
} else {
|
||||
// 无规则:正常处理
|
||||
return processNormal(message, otherFields);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 规则注入策略
|
||||
|
||||
**重要:规则必须注入到系统提示词层,而不是用户消息层**
|
||||
|
||||
推荐的注入顺序(优先级从高到低):
|
||||
|
||||
1. **平台安全策略**(最高优先级,不可被覆盖)
|
||||
2. **产品默认系统提示**
|
||||
3. **用户个人规则** ← 在这里注入
|
||||
4. **用户输入消息**
|
||||
|
||||
### 4.3 注入示例
|
||||
|
||||
```typescript
|
||||
// 伪代码示例
|
||||
function buildSystemPrompt(personalRules?: string): string {
|
||||
let systemPrompt = `
|
||||
你是一个专业的 AI 助手。
|
||||
遵循以下基本原则:
|
||||
- 安全第一
|
||||
- 准确回答
|
||||
- 友好交流
|
||||
`;
|
||||
|
||||
// 如果有个人规则,追加到系统提示词
|
||||
if (personalRules && personalRules.trim()) {
|
||||
systemPrompt += `\n\n用户的个人偏好和规则:\n${personalRules}`;
|
||||
}
|
||||
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
function processWithRules(
|
||||
userMessage: string,
|
||||
personalRules: string,
|
||||
otherFields: any
|
||||
) {
|
||||
const systemPrompt = buildSystemPrompt(personalRules);
|
||||
|
||||
// 调用 AI 模型
|
||||
return callAIModel({
|
||||
system: systemPrompt,
|
||||
user: userMessage,
|
||||
...otherFields
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
### 5.1 安全性
|
||||
- ⚠️ **个人规则不能覆盖平台安全策略**
|
||||
- ⚠️ **需要对规则内容进行基本的安全检查**
|
||||
- ⚠️ **防止注入攻击(如提示词注入)**
|
||||
|
||||
### 5.2 长度限制
|
||||
- 前端已限制单条规则内容,但多条规则合并后可能较长
|
||||
- 建议后端设置总长度上限(如 10000 字符)
|
||||
- 超限时可以截断或返回错误提示
|
||||
|
||||
### 5.3 兼容性
|
||||
- `personalRules` 字段为可选字段
|
||||
- 旧版本前端不传此字段时,后端应正常处理(向后兼容)
|
||||
- 字段为 `undefined` 或空字符串时,视为无规则
|
||||
|
||||
### 5.4 日志记录
|
||||
建议在日志中记录:
|
||||
- 本次请求是否包含个人规则
|
||||
- 规则内容的长度(不要记录完整内容,避免隐私泄露)
|
||||
- 规则注入是否成功
|
||||
|
||||
示例日志:
|
||||
```
|
||||
[INFO] Dialog request received
|
||||
- taskId: abc123
|
||||
- userId: user456
|
||||
- hasPersonalRules: true
|
||||
- rulesLength: 156
|
||||
- rulesInjected: success
|
||||
```
|
||||
|
||||
## 6. 测试建议
|
||||
|
||||
### 6.1 功能测试
|
||||
1. **无规则场景**:`personalRules` 为 `undefined`,正常对话
|
||||
2. **单条规则**:传入一条规则,验证 AI 是否遵循
|
||||
3. **多条规则**:传入多条规则,验证 AI 是否同时遵循
|
||||
4. **规则冲突**:传入相互矛盾的规则,观察 AI 行为
|
||||
5. **超长规则**:传入超长内容,验证截断或错误处理
|
||||
|
||||
### 6.2 安全测试
|
||||
1. **提示词注入**:尝试在规则中注入恶意提示词
|
||||
2. **覆盖安全策略**:尝试用规则覆盖平台安全限制
|
||||
3. **特殊字符**:测试规则中包含特殊字符的情况
|
||||
|
||||
### 6.3 性能测试
|
||||
1. **大量规则**:测试 10+ 条规则的性能影响
|
||||
2. **高频请求**:测试规则注入对响应时间的影响
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
### 7.1 可能的错误场景
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| 规则内容为空字符串 | 视为无规则,正常处理 |
|
||||
| 规则内容超长 | 截断或返回错误 |
|
||||
| 规则包含非法内容 | 过滤或拒绝请求 |
|
||||
| 规则注入失败 | 降级为无规则对话 |
|
||||
|
||||
### 7.2 错误响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "RULES_TOO_LONG",
|
||||
"message": "个人规则内容超过长度限制(最大 10000 字符)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
### 8.1 基本功能
|
||||
- [ ] 能正确接收 `personalRules` 字段
|
||||
- [ ] 规则能正确注入到系统提示词
|
||||
- [ ] 规则关闭时不影响正常对话
|
||||
- [ ] 多条规则能同时生效
|
||||
|
||||
### 8.2 安全性
|
||||
- [ ] 规则不能覆盖平台安全策略
|
||||
- [ ] 有基本的内容安全检查
|
||||
- [ ] 日志中不记录完整规则内容
|
||||
|
||||
### 8.3 兼容性
|
||||
- [ ] 旧版本前端(无此字段)能正常工作
|
||||
- [ ] 字段为 `undefined` 时正常处理
|
||||
|
||||
## 9. 联系方式
|
||||
|
||||
如有疑问,请联系前端开发团队。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2026-03-07
|
||||
379
docs/webpack-optimization-guide.md
Normal file
379
docs/webpack-optimization-guide.md
Normal file
@ -0,0 +1,379 @@
|
||||
# Webpack 打包优化完整教程
|
||||
|
||||
## 目录
|
||||
1. [优化前的问题](#优化前的问题)
|
||||
2. [优化方案详解](#优化方案详解)
|
||||
3. [配置对比](#配置对比)
|
||||
4. [使用指南](#使用指南)
|
||||
5. [效果验证](#效果验证)
|
||||
|
||||
---
|
||||
|
||||
## 优化前的问题
|
||||
|
||||
### 原始配置存在的问题
|
||||
|
||||
```javascript
|
||||
// ❌ 问题1:固定使用 none 模式
|
||||
mode: 'none'
|
||||
// 导致:生产环境代码不压缩,体积大
|
||||
|
||||
// ❌ 问题2:没有 Tree Shaking
|
||||
// 导致:未使用的代码也被打包
|
||||
|
||||
// ❌ 问题3:ts-loader 默认配置
|
||||
loader: 'ts-loader'
|
||||
// 导致:每次编译都做类型检查,速度慢
|
||||
|
||||
// ❌ 问题4:没有性能监控
|
||||
// 导致:打包体积过大时不知道
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优化方案详解
|
||||
|
||||
### 1. 自动模式切换
|
||||
|
||||
**原理**:根据环境变量自动选择打包模式
|
||||
|
||||
```javascript
|
||||
// 优化前
|
||||
mode: 'none'
|
||||
|
||||
// 优化后
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none'
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 开发模式:代码可读,方便调试
|
||||
- 生产模式:自动压缩,体积减小 40-60%
|
||||
|
||||
---
|
||||
|
||||
### 2. Tree Shaking(摇树优化)
|
||||
|
||||
**原理**:移除未使用的代码
|
||||
|
||||
```javascript
|
||||
optimization: {
|
||||
minimize: process.env.NODE_ENV === 'production',
|
||||
usedExports: true // 标记未使用的导出
|
||||
}
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```javascript
|
||||
// utils.ts
|
||||
export function usedFunc() { }
|
||||
export function unusedFunc() { } // 不会被打包
|
||||
|
||||
// main.ts
|
||||
import { usedFunc } from './utils';
|
||||
```
|
||||
|
||||
**效果**:减少 10-30% 体积
|
||||
|
||||
---
|
||||
|
||||
### 3. 加快编译速度
|
||||
|
||||
**原理**:跳过类型检查,只做转译
|
||||
|
||||
```javascript
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true, // 跳过类型检查
|
||||
compilerOptions: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 类型检查交给 IDE 和 CI
|
||||
- 编译速度提升 50-70%
|
||||
|
||||
---
|
||||
|
||||
### 4. 自动清理旧文件
|
||||
|
||||
```javascript
|
||||
output: {
|
||||
clean: true // 每次打包前清空 dist 目录
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:避免旧文件残留
|
||||
|
||||
---
|
||||
|
||||
### 5. 性能监控
|
||||
|
||||
```javascript
|
||||
performance: {
|
||||
hints: 'warning',
|
||||
maxAssetSize: 2 * 1024 * 1024, // 2MB
|
||||
maxEntrypointSize: 2 * 1024 * 1024
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:超过 2MB 会警告
|
||||
|
||||
---
|
||||
|
||||
### 6. Source Map 优化
|
||||
|
||||
```javascript
|
||||
devtool: process.env.NODE_ENV === 'production'
|
||||
? 'hidden-source-map' // 生产:隐藏源码
|
||||
: 'nosources-source-map' // 开发:保留调试信息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 模块解析优化
|
||||
|
||||
```javascript
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
mainFields: ['module', 'main'] // 优先使用 ES 模块
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:更好的 Tree Shaking 效果
|
||||
|
||||
---
|
||||
|
||||
## 配置对比
|
||||
|
||||
### 优化前
|
||||
```javascript
|
||||
const extensionConfig = {
|
||||
target: 'node',
|
||||
mode: 'none', // 固定模式
|
||||
entry: './src/extension.ts',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2'
|
||||
// 没有 clean
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
use: [{ loader: 'ts-loader' }] // 默认配置
|
||||
}]
|
||||
},
|
||||
devtool: 'nosources-source-map' // 固定
|
||||
// 没有 optimization
|
||||
// 没有 performance
|
||||
};
|
||||
```
|
||||
|
||||
### 优化后
|
||||
```javascript
|
||||
const extensionConfig = {
|
||||
target: 'node',
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
|
||||
entry: './src/extension.ts',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
clean: true // ✅ 自动清理
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
mainFields: ['module', 'main'] // ✅ 优化解析
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
use: [{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true, // ✅ 加速编译
|
||||
compilerOptions: { sourceMap: true }
|
||||
}
|
||||
}]
|
||||
}]
|
||||
},
|
||||
devtool: process.env.NODE_ENV === 'production'
|
||||
? 'hidden-source-map'
|
||||
: 'nosources-source-map',
|
||||
optimization: {
|
||||
minimize: process.env.NODE_ENV === 'production',
|
||||
usedExports: true // ✅ Tree Shaking
|
||||
},
|
||||
performance: {
|
||||
hints: 'warning',
|
||||
maxAssetSize: 2 * 1024 * 1024,
|
||||
maxEntrypointSize: 2 * 1024 * 1024
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
# 单次编译
|
||||
pnpm run compile
|
||||
|
||||
# 监听模式(推荐)
|
||||
pnpm run watch
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 不压缩代码
|
||||
- 快速编译
|
||||
- 保留调试信息
|
||||
|
||||
---
|
||||
|
||||
### 生产模式
|
||||
|
||||
#### Windows
|
||||
```bash
|
||||
set NODE_ENV=production && pnpm run package
|
||||
```
|
||||
|
||||
#### macOS/Linux
|
||||
```bash
|
||||
NODE_ENV=production pnpm run package
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 代码压缩
|
||||
- Tree Shaking
|
||||
- 隐藏源码
|
||||
|
||||
---
|
||||
|
||||
### 一键打包 VSIX
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
set NODE_ENV=production && pnpm run package && npx vsce package
|
||||
|
||||
# macOS/Linux
|
||||
NODE_ENV=production pnpm run package && npx vsce package
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 效果验证
|
||||
|
||||
### 1. 查看打包体积
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
dir dist\extension.js
|
||||
|
||||
# macOS/Linux
|
||||
ls -lh dist/extension.js
|
||||
```
|
||||
|
||||
### 2. 对比测试
|
||||
|
||||
| 模式 | 体积 | 编译时间 | 可读性 |
|
||||
|------|------|----------|--------|
|
||||
| 开发模式 | ~800KB | 5s | 高 |
|
||||
| 生产模式 | ~400KB | 8s | 低(压缩) |
|
||||
|
||||
### 3. 性能警告
|
||||
|
||||
如果看到这个警告:
|
||||
```
|
||||
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (2 MiB).
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
1. 检查是否引入了不必要的依赖
|
||||
2. 将大型库添加到 `externals`
|
||||
3. 考虑代码分割
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么开发模式不压缩?
|
||||
**A**: 保持代码可读性,方便调试和查看错误堆栈。
|
||||
|
||||
### Q2: transpileOnly 会影响类型安全吗?
|
||||
**A**: 不会。IDE 和 `tsc --noEmit` 仍会做类型检查。
|
||||
|
||||
### Q3: 如何查看 Tree Shaking 效果?
|
||||
**A**: 使用 `webpack-bundle-analyzer`:
|
||||
```bash
|
||||
pnpm add -D webpack-bundle-analyzer
|
||||
```
|
||||
|
||||
### Q4: 生产模式编译失败怎么办?
|
||||
**A**: 先用开发模式确认代码无误,再切换生产模式。
|
||||
|
||||
---
|
||||
|
||||
## 进阶优化(可选)
|
||||
|
||||
### 1. 排除更多依赖
|
||||
|
||||
```javascript
|
||||
externals: {
|
||||
vscode: 'commonjs vscode',
|
||||
'node-notifier': 'commonjs node-notifier',
|
||||
// 如果这些库很大,可以排除
|
||||
'vcdrom': 'commonjs vcdrom',
|
||||
'@wavedrom/doppler': 'commonjs @wavedrom/doppler'
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 代码分割
|
||||
|
||||
```javascript
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
priority: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 缓存优化
|
||||
|
||||
```javascript
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
experimentalWatchApi: true // 监听模式优化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
通过这些优化:
|
||||
- ✅ 生产体积减少 40-60%
|
||||
- ✅ 编译速度提升 50-70%
|
||||
- ✅ 自动清理和监控
|
||||
- ✅ 更好的开发体验
|
||||
|
||||
**推荐工作流**:
|
||||
1. 开发时用 `pnpm run watch`
|
||||
2. 提交前用 `pnpm run compile` 检查
|
||||
3. 发布前用生产模式打包
|
||||
87
docs/文档集功能需求文档.md
Normal file
87
docs/文档集功能需求文档.md
Normal file
@ -0,0 +1,87 @@
|
||||
# 文档集功能需求文档
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
文档集功能允许用户管理文档,在添加上下文时可以选择文档加载到输入框中,发送给 AI 作为对话上下文。
|
||||
|
||||
## 2. 核心流程
|
||||
|
||||
### 2.1 添加文档集入口
|
||||
1. 用户点击"添加上下文"中的"文档"按钮
|
||||
2. 如果文档集为空,显示"添加文档集"按钮
|
||||
3. 点击按钮跳转到"设置 → 上下文"页面
|
||||
|
||||
### 2.2 创建文档集
|
||||
1. 在设置的上下文页面,点击"添加文档集"按钮
|
||||
2. 弹出文档集创建对话框
|
||||
3. 用户输入文档集名称
|
||||
4. 用户点击"添加文件"按钮,选择文件
|
||||
5. 系统验证文件(格式、大小、数量)
|
||||
6. 显示已选文件列表和统计信息
|
||||
7. 用户点击"确定",保存文档集
|
||||
8. 系统持久化文档集信息到 `globalState`
|
||||
|
||||
### 2.3 使用文档
|
||||
1. 用户点击"添加上下文"中的"文档"按钮
|
||||
2. 显示所有文档列表(自动同步设置中的文档集)
|
||||
3. 用户点击选择一个或多个文档
|
||||
4. 文档路径加载到输入框中
|
||||
5. 用户发送消息,后端读取文档内容作为上下文
|
||||
|
||||
### 2.4 管理文档集
|
||||
1. 在设置的上下文页面查看文档列表
|
||||
2. 显示每个文档的更新时间
|
||||
3. 支持修改文档名称
|
||||
4. 支持删除文档
|
||||
|
||||
## 3. 功能详细需求
|
||||
|
||||
### 3.1 前端需求
|
||||
|
||||
#### 3.1.1 添加上下文 - 文档按钮
|
||||
**功能**:
|
||||
- 点击"文档"按钮,显示文档列表弹窗
|
||||
- 如果没有文档,显示"添加文档集"按钮
|
||||
- 点击"添加文档集"按钮,跳转到设置的上下文页面
|
||||
|
||||
#### 3.1.2 文档列表弹窗
|
||||
**UI 元素**:
|
||||
- 文档列表(显示所有文档集中的文档)
|
||||
- 每个文档显示:名称
|
||||
- 支持多选
|
||||
- 确定/取消按钮
|
||||
|
||||
**交互逻辑**:
|
||||
- 自动同步设置中的文档集
|
||||
- 点击文档选中/取消选中
|
||||
- 点击确定,将选中文档路径加载到输入框
|
||||
|
||||
#### 3.1.3 设置 - 上下文页面
|
||||
**UI 元素**:
|
||||
- "添加文档集"按钮
|
||||
- 文档列表
|
||||
- 每个文档显示:名称、更新时间、修改名称按钮、删除按钮
|
||||
|
||||
**交互逻辑**:
|
||||
- 点击"添加文档集"打开创建对话框
|
||||
- 点击修改名称,弹出输入框修改
|
||||
- 点击删除,删除二次确认弹窗 确认删除文档
|
||||
|
||||
#### 3.1.4 文档集创建对话框
|
||||
**UI 元素**:
|
||||
|
||||
- 文档集名称输入框
|
||||
- 添加文件按钮
|
||||
- 文件列表显示区域
|
||||
- 确定/取消按钮
|
||||
|
||||
**交互逻辑**:
|
||||
- 点击"添加文件"触发文件选择器
|
||||
- 显示已选文件的相对路径和大小
|
||||
- 支持删除单个文件
|
||||
- 实时更新统计信息
|
||||
|
||||
### 3.2 后端需求
|
||||
|
||||
后端只需要支持读取pdf,txt,.v,.sv,.md这些类型的文档
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "iccoder",
|
||||
"displayName": "IC Coder: Agentic Verilog Platform",
|
||||
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
||||
"version": "1.0.12",
|
||||
"version": "1.13.5",
|
||||
"publisher": "ICCoderAgenticVerilogPlatform",
|
||||
"engines": {
|
||||
"vscode": "^1.80.0"
|
||||
@ -117,6 +117,11 @@
|
||||
"configuration": {
|
||||
"title": "IC Coder",
|
||||
"properties": {
|
||||
"ic-coder.personalRulesEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "启用个人规则"
|
||||
},
|
||||
"ic-coder.enableSystemNotification": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
|
||||
@ -34,7 +34,7 @@ export function getCodeHighlightStyles(): string {
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
margin: 52px 0 12px 0;
|
||||
position: relative;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import * as vscode from "vscode";
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "prod";
|
||||
const CURRENT_ENV: Environment = "test";
|
||||
|
||||
/** 服务等级类型 */
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
@ -42,9 +42,9 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
},
|
||||
/** 测试服务器环境 - 通过 Gateway 路由 */
|
||||
test: {
|
||||
backendUrl: "http://192.168.1.108:2029/iccoder",
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
loginUrl: "http://192.168.1.108:2005/login",
|
||||
backendUrl: "http://192.168.1.134:2233",
|
||||
backendUrlStrongeLoop: "http://192.168.1.134:2233",
|
||||
loginUrl: "http://192.168.1.134/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max",
|
||||
|
||||
@ -205,3 +205,8 @@ export const successIconSvg = `<svg t="1771828214449" class="icon" viewBox="0 0
|
||||
* 任务完成的图标svg
|
||||
*/
|
||||
export const taskCompleteIconSvg = `<svg t="1773302386044" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4798" width="16" height="16"><path d="M512 42.666667C253.866667 42.666667 42.666667 253.866667 42.666667 512s211.2 469.333333 469.333333 469.333333 469.333333-211.2 469.333333-469.333333S770.133333 42.666667 512 42.666667z m221.866667 377.6L488.533333 663.466667c-8.533333 8.533333-19.2 12.8-29.866666 12.8s-21.333333-4.266667-29.866667-12.8l-138.666667-138.666667c-17.066667-17.066667-17.066667-42.666667 0-59.733333 17.066667-17.066667 42.666667-17.066667 59.733334 0l108.8 108.8 215.466666-215.466667c17.066667-17.066667 42.666667-17.066667 59.733334 0 17.066667 17.066667 17.066667 44.8 0 61.866667z" fill="#1afa29" p-id="4799" data-spm-anchor-id="a313x.search_index.0.i0.123d3a812ZEn1Z" class=""></path></svg>`;
|
||||
|
||||
/**
|
||||
* 个人规则的图标svg
|
||||
*/
|
||||
export const peopleRules = `<svg t="1772851533961" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11188" width="16" height="16"><path d="M652.8 534.4c70.4-44.8 115.2-124.8 115.2-214.4 0-140.8-115.2-256-256-256s-256 115.2-256 256c0 89.6 44.8 169.6 115.2 214.4C192 592 64 761.6 64 960h64c0-211.2 172.8-384 384-384s384 172.8 384 384h64c0-198.4-128-368-307.2-425.6zM512 512c-105.6 0-192-86.4-192-192s86.4-192 192-192 192 86.4 192 192-86.4 192-192 192z" fill="#6caed4" p-id="11189"></path></svg>`;
|
||||
|
||||
@ -11,10 +11,13 @@ import { isTokenExpired } from "./utils/jwtUtils";
|
||||
import { NotificationService } from "./services/notificationService";
|
||||
import { InvitationService } from "./services/invitationService";
|
||||
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
|
||||
import { initializeContextHelper } from "./panels/helpers/contextHelper";
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
initializeContextHelper(context);
|
||||
|
||||
// 创建装饰类型(代码旁边的提示)
|
||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||
after: {
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
setupBalanceUpdateCallback,
|
||||
} from "./helpers/userInfoHelper";
|
||||
import { handleWebviewMessage } from "./helpers/messageRouter";
|
||||
import { getDocumentSets } from "./helpers/contextHelper";
|
||||
|
||||
function getIconUris(
|
||||
webview: vscode.Webview,
|
||||
@ -123,6 +124,13 @@ export async function showICHelperPanel(
|
||||
await sendUserInfoToWebview(panel, context);
|
||||
setupBalanceUpdateCallback(panel, context);
|
||||
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({
|
||||
command: "documentSetSaved",
|
||||
documentSets: getDocumentSets(),
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const pendingMessage = context.globalState.get("pendingMessage") as any;
|
||||
if (pendingMessage) {
|
||||
await context.globalState.update("pendingMessage", undefined);
|
||||
|
||||
@ -6,6 +6,27 @@
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
let globalContext: vscode.ExtensionContext;
|
||||
const DOCUMENT_SETS_KEY = "iccoder.documentSets";
|
||||
|
||||
export interface DocumentFile {
|
||||
name: string;
|
||||
absolutePath: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface DocumentSet {
|
||||
id: string;
|
||||
name: string;
|
||||
files: DocumentFile[];
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export function initializeContextHelper(context: vscode.ExtensionContext) {
|
||||
globalContext = context;
|
||||
loadDocumentSets();
|
||||
}
|
||||
|
||||
export async function handleAddContextFile(panel: vscode.WebviewPanel) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
@ -102,3 +123,126 @@ export async function handleAddContextDocument(panel: vscode.WebviewPanel) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleAddContextDocumentSet(panel: vscode.WebviewPanel) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await vscode.workspace.findFiles(
|
||||
"**/*.{md,txt,pdf,doc,docx}",
|
||||
"**/node_modules/**",
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceDocumentSetList",
|
||||
files: files.map((uri) => ({
|
||||
path: uri.fsPath,
|
||||
relativePath: vscode.workspace.asRelativePath(uri),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleGetDocumentSetList(panel: vscode.WebviewPanel) {
|
||||
panel.webview.postMessage({
|
||||
command: "showDocumentSetList",
|
||||
documents: documentSets,
|
||||
});
|
||||
}
|
||||
|
||||
let documentSet: DocumentFile[] = [];
|
||||
|
||||
export function getDocumentSet() {
|
||||
return documentSet;
|
||||
}
|
||||
|
||||
export function addToDocumentSet(docs: DocumentFile[]) {
|
||||
documentSet = [...documentSet, ...docs];
|
||||
}
|
||||
|
||||
export function saveDocumentSet(
|
||||
docs: DocumentFile[],
|
||||
name: string,
|
||||
panel: vscode.WebviewPanel
|
||||
) {
|
||||
const newDocSet: DocumentSet = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
files: docs,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
documentSets.push(newDocSet);
|
||||
persistDocumentSets();
|
||||
panel.webview.postMessage({
|
||||
command: "documentSetSaved",
|
||||
documentSets: documentSets,
|
||||
});
|
||||
}
|
||||
|
||||
let documentSets: DocumentSet[] = [];
|
||||
|
||||
function loadDocumentSets() {
|
||||
const saved =
|
||||
globalContext.globalState.get<Array<any>>(DOCUMENT_SETS_KEY) ||
|
||||
globalContext.globalState.get<Array<any>>("documentSets", []);
|
||||
if (saved) {
|
||||
documentSets = saved.map((item: any) => ({
|
||||
id: String(item.id),
|
||||
name: String(item.name || ""),
|
||||
files: Array.isArray(item.files)
|
||||
? item.files.map((file: any) => ({
|
||||
name: String(file.name || ""),
|
||||
absolutePath: String(file.absolutePath || file.path || ""),
|
||||
size: Number(file.size || 0),
|
||||
}))
|
||||
: Array.isArray(item.documents)
|
||||
? item.documents.map((file: any) => ({
|
||||
name: String(
|
||||
file.name ||
|
||||
file.relativePath?.split(/[\\/]/).pop() ||
|
||||
file.path?.split(/[\\/]/).pop() ||
|
||||
"",
|
||||
),
|
||||
absolutePath: String(file.absolutePath || file.path || ""),
|
||||
size: Number(file.size || 0),
|
||||
}))
|
||||
: [],
|
||||
updatedAt:
|
||||
typeof item.updatedAt === "number"
|
||||
? item.updatedAt
|
||||
: new Date(item.updatedAt || Date.now()).getTime(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function persistDocumentSets() {
|
||||
globalContext.globalState.update(DOCUMENT_SETS_KEY, documentSets);
|
||||
}
|
||||
|
||||
export function getDocumentSets() {
|
||||
return documentSets;
|
||||
}
|
||||
|
||||
export function deleteDocumentSet(id: string, panel: vscode.WebviewPanel) {
|
||||
documentSets = documentSets.filter(ds => ds.id !== id);
|
||||
persistDocumentSets();
|
||||
panel.webview.postMessage({
|
||||
command: "documentSetSaved",
|
||||
documentSets: documentSets,
|
||||
});
|
||||
}
|
||||
|
||||
export function changeDocumentSetName(id: string, newName: string, panel: vscode.WebviewPanel) {
|
||||
const docSet = documentSets.find(ds => ds.id === id);
|
||||
if (docSet) {
|
||||
docSet.name = newName;
|
||||
docSet.updatedAt = Date.now();
|
||||
persistDocumentSets();
|
||||
panel.webview.postMessage({
|
||||
command: "documentSetSaved",
|
||||
documentSets: documentSets,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,16 +22,28 @@ import {
|
||||
handleOpenFileDiff,
|
||||
startChangeSession,
|
||||
} from "../../utils/messageHandler";
|
||||
import {
|
||||
loadPersonalRules,
|
||||
savePersonalRule,
|
||||
updatePersonalRule,
|
||||
deletePersonalRule,
|
||||
updatePersonalRulesEnabled,
|
||||
} from "../../utils/personalRulesManager";
|
||||
import { compactDialog } from "../../services/apiClient";
|
||||
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||
import { getCachedUserInfo } from "../../services/userService";
|
||||
import { loadConversationHistory, selectConversation } from "./conversationHelper";
|
||||
import {
|
||||
loadConversationHistory,
|
||||
selectConversation,
|
||||
} from "./conversationHelper";
|
||||
import { getVCDFileInfo } from "./vcdHelper";
|
||||
import {
|
||||
handleAddContextFile,
|
||||
handleAddContextFolder,
|
||||
handleAddContextImage,
|
||||
handleAddContextDocument,
|
||||
handleAddContextDocumentSet,
|
||||
handleGetDocumentSetList,
|
||||
} from "./contextHelper";
|
||||
import { openFile, openFileWithSelection, openFilePathTag } from "./fileHelper";
|
||||
|
||||
@ -130,16 +142,16 @@ export async function handleWebviewMessage(
|
||||
break;
|
||||
|
||||
case "loadConversationHistory":
|
||||
loadConversationHistory(
|
||||
panel,
|
||||
message.offset || 0,
|
||||
message.limit || 10,
|
||||
);
|
||||
loadConversationHistory(panel, message.offset || 0, message.limit || 10);
|
||||
break;
|
||||
|
||||
case "selectConversation":
|
||||
if (message.conversationId) {
|
||||
selectConversation(panel, message.conversationId, context.extensionPath);
|
||||
selectConversation(
|
||||
panel,
|
||||
message.conversationId,
|
||||
context.extensionPath,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -253,7 +265,9 @@ export async function handleWebviewMessage(
|
||||
verified: true,
|
||||
});
|
||||
} else {
|
||||
const { InvitationService } = require("../../services/invitationService");
|
||||
const {
|
||||
InvitationService,
|
||||
} = require("../../services/invitationService");
|
||||
const isVerified = await InvitationService.isVerified(context);
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeStatus",
|
||||
@ -284,7 +298,9 @@ export async function handleWebviewMessage(
|
||||
|
||||
case "checkTrialExpiration":
|
||||
{
|
||||
const { TrialExpirationService } = require("../../services/trialExpirationService");
|
||||
const {
|
||||
TrialExpirationService,
|
||||
} = require("../../services/trialExpirationService");
|
||||
const trialService = new TrialExpirationService(context, panel);
|
||||
await trialService.checkExpiration();
|
||||
}
|
||||
@ -292,7 +308,9 @@ export async function handleWebviewMessage(
|
||||
|
||||
case "verifyInvitationCode":
|
||||
{
|
||||
const { InvitationService } = require("../../services/invitationService");
|
||||
const {
|
||||
InvitationService,
|
||||
} = require("../../services/invitationService");
|
||||
const result = await InvitationService.verifyCode(message.code);
|
||||
|
||||
if (result.success) {
|
||||
@ -356,6 +374,104 @@ export async function handleWebviewMessage(
|
||||
await handleAddContextFolder(panel);
|
||||
break;
|
||||
|
||||
case "addContextDocumentSet":
|
||||
await handleAddContextDocumentSet(panel);
|
||||
break;
|
||||
|
||||
case "getDocumentSetList":
|
||||
await handleGetDocumentSetList(panel);
|
||||
break;
|
||||
|
||||
case "saveDocumentSet":
|
||||
const { saveDocumentSet } = await import("./contextHelper");
|
||||
saveDocumentSet(message.documents || [], message.name || "", panel);
|
||||
break;
|
||||
|
||||
case "deleteDocumentSet":
|
||||
const { deleteDocumentSet } = await import("./contextHelper");
|
||||
deleteDocumentSet(message.id, panel);
|
||||
break;
|
||||
|
||||
case "changeDocumentSetName":
|
||||
const { changeDocumentSetName } = await import("./contextHelper");
|
||||
changeDocumentSetName(message.id, message.newName, panel);
|
||||
break;
|
||||
|
||||
case "openContextSettings":
|
||||
panel.webview.postMessage({
|
||||
command: "openSettingsTab",
|
||||
tab: "context",
|
||||
});
|
||||
break;
|
||||
|
||||
case "selectFilesForDocset":
|
||||
const uris = await vscode.window.showOpenDialog({
|
||||
canSelectMany: true,
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
openLabel: "选择文件",
|
||||
filters: {
|
||||
支持的文件: ["md", "txt", "v", "sv", "pdf"],
|
||||
},
|
||||
});
|
||||
|
||||
if (uris && uris.length > 0) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const selectedFiles: Array<{
|
||||
name: string;
|
||||
absolutePath: string;
|
||||
size: number;
|
||||
}> = [];
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
const MAX_TOTAL_SIZE = 50 * 1024 * 1024; // 50 MB
|
||||
const MAX_FILES = 1000;
|
||||
let totalSize = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const uri of uris) {
|
||||
const filePath = uri.fsPath;
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
if (![".md", ".txt", ".v", ".sv", ".pdf"].includes(ext)) {
|
||||
errors.push(`文件 ${path.basename(filePath)} 格式不支持`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size > MAX_FILE_SIZE) {
|
||||
errors.push(`文件 ${path.basename(filePath)} 超过 10 MB`);
|
||||
continue;
|
||||
}
|
||||
if (totalSize + stat.size > MAX_TOTAL_SIZE) {
|
||||
errors.push("文档集总大小超过 50 MB");
|
||||
break;
|
||||
}
|
||||
if (selectedFiles.length >= MAX_FILES) {
|
||||
errors.push("文件数量超过 1000 个");
|
||||
break;
|
||||
}
|
||||
|
||||
selectedFiles.push({
|
||||
name: path.basename(filePath),
|
||||
absolutePath: filePath,
|
||||
size: stat.size,
|
||||
});
|
||||
totalSize += stat.size;
|
||||
} catch (err) {
|
||||
errors.push(`无法读取文件 ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "filesSelectedForDocset",
|
||||
files: selectedFiles,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "addContextImage":
|
||||
await handleAddContextImage(panel);
|
||||
break;
|
||||
@ -392,5 +508,84 @@ export async function handleWebviewMessage(
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
break;
|
||||
|
||||
case "loadPersonalRules":
|
||||
{
|
||||
const data = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "savePersonalRule":
|
||||
{
|
||||
const success = await savePersonalRule(
|
||||
message.name,
|
||||
message.content,
|
||||
message.enabled,
|
||||
);
|
||||
if (success) {
|
||||
const data = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "updatePersonalRule":
|
||||
{
|
||||
const success = await updatePersonalRule(
|
||||
message.filename,
|
||||
message.name,
|
||||
message.content,
|
||||
message.enabled,
|
||||
);
|
||||
if (success) {
|
||||
const data = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "deletePersonalRule":
|
||||
{
|
||||
const success = await deletePersonalRule(message.filename);
|
||||
if (success) {
|
||||
const data = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "updatePersonalRulesEnabled":
|
||||
{
|
||||
const success = await updatePersonalRulesEnabled(message.enabled);
|
||||
if (success) {
|
||||
const data = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "loadDocumentSets":
|
||||
const { getDocumentSets } = await import("./contextHelper");
|
||||
panel.webview.postMessage({
|
||||
command: "documentSetSaved",
|
||||
documentSets: getDocumentSets(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ import type {
|
||||
PlanConfirmEvent,
|
||||
} from "../types/api";
|
||||
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
|
||||
import { getActiveRules } from "../utils/personalRulesManager";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
|
||||
import { updateCachedBalance } from "./creditsService";
|
||||
@ -502,6 +503,7 @@ export class DialogSession {
|
||||
compactedData: compactedData || undefined,
|
||||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||||
knowledgeData: knowledgeData || undefined,
|
||||
personalRules: getActiveRules() || undefined,
|
||||
};
|
||||
|
||||
// 追踪用户消息
|
||||
|
||||
@ -162,6 +162,8 @@ export async function startStreamDialog(
|
||||
const body = JSON.stringify(request);
|
||||
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
|
||||
console.log("[SSE] 完整请求体:", request);
|
||||
console.log("[SSE] 请求体 JSON:", body);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options: http.RequestOptions = {
|
||||
|
||||
@ -48,6 +48,8 @@ export interface DialogRequest {
|
||||
newMessages?: CompactedMessage[];
|
||||
/** 知识图谱数据(JSON 字符串,用于恢复知识图谱) */
|
||||
knowledgeData?: string;
|
||||
/** 个人规则 */
|
||||
personalRules?: string;
|
||||
}
|
||||
|
||||
// ============== SSE 事件类型 ==============
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
checkVerilogProject,
|
||||
checkIverilogAvailable,
|
||||
} from "./iverilogRunner";
|
||||
import { createVivadoProject, runVivadoSynthesis } from "./vivadoRunner";
|
||||
import { ChatHistoryManager } from "./chatHistoryManager";
|
||||
import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
@ -269,8 +268,19 @@ async function handleUserMessageWithBackend(
|
||||
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}`;
|
||||
const docTypes = new Set(["file", "document", "docset"]);
|
||||
const regularPaths = contextItems
|
||||
.filter((item) => !docTypes.has(item.type))
|
||||
.map((item) => item.path);
|
||||
const docTags = contextItems
|
||||
.filter((item) => docTypes.has(item.type))
|
||||
.map((item) => `<doc>${item.path}</doc>`);
|
||||
if (regularPaths.length > 0) {
|
||||
enhancedText = `${regularPaths.join("\n")}\n\n${enhancedText}`;
|
||||
}
|
||||
if (docTags.length > 0) {
|
||||
enhancedText = `${enhancedText}\n\n${docTags.join("\n")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
||||
@ -308,8 +318,8 @@ async function handleUserMessageWithBackend(
|
||||
|
||||
onSegmentUpdate: (segments) => {
|
||||
// 过滤掉包含 [调用工具:xxx] 的段落
|
||||
const filteredSegments = segments.filter(seg => {
|
||||
if (seg.type === 'text' && typeof seg.content === 'string') {
|
||||
const filteredSegments = segments.filter((seg) => {
|
||||
if (seg.type === "text" && typeof seg.content === "string") {
|
||||
return !/\[调用工具:.+?\]/.test(seg.content);
|
||||
}
|
||||
return true;
|
||||
@ -428,7 +438,7 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 错误: ${message}`,
|
||||
text: `❌ 服务繁忙,请稍后重试`,
|
||||
});
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
@ -849,10 +859,10 @@ async function handleFileOperation(
|
||||
const errorMsg = error instanceof Error ? error.message : "操作失败";
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ ${errorMsg}`,
|
||||
text: `❌ 服务繁忙,请稍后重试`,
|
||||
});
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
await historyManager.addAiMessage(`❌ ${errorMsg}`);
|
||||
await historyManager.addAiMessage(`❌ 服务繁忙,请稍后重试`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1469,38 +1479,3 @@ export async function handleOpenFileDiff(
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
194
src/utils/personalRulesManager.ts
Normal file
194
src/utils/personalRulesManager.ts
Normal file
@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 个人规则管理工具
|
||||
* 功能:读写个人规则文件
|
||||
* 依赖:vscode, fs, path
|
||||
* 使用场景:保存和加载用户的个人规则
|
||||
*/
|
||||
|
||||
import * as vscode from "vscode";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
/**
|
||||
* 获取规则目录路径
|
||||
*/
|
||||
function getRulesDir(): string {
|
||||
return path.join(os.homedir(), ".iccoder", "rules");
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保规则目录存在
|
||||
*/
|
||||
function ensureRulesDir(): void {
|
||||
const dir = getRulesDir();
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件内容中提取规则名称
|
||||
*/
|
||||
function extractRuleName(content: string): string {
|
||||
const lines = content.split("\n");
|
||||
const firstLine = lines[0]?.trim();
|
||||
if (firstLine && firstLine.startsWith("# ")) {
|
||||
return firstLine.substring(2).trim();
|
||||
}
|
||||
return content.substring(0, 30) + (content.length > 30 ? "..." : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新规则
|
||||
*/
|
||||
export async function savePersonalRule(
|
||||
name: string,
|
||||
content: string,
|
||||
enabled: boolean,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
ensureRulesDir();
|
||||
|
||||
const timestamp = Date.now();
|
||||
const filename = `rule-${timestamp}.md`;
|
||||
const filePath = path.join(getRulesDir(), filename);
|
||||
|
||||
const fileContent = `# ${name}\n\n${content}`;
|
||||
fs.writeFileSync(filePath, fileContent, "utf-8");
|
||||
|
||||
await vscode.workspace
|
||||
.getConfiguration("ic-coder")
|
||||
.update(
|
||||
"personalRulesEnabled",
|
||||
enabled,
|
||||
vscode.ConfigurationTarget.Global,
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage("规则已保存");
|
||||
return true;
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`保存规则失败: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新规则
|
||||
*/
|
||||
export async function updatePersonalRule(
|
||||
filename: string,
|
||||
name: string,
|
||||
content: string,
|
||||
enabled: boolean,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const filePath = path.join(getRulesDir(), filename);
|
||||
const fileContent = `# ${name}\n\n${content}`;
|
||||
fs.writeFileSync(filePath, fileContent, "utf-8");
|
||||
|
||||
await vscode.workspace
|
||||
.getConfiguration("ic-coder")
|
||||
.update(
|
||||
"personalRulesEnabled",
|
||||
enabled,
|
||||
vscode.ConfigurationTarget.Global,
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage("规则已更新");
|
||||
return true;
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`更新规则失败: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除规则
|
||||
*/
|
||||
export async function deletePersonalRule(filename: string): Promise<boolean> {
|
||||
try {
|
||||
const filePath = path.join(getRulesDir(), filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
vscode.window.showInformationMessage("规则已删除");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`删除规则失败: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载所有规则
|
||||
*/
|
||||
export function loadPersonalRules(): {
|
||||
rules: Array<{ filename: string; name: string; content: string }>;
|
||||
enabled: boolean;
|
||||
} {
|
||||
const enabled = vscode.workspace
|
||||
.getConfiguration("ic-coder")
|
||||
.get<boolean>("personalRulesEnabled", true);
|
||||
const dir = getRulesDir();
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
return { rules: [], enabled };
|
||||
}
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
||||
const rules = files.map((filename) => {
|
||||
const content = fs.readFileSync(path.join(dir, filename), "utf-8");
|
||||
const lines = content.split("\n");
|
||||
let name = "";
|
||||
let actualContent = content;
|
||||
|
||||
if (lines[0]?.trim().startsWith("# ")) {
|
||||
name = lines[0].substring(2).trim();
|
||||
actualContent = lines.slice(2).join("\n").trim();
|
||||
} else {
|
||||
name = extractRuleName(content);
|
||||
}
|
||||
|
||||
return { filename, name, content: actualContent };
|
||||
});
|
||||
return { rules, enabled };
|
||||
} catch (error) {
|
||||
console.error("读取规则失败:", error);
|
||||
return { rules: [], enabled };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新个人规则启用状态
|
||||
*/
|
||||
export async function updatePersonalRulesEnabled(
|
||||
enabled: boolean,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await vscode.workspace
|
||||
.getConfiguration("ic-coder")
|
||||
.update(
|
||||
"personalRulesEnabled",
|
||||
enabled,
|
||||
vscode.ConfigurationTarget.Global,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("更新规则启用状态失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前生效的所有规则内容
|
||||
*/
|
||||
export function getActiveRules(): string | null {
|
||||
const { rules, enabled } = loadPersonalRules();
|
||||
if (!enabled || rules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return rules.map((r) => r.content).join("\n\n");
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
/**
|
||||
* 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')
|
||||
};
|
||||
}
|
||||
@ -1,246 +0,0 @@
|
||||
/**
|
||||
* 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}`
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -41,18 +41,15 @@ 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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- <div class="context-menu-item" onclick="handleAddImage()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>图片</span>
|
||||
</div> -->
|
||||
<!-- <div class="context-menu-item" onclick="handleAddDocument()">
|
||||
<div class="context-menu-item" onclick="handleAddDocumentSet()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>文档库</span>
|
||||
</div> -->
|
||||
<span>文档集</span>
|
||||
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件/文件夹列表视图 -->
|
||||
@ -382,6 +379,48 @@ export function getContextButtonScript(): string {
|
||||
vscode.postMessage({ command: 'addContextFolder' });
|
||||
}
|
||||
|
||||
// 显示文档集列表
|
||||
function showDocumentSetList() {
|
||||
vscode.postMessage({ command: 'getDocumentSetList' });
|
||||
}
|
||||
|
||||
// 显示文档集视图
|
||||
function showDocumentSetView(documents) {
|
||||
const mainMenu = document.getElementById('contextMenuMain');
|
||||
const listView = document.getElementById('contextMenuList');
|
||||
const titleEl = document.getElementById('contextMenuListTitle');
|
||||
const bodyEl = document.getElementById('contextMenuListBody');
|
||||
|
||||
if (mainMenu && listView && titleEl && bodyEl) {
|
||||
mainMenu.style.display = 'none';
|
||||
listView.style.display = 'flex';
|
||||
titleEl.textContent = '文档集';
|
||||
|
||||
if (documents.length === 0) {
|
||||
bodyEl.innerHTML = \`
|
||||
<div class="context-empty" style="padding: 40px 20px; text-align: center;">
|
||||
<p style="margin: 0 0 12px 0; color: var(--vscode-descriptionForeground);">暂无文档</p>
|
||||
<button class="context-add-empty-btn" onclick="addDocumentToSet()" style="padding: 8px 20px; background: transparent; color: var(--vscode-textLink-foreground); border: 1px solid var(--vscode-textLink-foreground); border-radius: 4px; cursor: pointer; font-size: 13px;">添加文档集</button>
|
||||
</div>
|
||||
\`;
|
||||
} else {
|
||||
currentListType = 'documentSetItem';
|
||||
currentListData = documents;
|
||||
filteredListData = documents;
|
||||
selectedItems.clear();
|
||||
renderDocumentSetList(documents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文档到文档集
|
||||
function addDocumentToSet() {
|
||||
vscode.postMessage({ command: 'openContextSettings' });
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 添加文档集项到上下文(已删除,使用统一的确认选择)
|
||||
|
||||
// 返回主菜单
|
||||
function backToMainMenu() {
|
||||
const mainMenu = document.getElementById('contextMenuMain');
|
||||
@ -442,6 +481,43 @@ export function getContextButtonScript(): string {
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 渲染文档集列表
|
||||
function renderDocumentSetList(data) {
|
||||
const body = document.getElementById('contextMenuListBody');
|
||||
if (!body) return;
|
||||
|
||||
filteredListData = data || [];
|
||||
|
||||
body.innerHTML = filteredListData.map((item, index) => \`
|
||||
<div class="context-menu-list-item \${selectedItems.has(item.id) ? 'selected' : ''}" onclick="toggleDocumentSetSelection(\${index})">
|
||||
<input type="checkbox" id="item-\${index}" \${selectedItems.has(item.id) ? 'checked' : ''} />
|
||||
<label>\${item.name}</label>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 切换文档集选择
|
||||
function toggleDocumentSetSelection(index) {
|
||||
const selectedItem = filteredListData[index];
|
||||
if (!selectedItem) return;
|
||||
|
||||
const selectedId = selectedItem.id;
|
||||
const checkbox = document.getElementById('item-' + index);
|
||||
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
||||
|
||||
if (selectedItems.has(selectedId)) {
|
||||
selectedItems.delete(selectedId);
|
||||
if (checkbox) checkbox.checked = false;
|
||||
if (item) item.classList.remove('selected');
|
||||
} else {
|
||||
selectedItems.add(selectedId);
|
||||
if (checkbox) checkbox.checked = true;
|
||||
if (item) item.classList.add('selected');
|
||||
}
|
||||
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
// 切换项选择
|
||||
function toggleItemSelection(index) {
|
||||
const selectedItem = filteredListData[index];
|
||||
@ -475,13 +551,28 @@ export function getContextButtonScript(): string {
|
||||
// 确认选择
|
||||
function confirmSelection() {
|
||||
try {
|
||||
if (currentListType === 'documentSetItem') {
|
||||
const selected = currentListData.filter(item => selectedItems.has(item.id));
|
||||
selected.forEach(docSet => {
|
||||
(docSet.files || []).forEach(doc => {
|
||||
addContextItem('docset', doc.absolutePath, doc.name || doc.absolutePath);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const selected = currentListData.filter(item => selectedItems.has(item.path));
|
||||
|
||||
if (selected.length > 0) {
|
||||
if (currentListType === 'documentSet') {
|
||||
vscode.postMessage({
|
||||
command: 'saveDocumentSet',
|
||||
documents: selected
|
||||
});
|
||||
} else {
|
||||
selected.forEach(item => {
|
||||
addContextItem(currentListType, item.path, item.relativePath || item.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const button = document.querySelector('.add-context-button');
|
||||
@ -507,15 +598,24 @@ export function getContextButtonScript(): string {
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 添加文档集
|
||||
function handleAddDocumentSet() {
|
||||
showDocumentSetList();
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const searchInput = document.getElementById('contextMenuSearch');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const keyword = (e.target.value || '').toLowerCase().trim();
|
||||
const filtered = currentListData.filter(item =>
|
||||
(item.relativePath || item.path || '').toLowerCase().includes(keyword)
|
||||
(item.name || item.relativePath || item.path || '').toLowerCase().includes(keyword)
|
||||
);
|
||||
if (currentListType === 'documentSetItem') {
|
||||
renderDocumentSetList(filtered);
|
||||
} else {
|
||||
renderList(filtered);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -527,6 +627,10 @@ export function getContextButtonScript(): string {
|
||||
switchToListView('选择文件', 'file', message.files);
|
||||
} else if (message.command === 'showWorkspaceFolderList') {
|
||||
switchToListView('选择文件夹', 'folder', message.folders);
|
||||
} else if (message.command === 'showWorkspaceDocumentSetList') {
|
||||
switchToListView('选择文档', 'documentSet', message.files);
|
||||
} else if (message.command === 'showDocumentSetList') {
|
||||
showDocumentSetView(message.documents || []);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
@ -181,6 +181,7 @@ export function getContextDisplayScript(): string {
|
||||
case 'folder': icon = getFolderIcon(); break;
|
||||
case 'image': icon = getImageIcon(); break;
|
||||
case 'document': icon = getDocumentIcon(); break;
|
||||
case 'docset': icon = getDocumentIcon(); break;
|
||||
case 'code': icon = getCodeIcon(); break;
|
||||
}
|
||||
|
||||
|
||||
160
src/views/contextSettingsComponent.ts
Normal file
160
src/views/contextSettingsComponent.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 上下文设置组件
|
||||
* 功能:管理文档集
|
||||
*/
|
||||
import {
|
||||
getDocsetDialogContent,
|
||||
getDocsetDialogStyles,
|
||||
getDocsetDialogScript,
|
||||
} from "./docsetDialog";
|
||||
|
||||
export function getContextSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="context-settings">
|
||||
<div class="context-header">
|
||||
<h3>上下文</h3>
|
||||
</div>
|
||||
<div class="context-section">
|
||||
<div class="context-section-header">
|
||||
<span>Docs</span>
|
||||
<button class="context-add-btn" onclick="openAddDocumentSetDialog()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="currentColor"/>
|
||||
</svg>
|
||||
添加文档集
|
||||
</button>
|
||||
</div>
|
||||
<div class="context-docs-list" id="contextDocsList">
|
||||
<div class="context-empty">
|
||||
<p>暂无文档集</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${getDocsetDialogContent()}
|
||||
`;
|
||||
}
|
||||
|
||||
export function getContextSettingsComponentStyles(): string {
|
||||
return `
|
||||
.context-settings {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.context-header h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.context-section {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.context-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.context-section-header span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.context-add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.context-add-btn:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
}
|
||||
|
||||
.context-add-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.context-docs-list {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.context-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.context-empty p {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.docset-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.docset-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.docset-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.docset-meta {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.docset-delete-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
opacity: 0.6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.docset-delete-btn:hover {
|
||||
opacity: 1;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
${getDocsetDialogStyles()}
|
||||
`;
|
||||
}
|
||||
|
||||
export function getContextSettingsComponentScript(): string {
|
||||
return getDocsetDialogScript();
|
||||
}
|
||||
571
src/views/docsetDialog.ts
Normal file
571
src/views/docsetDialog.ts
Normal file
@ -0,0 +1,571 @@
|
||||
/**
|
||||
* 文档集对话框组件
|
||||
* 功能:添加文档集的对话框
|
||||
*/
|
||||
|
||||
export function getDocsetDialogContent(): string {
|
||||
return `
|
||||
<div class="docset-dialog" id="docsetDialog">
|
||||
<div class="docset-dialog-overlay" onclick="closeDocsetDialog()"></div>
|
||||
<div class="docset-dialog-content">
|
||||
<div class="docset-dialog-header">
|
||||
<h3>添加文档集</h3>
|
||||
<button onclick="closeDocsetDialog()">×</button>
|
||||
</div>
|
||||
<div class="docset-dialog-body">
|
||||
<div class="docset-form-group">
|
||||
<label>名称</label>
|
||||
<input type="text" id="docsetName" placeholder="输入文档集名称" />
|
||||
</div>
|
||||
<div class="docset-form-group">
|
||||
<label>文件</label>
|
||||
<div class="docset-hint">支持 .md/.txt/.v/.sv/.pdf,单个文件最大 10 MB,文档集最大 50 MB,最多 1000 个文件</div>
|
||||
<button class="docset-add-file-btn" id="addFileBtn" onclick="addFileToDocset()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="currentColor"/>
|
||||
</svg>
|
||||
添加文件
|
||||
</button>
|
||||
<div id="docsetFilesDisplay" style="display: none; margin-top: 8px;">
|
||||
<div id="docsetFilesList" style="max-height: 200px; overflow-y: auto; border: 1px solid var(--vscode-input-border); border-radius: 4px; padding: 8px;"></div>
|
||||
<div id="docsetFilesSummary" style="margin-top: 8px; font-size: 12px; color: var(--vscode-descriptionForeground);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="docset-dialog-footer">
|
||||
<button class="docset-btn-cancel" onclick="closeDocsetDialog()">取消</button>
|
||||
<button class="docset-btn-confirm" onclick="confirmDocset()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="delete-confirm-dialog" id="deleteConfirmDialog">
|
||||
<div class="delete-confirm-content">
|
||||
<div class="delete-confirm-title">确认删除</div>
|
||||
<div class="delete-confirm-message" id="deleteConfirmMessage"></div>
|
||||
<div class="delete-confirm-actions">
|
||||
<button class="docset-btn-cancel" onclick="closeDeleteConfirm()">取消</button>
|
||||
<button class="docset-btn-confirm" onclick="confirmDelete()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rename-dialog" id="renameDialog">
|
||||
<div class="rename-content">
|
||||
<div class="rename-title">修改名称</div>
|
||||
<input type="text" id="renameInput" class="rename-input" placeholder="输入新名称" />
|
||||
<div class="rename-actions">
|
||||
<button class="docset-btn-cancel" onclick="closeRenameDialog()">取消</button>
|
||||
<button class="docset-btn-confirm" onclick="confirmRename()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function getDocsetDialogStyles(): string {
|
||||
return `
|
||||
.docset-dialog {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.docset-dialog.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.docset-dialog-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.docset-dialog-content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.docset-dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.docset-dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.docset-dialog-header button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: var(--vscode-foreground);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.docset-dialog-header button:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.docset-dialog-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.docset-form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.docset-form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.docset-hint {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.docset-form-group input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--vscode-input-foreground);
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.docset-add-file-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
border: 1px solid var(--vscode-textLink-foreground);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.docset-add-file-btn:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.docset-add-file-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.docset-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.docset-dialog-footer button {
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.docset-btn-cancel {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.docset-btn-cancel:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.docset-btn-confirm {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.docset-btn-confirm:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.docset-delete-btn, .docset-change-btn {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.docset-delete-btn:hover, .docset-change-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.docset-delete-btn:hover::after, .docset-change-btn:hover::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--vscode-editorHoverWidget-background);
|
||||
color: var(--vscode-editorHoverWidget-foreground);
|
||||
border: 1px solid var(--vscode-editorHoverWidget-border);
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.delete-confirm-dialog {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete-confirm-dialog.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.delete-confirm-content {
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.delete-confirm-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.delete-confirm-message {
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.delete-confirm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.delete-confirm-actions button {
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rename-dialog {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rename-dialog.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.rename-content {
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.rename-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rename-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--vscode-input-foreground);
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rename-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rename-actions button {
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export function getDocsetDialogScript(): string {
|
||||
return `
|
||||
let docsetFiles = [];
|
||||
|
||||
function openAddDocumentSetDialog() {
|
||||
const dialog = document.getElementById('docsetDialog');
|
||||
if (dialog) {
|
||||
dialog.classList.add('active');
|
||||
docsetFiles = [];
|
||||
document.getElementById('docsetName').value = '';
|
||||
document.getElementById('addFileBtn').style.display = 'flex';
|
||||
document.getElementById('docsetFilesDisplay').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function closeDocsetDialog() {
|
||||
const dialog = document.getElementById('docsetDialog');
|
||||
if (dialog) {
|
||||
dialog.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function addFileToDocset() {
|
||||
vscode.postMessage({ command: 'selectFilesForDocset' });
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function updateDocsetDisplay() {
|
||||
if (docsetFiles.length === 0) {
|
||||
document.getElementById('addFileBtn').style.display = 'flex';
|
||||
document.getElementById('docsetFilesDisplay').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('addFileBtn').style.display = 'none';
|
||||
document.getElementById('docsetFilesDisplay').style.display = 'block';
|
||||
|
||||
const listEl = document.getElementById('docsetFilesList');
|
||||
const summaryEl = document.getElementById('docsetFilesSummary');
|
||||
|
||||
listEl.innerHTML = docsetFiles.map((file, index) => \`
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 12px; padding: 4px 0; color: var(--vscode-foreground);">
|
||||
<span>\${file.name || file.absolutePath}</span>
|
||||
<button onclick="removeDocsetFile(\${index})" style="background: transparent; border: none; color: var(--vscode-foreground); cursor: pointer; padding: 0 4px; opacity: 0.7;">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M10 3h3v1h-1v9l-1 1H4l-1-1V4H2V3h3V2a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1zM9 2H6v1h3V2zM4 13h7V4H4v9zm2-8H5v7h1V5zm1 0h1v7H7V5zm2 0h1v7H9V5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
\`).join('');
|
||||
|
||||
const totalSize = docsetFiles.reduce((sum, f) => sum + (f.size || 0), 0);
|
||||
summaryEl.textContent = \`已选择 \${docsetFiles.length} 个文件,总大小 \${formatFileSize(totalSize)}\`;
|
||||
}
|
||||
|
||||
function removeDocsetFile(index) {
|
||||
docsetFiles.splice(index, 1);
|
||||
updateDocsetDisplay();
|
||||
}
|
||||
|
||||
function confirmDocset() {
|
||||
const name = document.getElementById('docsetName').value.trim();
|
||||
if (!name) {
|
||||
alert('请输入文档集名称');
|
||||
return;
|
||||
}
|
||||
if (docsetFiles.length === 0) {
|
||||
alert('请添加至少一个文件');
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
command: 'saveDocumentSet',
|
||||
name: name,
|
||||
documents: docsetFiles
|
||||
});
|
||||
|
||||
closeDocsetDialog();
|
||||
}
|
||||
|
||||
function renderDocumentSets(documentSets) {
|
||||
const listEl = document.getElementById('contextDocsList');
|
||||
if (!listEl) return;
|
||||
|
||||
if (!documentSets || documentSets.length === 0) {
|
||||
listEl.innerHTML = '<div class="context-empty"><p>暂无文档集</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = documentSets.map(ds => \`
|
||||
<div class="docset-item">
|
||||
<div class="docset-name">\${ds.name}</div>
|
||||
<div class="docset-meta">更新于 \${new Date(ds.updatedAt).toLocaleString('zh-CN')}</div>
|
||||
<button class="docset-change-btn" data-tooltip="修改名称" onclick="changeDocsetName('\${ds.id}', '\${ds.name}')">
|
||||
<svg t="1773883957219" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7170" width="14" height="14">
|
||||
<path d="M745.76 369.86l-451 537.48a18.693 18.693 0 0 1-8.46 5.74l-136.58 45.27c-13.24 4.39-26.46-6.71-24.43-20.5l20.86-142.36c0.5-3.44 1.95-6.67 4.19-9.33l451-537.48c6.65-7.93 18.47-8.96 26.4-2.31l115.71 97.1c7.92 6.64 8.96 18.46 2.31 26.39zM894.53 192.56l-65.9 78.53c-6.65 7.93-18.47 8.96-26.4 2.31l-115.71-97.1c-7.93-6.65-8.96-18.47-2.31-26.4l65.9-78.53c6.65-7.93 18.47-8.96 26.4-2.31l115.71 97.1c7.93 6.65 8.96 18.47 2.31 26.4z" fill="currentColor" p-id="7171"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="docset-delete-btn" data-tooltip="删除" onclick="showDeleteConfirm('\${ds.id}', '\${ds.name}')">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M10 3h3v1h-1v9l-1 1H4l-1-1V4H2V3h3V2a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1zM9 2H6v1h3V2zM4 13h7V4H4v9zm2-8H5v7h1V5zm1 0h1v7H7V5zm2 0h1v7H9V5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
let deleteTargetId = null;
|
||||
let renameTargetId = null;
|
||||
let renameOriginalName = null;
|
||||
|
||||
window.showDeleteConfirm = function(id, name) {
|
||||
deleteTargetId = id;
|
||||
document.getElementById('deleteConfirmMessage').textContent = \`确定要删除文档集 "\${name}" 吗?此操作不可恢复。\`;
|
||||
document.getElementById('deleteConfirmDialog').classList.add('active');
|
||||
};
|
||||
|
||||
window.changeDocsetName = function(id, name) {
|
||||
renameTargetId = id;
|
||||
renameOriginalName = name;
|
||||
document.getElementById('renameInput').value = name;
|
||||
document.getElementById('renameDialog').classList.add('active');
|
||||
setTimeout(() => document.getElementById('renameInput').focus(), 100);
|
||||
};
|
||||
|
||||
window.closeDeleteConfirm = function() {
|
||||
document.getElementById('deleteConfirmDialog').classList.remove('active');
|
||||
deleteTargetId = null;
|
||||
};
|
||||
|
||||
window.closeRenameDialog = function() {
|
||||
document.getElementById('renameDialog').classList.remove('active');
|
||||
renameTargetId = null;
|
||||
renameOriginalName = null;
|
||||
};
|
||||
|
||||
window.confirmDelete = function() {
|
||||
if (deleteTargetId) {
|
||||
vscode.postMessage({ command: 'deleteDocumentSet', id: deleteTargetId });
|
||||
closeDeleteConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
window.confirmRename = function() {
|
||||
const newName = document.getElementById('renameInput').value.trim();
|
||||
if (!newName) {
|
||||
alert('请输入名称');
|
||||
return;
|
||||
}
|
||||
if (newName !== renameOriginalName) {
|
||||
vscode.postMessage({ command: 'changeDocumentSetName', id: renameTargetId, newName: newName });
|
||||
}
|
||||
closeRenameDialog();
|
||||
};
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
if (message.command === 'filesSelectedForDocset') {
|
||||
if (message.errors && message.errors.length > 0) {
|
||||
alert('部分文件添加失败:\\n' + message.errors.join('\\n'));
|
||||
}
|
||||
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||
const MAX_TOTAL_SIZE = 50 * 1024 * 1024;
|
||||
const MAX_FILE_COUNT = 1000;
|
||||
|
||||
const vaildFiles = [];
|
||||
const errors = [];
|
||||
|
||||
for (const file of message.files) {
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
errors.push(\`\${file.name || file.absolutePath} 超过单个文件大小限制(\${formatFileSize(MAX_FILE_SIZE)})\`);
|
||||
} else {
|
||||
vaildFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
const newFiles = [...docsetFiles, ...vaildFiles];
|
||||
const totalSize = newFiles.reduce((sum, f) => sum + (f.size || 0), 0);
|
||||
|
||||
if (newFiles.length > MAX_FILE_COUNT) {
|
||||
errors.push(\`文档数量超过限制(最多1000个),当前数量(\${newFiles.length} 个)\`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalSize > MAX_TOTAL_SIZE) {
|
||||
errors.push(\`文档集总大小超过 50MB 限制,当前大小为(\${formatFileSize(MAX_TOTAL_SIZE)})\`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(errors.length > 0) {
|
||||
alert('以下文件被跳过:\\n' + errors.join('\\n'));
|
||||
}
|
||||
|
||||
docsetFiles = newFiles;
|
||||
updateDocsetDisplay();
|
||||
} else if (message.command === 'documentSetSaved') {
|
||||
renderDocumentSets(message.documentSets);
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
@ -553,7 +553,7 @@ export function getMessageAreaStyles(): string {
|
||||
}
|
||||
.segment-question .question-option {
|
||||
padding: 8px 16px;
|
||||
background: #3d3f41;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid #474747;
|
||||
border-radius: 6px;
|
||||
@ -563,13 +563,13 @@ export function getMessageAreaStyles(): string {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question .question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
border-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.segment-question .question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border-color: var(--vscode-button-background);
|
||||
}
|
||||
.segment-question.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
|
||||
@ -1,77 +1,67 @@
|
||||
import { peopleRules } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取规则设置组件的 HTML 内容
|
||||
*/
|
||||
export function getRulesSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="rules-settings">
|
||||
<h3 class="settings-section-title">规则设置</h3>
|
||||
<div class="rules-header">
|
||||
<h3 class="settings-section-title">个人规则</h3>
|
||||
<button class="add-rule-button" onclick="showAddRuleModal()">+ 创建</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">启用自定义规则</label>
|
||||
<span class="settings-item-description">使用自定义规则来控制 AI 行为</span>
|
||||
<label class="settings-item-label">启用个人规则</label>
|
||||
<span class="settings-item-description">规则将在每次对话时自动应用</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="enableCustomRulesCheckbox" checked>
|
||||
<input type="checkbox" id="enablePersonalRulesCheckbox" checked>
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">系统规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<div class="rules-list" id="rulesList">
|
||||
<!-- 规则列表将动态插入这里 -->
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑规则弹窗 -->
|
||||
<div class="rule-modal" id="ruleModal" style="display: none;">
|
||||
<div class="rule-modal-content">
|
||||
<h4 id="modalTitle">创建个人规则</h4>
|
||||
<input
|
||||
type="text"
|
||||
class="rule-name-input"
|
||||
id="ruleNameInput"
|
||||
placeholder="规则名称"
|
||||
/>
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="systemRulesTextarea"
|
||||
placeholder="在此输入系统规则,例如: - 始终使用中文回复 - 代码注释要详细 - 遵循项目编码规范"
|
||||
rows="8"
|
||||
class="rule-textarea"
|
||||
id="ruleTextarea"
|
||||
placeholder="输入规则内容..."
|
||||
rows="10"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
系统规则会在每次对话开始时应用
|
||||
<div class="rule-modal-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="saveRule()">保存</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="closeRuleModal()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">代码生成规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="codeRulesTextarea"
|
||||
placeholder="在此输入代码生成规则,例如: - 使用 TypeScript 严格模式 - 函数命名使用驼峰命名法 - 添加必要的错误处理"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
这些规则会在生成代码时应用
|
||||
<!-- 删除确认弹窗 -->
|
||||
<div class="rule-modal" id="deleteConfirmModal" style="display: none;">
|
||||
<div class="rule-modal-content" style="width: 400px;">
|
||||
<h4>确认删除</h4>
|
||||
<p id="deleteConfirmText" style="color: var(--vscode-foreground); margin: 16px 0;"></p>
|
||||
<div class="rule-modal-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="confirmDelete()">确定</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="closeDeleteConfirmModal()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">Verilog 规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="verilogRulesTextarea"
|
||||
placeholder="在此输入 Verilog 代码规则,例如: - 使用非阻塞赋值 (<=) 在时序逻辑中 - 模块命名使用小写加下划线 - 添加详细的端口注释"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
这些规则会在生成 Verilog 代码时应用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="saveRulesSettings()">
|
||||
保存规则
|
||||
</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="resetRulesSettings()">
|
||||
重置为默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -85,11 +75,144 @@ export function getRulesSettingsComponentStyles(): string {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.rules-textarea-container {
|
||||
margin-top: 8px;
|
||||
.rules-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rules-textarea {
|
||||
.add-rule-button {
|
||||
padding: 6px 12px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.add-rule-button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.rules-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rule-item-name {
|
||||
color: var(--vscode-foreground);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rule-item > div:first-child svg {
|
||||
background-color: rgba(148, 204, 241, 0.3);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.rule-item-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rule-menu-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.rule-menu-icon:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.rule-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 28px;
|
||||
background: var(--vscode-menu-background);
|
||||
border: 1px solid var(--vscode-menu-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.rule-dropdown button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-menu-foreground);
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rule-dropdown button:hover {
|
||||
background: var(--vscode-menu-selectionBackground);
|
||||
color: var(--vscode-menu-selectionForeground);
|
||||
}
|
||||
|
||||
.rule-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.rule-modal-content {
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.rule-modal-content h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.rule-name-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rule-name-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.rule-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--vscode-input-background);
|
||||
@ -98,26 +221,20 @@ export function getRulesSettingsComponentStyles(): string {
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rules-textarea:focus {
|
||||
.rule-textarea:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.rules-textarea::placeholder {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.rules-textarea-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
.rule-modal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -127,51 +244,169 @@ export function getRulesSettingsComponentStyles(): string {
|
||||
*/
|
||||
export function getRulesSettingsComponentScript(): string {
|
||||
return `
|
||||
// 保存规则设置
|
||||
function saveRulesSettings() {
|
||||
const settings = {
|
||||
enableCustomRules: document.getElementById('enableCustomRulesCheckbox').checked,
|
||||
systemRules: document.getElementById('systemRulesTextarea').value,
|
||||
codeRules: document.getElementById('codeRulesTextarea').value,
|
||||
verilogRules: document.getElementById('verilogRulesTextarea').value,
|
||||
};
|
||||
let currentRules = [];
|
||||
let editingRule = null;
|
||||
let deletingFilename = null;
|
||||
|
||||
// 发送消息到扩展
|
||||
// 显示添加规则弹窗
|
||||
function showAddRuleModal() {
|
||||
editingRule = null;
|
||||
document.getElementById('modalTitle').textContent = '创建个人规则';
|
||||
document.getElementById('ruleNameInput').value = '';
|
||||
document.getElementById('ruleTextarea').value = '';
|
||||
document.getElementById('ruleModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeRuleModal() {
|
||||
document.getElementById('ruleModal').style.display = 'none';
|
||||
closeAllDropdowns();
|
||||
}
|
||||
|
||||
// 切换下拉菜单
|
||||
function toggleDropdown(filename, event) {
|
||||
event.stopPropagation();
|
||||
closeAllDropdowns();
|
||||
const dropdown = document.getElementById('dropdown-' + filename);
|
||||
if (dropdown) {
|
||||
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭所有下拉菜单
|
||||
function closeAllDropdowns() {
|
||||
document.querySelectorAll('.rule-dropdown').forEach(d => d.style.display = 'none');
|
||||
}
|
||||
|
||||
// 点击页面其他地方关闭下拉菜单
|
||||
document.addEventListener('click', closeAllDropdowns);
|
||||
|
||||
// 编辑规则
|
||||
function editRule(filename) {
|
||||
const rule = currentRules.find(r => r.filename === filename);
|
||||
if (rule) {
|
||||
editingRule = rule;
|
||||
document.getElementById('modalTitle').textContent = '修改个人规则';
|
||||
document.getElementById('ruleNameInput').value = rule.name;
|
||||
document.getElementById('ruleTextarea').value = rule.content;
|
||||
document.getElementById('ruleModal').style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存规则
|
||||
function saveRule() {
|
||||
const name = document.getElementById('ruleNameInput').value.trim();
|
||||
const content = document.getElementById('ruleTextarea').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('规则名称不能为空');
|
||||
return;
|
||||
}
|
||||
if (!content) {
|
||||
alert('规则内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = document.getElementById('enablePersonalRulesCheckbox').checked;
|
||||
|
||||
if (editingRule) {
|
||||
vscode.postMessage({
|
||||
command: 'saveRulesSettings',
|
||||
settings: settings
|
||||
command: 'updatePersonalRule',
|
||||
filename: editingRule.filename,
|
||||
name: name,
|
||||
content: content,
|
||||
enabled: enabled
|
||||
});
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
command: 'savePersonalRule',
|
||||
name: name,
|
||||
content: content,
|
||||
enabled: enabled
|
||||
});
|
||||
}
|
||||
|
||||
closeRuleModal();
|
||||
}
|
||||
|
||||
// 删除规则
|
||||
function deleteRule(filename) {
|
||||
closeAllDropdowns();
|
||||
const rule = currentRules.find(r => r.filename === filename);
|
||||
const ruleName = rule ? rule.name : filename;
|
||||
|
||||
deletingFilename = filename;
|
||||
document.getElementById('deleteConfirmText').textContent = '确定要删除规则"' + ruleName + '"吗?此操作无法撤销。';
|
||||
document.getElementById('deleteConfirmModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 关闭删除确认弹窗
|
||||
function closeDeleteConfirmModal() {
|
||||
document.getElementById('deleteConfirmModal').style.display = 'none';
|
||||
deletingFilename = null;
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
function confirmDelete() {
|
||||
if (deletingFilename) {
|
||||
vscode.postMessage({
|
||||
command: 'deletePersonalRule',
|
||||
filename: deletingFilename
|
||||
});
|
||||
}
|
||||
closeDeleteConfirmModal();
|
||||
}
|
||||
|
||||
// 渲染规则列表
|
||||
function renderRulesList(rules) {
|
||||
currentRules = rules || [];
|
||||
const listEl = document.getElementById('rulesList');
|
||||
|
||||
if (currentRules.length === 0) {
|
||||
listEl.innerHTML = '<div style="color: var(--vscode-descriptionForeground); padding: 16px; text-align: center;">暂无规则,点击"+ 创建"添加</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const peopleRulesIcon = '${peopleRules}';
|
||||
|
||||
listEl.innerHTML = currentRules.map(rule => \`
|
||||
<div class="rule-item">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
\${peopleRulesIcon}
|
||||
<div class="rule-item-name">\${rule.filename}</div>
|
||||
</div>
|
||||
<div class="rule-item-menu">
|
||||
<svg class="rule-menu-icon" onclick="toggleDropdown('\${rule.filename}', event)" viewBox="0 0 16 16" fill="currentColor">
|
||||
<circle cx="8" cy="3" r="1.5"/>
|
||||
<circle cx="8" cy="8" r="1.5"/>
|
||||
<circle cx="8" cy="13" r="1.5"/>
|
||||
</svg>
|
||||
<div class="rule-dropdown" id="dropdown-\${rule.filename}" style="display: none;">
|
||||
<button onclick="editRule('\${rule.filename}')">编辑</button>
|
||||
<button onclick="deleteRule('\${rule.filename}')">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 加载规则列表
|
||||
function loadPersonalRules(data) {
|
||||
if (data && data.enabled !== undefined) {
|
||||
document.getElementById('enablePersonalRulesCheckbox').checked = data.enabled;
|
||||
}
|
||||
if (data && data.rules) {
|
||||
renderRulesList(data.rules);
|
||||
}
|
||||
}
|
||||
|
||||
//监听启用个人规则开关变化
|
||||
document.getElementById('enablePersonalRulesCheckbox').addEventListener('change', function() {
|
||||
const enabled = this.checked;
|
||||
vscode.postMessage({ command: 'updatePersonalRulesEnabled', enabled: enabled });
|
||||
});
|
||||
|
||||
// 显示保存成功提示
|
||||
console.log('规则设置已保存', settings);
|
||||
}
|
||||
|
||||
// 重置规则设置
|
||||
function resetRulesSettings() {
|
||||
document.getElementById('enableCustomRulesCheckbox').checked = true;
|
||||
document.getElementById('systemRulesTextarea').value = '';
|
||||
document.getElementById('codeRulesTextarea').value = '';
|
||||
document.getElementById('verilogRulesTextarea').value = '';
|
||||
|
||||
console.log('规则设置已重置为默认值');
|
||||
}
|
||||
|
||||
// 加载规则设置
|
||||
function loadRulesSettings(settings) {
|
||||
if (!settings) return;
|
||||
|
||||
if (settings.enableCustomRules !== undefined) {
|
||||
document.getElementById('enableCustomRulesCheckbox').checked = settings.enableCustomRules;
|
||||
}
|
||||
if (settings.systemRules) {
|
||||
document.getElementById('systemRulesTextarea').value = settings.systemRules;
|
||||
}
|
||||
if (settings.codeRules) {
|
||||
document.getElementById('codeRulesTextarea').value = settings.codeRules;
|
||||
}
|
||||
if (settings.verilogRules) {
|
||||
document.getElementById('verilogRulesTextarea').value = settings.verilogRules;
|
||||
}
|
||||
}
|
||||
// 页面加载时请求规则数据
|
||||
vscode.postMessage({ command: 'loadPersonalRules' });
|
||||
`;
|
||||
}
|
||||
|
||||
@ -8,6 +8,11 @@ import {
|
||||
getRulesSettingsComponentStyles,
|
||||
getRulesSettingsComponentScript,
|
||||
} from "./rulesSettingsComponent";
|
||||
import {
|
||||
getContextSettingsComponentContent,
|
||||
getContextSettingsComponentStyles,
|
||||
getContextSettingsComponentScript,
|
||||
} from "./contextSettingsComponent";
|
||||
|
||||
/**
|
||||
* 获取设置面板的 HTML 内容
|
||||
@ -34,6 +39,9 @@ export function getSettingsComponentContent(): string {
|
||||
<button class="settings-nav-item" data-tab="rules" onclick="switchSettingsTab('rules')">
|
||||
规则
|
||||
</button>
|
||||
<button class="settings-nav-item" data-tab="context" onclick="switchSettingsTab('context')">
|
||||
上下文
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
@ -43,6 +51,9 @@ export function getSettingsComponentContent(): string {
|
||||
<div class="settings-tab-content" id="rulesSettings">
|
||||
${getRulesSettingsComponentContent()}
|
||||
</div>
|
||||
<div class="settings-tab-content" id="contextSettings">
|
||||
${getContextSettingsComponentContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -187,6 +198,7 @@ export function getSettingsComponentStyles(): string {
|
||||
|
||||
${getGeneralSettingsComponentStyles()}
|
||||
${getRulesSettingsComponentStyles()}
|
||||
${getContextSettingsComponentStyles()}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -197,6 +209,7 @@ export function getSettingsComponentScript(): string {
|
||||
return `
|
||||
${getGeneralSettingsComponentScript()}
|
||||
${getRulesSettingsComponentScript()}
|
||||
${getContextSettingsComponentScript()}
|
||||
|
||||
// 打开设置面板
|
||||
function openSettingsModal() {
|
||||
@ -235,7 +248,21 @@ export function getSettingsComponentScript(): string {
|
||||
content.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 切换到上下文标签页时加载文档集列表
|
||||
if (tabName === 'context') {
|
||||
vscode.postMessage({ command: 'loadDocumentSets' });
|
||||
}
|
||||
}
|
||||
|
||||
// 监听打开设置标签页的消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
if (message.command === 'openSettingsTab') {
|
||||
openSettingsModal();
|
||||
switchSettingsTab(message.tab);
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止点击模态框内容时关闭
|
||||
document.addEventListener('click', (event) => {
|
||||
|
||||
@ -737,6 +737,13 @@ export function getWebviewContent(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'personalRulesLoaded':
|
||||
// 加载个人规则数据
|
||||
if (typeof loadPersonalRules === 'function') {
|
||||
loadPersonalRules(message.data);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'autoSendMessage':
|
||||
// 自动发送待发送的消息(登录后)
|
||||
console.log('[WebView] 自动发送待发送消息:', message.text);
|
||||
|
||||
Reference in New Issue
Block a user