50 Commits

Author SHA1 Message Date
ea19dfcbe6 fix: 修复 waveform_trace 工具执行失败和类型错误
- 修复 waveform_trace 工具因 stderr 输出导致的误判失败
   - 修复 messageHandler onQuestion 回调的类型签名错误
2026-03-05 17:25:29 +08:00
fa55e32153 feat: 支持 AskUserQuestion 多问题和多选功能
- 新增 QuestionItem 类型支持单个问题配置(question/options/multiSelect)
   - AskUserEvent 改为 questions 数组支持多问题
   - AnswerRequest 新增 answers 字段支持多问题答案提交
   - 前端渲染支持单选按钮(radio)和多选复选框(checkbox)
   - 答案格式:{\"0\": [\"选项1\"], \"1\": [\"选项A\", \"选项B\"]}
   - 保持向后兼容旧的单问题格式
2026-03-05 16:58:59 +08:00
f6b1f5c45a 1.0.11 2026-03-05 10:39:30 +08:00
1f9a1822c9 fix: 修复打包后图片资源无法加载的问题
- 配置 webpack copy-webpack-plugin 将 src/assets 复制到 dist/assets
- 更新所有图片引用路径从 src/assets 改为 dist/assets
- 修改 localResourceRoots 配置以允许访问 dist/assets
2026-03-05 10:38:40 +08:00
63015c6bbc fix:排除 PUBLISH.md,避免敏感信息被打包 2026-03-05 09:34:44 +08:00
24b30df992 fix: 排除 docs 目录避免中文文件名打包冲突 2026-03-05 09:24:27 +08:00
67b1003831 style:将宁德时代欢迎弹窗换成全企业的 2026-03-04 22:19:14 +08:00
00d37bdaf0 1.0.9 2026-03-04 21:08:08 +08:00
c5fcb1427e Update:README.md 2026-03-04 21:07:50 +08:00
9118ebd662 feat:企业欢迎弹窗优化 2026-03-04 18:58:18 +08:00
19cdf47bed style: 将工具折叠图标颜色从蓝色改为灰色
- 修改 toolIcons.ts 中的 SVG 填充色为 #8a8a8a
   - 清理 messageArea.ts 中冗余的 CSS 样式规则
2026-03-04 16:46:40 +08:00
95bac94479 fix:修改代码变更继续对话查找不到之前的代码变更信息的bug 2026-03-04 16:17:56 +08:00
421a8934a7 chore: 优化打包配置,排除重复的 exe 文件
- 添加 .vscodeignore 排除 tools/waveform_trace/src/dist
   - 移除 package.json 的 files 字段
   - 减小 .vsix 打包体积
2026-03-04 15:11:46 +08:00
f7f45668d3 style: 统一使用蓝色主题色
- 压缩图标改为蓝色 #007ACC
- 问题选项按钮改为蓝色背景,悬停深蓝色
- 按钮、进度条等组件统一使用蓝色主题
- 添加 CSS 强制规则确保图标在所有主题下显示蓝色
2026-03-04 14:51:36 +08:00
c8e9a5b897 fix: 修复继续对话时消息覆盖问题并添加波形追踪工具
- 修复继续对话时 AI 消息被覆盖的问题
- 用户发送新消息时重置分段消息容器
- 继续对话时复用未完成的消息容器
- 添加 waveform_trace.exe 工具到仓库
- 更新 .gitignore 规则
2026-03-04 11:43:45 +08:00
a1bfa62796 feat:Update CHANGE.md 2026-03-03 20:21:37 +08:00
64e11cbc3c 1.0.8 2026-03-03 20:19:46 +08:00
15445aa13c fix: 修复继续对话时消息覆盖问题
- 对话完成时正确重置 currentSegmentedMessage
- 继续对话时创建新的消息容器
- 删除调试日志代码
2026-03-03 20:04:41 +08:00
52834047f2 feat: 每次登录都显示试用用户欢迎弹窗
- 移除 hasWelcomed 标记,不再记录是否已显示
- 试用用户每次打开聊天面板都会看到欢迎弹窗
2026-03-03 19:19:37 +08:00
76817675f1 fix: 修复试用用户欢迎弹窗不显示的问题
- 修复 userService 中 null 值未正确赋值的问题
- 优化欢迎弹窗判断逻辑:null=长期有效,undefined=无效
- 添加测试命令 resetWelcomeModal 用于清除弹窗标记
2026-03-03 19:06:02 +08:00
2cce8f94c9 fix: 修复试用用户无过期时间时欢迎弹窗不显示的问题
- 优化欢迎弹窗判断逻辑,支持无过期时间的长期试用用户
   - 只有在有过期时间且已过期时才不显示欢迎弹窗
   - 改进日志输出,更清晰地显示判断流
2026-03-03 18:48:30 +08:00
9b5f102d9f feat: 完善企业试用用户欢迎弹窗逻辑
- 添加试用到期时间检查

- 仅在试用未过期且有到期时间时显示欢迎弹窗

- 试用过期或无到期时间则显示邀请码弹窗

- 修复窗口重载后弹窗标记丢失问题
2026-03-03 18:26:27 +08:00
68de33165e fix:修复企业试用用户仍弹出邀请码的问题 2026-03-03 18:03:43 +08:00
f56ad33366 feat:实现删除文件确认功能 2026-03-03 17:08:59 +08:00
35c63802b5 feat:添加文件路径标签显示和rules需求文档 2026-03-03 16:45:23 +08:00
3458f6fe23 fix:解决登录过期点击重新登录失败的bug 2026-03-03 14:19:00 +08:00
8f305033f7 release: 1.0.7
- 修复 AI 响应内容重复显示问题
2026-03-02 19:37:16 +08:00
4a18f1c418 1.0.7 2026-03-02 19:29:03 +08:00
373edb6d80 fix: 修复 AI 响应内容重复显示问题
- 完成标记不再重复发送 segments,避免内容在前端重复显示
   - 移除调试日志
2026-03-02 19:25:25 +08:00
1c66e0e599 feat:更新CHANGELOG 2026-03-02 17:46:09 +08:00
536e7720cb 1.0.6 2026-03-02 17:36:40 +08:00
75eac4b1ce feat:删除文件确认功能实现文档 2026-03-02 17:36:20 +08:00
9ed0afee6b feat:解决添加上下文搜索选择文件不匹配的问题 2026-03-02 15:43:33 +08:00
f700473967 fix: clear expired auth state before relogin 2026-03-02 14:51:11 +08:00
5fc0fd2a95 feat:企业用户弹窗逻辑
- 通过getuserinfo获取企业参数判断是否是企业用户
2026-03-02 14:02:19 +08:00
5f88c7ceac feat: 优化代码变更面板样式和交互
- 优化变更面板和 diff 视图样式
   - 新增全部采纳和全部拒绝按钮
   - 修复删除文件的变更追踪和采纳逻辑
   - 整个标题栏可点击展开/收起
   - 增强视觉效果和用户体验
2026-03-02 10:37:45 +08:00
4c7ec65577 feat:代码变更diff可视化功能实现 2026-03-02 10:00:04 +08:00
3e18299099 1.0.5 2026-02-26 21:47:19 +08:00
021be88880 feat:更新README.md 2026-02-26 21:45:52 +08:00
a479e81682 style: 优化界面样式和用户体验
- 调整工具调用显示的间距和字体大小
   - 优化低调工具调用的视觉效果
   - 改进整体界面的可读性
2026-02-26 21:44:58 +08:00
c3e3012a94 fix: 发送消息后清空上下文文件列表
修复了发送消息后上下文文件仍然显示在输入框中的问题。

   - 在 sendMessage() 函数中添加 clearContextItems() 调用
   - 调整脚本加载顺序,确保 contextDisplay 在 contextButton 之前初始化
2026-02-26 19:05:49 +08:00
c9e9df3825 feat:修改对话中的样式 + 欢迎宁德弹窗内容 2026-02-26 17:27:23 +08:00
7ca2fa1bcc feat:宁德时代的欢迎弹窗 2026-02-26 16:31:16 +08:00
208c24682b feat: 实现试用用户欢迎引导和过期检测功能
- 新增试用用户首次登录欢迎弹窗,展示使用教程
- 新增试用期过期检测服务和过期提醒弹窗
- 从 JWT token 中提取 ispluginTrial 标识判断用户类型
- 试用用户跳过邀请码验证流程
- 在消息发送前检查试用期是否过期
- 新增 ExpiredPanel 和 WelcomePanel 面板组件
- 新增 expiredModal 和 welcomeModal 视图组件
- 优化用户登录流程,根据用户类型显示不同引导
2026-02-26 15:42:18 +08:00
316c784bde Merge branch 'feat/backend' into feat/front-end 2026-02-25 10:15:50 +08:00
1467ae8a89 feat:资源点使用实时更新 2026-02-25 10:14:00 +08:00
0ea3afbe70 feat: 更新发布流程文档,优化编译和打包步骤;新增成功图标SVG并在消息区域中使用 2026-02-23 14:31:40 +08:00
4f1d7f495a feat: 更新Webview视图提供者,优化HTML内容生成和通知服务逻辑 2026-01-28 21:38:49 +08:00
7c4ecb013e 1.0.4 2026-01-28 20:34:38 +08:00
ed5976a22c feat: 更新版本号至1.0.4,完善插件描述及主要功能列表 2026-01-28 20:19:43 +08:00
56 changed files with 6235 additions and 657 deletions

3
.gitignore vendored
View File

@ -4,8 +4,7 @@ node_modules
.vscode-test/ .vscode-test/
*.vsix *.vsix
# waveform_trace 打包产物exe 太大,通过 Release 发布) # waveform_trace 打包产物
tools/waveform_trace/bin/
tools/waveform_trace/src/build/ tools/waveform_trace/src/build/
tools/waveform_trace/src/dist/ tools/waveform_trace/src/dist/
tools/waveform_trace/src/*.spec tools/waveform_trace/src/*.spec

4
.npmrc
View File

@ -1 +1,3 @@
enable-pre-post-scripts = true enable-pre-post-scripts = true
shamefully-hoist = true
public-hoist-pattern[] = *

28
.vscodeignore Normal file
View File

@ -0,0 +1,28 @@
# 开发文件
.vscode/**
.vscode-test/**
src/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
# 测试文件
out/test/**
# 依赖
node_modules/**
# 文档(避免中文文件名打包问题)
docs/**
PUBLISH.md
# 只排除 waveform_trace 的 src/dist 目录
tools/waveform_trace/src/dist/**
# Git 相关
.git/**
.github/**

View File

@ -2,22 +2,61 @@
所有重要的项目变更都将记录在此文件中。 所有重要的项目变更都将记录在此文件中。
## [1.0.2] - 2026-01-13 ## [1.0.9] - 2026-03-04
IC Coder插件端正式发布。 ### 优化
IC Coder 插件端是一个专为 FPGA 开发设计的 VS Code 扩展,提供 AI 驱动的智能辅助功能。 - 将工具折叠图标颜色从蓝色改为灰色
- 统一使用蓝色主题色
- 优化打包配置,排除重复的 exe 文件
### 修复
- 修复代码变更继续对话查找不到之前的代码变更信息的 bug
- 修复对话展示两遍的问题
## [1.0.8] - 2026-03-03
### 新增
- 删除文件确认功能
- 文件路径标签显示
- 企业试用用户欢迎弹窗优化
### 修复
- 修复继续对话时消息覆盖问题
- 修复试用用户欢迎弹窗显示逻辑
- 修复企业试用用户仍弹出邀请码的问题
- 修复登录过期点击重新登录失败的问题
## [1.0.7] - 2026-03-02
### 修复
- 修复 AI 响应内容重复显示问题
## [1.0.6] - 2026-03-02
### 新增
- Git Diff 功能:支持查看当前文件的 Git 差异对比
### 修复
- 修复添加上下文搜索选择文件不匹配的问题
- 修复过期认证状态未清除导致重新登录失败的问题
## [1.0.4] - 2026-01-28
IC Coder插件端正式上线。
IC Coder 插件端是一个是一个自主式人工智能 Verilog 编码平台可以将芯片设计与验证的效率提升至少20倍
主要功能: 主要功能:
- VCD波形解析
- 自动生成完整工程 - 自动搭建电路架构:够根据自然语言描述的设计需求,自动生成完整的电路架构
- 自动仿真 - AI自主仿真IC Coder提供完全自动化的仿真验证流程无需手动编写测试代码
- 自主代码迭代 - AI自主代码迭代:实现了真正的自主式开发循环,能够持续优化代码直到满足设计要求
- 智能匹配最优模型 - 随时可掌控提供透明化的开发过程让用户始终掌握AI的工作状态
-线程任务处理 -层次安全保障:将数据安全和隐私保护作为核心设计原则,提供企业级的安全保障
- 实时跟随
- 丰富的上下文工具
- 全双工交互
- 多层次安全保障
- 自动搭建电路架构
- 多平台支持

View File

@ -88,6 +88,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
5. 点击 **Create** 完成创建 5. 点击 **Create** 完成创建
**注意事项:** **注意事项:**
- Publisher ID 一旦创建无法修改 - Publisher ID 一旦创建无法修改
- Publisher ID 必须全局唯一 - Publisher ID 必须全局唯一
- 建议使用有意义且专业的 ID - 建议使用有意义且专业的 ID
@ -126,6 +127,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
## [0.0.2] - 2025-12-29 ## [0.0.2] - 2025-12-29
### 新增 ### 新增
- 添加发送和暂停按钮功能 - 添加发送和暂停按钮功能
- 添加一键优化按钮组件 - 添加一键优化按钮组件
- 添加 Plan 开关组件 - 添加 Plan 开关组件
@ -133,11 +135,13 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
- 添加上下文压缩功能 - 添加上下文压缩功能
### 改进 ### 改进
- 优化用户界面交互体验 - 优化用户界面交互体验
## [0.0.1] - 2025-12-XX ## [0.0.1] - 2025-12-XX
### 新增 ### 新增
- 初始版本发布 - 初始版本发布
- Verilog 代码智能生成 - Verilog 代码智能生成
- 集成 iverilog 仿真工具 - 集成 iverilog 仿真工具
@ -161,6 +165,7 @@ in the Software without restriction...
### 4. 优化 README.md ### 4. 优化 README.md
确保 README 包含: 确保 README 包含:
- 清晰的功能介绍 - 清晰的功能介绍
- 使用截图或 GIF 演示 - 使用截图或 GIF 演示
- 详细的使用说明 - 详细的使用说明
@ -219,6 +224,7 @@ pnpm vsce publish
**步骤:** **步骤:**
1. 本地打包插件: 1. 本地打包插件:
```bash ```bash
pnpm run package pnpm run package
pnpm vsce package[pnpm vsce package --no-dependencies] pnpm vsce package[pnpm vsce package --no-dependencies]
@ -257,7 +263,7 @@ pnpm vsce publish major
```bash ```bash
# 发布指定版本 # 发布指定版本
pnpm vsce publish 0.0.3 npx vsce publish --packagePath iccoder-1.0.7.vsix
``` ```
### 更新流程建议 ### 更新流程建议
@ -268,8 +274,6 @@ pnpm vsce publish 0.0.3
4. 执行发布命令 4. 执行发布命令
5. 验证市场上的插件是否正常 5. 验证市场上的插件是否正常
## 更新流程 ## 更新流程
1. 修改版本号 1. 修改版本号
@ -281,10 +285,10 @@ pnpm vsce publish 0.0.3
```bash ```bash
#补丁版本 1.0.0 -> 1.0.1) #补丁版本 1.0.0 -> 1.0.1)
pnpm version patch pnpm version patch
#次要版本 (1.0.0 -> 1.1.0) #次要版本 (1.0.0 -> 1.1.0)
pnpm version minor pnpm version minor
#主要版本 (1.0.0 -> 2.0.0) #主要版本 (1.0.0 -> 2.0.0)
pnpm version major pnpm version major
``` ```
@ -292,17 +296,17 @@ pnpm vsce publish 0.0.3
2. 打包 2. 打包
```bash ```bash
#先build #先编译
pnpm run compile
#中间build
pnpm run build pnpm run build
#后打包成.vsix #后打包成.vsix
pnpm vsce package --no-dependencies pnpm vsce package --no-dependencies
``` ```
3. 手动上传/命令上传 3. 手动上传/命令上传
- https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update - https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update
- 命令上传vsce publish - 命令上传vsce publish
@ -315,6 +319,7 @@ pnpm vsce publish 0.0.3
**原因:** PAT Token 无效或过期 **原因:** PAT Token 无效或过期
**解决方案:** **解决方案:**
- 重新生成 PAT Token - 重新生成 PAT Token
- 重新登录:`pnpm vsce login ic-coder-team` - 重新登录:`pnpm vsce login ic-coder-team`
@ -323,6 +328,7 @@ pnpm vsce publish 0.0.3
**原因:** Publisher ID 不存在或不匹配 **原因:** Publisher ID 不存在或不匹配
**解决方案:** **解决方案:**
- 检查 `package.json` 中的 `publisher` 字段 - 检查 `package.json` 中的 `publisher` 字段
- 确认已在市场创建对应的 Publisher - 确认已在市场创建对应的 Publisher
@ -331,17 +337,20 @@ pnpm vsce publish 0.0.3
**原因:** 必需文件缺失 **原因:** 必需文件缺失
**解决方案:** **解决方案:**
- 确保 `dist/` 目录存在且包含编译后的代码 - 确保 `dist/` 目录存在且包含编译后的代码
- 运行 `pnpm run package` 重新构建 - 运行 `pnpm run package` 重新构建
### 4. 插件审核被拒 ### 4. 插件审核被拒
**常见原因:** **常见原因:**
- 插件名称或描述违反市场规则 - 插件名称或描述违反市场规则
- 图标不符合要求(建议 128x128 PNG - 图标不符合要求(建议 128x128 PNG
- README 内容不完整 - README 内容不完整
**解决方案:** **解决方案:**
- 查看审核反馈邮件 - 查看审核反馈邮件
- 修改相关内容后重新发布 - 修改相关内容后重新发布
@ -363,6 +372,7 @@ code --install-extension ic-coder-plugin-0.0.2.vsix
``` ```
或者在 VS Code 中: 或者在 VS Code 中:
1. 打开扩展面板 1. 打开扩展面板
2. 点击 `...` 菜单 2. 点击 `...` 菜单
3. 选择 **Install from VSIX...** 3. 选择 **Install from VSIX...**

View File

@ -12,7 +12,6 @@ IC Coder是一款**The Agentic AI Verilog Coding Platform自主式人工智
- **多智能体架构Multi-Agent System**多个专业化AI智能体协同工作分别负责架构设计、代码生成、验证测试等不同环节 - **多智能体架构Multi-Agent System**多个专业化AI智能体协同工作分别负责架构设计、代码生成、验证测试等不同环节
- **增强上下文引擎**:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性 - **增强上下文引擎**:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性
- **自研EDA工具集**完整的仿真、综合、时序分析工具链无缝集成到AI工作流中
这些技术共同支撑着从需求分析、架构设计、代码生成到验证调试的全流程智能化开发体验。 这些技术共同支撑着从需求分析、架构设计、代码生成到验证调试的全流程智能化开发体验。

View File

@ -0,0 +1,261 @@
# AskUserQuestion 多选支持 - API 设计文档
## 问题描述
当前 AI 询问用户问题时存在以下问题:
1. 后端返回的选项不准确
2. 多个问题只给几个选项
3. 不支持多选方式
## 需求
实现一个问题对应多个选项,支持多选的方式。
## 数据结构设计
### 后端返回格式
后端通过 SSE 的 `ask_user` 事件返回以下格式:
```json
{
"askId": "ask_1234567890",
"questions": [
{
"question": "请确认 SPI 控制器的配置需求:工作模式?",
"options": [
"Master/8位/模式0/固定分频/需要CS",
"Master/可配置位宽/可配置模式/需要CS",
"Slave模式"
],
"multiSelect": false
},
{
"question": "数据位宽?",
"options": [
"8位 还是其他?"
],
"multiSelect": false
},
{
"question": "时钟极性和相位?",
"options": [
"CPOL=0/CPHA=0 (模式0) 还是其他模式?"
],
"multiSelect": false
},
{
"question": "时钟分频?",
"options": [
"需要可配置的分频比吗?"
],
"multiSelect": false
},
{
"question": "是否需要芯片选信号 (CS) 控制?",
"options": [
"是",
"否"
],
"multiSelect": false
}
]
}
```
### 前端数据结构
#### 1. API 类型定义 (`src/types/api.ts`)
```typescript
/** ask_user 事件数据 */
export interface AskUserEvent {
askId: string;
questions: QuestionItem[];
}
/** 单个问题项 */
export interface QuestionItem {
question: string;
options: string[];
multiSelect?: boolean; // 是否支持多选,默认 false
}
```
#### 2. MessageSegment 类型 (`src/services/dialogService.ts`)
```typescript
export interface MessageSegment {
type: "text" | "tool" | "question" | "agent" | "plan" | "progress";
// ... 其他字段
askId?: string;
questions?: QuestionItem[]; // 改为问题数组
}
```
#### 3. 用户回答格式 (`src/types/api.ts`)
```typescript
export interface AnswerRequest {
taskId: string;
askId: string;
answers: {
[questionIndex: number]: string[]; // 每个问题的答案数组(支持多选)
};
}
```
## 前端实现要点
### 1. 显示多个问题
```typescript
// 遍历 questions 数组,为每个问题生成 UI
segment.questions?.forEach((q, index) => {
// 显示问题标题
// 显示选项(单选或多选)
// 收集答案
});
```
### 2. 多选支持
```typescript
if (q.multiSelect) {
// 渲染复选框
// 允许选择多个选项
} else {
// 渲染单选按钮
// 只允许选择一个选项
}
```
### 3. 提交答案
```typescript
const answers = {
0: ["Master/8位/模式0/固定分频/需要CS"], // 第1个问题的答案
1: ["8位 还是其他?"], // 第2个问题的答案
2: ["CPOL=0/CPHA=0 (模式0) 还是其他模式?"], // 第3个问题的答案
// ...
};
vscode.postMessage({
command: 'userAnswer',
askId: 'ask_1234567890',
answers: answers
});
```
## 后端需要做的修改
### 1. 修改 AskUserQuestion 工具的返回格式
从:
```json
{
"askId": "xxx",
"question": "单个问题",
"options": ["选项1", "选项2"]
}
```
改为:
```json
{
"askId": "xxx",
"questions": [
{
"question": "问题1",
"options": ["选项1", "选项2"],
"multiSelect": false
},
{
"question": "问题2",
"options": ["选项A", "选项B", "选项C"],
"multiSelect": true
}
]
}
```
### 2. 接收答案的格式
从:
```json
{
"taskId": "xxx",
"askId": "xxx",
"selected": ["选项1"],
"customInput": "自定义输入"
}
```
改为:
```json
{
"taskId": "xxx",
"askId": "xxx",
"answers": {
"0": ["选项1"], // 第1个问题的答案
"1": ["选项A", "选项B"] // 第2个问题的答案多选
}
}
```
## 示例场景
### 场景SPI 控制器配置
**后端发送:**
```json
{
"askId": "ask_spi_config",
"questions": [
{
"question": "工作模式?",
"options": [
"Master/8位/模式0/固定分频/需要CS",
"Master/可配置位宽/可配置模式/需要CS",
"Slave模式"
],
"multiSelect": false
},
{
"question": "需要哪些功能?",
"options": [
"可配置时钟分频",
"可配置数据位宽",
"支持多个CS",
"DMA支持"
],
"multiSelect": true
}
]
}
```
**用户选择:**
- 问题1选择 "Master/8位/模式0/固定分频/需要CS"
- 问题2选择 "可配置时钟分频" 和 "可配置数据位宽"
**前端提交:**
```json
{
"taskId": "task_xxx",
"askId": "ask_spi_config",
"answers": {
"0": ["Master/8位/模式0/固定分频/需要CS"],
"1": ["可配置时钟分频", "可配置数据位宽"]
}
}
```
## 总结
这个设计方案:
1. ✅ 支持多个问题
2. ✅ 每个问题有多个选项
3. ✅ 支持单选和多选
4. ✅ 数据结构清晰,易于扩展
5. ✅ 向后兼容(可以只有一个问题)

View File

@ -0,0 +1,804 @@
# VS Code Extension API 核心知识点
## 目录
- [1. Extension 生命周期](#1-extension-生命周期) ⭐⭐⭐
- [2. 激活事件 (Activation Events)](#2-激活事件-activation-events) ⭐⭐
- [3. 命令系统 (Commands)](#3-命令系统-commands) ⭐⭐
- [4. Webview API](#4-webview-api) ⭐⭐⭐⭐⭐ **面试重点**
- [5. TreeView 和自定义视图](#5-treeview-和自定义视图) ⭐⭐
- [6. 文件系统操作](#6-文件系统操作) ⭐⭐⭐
- [7. 配置和存储](#7-配置和存储) ⭐⭐⭐⭐ **面试重点**
- [8. 消息通知](#8-消息通知) ⭐
- [9. 语言特性支持](#9-语言特性支持) ⭐
- [10. 调试和诊断](#10-调试和诊断) ⭐
---
## 1. Extension 生命周期 ⭐⭐⭐
### 1.1 核心函数 🔥必考
```typescript
// extension.ts
import * as vscode from 'vscode';
// 插件激活时调用(只调用一次)
export function activate(context: vscode.ExtensionContext) {
console.log('Extension is now active!');
// 注册命令、视图、事件监听等
// 使用 context.subscriptions 管理资源
}
// 插件停用时调用(清理资源)
export function deactivate() {
console.log('Extension is deactivated');
// 清理资源、关闭连接等
}
```
### 1.2 ExtensionContext 重要属性 🔥必考
```typescript
interface ExtensionContext {
// 插件订阅管理(自动清理)
subscriptions: { dispose(): any }[];
// 工作区存储路径
storageUri: vscode.Uri | undefined;
globalStorageUri: vscode.Uri;
// 插件路径
extensionUri: vscode.Uri;
extensionPath: string;
// 状态存储
workspaceState: Memento; // 工作区级别
globalState: Memento; // 全局级别
secrets: SecretStorage; // 敏感信息存储
// 环境变量
environmentVariableCollection: EnvironmentVariableCollection;
}
```
### 1.3 资源管理最佳实践 🔥必考
```typescript
export function activate(context: vscode.ExtensionContext) {
// ✅ 推荐:使用 context.subscriptions 自动管理
context.subscriptions.push(
vscode.commands.registerCommand('extension.command', () => {})
);
// ❌ 不推荐:手动管理容易忘记清理
const disposable = vscode.commands.registerCommand('extension.command', () => {});
// 需要在 deactivate 中手动调用 disposable.dispose()
}
```
---
## 2. 激活事件 (Activation Events) ⭐⭐
### 2.1 常用激活事件 📌重要
```json
// package.json
{
"activationEvents": [
// 启动时激活
"onStartupFinished",
// 执行命令时激活
"onCommand:extension.helloWorld",
// 打开特定语言文件时激活
"onLanguage:javascript",
"onLanguage:verilog",
// 打开特定文件类型时激活
"onFileSystem:sftp",
// 打开特定视图时激活
"onView:myCustomView",
// 调试时激活
"onDebug",
// 打开特定 URI 时激活
"onUri",
// Webview 恢复时激活
"onWebviewPanel:myWebview",
// 任务执行时激活
"onTaskType:npm"
]
}
```
### 2.2 延迟激活策略 🔥必考
```typescript
// ✅ 推荐:使用 onStartupFinished 延迟激活
"activationEvents": ["onStartupFinished"]
// ❌ 不推荐:使用 * 会拖慢启动速度
"activationEvents": ["*"]
```
---
## 3. 命令系统 (Commands)
### 3.1 注册命令
```typescript
// 注册简单命令
const disposable = vscode.commands.registerCommand(
'extension.helloWorld',
() => {
vscode.window.showInformationMessage('Hello World!');
}
);
context.subscriptions.push(disposable);
// 注册带参数的命令
vscode.commands.registerCommand(
'extension.openFile',
(filePath: string) => {
vscode.workspace.openTextDocument(filePath).then(doc => {
vscode.window.showTextDocument(doc);
});
}
);
```
### 3.2 执行命令
```typescript
// 执行内置命令
await vscode.commands.executeCommand('workbench.action.files.save');
// 执行自定义命令
await vscode.commands.executeCommand('extension.openFile', '/path/to/file');
// 获取所有可用命令
const commands = await vscode.commands.getCommands();
```
### 3.3 常用内置命令
```typescript
// 文件操作
'workbench.action.files.save'
'workbench.action.files.saveAll'
'workbench.action.closeActiveEditor'
// 编辑器操作
'editor.action.formatDocument'
'editor.action.commentLine'
'editor.action.selectAll'
// 窗口操作
'workbench.action.toggleSidebarVisibility'
'workbench.action.terminal.new'
'workbench.action.quickOpen'
// Git 操作
'git.commit'
'git.push'
'git.pull'
```
---
## 4. Webview API ⭐⭐⭐⭐⭐ **面试重点**
### 4.1 创建 Webview Panel 🔥必考
```typescript
const panel = vscode.window.createWebviewPanel(
'myWebview', // viewType唯一标识
'My Webview', // 标题
vscode.ViewColumn.One, // 显示位置
{
enableScripts: true, // 启用 JavaScript
retainContextWhenHidden: true, // 隐藏时保留状态
localResourceRoots: [ // 允许访问的本地资源路径
vscode.Uri.joinPath(context.extensionUri, 'media')
]
}
);
```
### 4.2 设置 Webview 内容
```typescript
panel.webview.html = getWebviewContent();
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Webview</title>
</head>
<body>
<h1>Hello from Webview!</h1>
<button onclick="sendMessage()">Send Message</button>
<script>
const vscode = acquireVsCodeApi();
function sendMessage() {
vscode.postMessage({
command: 'alert',
text: 'Hello from Webview!'
});
}
// 接收来自 Extension 的消息
window.addEventListener('message', event => {
const message = event.data;
console.log('Received:', message);
});
</script>
</body>
</html>`;
}
```
### 4.3 Webview 消息通信 🔥必考(项目核心)
```typescript
// Extension → Webview
panel.webview.postMessage({
command: 'update',
data: 'some data'
});
// Webview → Extension
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showInformationMessage(message.text);
break;
case 'getData':
// 处理数据请求
panel.webview.postMessage({
command: 'dataResponse',
data: fetchData()
});
break;
}
},
undefined,
context.subscriptions
);
```
### 4.4 Webview 生命周期管理 📌重要
```typescript
// 监听 Webview 关闭事件
panel.onDidDispose(
() => {
// 清理资源
console.log('Webview disposed');
},
null,
context.subscriptions
);
// 监听 Webview 可见性变化
panel.onDidChangeViewState(
e => {
if (e.webviewPanel.visible) {
console.log('Webview is now visible');
}
},
null,
context.subscriptions
);
```
### 4.5 加载本地资源 📌重要
```typescript
// 获取本地资源 URI
const scriptUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'media', 'script.js')
);
const styleUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'media', 'style.css')
);
// 在 HTML 中使用
const html = `
<link href="${styleUri}" rel="stylesheet">
<script src="${scriptUri}"></script>
`;
```
### 4.6 Webview 状态持久化 📌重要
```typescript
// Webview 中保存状态
const vscode = acquireVsCodeApi();
const state = vscode.getState() || { count: 0 };
// 更新状态
state.count++;
vscode.setState(state);
// Extension 中序列化状态
panel.webview.options = {
enableScripts: true,
retainContextWhenHidden: true
};
// 恢复 Webview
vscode.window.registerWebviewPanelSerializer('myWebview', {
async deserializeWebviewPanel(webviewPanel, state) {
webviewPanel.webview.html = getWebviewContent();
// 恢复状态
webviewPanel.webview.postMessage({ command: 'restore', state });
}
});
```
---
## 5. TreeView 和自定义视图
### 5.1 创建 TreeView Provider
```typescript
class MyTreeDataProvider implements vscode.TreeDataProvider<TreeItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
refresh(): void {
this._onDidChangeTreeData.fire(undefined);
}
getTreeItem(element: TreeItem): vscode.TreeItem {
return element;
}
getChildren(element?: TreeItem): Thenable<TreeItem[]> {
if (!element) {
// 返回根节点
return Promise.resolve([
new TreeItem('Item 1', vscode.TreeItemCollapsibleState.None),
new TreeItem('Item 2', vscode.TreeItemCollapsibleState.Collapsed)
]);
}
// 返回子节点
return Promise.resolve([]);
}
}
class TreeItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState
) {
super(label, collapsibleState);
this.tooltip = `Tooltip for ${label}`;
this.command = {
command: 'extension.itemClicked',
title: 'Click Item',
arguments: [this]
};
}
}
```
### 5.2 注册 TreeView
```typescript
const treeDataProvider = new MyTreeDataProvider();
const treeView = vscode.window.createTreeView('myTreeView', {
treeDataProvider,
showCollapseAll: true
});
context.subscriptions.push(treeView);
// 刷新视图
treeDataProvider.refresh();
```
### 5.3 WebviewView Provider侧边栏 Webview
```typescript
class MyWebviewViewProvider implements vscode.WebviewViewProvider {
resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
token: vscode.CancellationToken
) {
webviewView.webview.options = {
enableScripts: true
};
webviewView.webview.html = getWebviewContent();
webviewView.webview.onDidReceiveMessage(message => {
// 处理消息
});
}
}
// 注册
vscode.window.registerWebviewViewProvider(
'myWebviewView',
new MyWebviewViewProvider()
);
```
---
## 6. 文件系统操作 ⭐⭐⭐
### 6.1 读取文件 📌重要
```typescript
// 读取文本文件
const uri = vscode.Uri.file('/path/to/file.txt');
const content = await vscode.workspace.fs.readFile(uri);
const text = Buffer.from(content).toString('utf8');
// 使用 TextDocument API
const document = await vscode.workspace.openTextDocument(uri);
const text = document.getText();
```
### 6.2 写入文件
```typescript
// 写入文件
const uri = vscode.Uri.file('/path/to/file.txt');
const content = Buffer.from('Hello World', 'utf8');
await vscode.workspace.fs.writeFile(uri, content);
// 使用 WorkspaceEdit
const edit = new vscode.WorkspaceEdit();
edit.createFile(uri, { overwrite: true });
edit.insert(uri, new vscode.Position(0, 0), 'Hello World');
await vscode.workspace.applyEdit(edit);
```
### 6.3 文件监听
```typescript
// 监听文件变化
const watcher = vscode.workspace.createFileSystemWatcher('**/*.js');
watcher.onDidCreate(uri => {
console.log('File created:', uri.fsPath);
});
watcher.onDidChange(uri => {
console.log('File changed:', uri.fsPath);
});
watcher.onDidDelete(uri => {
console.log('File deleted:', uri.fsPath);
});
context.subscriptions.push(watcher);
```
### 6.4 工作区操作
```typescript
// 获取工作区文件夹
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders) {
const rootPath = workspaceFolders[0].uri.fsPath;
}
// 查找文件
const files = await vscode.workspace.findFiles(
'**/*.ts', // include pattern
'**/node_modules/**' // exclude pattern
);
// 打开文件
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
```
---
## 7. 配置和存储 ⭐⭐⭐⭐ **面试重点**
### 7.1 读取配置 📌重要
```typescript
// 读取配置
const config = vscode.workspace.getConfiguration('myExtension');
const value = config.get<string>('settingName', 'defaultValue');
// 监听配置变化
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('myExtension.settingName')) {
console.log('Configuration changed');
}
});
```
### 7.2 更新配置
```typescript
const config = vscode.workspace.getConfiguration('myExtension');
// 更新用户配置(全局)
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Global);
// 更新工作区配置
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Workspace);
```
### 7.3 状态存储 🔥必考
```typescript
// 工作区状态(仅当前工作区)
await context.workspaceState.update('key', 'value');
const value = context.workspaceState.get('key');
// 全局状态(跨工作区)
await context.globalState.update('key', 'value');
const value = context.globalState.get('key');
// 存储对象
await context.globalState.update('userData', { name: 'John', age: 30 });
```
### 7.4 敏感信息存储 🔥必考Token 管理)
```typescript
// 存储密码、Token 等敏感信息
await context.secrets.store('apiToken', 'secret-token-value');
// 读取
const token = await context.secrets.get('apiToken');
// 删除
await context.secrets.delete('apiToken');
// 监听变化
context.secrets.onDidChange(e => {
console.log('Secret changed:', e.key);
});
```
---
## 8. 消息通知
### 8.1 信息提示
```typescript
// 普通信息
vscode.window.showInformationMessage('Operation completed!');
// 警告
vscode.window.showWarningMessage('This action may cause issues');
// 错误
vscode.window.showErrorMessage('Operation failed!');
```
### 8.2 带按钮的提示
```typescript
const result = await vscode.window.showInformationMessage(
'Do you want to continue?',
'Yes',
'No',
'Cancel'
);
if (result === 'Yes') {
// 用户点击了 Yes
}
```
### 8.3 输入框
```typescript
// 简单输入
const input = await vscode.window.showInputBox({
prompt: 'Enter your name',
placeHolder: 'John Doe',
validateInput: (value) => {
return value.length < 3 ? 'Name too short' : null;
}
});
// 快速选择
const selected = await vscode.window.showQuickPick(
['Option 1', 'Option 2', 'Option 3'],
{
placeHolder: 'Select an option',
canPickMany: false
}
);
```
### 8.4 进度提示
```typescript
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Processing...',
cancellable: true
},
async (progress, token) => {
token.onCancellationRequested(() => {
console.log('User canceled');
});
progress.report({ increment: 0, message: 'Starting...' });
await doWork1();
progress.report({ increment: 50, message: 'Half done...' });
await doWork2();
progress.report({ increment: 100, message: 'Complete!' });
}
);
```
---
## 9. 语言特性支持
### 9.1 代码补全
```typescript
const provider = vscode.languages.registerCompletionItemProvider(
'javascript',
{
provideCompletionItems(document, position) {
const item = new vscode.CompletionItem('myFunction');
item.kind = vscode.CompletionItemKind.Function;
item.detail = 'My custom function';
item.documentation = 'This is a custom function';
item.insertText = new vscode.SnippetString('myFunction($1)$0');
return [item];
}
},
'.' // 触发字符
);
context.subscriptions.push(provider);
```
### 9.2 悬停提示
```typescript
const provider = vscode.languages.registerHoverProvider('javascript', {
provideHover(document, position) {
const range = document.getWordRangeAtPosition(position);
const word = document.getText(range);
return new vscode.Hover([
`**${word}**`,
'This is a hover tooltip'
]);
}
});
```
### 9.3 诊断(错误提示)
```typescript
const diagnosticCollection = vscode.languages.createDiagnosticCollection('myExtension');
context.subscriptions.push(diagnosticCollection);
function updateDiagnostics(document: vscode.TextDocument) {
const diagnostics: vscode.Diagnostic[] = [];
const text = document.getText();
const regex = /TODO/g;
let match;
while ((match = regex.exec(text))) {
const range = new vscode.Range(
document.positionAt(match.index),
document.positionAt(match.index + match[0].length)
);
const diagnostic = new vscode.Diagnostic(
range,
'TODO found',
vscode.DiagnosticSeverity.Warning
);
diagnostics.push(diagnostic);
}
diagnosticCollection.set(document.uri, diagnostics);
}
```
---
## 10. 调试和诊断
### 10.1 输出通道
```typescript
const outputChannel = vscode.window.createOutputChannel('My Extension');
context.subscriptions.push(outputChannel);
outputChannel.appendLine('Extension activated');
outputChannel.show(); // 显示输出面板
```
### 10.2 日志记录
```typescript
// 使用 LogOutputChannel带时间戳
const logger = vscode.window.createOutputChannel('My Extension', { log: true });
logger.trace('Trace message');
logger.debug('Debug message');
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message');
```
### 10.3 错误处理
```typescript
try {
await riskyOperation();
} catch (error) {
if (error instanceof Error) {
vscode.window.showErrorMessage(`Error: ${error.message}`);
logger.error(error.stack || error.message);
}
}
```
---
## 最佳实践总结
### ✅ 推荐做法
1. **资源管理**:所有 disposable 对象都放入 `context.subscriptions`
2. **延迟激活**:使用 `onStartupFinished` 而不是 `*`
3. **异步操作**:使用 `async/await` 处理异步操作
4. **错误处理**:捕获异常并给用户友好提示
5. **类型安全**:充分利用 TypeScript 类型系统
6. **状态持久化**:使用 `globalState`/`workspaceState` 保存状态
7. **敏感信息**:使用 `secrets` API 存储 Token、密码等
### ❌ 避免做法
1. 不要在 `activate` 中执行耗时操作
2. 不要忘记清理资源监听器、Webview 等)
3. 不要在 Webview 中直接访问文件系统
4. 不要在配置中存储敏感信息
5. 不要阻塞主线程(使用 Worker 或异步操作)
---
## 参考资源
- [VS Code Extension API 官方文档](https://code.visualstudio.com/api)
- [Extension Samples](https://github.com/microsoft/vscode-extension-samples)
- [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)

View File

@ -0,0 +1,45 @@
# 代码变更审查功能
## 功能概述
AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以查看所有修改并选择采纳或拒绝。
## 核心文件
### 1. 数据结构
- `src/types/fileChanges.ts` - 变更数据类型定义
### 2. 服务层
- `src/services/changeTracker.ts` - 变更追踪服务(单例)
- `trackChange()` - 记录文件变更
- `acceptChange()` - 采纳变更(保存文件)
- `rejectChange()` - 拒绝变更(恢复旧内容)
### 3. UI 组件
- `src/views/changePanel.ts` - 变更面板 UI
- `src/utils/diffRenderer.ts` - Diff 可视化渲染
### 4. 集成点
- `src/utils/messageHandler.ts` - 消息处理
- `trackFileChange()` - 记录变更
- `handleAcceptChange()` - 处理采纳
- `handleRejectChange()` - 处理拒绝
- `sendChangesToWebview()` - 发送变更到前端
- `src/services/toolExecutor.ts` - 工具执行器
-`executeFileWrite()` 中记录变更
## 使用流程
1. **开始对话** - 调用 `startChangeSession(sessionId)`
2. **修改文件** - 自动调用 `trackFileChange()`
3. **对话结束** - 调用 `sendChangesToWebview()` 显示变更面板
4. **用户操作** - 点击采纳/拒绝按钮
5. **处理结果** - 保存或恢复文件内容
## 待完成工作
1. 在 ICHelperPanel 中集成消息处理(监听 acceptChange/rejectChange 命令)
2. 在对话结束时调用 `sendChangesToWebview()`
3. 在 Webview 中实现变更列表的动态渲染
4. 处理前端的采纳/拒绝响应

View File

@ -0,0 +1,294 @@
# 删除文件确认功能实现文档
## 1. 功能概述
在 AI 返回删除文件命令时,前端拦截并弹出确认对话框,用户确认后才执行删除操作。
## 2. 架构设计
### 2.1 消息流程
```
AI 后端 → 删除文件工具调用 → 前端拦截 → 用户确认对话框
确定/取消
执行删除/返回取消结果
返回 TOOL_EXECUTION_RESULT
AI 后端
```
### 2.2 关键原则
**前端必须返回结果**:无论用户选择什么,前端都必须向后端返回 `TOOL_EXECUTION_RESULT`,否则后端会等待超时。
## 3. 实现方案
### 3.1 修改位置
文件:`src/utils/messageHandler.ts`
在处理工具调用的函数中,找到删除文件的工具处理逻辑。
### 3.2 核心代码实现
```typescript
/**
* 处理删除文件工具调用(带用户确认)
*/
async function handleDeleteFileTool(
toolCall: any,
panel: vscode.WebviewPanel
): Promise<ToolExecutionResult> {
const filePath = toolCall.arguments.filePath; // 根据实际参数名调整
// 弹出确认对话框
const confirmed = await vscode.window.showWarningMessage(
`确定要删除文件吗?\n\n${filePath}`,
{
modal: true, // 模态对话框,阻止其他操作
detail: '此操作不可撤销'
},
'确定删除',
'取消'
);
// 用户确认删除
if (confirmed === '确定删除') {
try {
// 执行删除操作
const uri = vscode.Uri.file(filePath);
await vscode.workspace.fs.delete(uri, {
recursive: false, // 如果是目录需要设置为 true
useTrash: true // 移到回收站而非永久删除(推荐)
});
// 返回成功结果
return {
type: 'TOOL_EXECUTION_RESULT',
toolCallId: toolCall.id,
result: JSON.stringify({
success: true,
message: `文件已删除: ${filePath}`
})
};
} catch (error) {
// 删除失败
return {
type: 'TOOL_EXECUTION_RESULT',
toolCallId: toolCall.id,
result: JSON.stringify({
success: false,
error: `删除失败: ${error.message}`
})
};
}
}
// 用户取消或关闭对话框
return {
type: 'TOOL_EXECUTION_RESULT',
toolCallId: toolCall.id,
result: JSON.stringify({
success: false,
message: '用户取消了删除操作'
})
};
}
```
### 3.3 集成到消息处理流程
`messageHandler.ts` 的工具调用处理逻辑中:
```typescript
// 示例:在处理工具调用的地方
async function handleToolCall(toolCall: any, panel: vscode.WebviewPanel) {
switch (toolCall.name) {
case 'deleteFile': // 根据实际工具名称调整
return await handleDeleteFileTool(toolCall, panel);
case 'deleteDirectory': // 如果有删除目录的工具
return await handleDeleteDirectoryTool(toolCall, panel);
// ... 其他工具
}
}
```
## 4. 用户体验优化
### 4.1 对话框样式
```typescript
const confirmed = await vscode.window.showWarningMessage(
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
{
modal: true,
detail: '⚠️ 文件将被移到回收站,可以恢复'
},
'确定删除',
'取消'
);
```
### 4.2 批量删除优化
如果 AI 一次返回多个删除操作:
```typescript
// 方案 1逐个确认
for (const file of filesToDelete) {
await handleDeleteFileTool(file, panel);
}
// 方案 2批量确认推荐
const confirmed = await vscode.window.showWarningMessage(
`确定要删除以下 ${filesToDelete.length} 个文件吗?\n\n${filesToDelete.join('\n')}`,
{ modal: true },
'全部删除',
'取消'
);
```
## 5. 安全考虑
### 5.1 使用回收站
```typescript
await vscode.workspace.fs.delete(uri, {
useTrash: true // 移到回收站,可恢复
});
```
### 5.2 路径验证
```typescript
// 防止删除工作区外的文件
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
return { success: false, error: '未打开工作区' };
}
const isInWorkspace = workspaceFolders.some(folder =>
filePath.startsWith(folder.uri.fsPath)
);
if (!isInWorkspace) {
return { success: false, error: '只能删除工作区内的文件' };
}
```
### 5.3 敏感文件保护
```typescript
const protectedFiles = [
'package.json',
'tsconfig.json',
'.git',
'node_modules'
];
const fileName = path.basename(filePath);
if (protectedFiles.includes(fileName)) {
vscode.window.showErrorMessage(`不允许删除系统文件: ${fileName}`);
return { success: false, error: '受保护的文件' };
}
```
## 6. 错误处理
### 6.1 常见错误
```typescript
try {
await vscode.workspace.fs.delete(uri, { useTrash: true });
} catch (error) {
if (error.code === 'FileNotFound') {
return { success: false, error: '文件不存在' };
}
if (error.code === 'NoPermissions') {
return { success: false, error: '没有删除权限' };
}
return { success: false, error: error.message };
}
```
## 7. 测试场景
### 7.1 基本测试
- [ ] 用户点击"确定删除" → 文件被删除
- [ ] 用户点击"取消" → 文件保留,返回取消消息
- [ ] 用户关闭对话框 → 文件保留,返回取消消息
- [ ] 文件不存在 → 返回错误消息
- [ ] 没有删除权限 → 返回错误消息
### 7.2 边界测试
- [ ] 删除工作区外的文件 → 拒绝
- [ ] 删除受保护文件 → 拒绝
- [ ] 批量删除 → 正确处理
- [ ] 后端收到取消消息后继续对话 → 流程正常
## 8. 配置选项(可选)
可以添加用户设置来控制行为:
```json
// package.json
"configuration": {
"properties": {
"ic-coder.confirmDelete": {
"type": "boolean",
"default": true,
"description": "删除文件前是否需要确认"
},
"ic-coder.useTrash": {
"type": "boolean",
"default": true,
"description": "删除文件时移到回收站而非永久删除"
}
}
}
```
读取配置:
```typescript
const config = vscode.workspace.getConfiguration('ic-coder');
const needConfirm = config.get<boolean>('confirmDelete', true);
const useTrash = config.get<boolean>('useTrash', true);
if (needConfirm) {
// 弹出确认对话框
}
```
## 9. 总结
### 9.1 后端是否需要修改?
**不需要**。后端继续返回删除工具调用,前端负责:
1. 拦截工具调用
2. 弹出确认对话框
3. 执行或取消删除
4. **必须返回结果给后端**
### 9.2 关键要点
- ✅ 前端必须返回 `TOOL_EXECUTION_RESULT`
- ✅ 使用 `useTrash: true` 提高安全性
- ✅ 验证文件路径在工作区内
- ✅ 保护敏感文件
- ✅ 提供清晰的错误消息
### 9.3 下一步
1.`messageHandler.ts` 中找到工具调用处理逻辑
2. 实现 `handleDeleteFileTool` 函数
3. 集成到现有流程
4. 测试各种场景
5. 考虑添加用户配置选项

50
docs/integration-guide.md Normal file
View File

@ -0,0 +1,50 @@
# 代码变更审查功能 - 使用说明
## 功能说明
AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以:
- 查看所有修改的文件列表
- 点击文件查看 diff 对比
- 采纳变更(保存文件)
- 拒绝变更(恢复旧内容)
## 已完成的集成
### 1. 后端集成
- ✅ 在 `ICHelperPanel.ts` 中添加了消息监听acceptChange/rejectChange
- ✅ 在发送消息时启动变更追踪会话
- ✅ 在文件操作时自动记录变更messageHandler.ts、toolExecutor.ts
### 2. 前端集成
- ✅ 在 `webviewContent.ts` 中添加了消息处理showChanges/changeAccepted/changeRejected
- ✅ 在 `changePanel.ts` 中实现了完整的 UI 交互逻辑
### 3. 核心功能
- ✅ 变更追踪服务changeTracker.ts
- ✅ Diff 可视化渲染diffRenderer.ts
- ✅ 采纳/拒绝变更逻辑
## 待完成工作
需要在对话结束时调用 `sendChangesToWebview(panel)` 来显示变更面板。
建议在以下位置添加:
1.`handleUserMessage` 函数中,对话流结束时
2. 或在 `dialogManager` 的对话完成回调中
示例代码:
```typescript
// 对话结束时
import { sendChangesToWebview } from '../utils/messageHandler';
// 在对话完成的地方调用
sendChangesToWebview(panel);
```
## 测试步骤
1. 启动插件F5
2. 发送消息让 AI 修改文件
3. 对话结束后,输入框上方应显示"代码变更"面板
4. 点击文件查看 diff
5. 点击"采纳"或"拒绝"按钮测试功能

View File

@ -0,0 +1,161 @@
# 个人规则功能需求文档(方案 A本地 `.md` 注入)
## 1. 文档目标
在不改动现有核心对话模式的前提下实现“个人规则Personal Rules”能力
用户可在插件内维护个人规则文本,插件保存到本地 `.md` 文件;每次发起对话时自动读取并随请求传递给后端,由后端注入模型上下文,影响回答风格与行为。
## 2. 范围定义
### 2.1 本期范围MVP
1. 支持用户编辑、保存、启用/停用个人规则。
2. 本地落盘为 `.md` 文件。
3. 发消息时自动加载规则并传给后端。
4. 后端接收结构化字段并注入提示词。
5. 基础异常处理和可观测提示。
### 2.2 非本期范围
1. 云端同步、多设备同步。
2. 规则版本历史/回滚。
3. 多规则集合管理(仅单份个人规则文本)。
4. 团队共享规则。
## 3. 术语与核心概念
1. `Personal Rules`:用户个人偏好与约束文本。
2. `Rules File`本地规则文件Markdown 格式。
3. `Rules Enabled`:规则开关;关闭时不注入。
4. `Rules Injection`:请求时将规则传后端并参与模型上下文构建。
## 4. 用户故事
1. 作为用户,我希望在插件里写下“回答风格、代码习惯、语言偏好”等个人规则。
2. 作为用户,我希望规则保存在本地可见文件中。
3. 作为用户,我希望发消息时自动生效,无需每次重复输入。
4. 作为用户,我希望可以一键关闭规则,临时不生效。
## 5. 功能需求(前端/Webview + 扩展端)
### 5.1 规则管理界面
1. 提供“个人规则”入口。
2. 提供多行编辑框(显示当前规则内容)。
3. 提供“保存”按钮。
4. 提供“启用/停用”开关。
5. 显示当前状态:
6. 规则是否启用。
7. 规则字数/长度。
8. 最近保存时间(可选)。
### 5.2 本地文件存储
1. 规则内容保存到本地 `.md`
2. 推荐文件名:`personal-rules.md`
3. 推荐路径(优先):插件全局存储目录下固定子路径。
4. 文件不存在时可自动创建。
5. 用户可通过“打开规则文件”查看(可选)。
### 5.3 对话发送前处理
1. 用户点击发送消息。
2. 扩展端检查规则开关:
3. 关闭:不读取规则,不传后端。
4. 开启:读取 `.md` 内容。
5. 读取成功且非空时,将规则文本附加到对话请求结构化字段。
6. 读取失败时:提示告警,但不阻断正常对话。
### 5.4 限制与防护
1. 规则长度上限(例如 4000 字符,可配置)。
2. 超限时保存被拒绝,提示用户缩短。
3. 空白内容视为“无规则”。
4. 不允许二进制或非文本写入。
## 6. 功能需求(后端)
### 6.1 请求协议扩展
在现有对话请求结构中增加字段:
1. `personalRules`:字符串,可选。
2. `rulesEnabled`:布尔,可选(便于追踪)。
3. `rulesMeta`:可选元信息(长度、来源)。
### 6.2 注入策略
1. 后端收到 `personalRules` 后,将其注入系统提示层(而非用户消息层)。
2. 注入顺序建议:
3. 系统安全与平台策略。
4. 产品默认系统提示。
5. 用户个人规则。
6. 用户输入。
7.`personalRules` 为空或开关关闭,则跳过注入。
### 6.3 风险控制
1. 规则文本不允许覆盖平台安全策略。
2. 记录本次是否注入规则(日志字段即可)。
3. 异常不应导致整次对话失败(可降级为无规则对话)。
## 7. 前后端对接设计
### 7.1 消息链路
1. Webview 触发 `sendMessage`
2. 扩展端 `messageHandler` 统一处理发送。
3. `messageHandler` 在调用 `dialogService.sendMessage` 前读取个人规则。
4. `dialogService` 组装 `DialogRequest`,带上 `personalRules`
5. `sseHandler` 发起流式请求。
6. 后端注入规则后进入模型推理。
7. 正常走现有 SSE 回传流程。
### 7.2 职责边界
1. Webview展示与编辑不直接拼接最终请求。
2. 扩展端:规则文件读写、开关状态管理、请求组装。
3. 后端:规则注入、优先级控制、审计日志。
## 8. 数据与状态设计
### 8.1 本地文件
1. 文件格式Markdown 纯文本。
2. 内容约定:无强制模板,允许自由文本。
3. 编码UTF-8。
### 8.2 本地配置状态
1. `personalRulesEnabled`:是否启用。
2. `personalRulesPath`:规则文件路径(可固定也可配置)。
3. `lastSavedAt`:最近保存时间(可选)。
## 9. 异常与降级
1. 文件不存在:自动创建空文件,视为无规则。
2. 文件读取失败:弹出提示,继续无规则发送。
3. 文件写入失败:保存失败提示,不更新状态。
4. 后端字段不识别:请求兼容,后端忽略新字段。
5. 后端注入失败:降级为普通对话,记录日志。
## 10. 安全与合规要求
1. 个人规则属于用户本地数据,不主动上传除非发起对话。
2. 日志中避免完整打印规则正文(最多打印长度和哈希)。
3. 后端注入时必须确保平台安全策略优先级更高。
## 11. 验收标准UAT
1. 用户保存规则后,本地存在 `personal-rules.md` 且内容一致。
2. 开启规则发送消息时,请求中可观测到 `personalRules` 字段。
3. 关闭规则发送消息时,请求中不含该字段或为空。
4. 规则文件损坏/读取失败时,不影响正常聊天。
5. 超过长度上限时,前端保存被拒绝且提示明确。
6. 后端日志可确认“本次是否注入个人规则”。
## 12. 迭代建议(下一阶段)
1. 规则模板(代码风格、语言风格、测试偏好)。
2. 项目规则与个人规则合并策略。
3. 云端同步(按 `userId`),多端一致。

View File

@ -0,0 +1,783 @@
# 插件试用用户功能实现方案
## 1. 方案概述
**核心思路:**
- Web 登录成功后只返回 token保持现状
- 插件调用 `getUserInfo(token)` 时,后端返回的数据里包含标识字段
- 前端根据该字段判断是否是插件试用用户
- 插件试用用户:显示欢迎弹窗,不显示邀请码弹窗
- 正式用户:显示邀请码弹窗(现有逻辑)
---
## 2. 后端需要做什么
### 2.1 在用户信息接口中添加字段
**接口:** `GET /system/user/getInfo`
**现有响应:**
```json
{
"userId": "xxx",
"username": "testuser",
"nickname": "测试用户",
"email": "test@example.com"
}
```
**新增字段:**
**方案 :添加 isPluginTrial 字段**
```json
{
"userId": "xxx",
"username": "testuser",
"nickname": "测试用户",
"email": "test@example.com",
"isPluginTrial": true, // ← 新增:是否是插件试用用户
"pluginTrialExpiresAt": 1709654400000 // ← 新增:试用到期时间(毫秒时间戳)
}
```
### 2.2 后端逻辑说明
**判断逻辑:**
```javascript
// 伪代码
function getUserInfo(userId) {
const user = db.users.findById(userId);
// 判断是否是插件试用用户(后端自己的逻辑)
const isPluginTrial = checkIfPluginTrialUser(user);
return {
userId: user.id,
username: user.username,
nickname: user.nickname,
email: user.email,
isPluginTrial: isPluginTrial,
pluginTrialExpiresAt: isPluginTrial ? user.trial_expires_at : null
};
}
```
**Token 过期时间:**
- 插件试用用户JWT Token 设置 15 天过期
- 正式用户JWT Token 设置 30 天过期(或现有逻辑)
---
## 3. 前端需要修改的地方
### 3.1 修改 UserInfo 接口
**文件:** `src/services/userService.ts`
**现有接口:**
```typescript
interface UserInfo {
userId: string;
username: string;
nickname: string;
email?: string;
phonenumber?: string;
avatar?: string;
roles?: string[];
permissions?: string[];
createTime?: string;
loginDate?: string;
membership?: {
tierCode: string;
tierName: string;
tierLevel: number;
remainingDays?: number;
monthlyCredits?: number;
};
credits?: number;
}
```
**新增字段:**
```typescript
interface UserInfo {
// ... 现有字段
isPluginTrial?: boolean; // ← 新增:是否是插件试用用户
pluginTrialExpiresAt?: number; // ← 新增:试用到期时间(毫秒时间戳)
membership?: {
tierCode: string;
tierName: string;
tierLevel: number;
remainingDays?: number;
monthlyCredits?: number;
};
credits?: number;
}
```
### 3.2 修改 onTokenReceived() 方法
**文件:** `src/services/userService.ts`
**现有代码位置:** 约 200 行左右
**修改内容:**
```typescript
async onTokenReceived(token: string) {
// 现有逻辑:并行获取三类信息
const [userInfo, membershipInfo, credits] = await Promise.all([
getUserInfo(token),
getMembershipInfo(token),
fetchBalanceWithToken(token)
]);
// 合并数据
const fullUserInfo = {
...userInfo,
membership: membershipInfo,
credits: credits
};
// 保存到 globalState
await this.context.globalState.update('icCoderUserInfo', fullUserInfo);
// ========== 新增逻辑 ==========
// 判断是否是插件试用用户
if (fullUserInfo.isPluginTrial === true) {
// 插件试用用户:显示欢迎弹窗,不显示邀请码弹窗
await this.showWelcomePanel();
// 标记为已显示欢迎弹窗(避免重复显示)
await this.context.globalState.update('pluginTrialWelcomed', true);
} else {
// 正式用户:显示邀请码弹窗(现有逻辑)
await this.checkAndShowInvitationModal();
}
// ========== 新增逻辑结束 ==========
return fullUserInfo;
}
```
### 3.3 新增欢迎弹窗面板
**新建文件:** `src/panels/WelcomePanel.ts`
```typescript
/**
* 欢迎引导面板
* 功能:插件试用用户首次登录显示使用教程
*/
import * as vscode from 'vscode';
export class WelcomePanel {
public static currentPanel: WelcomePanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];
private constructor(panel: vscode.WebviewPanel) {
this._panel = panel;
this._panel.webview.html = this.getHtmlContent();
// 监听关闭事件
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
}
public static render(context: vscode.ExtensionContext) {
// 避免重复显示
if (WelcomePanel.currentPanel) {
WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
return;
}
const panel = vscode.window.createWebviewPanel(
'icCoderWelcome',
'欢迎使用 IC Coder',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
WelcomePanel.currentPanel = new WelcomePanel(panel);
}
private getHtmlContent(): string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>欢迎使用 IC Coder</title>
<style>
body {
padding: 40px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
h1 {
color: var(--vscode-textLink-foreground);
margin-bottom: 20px;
}
.welcome-message {
font-size: 16px;
margin-bottom: 30px;
color: var(--vscode-descriptionForeground);
}
.step {
margin: 20px 0;
padding: 20px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 8px;
border-left: 4px solid var(--vscode-textLink-foreground);
}
.step h3 {
margin-top: 0;
color: var(--vscode-textLink-foreground);
}
.step p {
margin: 10px 0;
}
.button {
padding: 12px 24px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 20px;
}
.button:hover {
background: var(--vscode-button-hoverBackground);
}
</style>
</head>
<body>
<h1>🎉 欢迎使用 IC Coder</h1>
<p class="welcome-message">
您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
</p>
<div class="step">
<h3>📝 步骤 1打开聊天面板</h3>
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
</div>
<div class="step">
<h3>💬 步骤 2输入您的需求</h3>
<p>描述您想要生成的 Verilog 代码或需要帮助的问题AI 将为您提供专业的解决方案</p>
</div>
<div class="step">
<h3>🔬 步骤 3运行仿真</h3>
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
</div>
<button class="button" onclick="close()">开始使用</button>
<script>
function close() {
// 通知 VS Code 关闭面板
window.close();
}
</script>
</body>
</html>
`;
}
public dispose() {
WelcomePanel.currentPanel = undefined;
this._panel.dispose();
while (this._disposables.length) {
const disposable = this._disposables.pop();
if (disposable) {
disposable.dispose();
}
}
}
}
```
### 3.4 新增过期提醒面板
**新建文件:** `src/panels/ExpiredPanel.ts`
```typescript
/**
* 试用期到期提醒面板
* 功能:试用期到期时显示续费提示
*/
import * as vscode from 'vscode';
export class ExpiredPanel {
public static render() {
const panel = vscode.window.createWebviewPanel(
'icCoderExpired',
'试用期已到期',
vscode.ViewColumn.One,
{ enableScripts: true }
);
panel.webview.html = this.getHtmlContent();
}
private static getHtmlContent(): string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
body {
padding: 60px 40px;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
h1 {
color: var(--vscode-errorForeground);
font-size: 28px;
margin-bottom: 20px;
}
p {
font-size: 16px;
line-height: 1.6;
margin: 15px 0;
color: var(--vscode-descriptionForeground);
}
.button {
padding: 12px 30px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 10px;
}
.button:hover {
background: var(--vscode-button-hoverBackground);
}
</style>
</head>
<body>
<h1>⏰ 您的试用期已到期</h1>
<p>感谢您使用 IC Coder您的 15 天试用期已结束。</p>
<p>如需继续使用,请联系我们获取正式版本。</p>
<button class="button" onclick="contact()">联系我们</button>
<script>
function contact() {
// 可以打开联系页面或发送邮件
window.open('https://iccoder.com/contact', '_blank');
}
</script>
</body>
</html>
`;
}
}
```
### 3.5 新增过期检测服务
**新建文件:** `src/services/trialExpirationService.ts`
```typescript
/**
* 试用期过期检测服务
* 功能:检查插件试用用户是否过期
*/
import * as vscode from 'vscode';
import { getUserInfo } from './userService';
import { ExpiredPanel } from '../panels/ExpiredPanel';
export class TrialExpirationService {
private context: vscode.ExtensionContext;
constructor(context: vscode.ExtensionContext) {
this.context = context;
}
/**
* 检查是否过期
* @returns true=已过期false=未过期
*/
public async checkExpiration(): Promise<boolean> {
const userInfo = await getUserInfo();
// 不是插件试用用户,不需要检查
if (!userInfo?.isPluginTrial) {
return false;
}
// 没有过期时间,不检查
if (!userInfo.pluginTrialExpiresAt) {
return false;
}
// 检查是否过期
const now = Date.now();
if (now >= userInfo.pluginTrialExpiresAt) {
// 已过期
await this.handleExpired();
return true;
}
return false;
}
/**
* 处理过期逻辑
*/
private async handleExpired(): Promise<void> {
// 显示过期弹窗
ExpiredPanel.render();
// 清除本地数据(可选)
// await this.context.globalState.update('icCoderUserInfo', undefined);
// await this.context.globalState.update('icCoderSessions', undefined);
}
}
```
### 3.6 在消息发送前检查过期
**文件:** `src/utils/messageHandler.ts`
**修改位置:** 在发送消息给后端之前添加过期检查
```typescript
import { TrialExpirationService } from '../services/trialExpirationService';
// 在 handleUserMessage 或类似的消息处理函数中添加
async function handleUserMessage(message: string, context: vscode.ExtensionContext) {
// ========== 新增:检查试用期是否过期 ==========
const trialService = new TrialExpirationService(context);
const isExpired = await trialService.checkExpiration();
if (isExpired) {
// 已过期,禁止使用
return {
success: false,
message: '您的试用期已到期,请联系我们获取正式版本'
};
}
// ========== 新增结束 ==========
// 现有的消息处理逻辑
// ...
}
```
---
## 4. 完整的实现流程
### 4.1 登录流程(带过期检查)
```
1. 用户点击登录
2. 打开浏览器Web 端登录
3. 重定向回插件http://localhost:{port}/callback?token={token}
4. 插件调用 onTokenReceived(token)
5. 并行获取getUserInfo + getMembershipInfo + Credits
6. 后端返回 userInfo包含 isPluginTrial 和 pluginTrialExpiresAt
7. 判断 isPluginTrial === true
├─ 是:显示欢迎弹窗,不显示邀请码弹窗
└─ 否:显示邀请码弹窗(现有逻辑)
8. 保存用户信息到 globalState
```
### 4.2 使用功能时的过期检查
```
1. 用户发送消息/使用功能
2. 调用 trialService.checkExpiration()
3. 获取 userInfo检查 isPluginTrial
4. 如果是插件试用用户,检查 Date.now() >= pluginTrialExpiresAt
├─ 是:显示过期弹窗,禁止使用,返回 true
└─ 否:允许使用,返回 false
5. 继续正常的消息处理流程
```
---
## 5. ⚠️ 潜在 Bug 和注意事项
### 5.1 时间同步问题
**问题:** 前端使用 `Date.now()` 判断过期,如果用户本地时间不准确会导致误判
**场景:**
- 用户本地时间快了 1 天 → 提前显示过期弹窗
- 用户本地时间慢了 1 天 → 过期后仍可使用
**解决方案:**
```typescript
// 方案 1每次使用前调用后端验证推荐
async checkExpiration(): Promise<boolean> {
try {
// 调用后端接口验证 Token 是否过期
const response = await fetch(`${API_BASE}/auth/verify`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.status === 401) {
// Token 过期
await this.handleExpired();
return true;
}
return false;
} catch (error) {
// 网络错误,使用本地时间判断
const userInfo = await getUserInfo();
return Date.now() >= (userInfo?.pluginTrialExpiresAt || 0);
}
}
```
### 5.2 欢迎弹窗重复显示
**问题:** 每次登录都显示欢迎弹窗,用户体验不好
**场景:**
- 试用用户第一次登录显示欢迎弹窗 ✅
- 用户关闭插件后重新打开,又显示欢迎弹窗 ❌
**解决方案:**
```typescript
async onTokenReceived(token: string) {
// ... 获取用户信息
if (fullUserInfo.isPluginTrial === true) {
// 检查是否已经显示过欢迎弹窗
const hasWelcomed = this.context.globalState.get('pluginTrialWelcomed');
if (!hasWelcomed) {
await this.showWelcomePanel();
await this.context.globalState.update('pluginTrialWelcomed', true);
}
} else {
await this.checkAndShowInvitationModal();
}
}
```
### 5.3 isPluginTrial 字段类型不一致
**问题:** 后端可能返回 `true``false``null``undefined`,前端判断时需要严格处理
**场景:**
```typescript
// ❌ 错误写法
if (userInfo.isPluginTrial) {
// undefined 会被判断为 false
}
// ✅ 正确写法
if (userInfo.isPluginTrial === true) {
// 插件试用用户
}
```
**建议:**
- 后端统一返回 `true``false`,不要返回 `null`
- 前端使用严格相等 `===` 判断
### 5.4 Token 过期但前端未清除
**问题:** Token 在后端已过期,但前端仍保存着过期的 Token
**场景:**
- 用户 15 天后打开插件
- 前端尝试调用 API后端返回 401
- 前端没有处理 401导致功能异常
**解决方案:**
```typescript
// 在 apiClient.ts 中统一处理 401
async function apiCall(url: string, options: any) {
const response = await fetch(url, options);
if (response.status === 401) {
// Token 过期,清除本地数据
await clearAllData();
ExpiredPanel.render();
throw new Error('Token expired');
}
return response.json();
}
```
---
## 6. 前后端对接清单
### 6.1 后端需要提供
**1. 修改 `GET /system/user/getInfo` 接口**
- 新增字段:`isPluginTrial` (boolean)
- 新增字段:`pluginTrialExpiresAt` (number, 毫秒时间戳)
**2. Token 过期时间设置**
- 插件试用用户JWT Token 设置 15 天过期
- 正式用户:保持现有逻辑
**3. 测试账号**
- 提供 1-2 个插件试用用户账号用于测试
### 6.2 前端需要修改
**1. 修改文件:**
- `src/services/userService.ts` - 修改 UserInfo 接口和 onTokenReceived()
- `src/utils/messageHandler.ts` - 添加过期检查
**2. 新增文件:**
- `src/panels/WelcomePanel.ts` - 欢迎弹窗
- `src/panels/ExpiredPanel.ts` - 过期提醒弹窗
- `src/services/trialExpirationService.ts` - 过期检测服务
**3. globalState 新增存储键:**
- `pluginTrialWelcomed` (boolean) - 是否已显示欢迎弹窗
---
## 7. 测试计划
### 7.1 登录流程测试
**测试用例 1插件试用用户登录**
- 使用插件试用用户账号登录
- 验证是否显示欢迎弹窗
- 验证是否不显示邀请码弹窗
- 验证 userInfo 中 isPluginTrial === true
**测试用例 2正式用户登录**
- 使用正式用户账号登录
- 验证是否显示邀请码弹窗
- 验证是否不显示欢迎弹窗
- 验证 userInfo 中 isPluginTrial !== true
**测试用例 3欢迎弹窗不重复显示**
- 插件试用用户登录后显示欢迎弹窗
- 关闭插件重新打开
- 验证不再显示欢迎弹窗
### 7.2 过期检测测试
**测试用例 4未过期用户正常使用**
- 插件试用用户登录(未过期)
- 发送消息使用功能
- 验证功能正常使用
**测试用例 5已过期用户禁止使用**
- 修改本地时间到 15 天后(或修改 pluginTrialExpiresAt
- 尝试发送消息
- 验证显示过期弹窗
- 验证功能被禁用
**测试用例 6Token 过期处理**
- 使用过期的 Token 调用 API
- 验证后端返回 401
- 验证前端显示过期弹窗
---
## 8. 总结
### 8.1 核心改动点
**后端(最小改动):**
1. `GET /system/user/getInfo` 接口新增 2 个字段
2. JWT Token 根据用户类型设置不同过期时间
**前端(主要改动):**
1. 修改 UserInfo 接口定义
2. 修改 onTokenReceived() 添加判断逻辑
3. 新增 3 个文件(欢迎面板、过期面板、过期检测服务)
4. 在消息发送前添加过期检查
### 8.2 关键判断逻辑
```typescript
// 登录后判断
if (userInfo.isPluginTrial === true) {
showWelcomePanel(); // 显示欢迎弹窗
} else {
showInvitationModal(); // 显示邀请码弹窗
}
// 使用前判断
if (Date.now() >= userInfo.pluginTrialExpiresAt) {
showExpiredPanel(); // 显示过期弹窗
return; // 禁止使用
}
```
### 8.3 必须注意的问题
1. **时间同步问题** - 用户本地时间不准确导致误判,建议调用后端验证
2. **isPluginTrial 严格判断** - 必须使用 `=== true` 判断
3. **欢迎弹窗重复显示** - 使用 globalState 标记避免重复
4. **Token 过期处理** - 在 apiClient 中统一处理 401 响应
### 8.4 实现优先级
**P0必须实现**
1. 后端接口新增字段
2. 前端 UserInfo 接口修改
3. 前端 onTokenReceived() 判断逻辑
4. 过期检测逻辑
**P1重要**
1. 欢迎弹窗
2. 过期提醒弹窗
**P2优化**
1. 后端验证过期(避免时间同步问题)
2. 更友好的过期提示
---
**文档完成!** 基于你现有的代码架构,这个方案改动最小,实现最简单。

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.0.3", "version": "1.0.11",
"publisher": "ICCoderAgenticVerilogPlatform", "publisher": "ICCoderAgenticVerilogPlatform",
"engines": { "engines": {
"vscode": "^1.80.0" "vscode": "^1.80.0"
@ -27,7 +27,7 @@
}, },
"activationEvents": [ "activationEvents": [
"onCommand:ic-coder.openPanel", "onCommand:ic-coder.openPanel",
"onView:ic-coder-sidebar", "onView:ic-coder.mainView",
"onLanguage:verilog", "onLanguage:verilog",
"onLanguage:vhdl", "onLanguage:vhdl",
"onStartupFinished" "onStartupFinished"
@ -135,6 +135,7 @@
"@vscode/test-cli": "^0.0.12", "@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2", "@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.7.1", "@vscode/vsce": "^3.7.1",
"copy-webpack-plugin": "^14.0.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"ts-loader": "^9.5.4", "ts-loader": "^9.5.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
@ -142,14 +143,6 @@
"webpack": "^5.103.0", "webpack": "^5.103.0",
"webpack-cli": "^6.0.1" "webpack-cli": "^6.0.1"
}, },
"files": [
"dist",
"media",
"tools",
"src/assets",
"LICENSE",
"CHANGELOG.md"
],
"dependencies": { "dependencies": {
"@wavedrom/doppler": "^1.14.0", "@wavedrom/doppler": "^1.14.0",
"eventsource-parser": "^3.0.6", "eventsource-parser": "^3.0.6",

24
pnpm-lock.yaml generated
View File

@ -57,6 +57,9 @@ importers:
'@vscode/vsce': '@vscode/vsce':
specifier: ^3.7.1 specifier: ^3.7.1
version: 3.7.1 version: 3.7.1
copy-webpack-plugin:
specifier: ^14.0.0
version: 14.0.0(webpack@5.103.0)
eslint: eslint:
specifier: ^9.39.1 specifier: ^9.39.1
version: 9.39.1 version: 9.39.1
@ -823,6 +826,12 @@ packages:
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
copy-webpack-plugin@14.0.0:
resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==}
engines: {node: '>= 20.9.0'}
peerDependencies:
webpack: ^5.1.0
core-util-is@1.0.3: core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@ -1928,6 +1937,10 @@ packages:
serialize-javascript@6.0.2: serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
serialize-javascript@7.0.4:
resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==}
engines: {node: '>=20.0.0'}
setimmediate@1.0.5: setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
@ -3297,6 +3310,15 @@ snapshots:
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
copy-webpack-plugin@14.0.0(webpack@5.103.0):
dependencies:
glob-parent: 6.0.2
normalize-path: 3.0.0
schema-utils: 4.3.3
serialize-javascript: 7.0.4
tinyglobby: 0.2.15
webpack: 5.103.0(webpack-cli@6.0.1)
core-util-is@1.0.3: {} core-util-is@1.0.3: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
@ -4431,6 +4453,8 @@ snapshots:
dependencies: dependencies:
randombytes: 2.1.0 randombytes: 2.1.0
serialize-javascript@7.0.4: {}
setimmediate@1.0.5: {} setimmediate@1.0.5: {}
shallow-clone@3.0.1: shallow-clone@3.0.1:

File diff suppressed because one or more lines are too long

View File

@ -159,20 +159,32 @@ export async function activate(context: vscode.ExtensionContext) {
// 注册命令:用户登录 // 注册命令:用户登录
const loginCommand = vscode.commands.registerCommand( const loginCommand = vscode.commands.registerCommand(
"ic-coder.login", "ic-coder.login",
async () => { async (options?: { forceReauth?: boolean }) => {
try { try {
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框 const forceReauth = options?.forceReauth === true;
try { const session = await vscode.authentication.getSession("iccoder", [], {
await vscode.authentication.getSession("iccoder", [], { createIfNone: false,
clearSessionPreference: true, });
createIfNone: false const expired = session?.accessToken
}); ? isTokenExpired(session.accessToken)
} catch { : null;
// 忽略错误
// 会话仍有效时,直接打开聊天面板
if (session && expired === false && !forceReauth) {
vscode.commands.executeCommand("ic-coder.openChat");
return;
} }
// 创建新 session // 1) 清空当前登录状态信息
await vscode.authentication.getSession("iccoder", [], { createIfNone: true }); await authProvider.clearSessionsForRelogin();
await context.globalState.update("icCoderSessions", []);
await context.globalState.update("icCoderUserInfo", undefined);
// 2) 重新登录(强制新会话)
await vscode.authentication.getSession("iccoder", [], {
clearSessionPreference: true,
forceNewSession: true,
});
} catch (error) { } catch (error) {
vscode.window.showErrorMessage(`登录失败: ${error}`); vscode.window.showErrorMessage(`登录失败: ${error}`);
} }
@ -289,7 +301,12 @@ export async function activate(context: vscode.ExtensionContext) {
const viewProvider = new ICViewProvider(context.extensionUri, context); const viewProvider = new ICViewProvider(context.extensionUri, context);
const viewRegistration = vscode.window.registerWebviewViewProvider( const viewRegistration = vscode.window.registerWebviewViewProvider(
"ic-coder.mainView", "ic-coder.mainView",
viewProvider viewProvider,
{
webviewOptions: {
retainContextWhenHidden: true
}
}
); );
// 注册 VCD 自定义编辑器 // 注册 VCD 自定义编辑器
@ -305,6 +322,8 @@ export async function activate(context: vscode.ExtensionContext) {
logoutCommand, logoutCommand,
changeInvitationCodeCommand, changeInvitationCodeCommand,
testNotificationCommand, testNotificationCommand,
// testTrialUserCommand,
// testExpiredUserCommand,
// TODO: 等待重新实现这些命令 // TODO: 等待重新实现这些命令
// viewHistoryCommand, // viewHistoryCommand,
// newSessionCommand, // newSessionCommand,

View File

@ -0,0 +1,78 @@
/**
* 试用期到期提醒面板
* 功能:试用期到期时显示续费提示
* 依赖vscode
* 使用场景:试用用户到期时显示
*/
import * as vscode from 'vscode';
export class ExpiredPanel {
public static render() {
const panel = vscode.window.createWebviewPanel(
'icCoderExpired',
'试用期已到期',
vscode.ViewColumn.One,
{ enableScripts: true }
);
panel.webview.html = this.getHtmlContent();
}
private static getHtmlContent(): string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
body {
padding: 60px 40px;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
h1 {
color: var(--vscode-errorForeground);
font-size: 28px;
margin-bottom: 20px;
}
p {
font-size: 16px;
line-height: 1.6;
margin: 15px 0;
color: var(--vscode-descriptionForeground);
}
.button {
padding: 12px 30px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 10px;
}
.button:hover {
background: var(--vscode-button-hoverBackground);
}
</style>
</head>
<body>
<h1>⏰ 您的试用期已到期</h1>
<p>感谢您使用 IC Coder您的 15 天试用期已结束。</p>
<p>如需继续使用,请联系我们获取正式版本。</p>
<button class="button" onclick="contact()">联系我们</button>
<script>
function contact() {
window.open('https://iccoder.com/contact', '_blank');
}
</script>
</body>
</html>
`;
}
}

View File

@ -13,6 +13,10 @@ import {
handlePlanAction, handlePlanAction,
getCurrentTaskId, getCurrentTaskId,
setLastTaskId, setLastTaskId,
handleAcceptChange,
handleRejectChange,
startChangeSession,
handleOpenFileDiff,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
import { compactDialog } from "../services/apiClient"; import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel"; import { VCDViewerPanel } from "./VCDViewerPanel";
@ -20,6 +24,7 @@ import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory"; import { MessageType } from "../types/chatHistory";
import { getCachedUserInfo } from "../services/userService"; import { getCachedUserInfo } from "../services/userService";
import { isTokenExpired } from "../utils/jwtUtils"; import { isTokenExpired } from "../utils/jwtUtils";
import { setBalanceUpdateCallback } from "../services/creditsService";
/** /**
* 获取会员等级图标 URI * 获取会员等级图标 URI
@ -27,17 +32,17 @@ import { isTokenExpired } from "../utils/jwtUtils";
function getTierIconUri( function getTierIconUri(
webview: vscode.Webview, webview: vscode.Webview,
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
tierCode?: string tierCode?: string,
): string | undefined { ): string | undefined {
if (!tierCode) { if (!tierCode) {
return undefined; return undefined;
} }
const tierIconMap: Record<string, string> = { const tierIconMap: Record<string, string> = {
'BASIC': 'free.png', BASIC: "free.png",
'TRIAL': 'PRO-Try.png', TRIAL: "PRO-Try.png",
'ADVANCED': 'PRO.png', ADVANCED: "PRO.png",
'PROFESSIONAL': 'PRO+.png' PROFESSIONAL: "PRO+.png",
}; };
const iconFile = tierIconMap[tierCode]; const iconFile = tierIconMap[tierCode];
@ -46,7 +51,13 @@ function getTierIconUri(
} }
const iconUri = webview.asWebviewUri( const iconUri = webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'src', 'assets', 'titleIcon', iconFile) vscode.Uri.joinPath(
context.extensionUri,
"dist",
"assets",
"titleIcon",
iconFile,
),
); );
return iconUri.toString(); return iconUri.toString();
@ -57,28 +68,32 @@ function getTierIconUri(
*/ */
export async function showICHelperPanel( export async function showICHelperPanel(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
viewColumn?: vscode.ViewColumn viewColumn?: vscode.ViewColumn,
) { ) {
// 检查 token 是否过期 // 检查 token 是否过期
let token: string | undefined; let token: string | undefined;
try { try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false }); const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
token = session?.accessToken; token = session?.accessToken;
} catch (error) { } catch (error) {
console.warn('[ICHelperPanel] 获取 session 失败:', error); console.warn("[ICHelperPanel] 获取 session 失败:", error);
} }
if (token && isTokenExpired(token)) { if (token && isTokenExpired(token)) {
// 清除过期的 session // 清除过期的 session
await context.globalState.update('icCoderSessions', []); await context.globalState.update("icCoderSessions", []);
await context.globalState.update('icCoderUserInfo', undefined); await context.globalState.update("icCoderUserInfo", undefined);
const action = await vscode.window.showWarningMessage( const action = await vscode.window.showWarningMessage(
'登录已过期,请重新登录', "登录已过期,请重新登录",
'立即登录' "立即登录",
); );
if (action === '立即登录') { if (action === "立即登录") {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
} }
return; return;
} }
@ -93,7 +108,9 @@ export async function showICHelperPanel(
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录") .showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
.then((selection) => { .then((selection) => {
if (selection === "立即登录") { if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
} }
}); });
return; return;
@ -103,7 +120,9 @@ export async function showICHelperPanel(
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录") .showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
.then((selection) => { .then((selection) => {
if (selection === "立即登录") { if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
} }
}); });
return; return;
@ -119,9 +138,9 @@ export async function showICHelperPanel(
retainContextWhenHidden: true, retainContextWhenHidden: true,
localResourceRoots: [ localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"), vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets") vscode.Uri.joinPath(context.extensionUri, "dist", "assets"),
], ],
} },
); );
// 为面板生成唯一ID // 为面板生成唯一ID
@ -135,36 +154,66 @@ export async function showICHelperPanel(
panel.iconPath = vscode.Uri.joinPath( panel.iconPath = vscode.Uri.joinPath(
context.extensionUri, context.extensionUri,
"media", "media",
"icon.png" "icon.png",
); );
// 获取页面内图标URI // 获取页面内图标URI
const iconUri = panel.webview.asWebviewUri( const iconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png") vscode.Uri.joinPath(context.extensionUri, "media", "icon.png"),
); );
// 获取模型图标URI // 获取模型图标URI
const autoIconUri = panel.webview.asWebviewUri( const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png") vscode.Uri.joinPath(
context.extensionUri,
"dist",
"assets",
"model",
"Auto.png",
),
); );
const liteIconUri = panel.webview.asWebviewUri( const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png") vscode.Uri.joinPath(
context.extensionUri,
"dist",
"assets",
"model",
"lite.png",
),
); );
const syIconUri = panel.webview.asWebviewUri( const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png") vscode.Uri.joinPath(
context.extensionUri,
"dist",
"assets",
"model",
"Sy.png",
),
); );
const maxIconUri = panel.webview.asWebviewUri( const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png") vscode.Uri.joinPath(
context.extensionUri,
"dist",
"assets",
"model",
"Max.png",
),
); );
// 获取二维码图片URI // 获取二维码图片URI
const qrCodeUri = panel.webview.asWebviewUri( const qrCodeUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "QRCode", "wx.png") vscode.Uri.joinPath(
context.extensionUri,
"dist",
"assets",
"QRCode",
"wx.png",
),
); );
// 获取Logo URI // 获取Logo URI
const logoUri = panel.webview.asWebviewUri( const logoUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png") vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png"),
); );
// 设置HTML内容 // 设置HTML内容
@ -175,7 +224,7 @@ export async function showICHelperPanel(
syIconUri.toString(), syIconUri.toString(),
maxIconUri.toString(), maxIconUri.toString(),
qrCodeUri.toString(), qrCodeUri.toString(),
logoUri.toString() logoUri.toString(),
); );
// 获取并发送用户信息到 webview // 获取并发送用户信息到 webview
@ -185,21 +234,25 @@ export async function showICHelperPanel(
if (userInfo) { if (userInfo) {
// 使用缓存的用户信息 // 使用缓存的用户信息
console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo); console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo);
console.log('[ICHelperPanel] Credits 余额:', userInfo.credits); console.log("[ICHelperPanel] Credits 余额:", userInfo.credits);
const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode); const tierIconUrl = getTierIconUri(
panel.webview,
context,
userInfo.membership?.tierCode,
);
const messageData = { const messageData = {
command: 'updateUserInfo', command: "updateUserInfo",
userInfo: { userInfo: {
userId: userInfo.userId, userId: userInfo.userId,
nickname: userInfo.nickname, nickname: userInfo.nickname,
username: userInfo.username, username: userInfo.username,
credits: userInfo.credits, credits: userInfo.credits,
membership: userInfo.membership membership: userInfo.membership,
}, },
tierIconUrl: tierIconUrl tierIconUrl: tierIconUrl,
}; };
console.log('[ICHelperPanel] 发送用户信息到前端:', messageData); console.log("[ICHelperPanel] 发送用户信息到前端:", messageData);
panel.webview.postMessage(messageData); panel.webview.postMessage(messageData);
} else { } else {
// 如果没有缓存,从 session 中获取 // 如果没有缓存,从 session 中获取
@ -207,36 +260,63 @@ export async function showICHelperPanel(
createIfNone: false, createIfNone: false,
}); });
if (session) { if (session) {
console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account); console.log(
"[ICHelperPanel] 从 session 获取用户信息, account:",
session.account,
);
panel.webview.postMessage({ panel.webview.postMessage({
command: 'updateUserInfo', command: "updateUserInfo",
userInfo: { userInfo: {
userId: session.account.id, userId: session.account.id,
nickname: session.account.label, nickname: session.account.label,
username: session.account.label username: session.account.label,
} },
}); });
} }
} }
} catch (error) { } catch (error) {
console.error('[ICHelperPanel] 获取用户信息失败:', error); console.error("[ICHelperPanel] 获取用户信息失败:", error);
} }
// 设置余额更新回调
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,
});
}
});
// 检查是否有待发送的消息 // 检查是否有待发送的消息
const pendingMessage = context.globalState.get('pendingMessage') as any; const pendingMessage = context.globalState.get("pendingMessage") as any;
if (pendingMessage) { if (pendingMessage) {
console.log('[ICHelperPanel] 检测到待发送消息,准备自动发送'); console.log("[ICHelperPanel] 检测到待发送消息,准备自动发送");
// 清除待发送消息 // 清除待发送消息
await context.globalState.update('pendingMessage', undefined); await context.globalState.update("pendingMessage", undefined);
// 延迟发送,确保面板已完全初始化 // 延迟发送,确保面板已完全初始化
setTimeout(() => { setTimeout(() => {
panel.webview.postMessage({ panel.webview.postMessage({
command: 'autoSendMessage', command: "autoSendMessage",
text: pendingMessage.text, text: pendingMessage.text,
mode: pendingMessage.mode, mode: pendingMessage.mode,
serviceTier: pendingMessage.serviceTier serviceTier: pendingMessage.serviceTier,
}); });
}, 500); }, 500);
} }
@ -258,12 +338,12 @@ export async function showICHelperPanel(
try { try {
const taskMeta = await historyManager.createTask( const taskMeta = await historyManager.createTask(
workspacePath, workspacePath,
"新对话" "新对话",
); );
historyManager.setPanelTask( historyManager.setPanelTask(
panelId, panelId,
taskMeta.taskId, taskMeta.taskId,
workspacePath workspacePath,
); );
} catch (error) { } catch (error) {
console.error("创建任务失败:", error); console.error("创建任务失败:", error);
@ -274,15 +354,20 @@ export async function showICHelperPanel(
// 切换到当前面板的任务上下文 // 切换到当前面板的任务上下文
historyManager.switchToPanelTask(panelId); historyManager.switchToPanelTask(panelId);
// 启动变更追踪会话
const sessionId = `session_${panelId}_${Date.now()}`;
startChangeSession(sessionId);
// 显示进度条 // 显示进度条
panel.webview.postMessage({ type: 'showProgress' }); panel.webview.postMessage({ type: "showProgress" });
handleUserMessage( handleUserMessage(
panel, panel,
message.text, message.text,
context.extensionPath, context.extensionPath,
message.mode, message.mode,
message.model // 传递服务等级 message.model, // 传递服务等级
message.contextItems, // 传递上下文项
); );
break; break;
case "readFile": case "readFile":
@ -299,7 +384,7 @@ export async function showICHelperPanel(
panel, panel,
message.filePath, message.filePath,
message.searchText, message.searchText,
message.replaceText message.replaceText,
); );
break; break;
case "insertCode": case "insertCode":
@ -311,7 +396,10 @@ export async function showICHelperPanel(
case "openWaveformViewer": case "openWaveformViewer":
// 在新列中打开波形查看器 // 在新列中打开波形查看器
if (message.vcdFilePath) { if (message.vcdFilePath) {
vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath); vscode.commands.executeCommand(
"ic-coder.openVCDViewer",
message.vcdFilePath,
);
} }
break; break;
case "getVCDInfo": case "getVCDInfo":
@ -329,7 +417,7 @@ export async function showICHelperPanel(
loadConversationHistory( loadConversationHistory(
panel, panel,
message.offset || 0, message.offset || 0,
message.limit || 10 message.limit || 10,
); );
break; break;
case "selectConversation": case "selectConversation":
@ -338,7 +426,7 @@ export async function showICHelperPanel(
selectConversation( selectConversation(
panel, panel,
message.conversationId, message.conversationId,
context.extensionPath context.extensionPath,
); );
} }
break; break;
@ -347,7 +435,8 @@ export async function showICHelperPanel(
void handleUserAnswer( void handleUserAnswer(
message.askId, message.askId,
message.selected, message.selected,
message.customInput message.customInput,
message.answers
); );
break; break;
// 新增:中止对话 // 新增:中止对话
@ -402,36 +491,133 @@ export async function showICHelperPanel(
// 退出登录 // 退出登录
vscode.commands.executeCommand("ic-coder.logout"); vscode.commands.executeCommand("ic-coder.logout");
break; 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":
// 打开文件 diff
if (message.changeId) {
await handleOpenFileDiff(panel, message.changeId);
}
break;
case "checkInvitationCode": case "checkInvitationCode":
// 检查邀请码验证状态 // 检查邀请码验证状态
{ {
const { InvitationService } = require("../services/invitationService"); // 先检查是否是试用用户
const isVerified = await InvitationService.isVerified(context); const { getCachedUserInfo } = require("../services/userService");
panel.webview.postMessage({ const userInfo = getCachedUserInfo();
command: "invitationCodeStatus",
verified: isVerified if (userInfo?.isPluginTrial === true) {
}); // 试用用户,跳过邀请码验证,直接返回已验证
console.log("[ICHelperPanel] 试用用户,跳过邀请码验证");
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":
// 检查是否需要显示欢迎弹窗
{
console.log("[ICHelperPanel] 收到 checkWelcomeModal 消息");
const userInfo = getCachedUserInfo();
console.log("[ICHelperPanel] 用户信息:", userInfo);
console.log("[ICHelperPanel] isPluginTrial:", userInfo?.isPluginTrial);
console.log("[ICHelperPanel] pluginTrialExpiresAt:", userInfo?.pluginTrialExpiresAt);
if (userInfo?.isPluginTrial === true) {
// undefined 表示无效,不显示
if (userInfo.pluginTrialExpiresAt === undefined) {
console.log("[ICHelperPanel] pluginTrialExpiresAt 未设置,不显示欢迎弹窗");
break;
}
// null 表示长期有效,显示弹窗
// 有值则检查是否过期
if (userInfo.pluginTrialExpiresAt !== null) {
const now = Date.now();
const isExpired = now >= userInfo.pluginTrialExpiresAt;
console.log("[ICHelperPanel] 是否过期:", isExpired);
if (isExpired) {
console.log("[ICHelperPanel] 试用已过期,不显示欢迎弹窗");
break;
}
}
// 未过期或长期有效(null),显示欢迎弹窗
console.log("[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端");
panel.webview.postMessage({
command: "showWelcomeModal",
});
} else {
console.log("[ICHelperPanel] 非试用用户");
}
}
break;
case "checkTrialExpiration":
// 检查试用期是否过期
{
console.log("[ICHelperPanel] 收到 checkTrialExpiration 消息");
const {
TrialExpirationService,
} = require("../services/trialExpirationService");
const trialService = new TrialExpirationService(context, panel);
const isExpired = await trialService.checkExpiration();
console.log("[ICHelperPanel] 试用期过期状态:", isExpired);
} }
break; break;
case "verifyInvitationCode": case "verifyInvitationCode":
// 验证邀请码 // 验证邀请码
{ {
const { InvitationService } = require("../services/invitationService"); const {
InvitationService,
} = require("../services/invitationService");
const result = await InvitationService.verifyCode(message.code); const result = await InvitationService.verifyCode(message.code);
if (result.success) { if (result.success) {
// 验证成功,保存状态 // 验证成功,保存状态
await InvitationService.saveVerificationStatus(context, message.code); await InvitationService.saveVerificationStatus(
context,
message.code,
);
panel.webview.postMessage({ panel.webview.postMessage({
command: "invitationCodeVerified", command: "invitationCodeVerified",
success: true success: true,
}); });
// 延迟显示欢迎弹窗,确保邀请码弹窗已关闭
setTimeout(() => {
panel.webview.postMessage({
command: "showNdtWelcomeModal",
});
}, 300);
} else { } else {
// 验证失败,返回错误信息 // 验证失败,返回错误信息
panel.webview.postMessage({ panel.webview.postMessage({
command: "invitationCodeVerified", command: "invitationCodeVerified",
success: false, success: false,
message: result.message message: result.message,
}); });
} }
} }
@ -440,6 +626,14 @@ export async function showICHelperPanel(
// 跳转到 IC Coder 官网 // 跳转到 IC Coder 官网
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com")); vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
break; break;
case "openTutorial":
// 打开使用教程
vscode.env.openExternal(
vscode.Uri.parse(
"https://www.iccoder.com/guides/quick-start/first-task-plugin",
),
);
break;
case "openUserManual": case "openUserManual":
// 打开用户手册 // 打开用户手册
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com")); vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
@ -447,7 +641,7 @@ export async function showICHelperPanel(
case "openUserFeedback": case "openUserFeedback":
// 打开用户反馈二维码弹窗 // 打开用户反馈二维码弹窗
panel.webview.postMessage({ panel.webview.postMessage({
command: "showFeedbackQRCode" command: "showFeedbackQRCode",
}); });
break; break;
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
@ -459,13 +653,16 @@ export async function showICHelperPanel(
mode: "agent", mode: "agent",
}); });
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划 // 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
} else if (message.action === "modify" || message.action === "cancel") { } else if (
message.action === "modify" ||
message.action === "cancel"
) {
void handlePlanAction( void handlePlanAction(
panel, panel,
message.action, message.action,
message.planTitle || "", message.planTitle || "",
context.extensionPath, context.extensionPath,
message.model message.model,
); );
} }
break; break;
@ -481,7 +678,7 @@ export async function showICHelperPanel(
// 获取工作区所有文件 // 获取工作区所有文件
const files = await vscode.workspace.findFiles( const files = await vscode.workspace.findFiles(
"**/*", "**/*",
"**/node_modules/**" "**/node_modules/**",
); );
panel.webview.postMessage({ panel.webview.postMessage({
@ -511,7 +708,11 @@ export async function showICHelperPanel(
try { try {
const items = fs.readdirSync(dir, { withFileTypes: true }); const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) { for (const item of items) {
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) { if (
item.isDirectory() &&
item.name !== "node_modules" &&
!item.name.startsWith(".")
) {
const fullPath = path.join(dir, item.name); const fullPath = path.join(dir, item.name);
const relativePath = path.relative(baseDir, fullPath); const relativePath = path.relative(baseDir, fullPath);
folders.push({ path: fullPath, relativePath }); folders.push({ path: fullPath, relativePath });
@ -540,7 +741,7 @@ export async function showICHelperPanel(
canSelectMany: true, canSelectMany: true,
openLabel: "选择图片", openLabel: "选择图片",
filters: { filters: {
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"], : ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
}, },
}); });
if (imageUris && imageUris.length > 0) { if (imageUris && imageUris.length > 0) {
@ -560,8 +761,8 @@ export async function showICHelperPanel(
canSelectMany: true, canSelectMany: true,
openLabel: "选择文档", openLabel: "选择文档",
filters: { filters: {
"文档文件": ["pdf", "doc", "docx", "txt", "md"], : ["pdf", "doc", "docx", "txt", "md"],
"所有文件": ["*"], : ["*"],
}, },
}); });
if (docUris && docUris.length > 0) { if (docUris && docUris.length > 0) {
@ -572,6 +773,23 @@ export async function showICHelperPanel(
} }
} }
break; break;
// 打开文件
case "openFile":
{
let filePath = message.filePath;
if (filePath) {
// 如果是相对路径,转换为绝对路径
if (!require("path").isAbsolute(filePath)) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
filePath = require("path").join(workspaceFolder.uri.fsPath, filePath);
}
}
const uri = vscode.Uri.file(filePath);
vscode.window.showTextDocument(uri);
}
}
break;
// 新增:检查工作区状态 // 新增:检查工作区状态
case "checkWorkspace": case "checkWorkspace":
const hasWorkspace = !!( const hasWorkspace = !!(
@ -583,7 +801,7 @@ export async function showICHelperPanel(
vscode.window vscode.window
.showWarningMessage( .showWarningMessage(
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊", "请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
"打开文件夹" "打开文件夹",
) )
.then((selection) => { .then((selection) => {
if (selection === "打开文件夹") { if (selection === "打开文件夹") {
@ -605,16 +823,16 @@ export async function showICHelperPanel(
break; break;
case "openICCoder": case "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"));
break; break;
case "logout": case "logout":
// 退出登录(前端已有确认对话框) // 退出登录(前端已有确认对话框)
vscode.commands.executeCommand('ic-coder.logout'); vscode.commands.executeCommand("ic-coder.logout");
break; break;
} }
}, },
undefined, undefined,
context.subscriptions context.subscriptions,
); );
// 面板关闭时清理任务映射 // 面板关闭时清理任务映射
@ -625,7 +843,7 @@ export async function showICHelperPanel(
historyManager.removePanelTask(panelId); historyManager.removePanelTask(panelId);
}, },
undefined, undefined,
context.subscriptions context.subscriptions,
); );
} }
@ -635,7 +853,7 @@ export async function showICHelperPanel(
async function getVCDFileInfo( async function getVCDFileInfo(
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
vcdFilePath: string, vcdFilePath: string,
containerId: string containerId: string,
) { ) {
try { try {
const fs = require("fs"); const fs = require("fs");
@ -773,7 +991,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
if (signalDef.width === 1) { if (signalDef.width === 1) {
// 单比特信号 // 单比特信号
const singleBitMatch = trimmedLine.match( const singleBitMatch = trimmedLine.match(
new RegExp(`^([01xz])${signalDef.identifier}$`) new RegExp(`^([01xz])${signalDef.identifier}$`),
); );
if (singleBitMatch) { if (singleBitMatch) {
values.push({ time: currentTime, value: singleBitMatch[1] }); values.push({ time: currentTime, value: singleBitMatch[1] });
@ -781,7 +999,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
} else { } else {
// 多比特信号 // 多比特信号
const multiBitMatch = trimmedLine.match( const multiBitMatch = trimmedLine.match(
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`) new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`),
); );
if (multiBitMatch) { if (multiBitMatch) {
values.push({ time: currentTime, value: multiBitMatch[1] }); values.push({ time: currentTime, value: multiBitMatch[1] });
@ -814,7 +1032,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
async function loadConversationHistory( async function loadConversationHistory(
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
offset: number = 0, offset: number = 0,
limit: number = 10 limit: number = 10,
) { ) {
try { try {
const historyManager = ChatHistoryManager.getInstance(); const historyManager = ChatHistoryManager.getInstance();
@ -835,7 +1053,7 @@ async function loadConversationHistory(
const result = await historyManager.getConversationHistoryList( const result = await historyManager.getConversationHistoryList(
workspacePath, workspacePath,
offset, offset,
limit limit,
); );
// 发送会话历史到前端 // 发送会话历史到前端
@ -863,7 +1081,7 @@ async function loadConversationHistory(
async function selectConversation( async function selectConversation(
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
taskId: string, taskId: string,
extensionPath: string extensionPath: string,
) { ) {
try { try {
const historyManager = ChatHistoryManager.getInstance(); const historyManager = ChatHistoryManager.getInstance();
@ -877,12 +1095,12 @@ async function selectConversation(
// 加载任务会话 // 加载任务会话
const taskSession = await historyManager.loadTaskSession( const taskSession = await historyManager.loadTaskSession(
workspacePath, workspacePath,
taskId taskId,
); );
if (!taskSession) { if (!taskSession) {
vscode.window.showErrorMessage( vscode.window.showErrorMessage(
`加载任务 ${taskId} 失败: 任务不存在或数据损坏` `加载任务 ${taskId} 失败: 任务不存在或数据损坏`,
); );
return; return;
} }
@ -1043,7 +1261,7 @@ async function selectConversation(
} }
vscode.window.showInformationMessage( vscode.window.showInformationMessage(
`已加载会话: ${taskSession.meta.taskName}` `已加载会话: ${taskSession.meta.taskName}`,
); );
} catch (error) { } catch (error) {
console.error("选择会话失败:", error); console.error("选择会话失败:", error);

View File

@ -6,8 +6,13 @@ import { VCDFileServer } from "../services/vcdFileServer";
/** /**
* VCD 波形查看器自定义编辑器提供者 * VCD 波形查看器自定义编辑器提供者
*/ */
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider { export class VCDViewerEditorProvider
public static register(context: vscode.ExtensionContext, vcdFileServer: VCDFileServer): vscode.Disposable { implements vscode.CustomReadonlyEditorProvider
{
public static register(
context: vscode.ExtensionContext,
vcdFileServer: VCDFileServer,
): vscode.Disposable {
const provider = new VCDViewerEditorProvider(context, vcdFileServer); const provider = new VCDViewerEditorProvider(context, vcdFileServer);
const providerRegistration = vscode.window.registerCustomEditorProvider( const providerRegistration = vscode.window.registerCustomEditorProvider(
"ic-coder.vcdViewer", "ic-coder.vcdViewer",
@ -16,20 +21,20 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
webviewOptions: { webviewOptions: {
retainContextWhenHidden: true, retainContextWhenHidden: true,
}, },
} },
); );
return providerRegistration; return providerRegistration;
} }
constructor( constructor(
private readonly context: vscode.ExtensionContext, private readonly context: vscode.ExtensionContext,
private readonly vcdFileServer: VCDFileServer private readonly vcdFileServer: VCDFileServer,
) {} ) {}
async openCustomDocument( async openCustomDocument(
uri: vscode.Uri, uri: vscode.Uri,
openContext: vscode.CustomDocumentOpenContext, openContext: vscode.CustomDocumentOpenContext,
token: vscode.CancellationToken token: vscode.CancellationToken,
): Promise<vscode.CustomDocument> { ): Promise<vscode.CustomDocument> {
return { return {
uri, uri,
@ -40,7 +45,7 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
async resolveCustomEditor( async resolveCustomEditor(
document: vscode.CustomDocument, document: vscode.CustomDocument,
webviewPanel: vscode.WebviewPanel, webviewPanel: vscode.WebviewPanel,
token: vscode.CancellationToken token: vscode.CancellationToken,
): Promise<void> { ): Promise<void> {
webviewPanel.webview.options = { webviewPanel.webview.options = {
enableScripts: true, enableScripts: true,
@ -52,7 +57,7 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
webviewPanel, webviewPanel,
this.context.extensionUri, this.context.extensionUri,
document.uri.fsPath, document.uri.fsPath,
this.vcdFileServer this.vcdFileServer,
); );
} }
} }
@ -68,7 +73,11 @@ export class VCDViewerPanel {
private _currentVcdPath: string | undefined; private _currentVcdPath: string | undefined;
private _vcdFileServer: VCDFileServer | undefined; private _vcdFileServer: VCDFileServer | undefined;
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) { private constructor(
panel: vscode.WebviewPanel,
extensionUri: vscode.Uri,
vcdFileServer?: VCDFileServer,
) {
this._panel = panel; this._panel = panel;
this._extensionUri = extensionUri; this._extensionUri = extensionUri;
this._vcdFileServer = vcdFileServer; this._vcdFileServer = vcdFileServer;
@ -91,7 +100,10 @@ export class VCDViewerPanel {
break; break;
case "loaded": case "loaded":
// Surfer iframe 加载完成,发送 VCD 文件 // Surfer iframe 加载完成,发送 VCD 文件
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath); console.log(
"[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:",
this._currentVcdPath,
);
if (this._currentVcdPath) { if (this._currentVcdPath) {
this.sendVcdToSurfer(this._currentVcdPath); this.sendVcdToSurfer(this._currentVcdPath);
} }
@ -99,14 +111,18 @@ export class VCDViewerPanel {
} }
}, },
null, null,
this._disposables this._disposables,
); );
} }
/** /**
* 创建或显示 VCD 查看器面板 * 创建或显示 VCD 查看器面板
*/ */
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) { public static createOrShow(
extensionUri: vscode.Uri,
vcdFilePath?: string,
vcdFileServer?: VCDFileServer,
) {
// 在当前活动编辑器旁边打开新列 // 在当前活动编辑器旁边打开新列
const column = vscode.ViewColumn.Beside; const column = vscode.ViewColumn.Beside;
@ -128,10 +144,14 @@ export class VCDViewerPanel {
enableScripts: true, enableScripts: true,
retainContextWhenHidden: true, retainContextWhenHidden: true,
localResourceRoots: [extensionUri], localResourceRoots: [extensionUri],
} },
); );
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer); VCDViewerPanel.currentPanel = new VCDViewerPanel(
panel,
extensionUri,
vcdFileServer,
);
// 如果提供了 VCD 文件路径,加载它 // 如果提供了 VCD 文件路径,加载它
if (vcdFilePath) { if (vcdFilePath) {
@ -146,7 +166,7 @@ export class VCDViewerPanel {
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
extensionUri: vscode.Uri, extensionUri: vscode.Uri,
vcdFilePath: string, vcdFilePath: string,
vcdFileServer?: VCDFileServer vcdFileServer?: VCDFileServer,
) { ) {
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer); const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
viewer.loadVCDFile(vcdFilePath); viewer.loadVCDFile(vcdFilePath);
@ -172,14 +192,14 @@ export class VCDViewerPanel {
// 更新面板标题 // 更新面板标题
const fileName = path.basename(vcdFilePath); const fileName = path.basename(vcdFilePath);
this._panel.title = `Surfer 波形查看器 - ${fileName}`; this._panel.title = `波形查看器 - ${fileName}`;
// 设置 HTML 内容 // 设置 HTML 内容
this._panel.webview.html = this._getWebviewContent(); this._panel.webview.html = this._getWebviewContent();
console.log("[VCDViewerPanel] Webview HTML 已设置"); console.log("[VCDViewerPanel] Webview HTML 已设置");
} catch (error) { } catch (error) {
vscode.window.showErrorMessage( vscode.window.showErrorMessage(
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}` `加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
); );
} }
} }
@ -190,8 +210,8 @@ export class VCDViewerPanel {
private parseVcdRootScope(vcdFilePath: string): string[] { private parseVcdRootScope(vcdFilePath: string): string[] {
try { try {
// 读取 VCD 文件 // 读取 VCD 文件
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' }); const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
const lines = buffer.split('\n'); const lines = buffer.split("\n");
const scopeNames: string[] = []; const scopeNames: string[] = [];
let scopeDepth = 0; let scopeDepth = 0;
@ -201,7 +221,7 @@ export class VCDViewerPanel {
const trimmed = line.trim(); const trimmed = line.trim();
// 遇到 $enddefinitions 就停止解析 // 遇到 $enddefinitions 就停止解析
if (trimmed.startsWith('$enddefinitions')) { if (trimmed.startsWith("$enddefinitions")) {
break; break;
} }
@ -212,22 +232,22 @@ export class VCDViewerPanel {
const scopeName = scopeMatch[2]; const scopeName = scopeMatch[2];
// 记录顶层 module (depth = 0) // 记录顶层 module (depth = 0)
if (scopeDepth === 0 && scopeType === 'module') { if (scopeDepth === 0 && scopeType === "module") {
scopeStack.push(scopeName); scopeStack.push(scopeName);
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName); console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
} }
// 记录顶层下的直接子模块 (depth = 1) // 记录顶层下的直接子模块 (depth = 1)
else if (scopeDepth === 1 && scopeType === 'module') { else if (scopeDepth === 1 && scopeType === "module") {
const fullPath = [...scopeStack, scopeName]; const fullPath = [...scopeStack, scopeName];
scopeNames.push(fullPath.join('.')); scopeNames.push(fullPath.join("."));
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.')); console.log("[VCDViewerPanel] 找到子模块:", fullPath.join("."));
} }
scopeDepth++; scopeDepth++;
} }
// 遇到 $upscope 减少深度 // 遇到 $upscope 减少深度
if (trimmed.startsWith('$upscope')) { if (trimmed.startsWith("$upscope")) {
scopeDepth--; scopeDepth--;
if (scopeDepth === 0) { if (scopeDepth === 0) {
scopeStack.pop(); scopeStack.pop();
@ -277,7 +297,7 @@ export class VCDViewerPanel {
} catch (error) { } catch (error) {
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error); console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
vscode.window.showErrorMessage( vscode.window.showErrorMessage(
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}` `发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`,
); );
} }
} }
@ -352,13 +372,23 @@ export class VCDViewerPanel {
private _getWebviewContent(): string { private _getWebviewContent(): string {
// 获取 surfer 资源 URI // 获取 surfer 资源 URI
const surferJsUri = this._panel.webview.asWebviewUri( const surferJsUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js") vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js"),
); );
const surferWasmUri = this._panel.webview.asWebviewUri( const surferWasmUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm") vscode.Uri.joinPath(
this._extensionUri,
"media",
"surfer",
"surfer_bg.wasm",
),
); );
const integrationJsUri = this._panel.webview.asWebviewUri( const integrationJsUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js") vscode.Uri.joinPath(
this._extensionUri,
"media",
"surfer",
"integration.js",
),
); );
return `<!DOCTYPE html> return `<!DOCTYPE html>
@ -367,7 +397,7 @@ export class VCDViewerPanel {
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
<title>Surfer 波形查看器</title> <title>波形查看器</title>
<script> <script>
// 获取 VS Code API只能调用一次 // 获取 VS Code API只能调用一次

153
src/panels/WelcomePanel.ts Normal file
View File

@ -0,0 +1,153 @@
/**
* 欢迎引导面板
* 功能:插件试用用户首次登录显示使用教程
* 依赖vscode
* 使用场景:试用用户首次登录时显示
*/
import * as vscode from 'vscode';
export class WelcomePanel {
public static currentPanel: WelcomePanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];
private constructor(panel: vscode.WebviewPanel) {
this._panel = panel;
this._panel.webview.html = this.getHtmlContent();
// 监听来自 webview 的消息
this._panel.webview.onDidReceiveMessage(
(message) => {
if (message.command === 'close') {
this._panel.dispose();
}
},
null,
this._disposables
);
// 监听关闭事件
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
}
public static render(context: vscode.ExtensionContext) {
// 避免重复显示
if (WelcomePanel.currentPanel) {
WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
return;
}
const panel = vscode.window.createWebviewPanel(
'icCoderWelcome',
'欢迎使用 IC Coder',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
WelcomePanel.currentPanel = new WelcomePanel(panel);
}
private getHtmlContent(): string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>欢迎使用 IC Coder</title>
<style>
body {
padding: 40px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
h1 {
color: var(--vscode-textLink-foreground);
margin-bottom: 20px;
}
.welcome-message {
font-size: 16px;
margin-bottom: 30px;
color: var(--vscode-descriptionForeground);
}
.step {
margin: 20px 0;
padding: 20px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 8px;
border-left: 4px solid var(--vscode-textLink-foreground);
}
.step h3 {
margin-top: 0;
color: var(--vscode-textLink-foreground);
}
.step p {
margin: 10px 0;
}
.button {
padding: 12px 24px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 20px;
}
.button:hover {
background: var(--vscode-button-hoverBackground);
}
</style>
</head>
<body>
<h1>🎉 欢迎使用 IC Coder</h1>
<p class="welcome-message">
您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
</p>
<div class="step">
<h3>📝 步骤 1打开聊天面板</h3>
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
</div>
<div class="step">
<h3>💬 步骤 2输入您的需求</h3>
<p>描述您想要生成的 Verilog 代码或需要帮助的问题AI 将为您提供专业的解决方案</p>
</div>
<div class="step">
<h3>🔬 步骤 3运行仿真</h3>
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
</div>
<button class="button" onclick="closePanel()">开始使用</button>
<script>
const vscode = acquireVsCodeApi();
function closePanel() {
vscode.postMessage({ command: 'close' });
}
</script>
</body>
</html>
`;
}
public dispose() {
WelcomePanel.currentPanel = undefined;
this._panel.dispose();
while (this._disposables.length) {
const disposable = this._disposables.pop();
if (disposable) {
disposable.dispose();
}
}
}
}

View File

@ -0,0 +1,210 @@
/**
* 文件变更追踪服务
* 功能:收集和管理 AI 修改文件的变更记录
* 依赖types/fileChanges
* 使用场景:在文件操作时记录变更,供用户审查
*/
import { FileChange, ChangeSession } from '../types/fileChanges';
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
class ChangeTrackerService {
private currentSession: ChangeSession | null = null;
private changeListeners: Array<(session: ChangeSession) => void> = [];
/**
* 开始新的变更会话
*/
startSession(sessionId: string): void {
// 如果已有 session无论状态重用并重置为 active
if (this.currentSession) {
this.currentSession.status = 'active';
return;
}
this.currentSession = {
sessionId,
startTime: Date.now(),
changes: [],
status: 'active'
};
}
/**
* 记录文件变更
*/
trackChange(filePath: string, oldContent: string, newContent: string): string {
if (!this.currentSession) {
this.startSession(`session_${Date.now()}`);
}
const changeId = `change_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 判断变更类型
let changeType: 'create' | 'modify' | 'delete';
if (oldContent === '' && newContent !== '') {
changeType = 'create';
} else if (oldContent !== '' && newContent === '') {
changeType = 'delete';
} else {
changeType = 'modify';
}
const change: FileChange = {
filePath,
oldContent,
newContent,
timestamp: Date.now(),
changeType,
changeId
};
this.currentSession!.changes.push(change);
this.notifyListeners();
return changeId;
}
/**
* 结束当前会话
*/
endSession(): ChangeSession | null {
if (this.currentSession && this.currentSession.changes.length > 0) {
this.currentSession.status = 'completed';
const session = this.currentSession;
this.notifyListeners();
return session;
}
return null;
}
/**
* 获取当前会话
*/
getCurrentSession(): ChangeSession | null {
return this.currentSession;
}
/**
* 清空当前会话
*/
clearSession(): void {
this.currentSession = null;
this.notifyListeners();
}
/**
* 移除指定的变更
*/
removeChange(changeId: string): boolean {
if (!this.currentSession) {
return false;
}
const index = this.currentSession.changes.findIndex(c => c.changeId === changeId);
if (index !== -1) {
this.currentSession.changes.splice(index, 1);
this.notifyListeners();
return true;
}
return false;
}
/**
* 监听变更
*/
onChangeUpdate(listener: (session: ChangeSession) => void): void {
this.changeListeners.push(listener);
}
/**
* 通知所有监听器
*/
private notifyListeners(): void {
if (this.currentSession) {
this.changeListeners.forEach(listener => listener(this.currentSession!));
}
}
/**
* 采纳变更(保存文件)
*/
async acceptChange(changeId: string): Promise<boolean> {
if (!this.currentSession) {
return false;
}
const change = this.currentSession.changes.find(c => c.changeId === changeId);
if (!change) {
return false;
}
try {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
return false;
}
const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath);
// 如果是删除操作,删除文件
if (change.changeType === 'delete') {
if (fs.existsSync(absolutePath)) {
await fs.promises.unlink(absolutePath);
}
} else {
// 创建或修改文件
await fs.promises.writeFile(absolutePath, change.newContent, 'utf-8');
}
this.removeChange(changeId);
return true;
} catch (error) {
console.error('[ChangeTracker] 采纳变更失败:', error);
return false;
}
}
/**
* 拒绝变更(恢复旧内容)
*/
async rejectChange(changeId: string): Promise<boolean> {
if (!this.currentSession) {
return false;
}
const change = this.currentSession.changes.find(c => c.changeId === changeId);
if (!change) {
return false;
}
try {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
return false;
}
const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath);
// 如果是新建文件,删除它
if (change.changeType === 'create') {
if (fs.existsSync(absolutePath)) {
await fs.promises.unlink(absolutePath);
}
} else {
// 恢复旧内容
await fs.promises.writeFile(absolutePath, change.oldContent, 'utf-8');
}
this.removeChange(changeId);
return true;
} catch (error) {
console.error('[ChangeTracker] 拒绝变更失败:', error);
return false;
}
}
}
export const changeTracker = new ChangeTrackerService();

View File

@ -25,6 +25,9 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
/** ExtensionContext 用于持久化存储 */ /** ExtensionContext 用于持久化存储 */
let extensionContext: vscode.ExtensionContext | null = null; let extensionContext: vscode.ExtensionContext | null = null;
/** 余额更新回调函数 */
let onBalanceUpdateCallback: ((balance: number) => void) | null = null;
/** /**
* 初始化 Credits 服务(设置 context * 初始化 Credits 服务(设置 context
*/ */
@ -39,6 +42,13 @@ export function initCreditsService(context: vscode.ExtensionContext): void {
} }
} }
/**
* 设置余额更新回调
*/
export function setBalanceUpdateCallback(callback: (balance: number) => void): void {
onBalanceUpdateCallback = callback;
}
/** /**
* 保存余额到持久化存储 * 保存余额到持久化存储
*/ */
@ -60,6 +70,10 @@ export function updateCachedBalance(balance: number): void {
saveBalance(balance).catch(err => { saveBalance(balance).catch(err => {
console.error('[CreditsService] 保存余额失败:', err); console.error('[CreditsService] 保存余额失败:', err);
}); });
// 通知前端更新余额显示
if (onBalanceUpdateCallback) {
onBalanceUpdateCallback(balance);
}
} }
/** /**

View File

@ -43,8 +43,7 @@ export interface MessageSegment {
toolResult?: string; toolResult?: string;
toolDescription?: string; toolDescription?: string;
askId?: string; askId?: string;
question?: string; questions?: import("../types/api").QuestionItem[];
options?: string[];
// 智能体相关字段 // 智能体相关字段
agentId?: string; agentId?: string;
agentName?: string; agentName?: string;
@ -97,7 +96,7 @@ export interface DialogCallbacks {
summary: string summary: string
) => void; ) => void;
/** 显示问题ask_user */ /** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void; onQuestion?: (askId: string, questions: import("../types/api").QuestionItem[]) => void;
/** 实时更新段落(流式过程中) */ /** 实时更新段落(流式过程中) */
onSegmentUpdate?: (segments: MessageSegment[]) => void; onSegmentUpdate?: (segments: MessageSegment[]) => void;
/** 对话完成,返回所有段落 */ /** 对话完成,返回所有段落 */
@ -449,7 +448,9 @@ export class DialogSession {
.showErrorMessage("登录已过期,请重新登录", "重新登录") .showErrorMessage("登录已过期,请重新登录", "重新登录")
.then((selection) => { .then((selection) => {
if (selection === "重新登录") { if (selection === "重新登录") {
vscode.commands.executeCommand("iccoder.login"); vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
} }
}); });
throw new Error("登录已过期,请重新登录"); throw new Error("登录已过期,请重新登录");
@ -645,8 +646,11 @@ export class DialogSession {
this.segments.push({ this.segments.push({
type: "question", type: "question",
askId: askId, askId: askId,
question: question, questions: [{
options: ["确认执行", "取消"], question: question,
options: ["确认执行", "取消"],
multiSelect: false
}],
}); });
// 实时发送段落更新 // 实时发送段落更新
@ -664,8 +668,11 @@ export class DialogSession {
await userInteractionManager.handleAskUser( await userInteractionManager.handleAskUser(
{ {
askId: askId, askId: askId,
question: question, questions: [{
options: ["确认执行", "取消"], question: question,
options: ["确认执行", "取消"],
multiSelect: false
}]
} as AskUserEvent, } as AskUserEvent,
this.taskId this.taskId
); );
@ -712,8 +719,11 @@ export class DialogSession {
// 注册问题到前端(类似 askUser以便用户回答时能找到 // 注册问题到前端(类似 askUser以便用户回答时能找到
const planEvent = { const planEvent = {
askId: askId, askId: askId,
question: `请确认执行计划:${data.title}`, questions: [{
options: ["确认执行", "修改计划", "取消"], question: `请确认执行计划:${data.title}`,
options: ["确认执行", "修改计划", "取消"],
multiSelect: false
}]
}; };
try { try {
await userInteractionManager.handleAskUser( await userInteractionManager.handleAskUser(
@ -854,13 +864,12 @@ export class DialogSession {
this.segments.push({ this.segments.push({
type: "question", type: "question",
askId: data.askId, askId: data.askId,
question: data.question, questions: data.questions,
options: data.options,
}); });
// 实时发送段落更新(包含问题) // 实时发送段落更新(包含问题)
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
// 同时调用 onQuestion 用于更新状态栏等 // 同时调用 onQuestion 用于更新状态栏等
callbacks.onQuestion?.(data.askId, data.question, data.options); callbacks.onQuestion?.(data.askId, data.questions);
try { try {
await userInteractionManager.handleAskUser(data, this.taskId); await userInteractionManager.handleAskUser(data, this.taskId);
} catch (error) { } catch (error) {
@ -894,7 +903,9 @@ export class DialogSession {
.showErrorMessage("登录状态已过期,请重新登录", "重新登录") .showErrorMessage("登录状态已过期,请重新登录", "重新登录")
.then((selection) => { .then((selection) => {
if (selection === "重新登录") { if (selection === "重新登录") {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
} }
}); });
// 登录过期错误已处理,不再传递给外部 // 登录过期错误已处理,不再传递给外部
@ -1106,7 +1117,8 @@ export class DialogSession {
async submitAnswer( async submitAnswer(
askId: string, askId: string,
selected?: string[], selected?: string[],
customInput?: string customInput?: string,
answers?: { [questionIndex: string]: string[] }
): Promise<void> { ): Promise<void> {
// 直接调用 receiveAnswer传递 taskId 作为 fallbackTaskId // 直接调用 receiveAnswer传递 taskId 作为 fallbackTaskId
// 如果 pendingQuestions 中有问题,走正常流程 // 如果 pendingQuestions 中有问题,走正常流程
@ -1115,6 +1127,7 @@ export class DialogSession {
askId, askId,
selected, selected,
customInput, customInput,
answers,
this.taskId this.taskId
); );
} }

View File

@ -176,6 +176,28 @@ export class ICCoderAuthenticationProvider
} }
} }
/**
* Clear local authentication state without window reload.
* Used by re-login flow when session is expired.
*/
async clearSessionsForRelogin(): Promise<void> {
if (this._sessions.length === 0) {
await clearUserInfo();
return;
}
const removed = [...this._sessions];
this._sessions = [];
await this.saveSessions();
await clearUserInfo();
this._onDidChangeSessions.fire({
added: [],
removed,
changed: [],
});
}
/** /**
* 生成会话 ID * 生成会话 ID
*/ */

View File

@ -1,8 +1,14 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as path from 'path'; import * as path from 'path';
// 使用 require 导入 node-notifier // 尝试加载 node-notifier,如果失败则使用 null
const notifier = require('node-notifier'); let notifier: any = null;
try {
notifier = require('node-notifier');
console.log('[NotificationService] node-notifier 加载成功');
} catch (error) {
console.log('[NotificationService] node-notifier 加载失败,将只使用 VS Code 内置通知');
}
/** /**
* 通知类型枚举 * 通知类型枚举
@ -114,6 +120,13 @@ export class NotificationService {
} }
console.log('[NotificationService] 通过防抖检查'); console.log('[NotificationService] 通过防抖检查');
// 如果 node-notifier 不可用,直接使用 VS Code 内置通知
if (!notifier) {
console.log('[NotificationService] node-notifier 不可用,使用 VS Code 内置通知');
this.showVSCodeNotification(title, message, type, onClick);
return;
}
// 使用 node-notifier 发送系统通知 // 使用 node-notifier 发送系统通知
console.log('[NotificationService] 使用 node-notifier 发送系统通知'); console.log('[NotificationService] 使用 node-notifier 发送系统通知');

View File

@ -2,21 +2,31 @@
* 工具执行器 * 工具执行器
* 接收后端的 tool_call 事件,执行本地工具,返回结果 * 接收后端的 tool_call 事件,执行本地工具,返回结果
*/ */
import * as vscode from 'vscode'; 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 } from "../utils/readFiles";
import { createOrOverwriteFile } from '../utils/createFiles'; import { createOrOverwriteFile } from "../utils/createFiles";
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner'; import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
import { analyzeVcdFile } from '../utils/vcdParser'; import { changeTracker } from "./changeTracker";
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer'; import {
generateVCD,
checkIverilogAvailable,
generateMultiVCD,
DumpModule,
} from "../utils/iverilogRunner";
import { analyzeVcdFile } from "../utils/vcdParser";
import {
executeWaveformTrace,
WaveformTraceArgs,
} from "../utils/waveformTracer";
import { import {
submitToolResult, submitToolResult,
createSuccessResult, createSuccessResult,
createBusinessErrorResult, createBusinessErrorResult,
createSystemErrorResult createSystemErrorResult,
} from './apiClient'; } from "./apiClient";
import type { import type {
ToolCallRequest, ToolCallRequest,
ToolName, ToolName,
@ -29,8 +39,8 @@ import type {
SimulationArgs, SimulationArgs,
WaveformSummaryArgs, WaveformSummaryArgs,
KnowledgeSaveArgs, KnowledgeSaveArgs,
KnowledgeLoadArgs KnowledgeLoadArgs,
} from '../types/api'; } from "../types/api";
/** /**
* 工具执行器上下文 * 工具执行器上下文
@ -49,7 +59,7 @@ export interface ToolExecutorContext {
*/ */
export async function executeToolCall( export async function executeToolCall(
request: ToolCallRequest, request: ToolCallRequest,
context: ToolExecutorContext context: ToolExecutorContext,
): Promise<void> { ): Promise<void> {
const toolName = request.params.name as ToolName; const toolName = request.params.name as ToolName;
const args = request.params.arguments; const args = request.params.arguments;
@ -61,37 +71,53 @@ export async function executeToolCall(
let resultText: string; let resultText: string;
switch (toolName) { switch (toolName) {
case 'file_read': case "file_read":
resultText = await executeFileRead(args as unknown as FileReadArgs); resultText = await executeFileRead(args as unknown as FileReadArgs);
break; break;
case 'file_write': case "file_write":
resultText = await executeFileWrite(args as unknown as FileWriteArgs); resultText = await executeFileWrite(args as unknown as FileWriteArgs);
break; break;
case 'file_delete': case "file_delete":
resultText = await executeFileDelete(args as unknown as FileDeleteArgs); resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
break; break;
case 'file_list': case "file_list":
resultText = await executeFileList(args as unknown as FileListArgs); resultText = await executeFileList(args as unknown as FileListArgs);
break; break;
case 'syntax_check': case "syntax_check":
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context); resultText = await executeSyntaxCheck(
args as unknown as SyntaxCheckArgs,
context,
);
break; break;
case 'iverilog': case "iverilog":
resultText = await executeIverilog(args as unknown as IverilogArgs, context); resultText = await executeIverilog(
args as unknown as IverilogArgs,
context,
);
break; break;
case 'simulation': case "simulation":
resultText = await executeSimulation(args as unknown as SimulationArgs, context); resultText = await executeSimulation(
args as unknown as SimulationArgs,
context,
);
break; break;
case 'waveform_summary': case "waveform_summary":
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs); resultText = await executeWaveformSummary(
args as unknown as WaveformSummaryArgs,
);
break; break;
case 'waveform_trace': case "waveform_trace":
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context); resultText = await executeWaveformTrace(
args as unknown as WaveformTraceArgs,
context,
);
break; break;
case 'knowledge_save': case "knowledge_save":
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs); resultText = await executeKnowledgeSave(
args as unknown as KnowledgeSaveArgs,
);
break; break;
case 'knowledge_load': case "knowledge_load":
resultText = await executeKnowledgeLoad(); resultText = await executeKnowledgeLoad();
break; break;
default: default:
@ -102,10 +128,12 @@ export async function executeToolCall(
const result = createSuccessResult(callId, resultText); const result = createSuccessResult(callId, resultText);
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(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error); console.error(
`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`,
error,
);
// 提交错误结果 // 提交错误结果
const result = createBusinessErrorResult(callId, errorMessage); const result = createBusinessErrorResult(callId, errorMessage);
@ -125,10 +153,21 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
* 执行 file_write 工具 * 执行 file_write 工具
*/ */
async function executeFileWrite(args: FileWriteArgs): Promise<string> { async function executeFileWrite(args: FileWriteArgs): Promise<string> {
const absolutePath = resolveWorkspaceFilePath(args.path);
const existedBeforeWrite = fs.existsSync(absolutePath);
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : "";
await createOrOverwriteFile(args.path, args.content); await createOrOverwriteFile(args.path, args.content);
// 记录文件变更
try {
changeTracker.trackChange(args.path, oldContent, args.content);
} catch (error) {
console.warn("[ToolExecutor] 记录文件变更失败:", error);
}
// Verilog 文件添加知识图谱提示 // Verilog 文件添加知识图谱提示
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv'); const isVerilogFile = args.path.endsWith(".v") || args.path.endsWith(".sv");
if (isVerilogFile) { if (isVerilogFile) {
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`; return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
} }
@ -138,7 +177,7 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
/** /**
* 执行 file_delete 工具 * 执行 file_delete 工具
* 删除指定路径的文件 * 删除指定路径的文件(带用户确认)
*/ */
async function executeFileDelete(args: FileDeleteArgs): Promise<string> { async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
const filePath = args.path; const filePath = args.path;
@ -146,7 +185,7 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
// 获取工作区路径 // 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders; const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) { if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区'); throw new Error("请先打开一个工作区");
} }
const workspacePath = workspaceFolders[0].uri.fsPath; const workspacePath = workspaceFolders[0].uri.fsPath;
@ -167,11 +206,60 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`); throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
} }
// 删除文件 // 验证文件路径在工作区内
fs.unlinkSync(absolutePath); const isInWorkspace = workspaceFolders.some((folder) =>
absolutePath.startsWith(folder.uri.fsPath),
);
if (!isInWorkspace) {
throw new Error("只能删除工作区内的文件");
}
// 保护敏感文件
const protectedFiles = [
"package.json",
"tsconfig.json",
".git",
"node_modules",
];
const fileName = path.basename(absolutePath);
if (protectedFiles.includes(fileName)) {
throw new Error(`不允许删除系统文件: ${fileName}`);
}
// 弹出确认对话框
const confirmed = await vscode.window.showWarningMessage(
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
{
modal: true, // 模态对话框,阻止其他操作
detail: "⚠️ 文件将被移到回收站,可以恢复",
},
"确定删除",
"取消",
);
// 用户取消或关闭对话框
if (confirmed !== "确定删除") {
throw new Error("用户取消了删除操作");
}
// 读取文件内容用于变更追踪
const oldContent = fs.readFileSync(absolutePath, "utf-8");
// 记录删除变更
const relativePath = path.relative(workspacePath, absolutePath);
changeTracker.trackChange(relativePath, oldContent, "");
// 删除文件(移到回收站)
const uri = vscode.Uri.file(absolutePath);
await vscode.workspace.fs.delete(uri, {
recursive: false, // 不是目录,设为 false
useTrash: true, // 移到回收站而非永久删除
});
// Verilog 文件添加知识图谱提示 // Verilog 文件添加知识图谱提示
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv'); const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv");
if (isVerilogFile) { if (isVerilogFile) {
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`; return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
} }
@ -183,13 +271,13 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
* 执行 file_list 工具 * 执行 file_list 工具
*/ */
async function executeFileList(args: FileListArgs): Promise<string> { 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 readDirectory(dirPath, extensions);
const fileList = files.map(f => f.path).join('\n'); const fileList = files.map((f) => f.path).join("\n");
return fileList || '(目录为空)'; return fileList || "(目录为空)";
} }
/** /**
@ -198,7 +286,7 @@ async function executeFileList(args: FileListArgs): Promise<string> {
*/ */
async function executeSyntaxCheck( async function executeSyntaxCheck(
args: SyntaxCheckArgs, args: SyntaxCheckArgs,
context: ToolExecutorContext context: ToolExecutorContext,
): Promise<string> { ): Promise<string> {
// 检查 iverilog 是否可用 // 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(context.extensionPath); const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
@ -212,33 +300,33 @@ async function executeSyntaxCheck(
try { try {
// 写入代码到临时文件 // 写入代码到临时文件
fs.writeFileSync(tempFile, args.code, 'utf-8'); fs.writeFileSync(tempFile, args.code, "utf-8");
// 调用 iverilog 进行语法检查 // 调用 iverilog 进行语法检查
const { spawn } = require('child_process'); const { spawn } = require("child_process");
const iverilogPath = getIverilogPath(context.extensionPath); const iverilogPath = getIverilogPath(context.extensionPath);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn(iverilogPath, ['-t', 'null', tempFile], { const child = spawn(iverilogPath, ["-t", "null", tempFile], {
cwd: tempDir, cwd: tempDir,
env: { env: {
...process.env, ...process.env,
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog') IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
} },
}); });
let stdout = ''; let stdout = "";
let stderr = ''; let stderr = "";
child.stdout.on('data', (data: Buffer) => { child.stdout.on("data", (data: Buffer) => {
stdout += data.toString(); stdout += data.toString();
}); });
child.stderr.on('data', (data: Buffer) => { child.stderr.on("data", (data: Buffer) => {
stderr += data.toString(); stderr += data.toString();
}); });
child.on('close', (code: number) => { child.on("close", (code: number) => {
// 清理临时文件 // 清理临时文件
try { try {
fs.unlinkSync(tempFile); fs.unlinkSync(tempFile);
@ -247,13 +335,13 @@ async function executeSyntaxCheck(
} }
if (code === 0) { if (code === 0) {
resolve('语法检查通过,无错误。'); resolve("语法检查通过,无错误。");
} else { } else {
resolve(`语法检查发现错误:\n${stderr || stdout}`); resolve(`语法检查发现错误:\n${stderr || stdout}`);
} }
}); });
child.on('error', (error: Error) => { child.on("error", (error: Error) => {
try { try {
fs.unlinkSync(tempFile); fs.unlinkSync(tempFile);
} catch (e) { } catch (e) {
@ -262,7 +350,6 @@ async function executeSyntaxCheck(
reject(error); reject(error);
}); });
}); });
} catch (error) { } catch (error) {
// 确保清理临时文件 // 确保清理临时文件
try { try {
@ -280,7 +367,7 @@ async function executeSyntaxCheck(
*/ */
async function executeIverilog( async function executeIverilog(
args: IverilogArgs, args: IverilogArgs,
context: ToolExecutorContext context: ToolExecutorContext,
): Promise<string> { ): Promise<string> {
// 检查 iverilog 是否可用 // 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(context.extensionPath); const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
@ -291,7 +378,7 @@ async function executeIverilog(
// 获取工作目录 // 获取工作目录
const workspaceFolders = vscode.workspace.workspaceFolders; const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) { if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('没有打开的工作区'); throw new Error("没有打开的工作区");
} }
const projectPath = workspaceFolders[0].uri.fsPath; const projectPath = workspaceFolders[0].uri.fsPath;
const workDir = args.workDir const workDir = args.workDir
@ -300,32 +387,32 @@ async function executeIverilog(
// 解析参数 // 解析参数
const iverilogPath = getIverilogPath(context.extensionPath); const iverilogPath = getIverilogPath(context.extensionPath);
const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0); const cmdArgs = args.args.split(/\s+/).filter((a) => a.length > 0);
const { spawn } = require('child_process'); const { spawn } = require("child_process");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn(iverilogPath, cmdArgs, { const child = spawn(iverilogPath, cmdArgs, {
cwd: workDir, cwd: workDir,
env: { env: {
...process.env, ...process.env,
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog') IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
} },
}); });
let stdout = ''; let stdout = "";
let stderr = ''; let stderr = "";
child.stdout.on('data', (data: Buffer) => { child.stdout.on("data", (data: Buffer) => {
stdout += data.toString(); stdout += data.toString();
}); });
child.stderr.on('data', (data: Buffer) => { child.stderr.on("data", (data: Buffer) => {
stderr += data.toString(); stderr += data.toString();
}); });
child.on('close', (code: number) => { child.on("close", (code: number) => {
const output = stderr || stdout || '(无输出)'; const output = stderr || stdout || "(无输出)";
if (code === 0) { if (code === 0) {
resolve(`执行成功\n${output}`); resolve(`执行成功\n${output}`);
} else { } else {
@ -333,7 +420,7 @@ async function executeIverilog(
} }
}); });
child.on('error', (error: Error) => { child.on("error", (error: Error) => {
reject(error); reject(error);
}); });
}); });
@ -344,12 +431,12 @@ async function executeIverilog(
*/ */
async function executeSimulation( async function executeSimulation(
args: SimulationArgs, args: SimulationArgs,
context: ToolExecutorContext context: ToolExecutorContext,
): Promise<string> { ): Promise<string> {
// 获取工作区路径 // 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders; const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) { if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区'); throw new Error("请先打开一个工作区");
} }
const projectPath = workspaceFolders[0].uri.fsPath; const projectPath = workspaceFolders[0].uri.fsPath;
@ -357,21 +444,24 @@ async function executeSimulation(
// 检查是否有 dumpModules 参数(多 VCD 模式) // 检查是否有 dumpModules 参数(多 VCD 模式)
if (args.dumpModules) { if (args.dumpModules) {
const modules = parseDumpModules(args.dumpModules); const modules = parseDumpModules(args.dumpModules);
const vcdDir = args.vcdDir || 'vcd'; const vcdDir = args.vcdDir || "vcd";
const result = await generateMultiVCD( const result = await generateMultiVCD(
projectPath, projectPath,
context.extensionPath, context.extensionPath,
args.tbPath, args.tbPath,
modules, modules,
vcdDir vcdDir,
); );
if (result.success) { if (result.success) {
const vcdList = result.vcdFiles const vcdList = result.vcdFiles
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`) .map(
.join('\n'); (f) =>
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`; `- ${f.moduleName}: ${f.success ? f.vcdPath : "失败 - " + f.error}`,
)
.join("\n");
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? "\n\n仿真输出:" + result.stdout : ""}`;
} else { } else {
throw new Error(result.message); throw new Error(result.message);
} }
@ -400,8 +490,8 @@ async function executeSimulation(
* 格式name:path,name:path * 格式name:path,name:path
*/ */
function parseDumpModules(dumpModules: string): DumpModule[] { function parseDumpModules(dumpModules: string): DumpModule[] {
return dumpModules.split(',').map(item => { return dumpModules.split(",").map((item) => {
const [name, modulePath] = item.trim().split(':'); const [name, modulePath] = item.trim().split(":");
return { name: name.trim(), path: modulePath.trim() }; return { name: name.trim(), path: modulePath.trim() };
}); });
} }
@ -410,13 +500,15 @@ function parseDumpModules(dumpModules: string): DumpModule[] {
* 执行 waveform_summary 工具 * 执行 waveform_summary 工具
* 解析 VCD 文件并返回波形摘要 * 解析 VCD 文件并返回波形摘要
*/ */
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> { async function executeWaveformSummary(
args: WaveformSummaryArgs,
): Promise<string> {
const { vcdPath, signals, checkpoints } = args; const { vcdPath, signals, checkpoints } = args;
// 获取工作区路径 // 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders; const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) { if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区'); throw new Error("请先打开一个工作区");
} }
const workspacePath = workspaceFolders[0].uri.fsPath; const workspacePath = workspaceFolders[0].uri.fsPath;
@ -447,17 +539,20 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> { async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
const workspaceFolder = getWorkspaceFolder(); const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolder) { if (!workspaceFolder) {
throw new Error('请先打开一个工作区'); throw new Error("请先打开一个工作区");
} }
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder'); const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, ".iccoder");
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json'); const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, "knowledge.json");
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区) // 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
await vscode.workspace.fs.createDirectory(iccoderDirUri); await vscode.workspace.fs.createDirectory(iccoderDirUri);
// 写入知识图谱UTF-8 // 写入知识图谱UTF-8
await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8')); await vscode.workspace.fs.writeFile(
knowledgeUri,
Buffer.from(args.data || "", "utf-8"),
);
return `知识图谱已保存: .iccoder/knowledge.json`; return `知识图谱已保存: .iccoder/knowledge.json`;
} }
@ -469,20 +564,33 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
async function executeKnowledgeLoad(): Promise<string> { async function executeKnowledgeLoad(): Promise<string> {
const workspaceFolder = getWorkspaceFolder(); const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolder) { if (!workspaceFolder) {
throw new Error('请先打开一个工作区'); throw new Error("请先打开一个工作区");
} }
const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json'); const knowledgeUri = vscode.Uri.joinPath(
workspaceFolder.uri,
".iccoder",
"knowledge.json",
);
try { try {
const bytes = await vscode.workspace.fs.readFile(knowledgeUri); const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
const content = Buffer.from(bytes).toString('utf-8'); const content = Buffer.from(bytes).toString("utf-8");
return content; return content;
} catch (error) { } catch (error) {
// 文件不存在:返回空图谱 // 文件不存在:返回空图谱
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') { if (
error instanceof vscode.FileSystemError &&
error.code === "FileNotFound"
) {
// 与后端 KnowledgeGraph 结构保持一致nodes/edges + nodeClass 多态字段) // 与后端 KnowledgeGraph 结构保持一致nodes/edges + nodeClass 多态字段)
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] }); return JSON.stringify({
taskId: "",
version: 1,
module: null,
nodes: [],
edges: [],
});
} }
throw error; throw error;
} }
@ -495,7 +603,9 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
} }
const activeUri = vscode.window.activeTextEditor?.document?.uri; const activeUri = vscode.window.activeTextEditor?.document?.uri;
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined; const activeFolder = activeUri
? vscode.workspace.getWorkspaceFolder(activeUri)
: undefined;
return activeFolder ?? folders[0]; return activeFolder ?? folders[0];
} }
@ -504,22 +614,24 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
*/ */
function getIverilogPath(extensionPath: string): string { function getIverilogPath(extensionPath: string): string {
const platform = process.platform; const platform = process.platform;
if (platform === 'win32') { if (platform === "win32") {
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe'); return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
} else { } else {
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog'); return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
} }
} }
/** /**
* 创建工具执行器上下文 * 创建工具执行器上下文
*/ */
export function createToolExecutorContext(extensionPath: string): ToolExecutorContext { export function createToolExecutorContext(
extensionPath: string,
): ToolExecutorContext {
const workspaceFolders = vscode.workspace.workspaceFolders; const workspaceFolders = vscode.workspace.workspaceFolders;
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || ''; const workspacePath = workspaceFolders?.[0]?.uri.fsPath || "";
return { return {
extensionPath, extensionPath,
workspacePath workspacePath,
}; };
} }

View File

@ -0,0 +1,62 @@
/**
* 试用期过期检测服务
* 功能:检查插件试用用户是否过期
* 依赖vscode, userService
* 使用场景:用户使用功能前检查是否过期
*/
import * as vscode from 'vscode';
import { getCachedUserInfo } from './userService';
export class TrialExpirationService {
private context: vscode.ExtensionContext;
private panel?: vscode.WebviewPanel;
constructor(context: vscode.ExtensionContext, panel?: vscode.WebviewPanel) {
this.context = context;
this.panel = panel;
}
/**
* 检查是否过期
* @returns true=已过期false=未过期
*/
public async checkExpiration(): Promise<boolean> {
const userInfo = getCachedUserInfo();
// 不是插件试用用户,不需要检查
if (!userInfo?.isPluginTrial) {
return false;
}
// 没有过期时间,不检查
if (!userInfo.pluginTrialExpiresAt) {
return false;
}
// 检查是否过期
const now = Date.now();
if (now >= userInfo.pluginTrialExpiresAt) {
// 已过期
await this.handleExpired();
return true;
}
return false;
}
/**
* 处理过期逻辑
*/
private async handleExpired(): Promise<void> {
// 通知前端显示过期弹窗
if (this.panel) {
this.panel.webview.postMessage({
command: 'showExpiredModal'
});
console.log('[TrialExpirationService] 已通知前端显示过期弹窗');
} else {
console.warn('[TrialExpirationService] panel 未提供,无法显示过期弹窗');
}
}
}

View File

@ -4,7 +4,7 @@
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { submitAnswer, submitToolConfirm } from './apiClient'; import { submitAnswer, submitToolConfirm } from './apiClient';
import type { AskUserEvent, AnswerRequest } from '../types/api'; import type { AskUserEvent, AnswerRequest, QuestionItem } from '../types/api';
/** /**
* 待处理的用户问题 * 待处理的用户问题
@ -12,8 +12,7 @@ import type { AskUserEvent, AnswerRequest } from '../types/api';
interface PendingQuestion { interface PendingQuestion {
askId: string; askId: string;
taskId: string; taskId: string;
question: string; questions: QuestionItem[];
options: string[];
resolve: (answer: string) => void; resolve: (answer: string) => void;
reject: (error: Error) => void; reject: (error: Error) => void;
} }
@ -45,9 +44,9 @@ export class UserInteractionManager {
* @param taskId 当前任务ID * @param taskId 当前任务ID
*/ */
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> { async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
const { askId, question, options } = event; const { askId, questions } = event;
console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`); console.log(`[UserInteraction] 收到问题: askId=${askId}, count=${questions.length}`);
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理 // 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
// 这里不再单独发送 showQuestion 命令,避免重复显示 // 这里不再单独发送 showQuestion 命令,避免重复显示
@ -57,8 +56,7 @@ export class UserInteractionManager {
this.pendingQuestions.set(askId, { this.pendingQuestions.set(askId, {
askId, askId,
taskId, taskId,
question, questions,
options,
resolve: (answer: string) => { resolve: (answer: string) => {
this.submitUserAnswer(askId, taskId, answer) this.submitUserAnswer(askId, taskId, answer)
.then(() => resolve()) .then(() => resolve())
@ -80,24 +78,38 @@ export class UserInteractionManager {
/** /**
* 处理用户提交的回答(从 WebView 调用) * 处理用户提交的回答(从 WebView 调用)
* @param askId 问题ID * @param askId 问题ID
* @param selected 选中的选项 * @param selected 选中的选项(旧格式)
* @param customInput 自定义输入 * @param customInput 自定义输入(旧格式)
* @param answers 新格式:按问题索引的答案
* @param fallbackTaskId 当问题不存在时使用的 taskId用于直接发送到后端 * @param fallbackTaskId 当问题不存在时使用的 taskId用于直接发送到后端
*/ */
async receiveAnswer( async receiveAnswer(
askId: string, askId: string,
selected?: string[], selected?: string[],
customInput?: string, customInput?: string,
answers?: { [questionIndex: string]: string[] },
fallbackTaskId?: string fallbackTaskId?: string
): Promise<void> { ): Promise<void> {
const pending = this.pendingQuestions.get(askId); const pending = this.pendingQuestions.get(askId);
const answer = customInput || selected?.join(', ') || '';
// 构建答案字符串
let answer = '';
if (answers && Object.keys(answers).length > 0) {
// 新格式:多问题答案
answer = Object.entries(answers)
.sort(([a], [b]) => parseInt(a) - parseInt(b))
.map(([_, vals]) => vals.join('; '))
.join(' | ');
} else {
// 旧格式:单问题答案
answer = customInput || selected?.join(', ') || '';
}
if (!pending) { if (!pending) {
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端 // 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
if (fallbackTaskId) { if (fallbackTaskId) {
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`); console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
await this.submitUserAnswer(askId, fallbackTaskId, answer); await this.submitUserAnswer(askId, fallbackTaskId, answer, answers);
} else { } else {
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`); console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
} }
@ -119,7 +131,8 @@ export class UserInteractionManager {
private async submitUserAnswer( private async submitUserAnswer(
askId: string, askId: string,
taskId: string, taskId: string,
answer: string answer: string,
answers?: { [questionIndex: string]: string[] }
): Promise<void> { ): Promise<void> {
// 检查是否是工具确认类型的问题 // 检查是否是工具确认类型的问题
if (askId.startsWith('tool_confirm_')) { if (askId.startsWith('tool_confirm_')) {
@ -148,7 +161,8 @@ export class UserInteractionManager {
const request: AnswerRequest = { const request: AnswerRequest = {
askId, askId,
taskId, taskId,
customInput: answer answers: answers,
customInput: answers ? undefined : answer
}; };
try { try {

View File

@ -117,6 +117,10 @@ export interface UserInfo {
}; };
// Credits 余额 // Credits 余额
credits?: number; credits?: number;
// 插件试用用户标识(从 JWT token 中提取)
isPluginTrial?: boolean;
// 试用到期时间(毫秒时间戳)
pluginTrialExpiresAt?: number;
} }
/** /**
@ -139,7 +143,7 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
// 处理响应数据 - 检查 code 是否为 200 // 处理响应数据 - 检查 code 是否为 200
if (response.code === 200 && response.user) { if (response.code === 200 && response.user) {
const user = response.user; const user = response.user;
return { const userInfo: UserInfo = {
userId: String(user.userId), userId: String(user.userId),
username: user.userName, username: user.userName,
nickname: user.nickName, nickname: user.nickName,
@ -151,6 +155,24 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
createTime: user.createTime, createTime: user.createTime,
loginDate: user.loginDate loginDate: user.loginDate
}; };
// 从接口响应中获取企业试用标识
if (response.isPluginTrial === true) {
userInfo.isPluginTrial = true;
console.log('[UserService] 从 getInfo 接口获取到 isPluginTrial: true');
}
// 获取试用到期时间null 表示长期有效)
if (response.enterpriseTrialExpires !== undefined) {
userInfo.pluginTrialExpiresAt = response.enterpriseTrialExpires;
if (response.enterpriseTrialExpires === null) {
console.log('[UserService] 试用长期有效');
} else {
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
}
}
return userInfo;
} }
console.error('[UserService] 获取用户信息失败:', response); console.error('[UserService] 获取用户信息失败:', response);
@ -313,6 +335,44 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
// 保存到持久化存储 // 保存到持久化存储
await saveUserInfo(userInfo); await saveUserInfo(userInfo);
// 判断是否是插件试用用户
console.log('[UserService] 检查用户类型isPluginTrial:', userInfo.isPluginTrial);
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
// 检查是否过期
const now = Date.now();
const isExpired = now >= userInfo.pluginTrialExpiresAt;
console.log('[UserService] 试用到期时间:', new Date(userInfo.pluginTrialExpiresAt).toLocaleString());
console.log('[UserService] 当前时间:', new Date(now).toLocaleString());
console.log('[UserService] 是否过期:', isExpired);
if (isExpired) {
// 已过期:显示邀请码弹窗
console.log('[UserService] 试用已过期,将显示邀请码弹窗');
} else {
// 未过期:显示欢迎弹窗
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
if (!hasWelcomed && extensionContext) {
await extensionContext.globalState.update('showWelcomeModal', true);
await extensionContext.globalState.update('pluginTrialWelcomed', true);
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
const checkMark = extensionContext.globalState.get('showWelcomeModal');
console.log('[UserService] 验证标记:', checkMark);
} else if (!extensionContext) {
console.error('[UserService] ❌ extensionContext 为 null无法设置标记');
} else {
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
}
}
} else {
// isPluginTrial=false 或 enterpriseTrialExpires 为 null显示邀请码弹窗
console.log('[UserService] 非试用用户或无过期时间,将显示邀请码弹窗');
}
return userInfo; return userInfo;
} catch (error) { } catch (error) {
console.error('[UserService] 获取用户信息失败:', error); console.error('[UserService] 获取用户信息失败:', error);

View File

@ -5,7 +5,7 @@ import * as vscode from "vscode";
/** /**
* VCD 文件 HTTP 服务器 * VCD 文件 HTTP 服务器
* 用于为 Surfer 波形查看器提供 VCD 文件访问 * 用于为 波形查看器提供 VCD 文件访问
*/ */
export class VCDFileServer { export class VCDFileServer {
private server: http.Server | null = null; private server: http.Server | null = null;
@ -98,7 +98,10 @@ export class VCDFileServer {
/** /**
* 处理 HTTP 请求 * 处理 HTTP 请求
*/ */
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void { private handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
): void {
const url = req.url || ""; const url = req.url || "";
console.log(`[VCDFileServer] 收到请求: ${url}`); console.log(`[VCDFileServer] 收到请求: ${url}`);
@ -214,7 +217,12 @@ export class VCDFileServer {
} }
const fileName = match[1]; const fileName = match[1];
const filePath = path.join(this.extensionUri.fsPath, "media", "surfer", fileName); const filePath = path.join(
this.extensionUri.fsPath,
"media",
"surfer",
fileName,
);
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`); console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
@ -257,8 +265,8 @@ export class VCDFileServer {
*/ */
private parseVcdRootScope(vcdFilePath: string): string[] { private parseVcdRootScope(vcdFilePath: string): string[] {
try { try {
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' }); const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
const lines = buffer.split('\n'); const lines = buffer.split("\n");
const scopeNames: string[] = []; const scopeNames: string[] = [];
let scopeDepth = 0; let scopeDepth = 0;
@ -267,7 +275,7 @@ export class VCDFileServer {
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
if (trimmed.startsWith('$enddefinitions')) { if (trimmed.startsWith("$enddefinitions")) {
break; break;
} }
@ -276,17 +284,17 @@ export class VCDFileServer {
const scopeType = scopeMatch[1]; const scopeType = scopeMatch[1];
const scopeName = scopeMatch[2]; const scopeName = scopeMatch[2];
if (scopeDepth === 0 && scopeType === 'module') { if (scopeDepth === 0 && scopeType === "module") {
scopeStack.push(scopeName); scopeStack.push(scopeName);
} else if (scopeDepth === 1 && scopeType === 'module') { } else if (scopeDepth === 1 && scopeType === "module") {
const fullPath = [...scopeStack, scopeName]; const fullPath = [...scopeStack, scopeName];
scopeNames.push(fullPath.join('.')); scopeNames.push(fullPath.join("."));
} }
scopeDepth++; scopeDepth++;
} }
if (trimmed.startsWith('$upscope')) { if (trimmed.startsWith("$upscope")) {
scopeDepth--; scopeDepth--;
if (scopeDepth === 0) { if (scopeDepth === 0) {
scopeStack.pop(); scopeStack.pop();
@ -323,7 +331,7 @@ export class VCDFileServer {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Surfer 波形查看器 - ${fileName}</title> <title>波形查看器 - ${fileName}</title>
<script> <script>
window.surferReady = false; window.surferReady = false;
window.pendingVcdData = null; window.pendingVcdData = null;

View File

@ -194,11 +194,17 @@ export interface PlanSummaryUpdateEvent {
timestamp: number; timestamp: number;
} }
/** 单个问题项 */
export interface QuestionItem {
question: string;
options: string[];
multiSelect?: boolean;
}
/** ask_user 事件数据 */ /** ask_user 事件数据 */
export interface AskUserEvent { export interface AskUserEvent {
askId: string; askId: string;
question: string; questions: QuestionItem[];
options: string[];
} }
/** complete 事件数据 */ /** complete 事件数据 */
@ -351,10 +357,12 @@ export interface AnswerRequest {
askId: string; askId: string;
/** 任务ID */ /** 任务ID */
taskId: string; taskId: string;
/** 选中的选项列表 */ /** 选中的选项列表(旧格式,兼容) */
selected?: string[]; selected?: string[];
/** 自定义输入内容 */ /** 自定义输入内容(旧格式,兼容) */
customInput?: string; customInput?: string;
/** 新格式:按问题索引的答案 */
answers?: { [questionIndex: string]: string[] };
} }
/** 用户回答响应 */ /** 用户回答响应 */
@ -407,6 +415,10 @@ export interface UserInfoResponse {
isDefaultModifyPwd: boolean; isDefaultModifyPwd: boolean;
/** 密码是否过期 */ /** 密码是否过期 */
isPasswordExpired: boolean; isPasswordExpired: boolean;
/** 是否为插件试用用户 */
isPluginTrial?: boolean;
/** 企业试用到期时间(毫秒时间戳) */
enterpriseTrialExpires?: number;
/** 用户信息 */ /** 用户信息 */
user: { user: {
userId: number; userId: number;
@ -419,6 +431,7 @@ export interface UserInfoResponse {
status?: string; status?: string;
createTime?: string; createTime?: string;
loginDate?: string; loginDate?: string;
remark?: string;
[key: string]: any; [key: string]: any;
}; };
} }

47
src/types/fileChanges.ts Normal file
View File

@ -0,0 +1,47 @@
/**
* 文件变更追踪类型定义
* 功能:定义代码变更的数据结构
* 依赖:无
* 使用场景AI 修改文件后的变更审查
*/
/**
* 单个文件的变更记录
*/
export interface FileChange {
/** 文件相对路径 */
filePath: string;
/** 修改前的内容 */
oldContent: string;
/** 修改后的内容 */
newContent: string;
/** 变更时间戳 */
timestamp: number;
/** 变更类型 */
changeType: 'create' | 'modify' | 'delete';
/** 变更 ID唯一标识 */
changeId: string;
}
/**
* 变更会话(一次对话的所有变更)
*/
export interface ChangeSession {
/** 会话 ID */
sessionId: string;
/** 会话开始时间 */
startTime: number;
/** 所有文件变更 */
changes: FileChange[];
/** 会话状态 */
status: 'active' | 'completed';
}
/**
* 变更操作结果
*/
export interface ChangeActionResult {
success: boolean;
message: string;
changeId: string;
}

203
src/utils/diffRenderer.ts Normal file
View File

@ -0,0 +1,203 @@
/**
* Diff 渲染工具
* 功能:生成代码差异的 HTML 展示
* 依赖:无
* 使用场景:在变更面板中展示文件修改的 diff
*/
interface DiffLine {
type: 'add' | 'remove' | 'context';
content: string;
oldLineNumber?: number;
newLineNumber?: number;
}
/**
* 简单的 diff 算法(基于行)
*/
export function generateDiff(oldContent: string, newContent: string): DiffLine[] {
const oldLines = oldContent.split('\n');
const newLines = newContent.split('\n');
const result: DiffLine[] = [];
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldLines.length || newIndex < newLines.length) {
const oldLine = oldLines[oldIndex];
const newLine = newLines[newIndex];
if (oldIndex >= oldLines.length) {
// 只剩新行
result.push({
type: 'add',
content: newLine,
newLineNumber: newIndex + 1
});
newIndex++;
} else if (newIndex >= newLines.length) {
// 只剩旧行
result.push({
type: 'remove',
content: oldLine,
oldLineNumber: oldIndex + 1
});
oldIndex++;
} else if (oldLine === newLine) {
// 相同行
result.push({
type: 'context',
content: oldLine,
oldLineNumber: oldIndex + 1,
newLineNumber: newIndex + 1
});
oldIndex++;
newIndex++;
} else {
// 不同行,标记为删除和添加
result.push({
type: 'remove',
content: oldLine,
oldLineNumber: oldIndex + 1
});
result.push({
type: 'add',
content: newLine,
newLineNumber: newIndex + 1
});
oldIndex++;
newIndex++;
}
}
return result;
}
/**
* 将 diff 结果渲染为 HTML
*/
export function renderDiffHtml(diffLines: DiffLine[]): string {
let html = '<div class="diff-viewer">';
for (const line of diffLines) {
const lineClass = `diff-line diff-line-${line.type}`;
const oldNum = line.oldLineNumber ? `<span class="line-num">${line.oldLineNumber}</span>` : '<span class="line-num"></span>';
const newNum = line.newLineNumber ? `<span class="line-num">${line.newLineNumber}</span>` : '<span class="line-num"></span>';
const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' ';
const escapedContent = escapeHtml(line.content);
html += `
<div class="${lineClass}">
${oldNum}
${newNum}
<span class="line-prefix">${prefix}</span>
<span class="line-content">${escapedContent}</span>
</div>
`;
}
html += '</div>';
return html;
}
/**
* 转义 HTML 特殊字符
*/
function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
/**
* 获取 diff 样式
*/
export function getDiffStyles(): string {
return `
.diff-viewer {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
overflow: hidden;
}
.diff-line {
display: flex;
align-items: center;
padding: 4px 0;
white-space: pre;
transition: background 0.15s;
}
.diff-line:hover {
background: var(--vscode-list-hoverBackground);
}
.diff-line-add {
background: rgba(40, 167, 69, 0.2);
border-left: 3px solid #28a745;
}
.diff-line-add:hover {
background: rgba(40, 167, 69, 0.25);
}
.diff-line-remove {
background: rgba(220, 53, 69, 0.2);
border-left: 3px solid #dc3545;
}
.diff-line-remove:hover {
background: rgba(220, 53, 69, 0.25);
}
.diff-line-context {
background: transparent;
border-left: 3px solid transparent;
}
.line-num {
display: inline-block;
width: 45px;
text-align: right;
padding: 0 10px;
color: var(--vscode-editorLineNumber-foreground);
user-select: none;
flex-shrink: 0;
font-size: 11px;
opacity: 0.7;
}
.line-prefix {
display: inline-block;
width: 24px;
text-align: center;
font-weight: bold;
flex-shrink: 0;
font-size: 14px;
}
.diff-line-add .line-prefix {
color: #28a745;
}
.diff-line-remove .line-prefix {
color: #dc3545;
}
.line-content {
flex: 1;
padding: 0 12px 0 8px;
overflow-x: auto;
}
`;
}

51
src/utils/fileDiff.ts Normal file
View File

@ -0,0 +1,51 @@
import * as vscode from "vscode";
import * as path from "path";
/**
* 将相对路径解析为工作区绝对路径
*/
export function resolveWorkspaceFilePath(filePath: string): string {
if (path.isAbsolute(filePath)) {
return filePath;
}
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error("请先打开一个文件夹作为工作区,这样我就能为您修改文件了");
}
return path.join(workspaceFolders[0].uri.fsPath, filePath);
}
/**
* 使用 VS Code 原生 diff 视图展示文件修改前后对比
*/
export async function showFileDiff(
filePath: string,
oldContent: string,
title?: string
): Promise<void> {
const absolutePath = resolveWorkspaceFilePath(filePath);
const fileUri = vscode.Uri.file(absolutePath);
const newBytes = await vscode.workspace.fs.readFile(fileUri);
const newContent = Buffer.from(newBytes).toString("utf-8");
if (oldContent === newContent) {
return;
}
const language = (await vscode.workspace.openTextDocument(fileUri)).languageId;
const oldDoc = await vscode.workspace.openTextDocument({
content: oldContent,
language,
});
const diffTitle = title || `${path.basename(absolutePath)} (修改前 <-> 修改后)`;
await vscode.commands.executeCommand(
"vscode.diff",
oldDoc.uri,
fileUri,
diffTitle,
{ preview: false }
);
}

View File

@ -11,6 +11,7 @@ export interface JwtPayload {
user_id?: number; // 用户ID (下划线命名) user_id?: number; // 用户ID (下划线命名)
exp?: number; // 过期时间 exp?: number; // 过期时间
iat?: number; // 签发时间 iat?: number; // 签发时间
ispluginTrial?: boolean; // 是否是插件试用用户
[key: string]: unknown; [key: string]: unknown;
} }
@ -102,3 +103,24 @@ export function isTokenExpired(
return isExpired; return isExpired;
} }
/**
* 从 JWT token 中获取 ispluginTrial 标识
* @param token JWT token
* @returns true=插件试用用户false=正式用户null=无法判断
*/
export function getIsPluginTrialFromToken(token: string): boolean | null {
const payload = parseJwtPayload(token);
if (!payload) {
return null;
}
// 检查 ispluginTrial 字段
if (payload.ispluginTrial !== undefined) {
console.log("[JWT] 从 token 中获取到 ispluginTrial:", payload.ispluginTrial);
return payload.ispluginTrial === true;
}
console.log("[JWT] token 中没有 ispluginTrial 字段,判定为正式用户");
return false;
}

View File

@ -25,6 +25,10 @@ import {
} from "../services/creditsService"; } 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 { showFileDiff } from "./fileDiff";
import { changeTracker } from "../services/changeTracker";
import { generateDiff, renderDiffHtml } from "./diffRenderer";
import type { RunMode, ServiceTier } from "../types/api"; import type { RunMode, ServiceTier } from "../types/api";
@ -37,6 +41,14 @@ let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */ /** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null; let lastTaskId: string | null = null;
async function trackFileChange(filePath: string, oldContent: string, newContent: string): Promise<void> {
try {
changeTracker.trackChange(filePath, oldContent, newContent);
} catch (error) {
console.warn("[MessageHandler] 记录文件变更失败:", error);
}
}
/** /**
* 处理用户消息 * 处理用户消息
*/ */
@ -45,7 +57,8 @@ export async function handleUserMessage(
text: string, text: string,
extensionPath?: string, extensionPath?: string,
mode?: RunMode, mode?: RunMode,
serviceTier?: ServiceTier // 新增:服务等级参数 serviceTier?: ServiceTier, // 服务等级参数
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
) { ) {
console.log("收到用户消息:", text); console.log("收到用户消息:", text);
@ -79,7 +92,9 @@ export async function handleUserMessage(
); );
if (action === '立即登录') { if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
} }
// 恢复输入状态 // 恢复输入状态
@ -113,7 +128,9 @@ export async function handleUserMessage(
); );
if (action === '立即登录') { if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
} }
// 恢复输入状态 // 恢复输入状态
@ -124,6 +141,21 @@ export async function handleUserMessage(
}); });
return; return;
} }
// 检查试用期是否过期
const trialService = new TrialExpirationService(context, panel);
const isExpired = await trialService.checkExpiration();
if (isExpired) {
console.warn("[MessageHandler] 试用期已过期,阻止发送");
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
} }
// 记录用户消息到历史(允许失败,不阻塞主流程) // 记录用户消息到历史(允许失败,不阻塞主流程)
@ -183,7 +215,8 @@ export async function handleUserMessage(
extensionPath, extensionPath,
mode, mode,
undefined, undefined,
serviceTier serviceTier,
contextItems
); );
return; return;
} catch (error) { } catch (error) {
@ -220,10 +253,19 @@ async function handleUserMessageWithBackend(
extensionPath: string, extensionPath: string,
mode?: RunMode, mode?: RunMode,
reuseTaskId?: string, // 可选,复用现有 taskId用于 Plan 模式确认后继续执行) reuseTaskId?: string, // 可选,复用现有 taskId用于 Plan 模式确认后继续执行)
serviceTier?: ServiceTier // 新增:服务等级参数 serviceTier?: ServiceTier, // 服务等级参数
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
): Promise<void> { ): Promise<void> {
const historyManager = ChatHistoryManager.getInstance(); const historyManager = ChatHistoryManager.getInstance();
// 处理上下文项:在消息前附加文件/文件夹路径
let enhancedText = text;
if (contextItems && contextItems.length > 0) {
console.log("[MessageHandler] 处理上下文项:", contextItems.length);
const paths = contextItems.map(item => item.path).join('\n');
enhancedText = `${paths}\n\n${text}`;
}
// 获取 historyManager 中的 taskId由 ICHelperPanel 创建) // 获取 historyManager 中的 taskId由 ICHelperPanel 创建)
// 优先使用 reuseTaskId其次使用 historyManager 的 taskId // 优先使用 reuseTaskId其次使用 historyManager 的 taskId
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId(); const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
@ -251,7 +293,7 @@ async function handleUserMessageWithBackend(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
currentSession!.sendMessage( currentSession!.sendMessage(
text, enhancedText,
{ {
onText: (fullText, isStreaming) => { onText: (fullText, isStreaming) => {
// 不再单独处理文本,统一通过 onSegmentUpdate 处理 // 不再单独处理文本,统一通过 onSegmentUpdate 处理
@ -282,7 +324,7 @@ async function handleUserMessageWithBackend(
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新 // 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
}, },
onQuestion: (askId, question, options) => { onQuestion: (askId: string, questions: import("../types/api").QuestionItem[]) => {
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理 // 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
panel.webview.postMessage({ panel.webview.postMessage({
command: "updateStatus", command: "updateStatus",
@ -327,13 +369,12 @@ async function handleUserMessageWithBackend(
command: "hideStatus", command: "hideStatus",
}); });
// 最后一次发送完整的段落 // 发送完成标记(不再重复发送 segments避免内容重复显示
const result = await panel.webview.postMessage({ panel.webview.postMessage({
command: "updateSegments", command: "updateSegments",
segments: segments, segments: [],
isComplete: true, isComplete: true,
}); });
console.log("[MessageHandler] postMessage 返回值:", result);
// 发送系统通知 - AI 响应完成 // 发送系统通知 - AI 响应完成
const notificationService = NotificationService.getInstance(); const notificationService = NotificationService.getInstance();
@ -345,6 +386,9 @@ async function handleUserMessageWithBackend(
panel.reveal(); panel.reveal();
} }
); );
// 发送代码变更到前端
sendChangesToWebview(panel);
} catch (error) { } catch (error) {
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error); console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
} }
@ -425,10 +469,11 @@ async function handleUserMessageWithBackend(
export async function handleUserAnswer( export async function handleUserAnswer(
askId: string, askId: string,
selected?: string[], selected?: string[],
customInput?: string customInput?: string,
answers?: { [questionIndex: string]: string[] }
): Promise<void> { ): Promise<void> {
if (currentSession) { if (currentSession) {
await currentSession.submitAnswer(askId, selected, customInput); await currentSession.submitAnswer(askId, selected, customInput, answers);
} }
} }
@ -747,11 +792,14 @@ async function handleFileOperation(
if (!operation.searchText || !operation.replaceText) { if (!operation.searchText || !operation.replaceText) {
throw new Error("缺少替换内容"); throw new Error("缺少替换内容");
} }
const oldContentBeforeReplace = await readFileContent(operation.filePath);
await replaceFile( await replaceFile(
operation.filePath, operation.filePath,
operation.searchText, operation.searchText,
operation.replaceText operation.replaceText
); );
const newContentAfterReplace = await readFileContent(operation.filePath);
await trackFileChange(operation.filePath, oldContentBeforeReplace, newContentAfterReplace);
responseText = `✅ 文件内容替换成功: ${operation.filePath}`; responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
@ -887,7 +935,9 @@ export async function handleUpdateFile(
content: string content: string
) { ) {
try { try {
const oldContent = await readFileContent(filePath);
await updateFile(filePath, content); await updateFile(filePath, content);
await trackFileChange(filePath, oldContent, content);
panel.webview.postMessage({ panel.webview.postMessage({
command: "fileUpdated", command: "fileUpdated",
filePath: filePath, filePath: filePath,
@ -952,7 +1002,10 @@ export async function handleReplaceInFile(
replaceText: string replaceText: string
) { ) {
try { try {
const oldContent = await readFileContent(filePath);
await replaceFile(filePath, searchText, replaceText); await replaceFile(filePath, searchText, replaceText);
const newContent = await readFileContent(filePath);
await trackFileChange(filePath, oldContent, newContent);
panel.webview.postMessage({ panel.webview.postMessage({
command: "fileReplaced", command: "fileReplaced",
filePath: filePath, filePath: filePath,
@ -1209,3 +1262,165 @@ export async function handleOptimizePrompt(
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`); vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
} }
} }
/**
* 处理采纳变更
*/
export async function handleAcceptChange(
panel: vscode.WebviewPanel,
changeId: string
) {
try {
const success = await changeTracker.acceptChange(changeId);
if (success) {
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: true
});
} else {
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: false,
error: "采纳变更失败"
});
}
} catch (error) {
console.error("[MessageHandler] 采纳变更失败:", error);
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: false,
error: String(error)
});
}
}
/**
* 处理拒绝变更
*/
export async function handleRejectChange(
panel: vscode.WebviewPanel,
changeId: string
) {
try {
const success = await changeTracker.rejectChange(changeId);
if (success) {
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: true
});
} else {
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: false,
error: "拒绝变更失败"
});
}
} catch (error) {
console.error("[MessageHandler] 拒绝变更失败:", error);
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: false,
error: String(error)
});
}
}
/**
* 在对话结束时发送变更列表到前端
*/
export function sendChangesToWebview(panel: vscode.WebviewPanel) {
const session = changeTracker.endSession();
if (session && session.changes.length > 0) {
const changesWithDiff = session.changes.map(change => {
const diffLines = generateDiff(change.oldContent, change.newContent);
const diffHtml = renderDiffHtml(diffLines);
return {
...change,
diffHtml
};
});
panel.webview.postMessage({
command: "showChanges",
changes: changesWithDiff
});
}
}
/**
* 开始新的变更会话
*/
export function startChangeSession(sessionId: string) {
changeTracker.startSession(sessionId);
}
/**
* 打开文件 diff 编辑器
*/
export async function handleOpenFileDiff(
panel: vscode.WebviewPanel,
changeId: string
) {
try {
const session = changeTracker.getCurrentSession();
if (!session) {
vscode.window.showErrorMessage('没有找到变更会话');
return;
}
const change = session.changes.find(c => c.changeId === changeId);
if (!change) {
vscode.window.showErrorMessage('没有找到该变更');
return;
}
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showErrorMessage('没有打开的工作区');
return;
}
// 创建临时文件用于对比
const filePath = change.filePath;
const absolutePath = vscode.Uri.file(
path.join(workspaceFolder.uri.fsPath, filePath)
);
// 创建虚拟文档显示旧内容
const oldUri = vscode.Uri.parse(
`ic-coder-diff:${filePath}.old?${changeId}`
).with({ scheme: 'ic-coder-diff' });
// 注册文档内容提供者(如果还没注册)
if (!(global as any).__diffProviderRegistered) {
const provider = new (class implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri): string {
const changeId = uri.query;
const session = changeTracker.getCurrentSession();
const change = session?.changes.find(c => c.changeId === changeId);
return change?.oldContent || '';
}
})();
vscode.workspace.registerTextDocumentContentProvider('ic-coder-diff', provider);
(global as any).__diffProviderRegistered = true;
}
// 打开 diff 编辑器
await vscode.commands.executeCommand(
'vscode.diff',
oldUri,
absolutePath,
`${filePath} (变更对比)`
);
} catch (error) {
console.error('[MessageHandler] 打开 diff 失败:', error);
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
}
}

View File

@ -92,7 +92,8 @@ export async function executeWaveformTrace(
child.on('close', (code: number | null) => { child.on('close', (code: number | null) => {
if (code === 0) { if (code === 0) {
resolve(stdout); // 成功时返回 stdout忽略 stderr 中的进度信息
resolve(stdout || stderr);
} else { } else {
reject(new Error( reject(new Error(
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}` `waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`

View File

@ -28,7 +28,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
retainContextWhenHidden: true, retainContextWhenHidden: true,
localResourceRoots: [ localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"), vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets") vscode.Uri.joinPath(context.extensionUri, "dist", "assets")
], ],
} }
); );
@ -47,21 +47,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
// 获取模型图标URI // 获取模型图标URI
const autoIconUri = panel.webview.asWebviewUri( const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png") vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Auto.png")
); );
const liteIconUri = panel.webview.asWebviewUri( const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png") vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "lite.png")
); );
const syIconUri = panel.webview.asWebviewUri( const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png") vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Sy.png")
); );
const maxIconUri = panel.webview.asWebviewUri( const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png") vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Max.png")
); );
// 获取二维码图片URI // 获取二维码图片URI
const qrCodeUri = panel.webview.asWebviewUri( const qrCodeUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "QRCode", "wx.png") vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "QRCode", "wx.png")
); );
// 获取Logo URI // 获取Logo URI
@ -129,7 +129,8 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
handleUserAnswer( handleUserAnswer(
message.askId, message.askId,
message.selected, message.selected,
message.customInput message.customInput,
message.answers
); );
break; break;
// 新增:中止对话 // 新增:中止对话
@ -206,35 +207,46 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
} }
resolveWebviewView(webviewView: vscode.WebviewView) { resolveWebviewView(webviewView: vscode.WebviewView) {
console.log('[ICViewProvider] ========== resolveWebviewView 被调用 ==========');
// 保存引用以便后续刷新 // 保存引用以便后续刷新
this._view = webviewView; this._view = webviewView;
webviewView.webview.options = { webviewView.webview.options = {
enableScripts: true, enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")], localResourceRoots: [
vscode.Uri.joinPath(this.extensionUri, "media"),
vscode.Uri.joinPath(this.extensionUri, "src", "assets")
],
}; };
// 异步检查 token 是否过期并清除 console.log('[ICViewProvider] Webview options 已设置');
vscode.authentication.getSession("iccoder", [], { createIfNone: false }) console.log('[ICViewProvider] extensionUri:', this.extensionUri.toString());
.then((session) => {
const token = session?.accessToken;
if (token && isTokenExpired(token)) {
// 静默清除过期的 session
this.context.globalState.update('icCoderSessions', []);
this.context.globalState.update('icCoderUserInfo', undefined);
console.log('[ICViewProvider] Token 已过期,已清除所有登录状态');
}
}, () => {
// 忽略错误
});
// 检查是否已登录(使用 Authentication API // 【关键修复】先设置默认 HTML避免一直加载
this.checkLoginStatus().then((isLoggedIn) => { try {
webviewView.webview.html = this.getWebviewContent( const html = this.getWebviewContent(webviewView.webview, false);
webviewView.webview, console.log('[ICViewProvider] HTML 内容已生成,长度:', html.length);
isLoggedIn 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(
@ -245,7 +257,7 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login");
} else if (message.command === "logout") { } else if (message.command === "logout") {
// 退出登录(前端已有确认对话框) // 退出登录(前端已有确认对话框)
vscode.commands.executeCommand('iccoder.logout'); vscode.commands.executeCommand("ic-coder.logout");
} 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'));
@ -265,187 +277,88 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
webview: vscode.Webview, webview: vscode.Webview,
isLoggedIn: boolean isLoggedIn: boolean
): string { ): string {
console.log('[ICViewProvider] 开始生成 HTML 内容, isLoggedIn:', isLoggedIn);
const logoUri = webview.asWebviewUri( const logoUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, "media", "icon.png") vscode.Uri.joinPath(this.extensionUri, "media", "icon.png")
); );
return ` return `<!DOCTYPE html>
<!DOCTYPE html> <html>
<html> <head>
<head> <meta charset="UTF-8">
<style> <meta name="viewport" content="width=device-width, initial-scale=1.0">
body { <style>
margin: 0; body {
padding: 0; margin: 0;
font-family: var(--vscode-font-family); padding: 0;
color: var(--vscode-foreground); font-family: var(--vscode-font-family);
height: 100vh; color: var(--vscode-foreground);
display: flex; height: 100vh;
justify-content: center; display: flex;
align-items: center; justify-content: center;
background: linear-gradient(135deg, align-items: center;
var(--vscode-editor-background) 0%, background: linear-gradient(135deg,
color-mix(in srgb, var(--vscode-editor-background) 85%, var(--vscode-button-background) 15%) 50%, var(--vscode-editor-background) 0%,
color-mix(in srgb, var(--vscode-editor-background) 90%, var(--vscode-button-background) 10%) 100%); color-mix(in srgb, var(--vscode-editor-background) 85%, var(--vscode-button-background) 15%) 50%,
} color-mix(in srgb, var(--vscode-editor-background) 90%, var(--vscode-button-background) 10%) 100%);
.container { }
display: flex; .container {
flex-direction: column; display: flex;
justify-content: center; flex-direction: column;
align-items: center; justify-content: center;
text-align: center; align-items: center;
padding: 20px; text-align: center;
} padding: 20px;
.container img { }
margin-bottom: 16px; .container img {
} margin-bottom: 16px;
.container h2 { }
margin: 0 0 16px 0; .container h2 {
} margin: 0 0 16px 0;
.btn { }
width: 200px; .btn {
padding: 8px 12px; width: 200px;
margin: 4px 0; padding: 8px 12px;
background: var(--vscode-button-background); margin: 4px 0;
color: var(--vscode-button-foreground); background: #007ACC;
border: none; color: #ffffff;
border-radius: 4px; border: none;
cursor: pointer; border-radius: 4px;
text-align: center; cursor: pointer;
} text-align: center;
.btn:hover { }
background: var(--vscode-button-hoverBackground); .btn:hover {
} background: #005a9e;
h3 { }
margin: 0 0 8px 0; </style>
font-size: 12px; </head>
color: var(--vscode-descriptionForeground); <body>
} <div class="container">
</style> <img src="${logoUri}" alt="IC Coder" width="120" />
</head> <h2>欢迎使用 IC Coder</h2>
<body> ${isLoggedIn
<div class="container"> ? '<button class="btn" onclick="openChat()">开始创作</button>'
<img src="${logoUri}" alt="IC Coder" width="120" /> : '<button class="btn" onclick="login()">登录账户</button>'
<h2>欢迎使用 IC Coder</h2> }
${ </div>
isLoggedIn <script>
? '<button class="btn" onclick="openChat()">开始创作</button>' console.log('[Webview] 脚本已加载');
: '<button class="btn" onclick="login()">登录账户</button>' const vscode = acquireVsCodeApi();
}
</div>
<script> function openChat() {
const vscode = acquireVsCodeApi(); console.log('[Webview] 点击开始创作');
vscode.postMessage({ command: 'openChat' });
}
function openChat() { function login() {
vscode.postMessage({ command: 'openChat' }); console.log('[Webview] 点击登录');
} vscode.postMessage({ command: 'login' });
}
// 登录功能 console.log('[Webview] 初始化完成');
function login() { </script>
vscode.postMessage({ command: 'login' }); </body>
} </html>`;
function generateCode(type) {
const code = getCodeTemplate(type);
vscode.postMessage({
command: 'insertCode',
code: code
});
}
function getCodeTemplate(type) {
const templates = {
counter: \`module counter #(
parameter WIDTH = 4
)(
input wire clk,
input wire rst_n,
input wire enable,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 0;
end else if (enable) begin
count <= count + 1;
end
end
endmodule\`,
fsm: \`module fsm (
input wire clk,
input wire rst_n,
input wire start,
output reg done
);
parameter IDLE = 2'b00;
parameter STATE1 = 2'b01;
parameter STATE2 = 2'b10;
reg [1:0] state, next_state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
end else begin
state <= next_state;
end
end
always @(*) begin
case (state)
IDLE: next_state = start ? STATE1 : IDLE;
STATE1: next_state = STATE2;
STATE2: next_state = IDLE;
default: next_state = IDLE;
endcase
end
assign done = (state == STATE2);
endmodule\`,
fifo: \`module sync_fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [DATA_WIDTH-1:0] din,
input wire rd_en,
output reg [DATA_WIDTH-1:0] dout,
output wire full,
output wire empty
);
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg [$clog2(DEPTH):0] wr_ptr, rd_ptr;
assign full = (wr_ptr == rd_ptr + DEPTH);
assign empty = (wr_ptr == rd_ptr);
always @(posedge clk) begin
if (!rst_n) wr_ptr <= 0;
else if (wr_en && !full) begin
mem[wr_ptr] <= din;
wr_ptr <= wr_ptr + 1;
end
end
always @(posedge clk) begin
if (!rst_n) begin
rd_ptr <= 0;
dout <= 0;
end else if (rd_en && !empty) begin
dout <= mem[rd_ptr];
rd_ptr <= rd_ptr + 1;
end
end
endmodule\`
};
return templates[type] || '// 代码模板';
}
</script>
</body>
</html>
`;
} }
} }

View File

@ -38,6 +38,7 @@ export function getAgentCardStyles(): string {
.agent-name { .agent-name {
font-weight: 500; font-weight: 500;
flex: 1; flex: 1;
font-size:14px
} }
.agent-status { .agent-status {
font-size: 11px; font-size: 11px;
@ -99,14 +100,14 @@ export function getAgentCardStyles(): string {
/* 低调显示的工具调用样式 */ /* 低调显示的工具调用样式 */
.agent-step.low-profile { .agent-step.low-profile {
opacity: 0.85; opacity: 0.85;
font-size: 12px; font-size: 13px;
padding: 4px 8px; padding: 4px 8px;
background: transparent; background: transparent;
margin-bottom: 2px; margin-bottom: 2px;
} }
.agent-step.low-profile .step-icon { .agent-step.low-profile .step-icon {
opacity: 0.8; opacity: 0.8;
font-size: 12px; font-size: 13px;
} }
.agent-step.low-profile .step-name { .agent-step.low-profile .step-name {
font-weight: 400; font-weight: 400;
@ -115,7 +116,7 @@ export function getAgentCardStyles(): string {
} }
.agent-step.low-profile .step-result { .agent-step.low-profile .step-result {
opacity: 0.85; opacity: 0.85;
font-size: 11px; font-size: 12px;
} }
`; `;
} }

489
src/views/changePanel.ts Normal file
View File

@ -0,0 +1,489 @@
/**
* 代码变更面板组件
* 功能:显示 AI 修改的文件列表和 diff 对比
* 依赖utils/diffRenderer
* 使用场景:对话结束后展示代码变更供用户审查
*/
import { getDiffStyles } from "../utils/diffRenderer";
/**
* 获取变更面板的 HTML 内容
*/
export function getChangePanelContent(): string {
return `
<div class="change-panel" id="changePanel" style="display: none;">
<div class="change-panel-header" onclick="toggleChangePanel()">
<div class="change-panel-title">
<span>代码变更</span>
<span class="change-count" id="changeCount">0</span>
</div>
<div class="change-panel-actions" onclick="event.stopPropagation()">
<button class="batch-action-btn accept-all-btn" onclick="acceptAllChanges()" title="采纳全部">
<span>✓ 全部采纳</span>
</button>
<button class="batch-action-btn reject-all-btn" onclick="rejectAllChanges()" title="拒绝全部">
<span>✕ 全部拒绝</span>
</button>
<button class="change-toggle-btn" id="changePanelToggle">
<span class="toggle-icon">▼</span>
</button>
</div>
</div>
<div class="change-panel-body" id="changePanelBody" style="display: none;">
<div class="change-list" id="changeList">
<!-- 变更列表将动态插入 -->
</div>
</div>
</div>
`;
}
/**
* 获取变更面板的样式
*/
export function getChangePanelStyles(): string {
return `
.change-panel {
margin-bottom: 12px;
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
background: var(--vscode-editor-background);
overflow: hidden;
}
.change-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 16px;
background: var(--vscode-sideBar-background);
user-select: none;
border-bottom: 2px solid var(--vscode-panel-border);
cursor: pointer;
}
.change-panel-header:hover {
background: var(--vscode-list-hoverBackground);
}
.change-panel-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 600;
}
.change-icon {
font-size: 18px;
}
.change-count {
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
min-width: 24px;
text-align: center;
}
.change-panel-actions {
display: flex;
align-items: center;
gap: 8px;
}
.batch-action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.batch-action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.accept-all-btn {
background: #28a745;
color: white;
}
.accept-all-btn:hover {
background: #218838;
}
.reject-all-btn {
background: #dc3545;
color: white;
}
.reject-all-btn:hover {
background: #c82333;
}
.change-toggle-btn {
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
}
.change-toggle-btn:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.toggle-icon {
display: inline-block;
transition: transform 0.2s;
}
.toggle-icon.expanded {
transform: rotate(180deg);
}
.change-panel-body {
border-top: 1px solid var(--vscode-panel-border);
max-height: 400px;
overflow-y: auto;
}
.change-list {
padding: 0px;
}
.change-item {
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
margin-bottom: 10px;
overflow: hidden;
background: var(--vscode-editor-background);
transition: all 0.2s;
}
.change-item:hover {
border-color: var(--vscode-focusBorder);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.change-item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 16px;
background: var(--vscode-sideBar-background);
cursor: pointer;
transition: background 0.2s;
}
.change-item-header:hover {
background: var(--vscode-list-hoverBackground);
}
.change-item-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.change-type-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.change-type-create {
background: #28a745;
color: white;
}
.change-type-modify {
background: #ffc107;
color: #000;
}
.change-type-delete {
background: #dc3545;
color: white;
}
.change-file-path {
font-size: 13px;
font-weight: 500;
color: var(--vscode-foreground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.change-item-actions {
display: flex;
gap: 6px;
}
.change-action-btn {
padding: 6px 14px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.change-action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.accept-btn {
background: #28a745;
color: white;
}
.accept-btn:hover {
background: #218838;
}
.reject-btn {
background: #dc3545;
color: white;
}
.reject-btn:hover {
background: #c82333;
}
.change-item-diff {
padding: 12px;
background: var(--vscode-editor-background);
border-top: 1px solid var(--vscode-panel-border);
display: none;
max-height: 300px;
overflow-y: auto;
}
.change-item-diff.expanded {
display: block;
}
${getDiffStyles()}
`;
}
/**
* 获取变更面板的脚本
*/
export function getChangePanelScript(): string {
return `
// 切换变更面板展开/收起
function toggleChangePanel() {
const body = document.getElementById('changePanelBody');
const toggleIcon = document.querySelector('.toggle-icon');
if (body.style.display === 'none') {
body.style.display = 'block';
toggleIcon.classList.add('expanded');
} else {
body.style.display = 'none';
toggleIcon.classList.remove('expanded');
}
}
// 全部采纳
window.acceptAllChanges = function() {
const changeList = document.getElementById('changeList');
if (!changeList) {
console.error('changeList not found');
return;
}
const items = Array.from(changeList.querySelectorAll('.change-item'));
console.log('Found items:', items.length);
if (items.length === 0) {
alert('没有待处理的变更');
return;
}
items.forEach(item => {
const changeId = item.id.replace('change-item-', '');
console.log('Accepting change:', changeId);
vscode.postMessage({
command: 'acceptChange',
changeId: changeId
});
});
};
// 全部拒绝
window.rejectAllChanges = function() {
const changeList = document.getElementById('changeList');
if (!changeList) {
console.error('changeList not found');
return;
}
const items = Array.from(changeList.querySelectorAll('.change-item'));
console.log('Found items:', items.length);
if (items.length === 0) {
alert('没有待处理的变更');
return;
}
items.forEach(item => {
const changeId = item.id.replace('change-item-', '');
console.log('Rejecting change:', changeId);
vscode.postMessage({
command: 'rejectChange',
changeId: changeId
});
});
};
// 打开文件 diff在 VS Code 中打开)
function openFileDiff(changeId) {
vscode.postMessage({
command: 'openFileDiff',
changeId: changeId
});
}
// 采纳变更
function acceptChange(changeId) {
vscode.postMessage({
command: 'acceptChange',
changeId: changeId
});
}
// 拒绝变更
function rejectChange(changeId) {
vscode.postMessage({
command: 'rejectChange',
changeId: changeId
});
}
// 显示变更面板(从后端接收变更列表)
window.showChangesPanel = function(changes) {
const changePanel = document.getElementById('changePanel');
const changeList = document.getElementById('changeList');
const changeCount = document.getElementById('changeCount');
if (!changePanel || !changeList || !changeCount) {
return;
}
// 更新变更数量
changeCount.textContent = changes.length;
// 清空现有列表
changeList.innerHTML = '';
// 渲染每个变更项
changes.forEach(change => {
const changeItem = createChangeItem(change);
changeList.appendChild(changeItem);
});
// 显示面板
changePanel.style.display = 'block';
};
// 创建单个变更项的 DOM 元素
function createChangeItem(change) {
const item = document.createElement('div');
item.className = 'change-item';
item.id = 'change-item-' + change.changeId;
const typeLabel = change.changeType === 'create' ? '新建' :
change.changeType === 'modify' ? '修改' : '删除';
item.innerHTML = \`
<div class="change-item-header" onclick="openFileDiff('\${change.changeId}')">
<div class="change-item-info">
<span class="change-type-badge change-type-\${change.changeType}">\${typeLabel}</span>
<span class="change-file-path">\${change.filePath}</span>
</div>
<div class="change-item-actions">
<button class="change-action-btn accept-btn" onclick="event.stopPropagation(); acceptChange('\${change.changeId}')">采纳</button>
<button class="change-action-btn reject-btn" onclick="event.stopPropagation(); rejectChange('\${change.changeId}')">拒绝</button>
</div>
</div>
\`;
return item;
}
// 处理采纳变更的响应
window.handleChangeAccepted = function(changeId, success, error) {
if (success) {
// 从列表中移除该变更项
const item = document.getElementById('change-item-' + changeId);
if (item) {
item.remove();
}
// 更新变更数量
updateChangeCount();
} else {
console.error('采纳变更失败:', error);
alert('采纳变更失败: ' + (error || '未知错误'));
}
};
// 处理拒绝变更的响应
window.handleChangeRejected = function(changeId, success, error) {
if (success) {
// 从列表中移除该变更项
const item = document.getElementById('change-item-' + changeId);
if (item) {
item.remove();
}
// 更新变更数量
updateChangeCount();
} else {
console.error('拒绝变更失败:', error);
alert('拒绝变更失败: ' + (error || '未知错误'));
}
};
// 更新变更数量
function updateChangeCount() {
const changeList = document.getElementById('changeList');
const changeCount = document.getElementById('changeCount');
const changePanel = document.getElementById('changePanel');
if (changeList && changeCount) {
const count = changeList.children.length;
changeCount.textContent = count;
// 如果没有变更了,隐藏面板
if (count === 0 && changePanel) {
changePanel.style.display = 'none';
}
}
}
`;
}

View File

@ -16,7 +16,7 @@ export function getContextButtonContent(): string {
<span class="add-context-label">添加上下文</span> <span class="add-context-label">添加上下文</span>
</button> </button>
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span> <span class="tooltiptext">添加文件、文件夹作为上下文</span>
</div> </div>
<!-- 上拉菜单 --> <!-- 上拉菜单 -->
@ -41,18 +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="handleAddImage()"> <!-- <div class="context-menu-item" onclick="handleAddImage()">
<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="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"/> <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> </svg>
<span>图片</span> <span>图片</span>
</div> </div> -->
<div class="context-menu-item" onclick="handleAddDocument()"> <!-- <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>
</div> </div> -->
</div> </div>
<!-- 文件/文件夹列表视图 --> <!-- 文件/文件夹列表视图 -->
@ -271,6 +271,7 @@ export function getContextButtonStyles(): string {
width: 14px; width: 14px;
height: 14px; height: 14px;
flex-shrink: 0; flex-shrink: 0;
pointer-events: none;
} }
.context-menu-list-item label { .context-menu-list-item label {
@ -335,6 +336,7 @@ export function getContextButtonScript(): string {
return ` return `
// 上下文菜单状态 // 上下文菜单状态
let currentListData = []; let currentListData = [];
let filteredListData = [];
let currentListType = ''; let currentListType = '';
let selectedItems = new Set(); let selectedItems = new Set();
@ -392,6 +394,15 @@ export function getContextButtonScript(): string {
selectedItems.clear(); selectedItems.clear();
currentListData = []; currentListData = [];
filteredListData = [];
clearContextSearchInput();
}
function clearContextSearchInput() {
const searchInput = document.getElementById('contextMenuSearch');
if (searchInput) {
searchInput.value = '';
}
} }
// 切换到列表视图 // 切换到列表视图
@ -406,10 +417,12 @@ export function getContextButtonScript(): string {
titleEl.textContent = title; titleEl.textContent = title;
currentListType = type; currentListType = type;
currentListData = data; currentListData = data || [];
filteredListData = currentListData;
selectedItems.clear(); selectedItems.clear();
renderList(data); clearContextSearchInput();
renderList(filteredListData);
updateSelectedCount(); updateSelectedCount();
} }
} }
@ -419,32 +432,36 @@ export function getContextButtonScript(): string {
const body = document.getElementById('contextMenuListBody'); const body = document.getElementById('contextMenuListBody');
if (!body) return; if (!body) return;
body.innerHTML = data.map((item, index) => \` filteredListData = data || [];
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
<input type="checkbox" id="item-\${index}" /> body.innerHTML = filteredListData.map((item, index) => \`
<label for="item-\${index}">\${item.relativePath}</label> <div class="context-menu-list-item \${selectedItems.has(item.path) ? 'selected' : ''}" onclick="toggleItemSelection(\${index})">
<input type="checkbox" id="item-\${index}" \${selectedItems.has(item.path) ? 'checked' : ''} />
<label>\${item.relativePath || item.path}</label>
</div> </div>
\`).join(''); \`).join('');
} }
// 切换项选择 // 切换项选择
function toggleItemSelection(index) { function toggleItemSelection(index) {
const selectedItem = filteredListData[index];
if (!selectedItem) return;
const selectedPath = selectedItem.path;
const checkbox = document.getElementById('item-' + index); const checkbox = document.getElementById('item-' + index);
const item = document.querySelectorAll('.context-menu-list-item')[index]; const item = document.querySelectorAll('.context-menu-list-item')[index];
if (checkbox && item) { if (selectedItems.has(selectedPath)) {
checkbox.checked = !checkbox.checked; selectedItems.delete(selectedPath);
if (checkbox) checkbox.checked = false;
if (checkbox.checked) { if (item) item.classList.remove('selected');
selectedItems.add(index); } else {
item.classList.add('selected'); selectedItems.add(selectedPath);
} else { if (checkbox) checkbox.checked = true;
selectedItems.delete(index); if (item) item.classList.add('selected');
item.classList.remove('selected');
}
updateSelectedCount();
} }
updateSelectedCount();
} }
// 更新选中数量 // 更新选中数量
@ -457,15 +474,25 @@ export function getContextButtonScript(): string {
// 确认选择 // 确认选择
function confirmSelection() { function confirmSelection() {
const selected = Array.from(selectedItems).map(index => currentListData[index]); try {
const selected = currentListData.filter(item => selectedItems.has(item.path));
if (selected.length > 0) { if (selected.length > 0) {
selected.forEach(item => { selected.forEach(item => {
addContextItem(currentListType, item.path); addContextItem(currentListType, item.path, item.relativePath || item.path);
}); });
}
} finally {
const menu = document.getElementById('contextMenu');
const button = document.querySelector('.add-context-button');
if (menu) {
menu.classList.remove('show');
}
if (button) {
button.classList.remove('active');
}
backToMainMenu();
} }
toggleContextMenu();
} }
// 添加图片 // 添加图片
@ -484,9 +511,9 @@ export function getContextButtonScript(): string {
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(); const keyword = (e.target.value || '').toLowerCase().trim();
const filtered = currentListData.filter(item => const filtered = currentListData.filter(item =>
item.relativePath.toLowerCase().includes(keyword) (item.relativePath || item.path || '').toLowerCase().includes(keyword)
); );
renderList(filtered); renderList(filtered);
}); });

View File

@ -137,9 +137,12 @@ export function getContextDisplayScript(): string {
} }
// 添加上下文项 // 添加上下文项
function addContextItem(type, path) { function addContextItem(type, path, displayPath) {
const exists = contextItems.some(item => item.type === type && item.path === path);
if (exists) return;
const id = Date.now() + Math.random(); const id = Date.now() + Math.random();
contextItems.push({ id, type, path }); contextItems.push({ id, type, path, displayPath: displayPath || '' });
renderContextItems(); renderContextItems();
} }
@ -174,7 +177,7 @@ export function getContextDisplayScript(): string {
return \` return \`
<div class="context-item" title="\${item.path}"> <div class="context-item" title="\${item.path}">
\${icon} \${icon}
<span class="context-item-name">\${getFileName(item.path)}</span> <span class="context-item-name">\${item.displayPath || getFileName(item.path)}</span>
<span class="context-item-remove" onclick="removeContextItem(\${item.id})"> <span class="context-item-remove" onclick="removeContextItem(\${item.id})">
\${getRemoveIcon()} \${getRemoveIcon()}
</span> </span>

View File

@ -330,7 +330,7 @@ export function getConversationHistoryBarStyles(): string {
} }
.new-conversation-button:hover { .new-conversation-button:hover {
background: var(--vscode-toolbar-hoverBackground); background: #007ACC;
transform: scale(1.1); transform: scale(1.1);
} }

218
src/views/expiredModal.ts Normal file
View File

@ -0,0 +1,218 @@
/**
* 试用期过期弹窗
* 功能:在聊天面板内显示过期提醒模态窗口
* 依赖:无
* 使用场景:试用用户过期时在聊天面板内显示
*/
/**
* 获取过期弹窗的 HTML 内容
*/
export function getExpiredModalContent(logoUri?: string): string {
return `
<!-- 过期弹窗 -->
<div id="expiredModal" class="expired-modal" style="display: none;">
<div class="expired-modal-overlay"></div>
<div class="expired-modal-content">
${logoUri ? `<img src="${logoUri}" class="expired-logo-corner" alt="IC Coder" />` : ""}
<div class="expired-modal-header">
<div class="expired-icon">⏰</div>
<h2>您的试用期已到期</h2>
<p class="expired-modal-subtitle">感谢您使用 IC Coder您的 15 天试用期已结束。</p>
</div>
<div class="expired-modal-body">
<p class="expired-message">如需继续使用,请联系我们获取正式版本。</p>
<button id="expiredContactBtn" class="expired-btn expired-btn-primary">
<span>联系我们</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
`;
}
/**
* 获取过期弹窗的 CSS 样式
*/
export function getExpiredModalStyles(): string {
return `
/* 过期弹窗样式 */
.expired-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
}
.expired-modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease-out;
}
.expired-modal-content {
position: relative;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 12px;
width: 100%;
max-width: 450px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
}
.expired-logo-corner {
position: absolute;
top: 16px;
left: 24px;
height: 40px;
width: auto;
opacity: 0.9;
z-index: 10;
}
.expired-modal-header {
padding: 60px 32px 20px;
text-align: center;
}
.expired-icon {
font-size: 48px;
margin-bottom: 16px;
}
.expired-modal-header h2 {
margin: 0 0 12px;
font-size: 24px;
font-weight: 600;
color: var(--vscode-errorForeground);
}
.expired-modal-subtitle {
margin: 0;
font-size: 14px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
}
.expired-modal-body {
padding: 0 32px 32px;
text-align: center;
}
.expired-message {
font-size: 14px;
color: var(--vscode-descriptionForeground);
margin: 20px 0;
line-height: 1.6;
}
.expired-btn {
width: 100%;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
transition: all 0.2s;
margin-top: 24px;
}
.expired-btn:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.expired-btn:active {
transform: translateY(0);
}
`;
}
/**
* 获取过期弹窗的 JavaScript 逻辑
*/
export function getExpiredModalScript(): string {
return `
// 过期弹窗逻辑
(function() {
const modal = document.getElementById('expiredModal');
const contactBtn = document.getElementById('expiredContactBtn');
const overlay = modal?.querySelector('.expired-modal-overlay');
// 显示过期弹窗
window.showExpiredModal = function() {
if (modal) {
modal.style.display = 'flex';
}
};
// 隐藏过期弹窗
window.hideExpiredModal = function() {
if (modal) {
modal.style.display = 'none';
}
};
// 点击"联系我们"按钮
if (contactBtn) {
contactBtn.addEventListener('click', function() {
// 可以打开联系页面
// window.open('https://iccoder.com/contact', '_blank');
hideExpiredModal();
});
}
// 点击遮罩层关闭弹窗
if (overlay) {
overlay.addEventListener('click', function() {
hideExpiredModal();
});
}
// 阻止点击弹窗内容时关闭
const content = modal?.querySelector('.expired-modal-content');
if (content) {
content.addEventListener('click', function(e) {
e.stopPropagation();
});
}
// 监听来自后端的消息
window.addEventListener('message', function(event) {
const message = event.data;
if (message.command === 'showExpiredModal') {
showExpiredModal();
}
});
})();
`;
}

62
src/views/filePathTag.ts Normal file
View File

@ -0,0 +1,62 @@
/**
* 文件路径标签组件
* 功能:显示可点击的文件路径标签
* 使用场景:在用户消息中显示上下文文件
*/
/**
* 获取文件路径标签的样式
*/
export function getFilePathTagStyles(): string {
return `
/* 文件路径标签 */
.file-path-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
margin-right: 6px;
background: rgba(0, 122, 204, 0.15);
border: 1px solid rgba(0, 122, 204, 0.3);
border-radius: 4px;
color: #4fc3f7;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
font-family: 'Consolas', 'Monaco', monospace;
}
.file-path-tag:hover {
background: rgba(0, 122, 204, 0.25);
border-color: rgba(0, 122, 204, 0.5);
}
.file-path-tag svg {
width: 12px;
height: 12px;
opacity: 0.8;
}
`;
}
/**
* 获取文件路径标签的脚本
*/
export function getFilePathTagScript(): string {
return `
// 处理文件路径标签点击
function handleFilePathClick(filePath) {
vscode.postMessage({
command: 'openFile',
filePath: filePath
});
}
// 创建文件路径标签
window.createFilePathTag = function(filePath) {
const fileIcon = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
return '<span class="file-path-tag" onclick="handleFilePathClick(\\'' + escapedPath + '\\')">' + fileIcon + filePath + '</span>';
};
`;
}

View File

@ -24,6 +24,10 @@ import {
getContextCompressStyles, getContextCompressStyles,
getContextCompressScript, getContextCompressScript,
} from "./contextCompress"; } from "./contextCompress";
import {
getFilePathTagStyles,
getFilePathTagScript,
} from "./filePathTag";
import { import {
getOptimizeButtonContent, getOptimizeButtonContent,
getOptimizeButtonStyles, getOptimizeButtonStyles,
@ -34,6 +38,11 @@ import {
getExampleShowcaseStyles, getExampleShowcaseStyles,
getExampleShowcaseScript, getExampleShowcaseScript,
} from "./exampleShowcase"; } from "./exampleShowcase";
import {
getChangePanelContent,
getChangePanelStyles,
getChangePanelScript,
} from "./changePanel";
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons"; import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
/** /**
@ -49,6 +58,8 @@ export function getInputAreaContent(
<div class="input-area centered" id="inputArea"> <div class="input-area centered" id="inputArea">
<div class="input-group"> <div class="input-group">
<div class="input-wrapper"> <div class="input-wrapper">
<!-- 代码变更面板 -->
${getChangePanelContent()}
<!-- 顶部工具栏 --> <!-- 顶部工具栏 -->
<div class="input-top-toolbar"> <div class="input-top-toolbar">
${getContextButtonContent()} ${getContextButtonContent()}
@ -91,9 +102,11 @@ export function getInputAreaStyles(): string {
${getModelSelectorStyles()} ${getModelSelectorStyles()}
${getContextButtonStyles()} ${getContextButtonStyles()}
${getContextDisplayStyles()} ${getContextDisplayStyles()}
${getFilePathTagStyles()}
${getContextCompressStyles()} ${getContextCompressStyles()}
${getOptimizeButtonStyles()} ${getOptimizeButtonStyles()}
${getExampleShowcaseStyles()} ${getExampleShowcaseStyles()}
${getChangePanelStyles()}
.input-area { .input-area {
border-top: 1px solid var(--vscode-panel-border); border-top: 1px solid var(--vscode-panel-border);
padding-top: 15px; padding-top: 15px;
@ -296,10 +309,12 @@ export function getInputAreaScript(): string {
return ` return `
// 注意getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载 // 注意getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
${getModelSelectorScript()} ${getModelSelectorScript()}
${getContextButtonScript()}
${getContextDisplayScript()} ${getContextDisplayScript()}
${getContextButtonScript()}
${getContextCompressScript()} ${getContextCompressScript()}
${getOptimizeButtonScript()} ${getOptimizeButtonScript()}
${getChangePanelScript()}
${getFilePathTagScript()}
// 对话状态管理 // 对话状态管理
let isConversationActive = false; let isConversationActive = false;
@ -339,12 +354,14 @@ export function getInputAreaScript(): string {
if (messageInput) { if (messageInput) {
messageInput.addEventListener('input', autoResizeTextarea); messageInput.addEventListener('input', autoResizeTextarea);
// 监听点击事件,检测工作区状态和邀请码验证状态 // 监听点击事件,检测工作区状态、试用期过期和邀请码验证状态
messageInput.addEventListener('focus', () => { messageInput.addEventListener('focus', () => {
if (!hasCheckedWorkspace) { if (!hasCheckedWorkspace) {
hasCheckedWorkspace = true; hasCheckedWorkspace = true;
vscode.postMessage({ command: 'checkWorkspace' }); vscode.postMessage({ command: 'checkWorkspace' });
} }
// 检查试用期是否过期
vscode.postMessage({ command: 'checkTrialExpiration' });
// 检查邀请码验证状态 // 检查邀请码验证状态
vscode.postMessage({ command: 'checkInvitationCode' }); vscode.postMessage({ command: 'checkInvitationCode' });
}); });
@ -415,7 +432,22 @@ export function getInputAreaScript(): string {
// 获取上下文项 // 获取上下文项
const contextItems = window.getContextItems ? window.getContextItems() : []; const contextItems = window.getContextItems ? window.getContextItems() : [];
addMessage(text, 'user'); // 构建显示消息:如果有上下文文件,添加文件路径前缀
let displayText = text;
if (contextItems.length > 0) {
const filePaths = contextItems
.filter(item => item.type === 'file')
.map(item => item.displayPath || item.path)
.join(' ');
if (filePaths) {
displayText = filePaths + ' ' + text;
}
}
addMessage(displayText, 'user');
// 重置分段消息容器,强制下次创建新容器
currentSegmentedMessage = null;
// 标记已有消息,切换布局到底部 // 标记已有消息,切换布局到底部
hasMessages = true; hasMessages = true;
@ -436,6 +468,11 @@ export function getInputAreaScript(): string {
autoResizeTextarea(); // 重置输入框高度 autoResizeTextarea(); // 重置输入框高度
messageInput.focus(); messageInput.focus();
// 清空上下文项
if (window.clearContextItems) {
window.clearContextItems();
}
// 重置优化状态 // 重置优化状态
resetOptimizeButton(); resetOptimizeButton();
} }

View File

@ -25,6 +25,7 @@ import {
stateTransitionIconSvg, stateTransitionIconSvg,
userQuestionIconSvg, userQuestionIconSvg,
updateStageIconSvg, updateStageIconSvg,
successIconSvg,
} from "../constants/toolIcons"; } from "../constants/toolIcons";
import { import {
getWaveformPreviewContent, getWaveformPreviewContent,
@ -247,19 +248,21 @@ export function getMessageAreaStyles(): string {
} }
.question-option { .question-option {
padding: 8px 16px; padding: 8px 16px;
background: var(--vscode-button-secondaryBackground); background: #007ACC;
color: var(--vscode-button-secondaryForeground); color: #ffffff;
border: 1px solid var(--vscode-button-border); border: 1px solid #007ACC;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.question-option:hover { .question-option:hover {
background: var(--vscode-button-secondaryHoverBackground); background: #005a9e;
border-color: #005a9e;
} }
.question-option.selected { .question-option.selected {
background: var(--vscode-button-background); background: #007ACC;
color: var(--vscode-button-foreground); color: #ffffff;
border-color: #007ACC;
} }
.question-message.answered .question-option:not(.selected) { .question-message.answered .question-option:not(.selected) {
opacity: 0.5; opacity: 0.5;
@ -379,7 +382,7 @@ export function getMessageAreaStyles(): string {
} }
/* 低调显示的工具调用 - 移除边距和背景 */ /* 低调显示的工具调用 - 移除边距和背景 */
.segment-tool.low-profile { .segment-tool.low-profile {
margin: 2px 0px; margin: 5px 0px;
padding: 0; padding: 0;
background: none; background: none;
} }
@ -419,6 +422,9 @@ export function getMessageAreaStyles(): string {
height: 100%; height: 100%;
display: block; display: block;
} }
.icon-expanded svg path {
fill: #007ACC !important;
}
.tool-segment-header.collapsed .tool-collapse-icon { .tool-segment-header.collapsed .tool-collapse-icon {
transform: rotate(-90deg); transform: rotate(-90deg);
} }
@ -543,9 +549,9 @@ export function getMessageAreaStyles(): string {
max-height: 0; max-height: 0;
} }
.tool-segment-description { .tool-segment-description {
margin: 2px 0 0 0px; margin: 6px 0 0 0px;
font-size: 12px; font-size: 0.9rem;
color: #fff; color: var(--vscode-descriptionForeground);
line-height: 1.4; line-height: 1.4;
} }
/* 低调显示的工具调用样式 */ /* 低调显示的工具调用样式 */
@ -563,7 +569,7 @@ export function getMessageAreaStyles(): string {
} }
.segment-tool.low-profile .tool-segment-result { .segment-tool.low-profile .tool-segment-result {
opacity: 0.7; opacity: 0.7;
font-size: 10px; font-size: 12px;
} }
.segment-question { .segment-question {
background: var(--vscode-textBlockQuote-background); background: var(--vscode-textBlockQuote-background);
@ -584,20 +590,22 @@ export function getMessageAreaStyles(): string {
} }
.segment-question .question-option { .segment-question .question-option {
padding: 8px 16px; padding: 8px 16px;
background: var(--vscode-button-secondaryBackground); background: #007ACC;
color: var(--vscode-button-secondaryForeground); color: #ffffff;
border: 1px solid var(--vscode-button-border); border: 1px solid #007ACC;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
font-size: 13px; font-size: 13px;
} }
.segment-question .question-option:hover { .segment-question .question-option:hover {
background: var(--vscode-button-secondaryHoverBackground); background: #005a9e;
border-color: #005a9e;
} }
.segment-question .question-option.selected { .segment-question .question-option.selected {
background: var(--vscode-button-background); background: #007ACC;
color: var(--vscode-button-foreground); color: #ffffff;
border-color: #007ACC;
} }
.segment-question.answered .question-option:not(.selected) { .segment-question.answered .question-option:not(.selected) {
opacity: 0.5; opacity: 0.5;
@ -678,6 +686,7 @@ export function getMessageAreaScript(): string {
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`; const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
const userQuestionIconSvg = \`${userQuestionIconSvg}\`; const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
const updateStageIconSvg = \`${updateStageIconSvg}\`; const updateStageIconSvg = \`${updateStageIconSvg}\`;
const successIconSvg = \`${successIconSvg}\`;
${getAgentCardScript()} ${getAgentCardScript()}
@ -733,6 +742,7 @@ export function getMessageAreaScript(): string {
'addStateTransition': stateTransitionIconSvg, 'addStateTransition': stateTransitionIconSvg,
'askUser': userQuestionIconSvg, 'askUser': userQuestionIconSvg,
'updatePhase': updateStageIconSvg, 'updatePhase': updateStageIconSvg,
'iverilog': successIconSvg,
}; };
return iconMap[toolName] || ''; return iconMap[toolName] || '';
} }
@ -847,7 +857,26 @@ export function getMessageAreaScript(): string {
div.appendChild(actionsDiv); div.appendChild(actionsDiv);
} else { } else {
div.textContent = text; // 用户消息:解析文件路径并转换为标签
const parts = text.split(' ');
const filePaths = [];
const textParts = [];
parts.forEach(part => {
// 判断是否为文件路径:包含路径分隔符或文件扩展名
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-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;
}
// 当添加用户消息时,隐藏 header // 当添加用户消息时,隐藏 header
hideHeaderIfNeeded(); hideHeaderIfNeeded();
} }
@ -985,34 +1014,42 @@ export function getMessageAreaScript(): string {
// 实时更新分段消息(按后端返回顺序) // 实时更新分段消息(按后端返回顺序)
function updateSegmentsRealtime(segments, isComplete) { function updateSegmentsRealtime(segments, isComplete) {
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete); // 如果对话完成且没有新段落,只重置容器
if (isComplete && (!segments || segments.length === 0)) {
currentSegmentedMessage = null;
return;
}
if (!segments || segments.length === 0) { if (!segments || segments.length === 0) {
console.log('[WebView] segments 为空,跳过渲染');
return; return;
} }
// 如果没有当前分段消息容器,创建一个 // 如果没有当前分段消息容器,创建一个
if (!currentSegmentedMessage) { if (!currentSegmentedMessage) {
console.log('[WebView] 创建新的分段消息容器');
// 移除流式消息(如果有) // 移除流式消息(如果有)
if (currentStreamingMessage) { if (currentStreamingMessage) {
console.log('[WebView] 移除流式消息');
currentStreamingMessage.remove(); currentStreamingMessage.remove();
currentStreamingMessage = null; currentStreamingMessage = null;
} }
// 移除所有工具状态消息(因为会在分段中显示) // 移除所有工具状态消息(因为会在分段中显示)
const toolStatuses = messagesEl.querySelectorAll('.tool-status'); const toolStatuses = messagesEl.querySelectorAll('.tool-status');
console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
toolStatuses.forEach(el => { toolStatuses.forEach(el => {
console.log('[WebView] 移除工具状态消息:', el.className);
el.remove(); el.remove();
}); });
currentSegmentedMessage = document.createElement('div'); // 检查最后一个容器是否是未完成的对话(没有操作按钮)
currentSegmentedMessage.className = 'message bot-message segmented-message'; const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
messagesEl.appendChild(currentSegmentedMessage); 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;
} }
// 保存当前所有工具的展开/折叠状态 // 保存当前所有工具的展开/折叠状态
@ -1151,64 +1188,60 @@ export function getMessageAreaScript(): string {
} else if (segment.type === 'question') { } else if (segment.type === 'question') {
segmentDiv.className += ' segment-question'; segmentDiv.className += ' segment-question';
// 兼容旧格式:如果有 segment.question转换为 questions 数组
const questions = segment.questions || (segment.question ? [{
question: segment.question,
options: segment.options || [],
multiSelect: false
}] : []);
// 检查是否已回答 // 检查是否已回答
const isAnswered = answeredQuestions.has(segment.askId); const isAnswered = answeredQuestions.has(segment.askId);
const selectedAnswer = answeredQuestions.get(segment.askId); const savedAnswers = answeredQuestions.get(segment.askId) || {};
if (isAnswered) { if (isAnswered) {
segmentDiv.classList.add('answered'); segmentDiv.classList.add('answered');
} }
// 检查是否有选项 // 渲染多个问题
const hasOptions = segment.options && segment.options.length > 0; const questionsHtml = questions.map((q, qIndex) => {
const inputType = q.multiSelect ? 'checkbox' : 'radio';
const inputName = \`q\${qIndex}\`;
const selectedAnswers = savedAnswers[qIndex] || [];
const optionsHtml = hasOptions const optionsHtml = q.options.map(opt => {
? (segment.options || []).map(opt => { const isSelected = selectedAnswers.includes(opt);
const isSelected = isAnswered && opt === selectedAnswer; return \`<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 0;">
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`; <input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
}).join('') <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 = \` segmentDiv.innerHTML = \`
<div class="question-text">\${formatText(segment.question || '')}</div> \${questionsHtml}
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''} <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>
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
<button class="custom-submit">提交</button>
</div>
\`; \`;
// 只在未回答时添加事件监听 // 只在未回答时添加事件监听
if (!isAnswered) { if (!isAnswered) {
setTimeout(() => { setTimeout(() => {
if (hasOptions) {
const optionButtons = segmentDiv.querySelectorAll('.question-option');
optionButtons.forEach(btn => {
btn.addEventListener('click', function() {
const option = this.getAttribute('data-option');
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
});
});
}
const submitBtn = segmentDiv.querySelector('.custom-submit'); const submitBtn = segmentDiv.querySelector('.custom-submit');
const customInput = segmentDiv.querySelector('.custom-input'); if (submitBtn) {
if (submitBtn && customInput) {
submitBtn.addEventListener('click', function() { submitBtn.addEventListener('click', function() {
const customValue = customInput.value.trim(); const answers = {};
if (customValue) { questions.forEach((q, qIndex) => {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
} answers[qIndex] = Array.from(inputs).map(input => input.value);
}); });
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
// 支持回车提交
customInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const customValue = customInput.value.trim();
if (customValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
}
}
}); });
} }
}, 0); }, 0);
@ -1224,7 +1257,7 @@ export function getMessageAreaScript(): string {
currentSegmentedMessage.appendChild(segmentDiv); currentSegmentedMessage.appendChild(segmentDiv);
}); });
// 如果对话完成,添加操作按钮 // 如果对话完成,添加操作按钮并重置容器
if (isComplete) { if (isComplete) {
console.log('[WebView] 对话完成,添加操作按钮'); console.log('[WebView] 对话完成,添加操作按钮');
const actionsDiv = document.createElement('div'); const actionsDiv = document.createElement('div');
@ -1259,7 +1292,7 @@ export function getMessageAreaScript(): string {
actionsDiv.appendChild(dislikeBtn); actionsDiv.appendChild(dislikeBtn);
currentSegmentedMessage.appendChild(actionsDiv); currentSegmentedMessage.appendChild(actionsDiv);
// 重置当前分段消息容器 // 重置当前分段消息容器(继续对话时创建新容器)
currentSegmentedMessage = null; currentSegmentedMessage = null;
} }
@ -1686,6 +1719,34 @@ export function getMessageAreaScript(): string {
}); });
} }
// 处理多问题答案提交
function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
console.log('[WebView] 多问题答案提交:', askId, answers);
// 保存答案到 Map 中
answeredQuestions.set(askId, answers);
// 标记问题已回答
segmentDiv.classList.add('answered');
// 禁用所有输入
const inputs = segmentDiv.querySelectorAll('input');
inputs.forEach(input => input.disabled = true);
// 隐藏提交按钮
const submitBtn = segmentDiv.querySelector('.custom-submit');
if (submitBtn) {
submitBtn.style.display = 'none';
}
// 发送答案到后端
vscode.postMessage({
command: 'submitAnswer',
askId: askId,
answers: answers
});
}
${getWaveformPreviewScript()} ${getWaveformPreviewScript()}
${getCodeHighlightScript()} ${getCodeHighlightScript()}

View File

@ -0,0 +1,329 @@
/**
* 宁德时代欢迎弹窗
* 功能:邀请码验证成功后显示欢迎信息
* 依赖:无
* 使用场景:宁德时代用户首次验证邀请码成功后显示
*/
/**
* 获取宁德时代欢迎弹窗的 HTML 内容
*/
export function getNdtWelcomeModalContent(logoUri?: string): string {
return `
<!-- 宁德时代欢迎弹窗 -->
<div id="ndtWelcomeModal" class="ndt-welcome-modal" style="display: none;">
<div class="ndt-welcome-modal-overlay"></div>
<div class="ndt-welcome-modal-content">
${logoUri ? `<img src="${logoUri}" class="ndt-welcome-logo-corner" alt="IC Coder" />` : ""}
<div class="ndt-welcome-modal-header">
<div class="ndt-welcome-icon">🎉</div>
<h2>欢迎企业<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder</h2>
</div>
<div class="ndt-welcome-modal-body">
<!-- 试用期提示 -->
<div class="ndt-trial-banner">
<span>您已获得 <strong>5 天企业版试用期</strong>企业版试用期内Credits用量无限并可无限制使用所有功能</span>
</div>
<!-- IC Coder 简介 -->
<div class="ndt-intro-section">
<h3 class="ndt-section-title">关于 IC Coder</h3>
<p class="ndt-intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform自主式人工智能 Verilog 编码平台。我们采用全球顶尖的IC Coder自研芯片设计微调模型为代码生成提供强大的AI能力支撑。</p>
<div class="ndt-features">
<div class="ndt-feature-item">
<span class="ndt-feature-text">多智能体架构Multi-Agent System多个专业化AI智能体协同工作分别负责架构设计、代码生成、验证测试等不同环节</span>
</div>
<div class="ndt-feature-item">
<span class="ndt-feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
</div>
<div class="ndt-feature-item">
<span class="ndt-feature-text">AI自主仿真IC Coder提供完全自动化的仿真验证流程无需手动编写测试代码</span>
</div>
</div>
</div>
<!-- 按钮组 -->
<div class="ndt-button-group">
<button id="ndtTutorialBtn" class="ndt-welcome-btn ndt-welcome-btn-secondary">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 2C4.7 2 2 4.7 2 8s2.7 6 6 6 6-2.7 6-6-2.7-6-6-6zm0 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z" fill="currentColor"/>
<path d="M8 5v3l2 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<span>查看使用教程</span>
</button>
<button id="ndtWelcomeStartBtn" class="ndt-welcome-btn ndt-welcome-btn-primary">
<span>开始使用</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
</div>
`;
}
/**
* 获取宁德时代欢迎弹窗的 CSS 样式
*/
export function getNdtWelcomeModalStyles(): string {
return `
/* 宁德时代欢迎弹窗样式 */
.ndt-welcome-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
}
.ndt-welcome-modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease-out;
}
.ndt-welcome-modal-content {
position: relative;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 12px;
width: 100%;
max-width: 480px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
}
.ndt-welcome-logo-corner {
position: absolute;
top: 16px;
left: 24px;
height: 40px;
width: auto;
opacity: 0.9;
z-index: 10;
}
.ndt-welcome-modal-header {
padding: 60px 32px 20px;
text-align: center;
}
.ndt-welcome-icon {
font-size: 48px;
margin-bottom: 16px;
}
.ndt-welcome-modal-header h2 {
margin: 0 0 12px;
font-size: 20px;
font-weight: 600;
color: var(--vscode-foreground);
line-height: 1.4;
}
.ndt-welcome-modal-subtitle {
margin: 0;
font-size: 14px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
}
.ndt-welcome-modal-body {
padding: 0 32px 32px;
}
/* 试用期横幅 */
.ndt-trial-banner {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 8px;
margin-bottom: 20px;
font-size: 13px;
color: var(--vscode-descriptionForeground);
border-left: 3px solid var(--vscode-textLink-foreground);
}
.ndt-trial-banner strong {
color: var(--vscode-textLink-foreground);
font-weight: 600;
}
/* IC Coder 简介区域 */
.ndt-intro-section {
margin-bottom: 24px;
}
.ndt-section-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
color: var(--vscode-foreground);
}
.ndt-intro-text {
margin: 0 0 16px;
font-size: 13px;
color: var(--vscode-descriptionForeground);
line-height: 1.6;
}
.ndt-features {
display: flex;
flex-direction: column;
gap: 10px;
}
.ndt-feature-item {
padding: 10px 12px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 6px;
font-size: 13px;
color: var(--vscode-foreground);
border-left: 2px solid var(--vscode-textLink-foreground);
}
.ndt-feature-text {
display: block;
}
/* 按钮组 */
.ndt-button-group {
display: flex;
gap: 12px;
}
.ndt-welcome-btn {
flex: 1;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.ndt-welcome-btn-primary {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.ndt-welcome-btn-primary:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.ndt-welcome-btn-secondary {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
}
.ndt-welcome-btn-secondary:hover {
background: var(--vscode-button-secondaryHoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.ndt-welcome-btn:active {
transform: translateY(0);
}
`;
}
/**
* 获取宁德时代欢迎弹窗的 JavaScript 逻辑
*/
export function getNdtWelcomeModalScript(): string {
return `
// 宁德时代欢迎弹窗逻辑
(function() {
const modal = document.getElementById('ndtWelcomeModal');
const startBtn = document.getElementById('ndtWelcomeStartBtn');
const tutorialBtn = document.getElementById('ndtTutorialBtn');
const overlay = modal?.querySelector('.ndt-welcome-modal-overlay');
// 显示宁德时代欢迎弹窗
window.showNdtWelcomeModal = function() {
if (modal) {
modal.style.display = 'flex';
}
};
// 隐藏宁德时代欢迎弹窗
window.hideNdtWelcomeModal = function() {
if (modal) {
modal.style.display = 'none';
}
};
// 点击"查看使用教程"按钮
if (tutorialBtn) {
tutorialBtn.addEventListener('click', function() {
// 打开使用教程链接
vscode.postMessage({
command: 'openTutorial'
});
});
}
// 点击"开始使用"按钮
if (startBtn) {
startBtn.addEventListener('click', function() {
hideNdtWelcomeModal();
// 通知后端用户已查看欢迎弹窗
vscode.postMessage({ command: 'ndtWelcomeModalViewed' });
});
}
// 点击遮罩层关闭弹窗
if (overlay) {
overlay.addEventListener('click', function() {
hideNdtWelcomeModal();
vscode.postMessage({ command: 'ndtWelcomeModalViewed' });
});
}
// 阻止点击弹窗内容时关闭
const content = modal?.querySelector('.ndt-welcome-modal-content');
if (content) {
content.addEventListener('click', function(e) {
e.stopPropagation();
});
}
// 监听来自后端的消息
window.addEventListener('message', function(event) {
const message = event.data;
if (message.command === 'showNdtWelcomeModal') {
showNdtWelcomeModal();
}
});
})();
`;
}

View File

@ -195,11 +195,11 @@ export function getPlanCardStyles(): string {
background: var(--vscode-list-hoverBackground); background: var(--vscode-list-hoverBackground);
} }
.plan-btn-confirm { .plan-btn-confirm {
background: var(--vscode-button-background); background: #007ACC;
color: var(--vscode-button-foreground); color: #ffffff;
} }
.plan-btn-confirm:hover { .plan-btn-confirm:hover {
background: var(--vscode-button-hoverBackground); background: #005a9e;
} }
.plan-btn-cancel { .plan-btn-cancel {
background: transparent; background: transparent;
@ -720,7 +720,7 @@ export function getPlanCardScript(): string {
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="plan-card"> <div class="plan-card">
<div class="plan-header"> <div class="plan-header">
<span class="plan-icon">📋</span> <!-- <span class="plan-icon">📋</span> -->
<span class="plan-title">\${segment.planTitle || '执行计划'}</span> <span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div> </div>
\${progressHtml} \${progressHtml}

View File

@ -186,8 +186,8 @@ export function getProgressBarStyles(): string {
/* 已完成状态 */ /* 已完成状态 */
.progress-step.completed .step-circle { .progress-step.completed .step-circle {
background: var(--vscode-button-background); background: #007ACC;
border-color: var(--vscode-button-background); border-color: #007ACC;
} }
.progress-step.completed .step-number { .progress-step.completed .step-number {
@ -204,14 +204,14 @@ export function getProgressBarStyles(): string {
} }
.progress-step.completed + .progress-line { .progress-step.completed + .progress-line {
background: var(--vscode-button-background); background: #007ACC;
} }
/* 进行中状态 */ /* 进行中状态 */
.progress-step.active .step-circle { .progress-step.active .step-circle {
background: var(--vscode-button-background); background: #007ACC;
border-color: var(--vscode-button-background); border-color: #007ACC;
box-shadow: 0 0 0 2px var(--vscode-button-background)33; box-shadow: 0 0 0 2px #007ACC33;
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
@ -226,10 +226,10 @@ export function getProgressBarStyles(): string {
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%, 100% {
box-shadow: 0 0 0 2px var(--vscode-button-background)33; box-shadow: 0 0 0 2px #007ACC33;
} }
50% { 50% {
box-shadow: 0 0 0 4px var(--vscode-button-background)1a; box-shadow: 0 0 0 4px #007ACC1a;
} }
} }
@ -351,7 +351,7 @@ export function getProgressBarScript(): string {
// 更新连接线 // 更新连接线
document.querySelectorAll('.progress-line').forEach((line, index) => { document.querySelectorAll('.progress-line').forEach((line, index) => {
if (index < currentIndex) { if (index < currentIndex) {
line.style.background = 'var(--vscode-button-background)'; line.style.background = '#007ACC';
} else { } else {
line.style.background = 'var(--vscode-input-border)'; line.style.background = 'var(--vscode-input-border)';
} }

View File

@ -26,16 +26,16 @@ export function getUserInfoComponentContent(): string {
</div> </div>
</div> </div>
<!-- 升级到Pro按钮 (仅BASIC会员显示) --> <!-- 升级到Pro按钮 (仅BASIC会员显示) -->
<div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;"> <!-- <div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button> <button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
</div> </div> -->
</div> </div>
<div class="user-detail-body"> <div class="user-detail-body">
<div class="user-detail-item"> <!-- <div class="user-detail-item">
<span class="detail-label">剩余 Credits</span> <span class="detail-label">剩余 Credits</span>
<span class="detail-value" id="creditsDetail">-</span> <span class="detail-value" id="creditsDetail">-</span>
</div> </div> -->
<div class="user-detail-item logout-item" id="logoutItem"> <div class="user-detail-item logout-item" id="logoutItem">
<span class="detail-label">账户管理</span> <span class="detail-label">账户管理</span>
<span class="detail-value logout-link">退出登录</span> <span class="detail-value logout-link">退出登录</span>

View File

@ -30,6 +30,21 @@ import {
getInvitationModalStyles, getInvitationModalStyles,
getInvitationModalScript, getInvitationModalScript,
} from "./invitationModal"; } from "./invitationModal";
import {
getWelcomeModalContent,
getWelcomeModalStyles,
getWelcomeModalScript,
} from "./welcomeModal";
import {
getNdtWelcomeModalContent,
getNdtWelcomeModalStyles,
getNdtWelcomeModalScript,
} from "./ndtWelcomeModal";
import {
getExpiredModalContent,
getExpiredModalStyles,
getExpiredModalScript,
} from "./expiredModal";
/** /**
* 获取 WebView 面板的 HTML 内容 * 获取 WebView 面板的 HTML 内容
*/ */
@ -100,6 +115,9 @@ export function getWebviewContent(
${getProgressBarStyles()} ${getProgressBarStyles()}
${getInputAreaStyles()} ${getInputAreaStyles()}
${getInvitationModalStyles()} ${getInvitationModalStyles()}
${getWelcomeModalStyles()}
${getNdtWelcomeModalStyles()}
${getExpiredModalStyles()}
.file-editor-section { .file-editor-section {
margin-bottom: 15px; margin-bottom: 15px;
@ -286,6 +304,7 @@ export function getWebviewContent(
} }
.segment-text { .segment-text {
line-height: 1.6; line-height: 1.6;
font-size:0.9rem
} }
.segment-tool { .segment-tool {
background: var(--vscode-textBlockQuote-background); background: var(--vscode-textBlockQuote-background);
@ -307,7 +326,6 @@ export function getWebviewContent(
color: var(--vscode-foreground); color: var(--vscode-foreground);
} }
.tool-segment-result { .tool-segment-result {
margin-top: 6px;
font-size: 12px; font-size: 12px;
color: var(--vscode-descriptionForeground); color: var(--vscode-descriptionForeground);
padding-left: 22px; padding-left: 22px;
@ -466,6 +484,9 @@ export function getWebviewContent(
${getConversationHistoryBarContent()} ${getConversationHistoryBarContent()}
${getProgressBarContent()} ${getProgressBarContent()}
${getInvitationModalContent(qrCodeUri, logoUri)} ${getInvitationModalContent(qrCodeUri, logoUri)}
${getWelcomeModalContent(logoUri)}
${getNdtWelcomeModalContent(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: center; 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;" />
@ -861,6 +882,27 @@ export function getWebviewContent(
} }
break; break;
case 'showChanges':
// 显示代码变更
if (typeof showChangesPanel === 'function') {
showChangesPanel(message.changes);
}
break;
case 'changeAccepted':
// 变更已采纳
if (typeof handleChangeAccepted === 'function') {
handleChangeAccepted(message.changeId, message.success, message.error);
}
break;
case 'changeRejected':
// 变更已拒绝
if (typeof handleChangeRejected === 'function') {
handleChangeRejected(message.changeId, message.success, message.error);
}
break;
default: default:
console.log('[WebView] 未处理的消息类型:', message.command); console.log('[WebView] 未处理的消息类型:', message.command);
} }
@ -873,6 +915,9 @@ export function getWebviewContent(
${getProgressBarScript()} ${getProgressBarScript()}
${getInputAreaScript()} ${getInputAreaScript()}
${getInvitationModalScript()} ${getInvitationModalScript()}
${getWelcomeModalScript()}
${getNdtWelcomeModalScript()}
${getExpiredModalScript()}
</script></body> </script></body>
</html>`; </html>`;
} }

310
src/views/welcomeModal.ts Normal file
View File

@ -0,0 +1,310 @@
/**
* 欢迎弹窗(试用用户)
* 功能:在聊天面板内显示欢迎模态窗口
* 依赖:无
* 使用场景:试用用户首次登录时在聊天面板内显示
*/
/**
* 获取欢迎弹窗的 HTML 内容
*/
export function getWelcomeModalContent(logoUri?: string): string {
return `
<!-- 欢迎弹窗 -->
<div id="welcomeModal" class="welcome-modal" style="display: none;">
<div class="welcome-modal-overlay"></div>
<div class="welcome-modal-content">
${logoUri ? `<img src="${logoUri}" class="welcome-logo-corner" alt="IC Coder" />` : ""}
<div class="welcome-modal-header">
<div class="welcome-icon">🎉</div>
<h2>欢迎使用 IC Coder</h2>
</div>
<div class="welcome-modal-body">
<!-- 试用期提示 -->
<div class="trial-banner">
<span>您已获得 <strong>5 天企业版试用期</strong>企业版试用期内Credits用量无限并可无限制使用所有功能</span>
</div>
<!-- IC Coder 简介 -->
<div class="intro-section">
<h3 class="section-title">关于 IC Coder</h3>
<p class="intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform自主式人工智能 Verilog 编码平台。我们采用全球顶尖的IC Coder自研芯片设计微调模型为代码生成提供强大的AI能力支撑。</p>
<div class="features">
<div class="feature-item">
<span class="feature-text">多智能体架构Multi-Agent System多个专业化AI智能体协同工作分别负责架构设计、代码生成、验证测试等不同环节</span>
</div>
<div class="feature-item">
<span class="feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
</div>
<div class="feature-item">
<span class="feature-text">AI自主仿真IC Coder提供完全自动化的仿真验证流程无需手动编写测试代码</span>
</div>
</div>
</div>
<!-- 按钮组 -->
<div class="button-group">
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
<span>开始使用</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
</div>
`;
}
/**
* 获取欢迎弹窗的 CSS 样式
*/
export function getWelcomeModalStyles(): string {
return `
/* 欢迎弹窗样式 */
.welcome-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
}
.welcome-modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease-out;
}
.welcome-modal-content {
position: relative;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 12px;
width: 100%;
max-width: 480px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
}
.welcome-logo-corner {
position: absolute;
top: 16px;
left: 24px;
height: 40px;
width: auto;
opacity: 0.9;
z-index: 10;
}
.welcome-modal-header {
padding: 60px 32px 20px;
text-align: center;
}
.welcome-icon {
font-size: 48px;
margin-bottom: 16px;
}
.welcome-modal-header h2 {
margin: 0 0 12px;
font-size: 20px;
font-weight: 600;
color: var(--vscode-foreground);
line-height: 1.4;
}
.welcome-modal-subtitle {
margin: 0;
font-size: 14px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
}
.welcome-modal-body {
padding: 0 32px 32px;
}
/* 试用期横幅 */
.trial-banner {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 8px;
margin-bottom: 20px;
font-size: 13px;
color: var(--vscode-descriptionForeground);
border-left: 3px solid var(--vscode-textLink-foreground);
}
.trial-banner strong {
color: var(--vscode-textLink-foreground);
font-weight: 600;
}
/* IC Coder 简介区域 */
.intro-section {
margin-bottom: 24px;
}
.section-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
color: var(--vscode-foreground);
}
.intro-text {
margin: 0 0 16px;
font-size: 13px;
color: var(--vscode-descriptionForeground);
line-height: 1.6;
}
.features {
display: flex;
flex-direction: column;
gap: 10px;
}
.feature-item {
padding: 10px 12px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 6px;
font-size: 13px;
color: var(--vscode-foreground);
border-left: 2px solid var(--vscode-textLink-foreground);
}
.feature-text {
display: block;
}
/* 按钮组 */
.button-group {
display: flex;
}
.welcome-btn {
flex: 1;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.welcome-btn-primary {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.welcome-btn-primary:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.welcome-btn-secondary {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
}
.welcome-btn-secondary:hover {
background: var(--vscode-button-secondaryHoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.welcome-btn:active {
transform: translateY(0);
}
`;
}
/**
* 获取欢迎弹窗的 JavaScript 逻辑
*/
export function getWelcomeModalScript(): string {
return `
// 欢迎弹窗逻辑
(function() {
const modal = document.getElementById('welcomeModal');
const startBtn = document.getElementById('welcomeStartBtn');
const overlay = modal?.querySelector('.welcome-modal-overlay');
// 显示欢迎弹窗
window.showWelcomeModal = function() {
if (modal) {
modal.style.display = 'flex';
}
};
// 隐藏欢迎弹窗
window.hideWelcomeModal = function() {
if (modal) {
modal.style.display = 'none';
}
};
// 点击"开始使用"按钮
if (startBtn) {
startBtn.addEventListener('click', function() {
hideWelcomeModal();
});
}
// 点击遮罩层关闭弹窗
if (overlay) {
overlay.addEventListener('click', function() {
hideWelcomeModal();
});
}
// 阻止点击弹窗内容时关闭
const content = modal?.querySelector('.welcome-modal-content');
if (content) {
content.addEventListener('click', function(e) {
e.stopPropagation();
});
}
// 监听来自后端的消息
window.addEventListener('message', function(event) {
const message = event.data;
if (message.command === 'showWelcomeModal') {
showWelcomeModal();
}
});
// 页面加载时检查是否需要显示欢迎弹窗
vscode.postMessage({ command: 'checkWelcomeModal' });
})();
`;
}

Binary file not shown.

View File

@ -3,6 +3,7 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
//@ts-check //@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/ /** @typedef {import('webpack').Configuration} WebpackConfig **/
@ -45,5 +46,12 @@ const extensionConfig = {
infrastructureLogging: { infrastructureLogging: {
level: "log", // enables logging required for problem matchers level: "log", // enables logging required for problem matchers
}, },
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'src/assets', to: 'assets' }
]
})
]
}; };
module.exports = [ extensionConfig ]; module.exports = [ extensionConfig ];