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
64 changed files with 3847 additions and 8148 deletions

View File

@ -2,50 +2,6 @@
所有重要的项目变更都将记录在此文件中。 所有重要的项目变更都将记录在此文件中。
## [1.13.5] - 2026-03-17
### 新增
- 个人规则管理功能
- 代码选择添加到对话Ctrl+L / Cmd+L
### 优化
- 消息处理模块重构
- 提示信息优化
## [1.13.0] - 2026-03-15
### 新增
- Vivado 联动功能文档
- 示例刷新按钮
### 优化
- 界面样式和交互体验
## [1.12.0] - 2026-03-12
### 新增
- 对话结束提示
### 优化
- 面板组件架构优化
## [1.11.0] - 2026-03-10
### 优化
- 代码选择交互优化
### 修复
- AI 提问输入问题
- Windows 平台兼容性问题
## [1.0.12] - 2026-03-06 ## [1.0.12] - 2026-03-06
### 新增 ### 新增

View File

@ -1,392 +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 调用的完整逻辑
- **后端简化调用**:后端只需调用一个工具接口
- **文件自动导入**Vivado 执行完成后,自动将产出文件导入到项目
- **流程可视化**:执行进度、日志实时显示
### 2.2 非功能目标
- 配置简单,用户友好
- 执行过程可视化(进度、日志)
- 错误处理完善,提示清晰
## 3. 功能详细需求
### 3.1 Vivado 支持的操作
#### 3.1.1 综合Synthesis
- **输入**Verilog/VHDL 源文件、约束文件(.xdc
- **输出**:设计检查点(.dcp、综合报告.rpt
- **用途**:将 RTL 代码转换为门级网表,检查资源使用情况
#### 3.1.2 实现Implementation
- **输入**:综合后的 .dcp 文件
- **输出**:实现后的 .dcp 文件、时序报告、布局布线报告
- **用途**:完成布局布线,检查时序是否满足要求
#### 3.1.3 生成比特流Generate Bitstream
- **输入**:实现后的 .dcp 文件
- **输出**:比特流文件(.bit
- **用途**:生成可烧录到 FPGA 的配置文件
### 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 VivadoToolRequest {
command: string; // 命令类型synthesis | implementation | bitstream
parameters?: {
topModule?: string; // 顶层模块名
files?: string[]; // 输入文件列表
part?: string; // FPGA 型号(可选,使用配置中的默认值)
constraints?: string; // 约束文件路径(.xdc
outputDir?: string; // 输出目录
};
importOutput?: {
enabled: boolean; // 是否自动导入
targetDir: string; // 目标目录
};
}
interface VivadoToolResponse {
success: boolean;
command: string;
executionTime: number; // 执行时间(毫秒)
output: string; // 标准输出
error?: string; // 错误信息
importedFiles?: string[]; // 已导入的文件列表
reports?: {
// 报告摘要
resources?: string; // 资源使用情况
timing?: string; // 时序信息
};
}
```
### 3.4 执行流程
#### 3.4.1 参数验证
- 检查 Vivado 是否已配置
- 检查可执行文件是否存在
- 检查输入文件是否存在
- 检查工作目录是否存在
#### 3.4.2 TCL 脚本生成
根据命令类型自动生成 TCL 脚本:
**综合脚本示例synth.tcl**
```tcl
# 读取源文件
read_verilog counter.v
read_xdc constraints.xdc
# 设置顶层模块
set_property top counter [current_fileset]
# 综合
synth_design -part xc7a35tcpg236-1 -top counter
# 生成报告
report_utilization -file utilization_synth.rpt
report_timing -file timing_synth.rpt
# 保存检查点
write_checkpoint -force counter_synth.dcp
```
#### 3.4.3 命令执行
- 启动子进程执行 Vivado 命令
- 实时捕获标准输出和错误输出
- 向前端推送进度信息(解析日志中的进度标记)
#### 3.4.4 结果处理
- 检查执行结果(退出码)
- 解析报告文件,提取关键信息(资源使用、时序)
- 查找产出文件
#### 3.4.5 文件导入
- 根据配置的文件模式查找产出文件
- 复制文件到目标目录
- 通知用户导入结果
### 3.5 UI 交互
#### 3.5.1 配置界面
- 在设置页面添加 "Vivado 配置" 选项
- 支持配置 Vivado 路径、FPGA 型号
- 支持测试 Vivado 可用性(点击按钮测试)
#### 3.5.2 调用界面
- 在聊天面板中AI 可以建议使用 Vivado
- 用户确认后,显示执行进度对话框
- 实时显示日志输出(可折叠)
- 显示执行状态:准备中 → 执行中 → 完成/失败
#### 3.5.3 结果展示
- 执行成功:显示执行时间、资源使用、时序信息
- 执行失败:显示错误信息、建议解决方案
- 导入文件:高亮显示已导入的文件,支持点击打开报告
### 3.6 后端集成
#### 3.6.1 工具定义
后端在工具列表中添加 Vivado 工具:
```json
{
"name": "runVivado",
"description": "调用 Vivado 执行综合、实现或生成比特流。使用前必须先询问用户芯片型号等必要参数。",
"parameters": {
"command": "命令类型synthesis/implementation/bitstream",
"topModule": "顶层模块名",
"files": "输入文件列表",
"part": "FPGA 芯片型号(必须从用户获取)",
"constraints": "约束文件路径(可选)"
}
}
```
#### 3.6.2 后端交互流程
**关键点**:后端必须先收集必要参数,再调用工具
1. **用户发起请求**"打开 Vivado" 或 "用 Vivado 综合"
2. **后端识别意图**:需要调用 runVivado 工具
3. **后端询问参数**
- FPGA 芯片型号(必须)
- 约束文件(可选)
- 确认顶层模块名
4. **用户提供参数**
5. **后端调用工具**:传递完整参数给前端
6. **前端执行**VivadoRunner 执行命令
7. **返回结果**:后端接收结果并展示给用户
#### 3.6.3 调用示例(完整交互)
```
用户:帮我用 Vivado 综合一下 counter.v
AI好的我将使用 Vivado 进行综合。请提供以下信息:
1. FPGA 芯片型号例如xc7a35tcpg236-1
2. 是否有约束文件(.xdc
用户xc7a35tcpg236-1没有约束文件
AI收到开始综合...
[调用工具] runVivado
参数:
- command: synthesis
- topModule: counter
- files: ["counter.v"]
- part: "xc7a35tcpg236-1"
[执行中...]
Vivado 综合完成!
- 芯片型号xc7a35tcpg236-1
- 执行时间45 秒
- 资源使用LUT: 32/20800 (0.15%), FF: 8/41600 (0.02%)
- 产出文件counter_synth.dcp, utilization_synth.rpt
- 已自动导入到vivado_output/
```
## 4. 用户场景
### 4.1 场景一:单步综合
1. 用户编写完 Verilog 代码
2. 在聊天中输入:"用 Vivado 综合一下 counter.v"
3. AI 调用 `runVivado` 工具
4. 插件执行 Vivado 综合
5. 综合完成后,显示资源使用情况,自动导入报告文件
### 4.2 场景二:完整流程
1. 用户输入:"用 Vivado 跑完整个流程"
2. AI 依次调用:
- 综合Synthesis
- 实现Implementation
- 生成比特流Bitstream
3. 每个步骤完成后显示结果
4. 最终生成 .bit 文件,用户可以烧录到 FPGA
### 4.3 场景三:查看报告
1. Vivado 执行完成后
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 许可证
## 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,637 +0,0 @@
# Vivado 联动前后端对接文档
## 1. 概述
本文档描述后端 AI 服务如何调用前端的 Vivado 工具,以及前端如何响应和返回结果。
### 1.1 调用流程
```
后端 AI 服务
↓ (1) 发送工具调用请求
前端 Extension (MessageHandler)
↓ (2) 解析请求,调用 VivadoRunner
VivadoRunner
↓ (3) 执行 Vivado实时推送进度
前端 Webview
↓ (4) 显示进度和结果
前端 Extension
↓ (5) 返回执行结果给后端
后端 AI 服务
```
## 2. 工具定义(后端)
### 2.1 工具注册
后端需要在工具列表中注册 `runVivado` 工具:
```json
{
"name": "runVivado",
"description": "调用本地 Vivado 工具执行 FPGA 综合、实现或生成比特流。用于将 Verilog 代码部署到 FPGA 硬件。使用前必须先询问用户必要的参数(如芯片型号、执行模式)。",
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"enum": ["synthesis", "implementation", "bitstream"],
"description": "要执行的命令类型synthesis(综合)、implementation(实现)、bitstream(生成比特流)"
},
"topModule": {
"type": "string",
"description": "顶层模块名称"
},
"files": {
"type": "array",
"items": { "type": "string" },
"description": "输入的 Verilog 文件路径列表"
},
"constraints": {
"type": "string",
"description": "约束文件路径(.xdc 文件),可选"
},
"part": {
"type": "string",
"description": "FPGA 芯片型号(如 xc7a35tcpg236-1必须从用户处获取"
},
"mode": {
"type": "string",
"enum": ["batch", "gui"],
"description": "执行模式batch(后台批处理)、gui(打开图形界面),必须询问用户"
}
},
"required": ["command", "topModule", "files", "part", "mode"]
}
}
```
### 2.2 后端调用前的准备工作
**重要**:后端在调用 `runVivado` 工具前,必须先向用户询问必要参数:
1. **芯片型号part**:必须询问,例如 "xc7a35tcpg236-1"
2. **执行模式mode**:必须询问用户选择
- `batch`:后台批处理执行,自动完成
- `gui`:打开 Vivado 图形界面,用户手动操作
3. **顶层模块名**:可从文件名推断,但建议确认
4. **约束文件**:询问是否有时序约束文件(.xdc
**询问示例**
```
AI: 我将使用 Vivado 进行综合。请提供以下信息:
1. FPGA 芯片型号例如xc7a35tcpg236-1
2. 执行模式:
- 批处理模式:后台自动执行,完成后返回结果
- 图形界面:打开 Vivado GUI您可以手动操作
3. 是否有约束文件(.xdc
用户: xc7a35tcpg236-1批处理模式没有约束文件
AI: 好的,开始后台综合...
[调用 runVivado 工具]
```
### 2.3 调用示例
#### 示例 1综合单个文件批处理模式
```json
{
"tool": "runVivado",
"parameters": {
"command": "synthesis",
"topModule": "counter",
"files": ["counter.v"],
"part": "xc7a35tcpg236-1",
"mode": "batch"
}
}
```
#### 示例 2综合带约束文件图形界面模式
```json
{
"tool": "runVivado",
"parameters": {
"command": "synthesis",
"topModule": "uart_top",
"files": ["uart_tx.v", "uart_rx.v", "uart_top.v"],
"constraints": "constraints.xdc",
"part": "xc7k325tffg900-2",
"mode": "gui"
}
}
```
#### 示例 3实现批处理模式
```json
{
"tool": "runVivado",
"parameters": {
"command": "implementation",
"topModule": "counter",
"part": "xc7a35tcpg236-1",
"mode": "batch"
}
}
```
#### 示例 4生成比特流图形界面
```json
{
"tool": "runVivado",
"parameters": {
"command": "bitstream",
"topModule": "counter",
"part": "xc7a35tcpg236-1",
"mode": "gui"
}
}
```
## 3. 前端接收和处理
### 3.1 后端如何控制前端
**核心机制**:后端通过调用 `runVivado` 工具来控制前端执行 Vivado 命令。
**控制流程**
1. 后端识别用户意图(如"打开 Vivado"、"开始仿真"
2. 后端向用户询问必要参数(芯片型号等)
3. 后端调用 `runVivado` 工具,传递参数
4. 前端接收工具调用,执行相应操作
5. 前端返回执行结果给后端
6. 后端将结果展示给用户
**示例场景**
```
用户输入:"打开 Vivado 进行综合"
后端处理:
1. 识别意图 → 需要调用 runVivado 工具
2. 检查参数 → 缺少芯片型号
3. 询问用户 → "请提供 FPGA 芯片型号"
4. 用户回复 → "xc7a35tcpg236-1"
5. 调用工具 → runVivado({ command: "synthesis", part: "xc7a35tcpg236-1", ... })
6. 前端执行 → VivadoRunner 启动 Vivado
7. 返回结果 → { success: true, ... }
8. 展示结果 → "综合完成,耗时 45 秒"
```
### 3.2 MessageHandler 处理逻辑
前端在 `messageHandler.ts` 中添加工具处理:
```typescript
// src/utils/messageHandler.ts
export async function handleToolExecution(
panel: vscode.WebviewPanel,
toolName: string,
parameters: any
): Promise<any> {
if (toolName === 'runVivado') {
return await handleVivadoTool(panel, parameters);
}
// 其他工具处理...
}
async function handleVivadoTool(
panel: vscode.WebviewPanel,
parameters: any
): Promise<VivadoToolResponse> {
const { command, topModule, files, constraints, part, mode } = parameters;
// 验证必需参数
if (!part) {
return {
success: false,
command,
executionTime: 0,
output: '',
error: '缺少必需参数芯片型号part。后端应该先询问用户。'
};
}
if (!mode) {
return {
success: false,
command,
executionTime: 0,
output: '',
error: '缺少必需参数执行模式mode。后端应该询问用户选择 batch 或 gui。'
};
}
// 构建请求
const request: VivadoToolRequest = {
command,
parameters: {
topModule,
files,
constraints,
part,
mode
},
importOutput: {
enabled: mode === 'batch', // 只有批处理模式才自动导入
targetDir: path.join(
vscode.workspace.workspaceFolders![0].uri.fsPath,
'vivado_output'
)
}
};
// 向前端发送开始消息
panel.webview.postMessage({
type: 'vivado-start',
command
});
// 执行 Vivado
const response = await runVivado(request, (progress) => {
// 实时推送进度到前端
panel.webview.postMessage({
type: 'vivado-progress',
progress
});
});
// 向前端发送完成消息
panel.webview.postMessage({
type: 'vivado-complete',
response
});
// 返回结果给后端
return response;
}
```
## 4. 响应格式
### 4.1 成功响应
```json
{
"success": true,
"command": "synthesis",
"executionTime": 45230,
"output": "Vivado 执行日志...",
"importedFiles": [
"/path/to/vivado_output/counter_synth.dcp",
"/path/to/vivado_output/counter_utilization_synth.rpt"
],
"reports": {
"resources": "LUT: 32/20800 (0.15%)\nFF: 8/41600 (0.02%)",
"timing": "WNS: 5.234ns, TNS: 0.000ns"
}
}
```
### 4.2 失败响应
```json
{
"success": false,
"command": "synthesis",
"executionTime": 1250,
"output": "部分执行日志...",
"error": "ERROR: [Synth 8-439] module 'counter' not found"
}
```
## 5. 后端使用指南
### 5.1 AI 对话流程(完整版)
```
用户:帮我用 Vivado 综合一下 counter.v
AI 分析:
1. 用户想要综合 Verilog 文件
2. 需要调用 runVivado 工具
3. 命令类型是 synthesis
4. 顶层模块名从文件名推断为 counter
5. 输入文件是 counter.v
6. ⚠️ 缺少必要参数:芯片型号
AI 回复用户:
"好的,我将使用 Vivado 进行综合。请提供以下信息:
1. FPGA 芯片型号例如xc7a35tcpg236-1、xc7k325tffg900-2
2. 是否有约束文件(.xdc"
用户xc7a35tcpg236-1没有约束文件
AI 调用工具:
{
"tool": "runVivado",
"parameters": {
"command": "synthesis",
"topModule": "counter",
"files": ["counter.v"],
"part": "xc7a35tcpg236-1"
}
}
前端执行并返回结果
AI 回复用户:
"Vivado 综合完成!
- 执行时间45.2 秒
- 芯片型号xc7a35tcpg236-1
- 资源使用LUT: 32/20800 (0.15%), FF: 8/41600 (0.02%)
- 产出文件已导入到 vivado_output 目录"
```
### 5.2 完整流程示例
```
用户:用 Vivado 跑完整个流程
AI好的我将依次执行综合、实现和生成比特流。请提供
1. FPGA 芯片型号
2. 顶层模块名
3. 是否有约束文件
用户xc7a35tcpg236-1顶层模块是 counter没有约束文件
AI收到开始执行...
步骤 1综合
[调用] runVivado { command: "synthesis", topModule: "counter", files: ["counter.v"], part: "xc7a35tcpg236-1" }
[结果] 综合成功,耗时 45s
步骤 2实现
[调用] runVivado { command: "implementation", topModule: "counter", part: "xc7a35tcpg236-1" }
[结果] 实现成功,耗时 120s时序满足要求
步骤 3生成比特流
[调用] runVivado { command: "bitstream", topModule: "counter", part: "xc7a35tcpg236-1" }
[结果] 比特流生成成功文件counter.bit
完成!所有文件已导入到 vivado_output 目录。
```
## 6. 错误处理
### 6.1 常见错误
#### 错误 1Vivado 未配置
```json
{
"success": false,
"error": "Vivado 未配置,请在设置中配置 Vivado 路径"
}
```
**AI 应该回复**
"Vivado 尚未配置,请先在插件设置中配置 Vivado 的安装路径。"
#### 错误 2文件不存在
```json
{
"success": false,
"error": "输入文件不存在: counter.v"
}
```
**AI 应该回复**
"找不到文件 counter.v请确认文件路径是否正确。"
#### 错误 3综合失败
```json
{
"success": false,
"error": "ERROR: [Synth 8-439] module 'counter' not found",
"output": "详细日志..."
}
```
**AI 应该回复**
"综合失败,错误信息:找不到模块 'counter'。请检查:
1. 模块名是否正确
2. 文件中是否定义了该模块
3. 是否有语法错误"
### 6.2 错误处理建议
后端收到 `success: false` 时:
1. 提取 `error` 字段中的错误信息
2. 分析错误类型(配置问题、文件问题、语法问题等)
3. 给用户提供具体的解决建议
4. 必要时可以查看 `output` 字段获取详细日志
## 7. 进度推送(可选)
前端会实时推送进度信息到 Webview后端无需处理但可以了解进度格式
```json
{
"type": "vivado-progress",
"progress": {
"stage": "synthesis",
"percentage": 45,
"message": "正在综合模块 counter..."
}
}
```
## 8. 测试建议
### 8.1 后端测试用例
```javascript
// 测试用例 1基本综合
test('综合单个文件', async () => {
const result = await callTool('runVivado', {
command: 'synthesis',
topModule: 'counter',
files: ['counter.v']
});
expect(result.success).toBe(true);
expect(result.importedFiles.length).toBeGreaterThan(0);
});
// 测试用例 2错误处理
test('文件不存在', async () => {
const result = await callTool('runVivado', {
command: 'synthesis',
topModule: 'test',
files: ['not_exist.v']
});
expect(result.success).toBe(false);
expect(result.error).toContain('不存在');
});
```
## 9. 注意事项
### 9.1 后端必须询问的参数
**关键要求**:后端在调用 `runVivado` 前,必须先向用户收集以下参数:
| 参数 | 是否必须 | 询问方式 | 示例 |
|------|---------|---------|------|
| part芯片型号 | ✅ 必须 | 直接询问 | "请提供 FPGA 芯片型号(如 xc7a35tcpg236-1" |
| mode执行模式 | ✅ 必须 | 提供选项 | "选择执行模式1.批处理 2.图形界面" |
| topModule | ✅ 必须 | 可推断或询问 | 从文件名推断,或询问"顶层模块名是什么?" |
| files | ✅ 必须(综合时) | 可推断或询问 | 从当前文件或工作区推断 |
| constraints | ❌ 可选 | 询问 | "是否有约束文件(.xdc" |
### 9.2 常见用户输入场景
#### 场景 1用户说"打开 Vivado"
```
用户:打开 Vivado
后端分析:用户想使用 Vivado但没有指定具体操作
后端回复:
"好的,请问您想执行什么操作?
1. 综合Synthesis- 将 Verilog 转换为门级网表
2. 实现Implementation- 布局布线
3. 生成比特流Bitstream- 生成 .bit 文件
另外,请提供:
- FPGA 芯片型号例如xc7a35tcpg236-1
- 执行模式:批处理(后台自动)或 图形界面(手动操作)"
```
#### 场景 2用户说"开始仿真"
```
用户:开始仿真
后端分析:用户可能混淆了仿真和综合
后端回复:
"您是想:
1. 使用 iverilog 进行功能仿真(验证逻辑)
2. 使用 Vivado 进行综合(部署到 FPGA
请选择您需要的操作。"
```
#### 场景 3用户提供完整信息
```
用户:用 Vivado 综合 counter.v芯片是 xc7a35tcpg236-1批处理模式
后端分析:信息完整,可以直接调用
后端操作:
[调用] runVivado({
command: "synthesis",
topModule: "counter",
files: ["counter.v"],
part: "xc7a35tcpg236-1",
mode: "batch"
})
```
#### 场景 4用户选择图形界面
```
用户:打开 Vivado 图形界面做综合
后端分析:用户明确要求 GUI 模式
后端询问:
"好的,请提供:
1. FPGA 芯片型号
2. 顶层模块名"
用户xc7a35tcpg236-1, counter
后端操作:
[调用] runVivado({
command: "synthesis",
topModule: "counter",
files: ["counter.v"],
part: "xc7a35tcpg236-1",
mode: "gui"
})
前端执行:
- 生成 TCL 脚本和项目文件
- 执行: vivado counter_project.xpr (打开图形界面)
- 返回: { success: true, message: "Vivado GUI 已启动" }
后端回复:
"Vivado 图形界面已打开,您可以在界面中手动操作。"
```
### 9.3 执行时间
- 综合:小型设计 30s-2min大型设计 5-30min
- 实现:通常是综合时间的 2-3 倍
- 生成比特流:通常 10-30s
后端应该设置合理的超时时间(建议 10 分钟)。
### 9.4 依赖关系
- `implementation` 需要先执行 `synthesis`
- `bitstream` 需要先执行 `implementation`
后端 AI 应该理解这个依赖关系,按顺序调用。
### 9.5 文件路径
- 所有文件路径都是相对于工作区根目录
- 前端会自动解析为绝对路径
- 支持相对路径和绝对路径
## 10. 参数传递详细说明
### 10.1 必需参数
| 参数 | 类型 | 说明 | 获取方式 |
|------|------|------|----------|
| command | string | 命令类型 | 从用户意图推断 |
| topModule | string | 顶层模块名 | 从文件名推断或询问用户 |
| files | string[] | 源文件列表 | 从工作区查找或用户指定 |
| part | string | 芯片型号 | **必须询问用户** |
### 10.2 可选参数
| 参数 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| constraints | string | 约束文件路径 | 无 |
### 10.3 参数验证规则
后端在调用前应验证:
- `part` 格式正确(如 xc7a35tcpg236-1
- `files` 数组不为空
- `topModule` 不为空
- `command` 在枚举值内
## 11. 快速集成清单
后端开发者需要做的事情:
- [ ] 在工具列表中注册 `runVivado` 工具
- [ ] **实现参数询问逻辑(芯片型号等)**
- [ ] 实现工具调用逻辑(发送请求到前端)
- [ ] 处理返回结果success/error
- [ ] 实现错误处理和用户提示
- [ ] 理解三个命令的依赖关系
- [ ] 设置合理的超时时间(建议 10 分钟)
- [ ] 编写测试用例
前端开发者需要做的事情:
- [ ] 实现 `handleVivadoTool` 函数
- [ ] 集成 VivadoRunner
- [ ] 实现进度推送
- [ ] 实现结果展示
- [ ] 处理各种错误情况
- [ ] 验证传入的参数完整性

View File

@ -1,923 +0,0 @@
# Vivado 联动功能技术设计文档
## 1. 架构设计
### 1.1 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ 后端 AI 服务 │
│ (调用 runVivado 工具) │
└────────────────────────────┬────────────────────────────────┘
│ 工具调用请求
┌─────────────────────────────────────────────────────────────┐
│ VS Code Extension │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ICHelperPanel (Webview) │ │
│ │ - 接收后端工具调用 │ │
│ │ - 显示执行进度和日志 │ │
│ │ - 展示执行结果 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ VivadoRunner (utils/vivadoRunner.ts) │ │
│ │ - 配置管理 │ │
│ │ - TCL 脚本生成 │ │
│ │ - 命令执行 │ │
│ │ - 进度监控 │ │
│ │ - 结果解析 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ FileImporter (utils/fileImporter.ts) │ │
│ │ - 查找产出文件 │ │
│ │ - 复制文件到目标目录 │ │
│ │ - 通知文件变更 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 本地 Vivado 工具 │
│ (通过子进程执行 TCL 脚本) │
└─────────────────────────────────────────────────────────────┘
```
### 1.2 模块职责
#### 1.2.1 VivadoRunner
- 读取和验证 Vivado 配置
- 根据命令类型生成 TCL 脚本
- 启动子进程执行 Vivado
- 实时捕获输出并解析进度
- 返回执行结果
#### 1.2.2 FileImporter
- 根据文件模式查找产出文件
- 复制文件到指定目录
- 返回已导入的文件列表
#### 1.2.3 MessageHandler
- 接收后端的 `runVivado` 工具调用
- 调用 VivadoRunner 执行
- 向 Webview 推送进度和结果
## 2. 数据结构设计
### 2.1 配置结构
```typescript
/**
* Vivado 配置
*/
interface VivadoConfig {
enabled: boolean;
executablePath: string;
workingDir: string;
part: string;
commands: {
synthesis: string;
implementation: string;
bitstream: string;
};
outputFiles: {
synthesis: string[];
implementation: string[];
bitstream: string[];
};
}
```
### 2.2 请求和响应结构
```typescript
/**
* Vivado 工具请求
*/
interface VivadoToolRequest {
command: 'synthesis' | 'implementation' | 'bitstream';
parameters?: {
topModule?: string;
files?: string[];
part?: string;
constraints?: string;
outputDir?: string;
};
importOutput?: {
enabled: boolean;
targetDir: string;
};
}
/**
* Vivado 工具响应
*/
interface VivadoToolResponse {
success: boolean;
command: string;
executionTime: number;
output: string;
error?: string;
importedFiles?: string[];
reports?: {
resources?: string;
timing?: string;
};
}
/**
* 执行进度
*/
interface VivadoProgress {
stage: string;
percentage: number;
message: string;
}
```
## 3. 核心模块实现
### 3.1 配置管理
#### 3.1.1 配置读取
```typescript
// src/utils/vivadoConfig.ts
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
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');
return config.get<VivadoConfig>('vivado') || null;
}
export function validateConfig(config: VivadoConfig): string | null {
if (!config.enabled) {
return 'Vivado 未启用';
}
if (!fs.existsSync(config.executablePath)) {
return `Vivado 可执行文件不存在: ${config.executablePath}`;
}
return null;
}
```
### 3.2 TCL 脚本生成
#### 3.2.1 脚本生成器
```typescript
// src/utils/tclGenerator.ts
export function generateSynthesisTcl(
topModule: string,
files: string[],
part: string,
constraints?: string,
outputDir?: string
): string {
const output = outputDir || '.';
let tcl = `# Vivado 综合脚本\n\n`;
// 读取源文件
files.forEach(file => {
tcl += `read_verilog ${file}\n`;
});
// 读取约束文件
if (constraints) {
tcl += `read_xdc ${constraints}\n`;
}
tcl += `\n# 综合\n`;
tcl += `synth_design -part ${part} -top ${topModule}\n\n`;
// 生成报告
tcl += `# 生成报告\n`;
tcl += `report_utilization -file ${output}/${topModule}_utilization_synth.rpt\n`;
tcl += `report_timing -file ${output}/${topModule}_timing_synth.rpt\n\n`;
// 保存检查点
tcl += `# 保存检查点\n`;
tcl += `write_checkpoint -force ${output}/${topModule}_synth.dcp\n`;
return tcl;
}
export function generateImplementationTcl(
dcpFile: string,
outputDir?: string
): string {
const output = outputDir || '.';
const baseName = path.basename(dcpFile, '.dcp').replace('_synth', '');
let tcl = `# Vivado 实现脚本\n\n`;
tcl += `open_checkpoint ${dcpFile}\n\n`;
tcl += `# 优化\n`;
tcl += `opt_design\n`;
tcl += `place_design\n`;
tcl += `route_design\n\n`;
tcl += `# 生成报告\n`;
tcl += `report_utilization -file ${output}/${baseName}_utilization_impl.rpt\n`;
tcl += `report_timing_summary -file ${output}/${baseName}_timing_impl.rpt\n\n`;
tcl += `# 保存检查点\n`;
tcl += `write_checkpoint -force ${output}/${baseName}_impl.dcp\n`;
return tcl;
}
export function generateBitstreamTcl(
dcpFile: string,
outputDir?: string
): string {
const output = outputDir || '.';
const baseName = path.basename(dcpFile, '.dcp').replace('_impl', '');
let tcl = `# Vivado 比特流生成脚本\n\n`;
tcl += `open_checkpoint ${dcpFile}\n\n`;
tcl += `# 生成比特流\n`;
tcl += `write_bitstream -force ${output}/${baseName}.bit\n`;
return tcl;
}
```
### 3.3 VivadoRunner 实现
```typescript
// src/utils/vivadoRunner.ts
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { spawn } from 'child_process';
import { getVivadoConfig, validateConfig } from './vivadoConfig';
import { generateSynthesisTcl, generateImplementationTcl, generateBitstreamTcl } from './tclGenerator';
export async function runVivado(
request: VivadoToolRequest,
progressCallback?: (progress: VivadoProgress) => void
): Promise<VivadoToolResponse> {
const startTime = Date.now();
// 读取配置
const config = getVivadoConfig();
if (!config) {
return {
success: false,
command: request.command,
executionTime: 0,
output: '',
error: 'Vivado 未配置'
};
}
// 验证配置
const configError = validateConfig(config);
if (configError) {
return {
success: false,
command: request.command,
executionTime: 0,
output: '',
error: configError
};
}
// 准备工作目录
const workingDir = resolveWorkingDir(config.workingDir);
if (!fs.existsSync(workingDir)) {
fs.mkdirSync(workingDir, { recursive: true });
}
// 生成 TCL 脚本
const tclScript = generateTclScript(request, config, workingDir);
const tclPath = path.join(workingDir, `${request.command}.tcl`);
fs.writeFileSync(tclPath, tclScript);
// 执行 Vivado
const result = await executeVivado(
config.executablePath,
tclPath,
workingDir,
progressCallback
);
const executionTime = Date.now() - startTime;
// 解析报告
const reports = parseReports(request.command, workingDir, request.parameters?.topModule);
// 导入文件
let importedFiles: string[] = [];
if (request.importOutput?.enabled && result.success) {
importedFiles = await importOutputFiles(
request.command,
config,
workingDir,
request.importOutput.targetDir
);
}
return {
success: result.success,
command: request.command,
executionTime,
output: result.output,
error: result.error,
importedFiles,
reports
};
}
function generateTclScript(
request: VivadoToolRequest,
config: VivadoConfig,
workingDir: string
): string {
const { command, parameters } = request;
const part = parameters?.part || config.part;
switch (command) {
case 'synthesis':
return generateSynthesisTcl(
parameters?.topModule || 'top',
parameters?.files || [],
part,
parameters?.constraints,
parameters?.outputDir
);
case 'implementation':
const synthDcp = path.join(workingDir, `${parameters?.topModule}_synth.dcp`);
return generateImplementationTcl(synthDcp, parameters?.outputDir);
case 'bitstream':
const implDcp = path.join(workingDir, `${parameters?.topModule}_impl.dcp`);
return generateBitstreamTcl(implDcp, parameters?.outputDir);
default:
throw new Error(`未知命令: ${command}`);
}
}
async function executeVivado(
executablePath: string,
tclPath: string,
workingDir: string,
progressCallback?: (progress: VivadoProgress) => void
): Promise<{ success: boolean; output: string; error?: string }> {
return new Promise((resolve) => {
let output = '';
let errorOutput = '';
const process = spawn(executablePath, ['-mode', 'batch', '-source', tclPath], {
cwd: workingDir,
shell: true
});
process.stdout.on('data', (data) => {
const text = data.toString();
output += text;
// 解析进度
if (progressCallback) {
const progress = parseProgress(text);
if (progress) {
progressCallback(progress);
}
}
});
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 || '执行失败' });
}
});
});
}
function parseProgress(logText: string): VivadoProgress | null {
// 解析 Vivado 日志中的进度信息
if (logText.includes('Starting synthesis')) {
return { stage: 'synthesis', percentage: 10, message: '开始综合' };
}
if (logText.includes('Finished synthesis')) {
return { stage: 'synthesis', percentage: 100, message: '综合完成' };
}
// 更多进度解析...
return null;
}
function parseReports(
command: string,
workingDir: string,
topModule?: string
): { resources?: string; timing?: string } {
const reports: { resources?: string; timing?: string } = {};
if (command === 'synthesis' || command === 'implementation') {
const utilizationFile = path.join(
workingDir,
`${topModule}_utilization_${command === 'synthesis' ? 'synth' : 'impl'}.rpt`
);
if (fs.existsSync(utilizationFile)) {
const content = fs.readFileSync(utilizationFile, 'utf-8');
reports.resources = extractResourceSummary(content);
}
const timingFile = path.join(
workingDir,
`${topModule}_timing_${command === 'synthesis' ? 'synth' : 'impl'}.rpt`
);
if (fs.existsSync(timingFile)) {
const content = fs.readFileSync(timingFile, 'utf-8');
reports.timing = extractTimingSummary(content);
}
}
return reports;
}
function extractResourceSummary(reportContent: string): string {
// 提取资源使用摘要
const lines = reportContent.split('\n');
const summary: string[] = [];
for (const line of lines) {
if (line.includes('LUT') || line.includes('FF') || line.includes('BRAM')) {
summary.push(line.trim());
}
}
return summary.join('\n');
}
function extractTimingSummary(reportContent: string): string {
// 提取时序摘要
const lines = reportContent.split('\n');
for (const line of lines) {
if (line.includes('WNS') || line.includes('TNS')) {
return line.trim();
}
}
return '';
}
function resolveWorkingDir(workingDir: string): string {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
return workingDir.replace('${workspaceFolder}', workspaceFolder.uri.fsPath);
}
return workingDir;
}
```
### 3.4 文件导入实现
```typescript
// src/utils/fileImporter.ts
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as glob from 'glob';
export async function importOutputFiles(
command: string,
config: VivadoConfig,
sourceDir: string,
targetDir: string
): Promise<string[]> {
const patterns = config.outputFiles[command] || [];
const importedFiles: string[] = [];
for (const pattern of patterns) {
const files = glob.sync(pattern, { cwd: sourceDir });
for (const file of files) {
const sourcePath = path.join(sourceDir, file);
const targetPath = path.join(targetDir, file);
// 确保目标目录存在
const targetDirPath = path.dirname(targetPath);
if (!fs.existsSync(targetDirPath)) {
fs.mkdirSync(targetDirPath, { recursive: true });
}
// 复制文件
fs.copyFileSync(sourcePath, targetPath);
importedFiles.push(targetPath);
}
}
return importedFiles;
}
```
### 3.5 MessageHandler 集成
```typescript
// src/utils/messageHandler.ts (新增部分)
import { runVivado } from './vivadoRunner';
// 在 handleUserMessage 中添加 Vivado 工具处理
export async function handleVivadoTool(
panel: vscode.WebviewPanel,
toolCall: any
): Promise<VivadoToolResponse> {
const { command, topModule, files, constraints, part } = toolCall.parameters;
// 验证必需参数
if (!part) {
return {
success: false,
command,
executionTime: 0,
output: '',
error: '缺少必需参数芯片型号part'
};
}
// 构建请求
const request: VivadoToolRequest = {
command,
parameters: {
topModule,
files,
constraints,
part
},
importOutput: {
enabled: true,
targetDir: path.join(vscode.workspace.workspaceFolders![0].uri.fsPath, 'vivado_output')
}
};
// 向前端发送开始消息
panel.webview.postMessage({
type: 'vivado-start',
command
});
// 执行 Vivado
const response = await runVivado(request, (progress) => {
// 推送进度
panel.webview.postMessage({
type: 'vivado-progress',
progress
});
});
// 向前端发送结果
panel.webview.postMessage({
type: 'vivado-complete',
response
});
// 返回结果给后端
return response;
}
```
### 3.6 参数验证和处理
```typescript
// src/utils/vivadoValidator.ts
export interface ValidationResult {
valid: boolean;
error?: string;
}
export function validateVivadoRequest(request: VivadoToolRequest): ValidationResult {
const { command, parameters } = request;
// 验证命令类型
if (!['synthesis', 'implementation', 'bitstream'].includes(command)) {
return { valid: false, error: `无效的命令类型: ${command}` };
}
// 验证必需参数
if (!parameters?.topModule) {
return { valid: false, error: '缺少顶层模块名topModule' };
}
if (!parameters?.part) {
return { valid: false, error: '缺少芯片型号part' };
}
// 验证芯片型号格式
const partPattern = /^xc[0-9a-z]+$/i;
if (!partPattern.test(parameters.part)) {
return { valid: false, error: `芯片型号格式不正确: ${parameters.part}` };
}
// 综合命令需要文件列表
if (command === 'synthesis') {
if (!parameters?.files || parameters.files.length === 0) {
return { valid: false, error: '综合命令需要提供源文件列表' };
}
}
return { valid: true };
}
```
## 4. 前端 UI 实现
### 4.1 进度显示组件
```typescript
// src/views/vivadoProgress.ts
export function renderVivadoProgress(progress: VivadoProgress): string {
return `
<div class="vivado-progress">
<div class="progress-header">
<span class="stage">${progress.stage}</span>
<span class="percentage">${progress.percentage}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.percentage}%"></div>
</div>
<div class="progress-message">${progress.message}</div>
</div>
`;
}
```
### 4.2 结果展示组件
```typescript
// src/views/vivadoResult.ts
export function renderVivadoResult(response: VivadoToolResponse): string {
if (!response.success) {
return `
<div class="vivado-result error">
<h4>❌ 执行失败</h4>
<pre>${response.error}</pre>
</div>
`;
}
return `
<div class="vivado-result success">
<h4>✅ 执行成功</h4>
<div class="result-info">
<p>命令: ${response.command}</p>
<p>执行时间: ${(response.executionTime / 1000).toFixed(2)}s</p>
</div>
${response.reports?.resources ? `
<div class="report-section">
<h5>资源使用</h5>
<pre>${response.reports.resources}</pre>
</div>
` : ''}
${response.reports?.timing ? `
<div class="report-section">
<h5>时序信息</h5>
<pre>${response.reports.timing}</pre>
</div>
` : ''}
${response.importedFiles && response.importedFiles.length > 0 ? `
<div class="imported-files">
<h5>已导入文件</h5>
<ul>
${response.importedFiles.map(f => `<li>${f}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
}
```
## 5. 配置界面实现
### 5.1 设置页面扩展
```typescript
// src/views/vivadoSettings.ts
export function renderVivadoSettings(config: VivadoConfig | null): string {
return `
<div class="vivado-settings">
<h3>Vivado 配置</h3>
<div class="setting-item">
<label>启用 Vivado</label>
<input type="checkbox" id="vivado-enabled" ${config?.enabled ? 'checked' : ''}>
</div>
<div class="setting-item">
<label>可执行文件路径</label>
<input type="text" id="vivado-path" value="${config?.executablePath || ''}"
placeholder="C:/Xilinx/Vivado/2023.1/bin/vivado.bat">
<button onclick="testVivado()">测试</button>
</div>
<div class="setting-item">
<label>工作目录</label>
<input type="text" id="vivado-workdir" value="${config?.workingDir || ''}"
placeholder="\${workspaceFolder}/vivado_project">
</div>
<div class="setting-item">
<label>FPGA 型号</label>
<input type="text" id="vivado-part" value="${config?.part || ''}"
placeholder="xc7a35tcpg236-1">
</div>
<button onclick="saveVivadoConfig()">保存配置</button>
</div>
`;
}
```
## 6. 测试方案
### 6.1 单元测试
```typescript
// src/test/vivadoRunner.test.ts
import * as assert from 'assert';
import { generateSynthesisTcl } from '../utils/tclGenerator';
suite('Vivado TCL Generator', () => {
test('生成综合脚本', () => {
const tcl = generateSynthesisTcl(
'counter',
['counter.v'],
'xc7a35tcpg236-1'
);
assert.ok(tcl.includes('read_verilog counter.v'));
assert.ok(tcl.includes('synth_design'));
assert.ok(tcl.includes('write_checkpoint'));
});
});
```
### 6.2 集成测试
```typescript
// src/test/vivadoIntegration.test.ts
suite('Vivado Integration', () => {
test('完整综合流程', async () => {
const request: VivadoToolRequest = {
command: 'synthesis',
parameters: {
topModule: 'counter',
files: ['test/fixtures/counter.v']
}
};
const response = await runVivado(request);
assert.ok(response.success);
assert.ok(response.executionTime > 0);
});
});
```
## 7. 部署和发布
### 7.1 文件清单
新增文件:
- `src/utils/vivadoConfig.ts` - 配置管理
- `src/utils/tclGenerator.ts` - TCL 脚本生成
- `src/utils/vivadoRunner.ts` - Vivado 执行器
- `src/utils/fileImporter.ts` - 文件导入
- `src/views/vivadoProgress.ts` - 进度显示
- `src/views/vivadoResult.ts` - 结果展示
- `src/views/vivadoSettings.ts` - 设置界面
修改文件:
- `src/utils/messageHandler.ts` - 添加 Vivado 工具处理
- `src/views/settingsComponent.ts` - 添加 Vivado 设置页面
### 7.2 配置文件更新
```json
// package.json (新增配置项)
{
"contributes": {
"configuration": {
"properties": {
"ic-coder.vivado.enabled": {
"type": "boolean",
"default": false,
"description": "启用 Vivado 集成"
},
"ic-coder.vivado.executablePath": {
"type": "string",
"default": "",
"description": "Vivado 可执行文件路径"
},
"ic-coder.vivado.workingDir": {
"type": "string",
"default": "${workspaceFolder}/vivado_project",
"description": "Vivado 工作目录"
},
"ic-coder.vivado.part": {
"type": "string",
"default": "xc7a35tcpg236-1",
"description": "默认 FPGA 型号"
}
}
}
}
}
```
## 8. 常见问题和解决方案
### 8.1 Vivado 许可证问题
**问题**:执行时提示许可证错误
**解决方案**
1. 检查环境变量 `XILINX_VIVADO` 是否设置
2. 确认许可证服务器可访问
3. 在配置中添加许可证路径
### 8.2 路径问题
**问题**Windows 路径包含空格导致执行失败
**解决方案**
```typescript
function escapeWindowsPath(p: string): string {
return p.includes(' ') ? `"${p}"` : p;
}
```
### 8.3 执行超时
**问题**:大型项目综合时间过长
**解决方案**
- 增加超时时间配置
- 添加取消执行功能
- 显示详细进度信息
## 9. 性能优化
### 9.1 日志缓冲
限制日志输出大小,避免内存溢出:
```typescript
const MAX_LOG_SIZE = 1024 * 1024; // 1MB
let logBuffer = '';
process.stdout.on('data', (data) => {
logBuffer += data.toString();
if (logBuffer.length > MAX_LOG_SIZE) {
logBuffer = logBuffer.slice(-MAX_LOG_SIZE / 2);
}
});
```
### 9.2 增量构建
支持增量综合,只重新综合修改的模块。
## 10. 后续优化方向
1. **并行执行**:支持多个设计同时综合
2. **缓存机制**:缓存未修改模块的综合结果
3. **云端集成**:支持云端 Vivado 服务
4. **可视化报告**:图形化展示资源使用和时序
5. **自动约束生成**:根据设计自动生成 XDC 约束文件

View File

@ -1,247 +0,0 @@
# 个人规则功能 - 后端对接文档
## 1. 功能概述
个人规则功能允许用户创建多条自定义规则,这些规则会在每次对话时自动传递给后端,由后端注入到 AI 的系统提示词中,从而影响 AI 的回答风格和行为。
## 2. 前端实现说明
### 2.1 用户界面
- 用户可以在设置页面创建、修改、删除多条规则
- 每条规则包含:规则名称 + 规则内容
- 全局开关:启用/禁用所有规则
### 2.2 规则存储
- 存储位置:`C:\Users\{用户名}\.iccoder\rules\`
- 文件格式:每条规则一个独立的 `.md` 文件
- 文件命名:`rule-{时间戳}.md`
- 文件内容格式:
```markdown
# 规则名称
规则内容详细描述...
```
### 2.3 规则传输逻辑
- **开关开启**:所有规则内容合并后通过 `personalRules` 字段传给后端
- **开关关闭**`personalRules` 字段为 `undefined`,不传给后端
## 3. 后端接口变更
### 3.1 DialogRequest 接口新增字段
在现有的 `DialogRequest` 接口中新增 `personalRules` 字段:
```typescript
export interface DialogRequest {
taskId: string;
message: string;
userId: string;
mode: RunMode;
serviceTier?: ServiceTier;
token?: string;
compactedData?: CompactedMemory;
newMessages?: CompactedMessage[];
knowledgeData?: string;
personalRules?: string; // 新增:个人规则内容
}
```
### 3.2 字段说明
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `personalRules` | `string` | 否 | 用户的个人规则内容,多条规则用 `\n\n` 分隔 |
### 3.3 字段示例
**单条规则:**
```json
{
"message": "帮我写一个排序函数",
"personalRules": "始终使用中文回复,代码注释要详细"
}
```
**多条规则(合并后):**
```json
{
"message": "帮我写一个排序函数",
"personalRules": "始终使用中文回复,代码注释要详细\n\n使用 TypeScript 严格模式\n\n遵循项目编码规范"
}
```
**规则关闭:**
```json
{
"message": "帮我写一个排序函数",
"personalRules": undefined
}
```
## 4. 后端处理要求
### 4.1 接收处理
```typescript
// 伪代码示例
function handleDialogRequest(request: DialogRequest) {
const { message, personalRules, ...otherFields } = request;
// 检查是否有个人规则
if (personalRules && personalRules.trim()) {
// 有规则:注入到系统提示词
return processWithRules(message, personalRules, otherFields);
} else {
// 无规则:正常处理
return processNormal(message, otherFields);
}
}
```
### 4.2 规则注入策略
**重要:规则必须注入到系统提示词层,而不是用户消息层**
推荐的注入顺序(优先级从高到低):
1. **平台安全策略**(最高优先级,不可被覆盖)
2. **产品默认系统提示**
3. **用户个人规则** ← 在这里注入
4. **用户输入消息**
### 4.3 注入示例
```typescript
// 伪代码示例
function buildSystemPrompt(personalRules?: string): string {
let systemPrompt = `
你是一个专业的 AI 助手。
遵循以下基本原则:
- 安全第一
- 准确回答
- 友好交流
`;
// 如果有个人规则,追加到系统提示词
if (personalRules && personalRules.trim()) {
systemPrompt += `\n\n用户的个人偏好和规则\n${personalRules}`;
}
return systemPrompt;
}
function processWithRules(
userMessage: string,
personalRules: string,
otherFields: any
) {
const systemPrompt = buildSystemPrompt(personalRules);
// 调用 AI 模型
return callAIModel({
system: systemPrompt,
user: userMessage,
...otherFields
});
}
```
## 5. 注意事项
### 5.1 安全性
- ⚠️ **个人规则不能覆盖平台安全策略**
- ⚠️ **需要对规则内容进行基本的安全检查**
- ⚠️ **防止注入攻击(如提示词注入)**
### 5.2 长度限制
- 前端已限制单条规则内容,但多条规则合并后可能较长
- 建议后端设置总长度上限(如 10000 字符)
- 超限时可以截断或返回错误提示
### 5.3 兼容性
- `personalRules` 字段为可选字段
- 旧版本前端不传此字段时,后端应正常处理(向后兼容)
- 字段为 `undefined` 或空字符串时,视为无规则
### 5.4 日志记录
建议在日志中记录:
- 本次请求是否包含个人规则
- 规则内容的长度(不要记录完整内容,避免隐私泄露)
- 规则注入是否成功
示例日志:
```
[INFO] Dialog request received
- taskId: abc123
- userId: user456
- hasPersonalRules: true
- rulesLength: 156
- rulesInjected: success
```
## 6. 测试建议
### 6.1 功能测试
1. **无规则场景**`personalRules` 为 `undefined`,正常对话
2. **单条规则**:传入一条规则,验证 AI 是否遵循
3. **多条规则**:传入多条规则,验证 AI 是否同时遵循
4. **规则冲突**:传入相互矛盾的规则,观察 AI 行为
5. **超长规则**:传入超长内容,验证截断或错误处理
### 6.2 安全测试
1. **提示词注入**:尝试在规则中注入恶意提示词
2. **覆盖安全策略**:尝试用规则覆盖平台安全限制
3. **特殊字符**:测试规则中包含特殊字符的情况
### 6.3 性能测试
1. **大量规则**:测试 10+ 条规则的性能影响
2. **高频请求**:测试规则注入对响应时间的影响
## 7. 错误处理
### 7.1 可能的错误场景
| 错误场景 | 处理方式 |
|---------|---------|
| 规则内容为空字符串 | 视为无规则,正常处理 |
| 规则内容超长 | 截断或返回错误 |
| 规则包含非法内容 | 过滤或拒绝请求 |
| 规则注入失败 | 降级为无规则对话 |
### 7.2 错误响应示例
```json
{
"error": {
"code": "RULES_TOO_LONG",
"message": "个人规则内容超过长度限制(最大 10000 字符)"
}
}
```
## 8. 验收标准
### 8.1 基本功能
- [ ] 能正确接收 `personalRules` 字段
- [ ] 规则能正确注入到系统提示词
- [ ] 规则关闭时不影响正常对话
- [ ] 多条规则能同时生效
### 8.2 安全性
- [ ] 规则不能覆盖平台安全策略
- [ ] 有基本的内容安全检查
- [ ] 日志中不记录完整规则内容
### 8.3 兼容性
- [ ] 旧版本前端(无此字段)能正常工作
- [ ] 字段为 `undefined` 时正常处理
## 9. 联系方式
如有疑问,请联系前端开发团队。
---
**文档版本**v1.0
**最后更新**2026-03-07

View File

@ -1,379 +0,0 @@
# Webpack 打包优化完整教程
## 目录
1. [优化前的问题](#优化前的问题)
2. [优化方案详解](#优化方案详解)
3. [配置对比](#配置对比)
4. [使用指南](#使用指南)
5. [效果验证](#效果验证)
---
## 优化前的问题
### 原始配置存在的问题
```javascript
// ❌ 问题1固定使用 none 模式
mode: 'none'
// 导致:生产环境代码不压缩,体积大
// ❌ 问题2没有 Tree Shaking
// 导致:未使用的代码也被打包
// ❌ 问题3ts-loader 默认配置
loader: 'ts-loader'
// 导致:每次编译都做类型检查,速度慢
// ❌ 问题4没有性能监控
// 导致:打包体积过大时不知道
```
---
## 优化方案详解
### 1. 自动模式切换
**原理**:根据环境变量自动选择打包模式
```javascript
// 优化前
mode: 'none'
// 优化后
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none'
```
**效果**
- 开发模式:代码可读,方便调试
- 生产模式:自动压缩,体积减小 40-60%
---
### 2. Tree Shaking摇树优化
**原理**:移除未使用的代码
```javascript
optimization: {
minimize: process.env.NODE_ENV === 'production',
usedExports: true // 标记未使用的导出
}
```
**示例**
```javascript
// utils.ts
export function usedFunc() { }
export function unusedFunc() { } // 不会被打包
// main.ts
import { usedFunc } from './utils';
```
**效果**:减少 10-30% 体积
---
### 3. 加快编译速度
**原理**:跳过类型检查,只做转译
```javascript
{
loader: 'ts-loader',
options: {
transpileOnly: true, // 跳过类型检查
compilerOptions: {
sourceMap: true
}
}
}
```
**说明**
- 类型检查交给 IDE 和 CI
- 编译速度提升 50-70%
---
### 4. 自动清理旧文件
```javascript
output: {
clean: true // 每次打包前清空 dist 目录
}
```
**效果**:避免旧文件残留
---
### 5. 性能监控
```javascript
performance: {
hints: 'warning',
maxAssetSize: 2 * 1024 * 1024, // 2MB
maxEntrypointSize: 2 * 1024 * 1024
}
```
**效果**:超过 2MB 会警告
---
### 6. Source Map 优化
```javascript
devtool: process.env.NODE_ENV === 'production'
? 'hidden-source-map' // 生产:隐藏源码
: 'nosources-source-map' // 开发:保留调试信息
```
---
### 7. 模块解析优化
```javascript
resolve: {
extensions: ['.ts', '.js'],
mainFields: ['module', 'main'] // 优先使用 ES 模块
}
```
**效果**:更好的 Tree Shaking 效果
---
## 配置对比
### 优化前
```javascript
const extensionConfig = {
target: 'node',
mode: 'none', // 固定模式
entry: './src/extension.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
// 没有 clean
},
module: {
rules: [{
test: /\.ts$/,
use: [{ loader: 'ts-loader' }] // 默认配置
}]
},
devtool: 'nosources-source-map' // 固定
// 没有 optimization
// 没有 performance
};
```
### 优化后
```javascript
const extensionConfig = {
target: 'node',
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
entry: './src/extension.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2',
clean: true // ✅ 自动清理
},
resolve: {
extensions: ['.ts', '.js'],
mainFields: ['module', 'main'] // ✅ 优化解析
},
module: {
rules: [{
test: /\.ts$/,
use: [{
loader: 'ts-loader',
options: {
transpileOnly: true, // ✅ 加速编译
compilerOptions: { sourceMap: true }
}
}]
}]
},
devtool: process.env.NODE_ENV === 'production'
? 'hidden-source-map'
: 'nosources-source-map',
optimization: {
minimize: process.env.NODE_ENV === 'production',
usedExports: true // ✅ Tree Shaking
},
performance: {
hints: 'warning',
maxAssetSize: 2 * 1024 * 1024,
maxEntrypointSize: 2 * 1024 * 1024
}
};
```
---
## 使用指南
### 开发模式
```bash
# 单次编译
pnpm run compile
# 监听模式(推荐)
pnpm run watch
```
**特点**
- 不压缩代码
- 快速编译
- 保留调试信息
---
### 生产模式
#### Windows
```bash
set NODE_ENV=production && pnpm run package
```
#### macOS/Linux
```bash
NODE_ENV=production pnpm run package
```
**特点**
- 代码压缩
- Tree Shaking
- 隐藏源码
---
### 一键打包 VSIX
```bash
# Windows
set NODE_ENV=production && pnpm run package && npx vsce package
# macOS/Linux
NODE_ENV=production pnpm run package && npx vsce package
```
---
## 效果验证
### 1. 查看打包体积
```bash
# Windows
dir dist\extension.js
# macOS/Linux
ls -lh dist/extension.js
```
### 2. 对比测试
| 模式 | 体积 | 编译时间 | 可读性 |
|------|------|----------|--------|
| 开发模式 | ~800KB | 5s | 高 |
| 生产模式 | ~400KB | 8s | 低(压缩) |
### 3. 性能警告
如果看到这个警告:
```
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (2 MiB).
```
**解决方案**
1. 检查是否引入了不必要的依赖
2. 将大型库添加到 `externals`
3. 考虑代码分割
---
## 常见问题
### Q1: 为什么开发模式不压缩?
**A**: 保持代码可读性,方便调试和查看错误堆栈。
### Q2: transpileOnly 会影响类型安全吗?
**A**: 不会。IDE 和 `tsc --noEmit` 仍会做类型检查。
### Q3: 如何查看 Tree Shaking 效果?
**A**: 使用 `webpack-bundle-analyzer`
```bash
pnpm add -D webpack-bundle-analyzer
```
### Q4: 生产模式编译失败怎么办?
**A**: 先用开发模式确认代码无误,再切换生产模式。
---
## 进阶优化(可选)
### 1. 排除更多依赖
```javascript
externals: {
vscode: 'commonjs vscode',
'node-notifier': 'commonjs node-notifier',
// 如果这些库很大,可以排除
'vcdrom': 'commonjs vcdrom',
'@wavedrom/doppler': 'commonjs @wavedrom/doppler'
}
```
### 2. 代码分割
```javascript
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
}
}
}
}
```
### 3. 缓存优化
```javascript
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true // 监听模式优化
}
}
```
---
## 总结
通过这些优化:
- ✅ 生产体积减少 40-60%
- ✅ 编译速度提升 50-70%
- ✅ 自动清理和监控
- ✅ 更好的开发体验
**推荐工作流**
1. 开发时用 `pnpm run watch`
2. 提交前用 `pnpm run compile` 检查
3. 发布前用生产模式打包

View File

@ -1,87 +0,0 @@
# 文档集功能需求文档
## 1. 功能概述
文档集功能允许用户管理文档,在添加上下文时可以选择文档加载到输入框中,发送给 AI 作为对话上下文。
## 2. 核心流程
### 2.1 添加文档集入口
1. 用户点击"添加上下文"中的"文档"按钮
2. 如果文档集为空,显示"添加文档集"按钮
3. 点击按钮跳转到"设置 → 上下文"页面
### 2.2 创建文档集
1. 在设置的上下文页面,点击"添加文档集"按钮
2. 弹出文档集创建对话框
3. 用户输入文档集名称
4. 用户点击"添加文件"按钮,选择文件
5. 系统验证文件(格式、大小、数量)
6. 显示已选文件列表和统计信息
7. 用户点击"确定",保存文档集
8. 系统持久化文档集信息到 `globalState`
### 2.3 使用文档
1. 用户点击"添加上下文"中的"文档"按钮
2. 显示所有文档列表(自动同步设置中的文档集)
3. 用户点击选择一个或多个文档
4. 文档路径加载到输入框中
5. 用户发送消息,后端读取文档内容作为上下文
### 2.4 管理文档集
1. 在设置的上下文页面查看文档列表
2. 显示每个文档的更新时间
3. 支持修改文档名称
4. 支持删除文档
## 3. 功能详细需求
### 3.1 前端需求
#### 3.1.1 添加上下文 - 文档按钮
**功能**:
- 点击"文档"按钮,显示文档列表弹窗
- 如果没有文档,显示"添加文档集"按钮
- 点击"添加文档集"按钮,跳转到设置的上下文页面
#### 3.1.2 文档列表弹窗
**UI 元素**:
- 文档列表(显示所有文档集中的文档)
- 每个文档显示:名称
- 支持多选
- 确定/取消按钮
**交互逻辑**:
- 自动同步设置中的文档集
- 点击文档选中/取消选中
- 点击确定,将选中文档路径加载到输入框
#### 3.1.3 设置 - 上下文页面
**UI 元素**:
- "添加文档集"按钮
- 文档列表
- 每个文档显示:名称、更新时间、修改名称按钮、删除按钮
**交互逻辑**:
- 点击"添加文档集"打开创建对话框
- 点击修改名称,弹出输入框修改
- 点击删除,删除二次确认弹窗 确认删除文档
#### 3.1.4 文档集创建对话框
**UI 元素**:
- 文档集名称输入框
- 添加文件按钮
- 文件列表显示区域
- 确定/取消按钮
**交互逻辑**:
- 点击"添加文件"触发文件选择器
- 显示已选文件的相对路径和大小
- 支持删除单个文件
- 实时更新统计信息
### 3.2 后端需求
后端只需要支持读取pdf,txt,.v,.sv.md这些类型的文档

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

@ -2,7 +2,7 @@
"name": "iccoder", "name": "iccoder",
"displayName": "IC Coder: Agentic Verilog Platform", "displayName": "IC Coder: Agentic Verilog Platform",
"description": "Agentic Verilog Coding Platform for Real-World FPGAs", "description": "Agentic Verilog Coding Platform for Real-World FPGAs",
"version": "1.13.5", "version": "1.0.12",
"publisher": "ICCoderAgenticVerilogPlatform", "publisher": "ICCoderAgenticVerilogPlatform",
"engines": { "engines": {
"vscode": "^1.80.0" "vscode": "^1.80.0"
@ -117,11 +117,6 @@
"configuration": { "configuration": {
"title": "IC Coder", "title": "IC Coder",
"properties": { "properties": {
"ic-coder.personalRulesEnabled": {
"type": "boolean",
"default": true,
"description": "启用个人规则"
},
"ic-coder.enableSystemNotification": { "ic-coder.enableSystemNotification": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,

View File

@ -34,7 +34,7 @@ export function getCodeHighlightStyles(): string {
border-radius: 6px; border-radius: 6px;
padding: 12px; padding: 12px;
overflow-x: auto; overflow-x: auto;
margin: 52px 0 12px 0; margin: 12px 0;
position: relative; position: relative;
white-space: pre; white-space: pre;
} }

View File

@ -8,7 +8,7 @@ import * as vscode from "vscode";
type Environment = "dev" | "test" | "prod"; type Environment = "dev" | "test" | "prod";
/** 当前环境 - 修改这里切换环境 */ /** 当前环境 - 修改这里切换环境 */
const CURRENT_ENV: Environment = "test"; const CURRENT_ENV: Environment = "prod";
/** 服务等级类型 */ /** 服务等级类型 */
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto"; export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
@ -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,13 +41,13 @@ 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: {
backendUrl: "http://192.168.1.134:2233", backendUrl: "http://192.168.1.108:2029/iccoder",
backendUrlStrongeLoop: "http://192.168.1.134:2233", backendUrlStrongeLoop: "http://192.168.1.108:2029",
loginUrl: "http://192.168.1.134/login", loginUrl: "http://192.168.1.108:2005/login",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
serviceTier: "max", serviceTier: "max",
@ -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,13 +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>`;
/**
* 个人规则的图标svg
*/
export const peopleRules = `<svg t="1772851533961" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11188" width="16" height="16"><path d="M652.8 534.4c70.4-44.8 115.2-124.8 115.2-214.4 0-140.8-115.2-256-256-256s-256 115.2-256 256c0 89.6 44.8 169.6 115.2 214.4C192 592 64 761.6 64 960h64c0-211.2 172.8-384 384-384s384 172.8 384 384h64c0-198.4-128-368-307.2-425.6zM512 512c-105.6 0-192-86.4-192-192s86.4-192 192-192 192 86.4 192 192-86.4 192-192 192z" fill="#6caed4" p-id="11189"></path></svg>`;

View File

@ -2,21 +2,26 @@ 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 { initializeContextHelper } from "./panels/helpers/contextHelper"; 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 插件已激活!");
initializeContextHelper(context); // 加载保存的配置
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({
@ -34,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 {
@ -57,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);
@ -98,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(
@ -195,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",
@ -439,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,248 +0,0 @@
/**
* 上下文管理模块
* 功能:处理文件、文件夹、图片、文档上下文添加
* 依赖vscode, fs, path
* 使用场景:用户添加上下文项时
*/
import * as vscode from "vscode";
let globalContext: vscode.ExtensionContext;
const DOCUMENT_SETS_KEY = "iccoder.documentSets";
export interface DocumentFile {
name: string;
absolutePath: string;
size: number;
}
export interface DocumentSet {
id: string;
name: string;
files: DocumentFile[];
updatedAt: number;
}
export function initializeContextHelper(context: vscode.ExtensionContext) {
globalContext = context;
loadDocumentSets();
}
export async function handleAddContextFile(panel: vscode.WebviewPanel) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
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),
});
}
}
export async function handleAddContextDocumentSet(panel: vscode.WebviewPanel) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showWarningMessage("请先打开一个工作区");
return;
}
const files = await vscode.workspace.findFiles(
"**/*.{md,txt,pdf,doc,docx}",
"**/node_modules/**",
);
panel.webview.postMessage({
command: "showWorkspaceDocumentSetList",
files: files.map((uri) => ({
path: uri.fsPath,
relativePath: vscode.workspace.asRelativePath(uri),
})),
});
}
export async function handleGetDocumentSetList(panel: vscode.WebviewPanel) {
panel.webview.postMessage({
command: "showDocumentSetList",
documents: documentSets,
});
}
let documentSet: DocumentFile[] = [];
export function getDocumentSet() {
return documentSet;
}
export function addToDocumentSet(docs: DocumentFile[]) {
documentSet = [...documentSet, ...docs];
}
export function saveDocumentSet(
docs: DocumentFile[],
name: string,
panel: vscode.WebviewPanel
) {
const newDocSet: DocumentSet = {
id: Date.now().toString(),
name,
files: docs,
updatedAt: Date.now(),
};
documentSets.push(newDocSet);
persistDocumentSets();
panel.webview.postMessage({
command: "documentSetSaved",
documentSets: documentSets,
});
}
let documentSets: DocumentSet[] = [];
function loadDocumentSets() {
const saved =
globalContext.globalState.get<Array<any>>(DOCUMENT_SETS_KEY) ||
globalContext.globalState.get<Array<any>>("documentSets", []);
if (saved) {
documentSets = saved.map((item: any) => ({
id: String(item.id),
name: String(item.name || ""),
files: Array.isArray(item.files)
? item.files.map((file: any) => ({
name: String(file.name || ""),
absolutePath: String(file.absolutePath || file.path || ""),
size: Number(file.size || 0),
}))
: Array.isArray(item.documents)
? item.documents.map((file: any) => ({
name: String(
file.name ||
file.relativePath?.split(/[\\/]/).pop() ||
file.path?.split(/[\\/]/).pop() ||
"",
),
absolutePath: String(file.absolutePath || file.path || ""),
size: Number(file.size || 0),
}))
: [],
updatedAt:
typeof item.updatedAt === "number"
? item.updatedAt
: new Date(item.updatedAt || Date.now()).getTime(),
}));
}
}
function persistDocumentSets() {
globalContext.globalState.update(DOCUMENT_SETS_KEY, documentSets);
}
export function getDocumentSets() {
return documentSets;
}
export function deleteDocumentSet(id: string, panel: vscode.WebviewPanel) {
documentSets = documentSets.filter(ds => ds.id !== id);
persistDocumentSets();
panel.webview.postMessage({
command: "documentSetSaved",
documentSets: documentSets,
});
}
export function changeDocumentSetName(id: string, newName: string, panel: vscode.WebviewPanel) {
const docSet = documentSets.find(ds => ds.id === id);
if (docSet) {
docSet.name = newName;
docSet.updatedAt = Date.now();
persistDocumentSets();
panel.webview.postMessage({
command: "documentSetSaved",
documentSets: documentSets,
});
}
}

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,591 +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 {
loadPersonalRules,
savePersonalRule,
updatePersonalRule,
deletePersonalRule,
updatePersonalRulesEnabled,
} from "../../utils/personalRulesManager";
import { compactDialog } from "../../services/apiClient";
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
import { getCachedUserInfo } from "../../services/userService";
import {
loadConversationHistory,
selectConversation,
} from "./conversationHelper";
import { getVCDFileInfo } from "./vcdHelper";
import {
handleAddContextFile,
handleAddContextFolder,
handleAddContextImage,
handleAddContextDocument,
handleAddContextDocumentSet,
handleGetDocumentSetList,
} 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 "addContextDocumentSet":
await handleAddContextDocumentSet(panel);
break;
case "getDocumentSetList":
await handleGetDocumentSetList(panel);
break;
case "saveDocumentSet":
const { saveDocumentSet } = await import("./contextHelper");
saveDocumentSet(message.documents || [], message.name || "", panel);
break;
case "deleteDocumentSet":
const { deleteDocumentSet } = await import("./contextHelper");
deleteDocumentSet(message.id, panel);
break;
case "changeDocumentSetName":
const { changeDocumentSetName } = await import("./contextHelper");
changeDocumentSetName(message.id, message.newName, panel);
break;
case "openContextSettings":
panel.webview.postMessage({
command: "openSettingsTab",
tab: "context",
});
break;
case "selectFilesForDocset":
const uris = await vscode.window.showOpenDialog({
canSelectMany: true,
canSelectFiles: true,
canSelectFolders: false,
openLabel: "选择文件",
filters: {
: ["md", "txt", "v", "sv", "pdf"],
},
});
if (uris && uris.length > 0) {
const fs = require("fs");
const path = require("path");
const selectedFiles: Array<{
name: string;
absolutePath: string;
size: number;
}> = [];
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
const MAX_TOTAL_SIZE = 50 * 1024 * 1024; // 50 MB
const MAX_FILES = 1000;
let totalSize = 0;
const errors: string[] = [];
for (const uri of uris) {
const filePath = uri.fsPath;
const ext = path.extname(filePath).toLowerCase();
if (![".md", ".txt", ".v", ".sv", ".pdf"].includes(ext)) {
errors.push(`文件 ${path.basename(filePath)} 格式不支持`);
continue;
}
try {
const stat = fs.statSync(filePath);
if (stat.size > MAX_FILE_SIZE) {
errors.push(`文件 ${path.basename(filePath)} 超过 10 MB`);
continue;
}
if (totalSize + stat.size > MAX_TOTAL_SIZE) {
errors.push("文档集总大小超过 50 MB");
break;
}
if (selectedFiles.length >= MAX_FILES) {
errors.push("文件数量超过 1000 个");
break;
}
selectedFiles.push({
name: path.basename(filePath),
absolutePath: filePath,
size: stat.size,
});
totalSize += stat.size;
} catch (err) {
errors.push(`无法读取文件 ${path.basename(filePath)}`);
}
}
panel.webview.postMessage({
command: "filesSelectedForDocset",
files: selectedFiles,
errors: errors.length > 0 ? errors : undefined,
});
}
break;
case "addContextImage":
await handleAddContextImage(panel);
break;
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;
case "loadPersonalRules":
{
const data = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: data,
});
}
break;
case "savePersonalRule":
{
const success = await savePersonalRule(
message.name,
message.content,
message.enabled,
);
if (success) {
const data = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: data,
});
}
}
break;
case "updatePersonalRule":
{
const success = await updatePersonalRule(
message.filename,
message.name,
message.content,
message.enabled,
);
if (success) {
const data = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: data,
});
}
}
break;
case "deletePersonalRule":
{
const success = await deletePersonalRule(message.filename);
if (success) {
const data = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: data,
});
}
}
break;
case "updatePersonalRulesEnabled":
{
const success = await updatePersonalRulesEnabled(message.enabled);
if (success) {
const data = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: data,
});
}
}
break;
case "loadDocumentSets":
const { getDocumentSets } = await import("./contextHelper");
panel.webview.postMessage({
command: "documentSetSaved",
documentSets: getDocumentSets(),
});
break;
}
}

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

@ -28,10 +28,8 @@ import type {
PlanConfirmEvent, PlanConfirmEvent,
} from "../types/api"; } from "../types/api";
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient"; import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
import { getActiveRules } from "../utils/personalRulesManager";
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";
/** /**
* 消息段落类型 * 消息段落类型
@ -130,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 模式确认后继续执行)
@ -445,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) => {
@ -454,6 +455,7 @@ export class DialogSession {
}); });
} }
}); });
*/
throw new Error("登录已过期,请重新登录"); throw new Error("登录已过期,请重新登录");
} }
@ -503,7 +505,6 @@ export class DialogSession {
compactedData: compactedData || undefined, compactedData: compactedData || undefined,
newMessages: newMessages.length > 0 ? newMessages : undefined, newMessages: newMessages.length > 0 ? newMessages : undefined,
knowledgeData: knowledgeData || undefined, knowledgeData: knowledgeData || undefined,
personalRules: getActiveRules() || undefined,
}; };
// 追踪用户消息 // 追踪用户消息
@ -901,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) => {
@ -910,6 +912,7 @@ export class DialogSession {
}); });
} }
}); });
*/
// 登录过期错误已处理,不再传递给外部 // 登录过期错误已处理,不再传递给外部
return; return;
} }
@ -1019,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
@ -1032,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,9 +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] 请求详情:', {
console.log("[SSE] 完整请求体:", request); url: urlString,
console.log("[SSE] 请求体 JSON:", body); 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

@ -48,8 +48,6 @@ export interface DialogRequest {
newMessages?: CompactedMessage[]; newMessages?: CompactedMessage[];
/** 知识图谱数据JSON 字符串,用于恢复知识图谱) */ /** 知识图谱数据JSON 字符串,用于恢复知识图谱) */
knowledgeData?: string; knowledgeData?: string;
/** 个人规则 */
personalRules?: string;
} }
// ============== SSE 事件类型 ============== // ============== SSE 事件类型 ==============

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

@ -19,10 +19,6 @@ 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";
@ -41,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,
@ -66,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 {
@ -112,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 已过期,阻止发送");
// 保存待发送的消息 // 保存待发送的消息
@ -128,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(
"登录已过期,请重新登录", "登录已过期,请重新登录",
"立即登录", "立即登录",
@ -138,6 +138,7 @@ export async function handleUserMessage(
forceReauth: true, forceReauth: true,
}); });
} }
*/
// 恢复输入状态 // 恢复输入状态
panel.webview.postMessage({ panel.webview.postMessage({
@ -189,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 {
@ -226,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",
}); });
// 恢复输入状态 // 恢复输入状态
@ -268,19 +246,8 @@ async function handleUserMessageWithBackend(
let enhancedText = text; let enhancedText = text;
if (contextItems && contextItems.length > 0) { if (contextItems && contextItems.length > 0) {
console.log("[MessageHandler] 处理上下文项:", contextItems.length); console.log("[MessageHandler] 处理上下文项:", contextItems.length);
const docTypes = new Set(["file", "document", "docset"]); const paths = contextItems.map((item) => item.path).join("\n");
const regularPaths = contextItems enhancedText = `${paths}\n\n${text}`;
.filter((item) => !docTypes.has(item.type))
.map((item) => item.path);
const docTags = contextItems
.filter((item) => docTypes.has(item.type))
.map((item) => `<doc>${item.path}</doc>`);
if (regularPaths.length > 0) {
enhancedText = `${regularPaths.join("\n")}\n\n${enhancedText}`;
}
if (docTags.length > 0) {
enhancedText = `${enhancedText}\n\n${docTags.join("\n")}`;
}
} }
// 获取 historyManager 中的 taskId由 ICHelperPanel 创建) // 获取 historyManager 中的 taskId由 ICHelperPanel 创建)
@ -294,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,
@ -318,8 +287,8 @@ async function handleUserMessageWithBackend(
onSegmentUpdate: (segments) => { onSegmentUpdate: (segments) => {
// 过滤掉包含 [调用工具:xxx] 的段落 // 过滤掉包含 [调用工具:xxx] 的段落
const filteredSegments = segments.filter((seg) => { const filteredSegments = segments.filter(seg => {
if (seg.type === "text" && typeof seg.content === "string") { if (seg.type === 'text' && typeof seg.content === 'string') {
return !/\[调用工具:.+?\]/.test(seg.content); return !/\[调用工具:.+?\]/.test(seg.content);
} }
return true; return true;
@ -330,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) => {
@ -379,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 {
// 隐藏状态栏 // 隐藏状态栏
@ -438,7 +435,7 @@ async function handleUserMessageWithBackend(
}); });
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: `❌ 服务繁忙,请稍后重试`, text: `错误: ${message}`,
}); });
// 恢复输入状态 // 恢复输入状态
panel.webview.postMessage({ panel.webview.postMessage({
@ -859,10 +856,10 @@ async function handleFileOperation(
const errorMsg = error instanceof Error ? error.message : "操作失败"; const errorMsg = error instanceof Error ? error.message : "操作失败";
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: `服务繁忙,请稍后重试`, text: `${errorMsg}`,
}); });
vscode.window.showErrorMessage(errorMsg); vscode.window.showErrorMessage(errorMsg);
await historyManager.addAiMessage(`服务繁忙,请稍后重试`); await historyManager.addAiMessage(`${errorMsg}`);
} }
} }

View File

@ -1,194 +0,0 @@
/**
* 个人规则管理工具
* 功能:读写个人规则文件
* 依赖vscode, fs, path
* 使用场景:保存和加载用户的个人规则
*/
import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
/**
* 获取规则目录路径
*/
function getRulesDir(): string {
return path.join(os.homedir(), ".iccoder", "rules");
}
/**
* 确保规则目录存在
*/
function ensureRulesDir(): void {
const dir = getRulesDir();
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* 从文件内容中提取规则名称
*/
function extractRuleName(content: string): string {
const lines = content.split("\n");
const firstLine = lines[0]?.trim();
if (firstLine && firstLine.startsWith("# ")) {
return firstLine.substring(2).trim();
}
return content.substring(0, 30) + (content.length > 30 ? "..." : "");
}
/**
* 保存新规则
*/
export async function savePersonalRule(
name: string,
content: string,
enabled: boolean,
): Promise<boolean> {
try {
ensureRulesDir();
const timestamp = Date.now();
const filename = `rule-${timestamp}.md`;
const filePath = path.join(getRulesDir(), filename);
const fileContent = `# ${name}\n\n${content}`;
fs.writeFileSync(filePath, fileContent, "utf-8");
await vscode.workspace
.getConfiguration("ic-coder")
.update(
"personalRulesEnabled",
enabled,
vscode.ConfigurationTarget.Global,
);
vscode.window.showInformationMessage("规则已保存");
return true;
} catch (error) {
vscode.window.showErrorMessage(`保存规则失败: ${error}`);
return false;
}
}
/**
* 更新规则
*/
export async function updatePersonalRule(
filename: string,
name: string,
content: string,
enabled: boolean,
): Promise<boolean> {
try {
const filePath = path.join(getRulesDir(), filename);
const fileContent = `# ${name}\n\n${content}`;
fs.writeFileSync(filePath, fileContent, "utf-8");
await vscode.workspace
.getConfiguration("ic-coder")
.update(
"personalRulesEnabled",
enabled,
vscode.ConfigurationTarget.Global,
);
vscode.window.showInformationMessage("规则已更新");
return true;
} catch (error) {
vscode.window.showErrorMessage(`更新规则失败: ${error}`);
return false;
}
}
/**
* 删除规则
*/
export async function deletePersonalRule(filename: string): Promise<boolean> {
try {
const filePath = path.join(getRulesDir(), filename);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
vscode.window.showInformationMessage("规则已删除");
return true;
}
return false;
} catch (error) {
vscode.window.showErrorMessage(`删除规则失败: ${error}`);
return false;
}
}
/**
* 加载所有规则
*/
export function loadPersonalRules(): {
rules: Array<{ filename: string; name: string; content: string }>;
enabled: boolean;
} {
const enabled = vscode.workspace
.getConfiguration("ic-coder")
.get<boolean>("personalRulesEnabled", true);
const dir = getRulesDir();
if (!fs.existsSync(dir)) {
return { rules: [], enabled };
}
try {
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
const rules = files.map((filename) => {
const content = fs.readFileSync(path.join(dir, filename), "utf-8");
const lines = content.split("\n");
let name = "";
let actualContent = content;
if (lines[0]?.trim().startsWith("# ")) {
name = lines[0].substring(2).trim();
actualContent = lines.slice(2).join("\n").trim();
} else {
name = extractRuleName(content);
}
return { filename, name, content: actualContent };
});
return { rules, enabled };
} catch (error) {
console.error("读取规则失败:", error);
return { rules: [], enabled };
}
}
/**
* 更新个人规则启用状态
*/
export async function updatePersonalRulesEnabled(
enabled: boolean,
): Promise<boolean> {
try {
await vscode.workspace
.getConfiguration("ic-coder")
.update(
"personalRulesEnabled",
enabled,
vscode.ConfigurationTarget.Global,
);
return true;
} catch (error) {
console.error("更新规则启用状态失败:", error);
return false;
}
}
/**
* 获取当前生效的所有规则内容
*/
export function getActiveRules(): string | null {
const { rules, enabled } = loadPersonalRules();
if (!enabled || rules.length === 0) {
return null;
}
return rules.map((r) => r.content).join("\n\n");
}

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

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

@ -41,15 +41,18 @@ export function getContextButtonContent(): string {
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/> <path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
</svg> </svg>
</div> </div>
<div class="context-menu-item" onclick="handleAddDocumentSet()"> <!-- <div class="context-menu-item" onclick="handleAddImage()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
</svg>
<span>图片</span>
</div> -->
<!-- <div class="context-menu-item" onclick="handleAddDocument()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/> <path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
</svg> </svg>
<span>文档</span> <span>文档</span>
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"> </div> -->
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
</svg>
</div>
</div> </div>
<!-- 文件/文件夹列表视图 --> <!-- 文件/文件夹列表视图 -->
@ -379,48 +382,6 @@ export function getContextButtonScript(): string {
vscode.postMessage({ command: 'addContextFolder' }); vscode.postMessage({ command: 'addContextFolder' });
} }
// 显示文档集列表
function showDocumentSetList() {
vscode.postMessage({ command: 'getDocumentSetList' });
}
// 显示文档集视图
function showDocumentSetView(documents) {
const mainMenu = document.getElementById('contextMenuMain');
const listView = document.getElementById('contextMenuList');
const titleEl = document.getElementById('contextMenuListTitle');
const bodyEl = document.getElementById('contextMenuListBody');
if (mainMenu && listView && titleEl && bodyEl) {
mainMenu.style.display = 'none';
listView.style.display = 'flex';
titleEl.textContent = '文档集';
if (documents.length === 0) {
bodyEl.innerHTML = \`
<div class="context-empty" style="padding: 40px 20px; text-align: center;">
<p style="margin: 0 0 12px 0; color: var(--vscode-descriptionForeground);">暂无文档</p>
<button class="context-add-empty-btn" onclick="addDocumentToSet()" style="padding: 8px 20px; background: transparent; color: var(--vscode-textLink-foreground); border: 1px solid var(--vscode-textLink-foreground); border-radius: 4px; cursor: pointer; font-size: 13px;">添加文档集</button>
</div>
\`;
} else {
currentListType = 'documentSetItem';
currentListData = documents;
filteredListData = documents;
selectedItems.clear();
renderDocumentSetList(documents);
}
}
}
// 添加文档到文档集
function addDocumentToSet() {
vscode.postMessage({ command: 'openContextSettings' });
toggleContextMenu();
}
// 添加文档集项到上下文(已删除,使用统一的确认选择)
// 返回主菜单 // 返回主菜单
function backToMainMenu() { function backToMainMenu() {
const mainMenu = document.getElementById('contextMenuMain'); const mainMenu = document.getElementById('contextMenuMain');
@ -481,43 +442,6 @@ export function getContextButtonScript(): string {
\`).join(''); \`).join('');
} }
// 渲染文档集列表
function renderDocumentSetList(data) {
const body = document.getElementById('contextMenuListBody');
if (!body) return;
filteredListData = data || [];
body.innerHTML = filteredListData.map((item, index) => \`
<div class="context-menu-list-item \${selectedItems.has(item.id) ? 'selected' : ''}" onclick="toggleDocumentSetSelection(\${index})">
<input type="checkbox" id="item-\${index}" \${selectedItems.has(item.id) ? 'checked' : ''} />
<label>\${item.name}</label>
</div>
\`).join('');
}
// 切换文档集选择
function toggleDocumentSetSelection(index) {
const selectedItem = filteredListData[index];
if (!selectedItem) return;
const selectedId = selectedItem.id;
const checkbox = document.getElementById('item-' + index);
const item = document.querySelectorAll('.context-menu-list-item')[index];
if (selectedItems.has(selectedId)) {
selectedItems.delete(selectedId);
if (checkbox) checkbox.checked = false;
if (item) item.classList.remove('selected');
} else {
selectedItems.add(selectedId);
if (checkbox) checkbox.checked = true;
if (item) item.classList.add('selected');
}
updateSelectedCount();
}
// 切换项选择 // 切换项选择
function toggleItemSelection(index) { function toggleItemSelection(index) {
const selectedItem = filteredListData[index]; const selectedItem = filteredListData[index];
@ -551,28 +475,13 @@ export function getContextButtonScript(): string {
// 确认选择 // 确认选择
function confirmSelection() { function confirmSelection() {
try { try {
if (currentListType === 'documentSetItem') {
const selected = currentListData.filter(item => selectedItems.has(item.id));
selected.forEach(docSet => {
(docSet.files || []).forEach(doc => {
addContextItem('docset', doc.absolutePath, doc.name || doc.absolutePath);
});
});
} else {
const selected = currentListData.filter(item => selectedItems.has(item.path)); const selected = currentListData.filter(item => selectedItems.has(item.path));
if (selected.length > 0) { if (selected.length > 0) {
if (currentListType === 'documentSet') {
vscode.postMessage({
command: 'saveDocumentSet',
documents: selected
});
} else {
selected.forEach(item => { selected.forEach(item => {
addContextItem(currentListType, item.path, item.relativePath || item.path); addContextItem(currentListType, item.path, item.relativePath || item.path);
}); });
} }
}
}
} finally { } finally {
const menu = document.getElementById('contextMenu'); const menu = document.getElementById('contextMenu');
const button = document.querySelector('.add-context-button'); const button = document.querySelector('.add-context-button');
@ -598,24 +507,15 @@ export function getContextButtonScript(): string {
toggleContextMenu(); toggleContextMenu();
} }
// 添加文档集
function handleAddDocumentSet() {
showDocumentSetList();
}
// 搜索功能 // 搜索功能
const searchInput = document.getElementById('contextMenuSearch'); const searchInput = document.getElementById('contextMenuSearch');
if (searchInput) { if (searchInput) {
searchInput.addEventListener('input', function(e) { searchInput.addEventListener('input', function(e) {
const keyword = (e.target.value || '').toLowerCase().trim(); const keyword = (e.target.value || '').toLowerCase().trim();
const filtered = currentListData.filter(item => const filtered = currentListData.filter(item =>
(item.name || item.relativePath || item.path || '').toLowerCase().includes(keyword) (item.relativePath || item.path || '').toLowerCase().includes(keyword)
); );
if (currentListType === 'documentSetItem') {
renderDocumentSetList(filtered);
} else {
renderList(filtered); renderList(filtered);
}
}); });
} }
@ -627,10 +527,6 @@ export function getContextButtonScript(): string {
switchToListView('选择文件', 'file', message.files); switchToListView('选择文件', 'file', message.files);
} else if (message.command === 'showWorkspaceFolderList') { } else if (message.command === 'showWorkspaceFolderList') {
switchToListView('选择文件夹', 'folder', message.folders); switchToListView('选择文件夹', 'folder', message.folders);
} else if (message.command === 'showWorkspaceDocumentSetList') {
switchToListView('选择文档', 'documentSet', message.files);
} else if (message.command === 'showDocumentSetList') {
showDocumentSetView(message.documents || []);
} }
}); });
`; `;

View File

@ -181,7 +181,6 @@ export function getContextDisplayScript(): string {
case 'folder': icon = getFolderIcon(); break; case 'folder': icon = getFolderIcon(); break;
case 'image': icon = getImageIcon(); break; case 'image': icon = getImageIcon(); break;
case 'document': icon = getDocumentIcon(); break; case 'document': icon = getDocumentIcon(); break;
case 'docset': icon = getDocumentIcon(); break;
case 'code': icon = getCodeIcon(); break; case 'code': icon = getCodeIcon(); break;
} }

View File

@ -1,160 +0,0 @@
/**
* 上下文设置组件
* 功能:管理文档集
*/
import {
getDocsetDialogContent,
getDocsetDialogStyles,
getDocsetDialogScript,
} from "./docsetDialog";
export function getContextSettingsComponentContent(): string {
return `
<div class="context-settings">
<div class="context-header">
<h3>上下文</h3>
</div>
<div class="context-section">
<div class="context-section-header">
<span>Docs</span>
<button class="context-add-btn" onclick="openAddDocumentSetDialog()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="currentColor"/>
</svg>
添加文档集
</button>
</div>
<div class="context-docs-list" id="contextDocsList">
<div class="context-empty">
<p>暂无文档集</p>
</div>
</div>
</div>
</div>
${getDocsetDialogContent()}
`;
}
export function getContextSettingsComponentStyles(): string {
return `
.context-settings {
padding: 20px;
}
.context-header h3 {
margin: 0 0 20px 0;
font-size: 16px;
font-weight: 600;
}
.context-section {
background: transparent;
border: none;
padding: 0;
}
.context-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vscode-panel-border);
}
.context-section-header span {
font-size: 14px;
font-weight: 500;
}
.context-add-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: transparent;
color: var(--vscode-textLink-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.context-add-btn:hover {
background: var(--vscode-toolbar-hoverBackground);
color: var(--vscode-textLink-activeForeground);
}
.context-add-btn svg {
width: 14px;
height: 14px;
}
.context-docs-list {
min-height: 100px;
}
.context-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--vscode-descriptionForeground);
}
.context-empty p {
margin: 0 0 12px 0;
font-size: 13px;
}
.docset-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
margin-bottom: 8px;
background: var(--vscode-editor-background);
}
.docset-item:hover {
background: var(--vscode-list-hoverBackground);
}
.docset-name {
font-size: 13px;
font-weight: 500;
}
.docset-meta {
font-size: 11px;
color: var(--vscode-descriptionForeground);
flex: 1;
}
.docset-delete-btn {
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 4px;
opacity: 0.6;
border-radius: 4px;
}
.docset-delete-btn:hover {
opacity: 1;
background: var(--vscode-toolbar-hoverBackground);
}
${getDocsetDialogStyles()}
`;
}
export function getContextSettingsComponentScript(): string {
return getDocsetDialogScript();
}

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

@ -1,571 +0,0 @@
/**
* 文档集对话框组件
* 功能:添加文档集的对话框
*/
export function getDocsetDialogContent(): string {
return `
<div class="docset-dialog" id="docsetDialog">
<div class="docset-dialog-overlay" onclick="closeDocsetDialog()"></div>
<div class="docset-dialog-content">
<div class="docset-dialog-header">
<h3>添加文档集</h3>
<button onclick="closeDocsetDialog()">×</button>
</div>
<div class="docset-dialog-body">
<div class="docset-form-group">
<label>名称</label>
<input type="text" id="docsetName" placeholder="输入文档集名称" />
</div>
<div class="docset-form-group">
<label>文件</label>
<div class="docset-hint">支持 .md/.txt/.v/.sv/.pdf单个文件最大 10 MB文档集最大 50 MB最多 1000 个文件</div>
<button class="docset-add-file-btn" id="addFileBtn" onclick="addFileToDocset()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="currentColor"/>
</svg>
添加文件
</button>
<div id="docsetFilesDisplay" style="display: none; margin-top: 8px;">
<div id="docsetFilesList" style="max-height: 200px; overflow-y: auto; border: 1px solid var(--vscode-input-border); border-radius: 4px; padding: 8px;"></div>
<div id="docsetFilesSummary" style="margin-top: 8px; font-size: 12px; color: var(--vscode-descriptionForeground);"></div>
</div>
</div>
</div>
<div class="docset-dialog-footer">
<button class="docset-btn-cancel" onclick="closeDocsetDialog()">取消</button>
<button class="docset-btn-confirm" onclick="confirmDocset()">确定</button>
</div>
</div>
</div>
<div class="delete-confirm-dialog" id="deleteConfirmDialog">
<div class="delete-confirm-content">
<div class="delete-confirm-title">确认删除</div>
<div class="delete-confirm-message" id="deleteConfirmMessage"></div>
<div class="delete-confirm-actions">
<button class="docset-btn-cancel" onclick="closeDeleteConfirm()">取消</button>
<button class="docset-btn-confirm" onclick="confirmDelete()">确定</button>
</div>
</div>
</div>
<div class="rename-dialog" id="renameDialog">
<div class="rename-content">
<div class="rename-title">修改名称</div>
<input type="text" id="renameInput" class="rename-input" placeholder="输入新名称" />
<div class="rename-actions">
<button class="docset-btn-cancel" onclick="closeRenameDialog()">取消</button>
<button class="docset-btn-confirm" onclick="confirmRename()">确定</button>
</div>
</div>
</div>
`;
}
export function getDocsetDialogStyles(): string {
return `
.docset-dialog {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
}
.docset-dialog.active {
display: flex;
align-items: center;
justify-content: center;
}
.docset-dialog-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.docset-dialog-content {
position: relative;
width: 90%;
max-width: 500px;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 8px;
display: flex;
flex-direction: column;
max-height: 80vh;
}
.docset-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--vscode-panel-border);
}
.docset-dialog-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.docset-dialog-header button {
width: 32px;
height: 32px;
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--vscode-foreground);
border-radius: 4px;
}
.docset-dialog-header button:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.docset-dialog-body {
padding: 20px;
overflow-y: auto;
}
.docset-form-group {
margin-bottom: 20px;
}
.docset-form-group label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
}
.docset-hint {
font-size: 11px;
font-weight: 400;
color: var(--vscode-descriptionForeground);
margin-bottom: 8px;
}
.docset-form-group input {
width: 100%;
padding: 8px 12px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
color: var(--vscode-input-foreground);
font-size: 13px;
box-sizing: border-box;
}
.docset-add-file-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: transparent;
color: var(--vscode-textLink-foreground);
border: 1px solid var(--vscode-textLink-foreground);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.docset-add-file-btn:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.docset-add-file-btn svg {
width: 14px;
height: 14px;
}
.docset-dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 20px;
border-top: 1px solid var(--vscode-panel-border);
}
.docset-dialog-footer button {
padding: 6px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.docset-btn-cancel {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}
.docset-btn-cancel:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.docset-btn-confirm {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.docset-btn-confirm:hover {
background: var(--vscode-button-hoverBackground);
}
.docset-delete-btn, .docset-change-btn {
position: relative;
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
color: var(--vscode-foreground);
opacity: 0.6;
}
.docset-delete-btn:hover, .docset-change-btn:hover {
opacity: 1;
}
.docset-delete-btn:hover::after, .docset-change-btn:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--vscode-editorHoverWidget-background);
color: var(--vscode-editorHoverWidget-foreground);
border: 1px solid var(--vscode-editorHoverWidget-border);
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
white-space: nowrap;
margin-bottom: 4px;
z-index: 1000;
}
.delete-confirm-dialog {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 10000;
align-items: center;
justify-content: center;
}
.delete-confirm-dialog.active {
display: flex;
}
.delete-confirm-content {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 20px;
min-width: 300px;
max-width: 400px;
}
.delete-confirm-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
}
.delete-confirm-message {
font-size: 13px;
color: var(--vscode-descriptionForeground);
margin-bottom: 16px;
}
.delete-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.delete-confirm-actions button {
padding: 6px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.rename-dialog {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 10000;
align-items: center;
justify-content: center;
}
.rename-dialog.active {
display: flex;
}
.rename-content {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 20px;
min-width: 300px;
max-width: 400px;
}
.rename-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
}
.rename-input {
width: 100%;
padding: 8px 12px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
color: var(--vscode-input-foreground);
font-size: 13px;
box-sizing: border-box;
margin-bottom: 16px;
}
.rename-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.rename-actions button {
padding: 6px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
`;
}
export function getDocsetDialogScript(): string {
return `
let docsetFiles = [];
function openAddDocumentSetDialog() {
const dialog = document.getElementById('docsetDialog');
if (dialog) {
dialog.classList.add('active');
docsetFiles = [];
document.getElementById('docsetName').value = '';
document.getElementById('addFileBtn').style.display = 'flex';
document.getElementById('docsetFilesDisplay').style.display = 'none';
}
}
function closeDocsetDialog() {
const dialog = document.getElementById('docsetDialog');
if (dialog) {
dialog.classList.remove('active');
}
}
function addFileToDocset() {
vscode.postMessage({ command: 'selectFilesForDocset' });
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function updateDocsetDisplay() {
if (docsetFiles.length === 0) {
document.getElementById('addFileBtn').style.display = 'flex';
document.getElementById('docsetFilesDisplay').style.display = 'none';
return;
}
document.getElementById('addFileBtn').style.display = 'none';
document.getElementById('docsetFilesDisplay').style.display = 'block';
const listEl = document.getElementById('docsetFilesList');
const summaryEl = document.getElementById('docsetFilesSummary');
listEl.innerHTML = docsetFiles.map((file, index) => \`
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 12px; padding: 4px 0; color: var(--vscode-foreground);">
<span>\${file.name || file.absolutePath}</span>
<button onclick="removeDocsetFile(\${index})" style="background: transparent; border: none; color: var(--vscode-foreground); cursor: pointer; padding: 0 4px; opacity: 0.7;">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M10 3h3v1h-1v9l-1 1H4l-1-1V4H2V3h3V2a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1zM9 2H6v1h3V2zM4 13h7V4H4v9zm2-8H5v7h1V5zm1 0h1v7H7V5zm2 0h1v7H9V5z"/>
</svg>
</button>
</div>
\`).join('');
const totalSize = docsetFiles.reduce((sum, f) => sum + (f.size || 0), 0);
summaryEl.textContent = \`已选择 \${docsetFiles.length} 个文件,总大小 \${formatFileSize(totalSize)}\`;
}
function removeDocsetFile(index) {
docsetFiles.splice(index, 1);
updateDocsetDisplay();
}
function confirmDocset() {
const name = document.getElementById('docsetName').value.trim();
if (!name) {
alert('请输入文档集名称');
return;
}
if (docsetFiles.length === 0) {
alert('请添加至少一个文件');
return;
}
vscode.postMessage({
command: 'saveDocumentSet',
name: name,
documents: docsetFiles
});
closeDocsetDialog();
}
function renderDocumentSets(documentSets) {
const listEl = document.getElementById('contextDocsList');
if (!listEl) return;
if (!documentSets || documentSets.length === 0) {
listEl.innerHTML = '<div class="context-empty"><p>暂无文档集</p></div>';
return;
}
listEl.innerHTML = documentSets.map(ds => \`
<div class="docset-item">
<div class="docset-name">\${ds.name}</div>
<div class="docset-meta">更新于 \${new Date(ds.updatedAt).toLocaleString('zh-CN')}</div>
<button class="docset-change-btn" data-tooltip="修改名称" onclick="changeDocsetName('\${ds.id}', '\${ds.name}')">
<svg t="1773883957219" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7170" width="14" height="14">
<path d="M745.76 369.86l-451 537.48a18.693 18.693 0 0 1-8.46 5.74l-136.58 45.27c-13.24 4.39-26.46-6.71-24.43-20.5l20.86-142.36c0.5-3.44 1.95-6.67 4.19-9.33l451-537.48c6.65-7.93 18.47-8.96 26.4-2.31l115.71 97.1c7.92 6.64 8.96 18.46 2.31 26.39zM894.53 192.56l-65.9 78.53c-6.65 7.93-18.47 8.96-26.4 2.31l-115.71-97.1c-7.93-6.65-8.96-18.47-2.31-26.4l65.9-78.53c6.65-7.93 18.47-8.96 26.4-2.31l115.71 97.1c7.93 6.65 8.96 18.47 2.31 26.4z" fill="currentColor" p-id="7171"></path>
</svg>
</button>
<button class="docset-delete-btn" data-tooltip="删除" onclick="showDeleteConfirm('\${ds.id}', '\${ds.name}')">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M10 3h3v1h-1v9l-1 1H4l-1-1V4H2V3h3V2a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1zM9 2H6v1h3V2zM4 13h7V4H4v9zm2-8H5v7h1V5zm1 0h1v7H7V5zm2 0h1v7H9V5z"/>
</svg>
</button>
</div>
\`).join('');
}
let deleteTargetId = null;
let renameTargetId = null;
let renameOriginalName = null;
window.showDeleteConfirm = function(id, name) {
deleteTargetId = id;
document.getElementById('deleteConfirmMessage').textContent = \`确定要删除文档集 "\${name}" 吗?此操作不可恢复。\`;
document.getElementById('deleteConfirmDialog').classList.add('active');
};
window.changeDocsetName = function(id, name) {
renameTargetId = id;
renameOriginalName = name;
document.getElementById('renameInput').value = name;
document.getElementById('renameDialog').classList.add('active');
setTimeout(() => document.getElementById('renameInput').focus(), 100);
};
window.closeDeleteConfirm = function() {
document.getElementById('deleteConfirmDialog').classList.remove('active');
deleteTargetId = null;
};
window.closeRenameDialog = function() {
document.getElementById('renameDialog').classList.remove('active');
renameTargetId = null;
renameOriginalName = null;
};
window.confirmDelete = function() {
if (deleteTargetId) {
vscode.postMessage({ command: 'deleteDocumentSet', id: deleteTargetId });
closeDeleteConfirm();
}
};
window.confirmRename = function() {
const newName = document.getElementById('renameInput').value.trim();
if (!newName) {
alert('请输入名称');
return;
}
if (newName !== renameOriginalName) {
vscode.postMessage({ command: 'changeDocumentSetName', id: renameTargetId, newName: newName });
}
closeRenameDialog();
};
window.addEventListener('message', event => {
const message = event.data;
if (message.command === 'filesSelectedForDocset') {
if (message.errors && message.errors.length > 0) {
alert('部分文件添加失败:\\n' + message.errors.join('\\n'));
}
const MAX_FILE_SIZE = 10 * 1024 * 1024;
const MAX_TOTAL_SIZE = 50 * 1024 * 1024;
const MAX_FILE_COUNT = 1000;
const vaildFiles = [];
const errors = [];
for (const file of message.files) {
if (file.size > MAX_FILE_SIZE) {
errors.push(\`\${file.name || file.absolutePath} 超过单个文件大小限制(\${formatFileSize(MAX_FILE_SIZE)}\`);
} else {
vaildFiles.push(file);
}
}
const newFiles = [...docsetFiles, ...vaildFiles];
const totalSize = newFiles.reduce((sum, f) => sum + (f.size || 0), 0);
if (newFiles.length > MAX_FILE_COUNT) {
errors.push(\`文档数量超过限制最多1000个当前数量\${newFiles.length} 个)\`);
return;
}
if (totalSize > MAX_TOTAL_SIZE) {
errors.push(\`文档集总大小超过 50MB 限制,当前大小为(\${formatFileSize(MAX_TOTAL_SIZE)}\`);
return;
}
if(errors.length > 0) {
alert('以下文件被跳过:\\n' + errors.join('\\n'));
}
docsetFiles = newFiles;
updateDocsetDisplay();
} else if (message.command === 'documentSetSaved') {
renderDocumentSets(message.documentSets);
}
});
`;
}

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: var(--vscode-textBlockQuote-background);
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: var(--vscode-button-hoverBackground);
border-color: var(--vscode-button-hoverBackground);
}
.segment-question .question-option.selected {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border-color: var(--vscode-button-background);
}
.segment-question.answered .question-option:not(.selected) {
opacity: 0.5;
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,67 +1,77 @@
import { peopleRules } from "../constants/toolIcons";
/** /**
* 获取规则设置组件的 HTML 内容 * 获取规则设置组件的 HTML 内容
*/ */
export function getRulesSettingsComponentContent(): string { export function getRulesSettingsComponentContent(): string {
return ` return `
<div class="rules-settings"> <div class="rules-settings">
<div class="rules-header"> <h3 class="settings-section-title">规则设置</h3>
<h3 class="settings-section-title">个人规则</h3>
<button class="add-rule-button" onclick="showAddRuleModal()">+ 创建</button>
</div>
<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">使用自定义规则来控制 AI 行为</span>
</div> </div>
<label class="settings-switch"> <label class="settings-switch">
<input type="checkbox" id="enablePersonalRulesCheckbox" checked> <input type="checkbox" id="enableCustomRulesCheckbox" checked>
<span class="settings-switch-slider"></span> <span class="settings-switch-slider"></span>
</label> </label>
</div> </div>
</div> </div>
<div class="rules-list" id="rulesList"> <div class="settings-section">
<!-- 规则列表将动态插入这里 --> <h4 class="settings-subsection-title">系统规则</h4>
</div> <div class="rules-textarea-container">
<!-- 添加/编辑规则弹窗 -->
<div class="rule-modal" id="ruleModal" style="display: none;">
<div class="rule-modal-content">
<h4 id="modalTitle">创建个人规则</h4>
<input
type="text"
class="rule-name-input"
id="ruleNameInput"
placeholder="规则名称"
/>
<textarea <textarea
class="rule-textarea" class="rules-textarea"
id="ruleTextarea" id="systemRulesTextarea"
placeholder="输入规则内容..." placeholder="在此输入系统规则,例如:&#10;- 始终使用中文回复&#10;- 代码注释要详细&#10;- 遵循项目编码规范"
rows="10" rows="8"
></textarea> ></textarea>
<div class="rule-modal-actions"> <div class="rules-textarea-hint">
<button class="settings-button settings-button-primary" onclick="saveRule()">保存</button> 系统规则会在每次对话开始时应用
<button class="settings-button settings-button-secondary" onclick="closeRuleModal()">取消</button>
</div> </div>
</div> </div>
</div> </div>
<!-- 删除确认弹窗 --> <div class="settings-section">
<div class="rule-modal" id="deleteConfirmModal" style="display: none;"> <h4 class="settings-subsection-title">代码生成规则</h4>
<div class="rule-modal-content" style="width: 400px;"> <div class="rules-textarea-container">
<h4>确认删除</h4> <textarea
<p id="deleteConfirmText" style="color: var(--vscode-foreground); margin: 16px 0;"></p> class="rules-textarea"
<div class="rule-modal-actions"> id="codeRulesTextarea"
<button class="settings-button settings-button-primary" onclick="confirmDelete()">确定</button> placeholder="在此输入代码生成规则,例如:&#10;- 使用 TypeScript 严格模式&#10;- 函数命名使用驼峰命名法&#10;- 添加必要的错误处理"
<button class="settings-button settings-button-secondary" onclick="closeDeleteConfirmModal()">取消</button> rows="8"
></textarea>
<div class="rules-textarea-hint">
这些规则会在生成代码时应用
</div> </div>
</div> </div>
</div> </div>
<div class="settings-section">
<h4 class="settings-subsection-title">Verilog 规则</h4>
<div class="rules-textarea-container">
<textarea
class="rules-textarea"
id="verilogRulesTextarea"
placeholder="在此输入 Verilog 代码规则,例如:&#10;- 使用非阻塞赋值 (<=) 在时序逻辑中&#10;- 模块命名使用小写加下划线&#10;- 添加详细的端口注释"
rows="8"
></textarea>
<div class="rules-textarea-hint">
这些规则会在生成 Verilog 代码时应用
</div>
</div>
</div>
<div class="settings-actions">
<button class="settings-button settings-button-primary" onclick="saveRulesSettings()">
保存规则
</button>
<button class="settings-button settings-button-secondary" onclick="resetRulesSettings()">
重置为默认
</button>
</div>
</div> </div>
`; `;
} }
@ -75,144 +85,11 @@ export function getRulesSettingsComponentStyles(): string {
max-width: 700px; max-width: 700px;
} }
.rules-header { .rules-textarea-container {
display: flex; margin-top: 8px;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
} }
.add-rule-button { .rules-textarea {
padding: 6px 12px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.add-rule-button:hover {
background: var(--vscode-button-hoverBackground);
}
.rules-list {
margin-top: 16px;
}
.rule-item {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
padding: 12px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.rule-item-name {
color: var(--vscode-foreground);
font-size: 13px;
}
.rule-item > div:first-child svg {
background-color: rgba(148, 204, 241, 0.3);
border-radius: 4px;
padding: 4px;
}
.rule-item-menu {
position: relative;
}
.rule-menu-icon {
width: 20px;
height: 20px;
cursor: pointer;
padding: 4px;
border-radius: 3px;
}
.rule-menu-icon:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.rule-dropdown {
position: absolute;
right: 0;
top: 28px;
background: var(--vscode-menu-background);
border: 1px solid var(--vscode-menu-border);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 100;
min-width: 100px;
}
.rule-dropdown button {
display: block;
width: 100%;
padding: 8px 12px;
background: transparent;
color: var(--vscode-menu-foreground);
border: none;
text-align: left;
cursor: pointer;
font-size: 13px;
}
.rule-dropdown button:hover {
background: var(--vscode-menu-selectionBackground);
color: var(--vscode-menu-selectionForeground);
}
.rule-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.rule-modal-content {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
padding: 20px;
width: 500px;
max-width: 90%;
}
.rule-modal-content h4 {
margin: 0 0 16px 0;
color: var(--vscode-foreground);
}
.rule-name-input {
width: 100%;
padding: 8px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 13px;
margin-bottom: 12px;
box-sizing: border-box;
}
.rule-name-input:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.rule-textarea {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
background: var(--vscode-input-background); background: var(--vscode-input-background);
@ -221,20 +98,26 @@ export function getRulesSettingsComponentStyles(): string {
border-radius: 4px; border-radius: 4px;
font-size: 13px; font-size: 13px;
font-family: var(--vscode-editor-font-family); font-family: var(--vscode-editor-font-family);
line-height: 1.5;
resize: vertical; resize: vertical;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
} }
.rule-textarea:focus { .rules-textarea:focus {
border-color: var(--vscode-focusBorder); border-color: var(--vscode-focusBorder);
} }
.rule-modal-actions { .rules-textarea::placeholder {
display: flex; color: var(--vscode-input-placeholderForeground);
gap: 8px; opacity: 0.6;
margin-top: 16px; }
justify-content: flex-end;
.rules-textarea-hint {
margin-top: 8px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
font-style: italic;
} }
`; `;
} }
@ -244,169 +127,51 @@ export function getRulesSettingsComponentStyles(): string {
*/ */
export function getRulesSettingsComponentScript(): string { export function getRulesSettingsComponentScript(): string {
return ` return `
let currentRules = []; // 保存规则设置
let editingRule = null; function saveRulesSettings() {
let deletingFilename = null; const settings = {
enableCustomRules: document.getElementById('enableCustomRulesCheckbox').checked,
systemRules: document.getElementById('systemRulesTextarea').value,
codeRules: document.getElementById('codeRulesTextarea').value,
verilogRules: document.getElementById('verilogRulesTextarea').value,
};
// 显示添加规则弹窗 // 发送消息到扩展
function showAddRuleModal() {
editingRule = null;
document.getElementById('modalTitle').textContent = '创建个人规则';
document.getElementById('ruleNameInput').value = '';
document.getElementById('ruleTextarea').value = '';
document.getElementById('ruleModal').style.display = 'flex';
}
// 关闭弹窗
function closeRuleModal() {
document.getElementById('ruleModal').style.display = 'none';
closeAllDropdowns();
}
// 切换下拉菜单
function toggleDropdown(filename, event) {
event.stopPropagation();
closeAllDropdowns();
const dropdown = document.getElementById('dropdown-' + filename);
if (dropdown) {
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
}
}
// 关闭所有下拉菜单
function closeAllDropdowns() {
document.querySelectorAll('.rule-dropdown').forEach(d => d.style.display = 'none');
}
// 点击页面其他地方关闭下拉菜单
document.addEventListener('click', closeAllDropdowns);
// 编辑规则
function editRule(filename) {
const rule = currentRules.find(r => r.filename === filename);
if (rule) {
editingRule = rule;
document.getElementById('modalTitle').textContent = '修改个人规则';
document.getElementById('ruleNameInput').value = rule.name;
document.getElementById('ruleTextarea').value = rule.content;
document.getElementById('ruleModal').style.display = 'flex';
}
}
// 保存规则
function saveRule() {
const name = document.getElementById('ruleNameInput').value.trim();
const content = document.getElementById('ruleTextarea').value.trim();
if (!name) {
alert('规则名称不能为空');
return;
}
if (!content) {
alert('规则内容不能为空');
return;
}
const enabled = document.getElementById('enablePersonalRulesCheckbox').checked;
if (editingRule) {
vscode.postMessage({ vscode.postMessage({
command: 'updatePersonalRule', command: 'saveRulesSettings',
filename: editingRule.filename, settings: settings
name: name,
content: content,
enabled: enabled
});
} else {
vscode.postMessage({
command: 'savePersonalRule',
name: name,
content: content,
enabled: enabled
});
}
closeRuleModal();
}
// 删除规则
function deleteRule(filename) {
closeAllDropdowns();
const rule = currentRules.find(r => r.filename === filename);
const ruleName = rule ? rule.name : filename;
deletingFilename = filename;
document.getElementById('deleteConfirmText').textContent = '确定要删除规则"' + ruleName + '"吗?此操作无法撤销。';
document.getElementById('deleteConfirmModal').style.display = 'flex';
}
// 关闭删除确认弹窗
function closeDeleteConfirmModal() {
document.getElementById('deleteConfirmModal').style.display = 'none';
deletingFilename = null;
}
// 确认删除
function confirmDelete() {
if (deletingFilename) {
vscode.postMessage({
command: 'deletePersonalRule',
filename: deletingFilename
});
}
closeDeleteConfirmModal();
}
// 渲染规则列表
function renderRulesList(rules) {
currentRules = rules || [];
const listEl = document.getElementById('rulesList');
if (currentRules.length === 0) {
listEl.innerHTML = '<div style="color: var(--vscode-descriptionForeground); padding: 16px; text-align: center;">暂无规则,点击"+ 创建"添加</div>';
return;
}
const peopleRulesIcon = '${peopleRules}';
listEl.innerHTML = currentRules.map(rule => \`
<div class="rule-item">
<div style="display: flex; align-items: center; gap: 8px;">
\${peopleRulesIcon}
<div class="rule-item-name">\${rule.filename}</div>
</div>
<div class="rule-item-menu">
<svg class="rule-menu-icon" onclick="toggleDropdown('\${rule.filename}', event)" viewBox="0 0 16 16" fill="currentColor">
<circle cx="8" cy="3" r="1.5"/>
<circle cx="8" cy="8" r="1.5"/>
<circle cx="8" cy="13" r="1.5"/>
</svg>
<div class="rule-dropdown" id="dropdown-\${rule.filename}" style="display: none;">
<button onclick="editRule('\${rule.filename}')">编辑</button>
<button onclick="deleteRule('\${rule.filename}')">删除</button>
</div>
</div>
</div>
\`).join('');
}
// 加载规则列表
function loadPersonalRules(data) {
if (data && data.enabled !== undefined) {
document.getElementById('enablePersonalRulesCheckbox').checked = data.enabled;
}
if (data && data.rules) {
renderRulesList(data.rules);
}
}
//监听启用个人规则开关变化
document.getElementById('enablePersonalRulesCheckbox').addEventListener('change', function() {
const enabled = this.checked;
vscode.postMessage({ command: 'updatePersonalRulesEnabled', enabled: enabled });
}); });
// 页面加载时请求规则数据 // 显示保存成功提示
vscode.postMessage({ command: 'loadPersonalRules' }); console.log('规则设置已保存', settings);
}
// 重置规则设置
function resetRulesSettings() {
document.getElementById('enableCustomRulesCheckbox').checked = true;
document.getElementById('systemRulesTextarea').value = '';
document.getElementById('codeRulesTextarea').value = '';
document.getElementById('verilogRulesTextarea').value = '';
console.log('规则设置已重置为默认值');
}
// 加载规则设置
function loadRulesSettings(settings) {
if (!settings) return;
if (settings.enableCustomRules !== undefined) {
document.getElementById('enableCustomRulesCheckbox').checked = settings.enableCustomRules;
}
if (settings.systemRules) {
document.getElementById('systemRulesTextarea').value = settings.systemRules;
}
if (settings.codeRules) {
document.getElementById('codeRulesTextarea').value = settings.codeRules;
}
if (settings.verilogRules) {
document.getElementById('verilogRulesTextarea').value = settings.verilogRules;
}
}
`; `;
} }

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,16 +3,6 @@ import {
getGeneralSettingsComponentStyles, getGeneralSettingsComponentStyles,
getGeneralSettingsComponentScript, getGeneralSettingsComponentScript,
} from "./generalSettingsComponent"; } from "./generalSettingsComponent";
import {
getRulesSettingsComponentContent,
getRulesSettingsComponentStyles,
getRulesSettingsComponentScript,
} from "./rulesSettingsComponent";
import {
getContextSettingsComponentContent,
getContextSettingsComponentStyles,
getContextSettingsComponentScript,
} from "./contextSettingsComponent";
/** /**
* 获取设置面板的 HTML 内容 * 获取设置面板的 HTML 内容
@ -36,24 +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>
<button class="settings-nav-item" data-tab="context" onclick="switchSettingsTab('context')">
上下文
</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 class="settings-tab-content" id="contextSettings">
${getContextSettingsComponentContent()}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -197,8 +176,6 @@ export function getSettingsComponentStyles(): string {
} }
${getGeneralSettingsComponentStyles()} ${getGeneralSettingsComponentStyles()}
${getRulesSettingsComponentStyles()}
${getContextSettingsComponentStyles()}
`; `;
} }
@ -208,14 +185,14 @@ export function getSettingsComponentStyles(): string {
export function getSettingsComponentScript(): string { export function getSettingsComponentScript(): string {
return ` return `
${getGeneralSettingsComponentScript()} ${getGeneralSettingsComponentScript()}
${getRulesSettingsComponentScript()}
${getContextSettingsComponentScript()}
// 打开设置面板 // 打开设置面板
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' });
} }
} }
@ -248,21 +225,7 @@ export function getSettingsComponentScript(): string {
content.classList.remove('active'); content.classList.remove('active');
} }
}); });
// 切换到上下文标签页时加载文档集列表
if (tabName === 'context') {
vscode.postMessage({ command: 'loadDocumentSets' });
} }
}
// 监听打开设置标签页的消息
window.addEventListener('message', event => {
const message = event.data;
if (message.command === 'openSettingsTab') {
openSettingsModal();
switchSettingsTab(message.tab);
}
});
// 阻止点击模态框内容时关闭 // 阻止点击模态框内容时关闭
document.addEventListener('click', (event) => { document.addEventListener('click', (event) => {

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;
@ -737,13 +741,6 @@ export function getWebviewContent(
} }
break; break;
case 'personalRulesLoaded':
// 加载个人规则数据
if (typeof loadPersonalRules === 'function') {
loadPersonalRules(message.data);
}
break;
case 'autoSendMessage': case 'autoSendMessage':
// 自动发送待发送的消息(登录后) // 自动发送待发送的消息(登录后)
console.log('[WebView] 自动发送待发送消息:', message.text); console.log('[WebView] 自动发送待发送消息:', message.text);
@ -782,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':
@ -971,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);
} }
@ -978,6 +948,7 @@ export function getWebviewContent(
${getMessageAreaScript()} ${getMessageAreaScript()}
${getAgentCardScript()} ${getAgentCardScript()}
${getMoreOptionsComponentScript()}
${getWaveformPreviewScript()} ${getWaveformPreviewScript()}
${getConversationHistoryBarScript()} ${getConversationHistoryBarScript()}
${getProgressBarScript()} ${getProgressBarScript()}