Compare commits
12 Commits
feat/eda
...
b9dc631bf7
| Author | SHA1 | Date | |
|---|---|---|---|
| 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;
|
serviceTier: ServiceTier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 自定义配置缓存 */
|
||||||
|
let customConfig: Partial<IccoderConfig> | null = null;
|
||||||
|
|
||||||
/** 环境配置 */
|
/** 环境配置 */
|
||||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||||
/** 本地开发环境 - 通过 Gateway 路由 */
|
/** 本地开发环境 - 通过 Gateway 路由 */
|
||||||
@ -38,7 +41,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
loginUrl: "http://localhost/login",
|
loginUrl: "http://localhost/login",
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max", // 默认使用 max
|
serviceTier: "max",
|
||||||
},
|
},
|
||||||
/** 测试服务器环境 - 通过 Gateway 路由 */
|
/** 测试服务器环境 - 通过 Gateway 路由 */
|
||||||
test: {
|
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 {
|
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
|
* 成功的图标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>`;
|
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>`;
|
|
||||||
|
|||||||
@ -2,19 +2,27 @@ import * as vscode from "vscode";
|
|||||||
import { ICViewProvider } from "./views/ICViewProvider";
|
import { ICViewProvider } from "./views/ICViewProvider";
|
||||||
import { showICHelperPanel } from "./panels/ICHelperPanel";
|
import { showICHelperPanel } from "./panels/ICHelperPanel";
|
||||||
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
|
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
|
||||||
|
import { UserManualPanel } from "./panels/UserManualPanel";
|
||||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||||
import { VCDFileServer } from "./services/vcdFileServer";
|
import { VCDFileServer } from "./services/vcdFileServer";
|
||||||
import { initUserService } from "./services/userService";
|
|
||||||
import { initCreditsService } from "./services/creditsService";
|
|
||||||
import { isTokenExpired } from "./utils/jwtUtils";
|
import { isTokenExpired } from "./utils/jwtUtils";
|
||||||
import { NotificationService } from "./services/notificationService";
|
import { NotificationService } from "./services/notificationService";
|
||||||
import { InvitationService } from "./services/invitationService";
|
import { InvitationService } from "./services/invitationService";
|
||||||
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
|
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
|
||||||
|
import { setCustomConfig } from "./config/settings";
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
console.log("🎉 IC Coder 插件已激活!");
|
console.log("🎉 IC Coder 插件已激活!");
|
||||||
|
|
||||||
|
// 加载保存的配置
|
||||||
|
const savedSettings = context.globalState.get('generalSettings') as any;
|
||||||
|
if (savedSettings?.backendUrl) {
|
||||||
|
setCustomConfig({
|
||||||
|
backendUrl: savedSettings.backendUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 创建装饰类型(代码旁边的提示)
|
// 创建装饰类型(代码旁边的提示)
|
||||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||||
after: {
|
after: {
|
||||||
@ -31,11 +39,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
|
||||||
if (!editor.selection.isEmpty) {
|
if (!editor.selection.isEmpty) {
|
||||||
// 找到选区末尾所在的行,并将提示放在该行的末尾
|
const range = new vscode.Range(editor.selection.end, editor.selection.end);
|
||||||
const { anchor, active } = editor.selection;
|
|
||||||
const endPos = anchor.isAfter(active) ? anchor : active;
|
|
||||||
const lineEndPos = editor.document.lineAt(endPos.line).range.end;
|
|
||||||
const range = new vscode.Range(lineEndPos, lineEndPos);
|
|
||||||
const decoration = { range };
|
const decoration = { range };
|
||||||
editor.setDecorations(decorationType, [decoration]);
|
editor.setDecorations(decorationType, [decoration]);
|
||||||
} else {
|
} else {
|
||||||
@ -54,33 +58,26 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
const notificationService = NotificationService.getInstance(context);
|
const notificationService = NotificationService.getInstance(context);
|
||||||
console.log('[Extension] 通知服务已初始化');
|
console.log('[Extension] 通知服务已初始化');
|
||||||
|
|
||||||
// 【关键】在创建 AuthProvider 之前,先检查并清除过期的 session
|
// 【已禁用】登录和 token 验证 - 无需登录即可使用
|
||||||
const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
|
// const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
|
||||||
console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
|
// console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
|
||||||
|
//
|
||||||
if (storedSessions.length > 0) {
|
// if (storedSessions.length > 0) {
|
||||||
const session = storedSessions[0];
|
// const session = storedSessions[0];
|
||||||
const token = session.accessToken;
|
// const token = session.accessToken;
|
||||||
console.log('[Extension] 检查 token 是否过期...');
|
// console.log('[Extension] 检查 token 是否过期...');
|
||||||
|
//
|
||||||
if (token) {
|
// if (token) {
|
||||||
const expired = isTokenExpired(token);
|
// const expired = isTokenExpired(token);
|
||||||
console.log('[Extension] token 过期检查结果:', expired);
|
// console.log('[Extension] token 过期检查结果:', expired);
|
||||||
|
//
|
||||||
if (expired) {
|
// if (expired) {
|
||||||
// 必须等待清除完成后再创建 AuthProvider
|
// await context.globalState.update('icCoderSessions', []);
|
||||||
await context.globalState.update('icCoderSessions', []);
|
// await context.globalState.update('icCoderUserInfo', undefined);
|
||||||
await context.globalState.update('icCoderUserInfo', undefined);
|
// console.log('[Extension] Token 已过期,已清除所有登录状态');
|
||||||
console.log('[Extension] Token 已过期,已清除所有登录状态');
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化用户服务
|
|
||||||
initUserService(context);
|
|
||||||
|
|
||||||
// 初始化 Credits 服务
|
|
||||||
initCreditsService(context);
|
|
||||||
|
|
||||||
// 初始化 VCD 文件服务器
|
// 初始化 VCD 文件服务器
|
||||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||||
@ -95,25 +92,18 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
dispose: () => vcdFileServer.stop()
|
dispose: () => vcdFileServer.stop()
|
||||||
});
|
});
|
||||||
|
|
||||||
// 注册 Authentication Provider(此时 icCoderSessions 已经被清除)
|
// 【已禁用】Authentication Provider 注册 - 无需登录
|
||||||
const authProvider = new ICCoderAuthenticationProvider(context);
|
const authProvider = new ICCoderAuthenticationProvider(context);
|
||||||
context.subscriptions.push(
|
// context.subscriptions.push(
|
||||||
vscode.authentication.registerAuthenticationProvider(
|
// vscode.authentication.registerAuthenticationProvider(
|
||||||
"iccoder",
|
// "iccoder",
|
||||||
"IC Coder",
|
// "IC Coder",
|
||||||
authProvider
|
// 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(
|
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(
|
const loginCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.login",
|
"ic-coder.login",
|
||||||
@ -436,6 +434,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
openChatCommand,
|
openChatCommand,
|
||||||
openVCDViewerCommand,
|
openVCDViewerCommand,
|
||||||
openVCDViewerInBrowserCommand,
|
openVCDViewerInBrowserCommand,
|
||||||
|
openUserManualCommand,
|
||||||
loginCommand,
|
loginCommand,
|
||||||
logoutCommand,
|
logoutCommand,
|
||||||
changeInvitationCodeCommand,
|
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 { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
|
||||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||||
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
|
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
|
||||||
import { updateCachedBalance } from "./creditsService";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息段落类型
|
* 消息段落类型
|
||||||
@ -129,6 +128,8 @@ export class DialogSession {
|
|||||||
private currentTextSegment: MessageSegment | null = null;
|
private currentTextSegment: MessageSegment | null = null;
|
||||||
private completeCallback: ((segments: MessageSegment[]) => void) | null =
|
private completeCallback: ((segments: MessageSegment[]) => void) | null =
|
||||||
null; // 保存完成回调,用于 abort 时触发
|
null; // 保存完成回调,用于 abort 时触发
|
||||||
|
private consecutiveToolErrors = 0; // 连续工具错误计数
|
||||||
|
private readonly MAX_CONSECUTIVE_ERRORS = 5; // 最大连续错误次数
|
||||||
|
|
||||||
constructor(extensionPath: string, existingTaskId?: string) {
|
constructor(extensionPath: string, existingTaskId?: string) {
|
||||||
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
@ -444,6 +445,7 @@ export class DialogSession {
|
|||||||
const expired = isTokenExpired(session.accessToken);
|
const expired = isTokenExpired(session.accessToken);
|
||||||
if (expired === true) {
|
if (expired === true) {
|
||||||
console.error("[DialogSession] token 已过期,需要重新登录");
|
console.error("[DialogSession] token 已过期,需要重新登录");
|
||||||
|
/*
|
||||||
vscode.window
|
vscode.window
|
||||||
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
@ -453,6 +455,7 @@ export class DialogSession {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
throw new Error("登录已过期,请重新登录");
|
throw new Error("登录已过期,请重新登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -899,6 +902,7 @@ export class DialogSession {
|
|||||||
data.message.includes("LOGIN_EXPIRED") ||
|
data.message.includes("LOGIN_EXPIRED") ||
|
||||||
data.message.includes("登录状态已过期")
|
data.message.includes("登录状态已过期")
|
||||||
) {
|
) {
|
||||||
|
/*
|
||||||
vscode.window
|
vscode.window
|
||||||
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
@ -908,6 +912,7 @@ export class DialogSession {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
// 登录过期错误已处理,不再传递给外部
|
// 登录过期错误已处理,不再传递给外部
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1017,8 +1022,9 @@ export class DialogSession {
|
|||||||
data.remainingCredits
|
data.remainingCredits
|
||||||
);
|
);
|
||||||
// 更新余额缓存
|
// 更新余额缓存
|
||||||
updateCachedBalance(data.remainingCredits);
|
// updateCachedBalance(data.remainingCredits);
|
||||||
// 资源点余额低于阈值时弹窗提醒
|
// 资源点余额低于阈值时弹窗提醒
|
||||||
|
/*
|
||||||
const LOW_CREDIT_THRESHOLD = 5;
|
const LOW_CREDIT_THRESHOLD = 5;
|
||||||
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
|
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
|
||||||
vscode.window
|
vscode.window
|
||||||
@ -1030,13 +1036,13 @@ export class DialogSession {
|
|||||||
)
|
)
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
if (selection === "去充值") {
|
if (selection === "去充值") {
|
||||||
// 打开充值页面
|
|
||||||
vscode.env.openExternal(
|
vscode.env.openExternal(
|
||||||
vscode.Uri.parse("https://iccoder.com/recharge")
|
vscode.Uri.parse("https://iccoder.com/memberCenter")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
|
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import * as vscode from "vscode";
|
|||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
|
||||||
import { getConfig } from "../config/settings";
|
import { getConfig } from "../config/settings";
|
||||||
import { resetInvitationVerification } from "./apiClient";
|
import { resetInvitationVerification } from "./apiClient";
|
||||||
|
|
||||||
@ -85,7 +84,7 @@ export class ICCoderAuthenticationProvider
|
|||||||
const oldSession = this._sessions[0];
|
const oldSession = this._sessions[0];
|
||||||
this._sessions = [];
|
this._sessions = [];
|
||||||
await this.saveSessions();
|
await this.saveSessions();
|
||||||
await clearUserInfo();
|
// await clearUserInfo();
|
||||||
this._onDidChangeSessions.fire({
|
this._onDidChangeSessions.fire({
|
||||||
added: [],
|
added: [],
|
||||||
removed: [oldSession],
|
removed: [oldSession],
|
||||||
@ -97,15 +96,15 @@ export class ICCoderAuthenticationProvider
|
|||||||
const token = await this.login();
|
const token = await this.login();
|
||||||
|
|
||||||
// 获取到 token 后立即调用用户信息接口
|
// 获取到 token 后立即调用用户信息接口
|
||||||
const userInfo = await onTokenReceived(token);
|
// const userInfo = await onTokenReceived(token);
|
||||||
|
|
||||||
// 创建会话
|
// 创建会话
|
||||||
const session: vscode.AuthenticationSession = {
|
const session: vscode.AuthenticationSession = {
|
||||||
id: this.generateSessionId(),
|
id: this.generateSessionId(),
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
account: {
|
account: {
|
||||||
id: userInfo?.userId || "iccoder-user",
|
id: "user",
|
||||||
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
|
label: "IC Coder User",
|
||||||
},
|
},
|
||||||
scopes: [...scopes],
|
scopes: [...scopes],
|
||||||
};
|
};
|
||||||
@ -158,7 +157,7 @@ export class ICCoderAuthenticationProvider
|
|||||||
await this.saveSessions();
|
await this.saveSessions();
|
||||||
|
|
||||||
// 3. 清除用户信息缓存
|
// 3. 清除用户信息缓存
|
||||||
await clearUserInfo();
|
// await clearUserInfo();
|
||||||
|
|
||||||
// 4. 触发会话变化事件
|
// 4. 触发会话变化事件
|
||||||
this._onDidChangeSessions.fire({
|
this._onDidChangeSessions.fire({
|
||||||
@ -182,14 +181,14 @@ export class ICCoderAuthenticationProvider
|
|||||||
*/
|
*/
|
||||||
async clearSessionsForRelogin(): Promise<void> {
|
async clearSessionsForRelogin(): Promise<void> {
|
||||||
if (this._sessions.length === 0) {
|
if (this._sessions.length === 0) {
|
||||||
await clearUserInfo();
|
// await clearUserInfo();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removed = [...this._sessions];
|
const removed = [...this._sessions];
|
||||||
this._sessions = [];
|
this._sessions = [];
|
||||||
await this.saveSessions();
|
await this.saveSessions();
|
||||||
await clearUserInfo();
|
// await clearUserInfo();
|
||||||
|
|
||||||
this._onDidChangeSessions.fire({
|
this._onDidChangeSessions.fire({
|
||||||
added: [],
|
added: [],
|
||||||
|
|||||||
@ -161,7 +161,16 @@ export async function startStreamDialog(
|
|||||||
|
|
||||||
const body = JSON.stringify(request);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options: http.RequestOptions = {
|
const options: http.RequestOptions = {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import * as vscode from "vscode";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { readFileContent, readDirectory } from "../utils/readFiles";
|
import { readFileContent, readDirectory, listDirectory } from "../utils/readFiles";
|
||||||
import { createOrOverwriteFile } from "../utils/createFiles";
|
import { createOrOverwriteFile } from "../utils/createFiles";
|
||||||
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
|
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
|
||||||
import { changeTracker } from "./changeTracker";
|
import { changeTracker } from "./changeTracker";
|
||||||
@ -126,8 +126,9 @@ export async function executeToolCall(
|
|||||||
|
|
||||||
// 提交成功结果
|
// 提交成功结果
|
||||||
const result = createSuccessResult(callId, resultText);
|
const result = createSuccessResult(callId, resultText);
|
||||||
|
console.log(`[ToolExecutor] 准备提交结果: ${toolName}, callId=${callId}`);
|
||||||
await submitToolResult(result);
|
await submitToolResult(result);
|
||||||
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
|
console.log(`[ToolExecutor] 结果提交成功: ${toolName}, callId=${callId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||||
console.error(
|
console.error(
|
||||||
@ -136,8 +137,20 @@ export async function executeToolCall(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 提交错误结果
|
// 提交错误结果
|
||||||
|
try {
|
||||||
const result = createBusinessErrorResult(callId, errorMessage);
|
const result = createBusinessErrorResult(callId, errorMessage);
|
||||||
|
console.log(`[ToolExecutor] 准备提交错误结果: ${toolName}, callId=${callId}`);
|
||||||
await submitToolResult(result);
|
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 dirPath = args.path || ".";
|
||||||
const extensions = args.extension ? [args.extension] : undefined;
|
const extensions = args.extension ? [args.extension] : undefined;
|
||||||
|
|
||||||
const files = await readDirectory(dirPath, extensions);
|
const files = await listDirectory(dirPath, extensions);
|
||||||
const fileList = files.map((f) => f.path).join("\n");
|
return files.join("\n") || "(目录为空)";
|
||||||
|
|
||||||
return fileList || "(目录为空)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -335,43 +335,21 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
|||||||
// 保存到持久化存储
|
// 保存到持久化存储
|
||||||
await saveUserInfo(userInfo);
|
await saveUserInfo(userInfo);
|
||||||
|
|
||||||
// 判断是否是插件试用用户
|
// 【已禁用】试用用户和欢迎弹窗逻辑 - 无需登录
|
||||||
console.log('[UserService] 检查用户类型,isPluginTrial:', userInfo.isPluginTrial);
|
// if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
|
||||||
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
|
// const now = Date.now();
|
||||||
|
// const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||||
if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
|
// if (isExpired) {
|
||||||
// 检查是否过期
|
// console.log('[UserService] 试用已过期,将显示邀请码弹窗');
|
||||||
const now = Date.now();
|
// } else {
|
||||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
// const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
|
||||||
console.log('[UserService] 试用到期时间:', new Date(userInfo.pluginTrialExpiresAt).toLocaleString());
|
// if (!hasWelcomed && extensionContext) {
|
||||||
console.log('[UserService] 当前时间:', new Date(now).toLocaleString());
|
// await extensionContext.globalState.update('showWelcomeModal', true);
|
||||||
console.log('[UserService] 是否过期:', isExpired);
|
// await extensionContext.globalState.update('pluginTrialWelcomed', true);
|
||||||
|
// console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
|
||||||
if (isExpired) {
|
// }
|
||||||
// 已过期:显示邀请码弹窗
|
// }
|
||||||
console.log('[UserService] 试用已过期,将显示邀请码弹窗');
|
// }
|
||||||
} else {
|
|
||||||
// 未过期:显示欢迎弹窗
|
|
||||||
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
|
|
||||||
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
|
|
||||||
|
|
||||||
if (!hasWelcomed && extensionContext) {
|
|
||||||
await extensionContext.globalState.update('showWelcomeModal', true);
|
|
||||||
await extensionContext.globalState.update('pluginTrialWelcomed', true);
|
|
||||||
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
|
|
||||||
|
|
||||||
const checkMark = extensionContext.globalState.get('showWelcomeModal');
|
|
||||||
console.log('[UserService] 验证标记:', checkMark);
|
|
||||||
} else if (!extensionContext) {
|
|
||||||
console.error('[UserService] ❌ extensionContext 为 null,无法设置标记');
|
|
||||||
} else {
|
|
||||||
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// isPluginTrial=false 或 enterpriseTrialExpires 为 null:显示邀请码弹窗
|
|
||||||
console.log('[UserService] 非试用用户或无过期时间,将显示邀请码弹窗');
|
|
||||||
}
|
|
||||||
|
|
||||||
return userInfo;
|
return userInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -201,6 +201,33 @@ async function findVerilogFiles(dir: string): Promise<string[]> {
|
|||||||
return verilogFiles;
|
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 可执行文件路径
|
* 获取 iverilog 可执行文件路径
|
||||||
*/
|
*/
|
||||||
@ -304,8 +331,8 @@ export async function generateVCD(
|
|||||||
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
|
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. 构建 iverilog 编译参数
|
// 5. 构建 iverilog 编译参数(启用 SystemVerilog 2012 标准)
|
||||||
const compileArgs = ["-o", outputFile, ...projectCheck.allVerilogFiles];
|
const compileArgs = ["-g2012", "-o", outputFile, ...projectCheck.allVerilogFiles];
|
||||||
|
|
||||||
console.log("执行编译命令:", iverilogPath, compileArgs.join(" "));
|
console.log("执行编译命令:", iverilogPath, compileArgs.join(" "));
|
||||||
console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT);
|
console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT);
|
||||||
@ -317,7 +344,10 @@ export async function generateVCD(
|
|||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
env: env,
|
env: env,
|
||||||
});
|
});
|
||||||
|
console.log("编译成功,stdout:", compileResult.stdout);
|
||||||
|
console.log("编译成功,stderr:", compileResult.stderr);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error("编译失败:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `IC Coder编译器编译失败:\n${error.message}`,
|
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 解析错误)
|
// 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误)
|
||||||
try {
|
try {
|
||||||
const fs = require("fs");
|
|
||||||
const vvpContent = fs.readFileSync(outputFile, "utf8");
|
const vvpContent = fs.readFileSync(outputFile, "utf8");
|
||||||
const lines = vvpContent.split("\n");
|
const lines = vvpContent.split("\n");
|
||||||
|
|
||||||
@ -336,9 +377,62 @@ export async function generateVCD(
|
|||||||
const cleanedContent = lines.slice(1).join("\n");
|
const cleanedContent = lines.slice(1).join("\n");
|
||||||
fs.writeFileSync(outputFile, cleanedContent, "utf8");
|
fs.writeFileSync(outputFile, cleanedContent, "utf8");
|
||||||
console.log("已删除 .vvp 文件的 shebang 行");
|
console.log("已删除 .vvp 文件的 shebang 行");
|
||||||
|
} else {
|
||||||
|
console.log(".vvp 文件无 shebang 行,跳过");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
// 7. 执行仿真生成 VCD
|
||||||
@ -351,7 +445,11 @@ export async function generateVCD(
|
|||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
env: env,
|
env: env,
|
||||||
});
|
});
|
||||||
|
console.log("仿真执行完成");
|
||||||
|
console.log("仿真 stdout:", simResult.stdout);
|
||||||
|
console.log("仿真 stderr:", simResult.stderr);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error("仿真失败:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `VVP 仿真失败:\n${error.message}`,
|
message: `VVP 仿真失败:\n${error.message}`,
|
||||||
@ -361,16 +459,38 @@ export async function generateVCD(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 8. 查找生成的 VCD 文件
|
// 8. 查找生成的 VCD 文件
|
||||||
const projectUri = vscode.Uri.file(projectPath);
|
let vcdFile: string | null = null;
|
||||||
const entries = await vscode.workspace.fs.readDirectory(projectUri);
|
|
||||||
const vcdFiles = entries
|
|
||||||
.filter(
|
|
||||||
([fileName, fileType]) =>
|
|
||||||
fileType === vscode.FileType.File && fileName.endsWith(".vcd"),
|
|
||||||
)
|
|
||||||
.map(([fileName]) => fileName);
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message:
|
||||||
@ -379,9 +499,6 @@ export async function generateVCD(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用找到的第一个 VCD 文件
|
|
||||||
const vcdFile = path.join(projectPath, vcdFiles[0]);
|
|
||||||
|
|
||||||
// 9. 清理中间文件
|
// 9. 清理中间文件
|
||||||
try {
|
try {
|
||||||
const outputUri = vscode.Uri.file(outputFile);
|
const outputUri = vscode.Uri.file(outputFile);
|
||||||
@ -594,8 +711,9 @@ export async function generateMultiVCD(
|
|||||||
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
|
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 编译(带宏定义)
|
// 编译(带宏定义,启用 SystemVerilog 2012 标准)
|
||||||
const compileArgs = [
|
const compileArgs = [
|
||||||
|
"-g2012",
|
||||||
`-D${macroName}`,
|
`-D${macroName}`,
|
||||||
"-o",
|
"-o",
|
||||||
outputFile,
|
outputFile,
|
||||||
|
|||||||
@ -14,16 +14,11 @@ import {
|
|||||||
checkVerilogProject,
|
checkVerilogProject,
|
||||||
checkIverilogAvailable,
|
checkIverilogAvailable,
|
||||||
} from "./iverilogRunner";
|
} from "./iverilogRunner";
|
||||||
import { createVivadoProject, runVivadoSynthesis } from "./vivadoRunner";
|
|
||||||
import { ChatHistoryManager } from "./chatHistoryManager";
|
import { ChatHistoryManager } from "./chatHistoryManager";
|
||||||
import { dialogManager, DialogSession } from "../services/dialogService";
|
import { dialogManager, DialogSession } from "../services/dialogService";
|
||||||
import { userInteractionManager } from "../services/userInteraction";
|
import { userInteractionManager } from "../services/userInteraction";
|
||||||
import { healthCheck } from "../services/apiClient";
|
import { healthCheck } from "../services/apiClient";
|
||||||
import { isTokenExpired } from "./jwtUtils";
|
import { isTokenExpired } from "./jwtUtils";
|
||||||
import {
|
|
||||||
checkBalanceBeforeSend,
|
|
||||||
fetchBalance,
|
|
||||||
} from "../services/creditsService";
|
|
||||||
import { optimizePrompt } from "../services/promptOptimizeService";
|
import { optimizePrompt } from "../services/promptOptimizeService";
|
||||||
import { NotificationService } from "../services/notificationService";
|
import { NotificationService } from "../services/notificationService";
|
||||||
import { TrialExpirationService } from "../services/trialExpirationService";
|
import { TrialExpirationService } from "../services/trialExpirationService";
|
||||||
@ -42,6 +37,9 @@ let currentSession: DialogSession | null = null;
|
|||||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||||
let lastTaskId: string | null = null;
|
let lastTaskId: string | null = null;
|
||||||
|
|
||||||
|
/** 离线模式仿真模拟标志(防止重复触发) */
|
||||||
|
let offlineSimulationTriggered = false;
|
||||||
|
|
||||||
async function trackFileChange(
|
async function trackFileChange(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
oldContent: string,
|
oldContent: string,
|
||||||
@ -67,9 +65,9 @@ export async function handleUserMessage(
|
|||||||
) {
|
) {
|
||||||
console.log("收到用户消息:", text);
|
console.log("收到用户消息:", text);
|
||||||
|
|
||||||
// 检查 token 是否过期
|
// 【已禁用】检查 token 是否过期 - 无需登录
|
||||||
const context = (panel as any).__context;
|
const context = (panel as any).__context;
|
||||||
if (context) {
|
if (false && context) {
|
||||||
// 从 session 中获取 token
|
// 从 session 中获取 token
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
try {
|
try {
|
||||||
@ -113,7 +111,7 @@ export async function handleUserMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTokenExpired(token)) {
|
if (token && isTokenExpired(token as string)) {
|
||||||
console.warn("[MessageHandler] Token 已过期,阻止发送");
|
console.warn("[MessageHandler] Token 已过期,阻止发送");
|
||||||
|
|
||||||
// 保存待发送的消息
|
// 保存待发送的消息
|
||||||
@ -129,6 +127,7 @@ export async function handleUserMessage(
|
|||||||
await context.globalState.update("icCoderUserInfo", undefined);
|
await context.globalState.update("icCoderUserInfo", undefined);
|
||||||
|
|
||||||
// 显示弹窗提示
|
// 显示弹窗提示
|
||||||
|
/*
|
||||||
const action = await vscode.window.showWarningMessage(
|
const action = await vscode.window.showWarningMessage(
|
||||||
"登录已过期,请重新登录",
|
"登录已过期,请重新登录",
|
||||||
"立即登录",
|
"立即登录",
|
||||||
@ -139,6 +138,7 @@ export async function handleUserMessage(
|
|||||||
forceReauth: true,
|
forceReauth: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// 恢复输入状态
|
// 恢复输入状态
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -190,29 +190,6 @@ export async function handleUserMessage(
|
|||||||
return;
|
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) {
|
if (useBackendService && extensionPath) {
|
||||||
try {
|
try {
|
||||||
@ -227,10 +204,10 @@ export async function handleUserMessage(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("当前访问人数过多,请稍后重试:", error);
|
console.error("处理用户消息失败:", error);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "updateStatus",
|
command: "updateStatus",
|
||||||
text: "当前访问人数过多,请稍后重试",
|
text: "处理用户消息失败,请稍后重试",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
// 恢复输入状态
|
// 恢复输入状态
|
||||||
@ -284,6 +261,8 @@ async function handleUserMessageWithBackend(
|
|||||||
);
|
);
|
||||||
// 保存 taskId 用于后续操作(如压缩)
|
// 保存 taskId 用于后续操作(如压缩)
|
||||||
lastTaskId = currentSession.getTaskId();
|
lastTaskId = currentSession.getTaskId();
|
||||||
|
// 重置离线模式仿真标志(新会话开始)
|
||||||
|
offlineSimulationTriggered = false;
|
||||||
console.log(
|
console.log(
|
||||||
"[MessageHandler] 创建会话: taskId=",
|
"[MessageHandler] 创建会话: taskId=",
|
||||||
lastTaskId,
|
lastTaskId,
|
||||||
@ -320,6 +299,45 @@ async function handleUserMessageWithBackend(
|
|||||||
command: "updateSegments",
|
command: "updateSegments",
|
||||||
segments: filteredSegments,
|
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) => {
|
onToolStart: (toolName) => {
|
||||||
@ -369,17 +387,6 @@ async function handleUserMessageWithBackend(
|
|||||||
console.error("[MessageHandler] 保存AI响应历史失败:", error);
|
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 {
|
try {
|
||||||
// 隐藏状态栏
|
// 隐藏状态栏
|
||||||
@ -394,11 +401,6 @@ async function handleUserMessageWithBackend(
|
|||||||
isComplete: true,
|
isComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 发送任务完成消息
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "taskComplete",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 发送系统通知 - AI 响应完成
|
// 发送系统通知 - AI 响应完成
|
||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
notificationService.success(
|
notificationService.success(
|
||||||
@ -428,7 +430,7 @@ async function handleUserMessageWithBackend(
|
|||||||
});
|
});
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "receiveMessage",
|
command: "receiveMessage",
|
||||||
text: `❌ 错误: ${message}`,
|
text: `错误: ${message}`,
|
||||||
});
|
});
|
||||||
// 恢复输入状态
|
// 恢复输入状态
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -1469,38 +1471,3 @@ export async function handleOpenFileDiff(
|
|||||||
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
|
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理 Vivado 工具调用
|
|
||||||
*/
|
|
||||||
export async function handleVivadoToolCall(
|
|
||||||
toolName: string,
|
|
||||||
params: any
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
switch (toolName) {
|
|
||||||
case 'createVivadoProject':
|
|
||||||
return await createVivadoProject(params);
|
|
||||||
|
|
||||||
case 'runVivadoSynthesis':
|
|
||||||
return await runVivadoSynthesis(params);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
command: toolName,
|
|
||||||
executionTime: 0,
|
|
||||||
output: '',
|
|
||||||
error: `未知的工具: ${toolName}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
command: toolName,
|
|
||||||
executionTime: 0,
|
|
||||||
output: '',
|
|
||||||
error: error.message || String(error)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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,
|
abortCurrentDialog,
|
||||||
handleOptimizePrompt,
|
handleOptimizePrompt,
|
||||||
} from "../utils/messageHandler";
|
} from "../utils/messageHandler";
|
||||||
|
import { setCustomConfig } from "../config/settings";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建并显示IC 侧边栏视图
|
* 创建并显示IC 侧边栏视图
|
||||||
@ -124,6 +125,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
case "showWarning":
|
case "showWarning":
|
||||||
vscode.window.showWarningMessage(message.message);
|
vscode.window.showWarningMessage(message.message);
|
||||||
break;
|
break;
|
||||||
|
// 新增:打开用户手册
|
||||||
|
case "openUserManual":
|
||||||
|
vscode.commands.executeCommand("ic-coder.openUserManual");
|
||||||
|
break;
|
||||||
// 新增:处理用户回答
|
// 新增:处理用户回答
|
||||||
case "submitAnswer":
|
case "submitAnswer":
|
||||||
handleUserAnswer(
|
handleUserAnswer(
|
||||||
@ -141,6 +146,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
case "optimizePrompt":
|
case "optimizePrompt":
|
||||||
handleOptimizePrompt(panel, message.prompt);
|
handleOptimizePrompt(panel, message.prompt);
|
||||||
break;
|
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,
|
undefined,
|
||||||
@ -158,52 +178,21 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
private readonly extensionUri: vscode.Uri,
|
private readonly extensionUri: vscode.Uri,
|
||||||
private readonly context: vscode.ExtensionContext
|
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> {
|
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> {
|
private async checkLoginStatus(): Promise<boolean> {
|
||||||
try {
|
return true; // 始终返回已登录状态
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveWebviewView(webviewView: vscode.WebviewView) {
|
resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||||
@ -223,30 +212,8 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
console.log('[ICViewProvider] Webview options 已设置');
|
console.log('[ICViewProvider] Webview options 已设置');
|
||||||
console.log('[ICViewProvider] extensionUri:', this.extensionUri.toString());
|
console.log('[ICViewProvider] extensionUri:', this.extensionUri.toString());
|
||||||
|
|
||||||
// 【关键修复】先设置默认 HTML,避免一直加载
|
// 【已禁用】登录检查 - 直接显示"开始使用"按钮
|
||||||
try {
|
webviewView.webview.html = this.getWebviewContent(webviewView.webview, true);
|
||||||
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.onDidReceiveMessage(
|
webviewView.webview.onDidReceiveMessage(
|
||||||
@ -261,11 +228,28 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
} else if (message.command === "openICCoder") {
|
} else if (message.command === "openICCoder") {
|
||||||
// 打开 IC Coder 官网
|
// 打开 IC Coder 官网
|
||||||
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
||||||
|
} else if (message.command === "openUserManual") {
|
||||||
|
// 打开用户手册
|
||||||
|
vscode.commands.executeCommand("ic-coder.openUserManual");
|
||||||
} else if (message.command === "openExternalUrl") {
|
} else if (message.command === "openExternalUrl") {
|
||||||
// 打开外部链接
|
// 打开外部链接
|
||||||
if (message.url) {
|
if (message.url) {
|
||||||
vscode.env.openExternal(vscode.Uri.parse(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,
|
undefined,
|
||||||
@ -338,7 +322,7 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
<img src="${logoUri}" alt="IC Coder" width="120" />
|
<img src="${logoUri}" alt="IC Coder" width="120" />
|
||||||
<h2>欢迎使用 IC Coder</h2>
|
<h2>欢迎使用 IC Coder</h2>
|
||||||
${isLoggedIn
|
${isLoggedIn
|
||||||
? '<button class="btn" onclick="openChat()">开始创作</button>'
|
? '<button class="btn" onclick="openChat()">开始使用</button>'
|
||||||
: '<button class="btn" onclick="login()">登录账户</button>'
|
: '<button class="btn" onclick="login()">登录账户</button>'
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -47,11 +47,7 @@ export function getConversationHistoryBarContent(): string {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="user-info-container">
|
<div class="user-info-container" style="display: none;">
|
||||||
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
|
|
||||||
${userAvatarIconSvg}
|
|
||||||
</button>
|
|
||||||
${getUserInfoComponentContent()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='setting'>
|
<div class='setting'>
|
||||||
|
|||||||
@ -4,14 +4,7 @@
|
|||||||
export function getExampleShowcaseContent(): string {
|
export function getExampleShowcaseContent(): string {
|
||||||
return `
|
return `
|
||||||
<div class="example-showcase" id="exampleShowcase">
|
<div class="example-showcase" id="exampleShowcase">
|
||||||
<div class="showcase-header">
|
|
||||||
<div class="showcase-title">示例</div>
|
<div class="showcase-title">示例</div>
|
||||||
<button class="refresh-button" onclick="refreshExamples()" title="换一批">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M21.5 2V8M21.5 8H15.5M21.5 8L18 4.5C16.7429 3.24286 15.1767 2.35596 13.4606 1.93597C11.7446 1.51598 9.94736 1.57986 8.26381 2.12059C6.58027 2.66131 5.07831 3.65985 3.91872 4.99987C2.75913 6.33989 1.98648 7.96902 1.68 9.71M2.5 22V16M2.5 16H8.5M2.5 16L6 19.5C7.25714 20.7571 8.82331 21.644 10.5394 22.064C12.2554 22.484 14.0526 22.4201 15.7362 21.8794C17.4197 21.3387 18.9217 20.3401 20.0813 19.0001C21.2409 17.6601 22.0135 16.031 22.32 14.29" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="example-cards">
|
<div class="example-cards">
|
||||||
<div class="example-card" onclick="sendExample(0)">
|
<div class="example-card" onclick="sendExample(0)">
|
||||||
<div class="example-icon">
|
<div class="example-icon">
|
||||||
@ -41,14 +34,6 @@ export function getExampleShowcaseContent(): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="web-link">
|
|
||||||
<a href="https://iccoder.com" target="_blank" class="web-link-button">
|
|
||||||
<span class="link-icon">🌐</span>
|
|
||||||
<span>IC Coder Web端</span>
|
|
||||||
<span class="link-arrow">→</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -69,44 +54,12 @@ export function getExampleShowcaseStyles(): string {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.showcase-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase-title {
|
.showcase-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
}
|
margin-bottom: 12px;
|
||||||
|
text-align: left;
|
||||||
.refresh-button {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--vscode-foreground);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-button:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background: var(--vscode-input-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-button svg {
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-button:active svg {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.example-cards {
|
.example-cards {
|
||||||
@ -204,41 +157,6 @@ export function getExampleShowcaseStyles(): string {
|
|||||||
-webkit-box-orient: vertical;
|
-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 {
|
.link-icon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@ -247,10 +165,6 @@ export function getExampleShowcaseStyles(): string {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.web-link-button:hover .link-arrow {
|
|
||||||
transform: translateX(3px);
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,74 +173,15 @@ export function getExampleShowcaseStyles(): string {
|
|||||||
*/
|
*/
|
||||||
export function getExampleShowcaseScript(): string {
|
export function getExampleShowcaseScript(): string {
|
||||||
return `
|
return `
|
||||||
// 所有可用的示例
|
// 示例文本数组
|
||||||
const allExamples = [
|
const exampleTexts = [
|
||||||
'设计一个算术逻辑单元,完成常见运算',
|
'生成一个SPI控制器',
|
||||||
'实现一个优先编码器,多个输入同时有效时,只输出优先级最高的那个编号',
|
'生成一个GMII接口的以太网UDP通信模块'
|
||||||
'实现一个译码器,把二进制编号转换成 one-hot 输出',
|
|
||||||
'实现一个移位寄存器,完成串行/并行数据移位与装载',
|
|
||||||
'实现一个按键消抖模块,解决机械按键抖动问题',
|
|
||||||
'实现一个跑马灯控制器,控制 LED 形成不同流动效果',
|
|
||||||
'实现一个序列检测器,检测串行输入中是否出现指定比特序列',
|
|
||||||
'实现一个LFSR 伪随机数发生器',
|
|
||||||
'实现一个自动售货机,模拟一个简单售货逻辑',
|
|
||||||
'实现一个交通灯控制器,控制两方向交通灯的切换',
|
|
||||||
'实现一个先进先出的数据缓冲区',
|
|
||||||
'单端口 RAM 读写控制器',
|
|
||||||
'实现一个移位加法乘法器,不用 * 运算符'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 当前显示的示例文本
|
|
||||||
let exampleTexts = ['生成一个SPI控制器', '生成一个GMII接口的以太网UDP通信模块'];
|
|
||||||
|
|
||||||
// 存储待发送的示例索引
|
// 存储待发送的示例索引
|
||||||
let pendingExampleIndex = -1;
|
let pendingExampleIndex = -1;
|
||||||
|
|
||||||
// 节流控制
|
|
||||||
let refreshing = false;
|
|
||||||
|
|
||||||
// 刷新示例
|
|
||||||
function refreshExamples() {
|
|
||||||
if (refreshing) return;
|
|
||||||
refreshing = true;
|
|
||||||
|
|
||||||
const used = new Set();
|
|
||||||
const newExamples = [];
|
|
||||||
while (newExamples.length < 2) {
|
|
||||||
const idx = Math.floor(Math.random() * allExamples.length);
|
|
||||||
if (!used.has(idx)) {
|
|
||||||
used.add(idx);
|
|
||||||
newExamples.push(allExamples[idx]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exampleTexts = newExamples;
|
|
||||||
updateExampleCards();
|
|
||||||
|
|
||||||
setTimeout(() => { refreshing = false; }, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新示例卡片显示
|
|
||||||
function updateExampleCards() {
|
|
||||||
const container = document.querySelector('.example-cards');
|
|
||||||
if (!container) return;
|
|
||||||
container.innerHTML = exampleTexts.map((text, i) => \`
|
|
||||||
<div class="example-card" onclick="sendExample(\${i})">
|
|
||||||
<div class="example-icon">
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M10 9H9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="example-content">
|
|
||||||
<div class="example-title">\${text}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
\`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接发送示例消息
|
// 直接发送示例消息
|
||||||
function sendExample(index) {
|
function sendExample(index) {
|
||||||
// 先检查邀请码验证状态
|
// 先检查邀请码验证状态
|
||||||
|
|||||||
@ -4,75 +4,15 @@
|
|||||||
export function getGeneralSettingsComponentContent(): string {
|
export function getGeneralSettingsComponentContent(): string {
|
||||||
return `
|
return `
|
||||||
<div class="general-settings">
|
<div class="general-settings">
|
||||||
<h3 class="settings-section-title">通用设置</h3>
|
<h3 class="settings-section-title">后端服务配置</h3>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<div class="settings-item-header">
|
<div class="settings-item-header">
|
||||||
<label class="settings-item-label">主题</label>
|
<label class="settings-item-label">后端服务地址</label>
|
||||||
<span class="settings-item-description">选择界面主题</span>
|
<span class="settings-item-description">自定义后端 API 地址</span>
|
||||||
</div>
|
</div>
|
||||||
<select class="settings-select" id="themeSelect">
|
<input type="text" class="settings-input-text" id="backendUrlInput" placeholder="https://iccoder.com">
|
||||||
<option value="auto">跟随系统</option>
|
|
||||||
<option value="light">浅色</option>
|
|
||||||
<option value="dark">深色</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-item">
|
|
||||||
<div class="settings-item-header">
|
|
||||||
<label class="settings-item-label">语言</label>
|
|
||||||
<span class="settings-item-description">选择界面语言</span>
|
|
||||||
</div>
|
|
||||||
<select class="settings-select" id="languageSelect">
|
|
||||||
<option value="zh-CN">简体中文</option>
|
|
||||||
<option value="en-US">English</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-item">
|
|
||||||
<div class="settings-item-header">
|
|
||||||
<label class="settings-item-label">自动保存</label>
|
|
||||||
<span class="settings-item-description">自动保存会话历史</span>
|
|
||||||
</div>
|
|
||||||
<label class="settings-switch">
|
|
||||||
<input type="checkbox" id="autoSaveCheckbox" checked>
|
|
||||||
<span class="settings-switch-slider"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-item">
|
|
||||||
<div class="settings-item-header">
|
|
||||||
<label class="settings-item-label">显示时间戳</label>
|
|
||||||
<span class="settings-item-description">在消息中显示时间戳</span>
|
|
||||||
</div>
|
|
||||||
<label class="settings-switch">
|
|
||||||
<input type="checkbox" id="showTimestampCheckbox">
|
|
||||||
<span class="settings-switch-slider"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-section">
|
|
||||||
<h4 class="settings-subsection-title">编辑器设置</h4>
|
|
||||||
|
|
||||||
<div class="settings-item">
|
|
||||||
<div class="settings-item-header">
|
|
||||||
<label class="settings-item-label">字体大小</label>
|
|
||||||
<span class="settings-item-description">设置编辑器字体大小</span>
|
|
||||||
</div>
|
|
||||||
<input type="number" class="settings-input" id="fontSizeInput" value="14" min="10" max="24">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-item">
|
|
||||||
<div class="settings-item-header">
|
|
||||||
<label class="settings-item-label">代码高亮</label>
|
|
||||||
<span class="settings-item-description">启用代码语法高亮</span>
|
|
||||||
</div>
|
|
||||||
<label class="settings-switch">
|
|
||||||
<input type="checkbox" id="syntaxHighlightCheckbox" checked>
|
|
||||||
<span class="settings-switch-slider"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -176,6 +116,21 @@ export function getGeneralSettingsComponentStyles(): string {
|
|||||||
border-color: var(--vscode-focusBorder);
|
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 {
|
.settings-switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -270,57 +225,37 @@ export function getGeneralSettingsComponentScript(): string {
|
|||||||
// 保存通用设置
|
// 保存通用设置
|
||||||
function saveGeneralSettings() {
|
function saveGeneralSettings() {
|
||||||
const settings = {
|
const settings = {
|
||||||
theme: document.getElementById('themeSelect').value,
|
backendUrl: document.getElementById('backendUrlInput').value,
|
||||||
language: document.getElementById('languageSelect').value,
|
|
||||||
autoSave: document.getElementById('autoSaveCheckbox').checked,
|
|
||||||
showTimestamp: document.getElementById('showTimestampCheckbox').checked,
|
|
||||||
fontSize: document.getElementById('fontSizeInput').value,
|
|
||||||
syntaxHighlight: document.getElementById('syntaxHighlightCheckbox').checked,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送消息到扩展
|
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
command: 'saveGeneralSettings',
|
command: 'saveGeneralSettings',
|
||||||
settings: settings
|
settings: settings
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示保存成功提示
|
|
||||||
console.log('通用设置已保存', settings);
|
console.log('通用设置已保存', settings);
|
||||||
|
closeSettingsModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置通用设置
|
// 重置通用设置
|
||||||
function resetGeneralSettings() {
|
function resetGeneralSettings() {
|
||||||
document.getElementById('themeSelect').value = 'auto';
|
document.getElementById('backendUrlInput').value = '';
|
||||||
document.getElementById('languageSelect').value = 'zh-CN';
|
|
||||||
document.getElementById('autoSaveCheckbox').checked = true;
|
// 清空保存的配置
|
||||||
document.getElementById('showTimestampCheckbox').checked = false;
|
vscode.postMessage({
|
||||||
document.getElementById('fontSizeInput').value = '14';
|
command: 'saveGeneralSettings',
|
||||||
document.getElementById('syntaxHighlightCheckbox').checked = true;
|
settings: { backendUrl: '' }
|
||||||
|
});
|
||||||
|
|
||||||
console.log('通用设置已重置为默认值');
|
console.log('通用设置已重置为默认值');
|
||||||
|
closeSettingsModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载通用设置
|
// 加载通用设置
|
||||||
function loadGeneralSettings(settings) {
|
function loadGeneralSettings(settings) {
|
||||||
if (!settings) return;
|
if (!settings) return;
|
||||||
|
if (settings.backendUrl) {
|
||||||
if (settings.theme) {
|
document.getElementById('backendUrlInput').value = settings.backendUrl;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -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 = "",
|
autoIcon: string = "",
|
||||||
liteIcon: string = "",
|
liteIcon: string = "",
|
||||||
syIcon: string = "",
|
syIcon: string = "",
|
||||||
maxIcon: string = ""
|
maxIcon: string = "",
|
||||||
): string {
|
): string {
|
||||||
return `
|
return `
|
||||||
<!-- 模型选择 -->
|
<!-- 模型选择 -->
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
<div class="custom-select" id="modelSelect">
|
<div class="model-display">
|
||||||
<div class="select-trigger" onclick="toggleModelDropdown()">
|
<img src="${maxIcon || ""}" class="model-icon" alt="Max" style="display: ${maxIcon ? "block" : "none"};">
|
||||||
<span class="select-value" id="modelValue">Auto</span>
|
<span class="model-label">Max</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>
|
||||||
<div class="select-dropdown" id="modelDropdown">
|
<span class="tooltiptext">IC Coder自研FPGA专属微调模型</span>
|
||||||
<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>
|
|
||||||
<span class="tooltiptext">选择模型</span>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -78,72 +28,16 @@ export function getModelSelectorContent(
|
|||||||
*/
|
*/
|
||||||
export function getModelSelectorStyles(): string {
|
export function getModelSelectorStyles(): string {
|
||||||
return `
|
return `
|
||||||
/* 自定义下拉框样式 */
|
.model-display {
|
||||||
.custom-select {
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.select-trigger {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background: var(--vscode-input-background);
|
background: var(--vscode-input-background);
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
transition: background 0.2s ease;
|
cursor: default;
|
||||||
}
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
.model-icon {
|
.model-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@ -151,21 +45,7 @@ export function getModelSelectorStyles(): string {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
.option-content {
|
.model-label {
|
||||||
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);
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -176,58 +56,9 @@ export function getModelSelectorStyles(): string {
|
|||||||
*/
|
*/
|
||||||
export function getModelSelectorScript(): string {
|
export function getModelSelectorScript(): string {
|
||||||
return `
|
return `
|
||||||
// 模型选择相关变量
|
// 获取当前选中的模型(固定为 max)
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取当前选中的模型
|
|
||||||
function getCurrentModel() {
|
function getCurrentModel() {
|
||||||
return currentModel;
|
return 'max';
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 更多选项组件
|
* 更多选项组件
|
||||||
* 包含用户手册和用户反馈入口
|
* 包含用户手册入口
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,40 +28,10 @@ export function getMoreOptionsComponentContent(): string {
|
|||||||
<div class="option-desc">查看使用文档和帮助</div>
|
<div class="option-desc">查看使用文档和帮助</div>
|
||||||
</div>
|
</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>
|
||||||
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -163,125 +133,6 @@ export function getMoreOptionsComponentStyles(): string {
|
|||||||
.option-desc {
|
.option-desc {
|
||||||
display: none;
|
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();
|
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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// 绑定用户手册选项
|
// 绑定用户手册选项
|
||||||
@ -362,12 +190,6 @@ export function getMoreOptionsComponentScript(): string {
|
|||||||
userManualOption.addEventListener('click', openUserManual);
|
userManualOption.addEventListener('click', openUserManual);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定用户反馈选项
|
|
||||||
const userFeedbackOption = document.getElementById('userFeedbackOption');
|
|
||||||
if (userFeedbackOption) {
|
|
||||||
userFeedbackOption.addEventListener('click', openUserFeedback);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击页面其他地方关闭下拉面板
|
// 点击页面其他地方关闭下拉面板
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const dropdown = document.getElementById('moreOptionsDropdown');
|
const dropdown = document.getElementById('moreOptionsDropdown');
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export function getPlanCardStyles(): string {
|
|||||||
.plan-summary h2 { font-size: 16px; }
|
.plan-summary h2 { font-size: 16px; }
|
||||||
.plan-summary h3 { font-size: 14px; }
|
.plan-summary h3 { font-size: 14px; }
|
||||||
.plan-summary h4 { font-size: 13px; }
|
.plan-summary h4 { font-size: 13px; }
|
||||||
.plan-summary p { margin: 8px 0; letter-spacing: 0.5px; }
|
.plan-summary p { margin: 8px 0; }
|
||||||
.plan-summary ul, .plan-summary ol {
|
.plan-summary ul, .plan-summary ol {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
padding-left: 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,
|
getGeneralSettingsComponentStyles,
|
||||||
getGeneralSettingsComponentScript,
|
getGeneralSettingsComponentScript,
|
||||||
} from "./generalSettingsComponent";
|
} from "./generalSettingsComponent";
|
||||||
import {
|
|
||||||
getRulesSettingsComponentContent,
|
|
||||||
getRulesSettingsComponentStyles,
|
|
||||||
getRulesSettingsComponentScript,
|
|
||||||
} from "./rulesSettingsComponent";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取设置面板的 HTML 内容
|
* 获取设置面板的 HTML 内容
|
||||||
@ -31,18 +26,13 @@ export function getSettingsComponentContent(): string {
|
|||||||
<button class="settings-nav-item active" data-tab="general" onclick="switchSettingsTab('general')">
|
<button class="settings-nav-item active" data-tab="general" onclick="switchSettingsTab('general')">
|
||||||
通用
|
通用
|
||||||
</button>
|
</button>
|
||||||
<button class="settings-nav-item" data-tab="rules" onclick="switchSettingsTab('rules')">
|
|
||||||
规则
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<div class="settings-tab-content active" id="generalSettings">
|
<div class="settings-tab-content active" id="generalSettings">
|
||||||
${getGeneralSettingsComponentContent()}
|
${getGeneralSettingsComponentContent()}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-tab-content" id="rulesSettings">
|
|
||||||
${getRulesSettingsComponentContent()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,7 +176,6 @@ export function getSettingsComponentStyles(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
${getGeneralSettingsComponentStyles()}
|
${getGeneralSettingsComponentStyles()}
|
||||||
${getRulesSettingsComponentStyles()}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,13 +185,14 @@ export function getSettingsComponentStyles(): string {
|
|||||||
export function getSettingsComponentScript(): string {
|
export function getSettingsComponentScript(): string {
|
||||||
return `
|
return `
|
||||||
${getGeneralSettingsComponentScript()}
|
${getGeneralSettingsComponentScript()}
|
||||||
${getRulesSettingsComponentScript()}
|
|
||||||
|
|
||||||
// 打开设置面板
|
// 打开设置面板
|
||||||
function openSettingsModal() {
|
function openSettingsModal() {
|
||||||
const modal = document.getElementById('settingsModal');
|
const modal = document.getElementById('settingsModal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.classList.add('active');
|
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 {
|
export function getUserInfoComponentContent(): string {
|
||||||
return `
|
return `
|
||||||
<div class="user-info-wrapper">
|
<div class="user-info-wrapper" style="display: none;">
|
||||||
<!-- 用户详情下拉面板 -->
|
|
||||||
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,11 @@ import {
|
|||||||
getMessageAreaScript,
|
getMessageAreaScript,
|
||||||
} from "./messageArea";
|
} from "./messageArea";
|
||||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||||
|
import {
|
||||||
|
getMoreOptionsComponentContent,
|
||||||
|
getMoreOptionsComponentStyles,
|
||||||
|
getMoreOptionsComponentScript,
|
||||||
|
} from "./moreOptionsComponent";
|
||||||
import {
|
import {
|
||||||
getProgressBarContent,
|
getProgressBarContent,
|
||||||
getProgressBarStyles,
|
getProgressBarStyles,
|
||||||
@ -25,7 +30,6 @@ import {
|
|||||||
} from "./progressBar";
|
} from "./progressBar";
|
||||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||||
import { getCurrentEnv } from "../config/settings";
|
import { getCurrentEnv } from "../config/settings";
|
||||||
import { taskCompleteIconSvg } from "../constants/toolIcons";
|
|
||||||
import {
|
import {
|
||||||
getInvitationModalContent,
|
getInvitationModalContent,
|
||||||
getInvitationModalStyles,
|
getInvitationModalStyles,
|
||||||
@ -111,6 +115,7 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
${getMessageAreaStyles()}
|
${getMessageAreaStyles()}
|
||||||
${getAgentCardStyles()}
|
${getAgentCardStyles()}
|
||||||
|
${getMoreOptionsComponentStyles()}
|
||||||
${getWaveformPreviewContent()}
|
${getWaveformPreviewContent()}
|
||||||
${getConversationHistoryBarStyles()}
|
${getConversationHistoryBarStyles()}
|
||||||
${getProgressBarStyles()}
|
${getProgressBarStyles()}
|
||||||
@ -305,9 +310,7 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
.segment-text {
|
.segment-text {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
font-size:0.9rem;
|
font-size:0.9rem
|
||||||
color: var(--vscode-foreground);
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
}
|
||||||
.segment-tool {
|
.segment-tool {
|
||||||
background: var(--vscode-textBlockQuote-background);
|
background: var(--vscode-textBlockQuote-background);
|
||||||
@ -507,13 +510,17 @@ export function getWebviewContent(
|
|||||||
${getNdtWelcomeModalContent(logoUri)}
|
${getNdtWelcomeModalContent(logoUri)}
|
||||||
${getExpiredModalContent(logoUri)}
|
${getExpiredModalContent(logoUri)}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div style="display: flex; align-items: center; justify-content: center;">
|
<div style="display: flex; align-items: flex-end; justify-content: center">
|
||||||
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
|
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
|
||||||
|
<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>
|
</div>
|
||||||
<p style="font-size: 16px; margin-top: 12px; line-height: 1.5;">
|
<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,
|
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>
|
<span style="display: block; margin-top: 8px;">将FPGA研发效率提升至少20倍!</span>
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
@ -548,9 +555,6 @@ export function getWebviewContent(
|
|||||||
const modeSelect = document.getElementById('modeSelect');
|
const modeSelect = document.getElementById('modeSelect');
|
||||||
const messagesEl = document.getElementById('messages');
|
const messagesEl = document.getElementById('messages');
|
||||||
|
|
||||||
// 图标常量
|
|
||||||
const taskCompleteIconSvg = ${JSON.stringify(taskCompleteIconSvg)};
|
|
||||||
|
|
||||||
// 全局变量
|
// 全局变量
|
||||||
let currentStreamingMessage = null;
|
let currentStreamingMessage = null;
|
||||||
let loadingIndicator = null;
|
let loadingIndicator = null;
|
||||||
@ -775,45 +779,6 @@ export function getWebviewContent(
|
|||||||
// 隐藏加载指示器
|
// 隐藏加载指示器
|
||||||
hideLoadingIndicator();
|
hideLoadingIndicator();
|
||||||
break;
|
break;
|
||||||
case 'taskComplete':
|
|
||||||
// 显示任务完成提示
|
|
||||||
const taskDiv = document.createElement('div');
|
|
||||||
taskDiv.className = 'message bot-message';
|
|
||||||
const taskActionsDiv = document.createElement('div');
|
|
||||||
taskActionsDiv.className = 'message-actions';
|
|
||||||
const taskMessageContent = document.createElement('span');
|
|
||||||
taskMessageContent.innerHTML = taskCompleteIconSvg + ' 任务完成';
|
|
||||||
const taskCopyBtn = document.createElement('button');
|
|
||||||
taskCopyBtn.className = 'action-btn';
|
|
||||||
taskCopyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
|
||||||
taskCopyBtn.onclick = () => {
|
|
||||||
// 获取前一个 AI 消息的内容
|
|
||||||
const prevMessage = taskDiv.previousElementSibling;
|
|
||||||
if (prevMessage && prevMessage.classList.contains('bot-message')) {
|
|
||||||
const textContent = prevMessage.textContent || '';
|
|
||||||
copyMessage(textContent, taskCopyBtn);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const taskLikeBtn = document.createElement('button');
|
|
||||||
taskLikeBtn.className = 'action-btn';
|
|
||||||
taskLikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
|
||||||
taskLikeBtn.onclick = () => toggleLike(taskLikeBtn);
|
|
||||||
const taskDislikeBtn = document.createElement('button');
|
|
||||||
taskDislikeBtn.className = 'action-btn';
|
|
||||||
taskDislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
|
||||||
taskDislikeBtn.onclick = () => toggleDislike(taskDislikeBtn);
|
|
||||||
taskActionsDiv.appendChild(taskMessageContent);
|
|
||||||
taskActionsDiv.appendChild(taskCopyBtn);
|
|
||||||
taskActionsDiv.appendChild(taskLikeBtn);
|
|
||||||
taskActionsDiv.appendChild(taskDislikeBtn);
|
|
||||||
taskDiv.appendChild(taskActionsDiv);
|
|
||||||
messagesEl.appendChild(taskDiv);
|
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'taskCompleteHistory':
|
|
||||||
// 历史记录不显示任务完成提示
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'workspaceStatus':
|
case 'workspaceStatus':
|
||||||
// 更新工作区状态
|
// 更新工作区状态
|
||||||
@ -964,6 +929,13 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'loadedGeneralSettings':
|
||||||
|
// 加载通用设置
|
||||||
|
if (typeof loadGeneralSettings === 'function') {
|
||||||
|
loadGeneralSettings(message.settings);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||||
}
|
}
|
||||||
@ -971,6 +943,7 @@ export function getWebviewContent(
|
|||||||
|
|
||||||
${getMessageAreaScript()}
|
${getMessageAreaScript()}
|
||||||
${getAgentCardScript()}
|
${getAgentCardScript()}
|
||||||
|
${getMoreOptionsComponentScript()}
|
||||||
${getWaveformPreviewScript()}
|
${getWaveformPreviewScript()}
|
||||||
${getConversationHistoryBarScript()}
|
${getConversationHistoryBarScript()}
|
||||||
${getProgressBarScript()}
|
${getProgressBarScript()}
|
||||||
|
|||||||