61 Commits

Author SHA1 Message Date
fd5a01c67f Merge branch 'feat/codeToChat' into feat/ningDeShiDai 2026-03-10 18:40:13 +08:00
29e80ce296 feat: 优化消息处理和界面显示
- 增强 messageHandler 消息处理逻辑
   - 优化 messageArea 显示效果
   - 改进 webviewContent 界面交互
2026-03-10 18:39:50 +08:00
c244a308d7 style:将spec改为specification 2026-03-10 17:08:36 +08:00
a25d68f527 feat: 添加用户手册只读预览功能
- 新增 UserManualPanel 组件实现只读预览
   - 支持 Markdown 完整渲染(表格、代码块、图片、分隔线)
   - 优化排版和字体大小
   - 用户无法编辑手册内容
2026-03-10 17:01:16 +08:00
77b54aebf0 feat: 添加用户手册功能
- 新增用户手册 Markdown 文档及配套截图
   - 新增打开用户手册命令
   - 在侧边栏和主面板中集成用户手册入口
   - 优化用户手册打开方式,支持 Markdown 预览
2026-03-10 16:29:37 +08:00
840436eb36 refactor: 优化设置面板和模型提示文案
- 移除设置面板中的规则配置标签页
   - 更新模型选择器提示文案为"FPGA专属微调模型"
2026-03-10 14:23:37 +08:00
f5dd7534f0 feat:将模型选中固定为max模型
- 并且鼠标移动上去显示“IC Coder自研顶尖微调模型“
2026-03-10 14:13:20 +08:00
ebb9de5294 tyle:修改了聊天面板的样式及删除了用户反馈和web端的跳转 2026-03-10 14:02:44 +08:00
531d140b99 refactor: 优化错误提示信息
- 将"当前访问人数过多"改为更通用的"处理用户消息失败"
   - 移除错误消息中的  emoji,保持简洁
2026-03-09 15:55:51 +08:00
97b8e8aa7d refactor: 更新后端服务地址占位符 2026-03-09 15:34:41 +08:00
4ed998e937 feat: 添加后端服务地址自定义配置功能
- 在设置面板添加后端服务地址配置项
   - 支持保存、加载和重置自定义配置
   - 配置持久化存储,重启后保留
   - 添加 SSE 请求日志用于验证配置
2026-03-09 15:29:56 +08:00
ad0f0336d5 feat: 移除用户信息、余额检查和登录过期提示
- 隐藏用户信息显示和退出登录按钮
   - 删除发送消息前的余额检查逻辑
   - 删除对话完成后的余额更新逻辑
   - 注释掉所有登录过期弹窗提示
   - 移除用户服务和余额服务的初始化调用
2026-03-09 14:28:41 +08:00
7cde4fa138 refactor: 优化代码格式和用户提示
- 统一代码格式化(Prettier)
- 将 iverilog 相关错误提示改为 'IC Coder编译器'
- 优化后端服务错误提示为 '当前访问人数过多,请稍后重试'
- 修复代码风格一致性问题
2026-03-09 11:10:56 +08:00
1b7259d1c1 feat:排除打包项目中的waveform_trace文件中的无关文档 2026-03-09 10:41:17 +08:00
09ff812562 feat:修复 Windows vvp 解析问题
- 修复 iverilog 生成的 .vvp 文件 shebang 导致 Windows 解析失败
2026-03-07 18:41:42 +08:00
e7c631d532 feat: 优化文档结构
- 将文档移至 docs/ 目录统一管理
   - 更新 .vscodeignore 排除规则
2026-03-07 18:41:14 +08:00
06573e37d7 feat: 优化 webpack 打包配置
- 添加自动模式切换(开发/生产)
   - 启用 Tree Shaking 移除未使用代码
   - 加快编译速度(transpileOnly)
   - 添加打包体积监控
   - 自动清理旧文件
   - 添加打包优化文档
2026-03-06 18:27:56 +08:00
d740f4da44 feat: 支持文件路径标签带行号点击跳转
- 前端解析 file.v:1-2 格式,提取文件名和行号
   - 新增 openFilePathTag 命令,支持智能文件查找
   - 修复模板字符串中正则表达式转义问题
   - 不影响现有 openFile 和 diff 功能
2026-03-06 16:24:21 +08:00
f24bd38ec7 feat: 优化上下文项显示和识别逻辑
- 支持显示所有类型的上下文项(文件和代码片段)
   - 增强路径识别,支持代码片段格式(文件名:行号-行号)
2026-03-06 15:40:59 +08:00
45934baf0a feat: 添加上下文项点击功能
- 文件类型可点击打开文件
   - 代码片段可点击打开文件并选中对应代码
   - 文件夹类型不可点击
2026-03-06 10:13:27 +08:00
4384ee53c5 fix: 修复关闭面板后快捷键无法自动打开面板的问题
- 通过 try-catch 检测 webview 是否真正可用
   - 修复 panel._isDisposed 检测不准确的问题
   - 增加异常捕获防止发送消息时崩溃
   - 延长消息发送延迟至 500ms 确保面板加载完成
2026-03-06 10:05:52 +08:00
d89c326be5 Merge branch 'feat/DeleteConfirmation' into feat/codeToChat 2026-03-06 09:16:12 +08:00
2dccb4f871 update:changelog.md 2026-03-06 09:11:33 +08:00
a9ddf3074e 1.0.12 2026-03-06 09:10:51 +08:00
db087bb184 update:更新changelog.md 2026-03-06 09:10:09 +08:00
5e9083041f fix: 修复多选问题提交后选中项不显示高亮的问题 2026-03-06 09:08:38 +08:00
be0555d6bc feat:codeToChat 2026-03-06 08:59:02 +08:00
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
60 changed files with 3817 additions and 1538 deletions

3
.gitignore vendored
View File

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

33
.vscodeignore Normal file
View File

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

View File

@ -2,6 +2,57 @@
所有重要的项目变更都将记录在此文件中。
## [1.0.12] - 2026-03-06
### 新增
- 支持 AskUserQuestion 多问题和多选功能
## [1.0.9] - 2026-03-04
### 优化
- 将工具折叠图标颜色从蓝色改为灰色
- 统一使用蓝色主题色
- 优化打包配置,排除重复的 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插件端正式上线。

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

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

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,42 @@
# 代码快速添加到对话功能
## 功能说明
选中代码后,通过右键菜单/小灯泡/快捷键Ctrl+Shift+I将代码作为上下文添加到聊天面板输入框上方。
## 实现方式
### 1. Code Action Provider
`src/providers/codeActionProvider.ts` - 提供小灯泡菜单选项
### 2. 命令注册
`src/extension.ts` - 注册 `ic-coder.addCodeToChat` 命令,发送消息到 webview
### 3. 全局引用
`src/panels/ICHelperPanel.ts` - 保存 panel 到 `(global as any).currentICHelperPanel`
### 4. 上下文显示
`src/views/contextDisplay.ts` - 添加 `code` 类型支持和 `addCodeContext` 消息处理
### 5. 配置
`package.json` - 配置命令、右键菜单、快捷键
## 用户体验
1. 选中代码
2. 右键/小灯泡/Ctrl+Shift+I
3. 代码显示为上下文项:`文件名.v:10-25` 📄
4. 输入问题发送(代码自动作为上下文)
## 数据结构
代码上下文存储为 JSON
```json
{
"fileName": "路径",
"startLine": 10,
"endLine": 25,
"code": "代码内容",
"languageId": "verilog"
}
```

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. 考虑添加用户配置选项

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,55 @@
# Webpack 打包优化说明
## 优化内容
### 1. 自动模式切换
- 开发模式:保持源码可读性
- 生产模式:自动压缩代码
### 2. 性能优化
- **Tree Shaking**:移除未使用的代码
- **transpileOnly**:跳过类型检查,加快编译速度
- **自动清理**:每次打包自动删除旧文件
### 3. 体积监控
- 单文件超过 2MB 会发出警告
- 帮助及时发现打包体积问题
## 使用方法
### 开发模式
```bash
# 编译(不压缩)
pnpm run compile
# 监听模式(自动重新编译)
pnpm run watch
```
### 生产模式
```bash
# Windows
set NODE_ENV=production && pnpm run package
# macOS/Linux
NODE_ENV=production pnpm run package
```
## 打包结果
- **输出目录**`dist/`
- **入口文件**`dist/extension.js`
- **静态资源**`dist/assets/`
## 性能对比
| 模式 | 体积 | 编译速度 | Source Map |
|------|------|----------|------------|
| 开发 | 较大 | 快 | 完整 |
| 生产 | 小 | 较慢 | 隐藏 |
## 注意事项
1. 开发时使用 `pnpm run watch`,修改代码自动重新编译
2. 发布前必须使用生产模式打包
3. 如果打包体积超过 2MB检查是否引入了不必要的依赖

251
media/USER_MANUAL.md Normal file
View File

@ -0,0 +1,251 @@
# IC Coder 插件端用户手册
欢迎**宁德时代新能源科技股份有限公司**的各位专家使用 IC Coder 企业版!
本手册旨在为贵公司提供插件的安装、配置及使用指导,帮助您快速上手并充分发挥工具的功能。
在使用过程中,如遇任何功能异常、性能问题或操作疑问,欢迎随时联系我们,我们将在第一时间为您提供支持。
若贵司有特定的业务场景或个性化功能需求,也可直接与我们沟通,我们会为贵司进行定制化开发。
| 功能 | 说明 |
| ----------------------- | ---------------------------------------------------- |
| 自研微调模型 | IC Coder 自研顶尖 Max 微调模型,专为 FPGA 开发 |
| 高质量 Verilog 代码生成 | 根据自然语言描述生成符合规范的 Verilog 代码 |
| 自动语法检查 | 自动检测代码语法错误并自动修复 |
| 自动仿真 | 内置编译器,自动编译和仿真 |
| 波形检查工具 | 内置 VCD 波形查看器,自动波形解析 |
| 自主检查迭代 | AI 自动检查代码问题并迭代优化 |
| Diff 功能 | 快速比较代码差异,追踪 AI 修改,可一键确认和撤销修改 |
| Code Action 快捷操作 | Ctrl+L 快捷键或右键菜单快速添加代码到对话 |
| 支持上下文连续对话 | 多轮对话AI 记住之前的交互内容 |
| 会话历史管理 | 自动保存对话记录,随时恢复历史会话 |
## IC Coder 快速入门指南
### 系统要求
- **Visual Studio Code**: 版本 >= 1.60.0
各位专家也可以通过这个链接下载VSCode [下载链接](https://code.visualstudio.com/)
---
### 安装步骤
#### 步骤 1通过 VSIX 文件安装(推荐)
1. **获取安装包**
- 确保您已经获得 `iccoder-Trial-1.0.vsix` 文件
2. **打开 VS Code**
- 启动 Visual Studio Code
3. **安装插件**
有以下三种安装方式:
**方式 A通过命令面板**
-`Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS) 打开命令面板
- 输入 `Extensions: Install from VSIX...`
- 选择 `iccoder-Trial-1.0.vsix` 文件
- 等待安装完成
![安装方式1.png](./manual/安装方式1.png)
**方式 B通过扩展视图**
- 点击左侧活动栏的扩展图标(或按 `Ctrl+Shift+X`
- 点击扩展视图右上角的 `...` (更多操作)
- 选择 `从 VSIX 安装...`
- 选择 `iccoder-Trial-1.0.vsix` 文件
![安装方式2.png](./manual/安装方式2.png)
**方式 C通过命令行**
```bash
code --install-extension iccoder-Trial-1.0.vsix
```
4. **重启 VS Code**
- 安装完成后,建议重启 VS Code 以确保插件正常加载
#### 步骤 2打开 IC Coder 界面
**登录后会自动打开**,手动打开也有以下几种方式:
**方式 1通过侧边栏**
- 点击左侧活动栏的 IC Coder 图标
- 侧边栏会显示 IC Coder 聊天界面
![侧边栏打开.png](./manual/侧边栏打开.png)
**方式 2通过命令面板**
- 按 `Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS)
- 输入以下命令之一:
- `IC Coder: 打开聊天` - 打开聊天界面
- `打开 IC Coder 助手` - 打开助手面板
![命令面板打开.png](./manual/命令面板打开.png)
---
#### 步骤 3开始使用
插件已预配置好后端服务,安装后即可直接使用,无需手动配置。
![聊天界面.png](./manual/聊天界面.png)
### 故障排除
#### 问题 :插件无法安装
**症状**:安装 VSIX 文件时报错
#### 解决方案:
- 确认 VS Code 版本 >= 1.60.0
- 检查 VSIX 文件是否完整(未损坏)
- 尝试以管理员权限运行 VS Code
- 清除 VS Code 缓存后重试
### 完整使用流程示例
下面通过一个完整的案例,展示如何使用 IC Coder 从需求到代码生成的全过程。
#### 步骤 1输入设计需求
在对话框中输入设计需求,例如:
```
我需要设计一个 8 位加法器,要求有进位输入和进位输出
```
点击**发送**按钮。
![输入需求.png](./manual/输入需求.png)
#### 步骤 2AI 询问补充信息
AI 会根据需求,询问一些关键的设计细节。例如:
- 是否需要溢出检测?
- 时钟频率要求是多少?
- 是否需要流水线设计?
您只需要根据实际需求**选择相应的选项或者直接输入需求**即可AI 会根据您的选择生成最合适的设计方案。
#### 步骤 3确认 AI 生成的任务列表
AI 会根据您的需求和补充信息生成一个详细的任务列表Todo List
![确认任务.png](./manual/确认任务.png)
仔细查看任务列表,确认无误后点击**确认**按钮AI 将开始执行。
#### 步骤 4观察 AI 执行过程
AI 开始工作后,您可以在对话框中实时看到所有执行步骤:
![观察执行过程.png](./manual/观察执行过程.png)
每个步骤完成后,任务列表中对应的项目会被标记为完成状态。
#### 步骤 5仿真运行与结果查看
当 AI 完成代码生成后,会自动运行仿真验证:
![仿真运行结果.png](./manual/仿真运行结果.png)
#### 步骤 6查看生成的文件
所有生成的文件会自动保存到您的工作目录中:
```
project/
├── src/
│ └── tb_adder_8bit.v # RTL 设计文件
├── sim/
│ └── tb_adder_8bit # 测试平台文件
└── tb_adder_8bit.vcd # 波形文件
```
您可以在 VS Code 的文件资源管理器中直接打开这些文件进行查看或修改。
#### 步骤8继续对话
如果您对给出的结果不太满意您可以告诉IC Coder您想具体修改的地方或者文件
#### 使用流程总结
整个使用流程可以概括为:
- **输入需求** → 在对话框中描述您的设计需求
- **回答问题** → 根据 AI 的询问选择合适的选项
- **确认任务** → 查看并确认 AI 生成的任务列表
- **观察执行** → 实时查看 AI 的所有执行步骤
- **查看结果** → 仿真成功后查看生成的文件
#### 使用提示
**如何描述需求更准确?**
- **明确功能**:清楚说明模块要实现什么功能
- **指定参数**:说明位宽、时钟频率等关键参数
- **特殊要求**:如果有特殊的时序要求或接口规范,请明确说明
**示例:**
```
好的描述:设计一个 16 位的 FIFO深度为 256支持异步读写
不够清晰:帮我写一个 FIFO
```
#### 常见问题
**Q: 仿真失败了怎么办?**
A: AI 会根据错误自动修复代码并重新仿真。
**Q: 可以修改生成的代码吗?**
A: 可以,可以直接编辑文件,然后告诉 AI 重新运行仿真。
**Q: 可以导入已有的代码吗?**
A: 可以,在工作区中打开对应的代码文件夹,然后直接在对话中告诉 AI 您要修改或优化哪个文件AI 会读取并进行修改。
**Q: 如何查看 AI 的思考过程?**
A: 在执行过程中AI 会实时显示每一步的操作和决策依据。
**Q: 如何保存对话历史?**
A: 对话历史会自动保存在本地,可以点击历史对话查看历史会话记录。
---
### 卸载插件
如需卸载插件:
1. 打开扩展视图
2. 找到 "IC Coder" 插件
3. 点击卸载按钮
4. 重启 VS Code
---
### 注意事项
1. **需提前打开一个文件夹作为工作区**,否则会准确的为您服务
![打开文件夹.png](./manual/打开文件夹.png)
2. **开箱即用**
- 插件已预配置后端服务,无需手动设置
- 安装后即可直接使用所有功能
---
**祝您使用愉快!如有问题欢迎反馈。**

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -2,7 +2,7 @@
"name": "iccoder",
"displayName": "IC Coder: Agentic Verilog Platform",
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
"version": "1.0.5",
"version": "1.0.12",
"publisher": "ICCoderAgenticVerilogPlatform",
"engines": {
"vscode": "^1.80.0"
@ -54,6 +54,28 @@
"command": "ic-coder.testNotification",
"title": "测试系统通知",
"category": "IC Coder"
},
{
"command": "ic-coder.addCodeToChat",
"title": "添加到 IC Coder 对话",
"category": "IC Coder"
}
],
"menus": {
"editor/context": [
{
"command": "ic-coder.addCodeToChat",
"when": "editorHasSelection",
"group": "9_cutcopypaste"
}
]
},
"keybindings": [
{
"command": "ic-coder.addCodeToChat",
"key": "ctrl+l",
"mac": "cmd+l",
"when": "editorTextFocus && editorHasSelection"
}
],
"viewsContainers": {
@ -135,6 +157,7 @@
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.7.1",
"copy-webpack-plugin": "^14.0.0",
"eslint": "^9.39.1",
"ts-loader": "^9.5.4",
"typescript": "^5.9.3",
@ -142,14 +165,6 @@
"webpack": "^5.103.0",
"webpack-cli": "^6.0.1"
},
"files": [
"dist",
"media",
"tools",
"src/assets",
"LICENSE",
"CHANGELOG.md"
],
"dependencies": {
"@wavedrom/doppler": "^1.14.0",
"eventsource-parser": "^3.0.6",

24
pnpm-lock.yaml generated
View File

@ -57,6 +57,9 @@ importers:
'@vscode/vsce':
specifier: ^3.7.1
version: 3.7.1
copy-webpack-plugin:
specifier: ^14.0.0
version: 14.0.0(webpack@5.103.0)
eslint:
specifier: ^9.39.1
version: 9.39.1
@ -823,6 +826,12 @@ packages:
convert-source-map@2.0.0:
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:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@ -1928,6 +1937,10 @@ packages:
serialize-javascript@6.0.2:
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:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
@ -3297,6 +3310,15 @@ snapshots:
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: {}
cross-spawn@7.0.6:
@ -4431,6 +4453,8 @@ snapshots:
dependencies:
randombytes: 2.1.0
serialize-javascript@7.0.4: {}
setimmediate@1.0.5: {}
shallow-clone@3.0.1:

View File

@ -29,6 +29,9 @@ export interface IccoderConfig {
serviceTier: ServiceTier;
}
/** 自定义配置缓存 */
let customConfig: Partial<IccoderConfig> | null = null;
/** 环境配置 */
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 本地开发环境 - 通过 Gateway 路由 */
@ -38,7 +41,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
loginUrl: "http://localhost/login",
timeout: 300000,
userId: "default-user",
serviceTier: "max", // 默认使用 max
serviceTier: "max",
},
/** 测试服务器环境 - 通过 Gateway 路由 */
test: {
@ -60,6 +63,13 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
},
};
/**
* 设置自定义配置
*/
export function setCustomConfig(config: Partial<IccoderConfig>) {
customConfig = config;
}
/**
* 获取当前环境
*/
@ -71,7 +81,14 @@ export function getCurrentEnv(): Environment {
* 获取配置项
*/
export function getConfig(): IccoderConfig {
return { ...ENV_CONFIG[CURRENT_ENV] };
const baseConfig = { ...ENV_CONFIG[CURRENT_ENV] };
// 合并自定义配置(空字符串表示使用默认)
if (customConfig?.backendUrl && customConfig.backendUrl !== '') {
baseConfig.backendUrl = customConfig.backendUrl;
}
return baseConfig;
}
/**

View File

@ -2,49 +2,82 @@ import * as vscode from "vscode";
import { ICViewProvider } from "./views/ICViewProvider";
import { showICHelperPanel } from "./panels/ICHelperPanel";
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
import { UserManualPanel } from "./panels/UserManualPanel";
import { ChatHistoryManager } from "./utils/chatHistoryManager";
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
import { VCDFileServer } from "./services/vcdFileServer";
import { initUserService } from "./services/userService";
import { initCreditsService } from "./services/creditsService";
import { isTokenExpired } from "./utils/jwtUtils";
import { NotificationService } from "./services/notificationService";
import { InvitationService } from "./services/invitationService";
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
import { setCustomConfig } from "./config/settings";
export async function activate(context: vscode.ExtensionContext) {
console.log("🎉 IC Coder 插件已激活!");
// 加载保存的配置
const savedSettings = context.globalState.get('generalSettings') as any;
if (savedSettings?.backendUrl) {
setCustomConfig({
backendUrl: savedSettings.backendUrl,
});
}
// 创建装饰类型(代码旁边的提示)
const decorationType = vscode.window.createTextEditorDecorationType({
after: {
contentText: ' Ctrl+L 添加到 IC Coder 对话',
color: '#888',
fontStyle: 'italic',
margin: '0 0 0 1em'
}
});
// 更新装饰
const updateDecorations = () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
if (!editor.selection.isEmpty) {
const range = new vscode.Range(editor.selection.end, editor.selection.end);
const decoration = { range };
editor.setDecorations(decorationType, [decoration]);
} else {
editor.setDecorations(decorationType, []);
}
};
context.subscriptions.push(
vscode.window.onDidChangeTextEditorSelection(updateDecorations),
vscode.window.onDidChangeActiveTextEditor(updateDecorations)
);
updateDecorations();
// 初始化通知服务
const notificationService = NotificationService.getInstance(context);
console.log('[Extension] 通知服务已初始化');
// 【关键】在创建 AuthProvider 之前,先检查并清除过期的 session
const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
if (storedSessions.length > 0) {
const session = storedSessions[0];
const token = session.accessToken;
console.log('[Extension] 检查 token 是否过期...');
if (token) {
const expired = isTokenExpired(token);
console.log('[Extension] token 过期检查结果:', expired);
if (expired) {
// 必须等待清除完成后再创建 AuthProvider
await context.globalState.update('icCoderSessions', []);
await context.globalState.update('icCoderUserInfo', undefined);
console.log('[Extension] Token 已过期,已清除所有登录状态');
}
}
}
// 初始化用户服务
initUserService(context);
// 初始化 Credits 服务
initCreditsService(context);
// 【已禁用】登录和 token 验证 - 无需登录即可使用
// const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
// console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
//
// if (storedSessions.length > 0) {
// const session = storedSessions[0];
// const token = session.accessToken;
// console.log('[Extension] 检查 token 是否过期...');
//
// if (token) {
// const expired = isTokenExpired(token);
// console.log('[Extension] token 过期检查结果:', expired);
//
// if (expired) {
// await context.globalState.update('icCoderSessions', []);
// await context.globalState.update('icCoderUserInfo', undefined);
// console.log('[Extension] Token 已过期,已清除所有登录状态');
// }
// }
// }
// 初始化 VCD 文件服务器
const vcdFileServer = new VCDFileServer(context.extensionUri);
@ -59,25 +92,18 @@ export async function activate(context: vscode.ExtensionContext) {
dispose: () => vcdFileServer.stop()
});
// 注册 Authentication Provider(此时 icCoderSessions 已经被清除)
// 【已禁用】Authentication Provider 注册 - 无需登录
const authProvider = new ICCoderAuthenticationProvider(context);
context.subscriptions.push(
vscode.authentication.registerAuthenticationProvider(
"iccoder",
"IC Coder",
authProvider
)
);
// context.subscriptions.push(
// vscode.authentication.registerAuthenticationProvider(
// "iccoder",
// "IC Coder",
// authProvider
// )
// );
// 检查登录状态,如果已登录则自动打开聊天面板
vscode.authentication.getSession("iccoder", [], { createIfNone: false })
.then((session) => {
if (session) {
vscode.commands.executeCommand("ic-coder.openChat");
}
}, () => {
// 未登录,不做任何操作
});
// 【已禁用】登录状态检查 - 直接打开聊天面板
vscode.commands.executeCommand("ic-coder.openChat");
// 注册命令:打开助手面板
const openPanelCommand = vscode.commands.registerCommand(
@ -156,23 +182,43 @@ export async function activate(context: vscode.ExtensionContext) {
}
);
// 注册命令:打开用户手册
const openUserManualCommand = vscode.commands.registerCommand(
"ic-coder.openUserManual",
() => {
UserManualPanel.render(context.extensionUri);
}
);
// 注册命令:用户登录
const loginCommand = vscode.commands.registerCommand(
"ic-coder.login",
async () => {
async (options?: { forceReauth?: boolean }) => {
try {
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
try {
await vscode.authentication.getSession("iccoder", [], {
clearSessionPreference: true,
createIfNone: false
});
} catch {
// 忽略错误
const forceReauth = options?.forceReauth === true;
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
const expired = session?.accessToken
? isTokenExpired(session.accessToken)
: null;
// 会话仍有效时,直接打开聊天面板
if (session && expired === false && !forceReauth) {
vscode.commands.executeCommand("ic-coder.openChat");
return;
}
// 创建新 session
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
// 1) 清空当前登录状态信息
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) {
vscode.window.showErrorMessage(`登录失败: ${error}`);
}
@ -238,6 +284,81 @@ export async function activate(context: vscode.ExtensionContext) {
}
);
// 注册命令:将选中代码添加到对话
const addCodeToChat = vscode.commands.registerCommand(
"ic-coder.addCodeToChat",
async () => {
console.log('[addCodeToChat] 命令触发');
const editor = vscode.window.activeTextEditor;
if (!editor) {
console.log('[addCodeToChat] 没有活动编辑器');
return;
}
const selection = editor.selection;
const selectedText = editor.document.getText(selection);
if (!selectedText) {
vscode.window.showWarningMessage("请先选择代码");
return;
}
const fileName = editor.document.fileName;
const startLine = selection.start.line + 1;
const endLine = selection.end.line + 1;
// 检查是否已有打开的面板
let panel = (global as any).currentICHelperPanel;
let needCreatePanel = false;
if (!panel) {
needCreatePanel = true;
} else {
// 尝试访问 webview如果抛出异常说明已销毁
try {
const _ = panel.webview;
} catch (e) {
needCreatePanel = true;
}
}
console.log('[addCodeToChat] 需要创建面板:', needCreatePanel);
if (needCreatePanel) {
console.log('[addCodeToChat] 正在打开面板...');
await showICHelperPanel(context);
panel = (global as any).currentICHelperPanel;
console.log('[addCodeToChat] 面板打开后状态:', panel ? '成功' : '失败');
// 如果面板仍未创建(如未登录),直接返回
if (!panel) {
console.log('[addCodeToChat] 面板创建失败,退出');
return;
}
}
// 发送代码上下文
console.log('[addCodeToChat] 准备发送代码到面板');
setTimeout(() => {
try {
if (panel?.webview) {
console.log('[addCodeToChat] 发送 addCodeContext 消息');
panel.webview.postMessage({
command: 'addCodeContext',
fileName,
startLine,
endLine,
code: selectedText,
languageId: editor.document.languageId
});
}
} catch (e) {
console.log('[addCodeToChat] 发送消息失败:', e);
}
}, 500);
}
);
// 注册命令:查看会话历史
// TODO: 这些命令需要根据新的任务架构重新实现
// 暂时注释掉,等待重新实现
@ -300,16 +421,25 @@ export async function activate(context: vscode.ExtensionContext) {
// 注册 VCD 自定义编辑器
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
// 注册 Code Action Provider
const codeActionProvider = vscode.languages.registerCodeActionsProvider(
{ scheme: 'file' },
new ICCoderCodeActionProvider(),
{ providedCodeActionKinds: [vscode.CodeActionKind.RefactorRewrite] }
);
// 添加到订阅
context.subscriptions.push(
openPanelCommand,
openChatCommand,
openVCDViewerCommand,
openVCDViewerInBrowserCommand,
openUserManualCommand,
loginCommand,
logoutCommand,
changeInvitationCodeCommand,
testNotificationCommand,
addCodeToChat,
// testTrialUserCommand,
// testExpiredUserCommand,
// TODO: 等待重新实现这些命令
@ -320,7 +450,8 @@ export async function activate(context: vscode.ExtensionContext) {
// clearHistoryCommand,
// searchSessionCommand,
viewRegistration,
vcdEditorProvider
vcdEditorProvider,
codeActionProvider
);
}

View File

@ -18,50 +18,12 @@ import {
startChangeSession,
handleOpenFileDiff,
} from "../utils/messageHandler";
import { setCustomConfig } from "../config/settings";
import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory";
import { getCachedUserInfo } from "../services/userService";
import { isTokenExpired } from "../utils/jwtUtils";
import { setBalanceUpdateCallback } from "../services/creditsService";
/**
* 获取会员等级图标 URI
*/
function getTierIconUri(
webview: vscode.Webview,
context: vscode.ExtensionContext,
tierCode?: string,
): string | undefined {
if (!tierCode) {
return undefined;
}
const tierIconMap: Record<string, string> = {
BASIC: "free.png",
TRIAL: "PRO-Try.png",
ADVANCED: "PRO.png",
PROFESSIONAL: "PRO+.png",
};
const iconFile = tierIconMap[tierCode];
if (!iconFile) {
return undefined;
}
const iconUri = webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"assets",
"titleIcon",
iconFile,
),
);
return iconUri.toString();
}
/**
* 创建并显示 IC 助手面板
@ -70,57 +32,35 @@ export async function showICHelperPanel(
context: vscode.ExtensionContext,
viewColumn?: vscode.ViewColumn,
) {
// 检查 token 是否过期
let token: string | undefined;
try {
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
token = session?.accessToken;
} catch (error) {
console.warn("[ICHelperPanel] 获取 session 失败:", error);
}
if (token && isTokenExpired(token)) {
// 清除过期的 session
await context.globalState.update("icCoderSessions", []);
await context.globalState.update("icCoderUserInfo", undefined);
const action = await vscode.window.showWarningMessage(
"登录已过期,请重新登录",
"立即登录",
);
if (action === "立即登录") {
vscode.commands.executeCommand("ic-coder.login");
}
return;
}
// 检查用户是否已登录
try {
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
if (!session) {
vscode.window
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
.then((selection) => {
if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login");
}
});
return;
}
} catch (error) {
vscode.window
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
.then((selection) => {
if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login");
}
});
return;
}
// 创建WebView面板
// try {
// const session = await vscode.authentication.getSession("iccoder", [], {
// createIfNone: false,
// });
// if (!session) {
// vscode.window
// .showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
// .then((selection) => {
// if (selection === "立即登录") {
// vscode.commands.executeCommand("ic-coder.login", {
// forceReauth: true,
// });
// }
// });
// return;
// }
// } catch (error) {
// vscode.window
// .showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
// .then((selection) => {
// if (selection === "立即登录") {
// vscode.commands.executeCommand("ic-coder.login", {
// forceReauth: true,
// });
// }
// });
// return;
// }
// 创建WebView面板
const panel = vscode.window.createWebviewPanel(
@ -132,11 +72,14 @@ export async function showICHelperPanel(
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets"),
vscode.Uri.joinPath(context.extensionUri, "dist", "assets"),
],
},
);
// 保存 panel 引用到全局
(global as any).currentICHelperPanel = panel;
// 为面板生成唯一ID
const panelId = `panel_${Date.now()}_${Math.random()
.toString(36)
@ -160,7 +103,7 @@ export async function showICHelperPanel(
const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"model",
"Auto.png",
@ -169,7 +112,7 @@ export async function showICHelperPanel(
const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"model",
"lite.png",
@ -178,7 +121,7 @@ export async function showICHelperPanel(
const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"model",
"Sy.png",
@ -187,7 +130,7 @@ export async function showICHelperPanel(
const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"model",
"Max.png",
@ -198,7 +141,7 @@ export async function showICHelperPanel(
const qrCodeUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"QRCode",
"wx.png",
@ -221,81 +164,6 @@ export async function showICHelperPanel(
logoUri.toString(),
);
// 获取并发送用户信息到 webview
try {
// 优先使用缓存的用户信息
let userInfo = getCachedUserInfo();
if (userInfo) {
// 使用缓存的用户信息
console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo);
console.log("[ICHelperPanel] Credits 余额:", userInfo.credits);
const tierIconUrl = getTierIconUri(
panel.webview,
context,
userInfo.membership?.tierCode,
);
const messageData = {
command: "updateUserInfo",
userInfo: {
userId: userInfo.userId,
nickname: userInfo.nickname,
username: userInfo.username,
credits: userInfo.credits,
membership: userInfo.membership,
},
tierIconUrl: tierIconUrl,
};
console.log("[ICHelperPanel] 发送用户信息到前端:", messageData);
panel.webview.postMessage(messageData);
} else {
// 如果没有缓存,从 session 中获取
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
if (session) {
console.log(
"[ICHelperPanel] 从 session 获取用户信息, account:",
session.account,
);
panel.webview.postMessage({
command: "updateUserInfo",
userInfo: {
userId: session.account.id,
nickname: session.account.label,
username: session.account.label,
},
});
}
}
} catch (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;
if (pendingMessage) {
@ -430,6 +298,7 @@ export async function showICHelperPanel(
message.askId,
message.selected,
message.customInput,
message.answers
);
break;
// 新增:中止对话
@ -484,6 +353,78 @@ export async function showICHelperPanel(
// 退出登录
vscode.commands.executeCommand("ic-coder.logout");
break;
case "openFile":
// 打开文件
if (message.filePath) {
const path = require('path');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder
? message.filePath
: vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc);
});
}
break;
case "openFileWithSelection":
// 打开文件并选中代码
if (message.filePath) {
const path = require('path');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder
? message.filePath
: vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc).then(editor => {
const start = new vscode.Position(message.startLine - 1, 0);
const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length);
editor.selection = new vscode.Selection(start, end);
editor.revealRange(new vscode.Range(start, end));
});
});
}
break;
case "openFilePathTag":
// 打开文件路径标签(智能查找)
if (message.filePath) {
const path = require('path');
const fs = require('fs');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
let fullPath = message.filePath;
// 如果是相对路径且工作区存在
if (!path.isAbsolute(message.filePath) && workspaceFolder) {
const candidatePath = vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
// 检查文件是否存在
if (fs.existsSync(candidatePath)) {
fullPath = candidatePath;
} else {
// 尝试在工作区中搜索该文件
const fileName = path.basename(message.filePath);
const files = await vscode.workspace.findFiles(`**/${fileName}`, '**/node_modules/**', 1);
if (files.length > 0) {
fullPath = files[0].fsPath;
}
}
}
if (message.startLine && message.endLine) {
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc).then(editor => {
const start = new vscode.Position(message.startLine - 1, 0);
const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length);
editor.selection = new vscode.Selection(start, end);
editor.revealRange(new vscode.Range(start, end));
});
});
} else {
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc);
});
}
}
break;
case "acceptChange":
// 采纳变更
if (message.changeId) {
@ -503,57 +444,17 @@ export async function showICHelperPanel(
}
break;
case "checkInvitationCode":
// 检查邀请码验证状态
// 【已禁用】检查邀请码验证状态 - 现在所有用户都可以直接使用
{
// 先检查是否是试用用户
const { getCachedUserInfo } = require("../services/userService");
const userInfo = getCachedUserInfo();
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,
});
}
// 直接返回已验证,无需登录和邀请码
panel.webview.postMessage({
command: "invitationCodeStatus",
verified: true,
});
}
break;
case "checkWelcomeModal":
// 检查是否需要显示欢迎弹窗
{
console.log("[ICHelperPanel] 收到 checkWelcomeModal 消息");
const showWelcome = context.globalState.get("showWelcomeModal");
console.log(
"[ICHelperPanel] showWelcomeModal 标记值:",
showWelcome,
);
if (showWelcome) {
// 清除标记并显示欢迎弹窗
await context.globalState.update("showWelcomeModal", undefined);
console.log(
"[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端",
);
panel.webview.postMessage({
command: "showWelcomeModal",
});
} else {
console.log(
"[ICHelperPanel] showWelcomeModal 标记为 false不显示弹窗",
);
}
}
// 【已禁用】检查是否需要显示欢迎弹窗 - 无需登录,不显示欢迎弹窗
break;
case "checkTrialExpiration":
// 检查试用期是否过期
@ -568,37 +469,13 @@ export async function showICHelperPanel(
}
break;
case "verifyInvitationCode":
// 验证邀请码
// 【已禁用】验证邀请码 - 无需邀请码验证
{
const {
InvitationService,
} = require("../services/invitationService");
const result = await InvitationService.verifyCode(message.code);
if (result.success) {
// 验证成功,保存状态
await InvitationService.saveVerificationStatus(
context,
message.code,
);
panel.webview.postMessage({
command: "invitationCodeVerified",
success: true,
});
// 延迟显示欢迎弹窗,确保邀请码弹窗已关闭
setTimeout(() => {
panel.webview.postMessage({
command: "showNdtWelcomeModal",
});
}, 300);
} else {
// 验证失败,返回错误信息
panel.webview.postMessage({
command: "invitationCodeVerified",
success: false,
message: result.message,
});
}
// 直接返回验证成功
panel.webview.postMessage({
command: "invitationCodeVerified",
success: true,
});
}
break;
case "openICCoder":
@ -615,7 +492,7 @@ export async function showICHelperPanel(
break;
case "openUserManual":
// 打开用户手册
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
vscode.commands.executeCommand("ic-coder.openUserManual");
break;
case "openUserFeedback":
// 打开用户反馈二维码弹窗
@ -752,6 +629,23 @@ export async function showICHelperPanel(
}
}
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":
const hasWorkspace = !!(
@ -791,6 +685,21 @@ export async function showICHelperPanel(
// 退出登录(前端已有确认对话框)
vscode.commands.executeCommand("ic-coder.logout");
break;
case "saveGeneralSettings":
// 保存通用设置
context.globalState.update('generalSettings', message.settings);
// 更新运行时配置(包括清空)
setCustomConfig({ backendUrl: message.settings.backendUrl || '' });
vscode.window.showInformationMessage('设置已保存');
break;
case "loadGeneralSettings":
// 加载通用设置
const settings = context.globalState.get('generalSettings');
panel.webview.postMessage({
command: 'loadedGeneralSettings',
settings: settings
});
break;
}
},
undefined,

View File

@ -0,0 +1,181 @@
/**
* 用户手册只读预览面板
*/
import * as vscode from "vscode";
export class UserManualPanel {
public static currentPanel: UserManualPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
this._panel = panel;
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
this._update(extensionUri);
}
public static render(extensionUri: vscode.Uri) {
if (UserManualPanel.currentPanel) {
UserManualPanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
} else {
const panel = vscode.window.createWebviewPanel(
"userManual",
"IC Coder 用户手册",
vscode.ViewColumn.One,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")],
},
);
UserManualPanel.currentPanel = new UserManualPanel(panel, extensionUri);
}
}
private async _update(extensionUri: vscode.Uri) {
const manualPath = vscode.Uri.joinPath(
extensionUri,
"media",
"USER_MANUAL.md",
);
const markdown = await vscode.workspace.fs.readFile(manualPath);
const content = Buffer.from(markdown).toString("utf-8");
this._panel.webview.html = await this._getHtmlContent(
content,
extensionUri,
);
}
private async _getHtmlContent(
markdown: string,
extensionUri: vscode.Uri,
): Promise<string> {
let inCodeBlock = false;
let inTable = false;
let tableRows: string[] = [];
const lines: string[] = [];
// 先处理图片
markdown = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => {
const imgUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, "media", src),
);
return `<img src="${imgUri}" alt="${alt}">`;
});
markdown.split("\n").forEach((line) => {
// 代码块
if (line.startsWith("```")) {
if (inCodeBlock) {
lines.push("</code></pre>");
inCodeBlock = false;
} else {
lines.push("<pre><code>");
inCodeBlock = true;
}
return;
}
if (inCodeBlock) {
lines.push(line);
return;
}
// 表格
if (line.startsWith("|")) {
if (!inTable) inTable = true;
tableRows.push(line);
return;
} else if (inTable) {
// 表格结束
const headers = tableRows[0]
.split("|")
.filter((c) => c.trim())
.map((h) => `<th>${h.trim()}</th>`)
.join("");
const body = tableRows
.slice(2)
.map(
(r) =>
"<tr>" +
r
.split("|")
.filter((c) => c.trim())
.map((c) => `<td>${c.trim()}</td>`)
.join("") +
"</tr>",
)
.join("");
lines.push(
`<table><thead><tr>${headers}</tr></thead><tbody>${body}</tbody></table>`,
);
tableRows = [];
inTable = false;
}
// 其他行
if (line === "---") lines.push("<hr>");
else if (line.startsWith("#### "))
lines.push(`<h4>${line.slice(5)}</h4>`);
else if (line.startsWith("### ")) lines.push(`<h3>${line.slice(4)}</h3>`);
else if (line.startsWith("## ")) lines.push(`<h2>${line.slice(3)}</h2>`);
else if (line.startsWith("# ")) lines.push(`<h1>${line.slice(2)}</h1>`);
else if (line.startsWith("- "))
lines.push(
`<li>${line.slice(2).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")}</li>`,
);
else if (line.trim() === "") lines.push("<p></p>");
else
lines.push(
`<p>${line.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')}</p>`,
);
});
const html = lines
.join("\n")
.replace(/((?:<li>.*<\/li>\n?)+)/g, "<ul>$1</ul>");
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 40px;
line-height: 1.8;
font-size: 16px;
max-width: 1000px;
margin: 0 auto;
}
h1 { font-size: 2em; border-bottom: 3px solid #ddd; padding-bottom: 15px; margin: 30px 0 20px; }
h2 { font-size: 1.6em; margin-top: 40px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
h3 { font-size: 1.3em; margin-top: 30px; }
h4 { font-size: 1.1em; margin-top: 20px; font-weight: 600; }
p { margin: 15px 0; }
img { display: block; margin: 30px auto; max-width: 100%; border: 1px solid #ddd; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
table { border-collapse: collapse; width: 100%; margin: 25px 0; font-size: 15px; }
th, td { border: 1px solid #ddd; padding: 12px 16px; text-align: left; }
th { background: #636363; font-weight: 600; }
tr:hover { background: #636363; }
ul { margin: 15px 0; padding-left: 30px; }
li { margin: 8px 0; margin-left: 40px;}
pre { background: #2f2f2f; padding: 20px; border-radius: 6px; overflow-x: auto; margin: 20px 0; border: 1px solid #e0e0e0; }
code { font-family: 'Consolas', 'Monaco', monospace; font-size: 14px; line-height: 1.6; }
hr { border: none; border-top: 2px solid #e0e0e0; margin: 30px 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
strong { font-weight: 600; color: #e5e5e5; }
</style>
</head>
<body>${html}</body>
</html>`;
}
public dispose() {
UserManualPanel.currentPanel = undefined;
this._panel.dispose();
while (this._disposables.length) {
this._disposables.pop()?.dispose();
}
}
}

View File

@ -0,0 +1,26 @@
/**
* Code Action Provider - 为选中代码提供快捷操作
* 功能:在小灯泡菜单中显示"添加到 IC Coder 对话"选项
*/
import * as vscode from 'vscode';
export class ICCoderCodeActionProvider implements vscode.CodeActionProvider {
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range
): vscode.CodeAction[] {
const selectedText = document.getText(range);
if (!selectedText) return [];
const action = new vscode.CodeAction(
'💬 添加到 IC Coder 对话',
vscode.CodeActionKind.RefactorRewrite
);
action.command = {
command: 'ic-coder.addCodeToChat',
title: '添加到对话'
};
return [action];
}
}

View File

@ -18,6 +18,12 @@ class ChangeTrackerService {
* 开始新的变更会话
*/
startSession(sessionId: string): void {
// 如果已有 session无论状态重用并重置为 active
if (this.currentSession) {
this.currentSession.status = 'active';
return;
}
this.currentSession = {
sessionId,
startTime: Date.now(),
@ -71,7 +77,6 @@ class ChangeTrackerService {
this.notifyListeners();
return session;
}
this.currentSession = null;
return null;
}

View File

@ -30,7 +30,6 @@ import type {
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
import { updateCachedBalance } from "./creditsService";
/**
* 消息段落类型
@ -43,8 +42,7 @@ export interface MessageSegment {
toolResult?: string;
toolDescription?: string;
askId?: string;
question?: string;
options?: string[];
questions?: import("../types/api").QuestionItem[];
// 智能体相关字段
agentId?: string;
agentName?: string;
@ -97,7 +95,7 @@ export interface DialogCallbacks {
summary: string
) => void;
/** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void;
onQuestion?: (askId: string, questions: import("../types/api").QuestionItem[]) => void;
/** 实时更新段落(流式过程中) */
onSegmentUpdate?: (segments: MessageSegment[]) => void;
/** 对话完成,返回所有段落 */
@ -445,13 +443,17 @@ export class DialogSession {
const expired = isTokenExpired(session.accessToken);
if (expired === true) {
console.error("[DialogSession] token 已过期,需要重新登录");
/*
vscode.window
.showErrorMessage("登录已过期,请重新登录", "重新登录")
.then((selection) => {
if (selection === "重新登录") {
vscode.commands.executeCommand("iccoder.login");
vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
}
});
*/
throw new Error("登录已过期,请重新登录");
}
@ -645,8 +647,11 @@ export class DialogSession {
this.segments.push({
type: "question",
askId: askId,
question: question,
options: ["确认执行", "取消"],
questions: [{
question: question,
options: ["确认执行", "取消"],
multiSelect: false
}],
});
// 实时发送段落更新
@ -664,8 +669,11 @@ export class DialogSession {
await userInteractionManager.handleAskUser(
{
askId: askId,
question: question,
options: ["确认执行", "取消"],
questions: [{
question: question,
options: ["确认执行", "取消"],
multiSelect: false
}]
} as AskUserEvent,
this.taskId
);
@ -712,8 +720,11 @@ export class DialogSession {
// 注册问题到前端(类似 askUser以便用户回答时能找到
const planEvent = {
askId: askId,
question: `请确认执行计划:${data.title}`,
options: ["确认执行", "修改计划", "取消"],
questions: [{
question: `请确认执行计划:${data.title}`,
options: ["确认执行", "修改计划", "取消"],
multiSelect: false
}]
};
try {
await userInteractionManager.handleAskUser(
@ -854,13 +865,12 @@ export class DialogSession {
this.segments.push({
type: "question",
askId: data.askId,
question: data.question,
options: data.options,
questions: data.questions,
});
// 实时发送段落更新(包含问题)
callbacks.onSegmentUpdate?.(this.segments);
// 同时调用 onQuestion 用于更新状态栏等
callbacks.onQuestion?.(data.askId, data.question, data.options);
callbacks.onQuestion?.(data.askId, data.questions);
try {
await userInteractionManager.handleAskUser(data, this.taskId);
} catch (error) {
@ -890,13 +900,17 @@ export class DialogSession {
data.message.includes("LOGIN_EXPIRED") ||
data.message.includes("登录状态已过期")
) {
/*
vscode.window
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
.then((selection) => {
if (selection === "重新登录") {
vscode.commands.executeCommand("ic-coder.login");
vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
}
});
*/
// 登录过期错误已处理,不再传递给外部
return;
}
@ -1006,8 +1020,9 @@ export class DialogSession {
data.remainingCredits
);
// 更新余额缓存
updateCachedBalance(data.remainingCredits);
// updateCachedBalance(data.remainingCredits);
// 资源点余额低于阈值时弹窗提醒
/*
const LOW_CREDIT_THRESHOLD = 5;
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
vscode.window
@ -1019,13 +1034,13 @@ export class DialogSession {
)
.then((selection) => {
if (selection === "去充值") {
// 打开充值页面
vscode.env.openExternal(
vscode.Uri.parse("https://iccoder.com/recharge")
vscode.Uri.parse("https://iccoder.com/memberCenter")
);
}
});
}
*/
},
onOpen: () => {
@ -1106,7 +1121,8 @@ export class DialogSession {
async submitAnswer(
askId: string,
selected?: string[],
customInput?: string
customInput?: string,
answers?: { [questionIndex: string]: string[] }
): Promise<void> {
// 直接调用 receiveAnswer传递 taskId 作为 fallbackTaskId
// 如果 pendingQuestions 中有问题,走正常流程
@ -1115,6 +1131,7 @@ export class DialogSession {
askId,
selected,
customInput,
answers,
this.taskId
);
}

View File

@ -2,7 +2,6 @@ import * as vscode from "vscode";
import * as http from "http";
import * as path from "path";
import * as fs from "fs";
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
import { getConfig } from "../config/settings";
import { resetInvitationVerification } from "./apiClient";
@ -85,7 +84,7 @@ export class ICCoderAuthenticationProvider
const oldSession = this._sessions[0];
this._sessions = [];
await this.saveSessions();
await clearUserInfo();
// await clearUserInfo();
this._onDidChangeSessions.fire({
added: [],
removed: [oldSession],
@ -97,15 +96,15 @@ export class ICCoderAuthenticationProvider
const token = await this.login();
// 获取到 token 后立即调用用户信息接口
const userInfo = await onTokenReceived(token);
// const userInfo = await onTokenReceived(token);
// 创建会话
const session: vscode.AuthenticationSession = {
id: this.generateSessionId(),
accessToken: token,
account: {
id: userInfo?.userId || "iccoder-user",
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
id: "user",
label: "IC Coder User",
},
scopes: [...scopes],
};
@ -158,7 +157,7 @@ export class ICCoderAuthenticationProvider
await this.saveSessions();
// 3. 清除用户信息缓存
await clearUserInfo();
// await clearUserInfo();
// 4. 触发会话变化事件
this._onDidChangeSessions.fire({
@ -176,6 +175,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
*/

View File

@ -161,7 +161,16 @@ export async function startStreamDialog(
const body = JSON.stringify(request);
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
console.log('[SSE] 请求详情:', {
url: urlString,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
hasToken: !!request.token,
},
body: request
});
return new Promise((resolve, reject) => {
const options: http.RequestOptions = {

View File

@ -2,23 +2,31 @@
* 工具执行器
* 接收后端的 tool_call 事件,执行本地工具,返回结果
*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { readFileContent, readDirectory } from '../utils/readFiles';
import { createOrOverwriteFile } from '../utils/createFiles';
import { resolveWorkspaceFilePath, showFileDiff } from '../utils/fileDiff';
import { changeTracker } from './changeTracker';
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
import { analyzeVcdFile } from '../utils/vcdParser';
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
import * as vscode from "vscode";
import * as path from "path";
import * as os from "os";
import * as fs from "fs";
import { readFileContent, readDirectory } from "../utils/readFiles";
import { createOrOverwriteFile } from "../utils/createFiles";
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
import { changeTracker } from "./changeTracker";
import {
generateVCD,
checkIverilogAvailable,
generateMultiVCD,
DumpModule,
} from "../utils/iverilogRunner";
import { analyzeVcdFile } from "../utils/vcdParser";
import {
executeWaveformTrace,
WaveformTraceArgs,
} from "../utils/waveformTracer";
import {
submitToolResult,
createSuccessResult,
createBusinessErrorResult,
createSystemErrorResult
} from './apiClient';
createSystemErrorResult,
} from "./apiClient";
import type {
ToolCallRequest,
ToolName,
@ -31,8 +39,8 @@ import type {
SimulationArgs,
WaveformSummaryArgs,
KnowledgeSaveArgs,
KnowledgeLoadArgs
} from '../types/api';
KnowledgeLoadArgs,
} from "../types/api";
/**
* 工具执行器上下文
@ -51,7 +59,7 @@ export interface ToolExecutorContext {
*/
export async function executeToolCall(
request: ToolCallRequest,
context: ToolExecutorContext
context: ToolExecutorContext,
): Promise<void> {
const toolName = request.params.name as ToolName;
const args = request.params.arguments;
@ -63,37 +71,53 @@ export async function executeToolCall(
let resultText: string;
switch (toolName) {
case 'file_read':
case "file_read":
resultText = await executeFileRead(args as unknown as FileReadArgs);
break;
case 'file_write':
case "file_write":
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
break;
case 'file_delete':
case "file_delete":
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
break;
case 'file_list':
case "file_list":
resultText = await executeFileList(args as unknown as FileListArgs);
break;
case 'syntax_check':
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
case "syntax_check":
resultText = await executeSyntaxCheck(
args as unknown as SyntaxCheckArgs,
context,
);
break;
case 'iverilog':
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
case "iverilog":
resultText = await executeIverilog(
args as unknown as IverilogArgs,
context,
);
break;
case 'simulation':
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
case "simulation":
resultText = await executeSimulation(
args as unknown as SimulationArgs,
context,
);
break;
case 'waveform_summary':
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
case "waveform_summary":
resultText = await executeWaveformSummary(
args as unknown as WaveformSummaryArgs,
);
break;
case 'waveform_trace':
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
case "waveform_trace":
resultText = await executeWaveformTrace(
args as unknown as WaveformTraceArgs,
context,
);
break;
case 'knowledge_save':
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
case "knowledge_save":
resultText = await executeKnowledgeSave(
args as unknown as KnowledgeSaveArgs,
);
break;
case 'knowledge_load':
case "knowledge_load":
resultText = await executeKnowledgeLoad();
break;
default:
@ -104,10 +128,12 @@ export async function executeToolCall(
const result = createSuccessResult(callId, resultText);
await submitToolResult(result);
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error);
const errorMessage = error instanceof Error ? error.message : "未知错误";
console.error(
`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`,
error,
);
// 提交错误结果
const result = createBusinessErrorResult(callId, errorMessage);
@ -129,7 +155,7 @@ async function executeFileRead(args: FileReadArgs): 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) : '';
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : "";
await createOrOverwriteFile(args.path, args.content);
@ -137,11 +163,11 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
try {
changeTracker.trackChange(args.path, oldContent, args.content);
} catch (error) {
console.warn('[ToolExecutor] 记录文件变更失败:', error);
console.warn("[ToolExecutor] 记录文件变更失败:", error);
}
// Verilog 文件添加知识图谱提示
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
const isVerilogFile = args.path.endsWith(".v") || args.path.endsWith(".sv");
if (isVerilogFile) {
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
}
@ -151,7 +177,7 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
/**
* 执行 file_delete 工具
* 删除指定路径的文件
* 删除指定路径的文件(带用户确认)
*/
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
const filePath = args.path;
@ -159,7 +185,7 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const workspacePath = workspaceFolders[0].uri.fsPath;
@ -180,18 +206,60 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
}
// 验证文件路径在工作区内
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 oldContent = fs.readFileSync(absolutePath, "utf-8");
// 记录删除变更
const relativePath = path.relative(workspacePath, absolutePath);
changeTracker.trackChange(relativePath, oldContent, '');
changeTracker.trackChange(relativePath, oldContent, "");
// 删除文件
fs.unlinkSync(absolutePath);
// 删除文件(移到回收站)
const uri = vscode.Uri.file(absolutePath);
await vscode.workspace.fs.delete(uri, {
recursive: false, // 不是目录,设为 false
useTrash: true, // 移到回收站而非永久删除
});
// Verilog 文件添加知识图谱提示
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv");
if (isVerilogFile) {
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
}
@ -203,13 +271,13 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
* 执行 file_list 工具
*/
async function executeFileList(args: FileListArgs): Promise<string> {
const dirPath = args.path || '.';
const dirPath = args.path || ".";
const extensions = args.extension ? [args.extension] : undefined;
const files = await readDirectory(dirPath, extensions);
const fileList = files.map(f => f.path).join('\n');
const fileList = files.map((f) => f.path).join("\n");
return fileList || '(目录为空)';
return fileList || "(目录为空)";
}
/**
@ -218,12 +286,12 @@ async function executeFileList(args: FileListArgs): Promise<string> {
*/
async function executeSyntaxCheck(
args: SyntaxCheckArgs,
context: ToolExecutorContext
context: ToolExecutorContext,
): Promise<string> {
// 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
if (!iverilogCheck.available) {
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
}
// 创建临时文件
@ -232,33 +300,33 @@ async function executeSyntaxCheck(
try {
// 写入代码到临时文件
fs.writeFileSync(tempFile, args.code, 'utf-8');
fs.writeFileSync(tempFile, args.code, "utf-8");
// 调用 iverilog 进行语法检查
const { spawn } = require('child_process');
const { spawn } = require("child_process");
const iverilogPath = getIverilogPath(context.extensionPath);
return new Promise((resolve, reject) => {
const child = spawn(iverilogPath, ['-t', 'null', tempFile], {
const child = spawn(iverilogPath, ["-t", "null", tempFile], {
cwd: tempDir,
env: {
...process.env,
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
}
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
},
});
let stdout = '';
let stderr = '';
let stdout = "";
let stderr = "";
child.stdout.on('data', (data: Buffer) => {
child.stdout.on("data", (data: Buffer) => {
stdout += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
child.stderr.on("data", (data: Buffer) => {
stderr += data.toString();
});
child.on('close', (code: number) => {
child.on("close", (code: number) => {
// 清理临时文件
try {
fs.unlinkSync(tempFile);
@ -267,13 +335,13 @@ async function executeSyntaxCheck(
}
if (code === 0) {
resolve('语法检查通过,无错误。');
resolve("语法检查通过,无错误。");
} else {
resolve(`语法检查发现错误:\n${stderr || stdout}`);
}
});
child.on('error', (error: Error) => {
child.on("error", (error: Error) => {
try {
fs.unlinkSync(tempFile);
} catch (e) {
@ -282,7 +350,6 @@ async function executeSyntaxCheck(
reject(error);
});
});
} catch (error) {
// 确保清理临时文件
try {
@ -300,18 +367,18 @@ async function executeSyntaxCheck(
*/
async function executeIverilog(
args: IverilogArgs,
context: ToolExecutorContext
context: ToolExecutorContext,
): Promise<string> {
// 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
if (!iverilogCheck.available) {
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
}
// 获取工作目录
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('没有打开的工作区');
throw new Error("没有打开的工作区");
}
const projectPath = workspaceFolders[0].uri.fsPath;
const workDir = args.workDir
@ -320,32 +387,32 @@ async function executeIverilog(
// 解析参数
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) => {
const child = spawn(iverilogPath, cmdArgs, {
cwd: workDir,
env: {
...process.env,
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
}
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
},
});
let stdout = '';
let stderr = '';
let stdout = "";
let stderr = "";
child.stdout.on('data', (data: Buffer) => {
child.stdout.on("data", (data: Buffer) => {
stdout += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
child.stderr.on("data", (data: Buffer) => {
stderr += data.toString();
});
child.on('close', (code: number) => {
const output = stderr || stdout || '(无输出)';
child.on("close", (code: number) => {
const output = stderr || stdout || "(无输出)";
if (code === 0) {
resolve(`执行成功\n${output}`);
} else {
@ -353,7 +420,7 @@ async function executeIverilog(
}
});
child.on('error', (error: Error) => {
child.on("error", (error: Error) => {
reject(error);
});
});
@ -364,12 +431,12 @@ async function executeIverilog(
*/
async function executeSimulation(
args: SimulationArgs,
context: ToolExecutorContext
context: ToolExecutorContext,
): Promise<string> {
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const projectPath = workspaceFolders[0].uri.fsPath;
@ -377,21 +444,24 @@ async function executeSimulation(
// 检查是否有 dumpModules 参数(多 VCD 模式)
if (args.dumpModules) {
const modules = parseDumpModules(args.dumpModules);
const vcdDir = args.vcdDir || 'vcd';
const vcdDir = args.vcdDir || "vcd";
const result = await generateMultiVCD(
projectPath,
context.extensionPath,
args.tbPath,
modules,
vcdDir
vcdDir,
);
if (result.success) {
const vcdList = result.vcdFiles
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`)
.join('\n');
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`;
.map(
(f) =>
`- ${f.moduleName}: ${f.success ? f.vcdPath : "失败 - " + f.error}`,
)
.join("\n");
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? "\n\n仿真输出:" + result.stdout : ""}`;
} else {
throw new Error(result.message);
}
@ -420,8 +490,8 @@ async function executeSimulation(
* 格式name:path,name:path
*/
function parseDumpModules(dumpModules: string): DumpModule[] {
return dumpModules.split(',').map(item => {
const [name, modulePath] = item.trim().split(':');
return dumpModules.split(",").map((item) => {
const [name, modulePath] = item.trim().split(":");
return { name: name.trim(), path: modulePath.trim() };
});
}
@ -430,13 +500,15 @@ function parseDumpModules(dumpModules: string): DumpModule[] {
* 执行 waveform_summary 工具
* 解析 VCD 文件并返回波形摘要
*/
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
async function executeWaveformSummary(
args: WaveformSummaryArgs,
): Promise<string> {
const { vcdPath, signals, checkpoints } = args;
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const workspacePath = workspaceFolders[0].uri.fsPath;
@ -467,17 +539,20 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolder) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, ".iccoder");
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, "knowledge.json");
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
await vscode.workspace.fs.createDirectory(iccoderDirUri);
// 写入知识图谱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`;
}
@ -489,20 +564,33 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
async function executeKnowledgeLoad(): Promise<string> {
const workspaceFolder = getWorkspaceFolder();
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 {
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;
} catch (error) {
// 文件不存在:返回空图谱
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
if (
error instanceof vscode.FileSystemError &&
error.code === "FileNotFound"
) {
// 与后端 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;
}
@ -515,7 +603,9 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
}
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];
}
@ -524,22 +614,24 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
*/
function getIverilogPath(extensionPath: string): string {
const platform = process.platform;
if (platform === 'win32') {
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe');
if (platform === "win32") {
return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
} 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 workspacePath = workspaceFolders?.[0]?.uri.fsPath || '';
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || "";
return {
extensionPath,
workspacePath
workspacePath,
};
}

View File

@ -4,7 +4,7 @@
*/
import * as vscode from 'vscode';
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 {
askId: string;
taskId: string;
question: string;
options: string[];
questions: QuestionItem[];
resolve: (answer: string) => void;
reject: (error: Error) => void;
}
@ -45,9 +44,9 @@ export class UserInteractionManager {
* @param taskId 当前任务ID
*/
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 统一处理
// 这里不再单独发送 showQuestion 命令,避免重复显示
@ -57,8 +56,7 @@ export class UserInteractionManager {
this.pendingQuestions.set(askId, {
askId,
taskId,
question,
options,
questions,
resolve: (answer: string) => {
this.submitUserAnswer(askId, taskId, answer)
.then(() => resolve())
@ -80,24 +78,38 @@ export class UserInteractionManager {
/**
* 处理用户提交的回答(从 WebView 调用)
* @param askId 问题ID
* @param selected 选中的选项
* @param customInput 自定义输入
* @param selected 选中的选项(旧格式)
* @param customInput 自定义输入(旧格式)
* @param answers 新格式:按问题索引的答案
* @param fallbackTaskId 当问题不存在时使用的 taskId用于直接发送到后端
*/
async receiveAnswer(
askId: string,
selected?: string[],
customInput?: string,
answers?: { [questionIndex: string]: string[] },
fallbackTaskId?: string
): Promise<void> {
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 (fallbackTaskId) {
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
await this.submitUserAnswer(askId, fallbackTaskId, answer);
await this.submitUserAnswer(askId, fallbackTaskId, answer, answers);
} else {
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
}
@ -119,7 +131,8 @@ export class UserInteractionManager {
private async submitUserAnswer(
askId: string,
taskId: string,
answer: string
answer: string,
answers?: { [questionIndex: string]: string[] }
): Promise<void> {
// 检查是否是工具确认类型的问题
if (askId.startsWith('tool_confirm_')) {
@ -148,7 +161,8 @@ export class UserInteractionManager {
const request: AnswerRequest = {
askId,
taskId,
customInput: answer
answers: answers,
customInput: answers ? undefined : answer
};
try {

View File

@ -162,10 +162,14 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
console.log('[UserService] 从 getInfo 接口获取到 isPluginTrial: true');
}
// 获取试用到期时间
if (response.enterpriseTrialExpires) {
// 获取试用到期时间null 表示长期有效)
if (response.enterpriseTrialExpires !== undefined) {
userInfo.pluginTrialExpiresAt = response.enterpriseTrialExpires;
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
if (response.enterpriseTrialExpires === null) {
console.log('[UserService] 试用长期有效');
} else {
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
}
}
return userInfo;
@ -331,33 +335,21 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
// 保存到持久化存储
await saveUserInfo(userInfo);
// 判断是否是插件试用用户
console.log('[UserService] 检查用户类型isPluginTrial:', userInfo.isPluginTrial);
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
if (userInfo.isPluginTrial === true) {
// 插件试用用户:标记需要显示欢迎弹窗
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 {
// 正式用户:显示邀请码弹窗(现有逻辑)
console.log('[UserService] 正式用户登录,将在面板中检查邀请码');
}
// 【已禁用】试用用户和欢迎弹窗逻辑 - 无需登录
// if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
// const now = Date.now();
// const isExpired = now >= userInfo.pluginTrialExpiresAt;
// if (isExpired) {
// console.log('[UserService] 试用已过期,将显示邀请码弹窗');
// } else {
// const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
// if (!hasWelcomed && extensionContext) {
// await extensionContext.globalState.update('showWelcomeModal', true);
// await extensionContext.globalState.update('pluginTrialWelcomed', true);
// console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
// }
// }
// }
return userInfo;
} catch (error) {

View File

@ -194,11 +194,17 @@ export interface PlanSummaryUpdateEvent {
timestamp: number;
}
/** 单个问题项 */
export interface QuestionItem {
question: string;
options: string[];
multiSelect?: boolean;
}
/** ask_user 事件数据 */
export interface AskUserEvent {
askId: string;
question: string;
options: string[];
questions: QuestionItem[];
}
/** complete 事件数据 */
@ -351,10 +357,12 @@ export interface AnswerRequest {
askId: string;
/** 任务ID */
taskId: string;
/** 选中的选项列表 */
/** 选中的选项列表(旧格式,兼容) */
selected?: string[];
/** 自定义输入内容 */
/** 自定义输入内容(旧格式,兼容) */
customInput?: string;
/** 新格式:按问题索引的答案 */
answers?: { [questionIndex: string]: string[] };
}
/** 用户回答响应 */

View File

@ -7,7 +7,7 @@ import { promisify } from "util";
function execCommand(
command: string,
args: string[],
options: { cwd: string; env?: any }
options: { cwd: string; env?: any },
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
// 在 Windows 上,如果路径包含空格,不使用 shell直接用 spawn
@ -23,25 +23,25 @@ function execCommand(
let stderr = "";
// 在 Windows 上使用 GBK 编码解码输出
const encoding = process.platform === 'win32' ? 'gbk' : 'utf8';
const encoding = process.platform === "win32" ? "gbk" : "utf8";
child.stdout.on("data", (data) => {
try {
// 尝试使用 iconv-lite 解码(如果可用)
const iconv = require('iconv-lite');
const iconv = require("iconv-lite");
stdout += iconv.decode(data, encoding);
} catch {
// 如果 iconv-lite 不可用,使用默认解码
stdout += data.toString('utf8');
stdout += data.toString("utf8");
}
});
child.stderr.on("data", (data) => {
try {
const iconv = require('iconv-lite');
const iconv = require("iconv-lite");
stderr += iconv.decode(data, encoding);
} catch {
stderr += data.toString('utf8');
stderr += data.toString("utf8");
}
});
@ -93,7 +93,7 @@ export interface VCDGenerationResult {
* 检查项目中的 Verilog 文件完整性
*/
export async function checkVerilogProject(
projectPath: string
projectPath: string,
): Promise<VerilogProjectCheck> {
const result: VerilogProjectCheck = {
isComplete: false,
@ -164,7 +164,7 @@ export async function checkVerilogProject(
return result;
} catch (error) {
result.errors.push(
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`,
);
return result;
}
@ -209,12 +209,30 @@ async function getIverilogPath(extensionPath: string): Promise<string> {
let iverilogBin = "";
if (platform === "win32") {
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
iverilogBin = path.join(
extensionPath,
"tools",
"iverilog",
"bin",
"iverilog.exe",
);
} else if (platform === "darwin") {
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
iverilogBin = path.join(
extensionPath,
"tools",
"iverilog",
"bin",
"iverilog",
);
} else {
// Linux
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
iverilogBin = path.join(
extensionPath,
"tools",
"iverilog",
"bin",
"iverilog",
);
}
// 如果插件包中没有,尝试使用系统安装的 iverilog
@ -258,7 +276,7 @@ async function getVvpPath(extensionPath: string): Promise<string> {
*/
export async function generateVCD(
projectPath: string,
extensionPath: string
extensionPath: string,
): Promise<VCDGenerationResult> {
try {
// 1. 检查项目完整性
@ -302,12 +320,27 @@ export async function generateVCD(
} catch (error: any) {
return {
success: false,
message: `iverilog 编译失败:\n${error.message}`,
message: `IC Coder编译器编译失败:\n${error.message}`,
stderr: error.stderr,
stdout: error.stdout,
};
}
// 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误)
try {
const fs = require("fs");
const vvpContent = fs.readFileSync(outputFile, "utf8");
const lines = vvpContent.split("\n");
if (lines.length > 0 && lines[0].startsWith("#!")) {
const cleanedContent = lines.slice(1).join("\n");
fs.writeFileSync(outputFile, cleanedContent, "utf8");
console.log("已删除 .vvp 文件的 shebang 行");
}
} catch (error) {
console.warn("删除 shebang 失败,继续执行:", error);
}
// 7. 执行仿真生成 VCD
const simArgs = [outputFile];
console.log("执行仿真命令:", vvpPath, simArgs.join(" "));
@ -331,13 +364,17 @@ export async function generateVCD(
const projectUri = vscode.Uri.file(projectPath);
const entries = await vscode.workspace.fs.readDirectory(projectUri);
const vcdFiles = entries
.filter(([fileName, fileType]) => fileType === vscode.FileType.File && fileName.endsWith('.vcd'))
.filter(
([fileName, fileType]) =>
fileType === vscode.FileType.File && fileName.endsWith(".vcd"),
)
.map(([fileName]) => fileName);
if (vcdFiles.length === 0) {
return {
success: false,
message: "VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
message:
"VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
stdout: simResult.stdout,
};
}
@ -373,7 +410,7 @@ export async function generateVCD(
* 检查 iverilog 是否可用
*/
export async function checkIverilogAvailable(
extensionPath: string
extensionPath: string,
): Promise<{ available: boolean; version?: string; message: string }> {
try {
const iverilogPath = await getIverilogPath(extensionPath);
@ -385,7 +422,7 @@ export async function checkIverilogAvailable(
} catch (error) {
return {
available: false,
message: `iverilog 不可用。未找到文件: ${iverilogPath}`,
message: `IC Coder编译器不可用。未找到文件: ${iverilogPath}`,
};
}
@ -404,12 +441,12 @@ export async function checkIverilogAvailable(
return {
available: true,
version: version,
message: `iverilog 可用: ${version}`,
message: `IC Coder编译器可用: ${version}`,
};
} catch (error: any) {
return {
available: false,
message: `iverilog 执行失败: ${error.message}\n${error.stderr || ""}`,
message: `IC Coder编译器执行失败: ${error.message}\n${error.stderr || ""}`,
};
}
}
@ -418,8 +455,8 @@ export async function checkIverilogAvailable(
* 要 dump 的模块定义
*/
export interface DumpModule {
name: string; // 模块名(用于 VCD 文件名和宏名)
path: string; // 实例路径(如 dut.u_tx
name: string; // 模块名(用于 VCD 文件名和宏名)
path: string; // 实例路径(如 dut.u_tx
}
/**
@ -444,10 +481,11 @@ export interface MultiVCDResult {
function injectConditionalDump(
tbContent: string,
dumpModules: DumpModule[],
vcdDir: string
vcdDir: string,
): string {
// 匹配 $dumpfile 和 $dumpvars 语句(可能跨多行)
const dumpPattern = /(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
const dumpPattern =
/(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
// 生成条件编译代码
const conditionalCode = generateConditionalDumpCode(dumpModules, vcdDir);
@ -469,7 +507,7 @@ function injectConditionalDump(
*/
function generateConditionalDumpCode(
dumpModules: DumpModule[],
vcdDir: string
vcdDir: string,
): string {
if (dumpModules.length === 0) {
return '$dumpfile("output.vcd");\n $dumpvars(0, dut);';
@ -480,7 +518,7 @@ function generateConditionalDumpCode(
dumpModules.forEach((module, index) => {
const macroName = `DUMP_${module.name.toUpperCase()}`;
const vcdPath = `${vcdDir}/${module.name}.vcd`;
const directive = index === 0 ? '`ifdef' : '`elsif';
const directive = index === 0 ? "`ifdef" : "`elsif";
lines.push(`${directive} ${macroName}`);
lines.push(` $dumpfile("${vcdPath}");`);
@ -488,12 +526,12 @@ function generateConditionalDumpCode(
});
// 添加默认分支(使用第一个模块)
lines.push('`else');
lines.push("`else");
lines.push(` $dumpfile("${vcdDir}/${dumpModules[0].name}.vcd");`);
lines.push(` $dumpvars(1, ${dumpModules[0].path});`);
lines.push('`endif');
lines.push("`endif");
return lines.join('\n');
return lines.join("\n");
}
/**
@ -504,10 +542,10 @@ export async function generateMultiVCD(
extensionPath: string,
tbPath: string,
dumpModules: DumpModule[],
vcdDir: string = 'vcd'
vcdDir: string = "vcd",
): Promise<MultiVCDResult> {
const results: MultiVCDResult['vcdFiles'] = [];
let allStdout = '';
const results: MultiVCDResult["vcdFiles"] = [];
let allStdout = "";
try {
// 1. 创建 vcd 目录
@ -520,16 +558,21 @@ export async function generateMultiVCD(
}
// 2. 读取原始 testbench
const tbFullPath = path.isAbsolute(tbPath) ? tbPath : path.join(projectPath, tbPath);
const tbFullPath = path.isAbsolute(tbPath)
? tbPath
: path.join(projectPath, tbPath);
const tbUri = vscode.Uri.file(tbFullPath);
const tbBytes = await vscode.workspace.fs.readFile(tbUri);
const originalTb = Buffer.from(tbBytes).toString('utf-8');
const originalTb = Buffer.from(tbBytes).toString("utf-8");
// 3. 注入条件编译代码
const modifiedTb = injectConditionalDump(originalTb, dumpModules, vcdDir);
await vscode.workspace.fs.writeFile(tbUri, Buffer.from(modifiedTb, 'utf-8'));
await vscode.workspace.fs.writeFile(
tbUri,
Buffer.from(modifiedTb, "utf-8"),
);
console.log('[generateMultiVCD] Testbench 已修改,开始多次仿真...');
console.log("[generateMultiVCD] Testbench 已修改,开始多次仿真...");
// 4. 获取工具路径
const iverilogPath = await getIverilogPath(extensionPath);
@ -554,27 +597,34 @@ export async function generateMultiVCD(
// 编译(带宏定义)
const compileArgs = [
`-D${macroName}`,
"-o", outputFile,
...projectCheck.allVerilogFiles
"-o",
outputFile,
...projectCheck.allVerilogFiles,
];
await execCommand(iverilogPath, compileArgs, { cwd: projectPath, env });
// 仿真
const simResult = await execCommand(vvpPath, [outputFile], { cwd: projectPath, env });
const simResult = await execCommand(vvpPath, [outputFile], {
cwd: projectPath,
env,
});
allStdout += `\n[${module.name}] ${simResult.stdout}`;
results.push({
moduleName: module.name,
vcdPath: vcdPath,
success: true
success: true,
});
} catch (error: any) {
console.error(`[generateMultiVCD] 模块 ${module.name} 仿真失败:`, error.message);
console.error(
`[generateMultiVCD] 模块 ${module.name} 仿真失败:`,
error.message,
);
results.push({
moduleName: module.name,
vcdPath: vcdPath,
success: false,
error: error.message
error: error.message,
});
// 继续执行其他模块
}
@ -587,19 +637,18 @@ export async function generateMultiVCD(
// 忽略
}
const successCount = results.filter(r => r.success).length;
const successCount = results.filter((r) => r.success).length;
return {
success: successCount > 0,
vcdFiles: results,
message: `生成完成:${successCount}/${dumpModules.length} 个 VCD 文件成功`,
stdout: allStdout
stdout: allStdout,
};
} catch (error) {
return {
success: false,
vcdFiles: results,
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : '未知错误'}`
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
};
}
}

View File

@ -19,10 +19,6 @@ import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient";
import { isTokenExpired } from "./jwtUtils";
import {
checkBalanceBeforeSend,
fetchBalance,
} from "../services/creditsService";
import { optimizePrompt } from "../services/promptOptimizeService";
import { NotificationService } from "../services/notificationService";
import { TrialExpirationService } from "../services/trialExpirationService";
@ -41,7 +37,11 @@ let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null;
async function trackFileChange(filePath: string, oldContent: string, newContent: string): Promise<void> {
async function trackFileChange(
filePath: string,
oldContent: string,
newContent: string,
): Promise<void> {
try {
changeTracker.trackChange(filePath, oldContent, newContent);
} catch (error) {
@ -58,17 +58,19 @@ export async function handleUserMessage(
extensionPath?: string,
mode?: RunMode,
serviceTier?: ServiceTier, // 服务等级参数
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
contextItems?: Array<{ id: number; type: string; path: string }>, // 上下文项参数
) {
console.log("收到用户消息:", text);
// 检查 token 是否过期
// 【已禁用】检查 token 是否过期 - 无需登录
const context = (panel as any).__context;
if (context) {
if (false && context) {
// 从 session 中获取 token
let token: string | undefined;
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
token = session?.accessToken;
} catch (error) {
console.warn("[MessageHandler] 获取 session 失败:", error);
@ -78,21 +80,23 @@ export async function handleUserMessage(
console.warn("[MessageHandler] 未登录,阻止发送");
// 保存待发送的消息
await context.globalState.update('pendingMessage', {
await context.globalState.update("pendingMessage", {
text,
mode,
serviceTier,
timestamp: Date.now()
timestamp: Date.now(),
});
// 显示弹窗提示
const action = await vscode.window.showWarningMessage(
'请先登录后再发送消息',
'立即登录'
"请先登录后再发送消息",
"立即登录",
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
if (action === "立即登录") {
vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
}
// 恢复输入状态
@ -104,30 +108,34 @@ export async function handleUserMessage(
return;
}
if (isTokenExpired(token)) {
if (token && isTokenExpired(token as string)) {
console.warn("[MessageHandler] Token 已过期,阻止发送");
// 保存待发送的消息
await context.globalState.update('pendingMessage', {
await context.globalState.update("pendingMessage", {
text,
mode,
serviceTier,
timestamp: Date.now()
timestamp: Date.now(),
});
// 清除过期的 session
await context.globalState.update('icCoderSessions', []);
await context.globalState.update('icCoderUserInfo', undefined);
await context.globalState.update("icCoderSessions", []);
await context.globalState.update("icCoderUserInfo", undefined);
// 显示弹窗提示
/*
const action = await vscode.window.showWarningMessage(
'登录已过期,请重新登录',
'立即登录'
"登录已过期,请重新登录",
"立即登录",
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
if (action === "立即登录") {
vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
}
*/
// 恢复输入状态
panel.webview.postMessage({
@ -179,29 +187,6 @@ export async function handleUserMessage(
return;
}
// 发送前检测余额
const balanceCheck = await checkBalanceBeforeSend();
if (!balanceCheck.allowed) {
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
// 显示错误提示
const selection = await vscode.window.showWarningMessage(
balanceCheck.message || "资源点余额不足",
"去充值"
);
if (selection === "去充值") {
vscode.env.openExternal(
vscode.Uri.parse("https://iccoder.com/memberCenter")
);
}
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
// 尝试使用后端服务
if (useBackendService && extensionPath) {
try {
@ -212,14 +197,14 @@ export async function handleUserMessage(
mode,
undefined,
serviceTier,
contextItems
contextItems,
);
return;
} catch (error) {
console.error("后端服务不可用:", error);
console.error("处理用户消息失败:", error);
panel.webview.postMessage({
command: "updateStatus",
text: "后端服务不可用",
text: "处理用户消息失败,请稍后重试",
type: "error",
});
// 恢复输入状态
@ -250,7 +235,7 @@ async function handleUserMessageWithBackend(
mode?: RunMode,
reuseTaskId?: string, // 可选,复用现有 taskId用于 Plan 模式确认后继续执行)
serviceTier?: ServiceTier, // 服务等级参数
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
contextItems?: Array<{ id: number; type: string; path: string }>, // 上下文项参数
): Promise<void> {
const historyManager = ChatHistoryManager.getInstance();
@ -258,7 +243,7 @@ async function handleUserMessageWithBackend(
let enhancedText = text;
if (contextItems && contextItems.length > 0) {
console.log("[MessageHandler] 处理上下文项:", contextItems.length);
const paths = contextItems.map(item => item.path).join('\n');
const paths = contextItems.map((item) => item.path).join("\n");
enhancedText = `${paths}\n\n${text}`;
}
@ -269,7 +254,7 @@ async function handleUserMessageWithBackend(
// 创建会话dialogManager 会自动处理旧会话的中止)
currentSession = dialogManager.createSession(
extensionPath,
taskIdToUse || undefined
taskIdToUse || undefined,
);
// 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId();
@ -277,7 +262,7 @@ async function handleUserMessageWithBackend(
"[MessageHandler] 创建会话: taskId=",
lastTaskId,
"来源=",
taskIdToUse ? "historyManager" : "新生成"
taskIdToUse ? "historyManager" : "新生成",
);
// 显示状态栏
@ -296,10 +281,18 @@ async function handleUserMessageWithBackend(
},
onSegmentUpdate: (segments) => {
// 过滤掉包含 [调用工具:xxx] 的段落
const filteredSegments = segments.filter(seg => {
if (seg.type === 'text' && typeof seg.content === 'string') {
return !/\[调用工具:.+?\]/.test(seg.content);
}
return true;
});
// 实时发送段落更新,按后端返回顺序展示
panel.webview.postMessage({
command: "updateSegments",
segments: segments,
segments: filteredSegments,
});
},
@ -320,7 +313,10 @@ async function handleUserMessageWithBackend(
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
},
onQuestion: (askId, question, options) => {
onQuestion: (
askId: string,
questions: import("../types/api").QuestionItem[],
) => {
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
panel.webview.postMessage({
command: "updateStatus",
@ -347,17 +343,6 @@ async function handleUserMessageWithBackend(
console.error("[MessageHandler] 保存AI响应历史失败:", error);
}
// 对话完成后重新获取余额(因为已经消耗了 Credits
try {
console.log("[MessageHandler] 对话完成,重新获取余额...");
const newBalance = await fetchBalance();
if (newBalance !== null) {
console.log("[MessageHandler] 余额已更新:", newBalance);
}
} catch (error) {
console.error("[MessageHandler] 获取余额失败:", error);
}
// 尝试更新面板(如果面板已关闭,这些操作会失败,但不影响数据保存)
try {
// 隐藏状态栏
@ -365,29 +350,31 @@ async function handleUserMessageWithBackend(
command: "hideStatus",
});
// 最后一次发送完整的段落
const result = await panel.webview.postMessage({
// 发送完成标记(不再重复发送 segments避免内容重复显示
panel.webview.postMessage({
command: "updateSegments",
segments: segments,
segments: [],
isComplete: true,
});
console.log("[MessageHandler] postMessage 返回值:", result);
// 发送系统通知 - AI 响应完成
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - AI 响应完成',
'您的问题已得到回复,点击查看详情',
"IC Coder - AI 响应完成",
"您的问题已得到回复,点击查看详情",
() => {
// 点击通知时聚焦到面板
panel.reveal();
}
},
);
// 发送代码变更到前端
sendChangesToWebview(panel);
} catch (error) {
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
console.warn(
"[MessageHandler] 更新面板失败(面板可能已关闭):",
error,
);
}
resolve();
@ -399,7 +386,7 @@ async function handleUserMessageWithBackend(
});
panel.webview.postMessage({
command: "receiveMessage",
text: `错误: ${message}`,
text: `错误: ${message}`,
});
// 恢复输入状态
panel.webview.postMessage({
@ -455,7 +442,7 @@ async function handleUserMessageWithBackend(
},
},
mode,
serviceTier // 传递服务等级
serviceTier, // 传递服务等级
);
});
}
@ -466,10 +453,11 @@ async function handleUserMessageWithBackend(
export async function handleUserAnswer(
askId: string,
selected?: string[],
customInput?: string
customInput?: string,
answers?: { [questionIndex: string]: string[] },
): Promise<void> {
if (currentSession) {
await currentSession.submitAnswer(askId, selected, customInput);
await currentSession.submitAnswer(askId, selected, customInput, answers);
}
}
@ -536,7 +524,7 @@ export async function handlePlanAction(
action: string,
planTitle: string,
extensionPath: string,
serviceTier?: ServiceTier
serviceTier?: ServiceTier,
): Promise<void> {
console.log(
"[handlePlanAction] action:",
@ -544,7 +532,7 @@ export async function handlePlanAction(
"planTitle:",
planTitle,
"serviceTier:",
serviceTier
serviceTier,
);
switch (action) {
@ -560,7 +548,7 @@ export async function handlePlanAction(
`请按照刚才的计划执行:${planTitle}`,
extensionPath,
"agent",
serviceTier
serviceTier,
);
break;
@ -577,7 +565,7 @@ export async function handlePlanAction(
`请根据以下建议修改计划:${modification}`,
extensionPath,
"plan",
serviceTier
serviceTier,
);
}
break;
@ -632,7 +620,7 @@ function parseFileOperation(text: string): {
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts优先匹配避免被修改匹配
const renameMatch = lowerText.match(
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/,
);
if (renameMatch) {
const oldPath = renameMatch[1].trim();
@ -649,7 +637,7 @@ function parseFileOperation(text: string): {
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
const replaceMatch1 = lowerText.match(
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/,
);
if (replaceMatch1) {
const filePath = replaceMatch1[1].trim();
@ -665,7 +653,7 @@ function parseFileOperation(text: string): {
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
const replaceMatch2 = lowerText.match(
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/,
);
if (replaceMatch2) {
const filePath = replaceMatch2[1].trim();
@ -714,7 +702,7 @@ async function handleFileOperation(
newPath?: string;
searchText?: string;
replaceText?: string;
}
},
) {
const historyManager = ChatHistoryManager.getInstance();
@ -730,7 +718,7 @@ async function handleFileOperation(
text: responseText,
});
vscode.window.showInformationMessage(
`文件创建成功: ${operation.filePath}`
`文件创建成功: ${operation.filePath}`,
);
await historyManager.addAiMessage(responseText);
break;
@ -743,7 +731,7 @@ async function handleFileOperation(
text: responseText,
});
vscode.window.showInformationMessage(
`文件删除成功: ${operation.filePath}`
`文件删除成功: ${operation.filePath}`,
);
await historyManager.addAiMessage(responseText);
break;
@ -779,7 +767,7 @@ async function handleFileOperation(
text: responseText,
});
vscode.window.showInformationMessage(
`文件重命名成功: ${operation.filePath}${operation.newPath}`
`文件重命名成功: ${operation.filePath}${operation.newPath}`,
);
await historyManager.addAiMessage(responseText);
break;
@ -788,21 +776,29 @@ async function handleFileOperation(
if (!operation.searchText || !operation.replaceText) {
throw new Error("缺少替换内容");
}
const oldContentBeforeReplace = await readFileContent(operation.filePath);
const oldContentBeforeReplace = await readFileContent(
operation.filePath,
);
await replaceFile(
operation.filePath,
operation.searchText,
operation.replaceText
operation.replaceText,
);
const newContentAfterReplace = await readFileContent(
operation.filePath,
);
await trackFileChange(
operation.filePath,
oldContentBeforeReplace,
newContentAfterReplace,
);
const newContentAfterReplace = await readFileContent(operation.filePath);
await trackFileChange(operation.filePath, oldContentBeforeReplace, newContentAfterReplace);
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
panel.webview.postMessage({
command: "receiveMessage",
text: responseText,
});
vscode.window.showInformationMessage(
`文件内容替换成功: ${operation.filePath}`
`文件内容替换成功: ${operation.filePath}`,
);
await historyManager.addAiMessage(responseText);
break;
@ -862,7 +858,7 @@ function getDefaultContent(filePath: string): string {
*/
export async function handleReadFile(
panel: vscode.WebviewPanel,
filePath: string
filePath: string,
) {
try {
const content = await readFileContent(filePath);
@ -886,7 +882,7 @@ export async function handleCreateFile(
panel: vscode.WebviewPanel,
filePath: string,
content: string,
overwrite: boolean = false //是否覆盖
overwrite: boolean = false, //是否覆盖
) {
try {
if (overwrite) {
@ -905,11 +901,14 @@ export async function handleCreateFile(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - 文件创建',
"IC Coder - 文件创建",
`文件已创建: ${path.basename(filePath)}`,
() => {
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
}
vscode.commands.executeCommand(
"vscode.open",
vscode.Uri.file(filePath),
);
},
);
} catch (error) {
panel.webview.postMessage({
@ -917,7 +916,7 @@ export async function handleCreateFile(
error: error instanceof Error ? error.message : "创建文件失败",
});
vscode.window.showErrorMessage(
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
);
}
}
@ -928,7 +927,7 @@ export async function handleCreateFile(
export async function handleUpdateFile(
panel: vscode.WebviewPanel,
filePath: string,
content: string
content: string,
) {
try {
const oldContent = await readFileContent(filePath);
@ -944,8 +943,8 @@ export async function handleUpdateFile(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.info(
'IC Coder - 文件更新',
`文件已更新: ${path.basename(filePath)}`
"IC Coder - 文件更新",
`文件已更新: ${path.basename(filePath)}`,
);
} catch (error) {
panel.webview.postMessage({
@ -953,7 +952,7 @@ export async function handleUpdateFile(
error: error instanceof Error ? error.message : "更新文件失败",
});
vscode.window.showErrorMessage(
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
);
}
}
@ -964,7 +963,7 @@ export async function handleUpdateFile(
export async function handleRenameFile(
panel: vscode.WebviewPanel,
oldPath: string,
newPath: string
newPath: string,
) {
try {
await renameFile(oldPath, newPath);
@ -975,7 +974,7 @@ export async function handleRenameFile(
message: "文件重命名成功",
});
vscode.window.showInformationMessage(
`文件重命名成功: ${oldPath}${newPath}`
`文件重命名成功: ${oldPath}${newPath}`,
);
} catch (error) {
panel.webview.postMessage({
@ -983,7 +982,7 @@ export async function handleRenameFile(
error: error instanceof Error ? error.message : "重命名文件失败",
});
vscode.window.showErrorMessage(
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
);
}
}
@ -995,7 +994,7 @@ export async function handleReplaceInFile(
panel: vscode.WebviewPanel,
filePath: string,
searchText: string,
replaceText: string
replaceText: string,
) {
try {
const oldContent = await readFileContent(filePath);
@ -1014,7 +1013,7 @@ export async function handleReplaceInFile(
error: error instanceof Error ? error.message : "替换文件内容失败",
});
vscode.window.showErrorMessage(
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`,
);
}
}
@ -1059,7 +1058,7 @@ function isVCDGenerationCommand(text: string): boolean {
*/
async function handleVCDGeneration(
panel: vscode.WebviewPanel,
extensionPath: string
extensionPath: string,
) {
try {
// 获取当前工作区路径
@ -1086,7 +1085,7 @@ async function handleVCDGeneration(
if (!iverilogCheck.available) {
panel.webview.postMessage({
command: "receiveMessage",
text: `${iverilogCheck.message}\n\n请参考插件文档安装 iverilog 工具`,
text: `${iverilogCheck.message}`,
});
vscode.window.showErrorMessage(iverilogCheck.message);
return;
@ -1161,12 +1160,15 @@ async function handleVCDGeneration(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - 仿真完成',
"IC Coder - 仿真完成",
`VCD 文件已生成: ${fileName}`,
() => {
// 点击通知时打开 VCD 查看器
vscode.commands.executeCommand('ic-coder.openVCDViewer', result.vcdFilePath);
}
vscode.commands.executeCommand(
"ic-coder.openVCDViewer",
result.vcdFilePath,
);
},
);
} else {
panel.webview.postMessage({
@ -1195,12 +1197,12 @@ async function handleVCDGeneration(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.error(
'IC Coder - 仿真失败',
'VCD 文件生成失败,请查看错误信息',
"IC Coder - 仿真失败",
"VCD 文件生成失败,请查看错误信息",
() => {
// 点击通知时聚焦到面板
panel.reveal();
}
},
);
}
} catch (error) {
@ -1218,11 +1220,11 @@ async function handleVCDGeneration(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.error(
'IC Coder - 仿真错误',
error instanceof Error ? error.message : '生成 VCD 文件时出错',
"IC Coder - 仿真错误",
error instanceof Error ? error.message : "生成 VCD 文件时出错",
() => {
panel.reveal();
}
},
);
}
}
@ -1232,7 +1234,7 @@ async function handleVCDGeneration(
*/
export async function handleOptimizePrompt(
panel: vscode.WebviewPanel,
prompt: string
prompt: string,
): Promise<void> {
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
console.log("[MessageHandler] prompt:", prompt);
@ -1264,7 +1266,7 @@ export async function handleOptimizePrompt(
*/
export async function handleAcceptChange(
panel: vscode.WebviewPanel,
changeId: string
changeId: string,
) {
try {
const success = await changeTracker.acceptChange(changeId);
@ -1272,14 +1274,14 @@ export async function handleAcceptChange(
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: true
success: true,
});
} else {
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: false,
error: "采纳变更失败"
error: "采纳变更失败",
});
}
} catch (error) {
@ -1288,7 +1290,7 @@ export async function handleAcceptChange(
command: "changeAccepted",
changeId: changeId,
success: false,
error: String(error)
error: String(error),
});
}
}
@ -1298,7 +1300,7 @@ export async function handleAcceptChange(
*/
export async function handleRejectChange(
panel: vscode.WebviewPanel,
changeId: string
changeId: string,
) {
try {
const success = await changeTracker.rejectChange(changeId);
@ -1306,14 +1308,14 @@ export async function handleRejectChange(
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: true
success: true,
});
} else {
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: false,
error: "拒绝变更失败"
error: "拒绝变更失败",
});
}
} catch (error) {
@ -1322,7 +1324,7 @@ export async function handleRejectChange(
command: "changeRejected",
changeId: changeId,
success: false,
error: String(error)
error: String(error),
});
}
}
@ -1333,18 +1335,18 @@ export async function handleRejectChange(
export function sendChangesToWebview(panel: vscode.WebviewPanel) {
const session = changeTracker.endSession();
if (session && session.changes.length > 0) {
const changesWithDiff = session.changes.map(change => {
const changesWithDiff = session.changes.map((change) => {
const diffLines = generateDiff(change.oldContent, change.newContent);
const diffHtml = renderDiffHtml(diffLines);
return {
...change,
diffHtml
diffHtml,
};
});
panel.webview.postMessage({
command: "showChanges",
changes: changesWithDiff
changes: changesWithDiff,
});
}
}
@ -1361,62 +1363,67 @@ export function startChangeSession(sessionId: string) {
*/
export async function handleOpenFileDiff(
panel: vscode.WebviewPanel,
changeId: string
changeId: string,
) {
try {
const session = changeTracker.getCurrentSession();
if (!session) {
vscode.window.showErrorMessage('没有找到变更会话');
vscode.window.showErrorMessage("没有找到变更会话");
return;
}
const change = session.changes.find(c => c.changeId === changeId);
const change = session.changes.find((c) => c.changeId === changeId);
if (!change) {
vscode.window.showErrorMessage('没有找到该变更');
vscode.window.showErrorMessage("没有找到该变更");
return;
}
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showErrorMessage('没有打开的工作区');
vscode.window.showErrorMessage("没有打开的工作区");
return;
}
// 创建临时文件用于对比
const filePath = change.filePath;
const absolutePath = vscode.Uri.file(
path.join(workspaceFolder.uri.fsPath, filePath)
path.join(workspaceFolder.uri.fsPath, filePath),
);
// 创建虚拟文档显示旧内容
const oldUri = vscode.Uri.parse(
`ic-coder-diff:${filePath}.old?${changeId}`
).with({ scheme: 'ic-coder-diff' });
`ic-coder-diff:${filePath}.old?${changeId}`,
).with({ scheme: "ic-coder-diff" });
// 注册文档内容提供者(如果还没注册)
if (!(global as any).__diffProviderRegistered) {
const provider = new (class implements vscode.TextDocumentContentProvider {
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 || '';
const change = session?.changes.find((c) => c.changeId === changeId);
return change?.oldContent || "";
}
})();
vscode.workspace.registerTextDocumentContentProvider('ic-coder-diff', provider);
vscode.workspace.registerTextDocumentContentProvider(
"ic-coder-diff",
provider,
);
(global as any).__diffProviderRegistered = true;
}
// 打开 diff 编辑器
await vscode.commands.executeCommand(
'vscode.diff',
"vscode.diff",
oldUri,
absolutePath,
`${filePath} (变更对比)`
`${filePath} (变更对比)`,
);
} catch (error) {
console.error('[MessageHandler] 打开 diff 失败:', 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) => {
if (code === 0) {
resolve(stdout);
// 成功时返回 stdout忽略 stderr 中的进度信息
resolve(stdout || stderr);
} else {
reject(new Error(
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`

View File

@ -13,6 +13,7 @@ import {
abortCurrentDialog,
handleOptimizePrompt,
} from "../utils/messageHandler";
import { setCustomConfig } from "../config/settings";
/**
* 创建并显示IC 侧边栏视图
@ -28,7 +29,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
vscode.Uri.joinPath(context.extensionUri, "dist", "assets")
],
}
);
@ -47,21 +48,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
// 获取模型图标URI
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(
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(
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(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Max.png")
);
// 获取二维码图片URI
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
@ -124,12 +125,17 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
case "showWarning":
vscode.window.showWarningMessage(message.message);
break;
// 新增:打开用户手册
case "openUserManual":
vscode.commands.executeCommand("ic-coder.openUserManual");
break;
// 新增:处理用户回答
case "submitAnswer":
handleUserAnswer(
message.askId,
message.selected,
message.customInput
message.customInput,
message.answers
);
break;
// 新增:中止对话
@ -140,6 +146,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
case "optimizePrompt":
handleOptimizePrompt(panel, message.prompt);
break;
// 保存通用设置
case "saveGeneralSettings":
context.globalState.update('generalSettings', message.settings);
// 更新运行时配置(包括清空)
setCustomConfig({ backendUrl: message.settings.backendUrl || '' });
vscode.window.showInformationMessage('设置已保存');
break;
// 加载通用设置
case "loadGeneralSettings":
const settings = context.globalState.get('generalSettings');
panel.webview.postMessage({
command: 'loadedGeneralSettings',
settings: settings
});
break;
}
},
undefined,
@ -157,52 +178,21 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
private readonly extensionUri: vscode.Uri,
private readonly context: vscode.ExtensionContext
) {
// 监听认证状态变化
this.context.subscriptions.push(
vscode.authentication.onDidChangeSessions((e) => {
if (e.provider.id === "iccoder") {
this.refreshLoginStatus();
}
})
);
// 【已禁用】监听认证状态变化 - 无需登录
}
/**
* 刷新登录状态并更新视图
* 【已禁用】刷新登录状态并更新视图 - 无需登录
*/
private async refreshLoginStatus(): Promise<void> {
if (this._view) {
const isLoggedIn = await this.checkLoginStatus();
this._view.webview.html = this.getWebviewContent(
this._view.webview,
isLoggedIn
);
}
// 无需刷新登录状态
}
/**
* 检查登录状态(使用 Authentication API
* 【已禁用】检查登录状态 - 无需登录
*/
private async checkLoginStatus(): Promise<boolean> {
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
console.log("[ICViewProvider] 检查登录状态, session:", session ? "存在" : "不存在");
if (!session) {
return false;
}
// 检查 token 是否过期
const expired = isTokenExpired(session.accessToken);
console.log("[ICViewProvider] token 过期检查结果:", expired);
// 只有明确过期才认为未登录,无法判断时认为已登录
if (expired === true) {
console.log("[ICViewProvider] Token 已过期");
return false;
}
return true;
} catch (error) {
console.log("[ICViewProvider] 检查登录状态失败:", error);
return false;
}
return true; // 始终返回已登录状态
}
resolveWebviewView(webviewView: vscode.WebviewView) {
@ -222,30 +212,8 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
console.log('[ICViewProvider] Webview options 已设置');
console.log('[ICViewProvider] extensionUri:', this.extensionUri.toString());
// 【关键修复】先设置默认 HTML避免一直加载
try {
const html = this.getWebviewContent(webviewView.webview, false);
console.log('[ICViewProvider] HTML 内容已生成,长度:', html.length);
webviewView.webview.html = html;
console.log('[ICViewProvider] HTML 已设置到 webview');
} catch (error) {
console.error('[ICViewProvider] 设置 HTML 失败:', error);
}
// 异步检查登录状态并更新 UI
this.checkLoginStatus()
.then((isLoggedIn) => {
console.log('[ICViewProvider] 登录状态检查完成:', isLoggedIn);
webviewView.webview.html = this.getWebviewContent(
webviewView.webview,
isLoggedIn
);
})
.catch((error) => {
console.error('[ICViewProvider] 检查登录状态失败:', error);
// 即使失败也显示未登录状态
webviewView.webview.html = this.getWebviewContent(webviewView.webview, false);
});
// 【已禁用】登录检查 - 直接显示"开始使用"按钮
webviewView.webview.html = this.getWebviewContent(webviewView.webview, true);
// 处理侧边栏的消息
webviewView.webview.onDidReceiveMessage(
@ -256,15 +224,32 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
vscode.commands.executeCommand("ic-coder.login");
} else if (message.command === "logout") {
// 退出登录(前端已有确认对话框)
vscode.commands.executeCommand('iccoder.logout');
vscode.commands.executeCommand("ic-coder.logout");
} else if (message.command === "openICCoder") {
// 打开 IC Coder 官网
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
} else if (message.command === "openUserManual") {
// 打开用户手册
vscode.commands.executeCommand("ic-coder.openUserManual");
} else if (message.command === "openExternalUrl") {
// 打开外部链接
if (message.url) {
vscode.env.openExternal(vscode.Uri.parse(message.url));
}
} else if (message.command === "saveGeneralSettings") {
// 保存通用设置
this.context.globalState.update('generalSettings', message.settings);
if (message.settings.backendUrl) {
setCustomConfig({ backendUrl: message.settings.backendUrl });
}
vscode.window.showInformationMessage('设置已保存');
} else if (message.command === "loadGeneralSettings") {
// 加载通用设置
const settings = this.context.globalState.get('generalSettings');
webviewView.webview.postMessage({
command: 'loadedGeneralSettings',
settings: settings
});
}
},
undefined,
@ -320,15 +305,15 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
width: 200px;
padding: 8px 12px;
margin: 4px 0;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background: #007ACC;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
text-align: center;
}
.btn:hover {
background: var(--vscode-button-hoverBackground);
background: #005a9e;
}
</style>
</head>
@ -337,7 +322,7 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
<img src="${logoUri}" alt="IC Coder" width="120" />
<h2>欢迎使用 IC Coder</h2>
${isLoggedIn
? '<button class="btn" onclick="openChat()">开始创作</button>'
? '<button class="btn" onclick="openChat()">开始使用</button>'
: '<button class="btn" onclick="login()">登录账户</button>'
}
</div>

View File

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

View File

@ -51,7 +51,11 @@ export function getContextDisplayStyles(): string {
transition: all 0.2s ease;
}
.context-item:hover {
.context-item.clickable {
cursor: pointer;
}
.context-item.clickable:hover {
background: var(--vscode-list-hoverBackground);
}
@ -126,6 +130,11 @@ export function getContextDisplayScript(): string {
return '<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" fill="currentColor"/></svg>';
}
// 获取代码图标 SVG
function getCodeIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z m-484.7-122.1l39.6-39.5 113.1 113.1-39.6 39.5-113.1-113.1z m226.4-290.2l113.1 113.1-39.6 39.5-113.1-113.1 39.6-39.5z" fill="currentColor"/></svg>';
}
// 获取删除图标 SVG
function getRemoveIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
@ -137,9 +146,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();
contextItems.push({ id, type, path });
contextItems.push({ id, type, path, displayPath: displayPath || '' });
renderContextItems();
}
@ -169,13 +181,17 @@ export function getContextDisplayScript(): string {
case 'folder': icon = getFolderIcon(); break;
case 'image': icon = getImageIcon(); break;
case 'document': icon = getDocumentIcon(); break;
case 'code': icon = getCodeIcon(); break;
}
const clickable = item.type !== 'folder' ? 'clickable' : '';
const onclick = item.type !== 'folder' ? \`onclick="window.handleContextItemClick(\${item.id})"\` : '';
return \`
<div class="context-item" title="\${item.path}">
<div class="context-item \${clickable}" title="\${item.path || item.displayPath}" \${onclick}>
\${icon}
<span class="context-item-name">\${getFileName(item.path)}</span>
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
<span class="context-item-name">\${item.displayPath || getFileName(item.path)}</span>
<span class="context-item-remove" onclick="event.stopPropagation(); removeContextItem(\${item.id})">
\${getRemoveIcon()}
</span>
</div>
@ -183,6 +199,27 @@ export function getContextDisplayScript(): string {
}).join('');
}
// 全局访问函数
window.handleContextItemClick = function(id) {
const item = contextItems.find(i => i.id === id);
if (!item || item.type === 'folder') return;
if (item.type === 'code') {
const codeData = JSON.parse(item.path);
vscode.postMessage({
command: 'openFileWithSelection',
filePath: codeData.fileName,
startLine: codeData.startLine,
endLine: codeData.endLine
});
} else {
vscode.postMessage({
command: 'openFile',
filePath: item.path
});
}
};
// 处理后端返回的文件选择结果
window.addEventListener('message', event => {
const message = event.data;
@ -208,6 +245,18 @@ export function getContextDisplayScript(): string {
message.documents.forEach(doc => addContextItem('document', doc));
}
break;
case 'addCodeContext':
// 添加代码上下文
const displayName = \`\${message.fileName.split(/[\\\\/]/).pop()}:\${message.startLine}-\${message.endLine}\`;
const codeData = {
fileName: message.fileName,
startLine: message.startLine,
endLine: message.endLine,
code: message.code,
languageId: message.languageId
};
addContextItem('code', JSON.stringify(codeData), displayName);
break;
}
});

View File

@ -47,11 +47,7 @@ export function getConversationHistoryBarContent(): string {
</svg>
</button>
<div class="user-info-container">
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
${userAvatarIconSvg}
</button>
${getUserInfoComponentContent()}
<div class="user-info-container" style="display: none;">
</div>
<div class='setting'>
@ -330,7 +326,7 @@ export function getConversationHistoryBarStyles(): string {
}
.new-conversation-button:hover {
background: var(--vscode-toolbar-hoverBackground);
background: #007ACC;
transform: scale(1.1);
}

View File

@ -34,14 +34,6 @@ export function getExampleShowcaseContent(): string {
</div>
</div>
</div>
<div class="web-link">
<a href="https://iccoder.com" target="_blank" class="web-link-button">
<span class="link-icon">🌐</span>
<span>IC Coder Web端</span>
<span class="link-arrow">→</span>
</a>
</div>
</div>
`;
}
@ -165,41 +157,6 @@ export function getExampleShowcaseStyles(): string {
-webkit-box-orient: vertical;
}
.web-link {
display: flex;
justify-content: center;
padding-top: 20px;
border-top: 1px solid var(--vscode-panel-border);
margin-top: 8px;
}
.web-link-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: transparent;
border: none;
text-decoration: none;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
outline: none;
}
.web-link-button:focus {
outline: none;
}
.web-link-button:hover {
transform: translateY(-1px);
opacity: 0.8;
}
.link-icon {
font-size: 16px;
}
@ -208,10 +165,6 @@ export function getExampleShowcaseStyles(): string {
font-size: 16px;
transition: transform 0.2s ease;
}
.web-link-button:hover .link-arrow {
transform: translateX(3px);
}
`;
}

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

@ -0,0 +1,73 @@
/**
* 文件路径标签组件
* 功能:显示可点击的文件路径标签
* 使用场景:在用户消息中显示上下文文件
*/
/**
* 获取文件路径标签的样式
*/
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) {
// 解析文件路径,支持 file.v:5-8 格式
const match = filePath.match(/^(.+?):(\\d+)-(\\d+)$/);
if (match) {
vscode.postMessage({
command: 'openFilePathTag',
filePath: match[1],
startLine: parseInt(match[2]),
endLine: parseInt(match[3])
});
} else {
vscode.postMessage({
command: 'openFilePathTag',
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

@ -4,75 +4,15 @@
export function getGeneralSettingsComponentContent(): string {
return `
<div class="general-settings">
<h3 class="settings-section-title">通用设置</h3>
<h3 class="settings-section-title">后端服务配置</h3>
<div class="settings-section">
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">主题</label>
<span class="settings-item-description">选择界面主题</span>
<label class="settings-item-label">后端服务地址</label>
<span class="settings-item-description">自定义后端 API 地址</span>
</div>
<select class="settings-select" id="themeSelect">
<option value="auto">跟随系统</option>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">语言</label>
<span class="settings-item-description">选择界面语言</span>
</div>
<select class="settings-select" id="languageSelect">
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
</select>
</div>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">自动保存</label>
<span class="settings-item-description">自动保存会话历史</span>
</div>
<label class="settings-switch">
<input type="checkbox" id="autoSaveCheckbox" checked>
<span class="settings-switch-slider"></span>
</label>
</div>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">显示时间戳</label>
<span class="settings-item-description">在消息中显示时间戳</span>
</div>
<label class="settings-switch">
<input type="checkbox" id="showTimestampCheckbox">
<span class="settings-switch-slider"></span>
</label>
</div>
</div>
<div class="settings-section">
<h4 class="settings-subsection-title">编辑器设置</h4>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">字体大小</label>
<span class="settings-item-description">设置编辑器字体大小</span>
</div>
<input type="number" class="settings-input" id="fontSizeInput" value="14" min="10" max="24">
</div>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">代码高亮</label>
<span class="settings-item-description">启用代码语法高亮</span>
</div>
<label class="settings-switch">
<input type="checkbox" id="syntaxHighlightCheckbox" checked>
<span class="settings-switch-slider"></span>
</label>
<input type="text" class="settings-input-text" id="backendUrlInput" placeholder="https://iccoder.com">
</div>
</div>
@ -176,6 +116,21 @@ export function getGeneralSettingsComponentStyles(): string {
border-color: var(--vscode-focusBorder);
}
.settings-input-text {
width: 300px;
padding: 6px 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 13px;
outline: none;
}
.settings-input-text:focus {
border-color: var(--vscode-focusBorder);
}
.settings-switch {
position: relative;
display: inline-block;
@ -270,57 +225,37 @@ export function getGeneralSettingsComponentScript(): string {
// 保存通用设置
function saveGeneralSettings() {
const settings = {
theme: document.getElementById('themeSelect').value,
language: document.getElementById('languageSelect').value,
autoSave: document.getElementById('autoSaveCheckbox').checked,
showTimestamp: document.getElementById('showTimestampCheckbox').checked,
fontSize: document.getElementById('fontSizeInput').value,
syntaxHighlight: document.getElementById('syntaxHighlightCheckbox').checked,
backendUrl: document.getElementById('backendUrlInput').value,
};
// 发送消息到扩展
vscode.postMessage({
command: 'saveGeneralSettings',
settings: settings
});
// 显示保存成功提示
console.log('通用设置已保存', settings);
closeSettingsModal();
}
// 重置通用设置
function resetGeneralSettings() {
document.getElementById('themeSelect').value = 'auto';
document.getElementById('languageSelect').value = 'zh-CN';
document.getElementById('autoSaveCheckbox').checked = true;
document.getElementById('showTimestampCheckbox').checked = false;
document.getElementById('fontSizeInput').value = '14';
document.getElementById('syntaxHighlightCheckbox').checked = true;
document.getElementById('backendUrlInput').value = '';
// 清空保存的配置
vscode.postMessage({
command: 'saveGeneralSettings',
settings: { backendUrl: '' }
});
console.log('通用设置已重置为默认值');
closeSettingsModal();
}
// 加载通用设置
function loadGeneralSettings(settings) {
if (!settings) return;
if (settings.theme) {
document.getElementById('themeSelect').value = settings.theme;
}
if (settings.language) {
document.getElementById('languageSelect').value = settings.language;
}
if (settings.autoSave !== undefined) {
document.getElementById('autoSaveCheckbox').checked = settings.autoSave;
}
if (settings.showTimestamp !== undefined) {
document.getElementById('showTimestampCheckbox').checked = settings.showTimestamp;
}
if (settings.fontSize) {
document.getElementById('fontSizeInput').value = settings.fontSize;
}
if (settings.syntaxHighlight !== undefined) {
document.getElementById('syntaxHighlightCheckbox').checked = settings.syntaxHighlight;
if (settings.backendUrl) {
document.getElementById('backendUrlInput').value = settings.backendUrl;
}
}
`;

View File

@ -24,6 +24,10 @@ import {
getContextCompressStyles,
getContextCompressScript,
} from "./contextCompress";
import {
getFilePathTagStyles,
getFilePathTagScript,
} from "./filePathTag";
import {
getOptimizeButtonContent,
getOptimizeButtonStyles,
@ -98,6 +102,7 @@ export function getInputAreaStyles(): string {
${getModelSelectorStyles()}
${getContextButtonStyles()}
${getContextDisplayStyles()}
${getFilePathTagStyles()}
${getContextCompressStyles()}
${getOptimizeButtonStyles()}
${getExampleShowcaseStyles()}
@ -309,6 +314,7 @@ export function getInputAreaScript(): string {
${getContextCompressScript()}
${getOptimizeButtonScript()}
${getChangePanelScript()}
${getFilePathTagScript()}
// 对话状态管理
let isConversationActive = false;
@ -426,7 +432,21 @@ export function getInputAreaScript(): string {
// 获取上下文项
const contextItems = window.getContextItems ? window.getContextItems() : [];
addMessage(text, 'user');
// 构建显示消息:如果有上下文项,添加路径前缀
let displayText = text;
if (contextItems.length > 0) {
const contextPaths = contextItems
.map(item => item.displayPath || item.path)
.join(' ');
if (contextPaths) {
displayText = contextPaths + ' ' + text;
}
}
addMessage(displayText, 'user');
// 重置分段消息容器,强制下次创建新容器
currentSegmentedMessage = null;
// 标记已有消息,切换布局到底部
hasMessages = true;

View File

@ -248,19 +248,21 @@ export function getMessageAreaStyles(): string {
}
.question-option {
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
background: #007ACC;
color: #ffffff;
border: 1px solid #007ACC;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.question-option:hover {
background: var(--vscode-button-secondaryHoverBackground);
background: #005a9e;
border-color: #005a9e;
}
.question-option.selected {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background: #007ACC;
color: #ffffff;
border-color: #007ACC;
}
.question-message.answered .question-option:not(.selected) {
opacity: 0.5;
@ -380,7 +382,7 @@ export function getMessageAreaStyles(): string {
}
/* 低调显示的工具调用 - 移除边距和背景 */
.segment-tool.low-profile {
margin: 5px 0px;
margin: 25px 0px;
padding: 0;
background: none;
}
@ -420,6 +422,9 @@ export function getMessageAreaStyles(): string {
height: 100%;
display: block;
}
.icon-expanded svg path {
fill: #007ACC !important;
}
.tool-segment-header.collapsed .tool-collapse-icon {
transform: rotate(-90deg);
}
@ -544,9 +549,9 @@ export function getMessageAreaStyles(): string {
max-height: 0;
}
.tool-segment-description {
margin: 6px 0 0 0px;
margin: 25px 0 0 0px;
font-size: 0.9rem;
color: #ccc;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
}
/* 低调显示的工具调用样式 */
@ -585,20 +590,22 @@ export function getMessageAreaStyles(): string {
}
.segment-question .question-option {
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
background: #3d3f41;
color: #ffffff;
border: 1px solid #474747;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.segment-question .question-option:hover {
background: var(--vscode-button-secondaryHoverBackground);
background: #005a9e;
border-color: #005a9e;
}
.segment-question .question-option.selected {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background: #007ACC;
color: #ffffff;
border-color: #007ACC;
}
.segment-question.answered .question-option:not(.selected) {
opacity: 0.5;
@ -850,7 +857,26 @@ export function getMessageAreaScript(): string {
div.appendChild(actionsDiv);
} 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) || /:[0-9]+-[0-9]+$/.test(part)) {
filePaths.push(part);
} else {
textParts.push(part);
}
});
if (filePaths.length > 0) {
div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
} else {
div.textContent = text;
}
// 当添加用户消息时,隐藏 header
hideHeaderIfNeeded();
}
@ -988,34 +1014,42 @@ export function getMessageAreaScript(): string {
// 实时更新分段消息(按后端返回顺序)
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) {
console.log('[WebView] segments 为空,跳过渲染');
return;
}
// 如果没有当前分段消息容器,创建一个
if (!currentSegmentedMessage) {
console.log('[WebView] 创建新的分段消息容器');
// 移除流式消息(如果有)
if (currentStreamingMessage) {
console.log('[WebView] 移除流式消息');
currentStreamingMessage.remove();
currentStreamingMessage = null;
}
// 移除所有工具状态消息(因为会在分段中显示)
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
toolStatuses.forEach(el => {
console.log('[WebView] 移除工具状态消息:', el.className);
el.remove();
});
currentSegmentedMessage = document.createElement('div');
currentSegmentedMessage.className = 'message bot-message segmented-message';
messagesEl.appendChild(currentSegmentedMessage);
// 检查最后一个容器是否是未完成的对话(没有操作按钮)
const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
if (lastSegmented && !lastSegmented.querySelector('.message-actions')) {
// 复用未完成的容器
currentSegmentedMessage = lastSegmented;
} else {
// 创建新容器
currentSegmentedMessage = document.createElement('div');
currentSegmentedMessage.className = 'message bot-message segmented-message';
messagesEl.appendChild(currentSegmentedMessage);
}
renderedSegmentCount = 0;
}
// 保存当前所有工具的展开/折叠状态
@ -1154,64 +1188,60 @@ export function getMessageAreaScript(): string {
} else if (segment.type === '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 selectedAnswer = answeredQuestions.get(segment.askId);
const savedAnswers = answeredQuestions.get(segment.askId) || {};
if (isAnswered) {
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
? (segment.options || []).map(opt => {
const isSelected = isAnswered && opt === selectedAnswer;
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join('')
: '';
const optionsHtml = q.options.map(opt => {
const isSelected = selectedAnswers.includes(opt);
return \`<label class="question-option\${isSelected ? ' selected' : ''}" style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 5px 5px 0;">
<input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
<span>\${opt}</span>
</label>\`;
}).join('');
return \`
<div class="question-item" data-question-index="\${qIndex}" style="margin-bottom:12px;">
<div class="question-text" style="margin-bottom:8px;">\${formatText(q.question)}</div>
<div class="question-options">\${optionsHtml}</div>
</div>
\`;
}).join('');
segmentDiv.innerHTML = \`
<div class="question-text">\${formatText(segment.question || '')}</div>
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
<button class="custom-submit">提交</button>
</div>
\${questionsHtml}
<button class="custom-submit" style="display:\${isAnswered ? 'none' : 'block'};margin-top:8px;padding:8px 16px;background:var(--vscode-button-background);color:var(--vscode-button-foreground);border:none;border-radius:6px;cursor:pointer;">提交答案</button>
\`;
// 只在未回答时添加事件监听
if (!isAnswered) {
setTimeout(() => {
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 customInput = segmentDiv.querySelector('.custom-input');
if (submitBtn && customInput) {
if (submitBtn) {
submitBtn.addEventListener('click', function() {
const customValue = customInput.value.trim();
if (customValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
}
});
// 支持回车提交
customInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const customValue = customInput.value.trim();
if (customValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
}
}
const answers = {};
questions.forEach((q, qIndex) => {
const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
answers[qIndex] = Array.from(inputs).map(input => input.value);
});
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
});
}
}, 0);
@ -1227,7 +1257,7 @@ export function getMessageAreaScript(): string {
currentSegmentedMessage.appendChild(segmentDiv);
});
// 如果对话完成,添加操作按钮
// 如果对话完成,添加操作按钮并重置容器
if (isComplete) {
console.log('[WebView] 对话完成,添加操作按钮');
const actionsDiv = document.createElement('div');
@ -1262,7 +1292,7 @@ export function getMessageAreaScript(): string {
actionsDiv.appendChild(dislikeBtn);
currentSegmentedMessage.appendChild(actionsDiv);
// 重置当前分段消息容器
// 重置当前分段消息容器(继续对话时创建新容器)
currentSegmentedMessage = null;
}
@ -1689,6 +1719,43 @@ 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;
// 确保选中的选项保持高亮
if (input.checked) {
const label = input.closest('.question-option');
if (label) {
label.classList.add('selected');
}
}
});
// 隐藏提交按钮
const submitBtn = segmentDiv.querySelector('.custom-submit');
if (submitBtn) {
submitBtn.style.display = 'none';
}
// 发送答案到后端
vscode.postMessage({
command: 'submitAnswer',
askId: askId,
answers: answers
});
}
${getWaveformPreviewScript()}
${getCodeHighlightScript()}

View File

@ -9,66 +9,16 @@ export function getModelSelectorContent(
autoIcon: string = "",
liteIcon: string = "",
syIcon: string = "",
maxIcon: string = ""
maxIcon: string = "",
): string {
return `
<!-- 模型选择 -->
<div class="tooltip">
<div class="custom-select" id="modelSelect">
<div class="select-trigger" onclick="toggleModelDropdown()">
<span class="select-value" id="modelValue">Auto</span>
<svg class="select-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M507.8 727.728a30.016 30.016 0 0 1-21.288-8.824L231.104 463.496a30.088 30.088 0 0 1 0-42.568 30.088 30.088 0 0 1 42.568 0l234.128 234.128 234.16-234.128a30.088 30.088 0 0 1 42.568 0 30.088 30.088 0 0 1 0 42.568L529.08 718.904a30 30 0 0 1-21.28 8.824z" fill="#8a8a8a"/>
</svg>
</div>
<div class="select-dropdown" id="modelDropdown">
<div class="select-option selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
${
autoIcon
? `<img src="${autoIcon}" class="model-icon" alt="Auto">`
: ""
}
<div class="option-content">
<span class="option-label">Auto</span>
<span class="option-desc">智能匹配最优模型</span>
</div>
</div>
<div class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
${
liteIcon
? `<img src="${liteIcon}" class="model-icon" alt="Lite">`
: ""
}
<div class="option-content">
<span class="option-label">Lite</span>
<span class="option-desc">基础模型,快速相应,适合简单任务</span>
</div>
</div>
<div class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
${
syIcon
? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">`
: ""
}
<div class="option-content">
<span class="option-label">Syntaxic</span>
<span class="option-desc">均衡成本和性能节省credits同时保持可靠输出</span>
</div>
</div>
<div class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
${
maxIcon
? `<img src="${maxIcon}" class="model-icon" alt="Max">`
: ""
}
<div class="option-content">
<span class="option-label">Max</span>
<span class="option-desc">最强性能,质量优先,适合复杂任务</span>
</div>
</div>
</div>
<div class="model-display">
<img src="${maxIcon || ""}" class="model-icon" alt="Max" style="display: ${maxIcon ? "block" : "none"};">
<span class="model-label">Max</span>
</div>
<span class="tooltiptext">选择模型</span>
<span class="tooltiptext">IC Coder自研FPGA专属微调模型</span>
</div>
`;
}
@ -78,72 +28,16 @@ export function getModelSelectorContent(
*/
export function getModelSelectorStyles(): string {
return `
/* 自定义下拉框样式 */
.custom-select {
position: relative;
user-select: none;
}
.select-trigger {
.model-display {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s ease;
}
.select-trigger:hover {
background: var(--vscode-list-hoverBackground);
}
.select-value {
white-space: nowrap;
}
.select-arrow {
width: 12px;
height: 12px;
flex-shrink: 0;
transition: transform 0.2s ease;
}
.custom-select.active .select-arrow {
transform: rotate(180deg);
}
.select-dropdown {
position: absolute;
bottom: calc(100% + 2px);
left: 0;
min-width: 100%;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1100;
display: none;
overflow: visible;
}
.custom-select.active .select-dropdown {
display: block;
}
/* 模型选择器的选项样式 */
#modelDropdown .select-option {
position: relative;
padding: 8px 12px;
cursor: pointer;
transition: background 0.2s ease;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
#modelDropdown .select-option:hover {
background: rgba(128, 128, 128, 0.3);
}
#modelDropdown .select-option.selected {
background: rgba(128, 128, 128, 0.5);
color: var(--vscode-foreground);
cursor: default;
}
.model-icon {
width: 16px;
@ -151,21 +45,7 @@ export function getModelSelectorStyles(): string {
flex-shrink: 0;
object-fit: contain;
}
.option-content {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.option-label {
font-size: 13px;
color: var(--vscode-foreground);
font-weight: 500;
white-space: nowrap;
}
.option-desc {
font-size: 11px;
color: var(--vscode-descriptionForeground);
.model-label {
white-space: nowrap;
}
`;
@ -176,58 +56,9 @@ export function getModelSelectorStyles(): string {
*/
export function getModelSelectorScript(): string {
return `
// 模型选择相关变量
let currentModel = 'auto';
// 切换模型下拉框显示/隐藏
function toggleModelDropdown() {
const modelSelect = document.getElementById('modelSelect');
const customSelect = document.getElementById('customSelect');
if (modelSelect) {
modelSelect.classList.toggle('active');
// 关闭模式下拉框
if (customSelect) {
customSelect.classList.remove('active');
}
}
}
// 选择模型
function selectModel(value, label) {
currentModel = value;
const modelValue = document.getElementById('modelValue');
if (modelValue) {
modelValue.textContent = label;
}
// 更新选中状态
const options = document.querySelectorAll('#modelDropdown .select-option');
options.forEach(option => {
if (option.getAttribute('data-value') === value) {
option.classList.add('selected');
} else {
option.classList.remove('selected');
}
});
// 关闭下拉框
const modelSelect = document.getElementById('modelSelect');
if (modelSelect) {
modelSelect.classList.remove('active');
}
}
// 点击外部关闭模型下拉框
document.addEventListener('click', (event) => {
const modelSelect = document.getElementById('modelSelect');
if (modelSelect && !modelSelect.contains(event.target)) {
modelSelect.classList.remove('active');
}
});
// 获取当前选中的模型
// 获取当前选中的模型(固定为 max
function getCurrentModel() {
return currentModel;
return 'max';
}
`;
}

View File

@ -1,6 +1,6 @@
/**
* 更多选项组件
* 包含用户手册和用户反馈入口
* 包含用户手册入口
*/
/**
@ -28,40 +28,10 @@ export function getMoreOptionsComponentContent(): string {
<div class="option-desc">查看使用文档和帮助</div>
</div>
</div>
<div class="more-option-item" id="userFeedbackOption">
<div class="option-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z" fill="currentColor"/>
</svg>
</div>
<div class="option-text">
<div class="option-label">用户反馈</div>
<div class="option-desc">提交问题和建议</div>
</div>
</div>
</div>
</div>
</div>
<!-- 用户反馈二维码弹窗 -->
<div class="feedback-qrcode-modal" id="feedbackQRCodeModal">
<div class="feedback-qrcode-overlay" onclick="closeFeedbackQRCode()"></div>
<div class="feedback-qrcode-content">
<div class="feedback-qrcode-header">
<span class="feedback-qrcode-title">用户反馈</span>
<button class="feedback-qrcode-close" onclick="closeFeedbackQRCode()">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
</svg>
</button>
</div>
<div class="feedback-qrcode-body">
<img class="feedback-qrcode-image" id="feedbackQRCodeImage" alt="微信二维码" />
<p class="feedback-qrcode-text">扫描二维码添加微信反馈</p>
</div>
</div>
</div>
</div>
`;
}
@ -163,125 +133,6 @@ export function getMoreOptionsComponentStyles(): string {
.option-desc {
display: none;
}
/* 用户反馈二维码弹窗 */
.feedback-qrcode-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 20000;
align-items: center;
justify-content: center;
}
.feedback-qrcode-modal.active {
display: flex;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.feedback-qrcode-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
cursor: pointer;
}
.feedback-qrcode-content {
position: relative;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
max-width: 400px;
width: 90%;
animation: slideUp 0.2s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.feedback-qrcode-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--vscode-widget-border);
}
.feedback-qrcode-title {
font-size: 14px;
font-weight: 600;
color: var(--vscode-foreground);
}
.feedback-qrcode-close {
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s ease;
}
.feedback-qrcode-close:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.feedback-qrcode-close svg {
width: 16px;
height: 16px;
color: var(--vscode-foreground);
}
.feedback-qrcode-body {
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.feedback-qrcode-image {
width: 200px;
height: 200px;
border: 1px solid var(--vscode-widget-border);
border-radius: 8px;
}
.feedback-qrcode-text {
margin: 0;
font-size: 13px;
color: var(--vscode-descriptionForeground);
text-align: center;
}
`;
}
@ -331,29 +182,6 @@ export function getMoreOptionsComponentScript(): string {
closeMoreOptionsDropdown();
}
// 打开用户反馈
function openUserFeedback() {
console.log('打开用户反馈');
vscode.postMessage({ command: 'openUserFeedback' });
closeMoreOptionsDropdown();
}
// 显示用户反馈二维码弹窗
function showFeedbackQRCode() {
const modal = document.getElementById('feedbackQRCodeModal');
if (modal) {
modal.classList.add('active');
}
}
// 关闭用户反馈二维码弹窗
function closeFeedbackQRCode() {
const modal = document.getElementById('feedbackQRCodeModal');
if (modal) {
modal.classList.remove('active');
}
}
// 绑定更多选项事件
document.addEventListener('DOMContentLoaded', () => {
// 绑定用户手册选项
@ -362,12 +190,6 @@ export function getMoreOptionsComponentScript(): string {
userManualOption.addEventListener('click', openUserManual);
}
// 绑定用户反馈选项
const userFeedbackOption = document.getElementById('userFeedbackOption');
if (userFeedbackOption) {
userFeedbackOption.addEventListener('click', openUserFeedback);
}
// 点击页面其他地方关闭下拉面板
document.addEventListener('click', (e) => {
const dropdown = document.getElementById('moreOptionsDropdown');

View File

@ -18,7 +18,7 @@ export function getNdtWelcomeModalContent(logoUri?: string): string {
<div class="ndt-welcome-modal-header">
<div class="ndt-welcome-icon">🎉</div>
<h2>欢迎宁德时代新能源科技股份有限公司<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder</h2>
<h2>欢迎企业<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder</h2>
</div>
<div class="ndt-welcome-modal-body">

View File

@ -195,11 +195,11 @@ export function getPlanCardStyles(): string {
background: var(--vscode-list-hoverBackground);
}
.plan-btn-confirm {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background: #007ACC;
color: #ffffff;
}
.plan-btn-confirm:hover {
background: var(--vscode-button-hoverBackground);
background: #005a9e;
}
.plan-btn-cancel {
background: transparent;

View File

@ -25,7 +25,7 @@ export function getProgressBarContent(): string {
<span class="step-number">1</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Spec</div>
<div class="step-label">Specification</div>
</div>
<div class="progress-line"></div>
@ -186,8 +186,8 @@ export function getProgressBarStyles(): string {
/* 已完成状态 */
.progress-step.completed .step-circle {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
background: #007ACC;
border-color: #007ACC;
}
.progress-step.completed .step-number {
@ -204,14 +204,14 @@ export function getProgressBarStyles(): string {
}
.progress-step.completed + .progress-line {
background: var(--vscode-button-background);
background: #007ACC;
}
/* 进行中状态 */
.progress-step.active .step-circle {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
background: #007ACC;
border-color: #007ACC;
box-shadow: 0 0 0 2px #007ACC33;
animation: pulse 2s infinite;
}
@ -226,10 +226,10 @@ export function getProgressBarStyles(): string {
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
box-shadow: 0 0 0 2px #007ACC33;
}
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) => {
if (index < currentIndex) {
line.style.background = 'var(--vscode-button-background)';
line.style.background = '#007ACC';
} else {
line.style.background = 'var(--vscode-input-border)';
}

View File

@ -3,11 +3,6 @@ import {
getGeneralSettingsComponentStyles,
getGeneralSettingsComponentScript,
} from "./generalSettingsComponent";
import {
getRulesSettingsComponentContent,
getRulesSettingsComponentStyles,
getRulesSettingsComponentScript,
} from "./rulesSettingsComponent";
/**
* 获取设置面板的 HTML 内容
@ -31,18 +26,13 @@ export function getSettingsComponentContent(): string {
<button class="settings-nav-item active" data-tab="general" onclick="switchSettingsTab('general')">
通用
</button>
<button class="settings-nav-item" data-tab="rules" onclick="switchSettingsTab('rules')">
规则
</button>
</div>
<div class="settings-content">
<div class="settings-tab-content active" id="generalSettings">
${getGeneralSettingsComponentContent()}
</div>
<div class="settings-tab-content" id="rulesSettings">
${getRulesSettingsComponentContent()}
</div>
</div>
</div>
</div>
@ -186,7 +176,6 @@ export function getSettingsComponentStyles(): string {
}
${getGeneralSettingsComponentStyles()}
${getRulesSettingsComponentStyles()}
`;
}
@ -196,13 +185,14 @@ export function getSettingsComponentStyles(): string {
export function getSettingsComponentScript(): string {
return `
${getGeneralSettingsComponentScript()}
${getRulesSettingsComponentScript()}
// 打开设置面板
function openSettingsModal() {
const modal = document.getElementById('settingsModal');
if (modal) {
modal.classList.add('active');
// 请求加载设置
vscode.postMessage({ command: 'loadGeneralSettings' });
}
}

View File

@ -9,57 +9,7 @@
*/
export function getUserInfoComponentContent(): string {
return `
<div class="user-info-wrapper">
<!-- 用户详情下拉面板 -->
<div class="user-detail-dropdown" id="userDetailDropdown">
<div class="user-detail-content">
<div class="user-detail-header">
<div class="user-info-row">
<div class="user-avatar-small clickable" id="userAvatarClickable">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>
</svg>
</div>
<div class="user-name-tier">
<div class="user-detail-name clickable" id="userDetailName">加载中...</div>
<img class="tier-icon-inline" id="tierIconInline" style="display: none;" />
</div>
</div>
<!-- 升级到Pro按钮 (仅BASIC会员显示) -->
<!-- <div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
</div> -->
</div>
<div class="user-detail-body">
<!-- <div class="user-detail-item">
<span class="detail-label">剩余 Credits</span>
<span class="detail-value" id="creditsDetail">-</span>
</div> -->
<div class="user-detail-item logout-item" id="logoutItem">
<span class="detail-label">账户管理</span>
<span class="detail-value logout-link">退出登录</span>
</div>
</div>
</div>
</div>
</div>
<!-- 退出登录确认对话框 -->
<div class="logout-confirm-modal" id="logoutConfirmModal">
<div class="logout-confirm-overlay"></div>
<div class="logout-confirm-content">
<div class="logout-confirm-header">
<h3>确认退出</h3>
</div>
<div class="logout-confirm-body">
<p>确定要退出登录吗?</p>
</div>
<div class="logout-confirm-footer">
<button class="logout-confirm-btn logout-cancel-btn" id="logoutCancelBtn">取消</button>
<button class="logout-confirm-btn logout-ok-btn" id="logoutOkBtn">确定</button>
</div>
</div>
<div class="user-info-wrapper" style="display: none;">
</div>
`;
}

View File

@ -18,6 +18,11 @@ import {
getMessageAreaScript,
} from "./messageArea";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import {
getMoreOptionsComponentContent,
getMoreOptionsComponentStyles,
getMoreOptionsComponentScript,
} from "./moreOptionsComponent";
import {
getProgressBarContent,
getProgressBarStyles,
@ -110,6 +115,7 @@ export function getWebviewContent(
}
${getMessageAreaStyles()}
${getAgentCardStyles()}
${getMoreOptionsComponentStyles()}
${getWaveformPreviewContent()}
${getConversationHistoryBarStyles()}
${getProgressBarStyles()}
@ -375,16 +381,32 @@ export function getWebviewContent(
font-size: 13px;
color: var(--vscode-descriptionForeground);
}
.status-bar #statusText {
background: linear-gradient(90deg,
var(--vscode-descriptionForeground) 0%,
var(--vscode-foreground) 50%,
var(--vscode-descriptionForeground) 100%);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: textShimmer 2s linear infinite;
}
@keyframes textShimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--vscode-charts-blue);
animation: statusPulse 1.5s ease-in-out infinite;
box-shadow: 0 0 8px currentColor;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
0%, 100% { opacity: 1; transform: scale(1.2); }
50% { opacity: 0.3; transform: scale(0.6); }
}
.status-bar.working .status-indicator {
background: var(--vscode-charts-orange);
@ -488,13 +510,17 @@ export function getWebviewContent(
${getNdtWelcomeModalContent(logoUri)}
${getExpiredModalContent(logoUri)}
<div class="header">
<div style="display: flex; align-items: center; justify-content: center;">
<div style="display: flex; align-items: flex-end; justify-content: center">
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
<span style="font-size: 23px; font-weight: bold; background: linear-gradient(to bottom, #b2e4ff, #42bcff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin: 0 0 14px -16px;">企业版</span>
</div>
<p style="font-size: 16px; margin-top: 12px; line-height: 1.5;">
The <span style="background: linear-gradient(to right, #42bcff, #4A9EFF); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold;">Agentic AI</span> Verilog Coding Platform,
<span style="display: block; margin-top: 8px;">将芯片设计与验证的效率提升至少20倍</span>
<p style="font-size: 16px; margin-top: 8px; line-height: 1.5;">
The <span style="background: linear-gradient(to right, #42bcff, #4A9EFF); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold;">Agentic AI</span> Verilog Coding Platform
<span style="display: block; margin-top: 8px;">将FPGA研发效率提升至少20倍</span>
</p>
<div style="margin-top: 16px; padding: 8px 20px; background: linear-gradient(135deg, rgba(255, 215, 0, 0.15), rgba(255, 165, 0, 0.15)); border: 1px solid rgba(255, 215, 0, 0.3); border-radius: 6px;">
<p style="font-size: 13px; margin: 0; background: linear-gradient(135deg, #FFD700, #FFA500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 600; letter-spacing: 1px;">宁德时代专属定制版</p>
</div>
</div>
<div class="chat-container">
@ -903,6 +929,13 @@ export function getWebviewContent(
}
break;
case 'loadedGeneralSettings':
// 加载通用设置
if (typeof loadGeneralSettings === 'function') {
loadGeneralSettings(message.settings);
}
break;
default:
console.log('[WebView] 未处理的消息类型:', message.command);
}
@ -910,6 +943,7 @@ export function getWebviewContent(
${getMessageAreaScript()}
${getAgentCardScript()}
${getMoreOptionsComponentScript()}
${getWaveformPreviewScript()}
${getConversationHistoryBarScript()}
${getProgressBarScript()}

View File

@ -19,40 +19,41 @@ export function getWelcomeModalContent(logoUri?: string): string {
<div class="welcome-modal-header">
<div class="welcome-icon">🎉</div>
<h2>欢迎使用 IC Coder</h2>
<p class="welcome-modal-subtitle">您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!</p>
</div>
<div class="welcome-modal-body">
<div class="welcome-step">
<div class="welcome-step-icon">📝</div>
<div class="welcome-step-content">
<h3>步骤 1打开聊天面板</h3>
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
<!-- 试用期提示 -->
<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="welcome-step">
<div class="welcome-step-icon">💬</div>
<div class="welcome-step-content">
<h3>步骤 2输入您的需求</h3>
<p>描述您想要生成的 Verilog 代码或需要帮助的问题AI 将为您提供专业的解决方案</p>
</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 class="welcome-step">
<div class="welcome-step-icon">🔬</div>
<div class="welcome-step-content">
<h3>步骤 3运行仿真</h3>
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
</div>
</div>
<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>
@ -96,7 +97,7 @@ export function getWelcomeModalStyles(): string {
border: 1px solid var(--vscode-widget-border);
border-radius: 12px;
width: 100%;
max-width: 500px;
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;
@ -124,9 +125,10 @@ export function getWelcomeModalStyles(): string {
.welcome-modal-header h2 {
margin: 0 0 12px;
font-size: 24px;
font-size: 20px;
font-weight: 600;
color: var(--vscode-foreground);
line-height: 1.4;
}
.welcome-modal-subtitle {
@ -140,37 +142,70 @@ export function getWelcomeModalStyles(): string {
padding: 0 32px 32px;
}
.welcome-step {
/* 试用期横幅 */
.trial-banner {
display: flex;
gap: 16px;
margin: 20px 0;
padding: 16px;
align-items: center;
justify-content: center;
padding: 12px 16px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 8px;
border-left: 4px solid var(--vscode-textLink-foreground);
}
.welcome-step-icon {
font-size: 24px;
flex-shrink: 0;
}
.welcome-step-content h3 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 600;
color: var(--vscode-textLink-foreground);
}
.welcome-step-content p {
margin: 0;
margin-bottom: 20px;
font-size: 13px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
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 {
width: 100%;
flex: 1;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
@ -181,18 +216,32 @@ export function getWelcomeModalStyles(): string {
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;
}
.welcome-btn:hover {
.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);
}

Binary file not shown.

View File

@ -3,30 +3,30 @@
'use strict';
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
/** @type WebpackConfig */
const extensionConfig = {
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
target: 'node',
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
entry: './src/extension.ts',
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
libraryTarget: 'commonjs2',
clean: true // 自动清理旧文件
},
externals: {
vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
'node-notifier': 'commonjs node-notifier' // node-notifier 依赖原生模块,必须排除
// modules added here also need to be added in the .vscodeignore file
vscode: 'commonjs vscode',
'node-notifier': 'commonjs node-notifier'
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js']
extensions: ['.ts', '.js'],
mainFields: ['module', 'main']
},
module: {
rules: [
@ -35,15 +35,37 @@ const extensionConfig = {
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
loader: 'ts-loader',
options: {
transpileOnly: true, // 加快编译速度
compilerOptions: {
sourceMap: true
}
}
}
]
}
]
},
devtool: 'nosources-source-map',
devtool: process.env.NODE_ENV === 'production' ? 'hidden-source-map' : 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
level: "log",
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'src/assets', to: 'assets' }
]
})
],
optimization: {
minimize: process.env.NODE_ENV === 'production',
usedExports: true // Tree Shaking
},
performance: {
hints: 'warning',
maxAssetSize: 2 * 1024 * 1024, // 2MB
maxEntrypointSize: 2 * 1024 * 1024
}
};
module.exports = [ extensionConfig ];