15 Commits

Author SHA1 Message Date
47f95afabb Merge branch 'feat/codeToChat' into feat/ningDeShiDai 2026-03-10 20:53:31 +08:00
081ddec55c Merge branch 'feat/ningDeShiDai' of https://git.pengyejiatu.com/pengyejiatu/IC-Coder-Plugin into feat/ningDeShiDai 2026-03-10 19:18:06 +08:00
12c2f634bd Merge branch 'feat/codeToChat' into feat/ningDeShiDai 2026-03-10 19:05:45 +08:00
b9dc631bf7 Merge branch 'feat/ningDeShiDai' of https://git.pengyejiatu.com/pengyejiatu/Ic-coder-plugin into feat/ningDeShiDai 2026-03-10 19:01:38 +08:00
6425496d2e feat: 离线部署模式改进和 SystemVerilog 支持
- 添加离线模式仿真模拟:识别代码生成完成消息后自动模拟仿真流程
- 启用 iverilog SystemVerilog 2012 标准支持(-g2012)
- 优化进度条显示逻辑

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 18:56:53 +08:00
fd5a01c67f Merge branch 'feat/codeToChat' into feat/ningDeShiDai 2026-03-10 18:40:13 +08:00
a25d68f527 feat: 添加用户手册只读预览功能
- 新增 UserManualPanel 组件实现只读预览
   - 支持 Markdown 完整渲染(表格、代码块、图片、分隔线)
   - 优化排版和字体大小
   - 用户无法编辑手册内容
2026-03-10 17:01:16 +08:00
77b54aebf0 feat: 添加用户手册功能
- 新增用户手册 Markdown 文档及配套截图
   - 新增打开用户手册命令
   - 在侧边栏和主面板中集成用户手册入口
   - 优化用户手册打开方式,支持 Markdown 预览
2026-03-10 16:29:37 +08:00
840436eb36 refactor: 优化设置面板和模型提示文案
- 移除设置面板中的规则配置标签页
   - 更新模型选择器提示文案为"FPGA专属微调模型"
2026-03-10 14:23:37 +08:00
f5dd7534f0 feat:将模型选中固定为max模型
- 并且鼠标移动上去显示“IC Coder自研顶尖微调模型“
2026-03-10 14:13:20 +08:00
ebb9de5294 tyle:修改了聊天面板的样式及删除了用户反馈和web端的跳转 2026-03-10 14:02:44 +08:00
531d140b99 refactor: 优化错误提示信息
- 将"当前访问人数过多"改为更通用的"处理用户消息失败"
   - 移除错误消息中的  emoji,保持简洁
2026-03-09 15:55:51 +08:00
97b8e8aa7d refactor: 更新后端服务地址占位符 2026-03-09 15:34:41 +08:00
4ed998e937 feat: 添加后端服务地址自定义配置功能
- 在设置面板添加后端服务地址配置项
   - 支持保存、加载和重置自定义配置
   - 配置持久化存储,重启后保留
   - 添加 SSE 请求日志用于验证配置
2026-03-09 15:29:56 +08:00
ad0f0336d5 feat: 移除用户信息、余额检查和登录过期提示
- 隐藏用户信息显示和退出登录按钮
   - 删除发送消息前的余额检查逻辑
   - 删除对话完成后的余额更新逻辑
   - 注释掉所有登录过期弹窗提示
   - 移除用户服务和余额服务的初始化调用
2026-03-09 14:28:41 +08:00
54 changed files with 3719 additions and 4953 deletions

View File

@ -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布局布线
- **比特流**BitstreamFPGA 配置文件
- **DCP**Design CheckpointVivado 设计检查点文件
- **XDC**Xilinx Design Constraints约束文件
- **LUT**Look-Up Table查找表FPGA 基本逻辑单元
- **FF**Flip-Flop触发器

View File

@ -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. 给用户提示和建议

View File

@ -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
View 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` 文件
- 等待安装完成
![安装方式1.png](./manual/安装方式1.png)
**方式 B通过扩展视图**
- 点击左侧活动栏的扩展图标(或按 `Ctrl+Shift+X`
- 点击扩展视图右上角的 `...` (更多操作)
- 选择 `从 VSIX 安装...`
- 选择 `iccoder-Trial-1.0.vsix` 文件
![安装方式2.png](./manual/安装方式2.png)
**方式 C通过命令行**
```bash
code --install-extension iccoder-Trial-1.0.vsix
```
4. **重启 VS Code**
- 安装完成后,建议重启 VS Code 以确保插件正常加载
#### 步骤 2打开 IC Coder 界面
**登录后会自动打开**,手动打开也有以下几种方式:
**方式 1通过侧边栏**
- 点击左侧活动栏的 IC Coder 图标
- 侧边栏会显示 IC Coder 聊天界面
![侧边栏打开.png](./manual/侧边栏打开.png)
**方式 2通过命令面板**
- 按 `Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS)
- 输入以下命令之一:
- `IC Coder: 打开聊天` - 打开聊天界面
- `打开 IC Coder 助手` - 打开助手面板
![命令面板打开.png](./manual/命令面板打开.png)
---
#### 步骤 3开始使用
插件已预配置好后端服务,安装后即可直接使用,无需手动配置。
![聊天界面.png](./manual/聊天界面.png)
### 故障排除
#### 问题 :插件无法安装
**症状**:安装 VSIX 文件时报错
#### 解决方案:
- 确认 VS Code 版本 >= 1.60.0
- 检查 VSIX 文件是否完整(未损坏)
- 尝试以管理员权限运行 VS Code
- 清除 VS Code 缓存后重试
### 完整使用流程示例
下面通过一个完整的案例,展示如何使用 IC Coder 从需求到代码生成的全过程。
#### 步骤 1输入设计需求
在对话框中输入设计需求,例如:
```
我需要设计一个 8 位加法器,要求有进位输入和进位输出
```
点击**发送**按钮。
![输入需求.png](./manual/输入需求.png)
#### 步骤 2AI 询问补充信息
AI 会根据需求,询问一些关键的设计细节。例如:
- 是否需要溢出检测?
- 时钟频率要求是多少?
- 是否需要流水线设计?
您只需要根据实际需求**选择相应的选项或者直接输入需求**即可AI 会根据您的选择生成最合适的设计方案。
#### 步骤 3确认 AI 生成的任务列表
AI 会根据您的需求和补充信息生成一个详细的任务列表Todo List
![确认任务.png](./manual/确认任务.png)
仔细查看任务列表,确认无误后点击**确认**按钮AI 将开始执行。
#### 步骤 4观察 AI 执行过程
AI 开始工作后,您可以在对话框中实时看到所有执行步骤:
![观察执行过程.png](./manual/观察执行过程.png)
每个步骤完成后,任务列表中对应的项目会被标记为完成状态。
#### 步骤 5仿真运行与结果查看
当 AI 完成代码生成后,会自动运行仿真验证:
![仿真运行结果.png](./manual/仿真运行结果.png)
#### 步骤 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. **需提前打开一个文件夹作为工作区**,否则会准确的为您服务
![打开文件夹.png](./manual/打开文件夹.png)
2. **开箱即用**
- 插件已预配置后端服务,无需手动设置
- 安装后即可直接使用所有功能
---
**祝您使用愉快!如有问题欢迎反馈。**

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -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;
} }
/** /**

View File

@ -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>`;

View File

@ -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,

File diff suppressed because it is too large Load Diff

View 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();
}
}
}

View File

@ -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;
}

View File

@ -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),
});
}
}

View File

@ -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}`);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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,
});
}
});
}

View File

@ -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;
}

View File

@ -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: () => {

View File

@ -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: [],

View File

@ -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 = {

View File

@ -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 || "(目录为空)";
} }
/** /**

View File

@ -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) {

View File

@ -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,

View File

@ -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 {
// 隐藏状态栏 // 隐藏状态栏
@ -428,7 +435,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 +1476,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)
};
}
}

View File

@ -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;
}
}
/** /**
* 获取文件信息 * 获取文件信息
*/ */

View File

@ -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;
}

View File

@ -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')
};
}

View File

@ -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}`
});
});
});
}

View File

@ -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>

View File

@ -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'>

View File

@ -41,14 +41,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>
`; `;
} }
@ -204,41 +196,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 +204,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);
}
`; `;
} }

View File

@ -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;
} }
} }
`; `;

File diff suppressed because it is too large Load Diff

View File

@ -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()}
`;
}

View File

@ -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()}
`;
}

View File

@ -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';
} }
`; `;
} }

View File

@ -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');

View File

@ -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;

View File

@ -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();
}
`;
}

View File

@ -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();
}
`;
}

View File

@ -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' });
} }
} }

View File

@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const placeholder = `___INLINE_CODE_${inlineCodes.length}___`;
inlineCodes.push(`<code>${escapedCode}</code>`);
return placeholder;
});
html = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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;
}

View File

@ -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;
}

View File

@ -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>
`; `;
} }

View File

@ -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,44 +779,10 @@ export function getWebviewContent(
// 隐藏加载指示器 // 隐藏加载指示器
hideLoadingIndicator(); hideLoadingIndicator();
break; break;
case 'taskComplete': case 'taskComplete':
// 显示任务完成提示 // 显示任务完成提示
const taskDiv = document.createElement('div'); addMessage('✅ 任务已完成', 'bot');
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; break;
case 'workspaceStatus': case 'workspaceStatus':
@ -964,6 +934,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 +948,7 @@ export function getWebviewContent(
${getMessageAreaScript()} ${getMessageAreaScript()}
${getAgentCardScript()} ${getAgentCardScript()}
${getMoreOptionsComponentScript()}
${getWaveformPreviewScript()} ${getWaveformPreviewScript()}
${getConversationHistoryBarScript()} ${getConversationHistoryBarScript()}
${getProgressBarScript()} ${getProgressBarScript()}