Compare commits
15 Commits
feat/eda
...
feat/ningD
| Author | SHA1 | Date | |
|---|---|---|---|
| 47f95afabb | |||
| 081ddec55c | |||
| 12c2f634bd | |||
| b9dc631bf7 | |||
| 6425496d2e | |||
| fd5a01c67f | |||
| a25d68f527 | |||
| 77b54aebf0 | |||
| 840436eb36 | |||
| f5dd7534f0 | |||
| ebb9de5294 | |||
| 531d140b99 | |||
| 97b8e8aa7d | |||
| 4ed998e937 | |||
| ad0f0336d5 |
@ -1,526 +0,0 @@
|
||||
# Vivado 联动功能需求文档
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
### 1.1 当前状态
|
||||
|
||||
IC Coder Plugin 目前支持:
|
||||
|
||||
- iverilog 仿真(内置 Windows 版本)
|
||||
- VCD 波形查看
|
||||
- Verilog 代码生成和文件操作
|
||||
|
||||
### 1.2 需求来源
|
||||
|
||||
用户需要在 VS Code 中直接调用本地 Vivado 工具,并将产出文件自动导入到项目中,完成从仿真到 FPGA 部署的完整流程。
|
||||
|
||||
### 1.3 Vivado 是什么?
|
||||
|
||||
**Vivado** 是 Xilinx(现 AMD)的 FPGA 开发工具,用于将 Verilog 代码部署到 FPGA 硬件:
|
||||
|
||||
- **综合(Synthesis)**:将 RTL 代码转换为门级网表
|
||||
- **实现(Implementation)**:布局布线,映射到具体 FPGA 芯片
|
||||
- **生成比特流(Bitstream)**:生成 .bit 配置文件用于烧录
|
||||
|
||||
**与 iverilog 的区别**:
|
||||
|
||||
- iverilog:只做**仿真验证**(软件层面验证逻辑)
|
||||
- Vivado:做**综合+实现+生成配置文件**(真正部署到硬件)
|
||||
|
||||
**典型开发流程**:
|
||||
|
||||
```
|
||||
编写 Verilog → iverilog 仿真验证 → Vivado 综合 → Vivado 实现 → 生成 .bit 文件 → 烧录到 FPGA
|
||||
```
|
||||
|
||||
## 2. 功能目标
|
||||
|
||||
### 2.1 核心目标
|
||||
|
||||
- **前端提供原子工具**:前端只提供独立的 Vivado 命令工具,不控制流程
|
||||
- **后端AI控制流程**:所有执行顺序、依赖检查由后端AI决策
|
||||
- **工具职责单一**:每个工具只负责执行一个具体命令
|
||||
- **结果透明返回**:执行结果完整返回给后端,由后端决定下一步
|
||||
|
||||
### 2.2 设计原则
|
||||
|
||||
- 前端不做流程判断,只执行命令
|
||||
- 前端不检查依赖关系,由后端保证顺序
|
||||
- 前端返回详细的执行结果,包括成功/失败、输出、报告等
|
||||
- 后端AI根据结果智能决策是否继续
|
||||
|
||||
## 3. 功能详细需求
|
||||
|
||||
### 3.1 前端提供的工具
|
||||
|
||||
前端提供 4 个独立的工具,每个工具只负责执行一个命令:
|
||||
|
||||
#### 3.1.1 createVivadoProject - 创建工程
|
||||
|
||||
- **输入**:项目名称、芯片型号、源文件列表、约束文件(可选)
|
||||
- **输出**:工程文件(.xpr)
|
||||
- **说明**:创建 Vivado 工程,不执行任何构建操作
|
||||
|
||||
#### 3.1.2 runVivadoSynthesis - 综合
|
||||
|
||||
- **输入**:工程路径或源文件、芯片型号、顶层模块、约束文件(可选)
|
||||
- **输出**:.dcp 文件、综合报告
|
||||
- **说明**:执行综合,前端不检查工程是否存在。约束文件在此阶段可选,主要用于时序约束
|
||||
|
||||
#### 3.1.3 runVivadoImplementation - 实现
|
||||
|
||||
- **输入**:综合后的 .dcp 文件路径、约束文件(必需,包含管脚约束)
|
||||
- **输出**:实现后的 .dcp 文件、时序报告
|
||||
- **说明**:执行实现,前端不检查 .dcp 是否存在。**管脚约束是必需的**,否则无法完成布局布线
|
||||
|
||||
#### 3.1.4 runVivadoBitstream - 生成比特流
|
||||
|
||||
- **输入**:实现后的 .dcp 文件路径
|
||||
- **输出**:.bit 文件(可下载到 FPGA 的配置文件)
|
||||
- **核心依赖**:
|
||||
1. 实现已完成
|
||||
2. 工程指定目标芯片型号
|
||||
3. 已完成管脚约束(无管脚约束无法生成)
|
||||
- **说明**:生成比特流,前端不检查 .dcp 是否存在
|
||||
|
||||
### 3.2 配置管理
|
||||
|
||||
#### 3.2.1 配置项
|
||||
|
||||
```json
|
||||
{
|
||||
"vivado": {
|
||||
"enabled": true,
|
||||
"executablePath": "C:/Xilinx/Vivado/2023.1/bin/vivado.bat",
|
||||
"workingDir": "${workspaceFolder}/vivado_project",
|
||||
"part": "xc7a35tcpg236-1", // FPGA 型号
|
||||
"commands": {
|
||||
"synthesis": "vivado -mode batch -source synth.tcl",
|
||||
"implementation": "vivado -mode batch -source impl.tcl",
|
||||
"bitstream": "vivado -mode batch -source bitstream.tcl"
|
||||
},
|
||||
"outputFiles": {
|
||||
"synthesis": ["*.dcp", "*_synth.rpt"],
|
||||
"implementation": ["*.dcp", "*_timing.rpt", "*_utilization.rpt"],
|
||||
"bitstream": ["*.bit"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 存储位置
|
||||
|
||||
- 全局配置:VS Code Settings(`settings.json`)
|
||||
- 项目配置:`.vscode/ic-coder-vivado.json`(优先级更高)
|
||||
|
||||
### 3.3 工具调用接口
|
||||
|
||||
#### 3.3.1 通用响应格式
|
||||
|
||||
所有工具返回统一的响应格式:
|
||||
|
||||
```typescript
|
||||
interface VivadoToolResponse {
|
||||
success: boolean; // 是否成功
|
||||
command: string; // 执行的命令
|
||||
executionTime: number; // 执行时间(毫秒)
|
||||
output: string; // 完整输出日志
|
||||
error?: string; // 错误信息(如果失败)
|
||||
outputFiles?: string[]; // 产出文件路径列表
|
||||
reports?: {
|
||||
resources?: string; // 资源使用摘要
|
||||
timing?: string; // 时序信息摘要
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.2 各工具的参数定义
|
||||
|
||||
**createVivadoProject**
|
||||
```typescript
|
||||
{
|
||||
projectName: string; // 项目名称
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files: string[]; // 源文件列表
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoSynthesis**
|
||||
```typescript
|
||||
{
|
||||
projectPath?: string; // 工程路径(可选,如果有工程)
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files?: string[]; // 源文件(如果没有工程)
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoImplementation**
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||
constraints: string; // 约束文件(必需,包含管脚约束)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoBitstream**
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 后端AI的职责
|
||||
|
||||
后端AI负责:
|
||||
1. 询问用户必要参数(芯片型号、执行模式等)
|
||||
2. 理解用户意图,决定调用哪些工具
|
||||
3. 按正确顺序调用工具(遵循依赖关系)
|
||||
4. 检查每步执行结果,决定是否继续
|
||||
5. 汇总结果并展示给用户
|
||||
|
||||
#### 3.4.1 询问用户参数
|
||||
|
||||
后端必须询问:
|
||||
- **芯片型号**(必需):"请提供 FPGA 芯片型号(例如:xc7a35tcpg236-1)"
|
||||
- **执行模式**(必需):"选择执行模式:1) 图形化 2) 后端执行"
|
||||
- **约束文件**(必需):"请提供约束文件(.xdc),包含管脚约束和时序约束"
|
||||
|
||||
#### 3.4.2 理解依赖关系
|
||||
|
||||
后端AI需要理解:
|
||||
```
|
||||
创建工程 → 综合 → 实现 → 生成比特流
|
||||
```
|
||||
|
||||
如果用户说"做实现",后端应该:
|
||||
1. 先调用 `createVivadoProject` 创建工程
|
||||
2. 再调用 `runVivadoSynthesis` 执行综合
|
||||
3. 最后调用 `runVivadoImplementation` 执行实现
|
||||
|
||||
#### 3.4.3 逐步调用工具
|
||||
|
||||
```
|
||||
步骤1: 调用 createVivadoProject
|
||||
检查 response.success
|
||||
如果失败 → 停止并报错
|
||||
|
||||
步骤2: 调用 runVivadoSynthesis
|
||||
检查 response.success
|
||||
如果失败 → 停止并报错
|
||||
|
||||
步骤3: 调用 runVivadoImplementation
|
||||
检查 response.success
|
||||
返回最终结果
|
||||
```
|
||||
|
||||
### 3.5 UI 交互
|
||||
|
||||
#### 3.5.1 配置界面
|
||||
|
||||
- 在设置页面添加 "Vivado 配置" 选项
|
||||
- 支持配置 Vivado 路径、FPGA 型号
|
||||
- 支持测试 Vivado 可用性(点击按钮测试)
|
||||
|
||||
#### 3.5.2 调用界面
|
||||
|
||||
- 在聊天面板中,AI 可以建议使用 Vivado
|
||||
- 用户确认后,显示执行进度对话框
|
||||
- 实时显示日志输出(可折叠)
|
||||
- 显示执行状态:准备中 → 执行中 → 完成/失败
|
||||
|
||||
#### 3.5.3 结果展示
|
||||
|
||||
- 执行成功:显示执行时间、资源使用、时序信息
|
||||
- 执行失败:显示错误信息、建议解决方案
|
||||
- 导入文件:高亮显示已导入的文件,支持点击打开报告
|
||||
|
||||
### 3.6 后端集成
|
||||
|
||||
#### 3.6.1 工具定义
|
||||
|
||||
后端注册 4 个独立工具:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "createVivadoProject",
|
||||
"description": "创建 Vivado 工程。需要先询问用户芯片型号和执行模式。",
|
||||
"parameters": {
|
||||
"projectName": "项目名称",
|
||||
"part": "芯片型号(必须从用户获取)",
|
||||
"topModule": "顶层模块名",
|
||||
"files": "源文件列表",
|
||||
"constraints": "约束文件(可选)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoSynthesis",
|
||||
"description": "执行 Vivado 综合。前端不检查依赖,后端需确保工程已创建。",
|
||||
"parameters": {
|
||||
"projectPath": "工程路径(可选)",
|
||||
"part": "芯片型号",
|
||||
"topModule": "顶层模块",
|
||||
"files": "源文件(如果没有工程)",
|
||||
"constraints": "约束文件(可选)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoImplementation",
|
||||
"description": "执行 Vivado 实现。前端不检查依赖,后端需确保综合已完成且提供约束文件。",
|
||||
"parameters": {
|
||||
"dcpFile": "综合后的 .dcp 文件路径",
|
||||
"constraints": "约束文件(必需,包含管脚约束)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoBitstream",
|
||||
"description": "生成比特流。前端不检查依赖,后端需确保实现已完成。",
|
||||
"parameters": {
|
||||
"dcpFile": "实现后的 .dcp 文件路径",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.6.2 后端调用示例
|
||||
|
||||
**场景:用户要求完整流程**
|
||||
|
||||
```
|
||||
用户: 用 Vivado 跑完整流程
|
||||
|
||||
AI: 请提供芯片型号和执行模式
|
||||
用户: xc7a35tcpg236-1,后端执行
|
||||
|
||||
AI 执行:
|
||||
1. [调用] createVivadoProject({ projectName: "counter", part: "xc7a35tcpg236-1", ... })
|
||||
[结果] { success: true, outputFiles: ["counter.xpr"] }
|
||||
|
||||
2. [调用] runVivadoSynthesis({ projectPath: "counter.xpr", ... })
|
||||
[结果] { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||
|
||||
3. [调用] runVivadoImplementation({ dcpFile: "counter_synth.dcp", constraints: "counter.xdc", ... })
|
||||
[结果] { success: true, outputFiles: ["counter_impl.dcp"], reports: {...} }
|
||||
|
||||
4. [调用] runVivadoBitstream({ dcpFile: "counter_impl.dcp", ... })
|
||||
[结果] { success: true, outputFiles: ["counter.bit"] }
|
||||
|
||||
AI 回复: 完成!已生成 counter.bit 文件
|
||||
```
|
||||
|
||||
## 4. 用户场景
|
||||
|
||||
### 4.1 场景一:完整流程执行
|
||||
|
||||
**用户输入**:"用 Vivado 跑完整个 FPGA 流程"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 好的,我将使用 Vivado 执行完整的 FPGA 开发流程。请提供以下信息:
|
||||
|
||||
1. FPGA 芯片型号(例如:xc7a35tcpg236-1)
|
||||
2. 请选择要执行的步骤(可多选):
|
||||
- 综合(Synthesis)
|
||||
- 实现(Implementation)
|
||||
- 生成比特流(Bitstream)
|
||||
3. 执行模式:
|
||||
- 图形化模式:打开 Vivado GUI
|
||||
- 后端执行:自动在后台执行
|
||||
4. 约束文件路径(.xdc,必需)
|
||||
|
||||
用户: xc7a35tcpg236-1,全部步骤,后端执行,约束文件是 counter.xdc
|
||||
|
||||
AI: 收到!执行计划:
|
||||
1. 创建工程
|
||||
2. 综合
|
||||
3. 实现
|
||||
4. 生成比特流
|
||||
|
||||
开始执行...
|
||||
|
||||
[步骤 1/4] 创建工程...
|
||||
✓ 工程创建成功(耗时 2s)
|
||||
|
||||
[步骤 2/4] 综合中...
|
||||
✓ 综合完成(耗时 45s)
|
||||
资源使用:LUT 32/20800 (0.15%), FF 8/41600 (0.02%)
|
||||
|
||||
[步骤 3/4] 实现中...
|
||||
✓ 实现完成(耗时 120s)
|
||||
时序:WNS 5.234ns, TNS 0.000ns(满足要求)
|
||||
|
||||
[步骤 4/4] 生成比特流...
|
||||
✓ 比特流生成完成(耗时 15s)
|
||||
|
||||
所有步骤执行完毕!产出文件已导入到 vivado_output/ 目录。
|
||||
```
|
||||
|
||||
### 4.2 场景二:只执行实现(自动补充依赖)
|
||||
|
||||
**用户输入**:"用 Vivado 做实现"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 请提供以下信息:
|
||||
1. FPGA 芯片型号
|
||||
2. 执行模式(图形化/后端执行)
|
||||
|
||||
用户: xc7a35tcpg236-1,后端执行
|
||||
|
||||
AI: 注意:实现依赖综合,我将自动执行以下步骤:
|
||||
1. 创建工程
|
||||
2. 综合(前置依赖)
|
||||
3. 实现(您选择的步骤)
|
||||
|
||||
是否继续?
|
||||
|
||||
用户: 是
|
||||
|
||||
AI: 开始执行...
|
||||
[自动执行创建工程 → 综合 → 实现]
|
||||
```
|
||||
|
||||
### 4.3 场景三:图形化模式
|
||||
|
||||
**用户输入**:"打开 Vivado 图形界面做综合"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 请提供芯片型号:
|
||||
用户: xc7a35tcpg236-1
|
||||
|
||||
AI: 正在创建工程并打开 Vivado GUI...
|
||||
✓ 工程已创建
|
||||
✓ Vivado 图形界面已启动
|
||||
|
||||
您现在可以在 Vivado 中手动操作。完成后,产出文件将保存在工程目录中。
|
||||
```
|
||||
|
||||
### 4.4 场景四:执行中断处理
|
||||
|
||||
**执行过程**:
|
||||
```
|
||||
[步骤 1/3] 创建工程...
|
||||
✓ 成功
|
||||
|
||||
[步骤 2/3] 综合中...
|
||||
✗ 综合失败
|
||||
错误:ERROR: [Synth 8-439] module 'counter' not found
|
||||
|
||||
AI: 综合失败,发现以下问题:
|
||||
- 找不到模块 'counter'
|
||||
|
||||
建议检查:
|
||||
1. 模块名是否正确
|
||||
2. 文件中是否定义了该模块
|
||||
3. 是否有语法错误
|
||||
|
||||
执行已停止,请修复错误后重试。
|
||||
```
|
||||
|
||||
## 5. 技术约束
|
||||
|
||||
### 5.1 平台兼容性
|
||||
|
||||
- Windows:支持 `.bat` 可执行文件
|
||||
- Linux:支持 shell 脚本
|
||||
- 路径分隔符自动适配
|
||||
|
||||
### 5.2 性能要求
|
||||
|
||||
- 命令执行不阻塞 UI
|
||||
- 综合时间可能较长(分钟级),需要进度提示
|
||||
- 日志输出实时更新,限制缓冲区大小
|
||||
|
||||
### 5.3 安全性
|
||||
|
||||
- 工作目录限制在项目范围内
|
||||
- 许可证路径不记录到日志
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
### 6.1 功能验收
|
||||
|
||||
- [ ] 用户可以配置 Vivado 路径和 FPGA 型号
|
||||
- [ ] AI 可以通过工具调用成功执行 Vivado 综合
|
||||
- [ ] 产出文件自动导入到指定目录
|
||||
- [ ] 执行过程有清晰的进度提示
|
||||
- [ ] 报告文件可以正常打开查看
|
||||
|
||||
### 6.2 性能验收
|
||||
|
||||
- [ ] 小型项目综合时间 < 1 分钟
|
||||
- [ ] UI 响应流畅,不卡顿
|
||||
- [ ] 日志输出实时更新(延迟 < 500ms)
|
||||
|
||||
### 6.3 用户体验验收
|
||||
|
||||
- [ ] 配置界面直观易用
|
||||
- [ ] 首次使用有引导提示
|
||||
- [ ] 错误提示清晰,有解决建议
|
||||
- [ ] 导入的文件可以直接打开查看
|
||||
|
||||
## 7. 风险和依赖
|
||||
|
||||
### 7.1 风险
|
||||
|
||||
- **Vivado 版本差异**:不同版本的命令行参数可能不同
|
||||
- **许可证问题**:Vivado 需要许可证才能运行
|
||||
- **路径问题**:Windows 路径中的空格和特殊字符
|
||||
- **执行时间长**:大型项目可能需要数十分钟
|
||||
|
||||
### 7.2 依赖
|
||||
|
||||
- 用户需要自行安装 Vivado
|
||||
- 用户需要配置正确的 Vivado 路径
|
||||
- 需要设置环境变量(如 `XILINX_VIVADO`)
|
||||
- 需要有效的 Vivado 许可证
|
||||
- **需要提供 .xdc 约束文件**:
|
||||
- **管脚约束**(必需):定义信号与 FPGA 引脚的映射关系,实现阶段必须提供
|
||||
- **时序约束**(强烈推荐):定义时钟频率和时序要求,确保设计满足性能指标
|
||||
|
||||
## 8. 后续扩展
|
||||
|
||||
### 8.1 短期扩展
|
||||
|
||||
- 支持自定义 TCL 脚本模板
|
||||
- 支持批量处理多个设计
|
||||
- 支持时序约束编辑器
|
||||
|
||||
### 8.2 长期扩展
|
||||
|
||||
- 支持其他 FPGA 工具(Quartus)
|
||||
- 云端 Vivado 服务集成
|
||||
- 结果对比和版本管理
|
||||
- 性能分析和优化建议
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. Vivado 命令行参考
|
||||
|
||||
- 官方文档:https://docs.xilinx.com/
|
||||
- TCL 命令参考:UG835
|
||||
- 设计流程参考:UG892
|
||||
|
||||
### B. 术语表
|
||||
|
||||
- **RTL**:Register Transfer Level,寄存器传输级
|
||||
- **综合**:Synthesis,将 RTL 代码转换为门级网表
|
||||
- **实现**:Implementation,布局布线
|
||||
- **比特流**:Bitstream,FPGA 配置文件
|
||||
- **DCP**:Design Checkpoint,Vivado 设计检查点文件
|
||||
- **XDC**:Xilinx Design Constraints,约束文件
|
||||
- **LUT**:Look-Up Table,查找表(FPGA 基本逻辑单元)
|
||||
- **FF**:Flip-Flop,触发器
|
||||
@ -1,166 +0,0 @@
|
||||
# Vivado 联动前后端对接文档
|
||||
|
||||
## 1. 前端提供的工具
|
||||
|
||||
前端提供 4 个独立工具,每个工具执行一个 Vivado 命令。
|
||||
|
||||
### 1.1 createVivadoProject - 创建工程
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
projectName: string; // 项目名称
|
||||
part: string; // 芯片型号(如 xc7a35tcpg236-1)
|
||||
topModule: string; // 顶层模块名
|
||||
files: string[]; // 源文件路径列表
|
||||
constraints?: string; // 约束文件路径(可选)
|
||||
mode: 'gui' | 'batch'; // gui=打开图形界面,batch=后台执行
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean; // 是否成功
|
||||
command: "create_project";
|
||||
executionTime: number; // 执行时间(毫秒)
|
||||
output: string; // 完整日志
|
||||
error?: string; // 失败原因(如果失败)
|
||||
outputFiles?: string[]; // 产出的工程文件路径
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 runVivadoSynthesis - 综合
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
projectPath?: string; // 工程路径(可选)
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files?: string[]; // 源文件(如果没有工程)
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "synthesis";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // .dcp 文件等
|
||||
reports?: {
|
||||
resources?: string; // 资源使用摘要
|
||||
timing?: string; // 时序摘要
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 runVivadoImplementation - 实现
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "implementation";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // 实现后的 .dcp 文件等
|
||||
reports?: {
|
||||
resources?: string;
|
||||
timing?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 runVivadoBitstream - 生成比特流
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "bitstream";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // .bit 文件路径
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 前端职责
|
||||
|
||||
- 接收后端工具调用
|
||||
- 生成对应的 TCL 脚本
|
||||
- 执行 Vivado 命令
|
||||
- 捕获输出日志
|
||||
- 解析报告文件(提取资源和时序摘要)
|
||||
- 返回执行结果
|
||||
|
||||
**前端不做**:
|
||||
- 不检查依赖关系
|
||||
- 不验证执行顺序
|
||||
- 不控制流程
|
||||
|
||||
## 3. 后端职责
|
||||
|
||||
- 询问用户参数(芯片型号、执行模式等)
|
||||
- 理解依赖关系(创建工程 → 综合 → 实现 → 生成比特流)
|
||||
- 按正确顺序调用工具
|
||||
- 检查每步的 `success` 字段
|
||||
- 如果失败,读取 `error` 字段并提示用户
|
||||
- 汇总结果展示给用户
|
||||
|
||||
## 4. 调用示例
|
||||
|
||||
```
|
||||
用户: 用 Vivado 做综合
|
||||
|
||||
后端:
|
||||
1. 询问芯片型号 → xc7a35tcpg236-1
|
||||
2. 询问执行模式 → batch
|
||||
|
||||
3. 调用 createVivadoProject(...)
|
||||
返回: { success: true, outputFiles: ["counter.xpr"] }
|
||||
|
||||
4. 调用 runVivadoSynthesis(...)
|
||||
返回: { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||
|
||||
5. 展示结果给用户
|
||||
```
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
如果某步失败:
|
||||
```typescript
|
||||
{
|
||||
success: false,
|
||||
error: "ERROR: [Synth 8-439] module 'counter' not found",
|
||||
output: "详细日志..."
|
||||
}
|
||||
```
|
||||
|
||||
后端应该:
|
||||
1. 停止后续步骤
|
||||
2. 提取 `error` 字段
|
||||
3. 给用户提示和建议
|
||||
@ -1,153 +0,0 @@
|
||||
# Vivado 联动功能技术设计文档
|
||||
|
||||
## 1. 架构设计
|
||||
|
||||
```
|
||||
后端 AI
|
||||
↓ 调用工具
|
||||
前端 Extension (messageHandler.ts)
|
||||
↓ 调用
|
||||
VivadoRunner (utils/vivadoRunner.ts)
|
||||
↓ 生成 TCL 脚本并执行
|
||||
本地 Vivado
|
||||
```
|
||||
|
||||
## 2. 核心模块
|
||||
|
||||
### 2.1 VivadoRunner
|
||||
|
||||
**职责**:执行单个 Vivado 命令
|
||||
|
||||
**主要方法**:
|
||||
- `createProject()` - 创建工程
|
||||
- `runSynthesis()` - 执行综合
|
||||
- `runImplementation()` - 执行实现
|
||||
- `runBitstream()` - 生成比特流
|
||||
|
||||
**实现要点**:
|
||||
- 根据参数生成 TCL 脚本
|
||||
- 启动子进程执行 Vivado
|
||||
- 捕获输出日志
|
||||
- 解析报告文件
|
||||
- 返回结果
|
||||
|
||||
### 2.2 TCL 脚本生成
|
||||
|
||||
**创建工程**:
|
||||
```tcl
|
||||
create_project {projectName} {workDir} -part {part} -force
|
||||
add_files -norecurse {files}
|
||||
set_property top {topModule} [current_fileset]
|
||||
```
|
||||
|
||||
**综合**:
|
||||
```tcl
|
||||
synth_design -part {part} -top {topModule}
|
||||
report_utilization -file utilization.rpt
|
||||
write_checkpoint -force synth.dcp
|
||||
```
|
||||
|
||||
**实现**:
|
||||
```tcl
|
||||
open_checkpoint {dcpFile}
|
||||
opt_design
|
||||
place_design
|
||||
route_design
|
||||
report_timing_summary -file timing.rpt
|
||||
write_checkpoint -force impl.dcp
|
||||
```
|
||||
|
||||
**生成比特流**:
|
||||
```tcl
|
||||
open_checkpoint {dcpFile}
|
||||
write_bitstream -force output.bit
|
||||
```
|
||||
|
||||
### 2.3 MessageHandler 集成
|
||||
|
||||
```typescript
|
||||
// 处理工具调用
|
||||
async function handleToolCall(toolName: string, params: any) {
|
||||
switch(toolName) {
|
||||
case 'createVivadoProject':
|
||||
return await vivadoRunner.createProject(params);
|
||||
case 'runVivadoSynthesis':
|
||||
return await vivadoRunner.runSynthesis(params);
|
||||
case 'runVivadoImplementation':
|
||||
return await vivadoRunner.runImplementation(params);
|
||||
case 'runVivadoBitstream':
|
||||
return await vivadoRunner.runBitstream(params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 配置管理
|
||||
|
||||
**配置文件位置**:
|
||||
- 全局:`settings.json` 中的 `ic-coder.vivado`
|
||||
- 项目:`.vscode/ic-coder-vivado.json`
|
||||
|
||||
**配置项**:
|
||||
```json
|
||||
{
|
||||
"vivado": {
|
||||
"enabled": true,
|
||||
"executablePath": "C:/Xilinx/Vivado/2023.1/bin/vivado.bat",
|
||||
"workingDir": "${workspaceFolder}/vivado_project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── utils/
|
||||
│ ├── vivadoRunner.ts # Vivado 执行器
|
||||
│ ├── vivadoConfig.ts # 配置读取
|
||||
│ └── tclGenerator.ts # TCL 脚本生成
|
||||
└── utils/
|
||||
└── messageHandler.ts # 工具调用处理(新增部分)
|
||||
```
|
||||
|
||||
## 5. 实现要点
|
||||
|
||||
### 5.1 子进程执行
|
||||
|
||||
```typescript
|
||||
const process = spawn(vivadoPath, ['-mode', 'batch', '-source', tclPath]);
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
resolve({ success: code === 0, output });
|
||||
});
|
||||
```
|
||||
|
||||
### 5.2 报告解析
|
||||
|
||||
从 `.rpt` 文件中提取关键信息:
|
||||
- 资源使用:LUT、FF、BRAM 数量
|
||||
- 时序信息:WNS、TNS
|
||||
|
||||
### 5.3 错误处理
|
||||
|
||||
捕获常见错误:
|
||||
- Vivado 未配置
|
||||
- 文件不存在
|
||||
- 综合/实现失败
|
||||
- 时序不满足
|
||||
|
||||
返回清晰的错误信息给后端。
|
||||
|
||||
## 6. 测试
|
||||
|
||||
**单元测试**:
|
||||
- TCL 脚本生成正确性
|
||||
- 配置读取
|
||||
|
||||
**集成测试**:
|
||||
- 完整流程测试(需要本地 Vivado)
|
||||
- 错误处理测试
|
||||
251
media/USER_MANUAL.md
Normal file
@ -0,0 +1,251 @@
|
||||
# IC Coder 插件端用户手册
|
||||
|
||||
欢迎**宁德时代新能源科技股份有限公司**的各位专家使用 IC Coder 企业版!
|
||||
|
||||
本手册旨在为贵公司提供插件的安装、配置及使用指导,帮助您快速上手并充分发挥工具的功能。
|
||||
|
||||
在使用过程中,如遇任何功能异常、性能问题或操作疑问,欢迎随时联系我们,我们将在第一时间为您提供支持。
|
||||
|
||||
若贵司有特定的业务场景或个性化功能需求,也可直接与我们沟通,我们会为贵司进行定制化开发。
|
||||
|
||||
| 功能 | 说明 |
|
||||
| ----------------------- | ---------------------------------------------------- |
|
||||
| 自研微调模型 | IC Coder 自研顶尖 Max 微调模型,专为 FPGA 开发 |
|
||||
| 高质量 Verilog 代码生成 | 根据自然语言描述生成符合规范的 Verilog 代码 |
|
||||
| 自动语法检查 | 自动检测代码语法错误并自动修复 |
|
||||
| 自动仿真 | 内置编译器,自动编译和仿真 |
|
||||
| 波形检查工具 | 内置 VCD 波形查看器,自动波形解析 |
|
||||
| 自主检查迭代 | AI 自动检查代码问题并迭代优化 |
|
||||
| Diff 功能 | 快速比较代码差异,追踪 AI 修改,可一键确认和撤销修改 |
|
||||
| Code Action 快捷操作 | Ctrl+L 快捷键或右键菜单快速添加代码到对话 |
|
||||
| 支持上下文连续对话 | 多轮对话,AI 记住之前的交互内容 |
|
||||
| 会话历史管理 | 自动保存对话记录,随时恢复历史会话 |
|
||||
|
||||
## IC Coder 快速入门指南
|
||||
|
||||
### 系统要求
|
||||
|
||||
- **Visual Studio Code**: 版本 >= 1.60.0
|
||||
|
||||
各位专家也可以通过这个链接下载VSCode [下载链接](https://code.visualstudio.com/)
|
||||
|
||||
---
|
||||
|
||||
### 安装步骤
|
||||
|
||||
#### 步骤 1:通过 VSIX 文件安装(推荐)
|
||||
|
||||
1. **获取安装包**
|
||||
- 确保您已经获得 `iccoder-Trial-1.0.vsix` 文件
|
||||
|
||||
2. **打开 VS Code**
|
||||
- 启动 Visual Studio Code
|
||||
|
||||
3. **安装插件**
|
||||
|
||||
有以下三种安装方式:
|
||||
|
||||
**方式 A:通过命令面板**
|
||||
- 按 `Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS) 打开命令面板
|
||||
- 输入 `Extensions: Install from VSIX...`
|
||||
- 选择 `iccoder-Trial-1.0.vsix` 文件
|
||||
- 等待安装完成
|
||||
|
||||

|
||||
|
||||
**方式 B:通过扩展视图**
|
||||
- 点击左侧活动栏的扩展图标(或按 `Ctrl+Shift+X`)
|
||||
- 点击扩展视图右上角的 `...` (更多操作)
|
||||
- 选择 `从 VSIX 安装...`
|
||||
- 选择 `iccoder-Trial-1.0.vsix` 文件
|
||||
|
||||

|
||||
|
||||
**方式 C:通过命令行**
|
||||
|
||||
```bash
|
||||
code --install-extension iccoder-Trial-1.0.vsix
|
||||
```
|
||||
|
||||
4. **重启 VS Code**
|
||||
- 安装完成后,建议重启 VS Code 以确保插件正常加载
|
||||
|
||||
#### 步骤 2:打开 IC Coder 界面
|
||||
|
||||
**登录后会自动打开**,手动打开也有以下几种方式:
|
||||
|
||||
**方式 1:通过侧边栏**
|
||||
|
||||
- 点击左侧活动栏的 IC Coder 图标
|
||||
- 侧边栏会显示 IC Coder 聊天界面
|
||||
|
||||

|
||||
|
||||
**方式 2:通过命令面板**
|
||||
|
||||
- 按 `Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS)
|
||||
- 输入以下命令之一:
|
||||
- `IC Coder: 打开聊天` - 打开聊天界面
|
||||
- `打开 IC Coder 助手` - 打开助手面板
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
#### 步骤 3:开始使用
|
||||
|
||||
插件已预配置好后端服务,安装后即可直接使用,无需手动配置。
|
||||
|
||||

|
||||
|
||||
### 故障排除
|
||||
|
||||
#### 问题 :插件无法安装
|
||||
|
||||
**症状**:安装 VSIX 文件时报错
|
||||
|
||||
#### 解决方案:
|
||||
|
||||
- 确认 VS Code 版本 >= 1.60.0
|
||||
|
||||
- 检查 VSIX 文件是否完整(未损坏)
|
||||
|
||||
- 尝试以管理员权限运行 VS Code
|
||||
|
||||
- 清除 VS Code 缓存后重试
|
||||
|
||||
### 完整使用流程示例
|
||||
|
||||
下面通过一个完整的案例,展示如何使用 IC Coder 从需求到代码生成的全过程。
|
||||
|
||||
#### 步骤 1:输入设计需求
|
||||
|
||||
在对话框中输入设计需求,例如:
|
||||
|
||||
```
|
||||
我需要设计一个 8 位加法器,要求有进位输入和进位输出
|
||||
```
|
||||
|
||||
点击**发送**按钮。
|
||||
|
||||

|
||||
|
||||
#### 步骤 2:AI 询问补充信息
|
||||
|
||||
AI 会根据需求,询问一些关键的设计细节。例如:
|
||||
|
||||
- 是否需要溢出检测?
|
||||
- 时钟频率要求是多少?
|
||||
- 是否需要流水线设计?
|
||||
|
||||
您只需要根据实际需求**选择相应的选项或者直接输入需求**即可,AI 会根据您的选择生成最合适的设计方案。
|
||||
|
||||
#### 步骤 3:确认 AI 生成的任务列表
|
||||
|
||||
AI 会根据您的需求和补充信息,生成一个详细的任务列表(Todo List)
|
||||
|
||||

|
||||
|
||||
仔细查看任务列表,确认无误后点击**确认**按钮,AI 将开始执行。
|
||||
|
||||
#### 步骤 4:观察 AI 执行过程
|
||||
|
||||
AI 开始工作后,您可以在对话框中实时看到所有执行步骤:
|
||||
|
||||

|
||||
|
||||
每个步骤完成后,任务列表中对应的项目会被标记为完成状态。
|
||||
|
||||
#### 步骤 5:仿真运行与结果查看
|
||||
|
||||
当 AI 完成代码生成后,会自动运行仿真验证:
|
||||
|
||||

|
||||
|
||||
#### 步骤 6:查看生成的文件
|
||||
|
||||
所有生成的文件会自动保存到您的工作目录中:
|
||||
|
||||
```
|
||||
project/
|
||||
├── src/
|
||||
│ └── tb_adder_8bit.v # RTL 设计文件
|
||||
├── sim/
|
||||
│ └── tb_adder_8bit # 测试平台文件
|
||||
└── tb_adder_8bit.vcd # 波形文件
|
||||
```
|
||||
|
||||
您可以在 VS Code 的文件资源管理器中直接打开这些文件进行查看或修改。
|
||||
|
||||
#### 步骤8:继续对话
|
||||
|
||||
如果您对给出的结果不太满意,您可以告诉IC Coder您想具体修改的地方或者文件
|
||||
|
||||
#### 使用流程总结
|
||||
|
||||
整个使用流程可以概括为:
|
||||
|
||||
- **输入需求** → 在对话框中描述您的设计需求
|
||||
- **回答问题** → 根据 AI 的询问选择合适的选项
|
||||
- **确认任务** → 查看并确认 AI 生成的任务列表
|
||||
- **观察执行** → 实时查看 AI 的所有执行步骤
|
||||
- **查看结果** → 仿真成功后查看生成的文件
|
||||
|
||||
#### 使用提示
|
||||
|
||||
**如何描述需求更准确?**
|
||||
|
||||
- **明确功能**:清楚说明模块要实现什么功能
|
||||
- **指定参数**:说明位宽、时钟频率等关键参数
|
||||
- **特殊要求**:如果有特殊的时序要求或接口规范,请明确说明
|
||||
|
||||
**示例:**
|
||||
|
||||
```
|
||||
好的描述:设计一个 16 位的 FIFO,深度为 256,支持异步读写
|
||||
不够清晰:帮我写一个 FIFO
|
||||
```
|
||||
|
||||
#### 常见问题
|
||||
|
||||
**Q: 仿真失败了怎么办?**
|
||||
A: AI 会根据错误自动修复代码并重新仿真。
|
||||
|
||||
**Q: 可以修改生成的代码吗?**
|
||||
A: 可以,可以直接编辑文件,然后告诉 AI 重新运行仿真。
|
||||
|
||||
**Q: 可以导入已有的代码吗?**
|
||||
A: 可以,在工作区中打开对应的代码文件夹,然后直接在对话中告诉 AI 您要修改或优化哪个文件,AI 会读取并进行修改。
|
||||
|
||||
**Q: 如何查看 AI 的思考过程?**
|
||||
A: 在执行过程中,AI 会实时显示每一步的操作和决策依据。
|
||||
|
||||
**Q: 如何保存对话历史?**
|
||||
A: 对话历史会自动保存在本地,可以点击历史对话查看历史会话记录。
|
||||
|
||||
---
|
||||
|
||||
### 卸载插件
|
||||
|
||||
如需卸载插件:
|
||||
|
||||
1. 打开扩展视图
|
||||
2. 找到 "IC Coder" 插件
|
||||
3. 点击卸载按钮
|
||||
4. 重启 VS Code
|
||||
|
||||
---
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **需提前打开一个文件夹作为工作区**,否则会准确的为您服务
|
||||
|
||||

|
||||
|
||||
2. **开箱即用**
|
||||
- 插件已预配置后端服务,无需手动设置
|
||||
- 安装后即可直接使用所有功能
|
||||
|
||||
---
|
||||
|
||||
**祝您使用愉快!如有问题欢迎反馈。**
|
||||
BIN
media/manual/仿真运行结果.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
media/manual/侧边栏打开.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
media/manual/命令面板打开.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
media/manual/安装方式1.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
media/manual/安装方式2.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
media/manual/打开文件夹.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
media/manual/确认任务.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
media/manual/聊天界面.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
media/manual/观察执行过程.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
media/manual/输入需求.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
@ -29,6 +29,9 @@ export interface IccoderConfig {
|
||||
serviceTier: ServiceTier;
|
||||
}
|
||||
|
||||
/** 自定义配置缓存 */
|
||||
let customConfig: Partial<IccoderConfig> | null = null;
|
||||
|
||||
/** 环境配置 */
|
||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 - 通过 Gateway 路由 */
|
||||
@ -38,7 +41,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
loginUrl: "http://localhost/login",
|
||||
timeout: 300000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max", // 默认使用 max
|
||||
serviceTier: "max",
|
||||
},
|
||||
/** 测试服务器环境 - 通过 Gateway 路由 */
|
||||
test: {
|
||||
@ -60,6 +63,13 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置自定义配置
|
||||
*/
|
||||
export function setCustomConfig(config: Partial<IccoderConfig>) {
|
||||
customConfig = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前环境
|
||||
*/
|
||||
@ -71,7 +81,14 @@ export function getCurrentEnv(): Environment {
|
||||
* 获取配置项
|
||||
*/
|
||||
export function getConfig(): IccoderConfig {
|
||||
return { ...ENV_CONFIG[CURRENT_ENV] };
|
||||
const baseConfig = { ...ENV_CONFIG[CURRENT_ENV] };
|
||||
|
||||
// 合并自定义配置(空字符串表示使用默认)
|
||||
if (customConfig?.backendUrl && customConfig.backendUrl !== '') {
|
||||
baseConfig.backendUrl = customConfig.backendUrl;
|
||||
}
|
||||
|
||||
return baseConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -200,8 +200,3 @@ export const setting = `<svg t="1768535209135" class="icon" viewBox="0 0 1024 10
|
||||
* 成功的图标svg
|
||||
*/
|
||||
export const successIconSvg = `<svg t="1771828214449" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5360" width="16" height="16"><path d="M748.864 302.528a49.024 49.024 0 0 1 68.288-1.28 46.656 46.656 0 0 1 5.952 61.44l-4.608 5.44-346.56 353.344a49.024 49.024 0 0 1-64.384 4.608l-5.504-4.864-196.8-203.264a46.72 46.72 0 0 1 1.792-66.88A49.024 49.024 0 0 1 270.08 448l5.312 4.736 162.048 167.232L748.8 302.528z" fill="#8a8a8a" p-id="5361"></path></svg>`;
|
||||
|
||||
/**
|
||||
* 任务完成的图标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>`;
|
||||
|
||||
101
src/extension.ts
@ -2,19 +2,27 @@ import * as vscode from "vscode";
|
||||
import { ICViewProvider } from "./views/ICViewProvider";
|
||||
import { showICHelperPanel } from "./panels/ICHelperPanel";
|
||||
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
|
||||
import { UserManualPanel } from "./panels/UserManualPanel";
|
||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||
import { VCDFileServer } from "./services/vcdFileServer";
|
||||
import { initUserService } from "./services/userService";
|
||||
import { initCreditsService } from "./services/creditsService";
|
||||
import { isTokenExpired } from "./utils/jwtUtils";
|
||||
import { NotificationService } from "./services/notificationService";
|
||||
import { InvitationService } from "./services/invitationService";
|
||||
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
|
||||
import { setCustomConfig } from "./config/settings";
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 加载保存的配置
|
||||
const savedSettings = context.globalState.get('generalSettings') as any;
|
||||
if (savedSettings?.backendUrl) {
|
||||
setCustomConfig({
|
||||
backendUrl: savedSettings.backendUrl,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建装饰类型(代码旁边的提示)
|
||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||
after: {
|
||||
@ -31,11 +39,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
if (!editor) return;
|
||||
|
||||
if (!editor.selection.isEmpty) {
|
||||
// 找到选区末尾所在的行,并将提示放在该行的末尾
|
||||
const { anchor, active } = editor.selection;
|
||||
const endPos = anchor.isAfter(active) ? anchor : active;
|
||||
const lineEndPos = editor.document.lineAt(endPos.line).range.end;
|
||||
const range = new vscode.Range(lineEndPos, lineEndPos);
|
||||
const range = new vscode.Range(editor.selection.end, editor.selection.end);
|
||||
const decoration = { range };
|
||||
editor.setDecorations(decorationType, [decoration]);
|
||||
} else {
|
||||
@ -54,33 +58,26 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const notificationService = NotificationService.getInstance(context);
|
||||
console.log('[Extension] 通知服务已初始化');
|
||||
|
||||
// 【关键】在创建 AuthProvider 之前,先检查并清除过期的 session
|
||||
const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
|
||||
console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
|
||||
|
||||
if (storedSessions.length > 0) {
|
||||
const session = storedSessions[0];
|
||||
const token = session.accessToken;
|
||||
console.log('[Extension] 检查 token 是否过期...');
|
||||
|
||||
if (token) {
|
||||
const expired = isTokenExpired(token);
|
||||
console.log('[Extension] token 过期检查结果:', expired);
|
||||
|
||||
if (expired) {
|
||||
// 必须等待清除完成后再创建 AuthProvider
|
||||
await context.globalState.update('icCoderSessions', []);
|
||||
await context.globalState.update('icCoderUserInfo', undefined);
|
||||
console.log('[Extension] Token 已过期,已清除所有登录状态');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化用户服务
|
||||
initUserService(context);
|
||||
|
||||
// 初始化 Credits 服务
|
||||
initCreditsService(context);
|
||||
// 【已禁用】登录和 token 验证 - 无需登录即可使用
|
||||
// const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
|
||||
// console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
|
||||
//
|
||||
// if (storedSessions.length > 0) {
|
||||
// const session = storedSessions[0];
|
||||
// const token = session.accessToken;
|
||||
// console.log('[Extension] 检查 token 是否过期...');
|
||||
//
|
||||
// if (token) {
|
||||
// const expired = isTokenExpired(token);
|
||||
// console.log('[Extension] token 过期检查结果:', expired);
|
||||
//
|
||||
// if (expired) {
|
||||
// await context.globalState.update('icCoderSessions', []);
|
||||
// await context.globalState.update('icCoderUserInfo', undefined);
|
||||
// console.log('[Extension] Token 已过期,已清除所有登录状态');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 初始化 VCD 文件服务器
|
||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||
@ -95,25 +92,18 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
dispose: () => vcdFileServer.stop()
|
||||
});
|
||||
|
||||
// 注册 Authentication Provider(此时 icCoderSessions 已经被清除)
|
||||
// 【已禁用】Authentication Provider 注册 - 无需登录
|
||||
const authProvider = new ICCoderAuthenticationProvider(context);
|
||||
context.subscriptions.push(
|
||||
vscode.authentication.registerAuthenticationProvider(
|
||||
"iccoder",
|
||||
"IC Coder",
|
||||
authProvider
|
||||
)
|
||||
);
|
||||
// context.subscriptions.push(
|
||||
// vscode.authentication.registerAuthenticationProvider(
|
||||
// "iccoder",
|
||||
// "IC Coder",
|
||||
// authProvider
|
||||
// )
|
||||
// );
|
||||
|
||||
// 检查登录状态,如果已登录则自动打开聊天面板
|
||||
vscode.authentication.getSession("iccoder", [], { createIfNone: false })
|
||||
.then((session) => {
|
||||
if (session) {
|
||||
vscode.commands.executeCommand("ic-coder.openChat");
|
||||
}
|
||||
}, () => {
|
||||
// 未登录,不做任何操作
|
||||
});
|
||||
// 【已禁用】登录状态检查 - 直接打开聊天面板
|
||||
vscode.commands.executeCommand("ic-coder.openChat");
|
||||
|
||||
// 注册命令:打开助手面板
|
||||
const openPanelCommand = vscode.commands.registerCommand(
|
||||
@ -192,6 +182,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:打开用户手册
|
||||
const openUserManualCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.openUserManual",
|
||||
() => {
|
||||
UserManualPanel.render(context.extensionUri);
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:用户登录
|
||||
const loginCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.login",
|
||||
@ -436,6 +434,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
openChatCommand,
|
||||
openVCDViewerCommand,
|
||||
openVCDViewerInBrowserCommand,
|
||||
openUserManualCommand,
|
||||
loginCommand,
|
||||
logoutCommand,
|
||||
changeInvitationCodeCommand,
|
||||
|
||||
181
src/panels/UserManualPanel.ts
Normal file
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 用户手册只读预览面板
|
||||
*/
|
||||
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export class UserManualPanel {
|
||||
public static currentPanel: UserManualPanel | undefined;
|
||||
private readonly _panel: vscode.WebviewPanel;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
|
||||
this._panel = panel;
|
||||
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||
this._update(extensionUri);
|
||||
}
|
||||
|
||||
public static render(extensionUri: vscode.Uri) {
|
||||
if (UserManualPanel.currentPanel) {
|
||||
UserManualPanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
|
||||
} else {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"userManual",
|
||||
"IC Coder 用户手册",
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")],
|
||||
},
|
||||
);
|
||||
UserManualPanel.currentPanel = new UserManualPanel(panel, extensionUri);
|
||||
}
|
||||
}
|
||||
|
||||
private async _update(extensionUri: vscode.Uri) {
|
||||
const manualPath = vscode.Uri.joinPath(
|
||||
extensionUri,
|
||||
"media",
|
||||
"USER_MANUAL.md",
|
||||
);
|
||||
const markdown = await vscode.workspace.fs.readFile(manualPath);
|
||||
const content = Buffer.from(markdown).toString("utf-8");
|
||||
this._panel.webview.html = await this._getHtmlContent(
|
||||
content,
|
||||
extensionUri,
|
||||
);
|
||||
}
|
||||
|
||||
private async _getHtmlContent(
|
||||
markdown: string,
|
||||
extensionUri: vscode.Uri,
|
||||
): Promise<string> {
|
||||
let inCodeBlock = false;
|
||||
let inTable = false;
|
||||
let tableRows: string[] = [];
|
||||
const lines: string[] = [];
|
||||
|
||||
// 先处理图片
|
||||
markdown = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => {
|
||||
const imgUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(extensionUri, "media", src),
|
||||
);
|
||||
return `<img src="${imgUri}" alt="${alt}">`;
|
||||
});
|
||||
|
||||
markdown.split("\n").forEach((line) => {
|
||||
// 代码块
|
||||
if (line.startsWith("```")) {
|
||||
if (inCodeBlock) {
|
||||
lines.push("</code></pre>");
|
||||
inCodeBlock = false;
|
||||
} else {
|
||||
lines.push("<pre><code>");
|
||||
inCodeBlock = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (inCodeBlock) {
|
||||
lines.push(line);
|
||||
return;
|
||||
}
|
||||
|
||||
// 表格
|
||||
if (line.startsWith("|")) {
|
||||
if (!inTable) inTable = true;
|
||||
tableRows.push(line);
|
||||
return;
|
||||
} else if (inTable) {
|
||||
// 表格结束
|
||||
const headers = tableRows[0]
|
||||
.split("|")
|
||||
.filter((c) => c.trim())
|
||||
.map((h) => `<th>${h.trim()}</th>`)
|
||||
.join("");
|
||||
const body = tableRows
|
||||
.slice(2)
|
||||
.map(
|
||||
(r) =>
|
||||
"<tr>" +
|
||||
r
|
||||
.split("|")
|
||||
.filter((c) => c.trim())
|
||||
.map((c) => `<td>${c.trim()}</td>`)
|
||||
.join("") +
|
||||
"</tr>",
|
||||
)
|
||||
.join("");
|
||||
lines.push(
|
||||
`<table><thead><tr>${headers}</tr></thead><tbody>${body}</tbody></table>`,
|
||||
);
|
||||
tableRows = [];
|
||||
inTable = false;
|
||||
}
|
||||
|
||||
// 其他行
|
||||
if (line === "---") lines.push("<hr>");
|
||||
else if (line.startsWith("#### "))
|
||||
lines.push(`<h4>${line.slice(5)}</h4>`);
|
||||
else if (line.startsWith("### ")) lines.push(`<h3>${line.slice(4)}</h3>`);
|
||||
else if (line.startsWith("## ")) lines.push(`<h2>${line.slice(3)}</h2>`);
|
||||
else if (line.startsWith("# ")) lines.push(`<h1>${line.slice(2)}</h1>`);
|
||||
else if (line.startsWith("- "))
|
||||
lines.push(
|
||||
`<li>${line.slice(2).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")}</li>`,
|
||||
);
|
||||
else if (line.trim() === "") lines.push("<p></p>");
|
||||
else
|
||||
lines.push(
|
||||
`<p>${line.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')}</p>`,
|
||||
);
|
||||
});
|
||||
|
||||
const html = lines
|
||||
.join("\n")
|
||||
.replace(/((?:<li>.*<\/li>\n?)+)/g, "<ul>$1</ul>");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
padding: 40px;
|
||||
line-height: 1.8;
|
||||
font-size: 16px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 { font-size: 2em; border-bottom: 3px solid #ddd; padding-bottom: 15px; margin: 30px 0 20px; }
|
||||
h2 { font-size: 1.6em; margin-top: 40px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||
h3 { font-size: 1.3em; margin-top: 30px; }
|
||||
h4 { font-size: 1.1em; margin-top: 20px; font-weight: 600; }
|
||||
p { margin: 15px 0; }
|
||||
img { display: block; margin: 30px auto; max-width: 100%; border: 1px solid #ddd; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
table { border-collapse: collapse; width: 100%; margin: 25px 0; font-size: 15px; }
|
||||
th, td { border: 1px solid #ddd; padding: 12px 16px; text-align: left; }
|
||||
th { background: #636363; font-weight: 600; }
|
||||
tr:hover { background: #636363; }
|
||||
ul { margin: 15px 0; padding-left: 30px; }
|
||||
li { margin: 8px 0; margin-left: 40px;}
|
||||
pre { background: #2f2f2f; padding: 20px; border-radius: 6px; overflow-x: auto; margin: 20px 0; border: 1px solid #e0e0e0; }
|
||||
code { font-family: 'Consolas', 'Monaco', monospace; font-size: 14px; line-height: 1.6; }
|
||||
hr { border: none; border-top: 2px solid #e0e0e0; margin: 30px 0; }
|
||||
a { color: #0066cc; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
strong { font-weight: 600; color: #e5e5e5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>${html}</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
UserManualPanel.currentPanel = undefined;
|
||||
this._panel.dispose();
|
||||
while (this._disposables.length) {
|
||||
this._disposables.pop()?.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
/**
|
||||
* 认证辅助模块
|
||||
* 功能:处理用户登录状态检查和 token 验证
|
||||
* 依赖:vscode, jwtUtils
|
||||
* 使用场景:面板初始化时验证用户登录状态
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { isTokenExpired } from "../../utils/jwtUtils";
|
||||
|
||||
export async function checkAuthAndPromptLogin(
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<boolean> {
|
||||
let token: string | undefined;
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
token = session?.accessToken;
|
||||
} catch (error) {
|
||||
console.warn("[AuthHelper] 获取 session 失败:", error);
|
||||
}
|
||||
|
||||
if (token && isTokenExpired(token)) {
|
||||
await context.globalState.update("icCoderSessions", []);
|
||||
await context.globalState.update("icCoderUserInfo", undefined);
|
||||
const action = await vscode.window.showWarningMessage(
|
||||
"登录已过期,请重新登录",
|
||||
"立即登录",
|
||||
);
|
||||
if (action === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", { forceReauth: true });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (!session) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
/**
|
||||
* 上下文管理模块
|
||||
* 功能:处理文件、文件夹、图片、文档上下文添加
|
||||
* 依赖:vscode, fs, path
|
||||
* 使用场景:用户添加上下文项时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function handleAddContextFile(panel: vscode.WebviewPanel) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await vscode.workspace.findFiles(
|
||||
"**/*",
|
||||
"**/node_modules/**",
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFileList",
|
||||
files: files.map((uri) => ({
|
||||
path: uri.fsPath,
|
||||
relativePath: vscode.workspace.asRelativePath(uri),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleAddContextFolder(panel: vscode.WebviewPanel) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const folders: Array<{ path: string; relativePath: string }> = [];
|
||||
|
||||
function scanFolders(dir: string, baseDir: string) {
|
||||
try {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const item of items) {
|
||||
if (
|
||||
item.isDirectory() &&
|
||||
item.name !== "node_modules" &&
|
||||
!item.name.startsWith(".")
|
||||
) {
|
||||
const fullPath = path.join(dir, item.name);
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
folders.push({ path: fullPath, relativePath });
|
||||
scanFolders(fullPath, baseDir);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("扫描文件夹失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFolderList",
|
||||
folders: folders,
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleAddContextImage(panel: vscode.WebviewPanel) {
|
||||
const imageUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择图片",
|
||||
filters: {
|
||||
图片文件: ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||
},
|
||||
});
|
||||
if (imageUris && imageUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextImagesSelected",
|
||||
images: imageUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleAddContextDocument(panel: vscode.WebviewPanel) {
|
||||
const docUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择文档",
|
||||
filters: {
|
||||
文档文件: ["pdf", "doc", "docx", "txt", "md"],
|
||||
所有文件: ["*"],
|
||||
},
|
||||
});
|
||||
if (docUris && docUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextDocumentsSelected",
|
||||
documents: docUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,224 +0,0 @@
|
||||
/**
|
||||
* 会话历史管理模块
|
||||
* 功能:加载和选择会话历史
|
||||
* 依赖:vscode, chatHistoryManager, messageHandler
|
||||
* 使用场景:会话历史列表和切换
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||
import { MessageType } from "../../types/chatHistory";
|
||||
import { setLastTaskId } from "../../utils/messageHandler";
|
||||
|
||||
export async function loadConversationHistory(
|
||||
panel: vscode.WebviewPanel,
|
||||
offset: number = 0,
|
||||
limit: number = 10,
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await historyManager.getConversationHistoryList(
|
||||
workspacePath,
|
||||
offset,
|
||||
limit,
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: result.items,
|
||||
total: result.total,
|
||||
hasMore: result.hasMore,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载会话历史失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectConversation(
|
||||
panel: vscode.WebviewPanel,
|
||||
taskId: string,
|
||||
extensionPath: string,
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
vscode.window.showErrorMessage("没有打开的工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const taskSession = await historyManager.loadTaskSession(
|
||||
workspacePath,
|
||||
taskId,
|
||||
);
|
||||
|
||||
if (!taskSession) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const switched = await historyManager.switchTask(workspacePath, taskId);
|
||||
if (!switched) {
|
||||
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
|
||||
return;
|
||||
}
|
||||
|
||||
setLastTaskId(taskId);
|
||||
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||
|
||||
panel.webview.postMessage({ command: "clearChat" });
|
||||
|
||||
const segments: any[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < taskSession.messages.length) {
|
||||
const message = taskSession.messages[i];
|
||||
|
||||
if (message.type === MessageType.USER) {
|
||||
if (segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: [...segments],
|
||||
});
|
||||
segments.length = 0;
|
||||
}
|
||||
|
||||
const textContent = message.contents?.find((c) => c.type === "TEXT");
|
||||
if (textContent && "text" in textContent) {
|
||||
panel.webview.postMessage({
|
||||
command: "addUserMessage",
|
||||
text: textContent.text,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
} else if (message.type === MessageType.AI) {
|
||||
if (message.segments && message.segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: message.segments,
|
||||
});
|
||||
i++;
|
||||
} else {
|
||||
if (message.text) {
|
||||
segments.push({ type: "text", content: message.text });
|
||||
}
|
||||
|
||||
if (
|
||||
message.toolExecutionRequests &&
|
||||
message.toolExecutionRequests.length > 0
|
||||
) {
|
||||
for (const toolReq of message.toolExecutionRequests) {
|
||||
let toolResult = "";
|
||||
if (i + 1 < taskSession.messages.length) {
|
||||
const nextMsg = taskSession.messages[i + 1];
|
||||
if (
|
||||
nextMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||
nextMsg.id === toolReq.id
|
||||
) {
|
||||
toolResult = nextMsg.text;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
segments.push({
|
||||
type: "tool",
|
||||
toolName: toolReq.name,
|
||||
askId: toolReq.id,
|
||||
toolResult: toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
while (i < taskSession.messages.length) {
|
||||
const nextMsg = taskSession.messages[i];
|
||||
if (nextMsg.type === MessageType.USER) {
|
||||
break;
|
||||
}
|
||||
if (nextMsg.type === MessageType.AI) {
|
||||
if (nextMsg.segments && nextMsg.segments.length > 0) {
|
||||
break;
|
||||
}
|
||||
if (nextMsg.text) {
|
||||
segments.push({ type: "text", content: nextMsg.text });
|
||||
}
|
||||
if (
|
||||
nextMsg.toolExecutionRequests &&
|
||||
nextMsg.toolExecutionRequests.length > 0
|
||||
) {
|
||||
for (const toolReq of nextMsg.toolExecutionRequests) {
|
||||
let toolResult = "";
|
||||
if (i + 1 < taskSession.messages.length) {
|
||||
const resultMsg = taskSession.messages[i + 1];
|
||||
if (
|
||||
resultMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||
resultMsg.id === toolReq.id
|
||||
) {
|
||||
toolResult = resultMsg.text;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
segments.push({
|
||||
type: "tool",
|
||||
toolName: toolReq.name,
|
||||
askId: toolReq.id,
|
||||
toolResult: toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
i++;
|
||||
} else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) {
|
||||
i++;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: segments,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送任务完成消息(历史记录)
|
||||
panel.webview.postMessage({
|
||||
command: "taskCompleteHistory",
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`已加载会话: ${taskSession.meta.taskName}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("选择会话失败:", error);
|
||||
vscode.window.showErrorMessage(`加载会话失败: ${error}`);
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
/**
|
||||
* 文件操作辅助模块
|
||||
* 功能:处理文件打开、选择等操作
|
||||
* 依赖:vscode, fs, path
|
||||
* 使用场景:打开文件、跳转到代码位置
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function openFile(filePath: string) {
|
||||
const path = require("path");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const fullPath =
|
||||
path.isAbsolute(filePath) || !workspaceFolder
|
||||
? filePath
|
||||
: vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath;
|
||||
const doc = await vscode.workspace.openTextDocument(fullPath);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
}
|
||||
|
||||
export async function openFileWithSelection(
|
||||
filePath: string,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
) {
|
||||
const path = require("path");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const fullPath =
|
||||
path.isAbsolute(filePath) || !workspaceFolder
|
||||
? filePath
|
||||
: vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath;
|
||||
const doc = await vscode.workspace.openTextDocument(fullPath);
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
const start = new vscode.Position(startLine - 1, 0);
|
||||
const end = new vscode.Position(
|
||||
endLine - 1,
|
||||
doc.lineAt(endLine - 1).text.length,
|
||||
);
|
||||
editor.selection = new vscode.Selection(start, end);
|
||||
editor.revealRange(new vscode.Range(start, end));
|
||||
}
|
||||
|
||||
export async function openFilePathTag(
|
||||
filePath: string,
|
||||
startLine?: number,
|
||||
endLine?: number,
|
||||
) {
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
|
||||
let fullPath = filePath;
|
||||
|
||||
if (!path.isAbsolute(filePath) && workspaceFolder) {
|
||||
const candidatePath = vscode.Uri.joinPath(
|
||||
workspaceFolder.uri,
|
||||
filePath,
|
||||
).fsPath;
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
fullPath = candidatePath;
|
||||
} else {
|
||||
const fileName = path.basename(filePath);
|
||||
const files = await vscode.workspace.findFiles(
|
||||
`**/${fileName}`,
|
||||
"**/node_modules/**",
|
||||
1,
|
||||
);
|
||||
if (files.length > 0) {
|
||||
fullPath = files[0].fsPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startLine && endLine) {
|
||||
await openFileWithSelection(fullPath, startLine, endLine);
|
||||
} else {
|
||||
await openFile(fullPath);
|
||||
}
|
||||
}
|
||||
@ -1,396 +0,0 @@
|
||||
/**
|
||||
* 消息路由处理模块
|
||||
* 功能:处理 webview 消息的路由分发
|
||||
* 依赖:各个 helper 模块和 messageHandler
|
||||
* 使用场景:webview 消息接收时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
handleUserMessage,
|
||||
insertCodeToEditor,
|
||||
handleReadFile,
|
||||
handleUpdateFile,
|
||||
handleRenameFile,
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
handlePlanAction,
|
||||
getCurrentTaskId,
|
||||
handleAcceptChange,
|
||||
handleRejectChange,
|
||||
handleOpenFileDiff,
|
||||
startChangeSession,
|
||||
} from "../../utils/messageHandler";
|
||||
import { compactDialog } from "../../services/apiClient";
|
||||
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||
import { getCachedUserInfo } from "../../services/userService";
|
||||
import { loadConversationHistory, selectConversation } from "./conversationHelper";
|
||||
import { getVCDFileInfo } from "./vcdHelper";
|
||||
import {
|
||||
handleAddContextFile,
|
||||
handleAddContextFolder,
|
||||
handleAddContextImage,
|
||||
handleAddContextDocument,
|
||||
} from "./contextHelper";
|
||||
import { openFile, openFileWithSelection, openFilePathTag } from "./fileHelper";
|
||||
|
||||
export async function handleWebviewMessage(
|
||||
message: any,
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
if (!historyManager.getPanelTask(panelId)) {
|
||||
const workspacePath =
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (workspacePath) {
|
||||
try {
|
||||
const taskMeta = await historyManager.createTask(
|
||||
workspacePath,
|
||||
"新对话",
|
||||
);
|
||||
historyManager.setPanelTask(
|
||||
panelId,
|
||||
taskMeta.taskId,
|
||||
workspacePath,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("创建任务失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
const sessionId = `session_${panelId}_${Date.now()}`;
|
||||
startChangeSession(sessionId);
|
||||
panel.webview.postMessage({ type: "showProgress" });
|
||||
|
||||
handleUserMessage(
|
||||
panel,
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode,
|
||||
message.model,
|
||||
message.contextItems,
|
||||
);
|
||||
break;
|
||||
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
break;
|
||||
|
||||
case "updateFile":
|
||||
handleUpdateFile(panel, message.filePath, message.content);
|
||||
break;
|
||||
|
||||
case "renameFile":
|
||||
handleRenameFile(panel, message.oldPath, message.newPath);
|
||||
break;
|
||||
|
||||
case "replaceInFile":
|
||||
handleReplaceInFile(
|
||||
panel,
|
||||
message.filePath,
|
||||
message.searchText,
|
||||
message.replaceText,
|
||||
);
|
||||
break;
|
||||
|
||||
case "insertCode":
|
||||
insertCodeToEditor(message.code);
|
||||
break;
|
||||
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
|
||||
case "openWaveformViewer":
|
||||
if (message.vcdFilePath) {
|
||||
vscode.commands.executeCommand(
|
||||
"ic-coder.openVCDViewer",
|
||||
message.vcdFilePath,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "getVCDInfo":
|
||||
if (message.vcdFilePath && message.containerId) {
|
||||
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "createNewConversation":
|
||||
const { showICHelperPanel } = require("../ICHelperPanel");
|
||||
showICHelperPanel(context, panel.viewColumn);
|
||||
break;
|
||||
|
||||
case "loadConversationHistory":
|
||||
loadConversationHistory(
|
||||
panel,
|
||||
message.offset || 0,
|
||||
message.limit || 10,
|
||||
);
|
||||
break;
|
||||
|
||||
case "selectConversation":
|
||||
if (message.conversationId) {
|
||||
selectConversation(panel, message.conversationId, context.extensionPath);
|
||||
}
|
||||
break;
|
||||
|
||||
case "submitAnswer":
|
||||
void handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput,
|
||||
message.answers,
|
||||
);
|
||||
break;
|
||||
|
||||
case "abortDialog":
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
|
||||
case "compressConversation":
|
||||
{
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
compactDialog(taskId)
|
||||
.then((result) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: result.success
|
||||
? "✅ 会话压缩完成"
|
||||
: `❌ 压缩失败: ${result.error || "未知错误"}`,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "❌ 没有活跃的会话",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "optimizePrompt":
|
||||
if (typeof message.prompt === "string") {
|
||||
void handleOptimizePrompt(panel, message.prompt);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: "提示词为空或格式错误",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "logout":
|
||||
vscode.commands.executeCommand("ic-coder.logout");
|
||||
break;
|
||||
|
||||
case "openFile":
|
||||
if (message.filePath) {
|
||||
await openFile(message.filePath);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFileWithSelection":
|
||||
if (message.filePath) {
|
||||
await openFileWithSelection(
|
||||
message.filePath,
|
||||
message.startLine,
|
||||
message.endLine,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFilePathTag":
|
||||
if (message.filePath) {
|
||||
await openFilePathTag(
|
||||
message.filePath,
|
||||
message.startLine,
|
||||
message.endLine,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "acceptChange":
|
||||
if (message.changeId) {
|
||||
await handleAcceptChange(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "rejectChange":
|
||||
if (message.changeId) {
|
||||
await handleRejectChange(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFileDiff":
|
||||
if (message.changeId) {
|
||||
await handleOpenFileDiff(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkInvitationCode":
|
||||
{
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (userInfo?.isPluginTrial === true) {
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeStatus",
|
||||
verified: true,
|
||||
});
|
||||
} else {
|
||||
const { InvitationService } = require("../../services/invitationService");
|
||||
const isVerified = await InvitationService.isVerified(context);
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeStatus",
|
||||
verified: isVerified,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkWelcomeModal":
|
||||
{
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (userInfo?.isPluginTrial === true) {
|
||||
if (userInfo.pluginTrialExpiresAt === undefined) {
|
||||
break;
|
||||
}
|
||||
if (userInfo.pluginTrialExpiresAt !== null) {
|
||||
const now = Date.now();
|
||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
if (isExpired) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
panel.webview.postMessage({ command: "showWelcomeModal" });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkTrialExpiration":
|
||||
{
|
||||
const { TrialExpirationService } = require("../../services/trialExpirationService");
|
||||
const trialService = new TrialExpirationService(context, panel);
|
||||
await trialService.checkExpiration();
|
||||
}
|
||||
break;
|
||||
|
||||
case "verifyInvitationCode":
|
||||
{
|
||||
const { InvitationService } = require("../../services/invitationService");
|
||||
const result = await InvitationService.verifyCode(message.code);
|
||||
|
||||
if (result.success) {
|
||||
await InvitationService.saveVerificationStatus(context, message.code);
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeVerified",
|
||||
success: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({ command: "showNdtWelcomeModal" });
|
||||
}, 300);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeVerified",
|
||||
success: false,
|
||||
message: result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "openICCoder":
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
|
||||
case "openTutorial":
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse(
|
||||
"https://www.iccoder.com/guides/quick-start/first-task-plugin",
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
||||
case "openUserManual":
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
|
||||
case "openUserFeedback":
|
||||
panel.webview.postMessage({ command: "showFeedbackQRCode" });
|
||||
break;
|
||||
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
panel.webview.postMessage({ command: "switchMode", mode: "agent" });
|
||||
} else if (message.action === "modify" || message.action === "cancel") {
|
||||
void handlePlanAction(
|
||||
panel,
|
||||
message.action,
|
||||
message.planTitle || "",
|
||||
context.extensionPath,
|
||||
message.model,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "addContextFile":
|
||||
await handleAddContextFile(panel);
|
||||
break;
|
||||
|
||||
case "addContextFolder":
|
||||
await handleAddContextFolder(panel);
|
||||
break;
|
||||
|
||||
case "addContextImage":
|
||||
await handleAddContextImage(panel);
|
||||
break;
|
||||
|
||||
case "addContextDocument":
|
||||
await handleAddContextDocument(panel);
|
||||
break;
|
||||
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
vscode.workspace.workspaceFolders &&
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
);
|
||||
if (!hasWorkspace) {
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||
"打开文件夹",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "打开文件夹") {
|
||||
vscode.commands.executeCommand("vscode.openFolder");
|
||||
}
|
||||
});
|
||||
}
|
||||
panel.webview.postMessage({
|
||||
command: "workspaceStatus",
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
break;
|
||||
|
||||
case "openExternalUrl":
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
/**
|
||||
* 用户信息辅助模块
|
||||
* 功能:管理用户信息的获取、更新和发送
|
||||
* 依赖:vscode, userService, creditsService
|
||||
* 使用场景:面板初始化和余额更新时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { getCachedUserInfo } from "../../services/userService";
|
||||
import { setBalanceUpdateCallback } from "../../services/creditsService";
|
||||
|
||||
export function getTierIconUri(
|
||||
webview: vscode.Webview,
|
||||
context: vscode.ExtensionContext,
|
||||
tierCode?: string,
|
||||
): string | undefined {
|
||||
if (!tierCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tierIconMap: Record<string, string> = {
|
||||
BASIC: "free.png",
|
||||
TRIAL: "PRO-Try.png",
|
||||
ADVANCED: "PRO.png",
|
||||
PROFESSIONAL: "PRO+.png",
|
||||
};
|
||||
|
||||
const iconFile = tierIconMap[tierCode];
|
||||
if (!iconFile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iconUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"dist",
|
||||
"assets",
|
||||
"titleIcon",
|
||||
iconFile,
|
||||
),
|
||||
);
|
||||
|
||||
return iconUri.toString();
|
||||
}
|
||||
|
||||
export async function sendUserInfoToWebview(
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
try {
|
||||
let userInfo = getCachedUserInfo();
|
||||
|
||||
if (userInfo) {
|
||||
console.log("[UserInfoHelper] 使用缓存的用户信息:", userInfo);
|
||||
const tierIconUrl = getTierIconUri(
|
||||
panel.webview,
|
||||
context,
|
||||
userInfo.membership?.tierCode,
|
||||
);
|
||||
panel.webview.postMessage({
|
||||
command: "updateUserInfo",
|
||||
userInfo: {
|
||||
userId: userInfo.userId,
|
||||
nickname: userInfo.nickname,
|
||||
username: userInfo.username,
|
||||
credits: userInfo.credits,
|
||||
membership: userInfo.membership,
|
||||
},
|
||||
tierIconUrl: tierIconUrl,
|
||||
});
|
||||
} else {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (session) {
|
||||
panel.webview.postMessage({
|
||||
command: "updateUserInfo",
|
||||
userInfo: {
|
||||
userId: session.account.id,
|
||||
nickname: session.account.label,
|
||||
username: session.account.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[UserInfoHelper] 获取用户信息失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function setupBalanceUpdateCallback(
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
setBalanceUpdateCallback((balance: number) => {
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (userInfo) {
|
||||
userInfo.credits = balance;
|
||||
const tierIconUrl = getTierIconUri(
|
||||
panel.webview,
|
||||
context,
|
||||
userInfo.membership?.tierCode,
|
||||
);
|
||||
panel.webview.postMessage({
|
||||
command: "updateUserInfo",
|
||||
userInfo: {
|
||||
userId: userInfo.userId,
|
||||
nickname: userInfo.nickname,
|
||||
username: userInfo.username,
|
||||
credits: balance,
|
||||
membership: userInfo.membership,
|
||||
},
|
||||
tierIconUrl: tierIconUrl,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1,158 +0,0 @@
|
||||
/**
|
||||
* VCD 文件处理模块
|
||||
* 功能:VCD 文件信息获取和信号解析
|
||||
* 依赖:vscode, fs
|
||||
* 使用场景:波形查看器相关功能
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function getVCDFileInfo(
|
||||
panel: vscode.WebviewPanel,
|
||||
vcdFilePath: string,
|
||||
containerId: string,
|
||||
) {
|
||||
try {
|
||||
const fs = require("fs");
|
||||
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: "文件不存在",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(vcdFilePath);
|
||||
const fileSizeKB = stats.size / 1024;
|
||||
const fileSize =
|
||||
fileSizeKB < 1024
|
||||
? `${fileSizeKB.toFixed(2)} KB`
|
||||
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
|
||||
|
||||
const content = fs.readFileSync(vcdFilePath, "utf-8");
|
||||
const varMatches = content.match(/\$var/g);
|
||||
const signalCount = varMatches ? varMatches.length : 0;
|
||||
|
||||
let timeRange = "N/A";
|
||||
const timeMatch = content.match(/#(\d+)/g);
|
||||
if (timeMatch && timeMatch.length > 0) {
|
||||
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
|
||||
const minTime = Math.min(...times);
|
||||
const maxTime = Math.max(...times);
|
||||
timeRange = `${minTime} - ${maxTime}`;
|
||||
}
|
||||
|
||||
const signals = parseVCDSignals(content, 3);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: signalCount.toString(),
|
||||
timeRange: timeRange,
|
||||
fileSize: fileSize,
|
||||
signals: signals,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取 VCD 文件信息失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
const signals: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
values: Array<{ time: number; value: string }>;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
|
||||
let match;
|
||||
const signalDefs: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
}> = [];
|
||||
|
||||
while (
|
||||
(match = varRegex.exec(content)) !== null &&
|
||||
signalDefs.length < maxSignals
|
||||
) {
|
||||
const width = parseInt(match[2]);
|
||||
const identifier = match[3];
|
||||
const name = match[4].trim();
|
||||
signalDefs.push({ name, identifier, width });
|
||||
}
|
||||
|
||||
const dumpvarsIndex = content.indexOf("$dumpvars");
|
||||
if (dumpvarsIndex === -1) {
|
||||
return signals;
|
||||
}
|
||||
|
||||
const dataSection = content.substring(dumpvarsIndex);
|
||||
|
||||
for (const signalDef of signalDefs) {
|
||||
const values: Array<{ time: number; value: string }> = [];
|
||||
let currentTime = 0;
|
||||
const lines = dataSection.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.startsWith("#")) {
|
||||
currentTime = parseInt(trimmedLine.substring(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signalDef.width === 1) {
|
||||
const singleBitMatch = trimmedLine.match(
|
||||
new RegExp(`^([01xz])${signalDef.identifier}$`),
|
||||
);
|
||||
if (singleBitMatch) {
|
||||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||
}
|
||||
} else {
|
||||
const multiBitMatch = trimmedLine.match(
|
||||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`),
|
||||
);
|
||||
if (multiBitMatch) {
|
||||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||
}
|
||||
}
|
||||
|
||||
if (values.length >= 50) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
signals.push({
|
||||
name: signalDef.name,
|
||||
identifier: signalDef.identifier,
|
||||
width: signalDef.width,
|
||||
values: values,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析 VCD 信号数据失败:", error);
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
@ -30,7 +30,6 @@ import type {
|
||||
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
|
||||
import { updateCachedBalance } from "./creditsService";
|
||||
|
||||
/**
|
||||
* 消息段落类型
|
||||
@ -129,6 +128,8 @@ export class DialogSession {
|
||||
private currentTextSegment: MessageSegment | null = null;
|
||||
private completeCallback: ((segments: MessageSegment[]) => void) | null =
|
||||
null; // 保存完成回调,用于 abort 时触发
|
||||
private consecutiveToolErrors = 0; // 连续工具错误计数
|
||||
private readonly MAX_CONSECUTIVE_ERRORS = 5; // 最大连续错误次数
|
||||
|
||||
constructor(extensionPath: string, existingTaskId?: string) {
|
||||
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
@ -444,6 +445,7 @@ export class DialogSession {
|
||||
const expired = isTokenExpired(session.accessToken);
|
||||
if (expired === true) {
|
||||
console.error("[DialogSession] token 已过期,需要重新登录");
|
||||
/*
|
||||
vscode.window
|
||||
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
||||
.then((selection) => {
|
||||
@ -453,6 +455,7 @@ export class DialogSession {
|
||||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
throw new Error("登录已过期,请重新登录");
|
||||
}
|
||||
|
||||
@ -899,6 +902,7 @@ export class DialogSession {
|
||||
data.message.includes("LOGIN_EXPIRED") ||
|
||||
data.message.includes("登录状态已过期")
|
||||
) {
|
||||
/*
|
||||
vscode.window
|
||||
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
||||
.then((selection) => {
|
||||
@ -908,6 +912,7 @@ export class DialogSession {
|
||||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
// 登录过期错误已处理,不再传递给外部
|
||||
return;
|
||||
}
|
||||
@ -1017,8 +1022,9 @@ export class DialogSession {
|
||||
data.remainingCredits
|
||||
);
|
||||
// 更新余额缓存
|
||||
updateCachedBalance(data.remainingCredits);
|
||||
// updateCachedBalance(data.remainingCredits);
|
||||
// 资源点余额低于阈值时弹窗提醒
|
||||
/*
|
||||
const LOW_CREDIT_THRESHOLD = 5;
|
||||
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
|
||||
vscode.window
|
||||
@ -1030,13 +1036,13 @@ export class DialogSession {
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "去充值") {
|
||||
// 打开充值页面
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse("https://iccoder.com/recharge")
|
||||
vscode.Uri.parse("https://iccoder.com/memberCenter")
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
onOpen: () => {
|
||||
|
||||
@ -2,7 +2,6 @@ import * as vscode from "vscode";
|
||||
import * as http from "http";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||
import { getConfig } from "../config/settings";
|
||||
import { resetInvitationVerification } from "./apiClient";
|
||||
|
||||
@ -85,7 +84,7 @@ export class ICCoderAuthenticationProvider
|
||||
const oldSession = this._sessions[0];
|
||||
this._sessions = [];
|
||||
await this.saveSessions();
|
||||
await clearUserInfo();
|
||||
// await clearUserInfo();
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
removed: [oldSession],
|
||||
@ -97,15 +96,15 @@ export class ICCoderAuthenticationProvider
|
||||
const token = await this.login();
|
||||
|
||||
// 获取到 token 后立即调用用户信息接口
|
||||
const userInfo = await onTokenReceived(token);
|
||||
// const userInfo = await onTokenReceived(token);
|
||||
|
||||
// 创建会话
|
||||
const session: vscode.AuthenticationSession = {
|
||||
id: this.generateSessionId(),
|
||||
accessToken: token,
|
||||
account: {
|
||||
id: userInfo?.userId || "iccoder-user",
|
||||
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
|
||||
id: "user",
|
||||
label: "IC Coder User",
|
||||
},
|
||||
scopes: [...scopes],
|
||||
};
|
||||
@ -158,7 +157,7 @@ export class ICCoderAuthenticationProvider
|
||||
await this.saveSessions();
|
||||
|
||||
// 3. 清除用户信息缓存
|
||||
await clearUserInfo();
|
||||
// await clearUserInfo();
|
||||
|
||||
// 4. 触发会话变化事件
|
||||
this._onDidChangeSessions.fire({
|
||||
@ -182,14 +181,14 @@ export class ICCoderAuthenticationProvider
|
||||
*/
|
||||
async clearSessionsForRelogin(): Promise<void> {
|
||||
if (this._sessions.length === 0) {
|
||||
await clearUserInfo();
|
||||
// await clearUserInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
const removed = [...this._sessions];
|
||||
this._sessions = [];
|
||||
await this.saveSessions();
|
||||
await clearUserInfo();
|
||||
// await clearUserInfo();
|
||||
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
|
||||
@ -161,7 +161,16 @@ export async function startStreamDialog(
|
||||
|
||||
const body = JSON.stringify(request);
|
||||
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
|
||||
console.log('[SSE] 请求详情:', {
|
||||
url: urlString,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
hasToken: !!request.token,
|
||||
},
|
||||
body: request
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options: http.RequestOptions = {
|
||||
|
||||
@ -6,7 +6,7 @@ import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import * as fs from "fs";
|
||||
import { readFileContent, readDirectory } from "../utils/readFiles";
|
||||
import { readFileContent, readDirectory, listDirectory } from "../utils/readFiles";
|
||||
import { createOrOverwriteFile } from "../utils/createFiles";
|
||||
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
|
||||
import { changeTracker } from "./changeTracker";
|
||||
@ -126,8 +126,9 @@ export async function executeToolCall(
|
||||
|
||||
// 提交成功结果
|
||||
const result = createSuccessResult(callId, resultText);
|
||||
console.log(`[ToolExecutor] 准备提交结果: ${toolName}, callId=${callId}`);
|
||||
await submitToolResult(result);
|
||||
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
|
||||
console.log(`[ToolExecutor] 结果提交成功: ${toolName}, callId=${callId}`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||
console.error(
|
||||
@ -136,8 +137,20 @@ export async function executeToolCall(
|
||||
);
|
||||
|
||||
// 提交错误结果
|
||||
const result = createBusinessErrorResult(callId, errorMessage);
|
||||
await submitToolResult(result);
|
||||
try {
|
||||
const result = createBusinessErrorResult(callId, errorMessage);
|
||||
console.log(`[ToolExecutor] 准备提交错误结果: ${toolName}, callId=${callId}`);
|
||||
await submitToolResult(result);
|
||||
console.log(`[ToolExecutor] 错误结果提交成功: ${toolName}, callId=${callId}`);
|
||||
} catch (submitError) {
|
||||
console.error(
|
||||
`[ToolExecutor] 提交错误结果失败: ${toolName}, callId=${callId}`,
|
||||
submitError,
|
||||
);
|
||||
throw submitError;
|
||||
}
|
||||
// 重新抛出原始错误,让调用方知道工具执行失败
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,10 +287,8 @@ async function executeFileList(args: FileListArgs): Promise<string> {
|
||||
const dirPath = args.path || ".";
|
||||
const extensions = args.extension ? [args.extension] : undefined;
|
||||
|
||||
const files = await readDirectory(dirPath, extensions);
|
||||
const fileList = files.map((f) => f.path).join("\n");
|
||||
|
||||
return fileList || "(目录为空)";
|
||||
const files = await listDirectory(dirPath, extensions);
|
||||
return files.join("\n") || "(目录为空)";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -335,43 +335,21 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
// 保存到持久化存储
|
||||
await saveUserInfo(userInfo);
|
||||
|
||||
// 判断是否是插件试用用户
|
||||
console.log('[UserService] 检查用户类型,isPluginTrial:', userInfo.isPluginTrial);
|
||||
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
|
||||
|
||||
if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
|
||||
// 检查是否过期
|
||||
const now = Date.now();
|
||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
console.log('[UserService] 试用到期时间:', new Date(userInfo.pluginTrialExpiresAt).toLocaleString());
|
||||
console.log('[UserService] 当前时间:', new Date(now).toLocaleString());
|
||||
console.log('[UserService] 是否过期:', isExpired);
|
||||
|
||||
if (isExpired) {
|
||||
// 已过期:显示邀请码弹窗
|
||||
console.log('[UserService] 试用已过期,将显示邀请码弹窗');
|
||||
} else {
|
||||
// 未过期:显示欢迎弹窗
|
||||
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
|
||||
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
|
||||
|
||||
if (!hasWelcomed && extensionContext) {
|
||||
await extensionContext.globalState.update('showWelcomeModal', true);
|
||||
await extensionContext.globalState.update('pluginTrialWelcomed', true);
|
||||
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
|
||||
|
||||
const checkMark = extensionContext.globalState.get('showWelcomeModal');
|
||||
console.log('[UserService] 验证标记:', checkMark);
|
||||
} else if (!extensionContext) {
|
||||
console.error('[UserService] ❌ extensionContext 为 null,无法设置标记');
|
||||
} else {
|
||||
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// isPluginTrial=false 或 enterpriseTrialExpires 为 null:显示邀请码弹窗
|
||||
console.log('[UserService] 非试用用户或无过期时间,将显示邀请码弹窗');
|
||||
}
|
||||
// 【已禁用】试用用户和欢迎弹窗逻辑 - 无需登录
|
||||
// if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
|
||||
// const now = Date.now();
|
||||
// const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
// if (isExpired) {
|
||||
// console.log('[UserService] 试用已过期,将显示邀请码弹窗');
|
||||
// } else {
|
||||
// const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
|
||||
// if (!hasWelcomed && extensionContext) {
|
||||
// await extensionContext.globalState.update('showWelcomeModal', true);
|
||||
// await extensionContext.globalState.update('pluginTrialWelcomed', true);
|
||||
// console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
|
||||
@ -201,6 +201,33 @@ async function findVerilogFiles(dir: string): Promise<string[]> {
|
||||
return verilogFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找目录下所有 VCD 文件
|
||||
*/
|
||||
async function findVcdFilesRecursive(dir: string): Promise<string[]> {
|
||||
const vcdFiles: string[] = [];
|
||||
|
||||
async function searchDir(currentDir: string) {
|
||||
const dirUri = vscode.Uri.file(currentDir);
|
||||
const entries = await vscode.workspace.fs.readDirectory(dirUri);
|
||||
|
||||
for (const [fileName, fileType] of entries) {
|
||||
const filePath = path.join(currentDir, fileName);
|
||||
|
||||
if (fileType === vscode.FileType.Directory) {
|
||||
if (!fileName.startsWith(".") && fileName !== "node_modules") {
|
||||
await searchDir(filePath);
|
||||
}
|
||||
} else if (fileType === vscode.FileType.File && fileName.endsWith(".vcd")) {
|
||||
vcdFiles.push(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await searchDir(dir);
|
||||
return vcdFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 iverilog 可执行文件路径
|
||||
*/
|
||||
@ -304,8 +331,8 @@ export async function generateVCD(
|
||||
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
|
||||
};
|
||||
|
||||
// 5. 构建 iverilog 编译参数
|
||||
const compileArgs = ["-o", outputFile, ...projectCheck.allVerilogFiles];
|
||||
// 5. 构建 iverilog 编译参数(启用 SystemVerilog 2012 标准)
|
||||
const compileArgs = ["-g2012", "-o", outputFile, ...projectCheck.allVerilogFiles];
|
||||
|
||||
console.log("执行编译命令:", iverilogPath, compileArgs.join(" "));
|
||||
console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT);
|
||||
@ -317,7 +344,10 @@ export async function generateVCD(
|
||||
cwd: projectPath,
|
||||
env: env,
|
||||
});
|
||||
console.log("编译成功,stdout:", compileResult.stdout);
|
||||
console.log("编译成功,stderr:", compileResult.stderr);
|
||||
} catch (error: any) {
|
||||
console.error("编译失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `IC Coder编译器编译失败:\n${error.message}`,
|
||||
@ -326,9 +356,20 @@ export async function generateVCD(
|
||||
};
|
||||
}
|
||||
|
||||
// 6.1 检查 .vvp 文件是否生成
|
||||
const fs = require("fs");
|
||||
if (!fs.existsSync(outputFile)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `编译未生成 .vvp 文件: ${outputFile}`,
|
||||
stderr: compileResult.stderr,
|
||||
stdout: compileResult.stdout,
|
||||
};
|
||||
}
|
||||
console.log("已生成 .vvp 文件:", outputFile);
|
||||
|
||||
// 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误)
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const vvpContent = fs.readFileSync(outputFile, "utf8");
|
||||
const lines = vvpContent.split("\n");
|
||||
|
||||
@ -336,9 +377,62 @@ export async function generateVCD(
|
||||
const cleanedContent = lines.slice(1).join("\n");
|
||||
fs.writeFileSync(outputFile, cleanedContent, "utf8");
|
||||
console.log("已删除 .vvp 文件的 shebang 行");
|
||||
} else {
|
||||
console.log(".vvp 文件无 shebang 行,跳过");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("删除 shebang 失败,继续执行:", error);
|
||||
console.error("删除 shebang 失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `处理 .vvp 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 6.6. 检查并创建 VCD 输出目录,并处理 Windows 路径问题
|
||||
try {
|
||||
const tbPath = projectCheck.testbenchFile;
|
||||
if (tbPath && fs.existsSync(tbPath)) {
|
||||
const tbContent = fs.readFileSync(tbPath, "utf8");
|
||||
const dumpfileMatch = tbContent.match(/\$dumpfile\s*\(\s*["']([^"']+)["']\s*\)/);
|
||||
if (dumpfileMatch) {
|
||||
const vcdPath = dumpfileMatch[1];
|
||||
const vcdDir = path.dirname(vcdPath);
|
||||
console.log(`testbench 中的 VCD 路径: ${vcdPath}`);
|
||||
|
||||
if (vcdDir && vcdDir !== "." && vcdDir !== "") {
|
||||
const vcdDirPath = path.join(projectPath, vcdDir);
|
||||
console.log(`检查 VCD 目录: ${vcdDirPath}`);
|
||||
if (!fs.existsSync(vcdDirPath)) {
|
||||
fs.mkdirSync(vcdDirPath, { recursive: true });
|
||||
console.log(`已创建 VCD 输出目录: ${vcdDirPath}`);
|
||||
} else {
|
||||
console.log(`VCD 目录已存在: ${vcdDirPath}`);
|
||||
}
|
||||
|
||||
// Windows 兼容性:修改 .vvp 文件中的路径,将正斜杠替换为反斜杠
|
||||
if (process.platform === "win32" && vcdPath.includes("/")) {
|
||||
const vvpContent = fs.readFileSync(outputFile, "utf8");
|
||||
const windowsPath = vcdPath.replace(/\//g, "\\\\");
|
||||
const modifiedContent = vvpContent.replace(
|
||||
new RegExp(`"${vcdPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`, 'g'),
|
||||
`"${windowsPath}"`
|
||||
);
|
||||
fs.writeFileSync(outputFile, modifiedContent, "utf8");
|
||||
console.log(`已修正 VCD 路径: ${vcdPath} -> ${windowsPath}`);
|
||||
}
|
||||
} else {
|
||||
console.log("VCD 文件在根目录,无需创建子目录");
|
||||
}
|
||||
} else {
|
||||
console.warn("testbench 中未找到 $dumpfile 语句");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("处理 VCD 路径失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `处理 VCD 路径失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 7. 执行仿真生成 VCD
|
||||
@ -351,7 +445,11 @@ export async function generateVCD(
|
||||
cwd: projectPath,
|
||||
env: env,
|
||||
});
|
||||
console.log("仿真执行完成");
|
||||
console.log("仿真 stdout:", simResult.stdout);
|
||||
console.log("仿真 stderr:", simResult.stderr);
|
||||
} catch (error: any) {
|
||||
console.error("仿真失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `VVP 仿真失败:\n${error.message}`,
|
||||
@ -361,16 +459,38 @@ export async function generateVCD(
|
||||
}
|
||||
|
||||
// 8. 查找生成的 VCD 文件
|
||||
const projectUri = vscode.Uri.file(projectPath);
|
||||
const entries = await vscode.workspace.fs.readDirectory(projectUri);
|
||||
const vcdFiles = entries
|
||||
.filter(
|
||||
([fileName, fileType]) =>
|
||||
fileType === vscode.FileType.File && fileName.endsWith(".vcd"),
|
||||
)
|
||||
.map(([fileName]) => fileName);
|
||||
let vcdFile: string | null = null;
|
||||
|
||||
if (vcdFiles.length === 0) {
|
||||
// 8.1 尝试从 testbench 中提取 VCD 路径
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const tbPath = projectCheck.testbenchFile;
|
||||
if (tbPath && fs.existsSync(tbPath)) {
|
||||
const tbContent = fs.readFileSync(tbPath, "utf8");
|
||||
const dumpfileMatch = tbContent.match(/\$dumpfile\s*\(\s*["']([^"']+)["']\s*\)/);
|
||||
if (dumpfileMatch) {
|
||||
const vcdPath = dumpfileMatch[1];
|
||||
const absoluteVcdPath = path.join(projectPath, vcdPath);
|
||||
if (fs.existsSync(absoluteVcdPath)) {
|
||||
vcdFile = absoluteVcdPath;
|
||||
console.log(`找到 VCD 文件(从 testbench): ${vcdFile}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("从 testbench 提取 VCD 路径失败:", error);
|
||||
}
|
||||
|
||||
// 8.2 如果未找到,递归搜索项目目录
|
||||
if (!vcdFile) {
|
||||
const foundFiles = await findVcdFilesRecursive(projectPath);
|
||||
if (foundFiles.length > 0) {
|
||||
vcdFile = foundFiles[0];
|
||||
console.log(`找到 VCD 文件(递归搜索): ${vcdFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!vcdFile) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
@ -379,9 +499,6 @@ export async function generateVCD(
|
||||
};
|
||||
}
|
||||
|
||||
// 使用找到的第一个 VCD 文件
|
||||
const vcdFile = path.join(projectPath, vcdFiles[0]);
|
||||
|
||||
// 9. 清理中间文件
|
||||
try {
|
||||
const outputUri = vscode.Uri.file(outputFile);
|
||||
@ -594,8 +711,9 @@ export async function generateMultiVCD(
|
||||
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
|
||||
|
||||
try {
|
||||
// 编译(带宏定义)
|
||||
// 编译(带宏定义,启用 SystemVerilog 2012 标准)
|
||||
const compileArgs = [
|
||||
"-g2012",
|
||||
`-D${macroName}`,
|
||||
"-o",
|
||||
outputFile,
|
||||
|
||||
@ -14,16 +14,11 @@ 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";
|
||||
import { healthCheck } from "../services/apiClient";
|
||||
import { isTokenExpired } from "./jwtUtils";
|
||||
import {
|
||||
checkBalanceBeforeSend,
|
||||
fetchBalance,
|
||||
} from "../services/creditsService";
|
||||
import { optimizePrompt } from "../services/promptOptimizeService";
|
||||
import { NotificationService } from "../services/notificationService";
|
||||
import { TrialExpirationService } from "../services/trialExpirationService";
|
||||
@ -42,6 +37,9 @@ let currentSession: DialogSession | null = null;
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
/** 离线模式仿真模拟标志(防止重复触发) */
|
||||
let offlineSimulationTriggered = false;
|
||||
|
||||
async function trackFileChange(
|
||||
filePath: string,
|
||||
oldContent: string,
|
||||
@ -67,9 +65,9 @@ export async function handleUserMessage(
|
||||
) {
|
||||
console.log("收到用户消息:", text);
|
||||
|
||||
// 检查 token 是否过期
|
||||
// 【已禁用】检查 token 是否过期 - 无需登录
|
||||
const context = (panel as any).__context;
|
||||
if (context) {
|
||||
if (false && context) {
|
||||
// 从 session 中获取 token
|
||||
let token: string | undefined;
|
||||
try {
|
||||
@ -113,7 +111,7 @@ export async function handleUserMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTokenExpired(token)) {
|
||||
if (token && isTokenExpired(token as string)) {
|
||||
console.warn("[MessageHandler] Token 已过期,阻止发送");
|
||||
|
||||
// 保存待发送的消息
|
||||
@ -129,6 +127,7 @@ export async function handleUserMessage(
|
||||
await context.globalState.update("icCoderUserInfo", undefined);
|
||||
|
||||
// 显示弹窗提示
|
||||
/*
|
||||
const action = await vscode.window.showWarningMessage(
|
||||
"登录已过期,请重新登录",
|
||||
"立即登录",
|
||||
@ -139,6 +138,7 @@ export async function handleUserMessage(
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
@ -190,29 +190,6 @@ export async function handleUserMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送前检测余额
|
||||
const balanceCheck = await checkBalanceBeforeSend();
|
||||
if (!balanceCheck.allowed) {
|
||||
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
|
||||
// 显示错误提示
|
||||
const selection = await vscode.window.showWarningMessage(
|
||||
balanceCheck.message || "资源点余额不足",
|
||||
"去充值",
|
||||
);
|
||||
if (selection === "去充值") {
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse("https://iccoder.com/memberCenter"),
|
||||
);
|
||||
}
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试使用后端服务
|
||||
if (useBackendService && extensionPath) {
|
||||
try {
|
||||
@ -227,10 +204,10 @@ export async function handleUserMessage(
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("当前访问人数过多,请稍后重试:", error);
|
||||
console.error("处理用户消息失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "当前访问人数过多,请稍后重试",
|
||||
text: "处理用户消息失败,请稍后重试",
|
||||
type: "error",
|
||||
});
|
||||
// 恢复输入状态
|
||||
@ -284,6 +261,8 @@ async function handleUserMessageWithBackend(
|
||||
);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
// 重置离线模式仿真标志(新会话开始)
|
||||
offlineSimulationTriggered = false;
|
||||
console.log(
|
||||
"[MessageHandler] 创建会话: taskId=",
|
||||
lastTaskId,
|
||||
@ -320,6 +299,45 @@ async function handleUserMessageWithBackend(
|
||||
command: "updateSegments",
|
||||
segments: filteredSegments,
|
||||
});
|
||||
|
||||
// 【离线部署模式】检测代码生成完成消息,模拟仿真流程
|
||||
if (!offlineSimulationTriggered) {
|
||||
const hasCompletionMessage = segments.some(seg =>
|
||||
seg.type === 'text' &&
|
||||
seg.content?.includes('【代码生成完成】') &&
|
||||
seg.content?.includes('语法检查:已通过')
|
||||
);
|
||||
|
||||
if (hasCompletionMessage) {
|
||||
offlineSimulationTriggered = true;
|
||||
console.log('[离线模式] 检测到代码生成完成,开始模拟仿真流程');
|
||||
|
||||
// 立即点亮 Simulation 阶段
|
||||
panel.webview.postMessage({
|
||||
type: "updateProgress",
|
||||
step: "simulation"
|
||||
});
|
||||
|
||||
// 随机延时 8-13 秒后完成仿真
|
||||
const simulationDelay = 8000 + Math.random() * 5000;
|
||||
setTimeout(() => {
|
||||
console.log('[离线模式] 模拟仿真完成,进入 Done 阶段');
|
||||
// Simulation 完成,进入 Done
|
||||
panel.webview.postMessage({
|
||||
type: "updateProgress",
|
||||
step: "done"
|
||||
});
|
||||
|
||||
// 再延时 1 秒完成所有步骤
|
||||
setTimeout(() => {
|
||||
console.log('[离线模式] 所有阶段完成');
|
||||
panel.webview.postMessage({
|
||||
type: "completeProgress"
|
||||
});
|
||||
}, 1000);
|
||||
}, simulationDelay);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onToolStart: (toolName) => {
|
||||
@ -369,17 +387,6 @@ async function handleUserMessageWithBackend(
|
||||
console.error("[MessageHandler] 保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
// 对话完成后重新获取余额(因为已经消耗了 Credits)
|
||||
try {
|
||||
console.log("[MessageHandler] 对话完成,重新获取余额...");
|
||||
const newBalance = await fetchBalance();
|
||||
if (newBalance !== null) {
|
||||
console.log("[MessageHandler] 余额已更新:", newBalance);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[MessageHandler] 获取余额失败:", error);
|
||||
}
|
||||
|
||||
// 尝试更新面板(如果面板已关闭,这些操作会失败,但不影响数据保存)
|
||||
try {
|
||||
// 隐藏状态栏
|
||||
@ -428,7 +435,7 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 错误: ${message}`,
|
||||
text: `错误: ${message}`,
|
||||
});
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
@ -1469,38 +1476,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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,6 +129,62 @@ export async function readDirectory(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出目录下的文件和文件夹(不读取内容,仅返回路径)
|
||||
*/
|
||||
export async function listDirectory(
|
||||
dirPath: string,
|
||||
extensions?: string[]
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
// 如果是相对路径,转换为绝对路径
|
||||
let absolutePath = dirPath;
|
||||
if (!path.isAbsolute(dirPath)) {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
const dirUri = vscode.Uri.file(absolutePath);
|
||||
|
||||
// 检查目录是否存在
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(dirUri);
|
||||
if (stat.type !== vscode.FileType.Directory) {
|
||||
throw new Error(`路径不是目录: ${absolutePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`目录不存在: ${absolutePath}`);
|
||||
}
|
||||
|
||||
// 读取目录内容
|
||||
const entries = await vscode.workspace.fs.readDirectory(dirUri);
|
||||
const results: string[] = [];
|
||||
|
||||
for (const [fileName, fileType] of entries) {
|
||||
if (fileType === vscode.FileType.Directory) {
|
||||
results.push(fileName + '/');
|
||||
} else if (fileType === vscode.FileType.File) {
|
||||
// 扩展名过滤
|
||||
if (extensions && extensions.length > 0) {
|
||||
const ext = path.extname(fileName);
|
||||
// 规范化扩展名(支持 "v" 和 ".v" 两种格式)
|
||||
const normalizedExts = extensions.map(e => e.startsWith('.') ? e : '.' + e);
|
||||
if (!normalizedExts.includes(ext)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
results.push(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
*/
|
||||
|
||||
@ -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}`
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -13,6 +13,7 @@ import {
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
} from "../utils/messageHandler";
|
||||
import { setCustomConfig } from "../config/settings";
|
||||
|
||||
/**
|
||||
* 创建并显示IC 侧边栏视图
|
||||
@ -124,6 +125,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
case "showWarning":
|
||||
vscode.window.showWarningMessage(message.message);
|
||||
break;
|
||||
// 新增:打开用户手册
|
||||
case "openUserManual":
|
||||
vscode.commands.executeCommand("ic-coder.openUserManual");
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
@ -141,6 +146,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
case "optimizePrompt":
|
||||
handleOptimizePrompt(panel, message.prompt);
|
||||
break;
|
||||
// 保存通用设置
|
||||
case "saveGeneralSettings":
|
||||
context.globalState.update('generalSettings', message.settings);
|
||||
// 更新运行时配置(包括清空)
|
||||
setCustomConfig({ backendUrl: message.settings.backendUrl || '' });
|
||||
vscode.window.showInformationMessage('设置已保存');
|
||||
break;
|
||||
// 加载通用设置
|
||||
case "loadGeneralSettings":
|
||||
const settings = context.globalState.get('generalSettings');
|
||||
panel.webview.postMessage({
|
||||
command: 'loadedGeneralSettings',
|
||||
settings: settings
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
@ -158,52 +178,21 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
private readonly extensionUri: vscode.Uri,
|
||||
private readonly context: vscode.ExtensionContext
|
||||
) {
|
||||
// 监听认证状态变化
|
||||
this.context.subscriptions.push(
|
||||
vscode.authentication.onDidChangeSessions((e) => {
|
||||
if (e.provider.id === "iccoder") {
|
||||
this.refreshLoginStatus();
|
||||
}
|
||||
})
|
||||
);
|
||||
// 【已禁用】监听认证状态变化 - 无需登录
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新登录状态并更新视图
|
||||
* 【已禁用】刷新登录状态并更新视图 - 无需登录
|
||||
*/
|
||||
private async refreshLoginStatus(): Promise<void> {
|
||||
if (this._view) {
|
||||
const isLoggedIn = await this.checkLoginStatus();
|
||||
this._view.webview.html = this.getWebviewContent(
|
||||
this._view.webview,
|
||||
isLoggedIn
|
||||
);
|
||||
}
|
||||
// 无需刷新登录状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态(使用 Authentication API)
|
||||
* 【已禁用】检查登录状态 - 无需登录
|
||||
*/
|
||||
private async checkLoginStatus(): Promise<boolean> {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
console.log("[ICViewProvider] 检查登录状态, session:", session ? "存在" : "不存在");
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
// 检查 token 是否过期
|
||||
const expired = isTokenExpired(session.accessToken);
|
||||
console.log("[ICViewProvider] token 过期检查结果:", expired);
|
||||
// 只有明确过期才认为未登录,无法判断时认为已登录
|
||||
if (expired === true) {
|
||||
console.log("[ICViewProvider] Token 已过期");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("[ICViewProvider] 检查登录状态失败:", error);
|
||||
return false;
|
||||
}
|
||||
return true; // 始终返回已登录状态
|
||||
}
|
||||
|
||||
resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||
@ -223,30 +212,8 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
console.log('[ICViewProvider] Webview options 已设置');
|
||||
console.log('[ICViewProvider] extensionUri:', this.extensionUri.toString());
|
||||
|
||||
// 【关键修复】先设置默认 HTML,避免一直加载
|
||||
try {
|
||||
const html = this.getWebviewContent(webviewView.webview, false);
|
||||
console.log('[ICViewProvider] HTML 内容已生成,长度:', html.length);
|
||||
webviewView.webview.html = html;
|
||||
console.log('[ICViewProvider] HTML 已设置到 webview');
|
||||
} catch (error) {
|
||||
console.error('[ICViewProvider] 设置 HTML 失败:', error);
|
||||
}
|
||||
|
||||
// 异步检查登录状态并更新 UI
|
||||
this.checkLoginStatus()
|
||||
.then((isLoggedIn) => {
|
||||
console.log('[ICViewProvider] 登录状态检查完成:', isLoggedIn);
|
||||
webviewView.webview.html = this.getWebviewContent(
|
||||
webviewView.webview,
|
||||
isLoggedIn
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[ICViewProvider] 检查登录状态失败:', error);
|
||||
// 即使失败也显示未登录状态
|
||||
webviewView.webview.html = this.getWebviewContent(webviewView.webview, false);
|
||||
});
|
||||
// 【已禁用】登录检查 - 直接显示"开始使用"按钮
|
||||
webviewView.webview.html = this.getWebviewContent(webviewView.webview, true);
|
||||
|
||||
// 处理侧边栏的消息
|
||||
webviewView.webview.onDidReceiveMessage(
|
||||
@ -261,11 +228,28 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
} else if (message.command === "openICCoder") {
|
||||
// 打开 IC Coder 官网
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
||||
} else if (message.command === "openUserManual") {
|
||||
// 打开用户手册
|
||||
vscode.commands.executeCommand("ic-coder.openUserManual");
|
||||
} else if (message.command === "openExternalUrl") {
|
||||
// 打开外部链接
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
} else if (message.command === "saveGeneralSettings") {
|
||||
// 保存通用设置
|
||||
this.context.globalState.update('generalSettings', message.settings);
|
||||
if (message.settings.backendUrl) {
|
||||
setCustomConfig({ backendUrl: message.settings.backendUrl });
|
||||
}
|
||||
vscode.window.showInformationMessage('设置已保存');
|
||||
} else if (message.command === "loadGeneralSettings") {
|
||||
// 加载通用设置
|
||||
const settings = this.context.globalState.get('generalSettings');
|
||||
webviewView.webview.postMessage({
|
||||
command: 'loadedGeneralSettings',
|
||||
settings: settings
|
||||
});
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
@ -338,7 +322,7 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
<img src="${logoUri}" alt="IC Coder" width="120" />
|
||||
<h2>欢迎使用 IC Coder</h2>
|
||||
${isLoggedIn
|
||||
? '<button class="btn" onclick="openChat()">开始创作</button>'
|
||||
? '<button class="btn" onclick="openChat()">开始使用</button>'
|
||||
: '<button class="btn" onclick="login()">登录账户</button>'
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -47,11 +47,7 @@ export function getConversationHistoryBarContent(): string {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="user-info-container">
|
||||
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
|
||||
${userAvatarIconSvg}
|
||||
</button>
|
||||
${getUserInfoComponentContent()}
|
||||
<div class="user-info-container" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div class='setting'>
|
||||
|
||||
@ -41,14 +41,6 @@ export function getExampleShowcaseContent(): string {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="web-link">
|
||||
<a href="https://iccoder.com" target="_blank" class="web-link-button">
|
||||
<span class="link-icon">🌐</span>
|
||||
<span>IC Coder Web端</span>
|
||||
<span class="link-arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -204,41 +196,6 @@ export function getExampleShowcaseStyles(): string {
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.web-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.web-link-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.web-link-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.web-link-button:hover {
|
||||
transform: translateY(-1px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
@ -247,10 +204,6 @@ export function getExampleShowcaseStyles(): string {
|
||||
font-size: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.web-link-button:hover .link-arrow {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@ -4,75 +4,15 @@
|
||||
export function getGeneralSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="general-settings">
|
||||
<h3 class="settings-section-title">通用设置</h3>
|
||||
<h3 class="settings-section-title">后端服务配置</h3>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">主题</label>
|
||||
<span class="settings-item-description">选择界面主题</span>
|
||||
<label class="settings-item-label">后端服务地址</label>
|
||||
<span class="settings-item-description">自定义后端 API 地址</span>
|
||||
</div>
|
||||
<select class="settings-select" id="themeSelect">
|
||||
<option value="auto">跟随系统</option>
|
||||
<option value="light">浅色</option>
|
||||
<option value="dark">深色</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">语言</label>
|
||||
<span class="settings-item-description">选择界面语言</span>
|
||||
</div>
|
||||
<select class="settings-select" id="languageSelect">
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="en-US">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">自动保存</label>
|
||||
<span class="settings-item-description">自动保存会话历史</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="autoSaveCheckbox" checked>
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">显示时间戳</label>
|
||||
<span class="settings-item-description">在消息中显示时间戳</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="showTimestampCheckbox">
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">编辑器设置</h4>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">字体大小</label>
|
||||
<span class="settings-item-description">设置编辑器字体大小</span>
|
||||
</div>
|
||||
<input type="number" class="settings-input" id="fontSizeInput" value="14" min="10" max="24">
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">代码高亮</label>
|
||||
<span class="settings-item-description">启用代码语法高亮</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="syntaxHighlightCheckbox" checked>
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
<input type="text" class="settings-input-text" id="backendUrlInput" placeholder="https://iccoder.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -176,6 +116,21 @@ export function getGeneralSettingsComponentStyles(): string {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.settings-input-text {
|
||||
width: 300px;
|
||||
padding: 6px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-input-text:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.settings-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@ -270,57 +225,37 @@ export function getGeneralSettingsComponentScript(): string {
|
||||
// 保存通用设置
|
||||
function saveGeneralSettings() {
|
||||
const settings = {
|
||||
theme: document.getElementById('themeSelect').value,
|
||||
language: document.getElementById('languageSelect').value,
|
||||
autoSave: document.getElementById('autoSaveCheckbox').checked,
|
||||
showTimestamp: document.getElementById('showTimestampCheckbox').checked,
|
||||
fontSize: document.getElementById('fontSizeInput').value,
|
||||
syntaxHighlight: document.getElementById('syntaxHighlightCheckbox').checked,
|
||||
backendUrl: document.getElementById('backendUrlInput').value,
|
||||
};
|
||||
|
||||
// 发送消息到扩展
|
||||
vscode.postMessage({
|
||||
command: 'saveGeneralSettings',
|
||||
settings: settings
|
||||
});
|
||||
|
||||
// 显示保存成功提示
|
||||
console.log('通用设置已保存', settings);
|
||||
closeSettingsModal();
|
||||
}
|
||||
|
||||
// 重置通用设置
|
||||
function resetGeneralSettings() {
|
||||
document.getElementById('themeSelect').value = 'auto';
|
||||
document.getElementById('languageSelect').value = 'zh-CN';
|
||||
document.getElementById('autoSaveCheckbox').checked = true;
|
||||
document.getElementById('showTimestampCheckbox').checked = false;
|
||||
document.getElementById('fontSizeInput').value = '14';
|
||||
document.getElementById('syntaxHighlightCheckbox').checked = true;
|
||||
document.getElementById('backendUrlInput').value = '';
|
||||
|
||||
// 清空保存的配置
|
||||
vscode.postMessage({
|
||||
command: 'saveGeneralSettings',
|
||||
settings: { backendUrl: '' }
|
||||
});
|
||||
|
||||
console.log('通用设置已重置为默认值');
|
||||
closeSettingsModal();
|
||||
}
|
||||
|
||||
// 加载通用设置
|
||||
function loadGeneralSettings(settings) {
|
||||
if (!settings) return;
|
||||
|
||||
if (settings.theme) {
|
||||
document.getElementById('themeSelect').value = settings.theme;
|
||||
}
|
||||
if (settings.language) {
|
||||
document.getElementById('languageSelect').value = settings.language;
|
||||
}
|
||||
if (settings.autoSave !== undefined) {
|
||||
document.getElementById('autoSaveCheckbox').checked = settings.autoSave;
|
||||
}
|
||||
if (settings.showTimestamp !== undefined) {
|
||||
document.getElementById('showTimestampCheckbox').checked = settings.showTimestamp;
|
||||
}
|
||||
if (settings.fontSize) {
|
||||
document.getElementById('fontSizeInput').value = settings.fontSize;
|
||||
}
|
||||
if (settings.syntaxHighlight !== undefined) {
|
||||
document.getElementById('syntaxHighlightCheckbox').checked = settings.syntaxHighlight;
|
||||
if (settings.backendUrl) {
|
||||
document.getElementById('backendUrlInput').value = settings.backendUrl;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,220 +0,0 @@
|
||||
/**
|
||||
* 消息渲染脚本模块
|
||||
* 功能:消息渲染、滚动控制、工具状态显示
|
||||
* 依赖:toolHelpers, textFormatter, waveformPreviewContent, agentCard, planCard, codeHighlight
|
||||
* 使用场景:webview 中的消息显示逻辑
|
||||
*/
|
||||
|
||||
import { collapseIconSvg } from "../constants/toolIcons";
|
||||
import { getWaveformPreviewScript } from "./waveformPreviewContent";
|
||||
import { getAgentCardScript } from "./agentCard";
|
||||
import { getPlanCardScript } from "./planCard";
|
||||
import { getCodeHighlightScript } from "../components/codeHighlight";
|
||||
|
||||
export function getMessageRendererScript(): string {
|
||||
return `
|
||||
${getAgentCardScript()}
|
||||
${getPlanCardScript()}
|
||||
|
||||
const toolCollapseStates = new Map();
|
||||
let shouldAutoScroll = true;
|
||||
let lastScrollHeight = 0;
|
||||
|
||||
function isUserNearBottom() {
|
||||
const threshold = 50;
|
||||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||
}
|
||||
|
||||
messagesEl.addEventListener('scroll', () => {
|
||||
const isAtBottom = isUserNearBottom();
|
||||
if (isAtBottom) {
|
||||
shouldAutoScroll = true;
|
||||
} else {
|
||||
if (messagesEl.scrollHeight === lastScrollHeight) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
}
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
});
|
||||
|
||||
function smartScrollToBottom() {
|
||||
if (shouldAutoScroll) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(text, sender) {
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message \${sender}-message\`;
|
||||
if (sender === 'bot') {
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const messageContent = document.createElement('span');
|
||||
messageContent.textContent = text;
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => copyMessage(text, copyBtn);
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
actionsDiv.appendChild(messageContent);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
div.appendChild(actionsDiv);
|
||||
} else {
|
||||
const parts = text.split(' ');
|
||||
const filePaths = [];
|
||||
const textParts = [];
|
||||
parts.forEach(part => {
|
||||
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) {
|
||||
filePaths.push(part);
|
||||
} else {
|
||||
textParts.push(part);
|
||||
}
|
||||
});
|
||||
if (filePaths.length > 0) {
|
||||
div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
|
||||
} else {
|
||||
div.textContent = text;
|
||||
}
|
||||
hideHeaderIfNeeded();
|
||||
}
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function hideHeaderIfNeeded() {
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function copyMessage(text, button) {
|
||||
// 从按钮的父消息元素中获取实际文本内容
|
||||
const messageDiv = button.closest('.message');
|
||||
const messageContent = messageDiv?.querySelector('.message-content, div:not(.message-actions)');
|
||||
const textToCopy = messageContent ? messageContent.textContent : text;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1 0.4-12.8-6.3-12.8z" fill="currentColor"/></svg>\`;
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLike(button) {
|
||||
const isActive = button.classList.contains('active');
|
||||
const parent = button.parentElement;
|
||||
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDislike(button) {
|
||||
const isActive = button.classList.contains('active');
|
||||
const parent = button.parentElement;
|
||||
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function updateOrCreateStreamingMessage(text) {
|
||||
hideLoadingIndicator();
|
||||
if (!currentStreamingMessage) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message streaming';
|
||||
const messageContent = document.createElement('div');
|
||||
messageContent.className = 'message-content';
|
||||
messageContent.textContent = text;
|
||||
div.appendChild(messageContent);
|
||||
messagesEl.appendChild(div);
|
||||
currentStreamingMessage = div;
|
||||
} else {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = text;
|
||||
}
|
||||
}
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function finalizeStreamingMessage(finalText) {
|
||||
if (currentStreamingMessage) {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = finalText;
|
||||
}
|
||||
currentStreamingMessage.classList.remove('streaming');
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
copyBtn.onclick = () => copyMessage(finalText, copyBtn);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
currentStreamingMessage.appendChild(actionsDiv);
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function showLoadingIndicator(text) {
|
||||
hideLoadingIndicator();
|
||||
loadingIndicator = document.createElement('div');
|
||||
loadingIndicator.className = 'message bot-message loading-message';
|
||||
loadingIndicator.innerHTML = \`
|
||||
<div class="loading-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<span class="loading-text">\${text}</span>
|
||||
\`;
|
||||
messagesEl.appendChild(loadingIndicator);
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function hideLoadingIndicator() {
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
loadingIndicator = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addToolStatus(toolName, status, detail) {
|
||||
const statusIcons = {
|
||||
start: '🔧',
|
||||
complete: '✅',
|
||||
error: '❌'
|
||||
};
|
||||
const statusTexts = {
|
||||
start: '正在执行',
|
||||
complete: '执行完成',
|
||||
error: '执行失败'
|
||||
};
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message tool-status tool-\${status}\`;
|
||||
div.innerHTML = \`
|
||||
<span class="tool-icon">\${statusIcons[status]}</span>
|
||||
<span class="tool-name">\${getToolDisplayName(toolName)}</span>
|
||||
<span class="tool-status-text">\${statusTexts[status]}</span>
|
||||
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
||||
\`;
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
${getCodeHighlightScript()}
|
||||
`;
|
||||
}
|
||||
@ -1,632 +0,0 @@
|
||||
/**
|
||||
* 消息样式模块
|
||||
* 功能:提供消息区域的所有 CSS 样式
|
||||
* 依赖:agentCard, planCard, codeHighlight, waveformPreviewContent
|
||||
* 使用场景:webview 样式注入
|
||||
*/
|
||||
|
||||
import { getAgentCardStyles } from "./agentCard";
|
||||
import { getPlanCardStyles } from "./planCard";
|
||||
import { getCodeHighlightStyles } from "../components/codeHighlight";
|
||||
import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||||
|
||||
export function getMessageAreaStyles(): string {
|
||||
return `
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 15px;
|
||||
min-height: 0;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.user-message {
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
margin-left: auto;
|
||||
width: fit-content;
|
||||
max-width: 80%;
|
||||
}
|
||||
.bot-message {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: var(--vscode-foreground);
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.message-actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.2s ease;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
}
|
||||
.message-actions > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.message-actions:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
position: relative;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.action-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.action-btn.active {
|
||||
color: var(--vscode-button-background);
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn .action-tooltip {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(5px);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;white-space: nowrap;pointer-events: none;
|
||||
}
|
||||
.action-btn .action-tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #1e1e1e transparent transparent transparent;
|
||||
}
|
||||
.action-btn .action-tooltip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
.action-btn:hover .action-tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.streaming .message-content {
|
||||
border-right: 2px solid var(--vscode-focusBorder);
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 50% { border-color: var(--vscode-focusBorder); }
|
||||
51%, 100% { border-color: transparent; }
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;}
|
||||
.loading-dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-focusBorder);
|
||||
animation: loadingDot 1.4s infinite ease-in-out;
|
||||
}
|
||||
.loading-dots span:nth-child(1) { animation-delay: 0s; }
|
||||
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes loadingDot {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
}
|
||||
.tool-status.tool-start {
|
||||
border-left: 3px solid var(--vscode-charts-blue);
|
||||
}
|
||||
.tool-status.tool-complete {
|
||||
border-left: 3px solid var(--vscode-charts-green);
|
||||
}
|
||||
.tool-status.tool-error {
|
||||
border-left: 3px solid var(--vscode-charts-red);
|
||||
}
|
||||
.tool-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
.tool-name {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.tool-status-text {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.tool-detail {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: pre-wrap;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.question-message {
|
||||
padding: 16px;
|
||||
}
|
||||
.question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.question-option {
|
||||
padding: 8px 16px;
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border: 1px solid #007ACC;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.question-message.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.custom-submit {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.question-message.answered .custom-input-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.segmented-message {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.segment-text h1,
|
||||
.segment-text h2,
|
||||
.segment-text h3,
|
||||
.question-text h1,
|
||||
.question-text h2,
|
||||
.question-text h3 {
|
||||
margin: 0px 0 -10px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.segment-text h1,
|
||||
.question-text h1 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.segment-text h2,
|
||||
.question-text h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.segment-text h3,
|
||||
.question-text h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.segment-text ul,
|
||||
.segment-text ol,
|
||||
.question-text ul,
|
||||
.question-text ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.segment-text li,
|
||||
.question-text li {
|
||||
line-height: 1;
|
||||
}
|
||||
.segment-text strong,
|
||||
.question-text strong {
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.segment-text em,
|
||||
.question-text em {
|
||||
font-style: italic;
|
||||
}
|
||||
.segment-text a,
|
||||
.question-text a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.segment-text a:hover,
|
||||
.question-text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.segment-text p,
|
||||
.question-text p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.segment-text code,
|
||||
.question-text code {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.segment-tool {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.segment-tool.low-profile {
|
||||
margin: 25px 0px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
.tool-segment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-segment-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
.tool-segment-name {
|
||||
font-weight: normal;
|
||||
}
|
||||
.tool-segment-result {
|
||||
display: inline;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 500px;
|
||||
}
|
||||
.tool-collapse-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-collapse-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.icon-expanded svg path {
|
||||
fill: #007ACC !important;
|
||||
}
|
||||
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
.tool-file-write-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-write-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-read-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-read-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-delete-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-syntax-check-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-syntax-check-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-search-code-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-search-code-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-save-knowledge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-save-knowledge-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-simulation-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-simulation-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-waveform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-waveform-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-knowledge-load-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-knowledge-load-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-state-transition-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-state-transition-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-segment-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
.tool-segment-content.collapsed {
|
||||
max-height: 0;
|
||||
}
|
||||
.tool-segment-description {
|
||||
margin: 25px 0 0 0px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-header {
|
||||
opacity: 0.65;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-icon {
|
||||
opacity: 0.55;
|
||||
font-size: 11px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-name {
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-result {
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.segment-question .question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.segment-question .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.segment-question .question-option {
|
||||
padding: 8px 16px;
|
||||
background: #3d3f41;
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid #474747;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question .question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.segment-question .question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.segment-question.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.segment-question .custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.segment-question .custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.segment-question .custom-submit {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.segment-question .custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.segment-question.answered .custom-input-container {
|
||||
display: none;
|
||||
}
|
||||
.question-segment .question-text {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-segment .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.question-opt {
|
||||
padding: 4px 10px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;}
|
||||
|
||||
${getAgentCardStyles()}
|
||||
|
||||
${getPlanCardStyles()}
|
||||
|
||||
${getCodeHighlightStyles()}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||
@ -9,66 +9,16 @@ export function getModelSelectorContent(
|
||||
autoIcon: string = "",
|
||||
liteIcon: string = "",
|
||||
syIcon: string = "",
|
||||
maxIcon: string = ""
|
||||
maxIcon: string = "",
|
||||
): string {
|
||||
return `
|
||||
<!-- 模型选择 -->
|
||||
<div class="tooltip">
|
||||
<div class="custom-select" id="modelSelect">
|
||||
<div class="select-trigger" onclick="toggleModelDropdown()">
|
||||
<span class="select-value" id="modelValue">Auto</span>
|
||||
<svg class="select-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M507.8 727.728a30.016 30.016 0 0 1-21.288-8.824L231.104 463.496a30.088 30.088 0 0 1 0-42.568 30.088 30.088 0 0 1 42.568 0l234.128 234.128 234.16-234.128a30.088 30.088 0 0 1 42.568 0 30.088 30.088 0 0 1 0 42.568L529.08 718.904a30 30 0 0 1-21.28 8.824z" fill="#8a8a8a"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="select-dropdown" id="modelDropdown">
|
||||
<div class="select-option selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
|
||||
${
|
||||
autoIcon
|
||||
? `<img src="${autoIcon}" class="model-icon" alt="Auto">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Auto</span>
|
||||
<span class="option-desc">智能匹配最优模型</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
|
||||
${
|
||||
liteIcon
|
||||
? `<img src="${liteIcon}" class="model-icon" alt="Lite">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Lite</span>
|
||||
<span class="option-desc">基础模型,快速相应,适合简单任务</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
|
||||
${
|
||||
syIcon
|
||||
? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Syntaxic</span>
|
||||
<span class="option-desc">均衡成本和性能,节省credits同时保持可靠输出</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
|
||||
${
|
||||
maxIcon
|
||||
? `<img src="${maxIcon}" class="model-icon" alt="Max">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Max</span>
|
||||
<span class="option-desc">最强性能,质量优先,适合复杂任务</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-display">
|
||||
<img src="${maxIcon || ""}" class="model-icon" alt="Max" style="display: ${maxIcon ? "block" : "none"};">
|
||||
<span class="model-label">Max</span>
|
||||
</div>
|
||||
<span class="tooltiptext">选择模型</span>
|
||||
<span class="tooltiptext">IC Coder自研FPGA专属微调模型</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -78,72 +28,16 @@ export function getModelSelectorContent(
|
||||
*/
|
||||
export function getModelSelectorStyles(): string {
|
||||
return `
|
||||
/* 自定义下拉框样式 */
|
||||
.custom-select {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
.select-trigger {
|
||||
.model-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.select-trigger:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.select-value {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.select-arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.custom-select.active .select-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.select-dropdown {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 2px);
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1100;
|
||||
display: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.custom-select.active .select-dropdown {
|
||||
display: block;
|
||||
}
|
||||
/* 模型选择器的选项样式 */
|
||||
#modelDropdown .select-option {
|
||||
position: relative;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
#modelDropdown .select-option:hover {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
#modelDropdown .select-option.selected {
|
||||
background: rgba(128, 128, 128, 0.5);
|
||||
color: var(--vscode-foreground);
|
||||
cursor: default;
|
||||
}
|
||||
.model-icon {
|
||||
width: 16px;
|
||||
@ -151,21 +45,7 @@ export function getModelSelectorStyles(): string {
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
.option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
.option-label {
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.option-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
.model-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
@ -176,58 +56,9 @@ export function getModelSelectorStyles(): string {
|
||||
*/
|
||||
export function getModelSelectorScript(): string {
|
||||
return `
|
||||
// 模型选择相关变量
|
||||
let currentModel = 'auto';
|
||||
|
||||
// 切换模型下拉框显示/隐藏
|
||||
function toggleModelDropdown() {
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
const customSelect = document.getElementById('customSelect');
|
||||
if (modelSelect) {
|
||||
modelSelect.classList.toggle('active');
|
||||
// 关闭模式下拉框
|
||||
if (customSelect) {
|
||||
customSelect.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选择模型
|
||||
function selectModel(value, label) {
|
||||
currentModel = value;
|
||||
const modelValue = document.getElementById('modelValue');
|
||||
if (modelValue) {
|
||||
modelValue.textContent = label;
|
||||
}
|
||||
|
||||
// 更新选中状态
|
||||
const options = document.querySelectorAll('#modelDropdown .select-option');
|
||||
options.forEach(option => {
|
||||
if (option.getAttribute('data-value') === value) {
|
||||
option.classList.add('selected');
|
||||
} else {
|
||||
option.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭下拉框
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
if (modelSelect) {
|
||||
modelSelect.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭模型下拉框
|
||||
document.addEventListener('click', (event) => {
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
if (modelSelect && !modelSelect.contains(event.target)) {
|
||||
modelSelect.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 获取当前选中的模型
|
||||
// 获取当前选中的模型(固定为 max)
|
||||
function getCurrentModel() {
|
||||
return currentModel;
|
||||
return 'max';
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 更多选项组件
|
||||
* 包含用户手册和用户反馈入口
|
||||
* 包含用户手册入口
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -28,40 +28,10 @@ export function getMoreOptionsComponentContent(): string {
|
||||
<div class="option-desc">查看使用文档和帮助</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="more-option-item" id="userFeedbackOption">
|
||||
<div class="option-icon">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<div class="option-label">用户反馈</div>
|
||||
<div class="option-desc">提交问题和建议</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户反馈二维码弹窗 -->
|
||||
<div class="feedback-qrcode-modal" id="feedbackQRCodeModal">
|
||||
<div class="feedback-qrcode-overlay" onclick="closeFeedbackQRCode()"></div>
|
||||
<div class="feedback-qrcode-content">
|
||||
<div class="feedback-qrcode-header">
|
||||
<span class="feedback-qrcode-title">用户反馈</span>
|
||||
<button class="feedback-qrcode-close" onclick="closeFeedbackQRCode()">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="feedback-qrcode-body">
|
||||
<img class="feedback-qrcode-image" id="feedbackQRCodeImage" alt="微信二维码" />
|
||||
<p class="feedback-qrcode-text">扫描二维码添加微信反馈</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -163,125 +133,6 @@ export function getMoreOptionsComponentStyles(): string {
|
||||
.option-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 用户反馈二维码弹窗 */
|
||||
.feedback-qrcode-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feedback-qrcode-modal.active {
|
||||
display: flex;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-qrcode-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.feedback-qrcode-content {
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
animation: slideUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-qrcode-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.feedback-qrcode-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.feedback-qrcode-close {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.feedback-qrcode-close:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.feedback-qrcode-close svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.feedback-qrcode-body {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.feedback-qrcode-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.feedback-qrcode-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -331,29 +182,6 @@ export function getMoreOptionsComponentScript(): string {
|
||||
closeMoreOptionsDropdown();
|
||||
}
|
||||
|
||||
// 打开用户反馈
|
||||
function openUserFeedback() {
|
||||
console.log('打开用户反馈');
|
||||
vscode.postMessage({ command: 'openUserFeedback' });
|
||||
closeMoreOptionsDropdown();
|
||||
}
|
||||
|
||||
// 显示用户反馈二维码弹窗
|
||||
function showFeedbackQRCode() {
|
||||
const modal = document.getElementById('feedbackQRCodeModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭用户反馈二维码弹窗
|
||||
function closeFeedbackQRCode() {
|
||||
const modal = document.getElementById('feedbackQRCodeModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定更多选项事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 绑定用户手册选项
|
||||
@ -362,12 +190,6 @@ export function getMoreOptionsComponentScript(): string {
|
||||
userManualOption.addEventListener('click', openUserManual);
|
||||
}
|
||||
|
||||
// 绑定用户反馈选项
|
||||
const userFeedbackOption = document.getElementById('userFeedbackOption');
|
||||
if (userFeedbackOption) {
|
||||
userFeedbackOption.addEventListener('click', openUserFeedback);
|
||||
}
|
||||
|
||||
// 点击页面其他地方关闭下拉面板
|
||||
document.addEventListener('click', (e) => {
|
||||
const dropdown = document.getElementById('moreOptionsDropdown');
|
||||
|
||||
@ -59,7 +59,7 @@ export function getPlanCardStyles(): string {
|
||||
.plan-summary h2 { font-size: 16px; }
|
||||
.plan-summary h3 { font-size: 14px; }
|
||||
.plan-summary h4 { font-size: 13px; }
|
||||
.plan-summary p { margin: 8px 0; letter-spacing: 0.5px; }
|
||||
.plan-summary p { margin: 8px 0; }
|
||||
.plan-summary ul, .plan-summary ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 0;
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
/**
|
||||
* 问题处理脚本模块
|
||||
* 功能:用户问题交互逻辑
|
||||
* 依赖:textFormatter
|
||||
* 使用场景:webview 中的问题回答处理
|
||||
*/
|
||||
|
||||
export function getQuestionHandlerScript(): string {
|
||||
return `
|
||||
const answeredQuestions = new Map();
|
||||
|
||||
function handleQuestionAnswer(askId, answer, questionDiv) {
|
||||
console.log('[WebView] 用户选择答案:', askId, answer);
|
||||
questionDiv.classList.add('answered');
|
||||
const options = questionDiv.querySelectorAll('.question-option');
|
||||
options.forEach(opt => {
|
||||
if (opt.textContent === answer) {
|
||||
opt.classList.add('selected');
|
||||
}
|
||||
});
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
selected: [answer],
|
||||
customInput: answer
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuestionAnswerInSegment(askId, answer, segmentDiv) {
|
||||
console.log('[WebView] 段落中用户选择答案:', askId, answer);
|
||||
answeredQuestions.set(askId, answer);
|
||||
segmentDiv.classList.add('answered');
|
||||
const options = segmentDiv.querySelectorAll('.question-option');
|
||||
options.forEach(opt => {
|
||||
if (opt.getAttribute('data-option') === answer) {
|
||||
opt.classList.add('selected');
|
||||
}
|
||||
});
|
||||
const customContainer = segmentDiv.querySelector('.custom-input-container');
|
||||
if (customContainer) {
|
||||
customContainer.style.display = 'none';
|
||||
}
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
selected: [answer],
|
||||
customInput: answer
|
||||
});
|
||||
}
|
||||
|
||||
function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
|
||||
console.log('[WebView] 多问题答案提交:', askId, answers);
|
||||
answeredQuestions.set(askId, answers);
|
||||
segmentDiv.classList.add('answered');
|
||||
const inputs = segmentDiv.querySelectorAll('input');
|
||||
inputs.forEach(input => {
|
||||
input.disabled = true;
|
||||
if (input.checked) {
|
||||
const label = input.closest('.question-option');
|
||||
if (label) {
|
||||
label.classList.add('selected');
|
||||
}
|
||||
}
|
||||
});
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
if (submitBtn) {
|
||||
submitBtn.style.display = 'none';
|
||||
}
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
answers: answers
|
||||
});
|
||||
}
|
||||
|
||||
function showQuestion(askId, question, options) {
|
||||
console.log('[WebView] showQuestion 被调用:', askId, question, options);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message question-message';
|
||||
div.setAttribute('data-ask-id', askId);
|
||||
const questionText = document.createElement('div');
|
||||
questionText.className = 'question-text';
|
||||
questionText.textContent = question;
|
||||
div.appendChild(questionText);
|
||||
const optionsContainer = document.createElement('div');
|
||||
optionsContainer.className = 'question-options';
|
||||
options.forEach((option, index) => {
|
||||
const optionBtn = document.createElement('button');
|
||||
optionBtn.className = 'question-option';
|
||||
optionBtn.textContent = option;
|
||||
optionBtn.onclick = () => handleQuestionAnswer(askId, option, div);
|
||||
optionsContainer.appendChild(optionBtn);
|
||||
});
|
||||
div.appendChild(optionsContainer);
|
||||
const customContainer = document.createElement('div');
|
||||
customContainer.className = 'custom-input-container';
|
||||
const customInput = document.createElement('input');
|
||||
customInput.type = 'text';
|
||||
customInput.className = 'custom-input';
|
||||
customInput.placeholder = '输入其他答案...';
|
||||
const customSubmit = document.createElement('button');
|
||||
customSubmit.className = 'custom-submit';
|
||||
customSubmit.textContent = '提交';
|
||||
customSubmit.onclick = () => {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswer(askId, customValue, div);
|
||||
}
|
||||
};
|
||||
customContainer.appendChild(customInput);
|
||||
customContainer.appendChild(customSubmit);
|
||||
div.appendChild(customContainer);
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -1,272 +0,0 @@
|
||||
/**
|
||||
* 分段消息渲染脚本模块
|
||||
* 功能:实时更新分段消息、工具调用展示
|
||||
* 依赖:toolHelpers, textFormatter, waveformPreviewContent
|
||||
* 使用场景:webview 中的分段消息渲染
|
||||
*/
|
||||
|
||||
export function getSegmentRendererScript(): string {
|
||||
return `
|
||||
function updateSegmentsRealtime(segments, isComplete) {
|
||||
if (!isComplete && (!segments || segments.length === 0)) return;
|
||||
|
||||
if (!currentSegmentedMessage) {
|
||||
if (currentStreamingMessage) {
|
||||
currentStreamingMessage.remove();
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||||
toolStatuses.forEach(el => el.remove());
|
||||
const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
|
||||
if (lastSegmented && !lastSegmented.querySelector('.message-actions')) {
|
||||
currentSegmentedMessage = lastSegmented;
|
||||
} else {
|
||||
currentSegmentedMessage = document.createElement('div');
|
||||
currentSegmentedMessage.className = 'message bot-message segmented-message';
|
||||
messagesEl.appendChild(currentSegmentedMessage);
|
||||
}
|
||||
renderedSegmentCount = 0;
|
||||
}
|
||||
|
||||
if (currentSegmentedMessage) {
|
||||
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
|
||||
toolHeaders.forEach((header, idx) => {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
toolCollapseStates.set(idx, isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isComplete) {
|
||||
currentSegmentedMessage.innerHTML = '';
|
||||
}
|
||||
|
||||
const mergedSegments = [];
|
||||
let i = 0;
|
||||
while (i < (segments?.length || 0)) {
|
||||
const segment = segments[i];
|
||||
if (segment.type === 'tool') {
|
||||
let count = 1;
|
||||
while (i + count < segments.length &&
|
||||
segments[i + count].type === 'tool' &&
|
||||
segments[i + count].toolName === segment.toolName) {
|
||||
count++;
|
||||
}
|
||||
mergedSegments.push({ ...segment, toolCount: count });
|
||||
i += count;
|
||||
} else {
|
||||
mergedSegments.push(segment);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
let toolIndex = 0;
|
||||
mergedSegments.forEach((segment, index) => {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||
|
||||
if (segment.type === 'text' && segment.content) {
|
||||
segmentDiv.className += ' segment-text';
|
||||
segmentDiv.innerHTML = formatText(segment.content);
|
||||
} else if (segment.type === 'tool') {
|
||||
if (segment.toolName === 'spawnExplorer') return;
|
||||
segmentDiv.className += ' low-profile';
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
const toolDescription = segment.toolDescription || '';
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
const savedState = toolCollapseStates.get(toolIndex);
|
||||
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
|
||||
const currentToolIndex = toolIndex;
|
||||
toolIndex++;
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
||||
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\${toolDescription ? \`<p class="tool-segment-description">\${toolDescription}</p>\` : ''}
|
||||
\`;
|
||||
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
if (typeof createWaveformPreview === 'function') {
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
if (vcdPaths.length > 0) {
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
});
|
||||
} else {
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/(?:路径\\s*[::]\\s*|已生成[::]\\s*)(.+\\.vcd)/);
|
||||
if (match && match[1]) {
|
||||
vcdPath = match[1].trim();
|
||||
}
|
||||
}
|
||||
if (vcdPath) {
|
||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\\/]/).pop() || 'waveform.vcd';
|
||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('[VCD Preview] createWaveformPreview function not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCollapse) {
|
||||
setTimeout(() => {
|
||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||
if (header && content) {
|
||||
header.addEventListener('click', function() {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||
if (isCollapsed) {
|
||||
header.classList.remove('collapsed');
|
||||
content.classList.remove('collapsed');
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
toolCollapseStates.set(toolIdx, false);
|
||||
} else {
|
||||
header.classList.add('collapsed');
|
||||
content.classList.add('collapsed');
|
||||
content.style.maxHeight = '0';
|
||||
toolCollapseStates.set(toolIdx, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.className += ' segment-question';
|
||||
const questions = segment.questions || (segment.question ? [{
|
||||
question: segment.question,
|
||||
options: segment.options || [],
|
||||
multiSelect: false
|
||||
}] : []);
|
||||
const isAnswered = answeredQuestions.has(segment.askId);
|
||||
const savedAnswers = answeredQuestions.get(segment.askId) || {};
|
||||
if (isAnswered) {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
const questionsHtml = questions.map((q, qIndex) => {
|
||||
const inputType = q.multiSelect ? 'checkbox' : 'radio';
|
||||
const inputName = \`q\${qIndex}\`;
|
||||
const selectedAnswers = savedAnswers[qIndex] || [];
|
||||
let optionsHtml;
|
||||
if (!q.options || q.options.length === 0) {
|
||||
const savedText = selectedAnswers[0] || '';
|
||||
optionsHtml = \`<textarea class="question-text-input" name="\${inputName}" placeholder="请输入您的答案..." style="width:100%;min-height:80px;padding:8px;border:1px solid var(--vscode-input-border);border-radius:4px;background:var(--vscode-input-background);color:var(--vscode-input-foreground);resize:vertical;" \${isAnswered ? 'disabled' : ''}>\${savedText}</textarea>\`;
|
||||
} else {
|
||||
optionsHtml = q.options.map(opt => {
|
||||
const isSelected = selectedAnswers.includes(opt);
|
||||
return \`<label class="question-option\${isSelected ? ' selected' : ''}" style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 5px 5px 0;">
|
||||
<input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
|
||||
<span>\${opt}</span>
|
||||
</label>\`;
|
||||
}).join('');
|
||||
}
|
||||
return \`
|
||||
<div class="question-item" data-question-index="\${qIndex}" style="margin-bottom:12px;">
|
||||
<div class="question-text" style="margin-bottom:8px;">\${formatText(q.question)}</div>
|
||||
<div class="question-options">\${optionsHtml}</div>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
\${questionsHtml}
|
||||
<button class="custom-submit" style="display:\${isAnswered ? 'none' : 'block'};margin-top:8px;padding:8px 16px;background:var(--vscode-button-background);color:var(--vscode-button-foreground);border:none;border-radius:6px;cursor:pointer;">提交答案</button>
|
||||
\`;
|
||||
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const answers = {};
|
||||
questions.forEach((q, qIndex) => {
|
||||
const textarea = segmentDiv.querySelector(\`textarea[name="q\${qIndex}"]\`);
|
||||
if (textarea) {
|
||||
const value = textarea.value.trim();
|
||||
answers[qIndex] = value ? [value] : [];
|
||||
} else {
|
||||
const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
|
||||
answers[qIndex] = Array.from(inputs).map(input => input.value);
|
||||
}
|
||||
});
|
||||
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'agent') {
|
||||
renderAgentCard(segment, segmentDiv);
|
||||
} else if (segment.type === 'plan') {
|
||||
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||||
}
|
||||
|
||||
currentSegmentedMessage.appendChild(segmentDiv);
|
||||
});
|
||||
|
||||
if (isComplete) {
|
||||
currentSegmentedMessage = null;
|
||||
}
|
||||
|
||||
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function renderSegments(segments) {
|
||||
console.log('[WebView] renderSegments 被调用, segments:', segments);
|
||||
if (!segments || segments.length === 0) {
|
||||
console.log('[WebView] segments 为空,跳过渲染');
|
||||
return;
|
||||
}
|
||||
if (currentStreamingMessage) {
|
||||
console.log('[WebView] 移除流式消息');
|
||||
currentStreamingMessage.remove();
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||||
console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
|
||||
toolStatuses.forEach(el => {
|
||||
console.log('[WebView] 移除工具状态消息:', el.className);
|
||||
el.remove();
|
||||
});
|
||||
updateSegmentsRealtime(segments, false);
|
||||
|
||||
// 历史消息渲染完成后添加操作按钮
|
||||
if (currentSegmentedMessage) {
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments.filter(s => s.type === 'text' && s.content).map(s => s.content).join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
currentSegmentedMessage.appendChild(actionsDiv);
|
||||
currentSegmentedMessage = null;
|
||||
}
|
||||
|
||||
smartScrollToBottom();
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -3,11 +3,6 @@ import {
|
||||
getGeneralSettingsComponentStyles,
|
||||
getGeneralSettingsComponentScript,
|
||||
} from "./generalSettingsComponent";
|
||||
import {
|
||||
getRulesSettingsComponentContent,
|
||||
getRulesSettingsComponentStyles,
|
||||
getRulesSettingsComponentScript,
|
||||
} from "./rulesSettingsComponent";
|
||||
|
||||
/**
|
||||
* 获取设置面板的 HTML 内容
|
||||
@ -31,18 +26,13 @@ export function getSettingsComponentContent(): string {
|
||||
<button class="settings-nav-item active" data-tab="general" onclick="switchSettingsTab('general')">
|
||||
通用
|
||||
</button>
|
||||
<button class="settings-nav-item" data-tab="rules" onclick="switchSettingsTab('rules')">
|
||||
规则
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="settings-tab-content active" id="generalSettings">
|
||||
${getGeneralSettingsComponentContent()}
|
||||
</div>
|
||||
<div class="settings-tab-content" id="rulesSettings">
|
||||
${getRulesSettingsComponentContent()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -186,7 +176,6 @@ export function getSettingsComponentStyles(): string {
|
||||
}
|
||||
|
||||
${getGeneralSettingsComponentStyles()}
|
||||
${getRulesSettingsComponentStyles()}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -196,13 +185,14 @@ export function getSettingsComponentStyles(): string {
|
||||
export function getSettingsComponentScript(): string {
|
||||
return `
|
||||
${getGeneralSettingsComponentScript()}
|
||||
${getRulesSettingsComponentScript()}
|
||||
|
||||
// 打开设置面板
|
||||
function openSettingsModal() {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
// 请求加载设置
|
||||
vscode.postMessage({ command: 'loadGeneralSettings' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
/**
|
||||
* 文本格式化模块
|
||||
* 功能:Markdown 文本转 HTML
|
||||
* 依赖:无
|
||||
* 使用场景:消息内容格式化显示
|
||||
*/
|
||||
|
||||
export function formatText(text: string): string {
|
||||
if (!text) return "";
|
||||
|
||||
let html = text;
|
||||
|
||||
const codeBlocks: string[] = [];
|
||||
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||
const language = lang || "plaintext";
|
||||
const escapedCode = code
|
||||
.trim()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`;
|
||||
codeBlocks.push(`<pre><code class="language-${language}">${escapedCode}</code></pre>`);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
const inlineCodes: string[] = [];
|
||||
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
||||
const escapedCode = code.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const placeholder = `___INLINE_CODE_${inlineCodes.length}___`;
|
||||
inlineCodes.push(`<code>${escapedCode}</code>`);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
html = html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
html = html.replace(/^### (.+)$/gm, "<h3>$1</h3>");
|
||||
html = html.replace(/^## (.+)$/gm, "<h2>$1</h2>");
|
||||
html = html.replace(/^# (.+)$/gm, "<h1>$1</h1>");
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/^[\-\*] (.+)$/gm, "<li>$1</li>");
|
||||
html = html.replace(/(<li>.*<\/li>\n?)+/g, "<ul>$&</ul>");
|
||||
html = html.replace(/^\d+\. (.+)$/gm, "<li>$1</li>");
|
||||
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
html = html.replace(/\n/g, "<br>");
|
||||
|
||||
codeBlocks.forEach((block, index) => {
|
||||
html = html.replace(`___CODE_BLOCK_${index}___`, block);
|
||||
});
|
||||
|
||||
inlineCodes.forEach((code, index) => {
|
||||
html = html.replace(`___INLINE_CODE_${index}___`, code);
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
/**
|
||||
* 工具辅助函数模块
|
||||
* 功能:工具图标、名称映射、VCD 路径解析
|
||||
* 依赖:toolIcons
|
||||
* 使用场景:工具调用显示
|
||||
*/
|
||||
|
||||
import {
|
||||
fileWriteIconSvg,
|
||||
fileReadIconSvg,
|
||||
fileDeleteIconSvg,
|
||||
syntaxCheckIconSvg,
|
||||
SearchCode,
|
||||
saveKnowledgeIconSvg,
|
||||
simulationIconSvg,
|
||||
waveformIconSvg,
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
userQuestionIconSvg,
|
||||
updateStageIconSvg,
|
||||
successIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
|
||||
export function getToolIcon(toolName: string): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
file_read: fileReadIconSvg,
|
||||
file_write: fileWriteIconSvg,
|
||||
file_delete: fileDeleteIconSvg,
|
||||
file_list: SearchCode,
|
||||
syntax_check: syntaxCheckIconSvg,
|
||||
simulation: simulationIconSvg,
|
||||
waveform_summary: waveformIconSvg,
|
||||
knowledge_save: saveKnowledgeIconSvg,
|
||||
knowledge_load: knowledgeLoadIconSvg,
|
||||
queryKnowledgeSummary: knowledgeLoadIconSvg,
|
||||
queryRules: knowledgeLoadIconSvg,
|
||||
setModule: fileWriteIconSvg,
|
||||
addSignal: fileWriteIconSvg,
|
||||
addSignalExample: fileWriteIconSvg,
|
||||
validateKnowledgeGraph: syntaxCheckIconSvg,
|
||||
querySignals: SearchCode,
|
||||
addPlan: fileWriteIconSvg,
|
||||
addEdge: fileWriteIconSvg,
|
||||
showPlan: SearchCode,
|
||||
addRule: fileWriteIconSvg,
|
||||
updateNode: fileWriteIconSvg,
|
||||
addStateTransition: stateTransitionIconSvg,
|
||||
askUser: userQuestionIconSvg,
|
||||
updatePhase: updateStageIconSvg,
|
||||
iverilog: successIconSvg,
|
||||
};
|
||||
return iconMap[toolName] || "";
|
||||
}
|
||||
|
||||
export function getToolDisplayName(toolName: string): string {
|
||||
const toolNameMap: Record<string, string> = {
|
||||
file_read: "已完成文件读取",
|
||||
file_write: "已完成文件写入",
|
||||
file_delete: "已完成文件删除",
|
||||
file_list: "已检索代码文件",
|
||||
syntax_check: "已完成语法检查",
|
||||
simulation: "已完成仿真",
|
||||
waveform_summary: "已完成波形分析",
|
||||
knowledge_save: "已保存知识库",
|
||||
knowledge_load: "已加载知识库",
|
||||
queryKnowledgeSummary: "已查询知识摘要",
|
||||
queryRules: "已查询规则",
|
||||
setModule: "已设置模块",
|
||||
addSignal: "信号分析完成",
|
||||
addSignalExample: "信号示例处理完成",
|
||||
validateKnowledgeGraph: "已验证知识图谱",
|
||||
querySignals: "已查询信号",
|
||||
addPlan: "已添加计划",
|
||||
addEdge: "已添加边",
|
||||
showPlan: "已显示计划",
|
||||
addRule: "已添加规则",
|
||||
updateNode: "已更新节点",
|
||||
addStateTransition: "已添加状态转换",
|
||||
spawnExplorer: "代码探索",
|
||||
spawnDebugger: "波形调试",
|
||||
askUser: "用户提问",
|
||||
updatePhase: "已更新阶段",
|
||||
iverilog: "已完成编译",
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
export function parseMultiVcdPaths(toolResult: string): Array<{ name: string; path: string }> {
|
||||
if (!toolResult) return [];
|
||||
const result = String(toolResult);
|
||||
|
||||
const vcdListMatch = result.match(/VCD 文件列表:[\s\S]*?(?=\n\n|$)/);
|
||||
if (!vcdListMatch) return [];
|
||||
|
||||
const paths: Array<{ name: string; path: string }> = [];
|
||||
const lineRegex = /- (\w+): ([^\n]+)/g;
|
||||
let match;
|
||||
while ((match = lineRegex.exec(vcdListMatch[0])) !== null) {
|
||||
const name = match[1];
|
||||
const pathOrError = match[2].trim();
|
||||
if (!pathOrError.startsWith("失败")) {
|
||||
paths.push({ name: name + ".vcd", path: pathOrError });
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
@ -9,57 +9,7 @@
|
||||
*/
|
||||
export function getUserInfoComponentContent(): string {
|
||||
return `
|
||||
<div class="user-info-wrapper">
|
||||
<!-- 用户详情下拉面板 -->
|
||||
<div class="user-detail-dropdown" id="userDetailDropdown">
|
||||
<div class="user-detail-content">
|
||||
<div class="user-detail-header">
|
||||
<div class="user-info-row">
|
||||
<div class="user-avatar-small clickable" id="userAvatarClickable">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-name-tier">
|
||||
<div class="user-detail-name clickable" id="userDetailName">加载中...</div>
|
||||
<img class="tier-icon-inline" id="tierIconInline" style="display: none;" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 升级到Pro按钮 (仅BASIC会员显示) -->
|
||||
<!-- <div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
|
||||
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="user-detail-body">
|
||||
<!-- <div class="user-detail-item">
|
||||
<span class="detail-label">剩余 Credits</span>
|
||||
<span class="detail-value" id="creditsDetail">-</span>
|
||||
</div> -->
|
||||
<div class="user-detail-item logout-item" id="logoutItem">
|
||||
<span class="detail-label">账户管理</span>
|
||||
<span class="detail-value logout-link">退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 退出登录确认对话框 -->
|
||||
<div class="logout-confirm-modal" id="logoutConfirmModal">
|
||||
<div class="logout-confirm-overlay"></div>
|
||||
<div class="logout-confirm-content">
|
||||
<div class="logout-confirm-header">
|
||||
<h3>确认退出</h3>
|
||||
</div>
|
||||
<div class="logout-confirm-body">
|
||||
<p>确定要退出登录吗?</p>
|
||||
</div>
|
||||
<div class="logout-confirm-footer">
|
||||
<button class="logout-confirm-btn logout-cancel-btn" id="logoutCancelBtn">取消</button>
|
||||
<button class="logout-confirm-btn logout-ok-btn" id="logoutOkBtn">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info-wrapper" style="display: none;">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -18,6 +18,11 @@ import {
|
||||
getMessageAreaScript,
|
||||
} from "./messageArea";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
import {
|
||||
getMoreOptionsComponentContent,
|
||||
getMoreOptionsComponentStyles,
|
||||
getMoreOptionsComponentScript,
|
||||
} from "./moreOptionsComponent";
|
||||
import {
|
||||
getProgressBarContent,
|
||||
getProgressBarStyles,
|
||||
@ -25,7 +30,6 @@ import {
|
||||
} from "./progressBar";
|
||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||
import { getCurrentEnv } from "../config/settings";
|
||||
import { taskCompleteIconSvg } from "../constants/toolIcons";
|
||||
import {
|
||||
getInvitationModalContent,
|
||||
getInvitationModalStyles,
|
||||
@ -111,6 +115,7 @@ export function getWebviewContent(
|
||||
}
|
||||
${getMessageAreaStyles()}
|
||||
${getAgentCardStyles()}
|
||||
${getMoreOptionsComponentStyles()}
|
||||
${getWaveformPreviewContent()}
|
||||
${getConversationHistoryBarStyles()}
|
||||
${getProgressBarStyles()}
|
||||
@ -305,9 +310,7 @@ export function getWebviewContent(
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
font-size:0.9rem;
|
||||
color: var(--vscode-foreground);
|
||||
letter-spacing: 0.5px;
|
||||
font-size:0.9rem
|
||||
}
|
||||
.segment-tool {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
@ -507,13 +510,17 @@ export function getWebviewContent(
|
||||
${getNdtWelcomeModalContent(logoUri)}
|
||||
${getExpiredModalContent(logoUri)}
|
||||
<div class="header">
|
||||
<div style="display: flex; align-items: center; justify-content: center;">
|
||||
<div style="display: flex; align-items: flex-end; justify-content: center">
|
||||
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
|
||||
<span style="font-size: 23px; font-weight: bold; background: linear-gradient(to bottom, #b2e4ff, #42bcff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin: 0 0 14px -16px;">企业版</span>
|
||||
</div>
|
||||
<p style="font-size: 16px; margin-top: 12px; line-height: 1.5;">
|
||||
The <span style="background: linear-gradient(to right, #42bcff, #4A9EFF); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold;">Agentic AI</span> Verilog Coding Platform,
|
||||
<span style="display: block; margin-top: 8px;">将芯片设计与验证的效率提升至少20倍!</span>
|
||||
<p style="font-size: 16px; margin-top: 8px; line-height: 1.5;">
|
||||
The <span style="background: linear-gradient(to right, #42bcff, #4A9EFF); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold;">Agentic AI</span> Verilog Coding Platform
|
||||
<span style="display: block; margin-top: 8px;">将FPGA研发效率提升至少20倍!</span>
|
||||
</p>
|
||||
<div style="margin-top: 16px; padding: 8px 20px; background: linear-gradient(135deg, rgba(255, 215, 0, 0.15), rgba(255, 165, 0, 0.15)); border: 1px solid rgba(255, 215, 0, 0.3); border-radius: 6px;">
|
||||
<p style="font-size: 13px; margin: 0; background: linear-gradient(135deg, #FFD700, #FFA500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 600; letter-spacing: 1px;">宁德时代专属定制版</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
@ -548,9 +555,6 @@ export function getWebviewContent(
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
const messagesEl = document.getElementById('messages');
|
||||
|
||||
// 图标常量
|
||||
const taskCompleteIconSvg = ${JSON.stringify(taskCompleteIconSvg)};
|
||||
|
||||
// 全局变量
|
||||
let currentStreamingMessage = null;
|
||||
let loadingIndicator = null;
|
||||
@ -775,44 +779,10 @@ export function getWebviewContent(
|
||||
// 隐藏加载指示器
|
||||
hideLoadingIndicator();
|
||||
break;
|
||||
|
||||
case 'taskComplete':
|
||||
// 显示任务完成提示
|
||||
const taskDiv = document.createElement('div');
|
||||
taskDiv.className = 'message bot-message';
|
||||
const taskActionsDiv = document.createElement('div');
|
||||
taskActionsDiv.className = 'message-actions';
|
||||
const taskMessageContent = document.createElement('span');
|
||||
taskMessageContent.innerHTML = taskCompleteIconSvg + ' 任务完成';
|
||||
const taskCopyBtn = document.createElement('button');
|
||||
taskCopyBtn.className = 'action-btn';
|
||||
taskCopyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
taskCopyBtn.onclick = () => {
|
||||
// 获取前一个 AI 消息的内容
|
||||
const prevMessage = taskDiv.previousElementSibling;
|
||||
if (prevMessage && prevMessage.classList.contains('bot-message')) {
|
||||
const textContent = prevMessage.textContent || '';
|
||||
copyMessage(textContent, taskCopyBtn);
|
||||
}
|
||||
};
|
||||
const taskLikeBtn = document.createElement('button');
|
||||
taskLikeBtn.className = 'action-btn';
|
||||
taskLikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
taskLikeBtn.onclick = () => toggleLike(taskLikeBtn);
|
||||
const taskDislikeBtn = document.createElement('button');
|
||||
taskDislikeBtn.className = 'action-btn';
|
||||
taskDislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
taskDislikeBtn.onclick = () => toggleDislike(taskDislikeBtn);
|
||||
taskActionsDiv.appendChild(taskMessageContent);
|
||||
taskActionsDiv.appendChild(taskCopyBtn);
|
||||
taskActionsDiv.appendChild(taskLikeBtn);
|
||||
taskActionsDiv.appendChild(taskDislikeBtn);
|
||||
taskDiv.appendChild(taskActionsDiv);
|
||||
messagesEl.appendChild(taskDiv);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
break;
|
||||
|
||||
case 'taskCompleteHistory':
|
||||
// 历史记录不显示任务完成提示
|
||||
addMessage('✅ 任务已完成', 'bot');
|
||||
break;
|
||||
|
||||
case 'workspaceStatus':
|
||||
@ -964,6 +934,13 @@ export function getWebviewContent(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'loadedGeneralSettings':
|
||||
// 加载通用设置
|
||||
if (typeof loadGeneralSettings === 'function') {
|
||||
loadGeneralSettings(message.settings);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||
}
|
||||
@ -971,6 +948,7 @@ export function getWebviewContent(
|
||||
|
||||
${getMessageAreaScript()}
|
||||
${getAgentCardScript()}
|
||||
${getMoreOptionsComponentScript()}
|
||||
${getWaveformPreviewScript()}
|
||||
${getConversationHistoryBarScript()}
|
||||
${getProgressBarScript()}
|
||||
|
||||