Compare commits
133 Commits
feat/detai
...
feat/Delet
| Author | SHA1 | Date | |
|---|---|---|---|
| ea19dfcbe6 | |||
| fa55e32153 | |||
| f6b1f5c45a | |||
| 1f9a1822c9 | |||
| 63015c6bbc | |||
| 24b30df992 | |||
| 67b1003831 | |||
| 00d37bdaf0 | |||
| c5fcb1427e | |||
| 9118ebd662 | |||
| 19cdf47bed | |||
| 95bac94479 | |||
| 421a8934a7 | |||
| f7f45668d3 | |||
| c8e9a5b897 | |||
| a1bfa62796 | |||
| 64e11cbc3c | |||
| 15445aa13c | |||
| 52834047f2 | |||
| 76817675f1 | |||
| 2cce8f94c9 | |||
| 9b5f102d9f | |||
| 68de33165e | |||
| f56ad33366 | |||
| 35c63802b5 | |||
| 3458f6fe23 | |||
| 8f305033f7 | |||
| 4a18f1c418 | |||
| 373edb6d80 | |||
| 1c66e0e599 | |||
| 536e7720cb | |||
| 75eac4b1ce | |||
| 9ed0afee6b | |||
| f700473967 | |||
| 5fc0fd2a95 | |||
| 5f88c7ceac | |||
| 4c7ec65577 | |||
| 3e18299099 | |||
| 021be88880 | |||
| a479e81682 | |||
| c3e3012a94 | |||
| c9e9df3825 | |||
| 7ca2fa1bcc | |||
| 208c24682b | |||
| 316c784bde | |||
| 1467ae8a89 | |||
| 1881615860 | |||
| 0ea3afbe70 | |||
| 4f1d7f495a | |||
| 7c4ecb013e | |||
| ed5976a22c | |||
| d0462ca4b9 | |||
| eae3968465 | |||
| a734ccbb88 | |||
| 7444bb1140 | |||
| 6ef7e976cc | |||
| 31419e93a1 | |||
| 173132399e | |||
| ae703091d4 | |||
| 8daea722bd | |||
| 032dd1b215 | |||
| 885e2cef75 | |||
| 9296b10150 | |||
| 423c9ddb0e | |||
| 50eacdafde | |||
| d90cca7cef | |||
| 5347425327 | |||
| 28d93c7e75 | |||
| 5339212de9 | |||
| 73a1510de4 | |||
| 606f757699 | |||
| 342bf22f3f | |||
| d2ec73f796 | |||
| c9f597beec | |||
| e9a201ef01 | |||
| 77a89847cb | |||
| c14b7f4dbc | |||
| 64724bf48c | |||
| c9e160f2ef | |||
| 3a19cc638f | |||
| a2e8e74572 | |||
| ad96743fad | |||
| 95b1bd7678 | |||
| 94b6fb056f | |||
| a24fd71636 | |||
| 7d1b8f7e26 | |||
| 5753e120ba | |||
| f55a5bfbcb | |||
| 83b706d5be | |||
| b9e63bc9a9 | |||
| ef0c8748f7 | |||
| 430a2c4062 | |||
| f5bd35c71a | |||
| f958683f53 | |||
| 21a8abd5cf | |||
| 4b2da8244f | |||
| c571cd9137 | |||
| 8cf0e32184 | |||
| 1cbd0c5fe7 | |||
| 72a84ed9e2 | |||
| 58113fb109 | |||
| 25966bc1e2 | |||
| 3c93c07afd | |||
| 85a37b546c | |||
| 37a121c3de | |||
| 341b6540fa | |||
| 1d074e5a94 | |||
| 5a5d82eef8 | |||
| 43189e144a | |||
| fd11eadc19 | |||
| 1231ef0892 | |||
| a1e88d473b | |||
| d08f9a7366 | |||
| faa7b63aee | |||
| e440dd2773 | |||
| a02027e7c9 | |||
| 772b067202 | |||
| a3fd5df8e8 | |||
| bdc55c727a | |||
| 52e4522ed0 | |||
| d44b316c9a | |||
| 939768986c | |||
| 1e99f3cb20 | |||
| 2af79cf1dc | |||
| 5b225126f1 | |||
| 4abb979eab | |||
| 4a790b5aca | |||
| 9786b7141c | |||
| 4a7af49fea | |||
| 15a1de3a90 | |||
| 5c19be22d3 | |||
| 5546791549 | |||
| 178f3a7498 |
3
.gitignore
vendored
@ -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
|
||||
|
||||
2
.npmrc
@ -1 +1,3 @@
|
||||
enable-pre-post-scripts = true
|
||||
shamefully-hoist = true
|
||||
public-hoist-pattern[] = *
|
||||
@ -1,29 +1,28 @@
|
||||
# 排除开发文件
|
||||
# 开发文件
|
||||
.vscode/**
|
||||
.git/**
|
||||
.gitignore
|
||||
node_modules/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
**/*.ts
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
|
||||
# 排除测试文件
|
||||
test/**
|
||||
**/*.test.js
|
||||
# 测试文件
|
||||
out/test/**
|
||||
|
||||
# 排除文档
|
||||
*.md
|
||||
!README.md
|
||||
# 依赖
|
||||
node_modules/**
|
||||
|
||||
# 排除 waveform_trace Python 源码(只保留 exe)
|
||||
tools/waveform_trace/src/**
|
||||
tools/waveform_trace/build/**
|
||||
tools/waveform_trace/dist/**
|
||||
tools/waveform_trace/build.bat
|
||||
tools/waveform_trace/build.sh
|
||||
# 文档(避免中文文件名打包问题)
|
||||
docs/**
|
||||
PUBLISH.md
|
||||
|
||||
# 排除打包临时文件
|
||||
**/__pycache__/**
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
**/*.spec
|
||||
# 只排除 waveform_trace 的 src/dist 目录
|
||||
tools/waveform_trace/src/dist/**
|
||||
|
||||
# Git 相关
|
||||
.git/**
|
||||
.github/**
|
||||
|
||||
63
CHANGELOG.md
@ -1,9 +1,62 @@
|
||||
# Change Log
|
||||
# 更新日志
|
||||
|
||||
All notable changes to the "ic-coder" extension will be documented in this file.
|
||||
所有重要的项目变更都将记录在此文件中。
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
## [1.0.9] - 2026-03-04
|
||||
|
||||
## [Unreleased]
|
||||
### 优化
|
||||
|
||||
- Initial release
|
||||
- 将工具折叠图标颜色从蓝色改为灰色
|
||||
- 统一使用蓝色主题色
|
||||
- 优化打包配置,排除重复的 exe 文件
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复代码变更继续对话查找不到之前的代码变更信息的 bug
|
||||
- 修复对话展示两遍的问题
|
||||
|
||||
## [1.0.8] - 2026-03-03
|
||||
|
||||
### 新增
|
||||
|
||||
- 删除文件确认功能
|
||||
- 文件路径标签显示
|
||||
- 企业试用用户欢迎弹窗优化
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复继续对话时消息覆盖问题
|
||||
- 修复试用用户欢迎弹窗显示逻辑
|
||||
- 修复企业试用用户仍弹出邀请码的问题
|
||||
- 修复登录过期点击重新登录失败的问题
|
||||
|
||||
## [1.0.7] - 2026-03-02
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 AI 响应内容重复显示问题
|
||||
|
||||
## [1.0.6] - 2026-03-02
|
||||
|
||||
### 新增
|
||||
|
||||
- Git Diff 功能:支持查看当前文件的 Git 差异对比
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复添加上下文搜索选择文件不匹配的问题
|
||||
- 修复过期认证状态未清除导致重新登录失败的问题
|
||||
|
||||
## [1.0.4] - 2026-01-28
|
||||
|
||||
IC Coder插件端正式上线。
|
||||
|
||||
IC Coder 插件端是一个是一个自主式人工智能 Verilog 编码平台,可以将芯片设计与验证的效率提升至少20倍!
|
||||
|
||||
主要功能:
|
||||
|
||||
- 自动搭建电路架构:够根据自然语言描述的设计需求,自动生成完整的电路架构
|
||||
- AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码
|
||||
- AI自主代码迭代:实现了真正的自主式开发循环,能够持续优化代码直到满足设计要求
|
||||
- 随时可掌控:提供透明化的开发过程,让用户始终掌握AI的工作状态
|
||||
- 多层次安全保障:将数据安全和隐私保护作为核心设计原则,提供企业级的安全保障
|
||||
|
||||
55
PUBLISH.md
@ -67,6 +67,11 @@
|
||||
CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDOVVyT
|
||||
```
|
||||
|
||||
```
|
||||
//蔡工的token
|
||||
6CB3tOZPiwNi6rrOuFHMe6QzrVWBnajW5fJsNgCWu8jtERUCCRnJJQQJ99CAACAAAAAAAAAAAAASAZDO3FnY
|
||||
```
|
||||
|
||||
### 3. 创建发布者账号
|
||||
|
||||
发布者账号是你在 VS Code 市场的身份标识。
|
||||
@ -83,6 +88,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
||||
5. 点击 **Create** 完成创建
|
||||
|
||||
**注意事项:**
|
||||
|
||||
- Publisher ID 一旦创建无法修改
|
||||
- Publisher ID 必须全局唯一
|
||||
- 建议使用有意义且专业的 ID
|
||||
@ -121,6 +127,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
||||
## [0.0.2] - 2025-12-29
|
||||
|
||||
### 新增
|
||||
|
||||
- 添加发送和暂停按钮功能
|
||||
- 添加一键优化按钮组件
|
||||
- 添加 Plan 开关组件
|
||||
@ -128,11 +135,13 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
||||
- 添加上下文压缩功能
|
||||
|
||||
### 改进
|
||||
|
||||
- 优化用户界面交互体验
|
||||
|
||||
## [0.0.1] - 2025-12-XX
|
||||
|
||||
### 新增
|
||||
|
||||
- 初始版本发布
|
||||
- Verilog 代码智能生成
|
||||
- 集成 iverilog 仿真工具
|
||||
@ -156,6 +165,7 @@ in the Software without restriction...
|
||||
### 4. 优化 README.md
|
||||
|
||||
确保 README 包含:
|
||||
|
||||
- 清晰的功能介绍
|
||||
- 使用截图或 GIF 演示
|
||||
- 详细的使用说明
|
||||
@ -214,6 +224,7 @@ pnpm vsce publish
|
||||
**步骤:**
|
||||
|
||||
1. 本地打包插件:
|
||||
|
||||
```bash
|
||||
pnpm run package
|
||||
pnpm vsce package[pnpm vsce package --no-dependencies]
|
||||
@ -252,7 +263,7 @@ pnpm vsce publish major
|
||||
|
||||
```bash
|
||||
# 发布指定版本
|
||||
pnpm vsce publish 0.0.3
|
||||
npx vsce publish --packagePath iccoder-1.0.7.vsix
|
||||
```
|
||||
|
||||
### 更新流程建议
|
||||
@ -263,6 +274,42 @@ pnpm vsce publish 0.0.3
|
||||
4. 执行发布命令
|
||||
5. 验证市场上的插件是否正常
|
||||
|
||||
## 更新流程
|
||||
|
||||
1. 修改版本号
|
||||
|
||||
手动修改 修改package.json文件
|
||||
|
||||
命令修改
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
2. 打包
|
||||
|
||||
```bash
|
||||
#先编译
|
||||
pnpm run compile
|
||||
|
||||
#中间build
|
||||
pnpm run build
|
||||
|
||||
#后打包成.vsix
|
||||
pnpm vsce package --no-dependencies
|
||||
```
|
||||
|
||||
3. 手动上传/命令上传
|
||||
- https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update
|
||||
- 命令上传:vsce publish
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
@ -272,6 +319,7 @@ pnpm vsce publish 0.0.3
|
||||
**原因:** PAT Token 无效或过期
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 重新生成 PAT Token
|
||||
- 重新登录:`pnpm vsce login ic-coder-team`
|
||||
|
||||
@ -280,6 +328,7 @@ pnpm vsce publish 0.0.3
|
||||
**原因:** Publisher ID 不存在或不匹配
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 检查 `package.json` 中的 `publisher` 字段
|
||||
- 确认已在市场创建对应的 Publisher
|
||||
|
||||
@ -288,17 +337,20 @@ pnpm vsce publish 0.0.3
|
||||
**原因:** 必需文件缺失
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 确保 `dist/` 目录存在且包含编译后的代码
|
||||
- 运行 `pnpm run package` 重新构建
|
||||
|
||||
### 4. 插件审核被拒
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- 插件名称或描述违反市场规则
|
||||
- 图标不符合要求(建议 128x128 PNG)
|
||||
- README 内容不完整
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 查看审核反馈邮件
|
||||
- 修改相关内容后重新发布
|
||||
|
||||
@ -320,6 +372,7 @@ code --install-extension ic-coder-plugin-0.0.2.vsix
|
||||
```
|
||||
|
||||
或者在 VS Code 中:
|
||||
|
||||
1. 打开扩展面板
|
||||
2. 点击 `...` 菜单
|
||||
3. 选择 **Install from VSIX...**
|
||||
|
||||
93
README.md
@ -1,24 +1,87 @@
|
||||
# IC Coder Plugin
|
||||
## 什么是IC Coder
|
||||
|
||||
IC Coder 是一个面向 Verilog/FPGA 开发的智能辅助插件。
|
||||
IC Coder是一款**The Agentic AI Verilog Coding Platform(自主式人工智能 Verilog 编码平台)**。我们立志于用AI重塑芯片开发者的效率,将芯片设计与验证的效率提升至少20倍!让芯片开发者们,都能享受到AI发展所带来的科技福利!目标成为全球最好用的"LLM生成Verilog"的平台!
|
||||
|
||||
## 功能特性
|
||||

|
||||
|
||||
- Verilog 代码智能生成
|
||||
- 文件操作支持(创建、读取、修改、删除)
|
||||
- 集成 iverilog 仿真工具
|
||||
- VCD 波形文件生成
|
||||
- 智能对话助手
|
||||
### 核心技术架构
|
||||
|
||||
## 使用说明
|
||||
**我们采用全球顶尖的大语言模型**,加上自研的针对芯片设计领域深度优化的微调模型,为代码生成提供强大的AI能力支撑。
|
||||
|
||||
安装插件后,点击侧边栏的 IC Coder 图标即可开始使用。
|
||||
**核心技术栈**包括:
|
||||
|
||||
## 系统要求
|
||||
- **多智能体架构(Multi-Agent System)**:多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节
|
||||
- **增强上下文引擎**:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性
|
||||
|
||||
- VS Code 1.107.0 或更高版本
|
||||
- 插件已内置 iverilog 工具(Windows 平台)
|
||||
这些技术共同支撑着从需求分析、架构设计、代码生成到验证调试的全流程智能化开发体验。
|
||||
|
||||
## 许可证
|
||||

|
||||
|
||||
MIT
|
||||
## 自动搭建电路架构
|
||||
|
||||
IC Coder能够根据自然语言描述的设计需求,自动生成完整的电路架构。系统会:
|
||||
|
||||
- **智能解析需求**:理解功能规格、性能指标、接口要求等设计约束
|
||||
- **自动模块划分**:根据功能将设计合理拆分为多个子模块,确保模块化和可复用性
|
||||
- **生成层次结构**:建立清晰的模块层次关系,自动处理模块间的信号连接
|
||||
- **结构化信号管理**:将所有电路信号关系进行结构化表示,包括数据流向、控制逻辑、时序关系等
|
||||
- **可视化展示**:以图形化方式展示整体架构,便于理解和审查设计方案
|
||||
|
||||

|
||||
|
||||
## AI自主仿真
|
||||
|
||||
IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码:
|
||||
|
||||
- **智能Testbench生成**:根据设计模块自动生成完整的测试平台,包括激励生成、时钟复位、接口驱动等
|
||||
- **测试用例自动化**:根据设计规格自动生成覆盖各种场景的测试用例,包括正常功能、边界条件、异常情况等
|
||||
- **一键运行仿真**:自动调用集成仿真器执行仿真
|
||||
- **波形自动生成**:仿真完成后自动生成VCD、波形文件,便于后续分析
|
||||
- **实时进度反馈**:仿真过程中实时显示执行状态和日志信息
|
||||
|
||||

|
||||
|
||||
## AI自主代码迭代
|
||||
|
||||
IC Coder实现了真正的自主式开发循环,能够持续优化代码直到满足设计要求:
|
||||
|
||||
- **智能问题诊断**:根据波形分析结果,自动定位代码中的问题根源
|
||||
- **自动代码修复**:针对发现的问题自动生成修复方案并更新代码
|
||||
- **迭代验证循环**:修复后自动重新运行仿真和波形分析,验证问题是否解决
|
||||
- **持续优化**:如果仍存在问题,继续分析和修复,形成闭环迭代
|
||||
- **收敛保证**:智能判断迭代进展,避免无效循环,确保最终收敛到正确设计
|
||||
- **全程可追溯**:记录每次迭代的修改内容和验证结果,便于回溯和审查
|
||||
|
||||
这种自主迭代能力大幅减少了人工调试时间,让设计验证过程更加高效可靠。
|
||||
|
||||
## 随时可掌控
|
||||
|
||||
IC Coder提供透明化的开发过程,让用户始终掌握AI的工作状态:
|
||||
|
||||
- **实时流程展示**:可视化展示当前执行到哪个阶段(需求分析、架构设计、代码生成、仿真验证等)
|
||||
- **详细执行日志**:记录每一步操作的详细信息,包括AI的思考过程、决策依据、执行结果
|
||||
- **人机协同交互**:在关键决策点支持用户介入,可随时提供反馈、调整方向或修改参数
|
||||
- **进度实时追踪**:显示任务完成进度、预计剩余步骤,让开发过程更加可预期
|
||||
- **智能建议系统**:AI主动提供优化建议和替代方案,用户可选择采纳或自定义
|
||||
- **即时响应机制**:支持随时暂停、恢复或调整AI的工作流程
|
||||
|
||||
这种透明可控的设计理念,让AI开发不再是"黑盒",而是真正的智能协作伙伴。
|
||||
|
||||
## 多层次安全保障
|
||||
|
||||
IC Coder将数据安全和隐私保护作为核心设计原则,提供企业级的安全保障:
|
||||
|
||||
- **本地优先存储**:所有设计文件默认存储在本地,用户完全掌控自己的代码资产
|
||||
- **全链路加密传输**:与云端通信采用TLS/SSL加密,确保数据传输过程中不被窃取或篡改
|
||||
- **云端零存储策略**:云端服务器不保存用户的源代码,仅处理加密后的临时数据,处理完成后立即销毁
|
||||
- **定制化部署选项**:支持企业私有云或本地部署,满足高安全等级需求
|
||||
|
||||
真正做到了代码全链路加密传输、云端零存储,让芯片设计企业可以放心使用AI工具。
|
||||
|
||||
## 反馈
|
||||
|
||||
无论是想与我们深入交流还是遇到任何问题,欢迎您[进入社区](https://iccoder.com:888/community)与我们联系
|
||||
|
||||
## 服务条款和隐私协议
|
||||
|
||||
请阅读我们的[服务条款](https://iccoder.com:888/guides/legal/terms-of-service)和[隐私协议](https://iccoder.com:888/guides/legal/privacy-policy)了解更多细节。
|
||||
|
||||
261
docs/AskUserQuestion-API设计.md
Normal 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. ✅ 向后兼容(可以只有一个问题)
|
||||
804
docs/VSCode-Extension-API-Guide.md
Normal 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)
|
||||
45
docs/code-changes-feature.md
Normal file
@ -0,0 +1,45 @@
|
||||
# 代码变更审查功能
|
||||
|
||||
## 功能概述
|
||||
|
||||
AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以查看所有修改并选择采纳或拒绝。
|
||||
|
||||
## 核心文件
|
||||
|
||||
### 1. 数据结构
|
||||
- `src/types/fileChanges.ts` - 变更数据类型定义
|
||||
|
||||
### 2. 服务层
|
||||
- `src/services/changeTracker.ts` - 变更追踪服务(单例)
|
||||
- `trackChange()` - 记录文件变更
|
||||
- `acceptChange()` - 采纳变更(保存文件)
|
||||
- `rejectChange()` - 拒绝变更(恢复旧内容)
|
||||
|
||||
### 3. UI 组件
|
||||
- `src/views/changePanel.ts` - 变更面板 UI
|
||||
- `src/utils/diffRenderer.ts` - Diff 可视化渲染
|
||||
|
||||
### 4. 集成点
|
||||
- `src/utils/messageHandler.ts` - 消息处理
|
||||
- `trackFileChange()` - 记录变更
|
||||
- `handleAcceptChange()` - 处理采纳
|
||||
- `handleRejectChange()` - 处理拒绝
|
||||
- `sendChangesToWebview()` - 发送变更到前端
|
||||
|
||||
- `src/services/toolExecutor.ts` - 工具执行器
|
||||
- 在 `executeFileWrite()` 中记录变更
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. **开始对话** - 调用 `startChangeSession(sessionId)`
|
||||
2. **修改文件** - 自动调用 `trackFileChange()`
|
||||
3. **对话结束** - 调用 `sendChangesToWebview()` 显示变更面板
|
||||
4. **用户操作** - 点击采纳/拒绝按钮
|
||||
5. **处理结果** - 保存或恢复文件内容
|
||||
|
||||
## 待完成工作
|
||||
|
||||
1. 在 ICHelperPanel 中集成消息处理(监听 acceptChange/rejectChange 命令)
|
||||
2. 在对话结束时调用 `sendChangesToWebview()`
|
||||
3. 在 Webview 中实现变更列表的动态渲染
|
||||
4. 处理前端的采纳/拒绝响应
|
||||
294
docs/delete-file-confirmation.md
Normal file
@ -0,0 +1,294 @@
|
||||
# 删除文件确认功能实现文档
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
在 AI 返回删除文件命令时,前端拦截并弹出确认对话框,用户确认后才执行删除操作。
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 消息流程
|
||||
|
||||
```
|
||||
AI 后端 → 删除文件工具调用 → 前端拦截 → 用户确认对话框
|
||||
↓
|
||||
确定/取消
|
||||
↓
|
||||
执行删除/返回取消结果
|
||||
↓
|
||||
返回 TOOL_EXECUTION_RESULT
|
||||
↓
|
||||
AI 后端
|
||||
```
|
||||
|
||||
### 2.2 关键原则
|
||||
|
||||
**前端必须返回结果**:无论用户选择什么,前端都必须向后端返回 `TOOL_EXECUTION_RESULT`,否则后端会等待超时。
|
||||
|
||||
## 3. 实现方案
|
||||
|
||||
### 3.1 修改位置
|
||||
|
||||
文件:`src/utils/messageHandler.ts`
|
||||
|
||||
在处理工具调用的函数中,找到删除文件的工具处理逻辑。
|
||||
|
||||
### 3.2 核心代码实现
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 处理删除文件工具调用(带用户确认)
|
||||
*/
|
||||
async function handleDeleteFileTool(
|
||||
toolCall: any,
|
||||
panel: vscode.WebviewPanel
|
||||
): Promise<ToolExecutionResult> {
|
||||
const filePath = toolCall.arguments.filePath; // 根据实际参数名调整
|
||||
|
||||
// 弹出确认对话框
|
||||
const confirmed = await vscode.window.showWarningMessage(
|
||||
`确定要删除文件吗?\n\n${filePath}`,
|
||||
{
|
||||
modal: true, // 模态对话框,阻止其他操作
|
||||
detail: '此操作不可撤销'
|
||||
},
|
||||
'确定删除',
|
||||
'取消'
|
||||
);
|
||||
|
||||
// 用户确认删除
|
||||
if (confirmed === '确定删除') {
|
||||
try {
|
||||
// 执行删除操作
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
await vscode.workspace.fs.delete(uri, {
|
||||
recursive: false, // 如果是目录需要设置为 true
|
||||
useTrash: true // 移到回收站而非永久删除(推荐)
|
||||
});
|
||||
|
||||
// 返回成功结果
|
||||
return {
|
||||
type: 'TOOL_EXECUTION_RESULT',
|
||||
toolCallId: toolCall.id,
|
||||
result: JSON.stringify({
|
||||
success: true,
|
||||
message: `文件已删除: ${filePath}`
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
// 删除失败
|
||||
return {
|
||||
type: 'TOOL_EXECUTION_RESULT',
|
||||
toolCallId: toolCall.id,
|
||||
result: JSON.stringify({
|
||||
success: false,
|
||||
error: `删除失败: ${error.message}`
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 用户取消或关闭对话框
|
||||
return {
|
||||
type: 'TOOL_EXECUTION_RESULT',
|
||||
toolCallId: toolCall.id,
|
||||
result: JSON.stringify({
|
||||
success: false,
|
||||
message: '用户取消了删除操作'
|
||||
})
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 集成到消息处理流程
|
||||
|
||||
在 `messageHandler.ts` 的工具调用处理逻辑中:
|
||||
|
||||
```typescript
|
||||
// 示例:在处理工具调用的地方
|
||||
async function handleToolCall(toolCall: any, panel: vscode.WebviewPanel) {
|
||||
switch (toolCall.name) {
|
||||
case 'deleteFile': // 根据实际工具名称调整
|
||||
return await handleDeleteFileTool(toolCall, panel);
|
||||
|
||||
case 'deleteDirectory': // 如果有删除目录的工具
|
||||
return await handleDeleteDirectoryTool(toolCall, panel);
|
||||
|
||||
// ... 其他工具
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 用户体验优化
|
||||
|
||||
### 4.1 对话框样式
|
||||
|
||||
```typescript
|
||||
const confirmed = await vscode.window.showWarningMessage(
|
||||
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
|
||||
{
|
||||
modal: true,
|
||||
detail: '⚠️ 文件将被移到回收站,可以恢复'
|
||||
},
|
||||
'确定删除',
|
||||
'取消'
|
||||
);
|
||||
```
|
||||
|
||||
### 4.2 批量删除优化
|
||||
|
||||
如果 AI 一次返回多个删除操作:
|
||||
|
||||
```typescript
|
||||
// 方案 1:逐个确认
|
||||
for (const file of filesToDelete) {
|
||||
await handleDeleteFileTool(file, panel);
|
||||
}
|
||||
|
||||
// 方案 2:批量确认(推荐)
|
||||
const confirmed = await vscode.window.showWarningMessage(
|
||||
`确定要删除以下 ${filesToDelete.length} 个文件吗?\n\n${filesToDelete.join('\n')}`,
|
||||
{ modal: true },
|
||||
'全部删除',
|
||||
'取消'
|
||||
);
|
||||
```
|
||||
|
||||
## 5. 安全考虑
|
||||
|
||||
### 5.1 使用回收站
|
||||
|
||||
```typescript
|
||||
await vscode.workspace.fs.delete(uri, {
|
||||
useTrash: true // 移到回收站,可恢复
|
||||
});
|
||||
```
|
||||
|
||||
### 5.2 路径验证
|
||||
|
||||
```typescript
|
||||
// 防止删除工作区外的文件
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders) {
|
||||
return { success: false, error: '未打开工作区' };
|
||||
}
|
||||
|
||||
const isInWorkspace = workspaceFolders.some(folder =>
|
||||
filePath.startsWith(folder.uri.fsPath)
|
||||
);
|
||||
|
||||
if (!isInWorkspace) {
|
||||
return { success: false, error: '只能删除工作区内的文件' };
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 敏感文件保护
|
||||
|
||||
```typescript
|
||||
const protectedFiles = [
|
||||
'package.json',
|
||||
'tsconfig.json',
|
||||
'.git',
|
||||
'node_modules'
|
||||
];
|
||||
|
||||
const fileName = path.basename(filePath);
|
||||
if (protectedFiles.includes(fileName)) {
|
||||
vscode.window.showErrorMessage(`不允许删除系统文件: ${fileName}`);
|
||||
return { success: false, error: '受保护的文件' };
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 错误处理
|
||||
|
||||
### 6.1 常见错误
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await vscode.workspace.fs.delete(uri, { useTrash: true });
|
||||
} catch (error) {
|
||||
if (error.code === 'FileNotFound') {
|
||||
return { success: false, error: '文件不存在' };
|
||||
}
|
||||
if (error.code === 'NoPermissions') {
|
||||
return { success: false, error: '没有删除权限' };
|
||||
}
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 测试场景
|
||||
|
||||
### 7.1 基本测试
|
||||
|
||||
- [ ] 用户点击"确定删除" → 文件被删除
|
||||
- [ ] 用户点击"取消" → 文件保留,返回取消消息
|
||||
- [ ] 用户关闭对话框 → 文件保留,返回取消消息
|
||||
- [ ] 文件不存在 → 返回错误消息
|
||||
- [ ] 没有删除权限 → 返回错误消息
|
||||
|
||||
### 7.2 边界测试
|
||||
|
||||
- [ ] 删除工作区外的文件 → 拒绝
|
||||
- [ ] 删除受保护文件 → 拒绝
|
||||
- [ ] 批量删除 → 正确处理
|
||||
- [ ] 后端收到取消消息后继续对话 → 流程正常
|
||||
|
||||
## 8. 配置选项(可选)
|
||||
|
||||
可以添加用户设置来控制行为:
|
||||
|
||||
```json
|
||||
// package.json
|
||||
"configuration": {
|
||||
"properties": {
|
||||
"ic-coder.confirmDelete": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "删除文件前是否需要确认"
|
||||
},
|
||||
"ic-coder.useTrash": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "删除文件时移到回收站而非永久删除"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
读取配置:
|
||||
|
||||
```typescript
|
||||
const config = vscode.workspace.getConfiguration('ic-coder');
|
||||
const needConfirm = config.get<boolean>('confirmDelete', true);
|
||||
const useTrash = config.get<boolean>('useTrash', true);
|
||||
|
||||
if (needConfirm) {
|
||||
// 弹出确认对话框
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 总结
|
||||
|
||||
### 9.1 后端是否需要修改?
|
||||
|
||||
**不需要**。后端继续返回删除工具调用,前端负责:
|
||||
1. 拦截工具调用
|
||||
2. 弹出确认对话框
|
||||
3. 执行或取消删除
|
||||
4. **必须返回结果给后端**
|
||||
|
||||
### 9.2 关键要点
|
||||
|
||||
- ✅ 前端必须返回 `TOOL_EXECUTION_RESULT`
|
||||
- ✅ 使用 `useTrash: true` 提高安全性
|
||||
- ✅ 验证文件路径在工作区内
|
||||
- ✅ 保护敏感文件
|
||||
- ✅ 提供清晰的错误消息
|
||||
|
||||
### 9.3 下一步
|
||||
|
||||
1. 在 `messageHandler.ts` 中找到工具调用处理逻辑
|
||||
2. 实现 `handleDeleteFileTool` 函数
|
||||
3. 集成到现有流程
|
||||
4. 测试各种场景
|
||||
5. 考虑添加用户配置选项
|
||||
50
docs/integration-guide.md
Normal file
@ -0,0 +1,50 @@
|
||||
# 代码变更审查功能 - 使用说明
|
||||
|
||||
## 功能说明
|
||||
|
||||
AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以:
|
||||
- 查看所有修改的文件列表
|
||||
- 点击文件查看 diff 对比
|
||||
- 采纳变更(保存文件)
|
||||
- 拒绝变更(恢复旧内容)
|
||||
|
||||
## 已完成的集成
|
||||
|
||||
### 1. 后端集成
|
||||
- ✅ 在 `ICHelperPanel.ts` 中添加了消息监听(acceptChange/rejectChange)
|
||||
- ✅ 在发送消息时启动变更追踪会话
|
||||
- ✅ 在文件操作时自动记录变更(messageHandler.ts、toolExecutor.ts)
|
||||
|
||||
### 2. 前端集成
|
||||
- ✅ 在 `webviewContent.ts` 中添加了消息处理(showChanges/changeAccepted/changeRejected)
|
||||
- ✅ 在 `changePanel.ts` 中实现了完整的 UI 交互逻辑
|
||||
|
||||
### 3. 核心功能
|
||||
- ✅ 变更追踪服务(changeTracker.ts)
|
||||
- ✅ Diff 可视化渲染(diffRenderer.ts)
|
||||
- ✅ 采纳/拒绝变更逻辑
|
||||
|
||||
## 待完成工作
|
||||
|
||||
需要在对话结束时调用 `sendChangesToWebview(panel)` 来显示变更面板。
|
||||
|
||||
建议在以下位置添加:
|
||||
1. 在 `handleUserMessage` 函数中,对话流结束时
|
||||
2. 或在 `dialogManager` 的对话完成回调中
|
||||
|
||||
示例代码:
|
||||
```typescript
|
||||
// 对话结束时
|
||||
import { sendChangesToWebview } from '../utils/messageHandler';
|
||||
|
||||
// 在对话完成的地方调用
|
||||
sendChangesToWebview(panel);
|
||||
```
|
||||
|
||||
## 测试步骤
|
||||
|
||||
1. 启动插件(F5)
|
||||
2. 发送消息让 AI 修改文件
|
||||
3. 对话结束后,输入框上方应显示"代码变更"面板
|
||||
4. 点击文件查看 diff
|
||||
5. 点击"采纳"或"拒绝"按钮测试功能
|
||||
739
docs/invitation-code-design.md
Normal file
@ -0,0 +1,739 @@
|
||||
# 邀请码验证功能设计方案
|
||||
|
||||
## 一、整体流程
|
||||
|
||||
```
|
||||
用户首次使用 → 检查邀请码状态 → 未验证则弹窗输入 → 后端验证 → 验证通过后可正常对话
|
||||
```
|
||||
|
||||
## 二、前端设计
|
||||
|
||||
### 2.1 邀请码状态管理
|
||||
|
||||
在 `ExtensionContext.globalState` 中存储邀请码验证状态:
|
||||
|
||||
```typescript
|
||||
// 存储结构
|
||||
{
|
||||
"invitationCodeVerified": true,
|
||||
"invitationCode": "INVITE2024ABC",
|
||||
"verifiedTime": "2024-01-20T10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 UI 交互流程
|
||||
|
||||
#### 弹窗输入邀请码
|
||||
使用 `vscode.window.showInputBox` 实现:
|
||||
|
||||
```typescript
|
||||
const invitationCode = await vscode.window.showInputBox({
|
||||
prompt: '请输入邀请码以继续使用 IC Coder',
|
||||
placeHolder: '例如:INVITE2024ABC',
|
||||
ignoreFocusOut: true,
|
||||
validateInput: (value) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
return '邀请码不能为空';
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return '邀请码格式不正确';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 验证结果提示
|
||||
- 成功:`vscode.window.showInformationMessage('邀请码验证成功!')`
|
||||
- 失败:`vscode.window.showErrorMessage('邀请码无效或已过期,请重新输入')`
|
||||
|
||||
### 2.3 验证时机
|
||||
|
||||
在以下场景触发邀请码验证:
|
||||
|
||||
1. **用户首次发送消息时**(在 `handleUserMessage` 中检查)
|
||||
2. **用户登录后**(在登录成功回调中检查)
|
||||
3. **Token 过期重新登录后**
|
||||
|
||||
### 2.4 前端验证流程图
|
||||
|
||||
```
|
||||
发送消息前检查
|
||||
├─ 检查是否已登录
|
||||
│ └─ 未登录 → 提示登录
|
||||
├─ 检查邀请码是否已验证
|
||||
│ ├─ 未验证
|
||||
│ │ ├─ 弹窗输入邀请码
|
||||
│ │ ├─ 调用后端验证接口 POST /api/invitation/verify
|
||||
│ │ ├─ 验证成功
|
||||
│ │ │ ├─ 保存验证状态到 globalState
|
||||
│ │ │ └─ 继续发送消息
|
||||
│ │ └─ 验证失败
|
||||
│ │ ├─ 显示错误提示
|
||||
│ │ └─ 阻止发送消息
|
||||
│ └─ 已验证 → 继续发送消息
|
||||
```
|
||||
|
||||
### 2.5 前端文件修改清单
|
||||
|
||||
#### 新增文件
|
||||
|
||||
**`src/services/invitationService.ts`** - 邀请码服务
|
||||
```typescript
|
||||
/**
|
||||
* 邀请码验证服务
|
||||
*/
|
||||
export class InvitationService {
|
||||
/**
|
||||
* 检查用户是否已验证邀请码
|
||||
*/
|
||||
static async isVerified(context: vscode.ExtensionContext): Promise<boolean>
|
||||
|
||||
/**
|
||||
* 验证邀请码
|
||||
*/
|
||||
static async verifyCode(code: string): Promise<boolean>
|
||||
|
||||
/**
|
||||
* 保存验证状态
|
||||
*/
|
||||
static async saveVerificationStatus(
|
||||
context: vscode.ExtensionContext,
|
||||
code: string
|
||||
): Promise<void>
|
||||
|
||||
/**
|
||||
* 清除验证状态(用于退出登录)
|
||||
*/
|
||||
static async clearVerificationStatus(
|
||||
context: vscode.ExtensionContext
|
||||
): Promise<void>
|
||||
|
||||
/**
|
||||
* 显示邀请码输入弹窗
|
||||
*/
|
||||
static async showInputDialog(): Promise<string | undefined>
|
||||
}
|
||||
```
|
||||
|
||||
#### 修改文件
|
||||
|
||||
**`src/utils/messageHandler.ts`**
|
||||
- 在 `handleUserMessage` 函数开头添加邀请码验证检查
|
||||
|
||||
**`src/services/apiClient.ts`**
|
||||
- 添加 `verifyInvitationCode` 函数
|
||||
- 添加 `checkInvitationStatus` 函数
|
||||
|
||||
**`src/types/api.ts`**
|
||||
- 添加邀请码相关类型定义
|
||||
|
||||
**`src/panels/ICHelperPanel.ts`**
|
||||
- 在面板创建时检查邀请码状态(可选)
|
||||
|
||||
**`src/extension.ts`**
|
||||
- 在登录成功后检查邀请码状态
|
||||
|
||||
### 2.6 类型定义
|
||||
|
||||
在 `src/types/api.ts` 中添加:
|
||||
|
||||
```typescript
|
||||
// ============== 邀请码验证 ==============
|
||||
|
||||
/**
|
||||
* 邀请码验证请求
|
||||
* POST /api/invitation/verify
|
||||
*/
|
||||
export interface InvitationVerifyRequest {
|
||||
/** 邀请码 */
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请码验证响应
|
||||
*/
|
||||
export interface InvitationVerifyResponse {
|
||||
/** 响应代码 */
|
||||
code: number;
|
||||
/** 响应消息 */
|
||||
msg: string;
|
||||
/** 验证结果数据 */
|
||||
data?: {
|
||||
/** 是否验证成功 */
|
||||
verified: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请码状态响应
|
||||
* GET /api/invitation/status
|
||||
*/
|
||||
export interface InvitationStatusResponse {
|
||||
/** 响应代码 */
|
||||
code: number;
|
||||
/** 响应消息 */
|
||||
msg?: string;
|
||||
/** 状态数据 */
|
||||
data?: {
|
||||
/** 是否已验证 */
|
||||
verified: boolean;
|
||||
/** 使用的邀请码 */
|
||||
invitationCode?: string;
|
||||
/** 验证时间 */
|
||||
verifiedTime?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 三、后端设计
|
||||
|
||||
### 3.1 数据库设计
|
||||
|
||||
#### 邀请码表 (invitation_codes)
|
||||
|
||||
```sql
|
||||
CREATE TABLE invitation_codes (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||
code VARCHAR(32) UNIQUE NOT NULL COMMENT '邀请码',
|
||||
max_uses INT DEFAULT 1 COMMENT '最大使用次数,-1表示无限制',
|
||||
used_count INT DEFAULT 0 COMMENT '已使用次数',
|
||||
expire_time DATETIME COMMENT '过期时间,NULL表示永不过期',
|
||||
created_by BIGINT COMMENT '创建者用户ID',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-有效,0-禁用',
|
||||
remark VARCHAR(500) COMMENT '备注',
|
||||
INDEX idx_code (code),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_expire_time (expire_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请码表';
|
||||
```
|
||||
|
||||
#### 用户邀请码关联表 (user_invitation)
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_invitation (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
invitation_code VARCHAR(32) NOT NULL COMMENT '使用的邀请码',
|
||||
verified_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '验证时间',
|
||||
ip_address VARCHAR(50) COMMENT '验证时的IP地址',
|
||||
UNIQUE KEY uk_user_id (user_id),
|
||||
INDEX idx_invitation_code (invitation_code),
|
||||
INDEX idx_verified_time (verified_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户邀请码关联表';
|
||||
```
|
||||
|
||||
### 3.2 API 接口设计
|
||||
|
||||
#### 3.2.1 验证邀请码
|
||||
|
||||
**接口地址**:`POST /api/invitation/verify`
|
||||
|
||||
**请求头**:
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"code": "INVITE2024ABC"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
成功:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "验证成功",
|
||||
"data": {
|
||||
"verified": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"msg": "邀请码无效或已过期"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"msg": "邀请码使用次数已达上限"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"msg": "您已验证过邀请码,无需重复验证"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 查询验证状态
|
||||
|
||||
**接口地址**:`GET /api/invitation/status`
|
||||
|
||||
**请求头**:
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
已验证:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"verified": true,
|
||||
"invitationCode": "INVITE2024ABC",
|
||||
"verifiedTime": "2024-01-20T10:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
未验证:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"verified": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.3 管理接口(管理员使用)
|
||||
|
||||
**生成邀请码**:`POST /api/admin/invitation/generate`
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"count": 10,
|
||||
"maxUses": 1,
|
||||
"expireTime": "2024-12-31T23:59:59",
|
||||
"remark": "2024年1月批次"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "生成成功",
|
||||
"data": {
|
||||
"codes": [
|
||||
"INVITE2024001",
|
||||
"INVITE2024002",
|
||||
"..."
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**查询邀请码列表**:`GET /api/admin/invitation/list`
|
||||
|
||||
**禁用邀请码**:`PUT /api/admin/invitation/disable/{code}`
|
||||
|
||||
**查询使用记录**:`GET /api/admin/invitation/usage/{code}`
|
||||
|
||||
### 3.3 后端验证逻辑
|
||||
|
||||
#### 验证流程
|
||||
|
||||
```java
|
||||
public boolean verifyInvitationCode(Long userId, String code) {
|
||||
// 1. 检查邀请码是否存在
|
||||
InvitationCode invitationCode = invitationCodeMapper.selectByCode(code);
|
||||
if (invitationCode == null) {
|
||||
throw new BusinessException("邀请码不存在");
|
||||
}
|
||||
|
||||
// 2. 检查邀请码状态
|
||||
if (invitationCode.getStatus() != 1) {
|
||||
throw new BusinessException("邀请码已被禁用");
|
||||
}
|
||||
|
||||
// 3. 检查是否过期
|
||||
if (invitationCode.getExpireTime() != null
|
||||
&& invitationCode.getExpireTime().before(new Date())) {
|
||||
throw new BusinessException("邀请码已过期");
|
||||
}
|
||||
|
||||
// 4. 检查使用次数
|
||||
if (invitationCode.getMaxUses() != -1
|
||||
&& invitationCode.getUsedCount() >= invitationCode.getMaxUses()) {
|
||||
throw new BusinessException("邀请码使用次数已达上限");
|
||||
}
|
||||
|
||||
// 5. 检查用户是否已验证过
|
||||
UserInvitation existing = userInvitationMapper.selectByUserId(userId);
|
||||
if (existing != null) {
|
||||
throw new BusinessException("您已验证过邀请码,无需重复验证");
|
||||
}
|
||||
|
||||
// 6. 创建用户验证记录
|
||||
UserInvitation userInvitation = new UserInvitation();
|
||||
userInvitation.setUserId(userId);
|
||||
userInvitation.setInvitationCode(code);
|
||||
userInvitation.setVerifiedTime(new Date());
|
||||
userInvitationMapper.insert(userInvitation);
|
||||
|
||||
// 7. 增加邀请码使用次数
|
||||
invitationCodeMapper.incrementUsedCount(code);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 权限拦截
|
||||
|
||||
在对话接口中添加邀请码验证拦截:
|
||||
|
||||
```java
|
||||
@PostMapping("/dialog/stream")
|
||||
public SseEmitter dialog(@RequestBody DialogRequest request) {
|
||||
// 获取当前用户ID
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
|
||||
// 检查用户是否已验证邀请码
|
||||
if (!invitationService.isUserVerified(userId)) {
|
||||
throw new BusinessException("请先验证邀请码后再使用对话功能");
|
||||
}
|
||||
|
||||
// 继续处理对话请求
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
或者使用拦截器统一处理:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class InvitationInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private InvitationService invitationService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
// 获取当前用户ID
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
|
||||
// 检查是否需要验证邀请码的接口
|
||||
String uri = request.getRequestURI();
|
||||
if (needsInvitationVerification(uri)) {
|
||||
if (!invitationService.isUserVerified(userId)) {
|
||||
throw new BusinessException("请先验证邀请码");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean needsInvitationVerification(String uri) {
|
||||
// 需要验证邀请码的接口列表
|
||||
return uri.startsWith("/api/dialog/")
|
||||
|| uri.startsWith("/api/task/");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 后端文件清单
|
||||
|
||||
#### 实体类
|
||||
- `com.iccoder.entity.InvitationCode` - 邀请码实体
|
||||
- `com.iccoder.entity.UserInvitation` - 用户邀请码关联实体
|
||||
|
||||
#### Mapper
|
||||
- `com.iccoder.mapper.InvitationCodeMapper` - 邀请码数据访问
|
||||
- `com.iccoder.mapper.UserInvitationMapper` - 用户邀请码关联数据访问
|
||||
|
||||
#### Service
|
||||
- `com.iccoder.service.InvitationService` - 邀请码业务逻辑
|
||||
- `com.iccoder.service.impl.InvitationServiceImpl` - 实现类
|
||||
|
||||
#### Controller
|
||||
- `com.iccoder.controller.InvitationController` - 邀请码接口
|
||||
- `com.iccoder.controller.admin.InvitationAdminController` - 管理接口
|
||||
|
||||
#### 拦截器
|
||||
- `com.iccoder.interceptor.InvitationInterceptor` - 邀请码验证拦截器
|
||||
|
||||
## 四、用户体验优化
|
||||
|
||||
### 4.1 首次使用引导
|
||||
|
||||
在用户首次打开聊天面板时,如果未验证邀请码,显示友好的引导信息:
|
||||
|
||||
```typescript
|
||||
// 在 ICHelperPanel.ts 中
|
||||
if (!await InvitationService.isVerified(context)) {
|
||||
panel.webview.postMessage({
|
||||
command: 'showInvitationGuide',
|
||||
message: '欢迎使用 IC Coder!请先输入邀请码以开始使用。'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 状态持久化
|
||||
|
||||
验证状态保存在 `globalState` 中,避免重复验证:
|
||||
|
||||
```typescript
|
||||
// 保存验证状态
|
||||
await context.globalState.update('invitationCodeVerified', true);
|
||||
await context.globalState.update('invitationCode', code);
|
||||
await context.globalState.update('verifiedTime', new Date().toISOString());
|
||||
```
|
||||
|
||||
### 4.3 错误提示优化
|
||||
|
||||
根据不同的错误类型,提供清晰的提示信息:
|
||||
|
||||
| 错误类型 | 提示信息 |
|
||||
|---------|---------|
|
||||
| 邀请码不存在 | "邀请码不存在,请检查后重新输入" |
|
||||
| 邀请码已过期 | "邀请码已过期,请联系管理员获取新的邀请码" |
|
||||
| 使用次数已达上限 | "该邀请码使用次数已达上限,请使用其他邀请码" |
|
||||
| 已验证过 | "您已验证过邀请码,无需重复验证" |
|
||||
| 网络错误 | "网络连接失败,请检查网络后重试" |
|
||||
|
||||
### 4.4 支持重新验证
|
||||
|
||||
提供命令允许用户更换邀请码:
|
||||
|
||||
```typescript
|
||||
// 在 extension.ts 中注册命令
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('ic-coder.changeInvitationCode', async () => {
|
||||
const confirm = await vscode.window.showWarningMessage(
|
||||
'确定要更换邀请码吗?',
|
||||
'确定',
|
||||
'取消'
|
||||
);
|
||||
|
||||
if (confirm === '确定') {
|
||||
await InvitationService.clearVerificationStatus(context);
|
||||
vscode.window.showInformationMessage('已清除邀请码,请重新验证');
|
||||
}
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### 4.5 Webview 中显示状态(可选)
|
||||
|
||||
在聊天界面顶部显示邀请码验证状态:
|
||||
|
||||
```html
|
||||
<!-- 已验证 -->
|
||||
<div class="invitation-status verified">
|
||||
<span class="icon">✓</span>
|
||||
<span>邀请码已验证</span>
|
||||
</div>
|
||||
|
||||
<!-- 未验证 -->
|
||||
<div class="invitation-status unverified">
|
||||
<span class="icon">!</span>
|
||||
<span>请先验证邀请码</span>
|
||||
<button onclick="verifyInvitationCode()">立即验证</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 五、安全考虑
|
||||
|
||||
### 5.1 邀请码生成规则
|
||||
|
||||
使用安全的随机算法生成邀请码:
|
||||
|
||||
```java
|
||||
public String generateInvitationCode() {
|
||||
// 使用 UUID + 时间戳 + 随机数
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String random = RandomStringUtils.randomAlphanumeric(6);
|
||||
|
||||
// 组合并取前16位
|
||||
String combined = uuid + timestamp + random;
|
||||
String code = DigestUtils.sha256Hex(combined).substring(0, 16).toUpperCase();
|
||||
|
||||
return "IC" + code; // 添加前缀,例如:IC3F2A9B1C4D5E6F
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 防暴力破解
|
||||
|
||||
限制验证频率,添加验证失败次数限制:
|
||||
|
||||
```java
|
||||
// 使用 Redis 记录验证失败次数
|
||||
String key = "invitation:fail:" + userId;
|
||||
Integer failCount = redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (failCount != null && failCount >= 5) {
|
||||
throw new BusinessException("验证失败次数过多,请1小时后再试");
|
||||
}
|
||||
|
||||
// 验证失败时增加计数
|
||||
if (!verifySuccess) {
|
||||
redisTemplate.opsForValue().increment(key);
|
||||
redisTemplate.expire(key, 1, TimeUnit.HOURS);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Token 绑定
|
||||
|
||||
邀请码验证状态与用户 Token 绑定,退出登录时清除:
|
||||
|
||||
```typescript
|
||||
// 在退出登录时清除验证状态
|
||||
vscode.commands.registerCommand('ic-coder.logout', async () => {
|
||||
// 清除 session
|
||||
await clearSession();
|
||||
|
||||
// 清除邀请码验证状态
|
||||
await InvitationService.clearVerificationStatus(context);
|
||||
|
||||
vscode.window.showInformationMessage('已退出登录');
|
||||
});
|
||||
```
|
||||
|
||||
### 5.4 日志记录
|
||||
|
||||
记录所有验证尝试,便于审计和分析:
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
public class InvitationServiceImpl implements InvitationService {
|
||||
|
||||
@Override
|
||||
public boolean verifyInvitationCode(Long userId, String code) {
|
||||
log.info("用户 {} 尝试验证邀请码: {}", userId, code);
|
||||
|
||||
try {
|
||||
// 验证逻辑
|
||||
// ...
|
||||
|
||||
log.info("用户 {} 验证邀请码成功: {}", userId, code);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("用户 {} 验证邀请码失败: {}, 原因: {}",
|
||||
userId, code, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 敏感信息保护
|
||||
|
||||
- 邀请码在数据库中可以考虑加密存储(可选)
|
||||
- API 响应中不暴露邀请码的详细信息(如剩余次数)
|
||||
- 前端不缓存邀请码明文,只保存验证状态
|
||||
|
||||
## 六、实施步骤
|
||||
|
||||
### 阶段一:后端开发(优先)
|
||||
|
||||
1. 创建数据库表
|
||||
2. 实现邀请码生成和管理功能
|
||||
3. 实现验证接口
|
||||
4. 添加权限拦截
|
||||
5. 测试接口功能
|
||||
|
||||
### 阶段二:前端开发
|
||||
|
||||
1. 添加类型定义
|
||||
2. 实现 `InvitationService`
|
||||
3. 修改 `apiClient.ts` 添加接口调用
|
||||
4. 修改 `messageHandler.ts` 添加验证检查
|
||||
5. 测试完整流程
|
||||
|
||||
### 阶段三:联调测试
|
||||
|
||||
1. 前后端联调
|
||||
2. 测试各种异常场景
|
||||
3. 优化用户体验
|
||||
4. 性能测试
|
||||
|
||||
### 阶段四:上线部署
|
||||
|
||||
1. 生成初始邀请码
|
||||
2. 更新用户文档
|
||||
3. 灰度发布
|
||||
4. 监控运行状态
|
||||
|
||||
## 七、测试用例
|
||||
|
||||
### 7.1 正常流程测试
|
||||
|
||||
| 测试场景 | 预期结果 |
|
||||
|---------|---------|
|
||||
| 首次使用,输入有效邀请码 | 验证成功,可以正常对话 |
|
||||
| 已验证用户再次打开面板 | 无需重复验证,直接使用 |
|
||||
| 退出登录后重新登录 | 需要重新验证邀请码 |
|
||||
|
||||
### 7.2 异常场景测试
|
||||
|
||||
| 测试场景 | 预期结果 |
|
||||
|---------|---------|
|
||||
| 输入不存在的邀请码 | 提示"邀请码不存在" |
|
||||
| 输入已过期的邀请码 | 提示"邀请码已过期" |
|
||||
| 输入使用次数已满的邀请码 | 提示"使用次数已达上限" |
|
||||
| 已验证用户尝试再次验证 | 提示"已验证过,无需重复验证" |
|
||||
| 网络断开时验证 | 提示"网络连接失败" |
|
||||
| 连续输入错误邀请码5次 | 提示"验证失败次数过多,请稍后再试" |
|
||||
|
||||
### 7.3 边界条件测试
|
||||
|
||||
| 测试场景 | 预期结果 |
|
||||
|---------|---------|
|
||||
| 邀请码为空 | 前端验证拦截,提示"邀请码不能为空" |
|
||||
| 邀请码长度不足 | 前端验证拦截,提示"邀请码格式不正确" |
|
||||
| 邀请码包含特殊字符 | 后端验证失败,提示"邀请码不存在" |
|
||||
| 同一邀请码多人同时使用 | 使用数据库锁,确保不超过最大次数 |
|
||||
|
||||
## 八、FAQ
|
||||
|
||||
### Q1: 用户忘记邀请码怎么办?
|
||||
A: 邀请码验证成功后,用户无需记住邀请码。如果需要查看,可以在设置中显示已验证的邀请码。
|
||||
|
||||
### Q2: 邀请码可以重复使用吗?
|
||||
A: 取决于邀请码的 `maxUses` 设置。可以设置为 1(一次性)、N(限定次数)或 -1(无限制)。
|
||||
|
||||
### Q3: 如何批量生成邀请码?
|
||||
A: 使用管理接口 `POST /api/admin/invitation/generate`,指定生成数量即可。
|
||||
|
||||
### Q4: 邀请码验证失败会影响登录吗?
|
||||
A: 不会。邀请码验证是独立的,只影响对话功能的使用,不影响登录。
|
||||
|
||||
### Q5: 可以为不同用户群体设置不同的邀请码吗?
|
||||
A: 可以。通过 `remark` 字段标记不同批次的邀请码,便于管理和统计。
|
||||
|
||||
## 九、后续优化方向
|
||||
|
||||
1. **邀请码分级**:不同等级的邀请码对应不同的权限(如对话次数、模型选择等)
|
||||
2. **邀请奖励**:邀请他人使用可获得积分或额外权限
|
||||
3. **邀请统计**:统计每个邀请码的使用情况和用户活跃度
|
||||
4. **自动过期**:根据使用情况自动延长或缩短邀请码有效期
|
||||
5. **白名单机制**:特定用户可以免邀请码使用
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2026-01-27
|
||||
**维护者**:IC Coder Team
|
||||
161
docs/personal-rules-mvp-requirements.md
Normal 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`),多端一致。
|
||||
911
docs/system-notification-implementation.md
Normal file
@ -0,0 +1,911 @@
|
||||
# IC Coder 系统通知功能实现方案
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 需求背景](#1-需求背景)
|
||||
- [2. 技术方案对比](#2-技术方案对比)
|
||||
- [3. 推荐方案详解](#3-推荐方案详解)
|
||||
- [4. 实现步骤](#4-实现步骤)
|
||||
- [5. API 设计](#5-api-设计)
|
||||
- [6. 配置选项](#6-配置选项)
|
||||
- [7. 测试方案](#7-测试方案)
|
||||
- [8. 注意事项](#8-注意事项)
|
||||
- [9. 常见问题](#9-常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求背景
|
||||
|
||||
### 1.1 问题描述
|
||||
|
||||
当前 IC Coder 插件使用 VS Code 内置的通知 API (`vscode.window.showInformationMessage`) 来提示用户任务完成。这种方式存在以下问题:
|
||||
|
||||
- **可见性问题**: 用户切换到其他应用时,无法看到 VS Code 内部的通知
|
||||
- **错过通知**: 长时间运行的任务(如 iverilog 仿真)完成时,用户可能已经离开 VS Code
|
||||
- **用户体验**: 需要用户主动回到 VS Code 才能知道任务状态
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
实现系统级通知功能,使得:
|
||||
|
||||
1. 用户在任何应用中都能收到任务完成通知
|
||||
2. 通知显示在操作系统的通知中心(Windows Action Center / macOS Notification Center / Linux notify-send)
|
||||
3. 支持自定义通知内容、图标、声音
|
||||
4. 用户可以配置是否启用系统通知
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术方案对比
|
||||
|
||||
### 2.1 方案一:node-notifier(推荐)
|
||||
|
||||
**描述**: 使用 `node-notifier` 库,封装了各平台的原生通知 API
|
||||
|
||||
**优点**:
|
||||
- ✅ 跨平台支持(Windows/macOS/Linux)
|
||||
- ✅ API 简单易用
|
||||
- ✅ 支持自定义图标、声音、操作按钮
|
||||
- ✅ 活跃维护,社区支持良好
|
||||
- ✅ 支持通知点击回调
|
||||
|
||||
**缺点**:
|
||||
- ❌ 需要添加额外依赖(~500KB)
|
||||
- ❌ 首次使用需要用户授权
|
||||
|
||||
**适用场景**: 需要跨平台支持的生产环境
|
||||
|
||||
---
|
||||
|
||||
### 2.2 方案二:Windows PowerShell Toast 通知
|
||||
|
||||
**描述**: 使用 PowerShell 脚本调用 Windows 10/11 的 Toast 通知 API
|
||||
|
||||
**优点**:
|
||||
- ✅ 无需额外依赖
|
||||
- ✅ 支持丰富的 Toast 样式(按钮、输入框等)
|
||||
- ✅ 与 Windows 系统深度集成
|
||||
|
||||
**缺点**:
|
||||
- ❌ 仅支持 Windows 10/11
|
||||
- ❌ 需要执行 PowerShell 脚本,可能有安全限制
|
||||
- ❌ 实现复杂度较高
|
||||
|
||||
**适用场景**: 仅针对 Windows 平台的专用功能
|
||||
|
||||
---
|
||||
|
||||
### 2.3 方案三:Electron Notification API
|
||||
|
||||
**描述**: 使用 Electron 的 `Notification` API(VS Code 基于 Electron)
|
||||
|
||||
**优点**:
|
||||
- ✅ 无需额外依赖
|
||||
- ✅ 跨平台支持
|
||||
- ✅ API 简洁
|
||||
|
||||
**缺点**:
|
||||
- ❌ VS Code 扩展 API 未直接暴露 Electron API
|
||||
- ❌ 需要通过 `@vscode/webview-ui-toolkit` 或其他方式间接调用
|
||||
- ❌ 可能存在兼容性问题
|
||||
|
||||
**适用场景**: 理论可行,但实际受限于 VS Code 扩展沙箱
|
||||
|
||||
---
|
||||
|
||||
### 2.4 方案四:结合 VS Code 通知 + 系统通知
|
||||
|
||||
**描述**: 同时使用 VS Code 内置通知和系统通知
|
||||
|
||||
**优点**:
|
||||
- ✅ 双重保障,覆盖所有场景
|
||||
- ✅ 用户在 VS Code 内外都能看到
|
||||
|
||||
**缺点**:
|
||||
- ❌ 可能显得冗余
|
||||
- ❌ 需要处理两种通知的协调逻辑
|
||||
|
||||
**适用场景**: 对通知可靠性要求极高的场景
|
||||
|
||||
---
|
||||
|
||||
### 2.5 方案对比表
|
||||
|
||||
| 方案 | 跨平台 | 依赖大小 | 实现难度 | 用户体验 | 推荐度 |
|
||||
|------|--------|----------|----------|----------|--------|
|
||||
| node-notifier | ✅ | ~500KB | ⭐ 低 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| PowerShell Toast | ❌ Windows Only | 0 | ⭐⭐⭐ 高 | ⭐⭐⭐⭐ | ⭐⭐ |
|
||||
| Electron API | ✅ | 0 | ⭐⭐⭐⭐ 很高 | ⭐⭐⭐ | ⭐ |
|
||||
| 双重通知 | ✅ | ~500KB | ⭐⭐ 中 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 3. 推荐方案详解
|
||||
|
||||
### 3.1 选择 node-notifier 的理由
|
||||
|
||||
1. **成熟稳定**: 被广泛使用(npm 周下载量 > 200 万)
|
||||
2. **跨平台**: 自动适配不同操作系统的通知机制
|
||||
3. **功能丰富**: 支持图标、声音、操作按钮、回调
|
||||
4. **易于集成**: 与 VS Code 扩展开发无缝集成
|
||||
|
||||
### 3.2 node-notifier 工作原理
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ IC Coder Extension │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ notificationService.ts │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ sendSystemNotification(title, message, options) │ │ │
|
||||
│ │ └──────────────────┬───────────────────────────────┘ │ │
|
||||
│ └────────────────────┼──────────────────────────────────┘ │
|
||||
└────────────────────────┼─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ node-notifier │
|
||||
│ (跨平台适配层) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Windows │ │ macOS │ │ Linux │
|
||||
│ Toast │ │ NSUser │ │ notify- │
|
||||
│ Notif. │ │ Notif. │ │ send │
|
||||
└─────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### 3.3 各平台通知效果
|
||||
|
||||
#### Windows 10/11
|
||||
- 显示在右下角 Action Center
|
||||
- 支持应用图标、标题、消息、操作按钮
|
||||
- 可以播放系统声音
|
||||
- 通知历史保存在通知中心
|
||||
|
||||
#### macOS
|
||||
- 显示在右上角 Notification Center
|
||||
- 支持应用图标、标题、副标题、消息
|
||||
- 可以播放系统声音
|
||||
- 支持回复和操作按钮
|
||||
|
||||
#### Linux
|
||||
- 使用 `notify-send` 或 `libnotify`
|
||||
- 显示位置取决于桌面环境(GNOME/KDE/XFCE)
|
||||
- 支持图标、标题、消息、紧急程度
|
||||
|
||||
---
|
||||
|
||||
## 4. 实现步骤
|
||||
|
||||
### 4.1 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装 node-notifier
|
||||
pnpm add node-notifier
|
||||
|
||||
# 安装类型定义
|
||||
pnpm add -D @types/node-notifier
|
||||
```
|
||||
|
||||
### 4.2 创建通知服务模块
|
||||
|
||||
创建 `src/services/notificationService.ts`
|
||||
|
||||
```typescript
|
||||
import * as notifier from 'node-notifier';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* 通知类型枚举
|
||||
*/
|
||||
export enum NotificationType {
|
||||
INFO = 'info',
|
||||
SUCCESS = 'success',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知选项接口
|
||||
*/
|
||||
export interface NotificationOptions {
|
||||
/** 通知标题 */
|
||||
title: string;
|
||||
/** 通知消息 */
|
||||
message: string;
|
||||
/** 通知类型 */
|
||||
type?: NotificationType;
|
||||
/** 是否播放声音 */
|
||||
sound?: boolean;
|
||||
/** 超时时间(秒),0 表示不自动消失 */
|
||||
timeout?: number;
|
||||
/** 自定义图标路径 */
|
||||
icon?: string;
|
||||
/** 点击通知时的回调 */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统通知服务类
|
||||
*/
|
||||
export class NotificationService {
|
||||
private static instance: NotificationService;
|
||||
private readonly extensionPath: string;
|
||||
private readonly iconPath: string;
|
||||
|
||||
private constructor(context: vscode.ExtensionContext) {
|
||||
this.extensionPath = context.extensionPath;
|
||||
this.iconPath = path.join(this.extensionPath, 'resources', 'icon.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(context?: vscode.ExtensionContext): NotificationService {
|
||||
if (!NotificationService.instance && context) {
|
||||
NotificationService.instance = new NotificationService(context);
|
||||
}
|
||||
return NotificationService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用系统通知
|
||||
*/
|
||||
private isSystemNotificationEnabled(): boolean {
|
||||
const config = vscode.workspace.getConfiguration('ic-coder');
|
||||
return config.get<boolean>('enableSystemNotification', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统通知
|
||||
*/
|
||||
public sendNotification(options: NotificationOptions): void {
|
||||
// 检查用户配置
|
||||
if (!this.isSystemNotificationEnabled()) {
|
||||
console.log('[NotificationService] 系统通知已禁用');
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
message,
|
||||
type = NotificationType.INFO,
|
||||
sound = true,
|
||||
timeout = 10,
|
||||
icon,
|
||||
onClick
|
||||
} = options;
|
||||
|
||||
// 准备通知参数
|
||||
const notificationConfig: notifier.Notification = {
|
||||
title: title,
|
||||
message: message,
|
||||
icon: icon || this.iconPath,
|
||||
sound: sound,
|
||||
wait: false,
|
||||
timeout: timeout,
|
||||
appID: 'IC Coder' // Windows 10/11 需要
|
||||
};
|
||||
|
||||
// 发送通知
|
||||
notifier.notify(notificationConfig, (err, response, metadata) => {
|
||||
if (err) {
|
||||
console.error('[NotificationService] 通知发送失败:', err);
|
||||
// 降级到 VS Code 内置通知
|
||||
this.fallbackToVSCodeNotification(title, message, type);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[NotificationService] 通知已发送:', response, metadata);
|
||||
});
|
||||
|
||||
// 监听通知点击事件
|
||||
if (onClick) {
|
||||
notifier.on('click', (notifierObject, options, event) => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 降级到 VS Code 内置通知
|
||||
*/
|
||||
private fallbackToVSCodeNotification(
|
||||
title: string,
|
||||
message: string,
|
||||
type: NotificationType
|
||||
): void {
|
||||
const fullMessage = `${title}: ${message}`;
|
||||
|
||||
switch (type) {
|
||||
case NotificationType.ERROR:
|
||||
vscode.window.showErrorMessage(fullMessage);
|
||||
break;
|
||||
case NotificationType.WARNING:
|
||||
vscode.window.showWarningMessage(fullMessage);
|
||||
break;
|
||||
case NotificationType.SUCCESS:
|
||||
case NotificationType.INFO:
|
||||
default:
|
||||
vscode.window.showInformationMessage(fullMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送成功通知
|
||||
*/
|
||||
public success(title: string, message: string, onClick?: () => void): void {
|
||||
this.sendNotification({
|
||||
title,
|
||||
message,
|
||||
type: NotificationType.SUCCESS,
|
||||
sound: true,
|
||||
timeout: 10,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误通知
|
||||
*/
|
||||
public error(title: string, message: string, onClick?: () => void): void {
|
||||
this.sendNotification({
|
||||
title,
|
||||
message,
|
||||
type: NotificationType.ERROR,
|
||||
sound: true,
|
||||
timeout: 15,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送警告通知
|
||||
*/
|
||||
public warning(title: string, message: string, onClick?: () => void): void {
|
||||
this.sendNotification({
|
||||
title,
|
||||
message,
|
||||
type: NotificationType.WARNING,
|
||||
sound: true,
|
||||
timeout: 10,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送信息通知
|
||||
*/
|
||||
public info(title: string, message: string, onClick?: () => void): void {
|
||||
this.sendNotification({
|
||||
title,
|
||||
message,
|
||||
type: NotificationType.INFO,
|
||||
sound: false,
|
||||
timeout: 8,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 在扩展入口初始化服务
|
||||
|
||||
修改 `src/extension.ts`
|
||||
|
||||
```typescript
|
||||
import { NotificationService } from './services/notificationService';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
// 初始化通知服务
|
||||
const notificationService = NotificationService.getInstance(context);
|
||||
|
||||
// ... 其他初始化代码
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 在消息处理器中使用
|
||||
|
||||
修改 `src/utils/messageHandler.ts`
|
||||
|
||||
```typescript
|
||||
import { NotificationService } from '../services/notificationService';
|
||||
|
||||
// 在适当的位置添加通知
|
||||
export async function handleMessage(message: any, panel: vscode.WebviewPanel) {
|
||||
const notificationService = NotificationService.getInstance();
|
||||
|
||||
// 示例:iverilog 仿真完成
|
||||
if (message.type === 'simulationComplete') {
|
||||
notificationService.success(
|
||||
'IC Coder - 仿真完成',
|
||||
'iverilog 仿真已成功完成,VCD 文件已生成',
|
||||
() => {
|
||||
// 点击通知时聚焦到 VS Code
|
||||
vscode.window.showTextDocument(vscode.window.activeTextEditor!.document);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 示例:仿真失败
|
||||
if (message.type === 'simulationError') {
|
||||
notificationService.error(
|
||||
'IC Coder - 仿真失败',
|
||||
`仿真过程中发生错误: ${message.error}`,
|
||||
() => {
|
||||
// 点击通知时打开输出面板
|
||||
panel.reveal();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 添加配置项
|
||||
|
||||
修改 `package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "IC Coder",
|
||||
"properties": {
|
||||
"ic-coder.enableSystemNotification": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "启用系统级通知(任务完成时显示操作系统通知)"
|
||||
},
|
||||
"ic-coder.notificationSound": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "通知时播放系统声音"
|
||||
},
|
||||
"ic-coder.notificationTimeout": {
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"minimum": 0,
|
||||
"maximum": 60,
|
||||
"description": "通知自动消失时间(秒),0 表示不自动消失"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 设计
|
||||
|
||||
### 5.1 核心 API
|
||||
|
||||
#### `NotificationService.getInstance(context?)`
|
||||
|
||||
获取通知服务单例实例
|
||||
|
||||
**参数**:
|
||||
- `context` (可选): `vscode.ExtensionContext` - 扩展上下文,首次调用时必须提供
|
||||
|
||||
**返回**: `NotificationService` 实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const notificationService = NotificationService.getInstance(context);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `sendNotification(options)`
|
||||
|
||||
发送自定义通知
|
||||
|
||||
**参数**:
|
||||
- `options`: `NotificationOptions` - 通知选项对象
|
||||
|
||||
**返回**: `void`
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
notificationService.sendNotification({
|
||||
title: 'IC Coder',
|
||||
message: '任务已完成',
|
||||
type: NotificationType.SUCCESS,
|
||||
sound: true,
|
||||
timeout: 10,
|
||||
onClick: () => {
|
||||
console.log('用户点击了通知');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `success(title, message, onClick?)`
|
||||
|
||||
发送成功通知(快捷方法)
|
||||
|
||||
**参数**:
|
||||
- `title`: `string` - 通知标题
|
||||
- `message`: `string` - 通知消息
|
||||
- `onClick` (可选): `() => void` - 点击回调
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
notificationService.success(
|
||||
'IC Coder',
|
||||
'VCD 文件生成成功',
|
||||
() => panel.reveal()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `error(title, message, onClick?)`
|
||||
|
||||
发送错误通知(快捷方法)
|
||||
|
||||
**参数**:
|
||||
- `title`: `string` - 通知标题
|
||||
- `message`: `string` - 通知消息
|
||||
- `onClick` (可选): `() => void` - 点击回调
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
notificationService.error(
|
||||
'IC Coder',
|
||||
'编译失败: 语法错误',
|
||||
() => vscode.commands.executeCommand('workbench.action.showErrorsWarnings')
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `warning(title, message, onClick?)`
|
||||
|
||||
发送警告通知(快捷方法)
|
||||
|
||||
---
|
||||
|
||||
#### `info(title, message, onClick?)`
|
||||
|
||||
发送信息通知(快捷方法)
|
||||
|
||||
---
|
||||
|
||||
### 5.2 类型定义
|
||||
|
||||
```typescript
|
||||
enum NotificationType {
|
||||
INFO = 'info',
|
||||
SUCCESS = 'success',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
interface NotificationOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
type?: NotificationType;
|
||||
sound?: boolean;
|
||||
timeout?: number;
|
||||
icon?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置选项
|
||||
|
||||
### 6.1 用户配置项
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `ic-coder.enableSystemNotification` | `boolean` | `true` | 是否启用系统通知 |
|
||||
| `ic-coder.notificationSound` | `boolean` | `true` | 是否播放通知声音 |
|
||||
| `ic-coder.notificationTimeout` | `number` | `10` | 通知自动消失时间(秒) |
|
||||
|
||||
### 6.2 配置方式
|
||||
|
||||
#### 方式 1: VS Code 设置界面
|
||||
|
||||
1. 打开 VS Code 设置 (`Ctrl+,` / `Cmd+,`)
|
||||
2. 搜索 "IC Coder"
|
||||
3. 找到 "Enable System Notification" 选项
|
||||
4. 勾选或取消勾选
|
||||
|
||||
#### 方式 2: settings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"ic-coder.enableSystemNotification": true,
|
||||
"ic-coder.notificationSound": true,
|
||||
"ic-coder.notificationTimeout": 10
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试方案
|
||||
|
||||
### 7.1 单元测试
|
||||
|
||||
创建 `src/test/suite/notificationService.test.ts`
|
||||
|
||||
```typescript
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import { NotificationService, NotificationType } from '../../services/notificationService';
|
||||
|
||||
suite('NotificationService Test Suite', () => {
|
||||
let notificationService: NotificationService;
|
||||
|
||||
suiteSetup(() => {
|
||||
const context = {
|
||||
extensionPath: __dirname
|
||||
} as vscode.ExtensionContext;
|
||||
notificationService = NotificationService.getInstance(context);
|
||||
});
|
||||
|
||||
test('应该成功创建单例实例', () => {
|
||||
const instance1 = NotificationService.getInstance();
|
||||
const instance2 = NotificationService.getInstance();
|
||||
assert.strictEqual(instance1, instance2);
|
||||
});
|
||||
|
||||
test('应该发送成功通知', (done) => {
|
||||
notificationService.success('测试标题', '测试消息');
|
||||
setTimeout(() => done(), 1000);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 7.2 手动测试清单
|
||||
|
||||
#### Windows 测试
|
||||
- [ ] 通知显示在 Action Center
|
||||
- [ ] 点击通知能够聚焦到 VS Code
|
||||
- [ ] 通知声音正常播放
|
||||
- [ ] 通知图标正确显示
|
||||
- [ ] 通知在设定时间后自动消失
|
||||
- [ ] 禁用系统通知后不再显示
|
||||
|
||||
---
|
||||
|
||||
## 8. 注意事项
|
||||
|
||||
### 8.1 权限问题
|
||||
|
||||
**Windows**:
|
||||
- 首次使用时,Windows 可能会弹出权限请求
|
||||
- 用户需要在"设置 > 系统 > 通知和操作"中允许应用通知
|
||||
|
||||
**macOS**:
|
||||
- 需要在"系统偏好设置 > 通知"中允许 VS Code 发送通知
|
||||
|
||||
**Linux**:
|
||||
- 需要安装 `libnotify-bin` 包
|
||||
- 不同桌面环境的通知样式可能不同
|
||||
|
||||
### 8.2 通知频率控制
|
||||
|
||||
为避免通知轰炸,建议实现防抖机制:
|
||||
|
||||
```typescript
|
||||
export class NotificationService {
|
||||
private lastNotificationTime: Map<string, number> = new Map();
|
||||
private readonly DEBOUNCE_INTERVAL = 3000; // 3 秒
|
||||
|
||||
private shouldSendNotification(key: string): boolean {
|
||||
const now = Date.now();
|
||||
const lastTime = this.lastNotificationTime.get(key) || 0;
|
||||
|
||||
if (now - lastTime < this.DEBOUNCE_INTERVAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.lastNotificationTime.set(key, now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 错误处理
|
||||
|
||||
通知发送失败时,自动降级到 VS Code 内置通知。
|
||||
|
||||
### 8.4 安全考虑
|
||||
|
||||
- **不要在通知中显示敏感信息**(如 token、密码)
|
||||
- **验证通知内容**,防止 XSS 攻击
|
||||
- **限制通知频率**,防止滥用
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
### 9.1 通知不显示
|
||||
|
||||
**问题**: 调用通知 API 后,系统没有显示通知
|
||||
|
||||
**可能原因**:
|
||||
1. 用户禁用了系统通知权限
|
||||
2. 操作系统的"勿扰模式"已启用
|
||||
3. `node-notifier` 安装失败或版本不兼容
|
||||
|
||||
**解决方案**:
|
||||
```typescript
|
||||
// 添加调试日志
|
||||
notifier.notify(notificationConfig, (err, response, metadata) => {
|
||||
if (err) {
|
||||
console.error('[NotificationService] 错误:', err);
|
||||
} else {
|
||||
console.log('[NotificationService] 响应:', response);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 9.2 通知点击回调不触发
|
||||
|
||||
**问题**: 点击通知后,`onClick` 回调没有执行
|
||||
|
||||
**解决方案**:
|
||||
```typescript
|
||||
// 设置 wait: true
|
||||
const notificationConfig: notifier.Notification = {
|
||||
title: title,
|
||||
message: message,
|
||||
wait: true, // 等待用户交互
|
||||
};
|
||||
```
|
||||
|
||||
### 9.3 通知图标不显示
|
||||
|
||||
**问题**: 通知显示时没有自定义图标
|
||||
|
||||
**解决方案**:
|
||||
```typescript
|
||||
import * as fs from 'fs';
|
||||
|
||||
// 检查图标是否存在
|
||||
if (!fs.existsSync(this.iconPath)) {
|
||||
console.warn(`图标文件不存在: ${this.iconPath}`);
|
||||
this.iconPath = ''; // 使用系统默认图标
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 Linux 上通知不工作
|
||||
|
||||
**问题**: 在 Linux 系统上通知无法显示
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install libnotify-bin
|
||||
|
||||
# Fedora/RHEL
|
||||
sudo dnf install libnotify
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 最佳实践
|
||||
|
||||
### 10.1 通知时机
|
||||
|
||||
**推荐发送通知的场景**:
|
||||
- ✅ 长时间运行的任务完成(> 10 秒)
|
||||
- ✅ 后台任务完成(用户可能已切换到其他应用)
|
||||
- ✅ 发生错误需要用户关注
|
||||
- ✅ 重要状态变更
|
||||
|
||||
**不推荐发送通知的场景**:
|
||||
- ❌ 即时完成的操作(< 3 秒)
|
||||
- ❌ 用户主动触发且立即完成的操作
|
||||
- ❌ 频繁发生的事件(如自动保存)
|
||||
- ❌ 调试信息或日志
|
||||
|
||||
### 10.2 通知内容
|
||||
|
||||
**标题**:
|
||||
- 简洁明了,不超过 20 个字符
|
||||
- 包含应用名称(如 "IC Coder - 仿真完成")
|
||||
- 使用动作完成时态("已完成" 而不是 "完成中")
|
||||
|
||||
**消息**:
|
||||
- 提供具体信息,不超过 100 个字符
|
||||
- 包含关键细节(如文件名、错误类型)
|
||||
- 避免技术术语,使用用户友好的语言
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
// ✅ 好的通知
|
||||
notificationService.success(
|
||||
'IC Coder - 仿真完成',
|
||||
'testbench.v 仿真成功,VCD 文件已生成'
|
||||
);
|
||||
|
||||
// ❌ 不好的通知
|
||||
notificationService.success('完成', '操作已完成');
|
||||
```
|
||||
|
||||
### 10.3 通知优先级
|
||||
|
||||
根据重要性设置不同的通知类型和超时时间:
|
||||
|
||||
```typescript
|
||||
// 高优先级:错误(15 秒)
|
||||
notificationService.error(
|
||||
'IC Coder - 编译失败',
|
||||
'发现 3 个语法错误,请检查代码'
|
||||
);
|
||||
|
||||
// 中优先级:警告(10 秒)
|
||||
notificationService.warning(
|
||||
'IC Coder - 警告',
|
||||
'仿真时间过长,可能存在死循环'
|
||||
);
|
||||
|
||||
// 低优先级:信息(8 秒,无声音)
|
||||
notificationService.info(
|
||||
'IC Coder - 提示',
|
||||
'已自动保存工作区'
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 性能指标
|
||||
|
||||
### 11.1 预期性能
|
||||
|
||||
| 指标 | 目标值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 通知发送延迟 | < 100ms | 从调用到系统显示 |
|
||||
| 内存占用 | < 5MB | 通知服务常驻内存 |
|
||||
| CPU 占用 | < 1% | 空闲时 CPU 使用率 |
|
||||
| 包体积增加 | ~500KB | node-notifier 依赖 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 参考资料
|
||||
|
||||
### 12.1 官方文档
|
||||
|
||||
- [node-notifier GitHub](https://github.com/mikaelbr/node-notifier)
|
||||
- [VS Code Extension API](https://code.visualstudio.com/api)
|
||||
- [Windows Toast Notifications](https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-notifications-overview)
|
||||
|
||||
### 12.2 相关文章
|
||||
|
||||
- [Best Practices for Desktop Notifications](https://web.dev/notifications/)
|
||||
- [Designing Better Notifications](https://uxdesign.cc/designing-better-notifications-36ba9c0b3e0e)
|
||||
|
||||
---
|
||||
|
||||
## 13. 总结
|
||||
|
||||
本文档详细介绍了在 IC Coder 插件中实现系统级通知功能的完整方案,包括:
|
||||
|
||||
✅ **技术选型**: 选择 `node-notifier` 作为跨平台通知解决方案
|
||||
✅ **架构设计**: 单例模式的通知服务类,支持多种通知类型
|
||||
✅ **实现细节**: 完整的代码示例和配置说明
|
||||
✅ **测试方案**: 单元测试、集成测试和手动测试清单
|
||||
✅ **最佳实践**: 通知时机、内容设计和用户体验优化
|
||||
✅ **故障排查**: 常见问题和解决方案
|
||||
|
||||
通过实现系统级通知,IC Coder 插件能够在用户切换到其他应用时仍然及时通知任务状态,显著提升用户体验。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**最后更新**: 2026-01-26
|
||||
**作者**: IC Coder Team
|
||||
**许可**: MIT License
|
||||
|
||||
277
docs/token-expiration-check.md
Normal file
@ -0,0 +1,277 @@
|
||||
# Token 过期检查实现方案
|
||||
|
||||
## 1. 概述
|
||||
|
||||
实现三个关键时机的 Token 过期检查:
|
||||
- 插件激活时
|
||||
- 发起 API 请求前
|
||||
- 用户交互时(打开面板/侧边栏)
|
||||
|
||||
## 2. 数据存储
|
||||
|
||||
### 2.1 存储位置
|
||||
使用 VS Code 的 `globalState` 存储:
|
||||
```typescript
|
||||
context.globalState.update('tokenExp', exp);
|
||||
```
|
||||
|
||||
### 2.2 存储内容
|
||||
- `token`: 用户 token
|
||||
- `tokenExp`: 过期时间戳(秒)
|
||||
- `userInfo`: 用户信息
|
||||
|
||||
## 3. 核心函数设计
|
||||
|
||||
### 3.1 过期检查函数
|
||||
```typescript
|
||||
/**
|
||||
* 检查 token 是否过期
|
||||
* @param exp - 过期时间戳(秒)
|
||||
* @param bufferSeconds - 提前判断过期的缓冲时间(默认 60 秒)
|
||||
* @returns true 表示已过期或即将过期
|
||||
*/
|
||||
function isTokenExpired(exp: number | undefined, bufferSeconds: number = 60): boolean {
|
||||
if (!exp) {
|
||||
return true; // 没有过期时间,视为已过期
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000); // 当前时间戳(秒)
|
||||
return now >= (exp - bufferSeconds); // 提前 60 秒判断过期
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 清除登录状态函数
|
||||
```typescript
|
||||
/**
|
||||
* 清除所有登录相关状态
|
||||
*/
|
||||
async function clearAuthState(context: vscode.ExtensionContext): Promise<void> {
|
||||
await context.globalState.update('token', undefined);
|
||||
await context.globalState.update('tokenExp', undefined);
|
||||
await context.globalState.update('userInfo', undefined);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 统一过期处理函数
|
||||
```typescript
|
||||
/**
|
||||
* 处理 token 过期情况
|
||||
* @param context - 扩展上下文
|
||||
* @param showMessage - 是否显示提示消息
|
||||
*/
|
||||
async function handleTokenExpired(
|
||||
context: vscode.ExtensionContext,
|
||||
showMessage: boolean = true
|
||||
): Promise<void> {
|
||||
await clearAuthState(context);
|
||||
|
||||
if (showMessage) {
|
||||
const action = await vscode.window.showWarningMessage(
|
||||
'登录已过期,请重新登录',
|
||||
'立即登录'
|
||||
);
|
||||
|
||||
if (action === '立即登录') {
|
||||
// 触发登录流程(打开登录面板)
|
||||
vscode.commands.executeCommand('ic-coder.openPanel');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 三个检查时机实现
|
||||
|
||||
### 4.1 插件激活时检查
|
||||
|
||||
**位置**: `src/extension.ts` 的 `activate` 函数
|
||||
|
||||
**实现**:
|
||||
```typescript
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
console.log('IC Coder 插件正在激活...');
|
||||
|
||||
// 1. 检查 token 是否过期
|
||||
const tokenExp = context.globalState.get<number>('tokenExp');
|
||||
if (isTokenExpired(tokenExp)) {
|
||||
// 静默清除,不显示提示(避免启动时打扰用户)
|
||||
await handleTokenExpired(context, false);
|
||||
}
|
||||
|
||||
// ... 其他激活逻辑
|
||||
}
|
||||
```
|
||||
|
||||
**说明**: 启动时静默检查,如果过期则清除状态,但不弹窗提示
|
||||
|
||||
---
|
||||
|
||||
### 4.2 发起 API 请求前检查
|
||||
|
||||
**位置**: `src/utils/messageHandler.ts` 的 API 请求函数
|
||||
|
||||
**实现**:
|
||||
```typescript
|
||||
// 在发送消息到后端前检查
|
||||
async function sendMessageToBackend(message: string, context: vscode.ExtensionContext) {
|
||||
// 1. 检查 token 是否过期
|
||||
const tokenExp = context.globalState.get<number>('tokenExp');
|
||||
if (isTokenExpired(tokenExp)) {
|
||||
await handleTokenExpired(context, true); // 显示提示
|
||||
return; // 中断请求
|
||||
}
|
||||
|
||||
const token = context.globalState.get<string>('token');
|
||||
if (!token) {
|
||||
vscode.window.showWarningMessage('请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 继续发送请求
|
||||
// ... 原有请求逻辑
|
||||
}
|
||||
```
|
||||
|
||||
**说明**: 每次 API 请求前检查,如果过期则提示用户并中断请求
|
||||
|
||||
---
|
||||
|
||||
### 4.3 用户交互时检查
|
||||
|
||||
**位置**:
|
||||
- `src/panels/ICHelperPanel.ts` - 打开聊天面板时
|
||||
- `src/views/ICViewProvider.ts` - 侧边栏视图加载时
|
||||
|
||||
**实现 - 聊天面板**:
|
||||
```typescript
|
||||
// ICHelperPanel.ts
|
||||
public static render(extensionUri: vscode.Uri, context: vscode.ExtensionContext) {
|
||||
// 1. 检查 token 是否过期
|
||||
const tokenExp = context.globalState.get<number>('tokenExp');
|
||||
if (isTokenExpired(tokenExp)) {
|
||||
handleTokenExpired(context, true); // 显示提示
|
||||
// 继续渲染面板,但会显示未登录状态
|
||||
}
|
||||
|
||||
// 2. 创建或显示面板
|
||||
// ... 原有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
**实现 - 侧边栏视图**:
|
||||
```typescript
|
||||
// ICViewProvider.ts
|
||||
public resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||
// 1. 检查 token 是否过期
|
||||
const tokenExp = this._context.globalState.get<number>('tokenExp');
|
||||
if (isTokenExpired(tokenExp)) {
|
||||
handleTokenExpired(this._context, false); // 静默清除
|
||||
// 继续渲染,显示未登录状态
|
||||
}
|
||||
|
||||
// 2. 渲染视图
|
||||
// ... 原有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
**说明**: 打开面板时检查,聊天面板显示提示,侧边栏静默处理
|
||||
|
||||
## 5. 后端响应处理
|
||||
|
||||
### 5.1 保存 exp 字段
|
||||
|
||||
**位置**: `src/utils/messageHandler.ts` 处理登录响应的地方
|
||||
|
||||
**实现**:
|
||||
```typescript
|
||||
// 处理登录成功响应
|
||||
if (response.data.token) {
|
||||
await context.globalState.update('token', response.data.token);
|
||||
|
||||
// 保存过期时间
|
||||
if (response.data.exp) {
|
||||
await context.globalState.update('tokenExp', response.data.exp);
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
if (response.data.userInfo) {
|
||||
await context.globalState.update('userInfo', response.data.userInfo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 处理 401 响应
|
||||
|
||||
**实现**:
|
||||
```typescript
|
||||
// API 请求错误处理
|
||||
if (error.response?.status === 401) {
|
||||
// 后端返回 401,说明 token 无效或过期
|
||||
await handleTokenExpired(context, true);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 工具函数位置
|
||||
|
||||
建议创建新文件 `src/utils/authHelper.ts`:
|
||||
|
||||
```typescript
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function isTokenExpired(exp: number | undefined, bufferSeconds: number = 60): boolean {
|
||||
if (!exp) {
|
||||
return true;
|
||||
}
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return now >= (exp - bufferSeconds);
|
||||
}
|
||||
|
||||
export async function clearAuthState(context: vscode.ExtensionContext): Promise<void> {
|
||||
await context.globalState.update('token', undefined);
|
||||
await context.globalState.update('tokenExp', undefined);
|
||||
await context.globalState.update('userInfo', undefined);
|
||||
}
|
||||
|
||||
export async function handleTokenExpired(
|
||||
context: vscode.ExtensionContext,
|
||||
showMessage: boolean = true
|
||||
): Promise<void> {
|
||||
await clearAuthState(context);
|
||||
|
||||
if (showMessage) {
|
||||
const action = await vscode.window.showWarningMessage(
|
||||
'登录已过期,请重新登录',
|
||||
'立即登录'
|
||||
);
|
||||
|
||||
if (action === '立即登录') {
|
||||
vscode.commands.executeCommand('ic-coder.openPanel');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 测试场景
|
||||
|
||||
1. **启动测试**: 设置过期的 exp,重启插件,验证状态被清除
|
||||
2. **请求测试**: 设置即将过期的 exp,发送消息,验证被拦截
|
||||
3. **交互测试**: 设置过期的 exp,打开面板,验证提示显示
|
||||
4. **401 测试**: 模拟后端返回 401,验证状态清除
|
||||
|
||||
## 8. 注意事项
|
||||
|
||||
- 使用 60 秒缓冲时间,避免请求中途过期
|
||||
- 启动和侧边栏加载时静默处理,避免打扰用户
|
||||
- 主动操作(发消息、打开聊天面板)时显示提示
|
||||
- 所有时间戳使用秒为单位(与后端保持一致)
|
||||
- 过期检查应该在所有需要 token 的操作前执行
|
||||
|
||||
## 9. 修改文件清单
|
||||
|
||||
需要修改的文件:
|
||||
1. **新建**: `src/utils/authHelper.ts` - 认证辅助工具函数
|
||||
2. **修改**: `src/extension.ts` - 插件激活时检查
|
||||
3. **修改**: `src/utils/messageHandler.ts` - API 请求前检查 + 保存 exp + 处理 401
|
||||
4. **修改**: `src/panels/ICHelperPanel.ts` - 打开聊天面板时检查
|
||||
5. **修改**: `src/views/ICViewProvider.ts` - 侧边栏加载时检查
|
||||
|
||||
783
docs/插件试用用户功能实现方案.md
Normal file
@ -0,0 +1,783 @@
|
||||
# 插件试用用户功能实现方案
|
||||
|
||||
## 1. 方案概述
|
||||
|
||||
**核心思路:**
|
||||
- Web 登录成功后只返回 token(保持现状)
|
||||
- 插件调用 `getUserInfo(token)` 时,后端返回的数据里包含标识字段
|
||||
- 前端根据该字段判断是否是插件试用用户
|
||||
- 插件试用用户:显示欢迎弹窗,不显示邀请码弹窗
|
||||
- 正式用户:显示邀请码弹窗(现有逻辑)
|
||||
|
||||
---
|
||||
|
||||
## 2. 后端需要做什么
|
||||
|
||||
### 2.1 在用户信息接口中添加字段
|
||||
|
||||
**接口:** `GET /system/user/getInfo`
|
||||
|
||||
**现有响应:**
|
||||
```json
|
||||
{
|
||||
"userId": "xxx",
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**新增字段:**
|
||||
|
||||
**方案 :添加 isPluginTrial 字段**
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "xxx",
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"isPluginTrial": true, // ← 新增:是否是插件试用用户
|
||||
"pluginTrialExpiresAt": 1709654400000 // ← 新增:试用到期时间(毫秒时间戳)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 后端逻辑说明
|
||||
|
||||
**判断逻辑:**
|
||||
```javascript
|
||||
// 伪代码
|
||||
function getUserInfo(userId) {
|
||||
const user = db.users.findById(userId);
|
||||
|
||||
// 判断是否是插件试用用户(后端自己的逻辑)
|
||||
const isPluginTrial = checkIfPluginTrialUser(user);
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
email: user.email,
|
||||
isPluginTrial: isPluginTrial,
|
||||
pluginTrialExpiresAt: isPluginTrial ? user.trial_expires_at : null
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Token 过期时间:**
|
||||
- 插件试用用户:JWT Token 设置 15 天过期
|
||||
- 正式用户:JWT Token 设置 30 天过期(或现有逻辑)
|
||||
|
||||
---
|
||||
|
||||
## 3. 前端需要修改的地方
|
||||
|
||||
### 3.1 修改 UserInfo 接口
|
||||
|
||||
**文件:** `src/services/userService.ts`
|
||||
|
||||
**现有接口:**
|
||||
```typescript
|
||||
interface UserInfo {
|
||||
userId: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
phonenumber?: string;
|
||||
avatar?: string;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
createTime?: string;
|
||||
loginDate?: string;
|
||||
membership?: {
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
remainingDays?: number;
|
||||
monthlyCredits?: number;
|
||||
};
|
||||
credits?: number;
|
||||
}
|
||||
```
|
||||
|
||||
**新增字段:**
|
||||
```typescript
|
||||
interface UserInfo {
|
||||
// ... 现有字段
|
||||
isPluginTrial?: boolean; // ← 新增:是否是插件试用用户
|
||||
pluginTrialExpiresAt?: number; // ← 新增:试用到期时间(毫秒时间戳)
|
||||
membership?: {
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
remainingDays?: number;
|
||||
monthlyCredits?: number;
|
||||
};
|
||||
credits?: number;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3.2 修改 onTokenReceived() 方法
|
||||
|
||||
**文件:** `src/services/userService.ts`
|
||||
|
||||
**现有代码位置:** 约 200 行左右
|
||||
|
||||
**修改内容:**
|
||||
```typescript
|
||||
async onTokenReceived(token: string) {
|
||||
// 现有逻辑:并行获取三类信息
|
||||
const [userInfo, membershipInfo, credits] = await Promise.all([
|
||||
getUserInfo(token),
|
||||
getMembershipInfo(token),
|
||||
fetchBalanceWithToken(token)
|
||||
]);
|
||||
|
||||
// 合并数据
|
||||
const fullUserInfo = {
|
||||
...userInfo,
|
||||
membership: membershipInfo,
|
||||
credits: credits
|
||||
};
|
||||
|
||||
// 保存到 globalState
|
||||
await this.context.globalState.update('icCoderUserInfo', fullUserInfo);
|
||||
|
||||
// ========== 新增逻辑 ==========
|
||||
// 判断是否是插件试用用户
|
||||
if (fullUserInfo.isPluginTrial === true) {
|
||||
// 插件试用用户:显示欢迎弹窗,不显示邀请码弹窗
|
||||
await this.showWelcomePanel();
|
||||
// 标记为已显示欢迎弹窗(避免重复显示)
|
||||
await this.context.globalState.update('pluginTrialWelcomed', true);
|
||||
} else {
|
||||
// 正式用户:显示邀请码弹窗(现有逻辑)
|
||||
await this.checkAndShowInvitationModal();
|
||||
}
|
||||
// ========== 新增逻辑结束 ==========
|
||||
|
||||
return fullUserInfo;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3.3 新增欢迎弹窗面板
|
||||
|
||||
**新建文件:** `src/panels/WelcomePanel.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 欢迎引导面板
|
||||
* 功能:插件试用用户首次登录显示使用教程
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class WelcomePanel {
|
||||
public static currentPanel: WelcomePanel | undefined;
|
||||
private readonly _panel: vscode.WebviewPanel;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel) {
|
||||
this._panel = panel;
|
||||
this._panel.webview.html = this.getHtmlContent();
|
||||
|
||||
// 监听关闭事件
|
||||
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||
}
|
||||
|
||||
public static render(context: vscode.ExtensionContext) {
|
||||
// 避免重复显示
|
||||
if (WelcomePanel.currentPanel) {
|
||||
WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'icCoderWelcome',
|
||||
'欢迎使用 IC Coder',
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true
|
||||
}
|
||||
);
|
||||
|
||||
WelcomePanel.currentPanel = new WelcomePanel(panel);
|
||||
}
|
||||
|
||||
private getHtmlContent(): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>欢迎使用 IC Coder</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 40px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--vscode-foreground);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
h1 {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.welcome-message {
|
||||
font-size: 16px;
|
||||
margin-bottom: 30px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.step {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
.step h3 {
|
||||
margin-top: 0;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
.step p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.button {
|
||||
padding: 12px 24px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🎉 欢迎使用 IC Coder!</h1>
|
||||
<p class="welcome-message">
|
||||
您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
|
||||
</p>
|
||||
|
||||
<div class="step">
|
||||
<h3>📝 步骤 1:打开聊天面板</h3>
|
||||
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<h3>💬 步骤 2:输入您的需求</h3>
|
||||
<p>描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案</p>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<h3>🔬 步骤 3:运行仿真</h3>
|
||||
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
|
||||
</div>
|
||||
|
||||
<button class="button" onclick="close()">开始使用</button>
|
||||
|
||||
<script>
|
||||
function close() {
|
||||
// 通知 VS Code 关闭面板
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
WelcomePanel.currentPanel = undefined;
|
||||
this._panel.dispose();
|
||||
while (this._disposables.length) {
|
||||
const disposable = this._disposables.pop();
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3.4 新增过期提醒面板
|
||||
|
||||
**新建文件:** `src/panels/ExpiredPanel.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 试用期到期提醒面板
|
||||
* 功能:试用期到期时显示续费提示
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class ExpiredPanel {
|
||||
public static render() {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'icCoderExpired',
|
||||
'试用期已到期',
|
||||
vscode.ViewColumn.One,
|
||||
{ enableScripts: true }
|
||||
);
|
||||
|
||||
panel.webview.html = this.getHtmlContent();
|
||||
}
|
||||
|
||||
private static getHtmlContent(): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
color: var(--vscode-foreground);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
h1 {
|
||||
color: var(--vscode-errorForeground);
|
||||
font-size: 28px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin: 15px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.button {
|
||||
padding: 12px 30px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin: 10px;
|
||||
}
|
||||
.button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>⏰ 您的试用期已到期</h1>
|
||||
<p>感谢您使用 IC Coder!您的 15 天试用期已结束。</p>
|
||||
<p>如需继续使用,请联系我们获取正式版本。</p>
|
||||
|
||||
<button class="button" onclick="contact()">联系我们</button>
|
||||
|
||||
<script>
|
||||
function contact() {
|
||||
// 可以打开联系页面或发送邮件
|
||||
window.open('https://iccoder.com/contact', '_blank');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3.5 新增过期检测服务
|
||||
|
||||
**新建文件:** `src/services/trialExpirationService.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 试用期过期检测服务
|
||||
* 功能:检查插件试用用户是否过期
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { getUserInfo } from './userService';
|
||||
import { ExpiredPanel } from '../panels/ExpiredPanel';
|
||||
|
||||
export class TrialExpirationService {
|
||||
private context: vscode.ExtensionContext;
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期
|
||||
* @returns true=已过期,false=未过期
|
||||
*/
|
||||
public async checkExpiration(): Promise<boolean> {
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
// 不是插件试用用户,不需要检查
|
||||
if (!userInfo?.isPluginTrial) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 没有过期时间,不检查
|
||||
if (!userInfo.pluginTrialExpiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
const now = Date.now();
|
||||
if (now >= userInfo.pluginTrialExpiresAt) {
|
||||
// 已过期
|
||||
await this.handleExpired();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理过期逻辑
|
||||
*/
|
||||
private async handleExpired(): Promise<void> {
|
||||
// 显示过期弹窗
|
||||
ExpiredPanel.render();
|
||||
|
||||
// 清除本地数据(可选)
|
||||
// await this.context.globalState.update('icCoderUserInfo', undefined);
|
||||
// await this.context.globalState.update('icCoderSessions', undefined);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3.6 在消息发送前检查过期
|
||||
|
||||
**文件:** `src/utils/messageHandler.ts`
|
||||
|
||||
**修改位置:** 在发送消息给后端之前添加过期检查
|
||||
|
||||
```typescript
|
||||
import { TrialExpirationService } from '../services/trialExpirationService';
|
||||
|
||||
// 在 handleUserMessage 或类似的消息处理函数中添加
|
||||
async function handleUserMessage(message: string, context: vscode.ExtensionContext) {
|
||||
// ========== 新增:检查试用期是否过期 ==========
|
||||
const trialService = new TrialExpirationService(context);
|
||||
const isExpired = await trialService.checkExpiration();
|
||||
|
||||
if (isExpired) {
|
||||
// 已过期,禁止使用
|
||||
return {
|
||||
success: false,
|
||||
message: '您的试用期已到期,请联系我们获取正式版本'
|
||||
};
|
||||
}
|
||||
// ========== 新增结束 ==========
|
||||
|
||||
// 现有的消息处理逻辑
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. 完整的实现流程
|
||||
|
||||
### 4.1 登录流程(带过期检查)
|
||||
|
||||
```
|
||||
1. 用户点击登录
|
||||
↓
|
||||
2. 打开浏览器,Web 端登录
|
||||
↓
|
||||
3. 重定向回插件:http://localhost:{port}/callback?token={token}
|
||||
↓
|
||||
4. 插件调用 onTokenReceived(token)
|
||||
↓
|
||||
5. 并行获取:getUserInfo + getMembershipInfo + Credits
|
||||
↓
|
||||
6. 后端返回 userInfo(包含 isPluginTrial 和 pluginTrialExpiresAt)
|
||||
↓
|
||||
7. 判断 isPluginTrial === true?
|
||||
├─ 是:显示欢迎弹窗,不显示邀请码弹窗
|
||||
└─ 否:显示邀请码弹窗(现有逻辑)
|
||||
↓
|
||||
8. 保存用户信息到 globalState
|
||||
```
|
||||
|
||||
### 4.2 使用功能时的过期检查
|
||||
|
||||
```
|
||||
1. 用户发送消息/使用功能
|
||||
↓
|
||||
2. 调用 trialService.checkExpiration()
|
||||
↓
|
||||
3. 获取 userInfo,检查 isPluginTrial
|
||||
↓
|
||||
4. 如果是插件试用用户,检查 Date.now() >= pluginTrialExpiresAt?
|
||||
├─ 是:显示过期弹窗,禁止使用,返回 true
|
||||
└─ 否:允许使用,返回 false
|
||||
↓
|
||||
5. 继续正常的消息处理流程
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. ⚠️ 潜在 Bug 和注意事项
|
||||
|
||||
### 5.1 时间同步问题
|
||||
|
||||
**问题:** 前端使用 `Date.now()` 判断过期,如果用户本地时间不准确会导致误判
|
||||
|
||||
**场景:**
|
||||
- 用户本地时间快了 1 天 → 提前显示过期弹窗
|
||||
- 用户本地时间慢了 1 天 → 过期后仍可使用
|
||||
|
||||
**解决方案:**
|
||||
```typescript
|
||||
// 方案 1:每次使用前调用后端验证(推荐)
|
||||
async checkExpiration(): Promise<boolean> {
|
||||
try {
|
||||
// 调用后端接口验证 Token 是否过期
|
||||
const response = await fetch(`${API_BASE}/auth/verify`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
// Token 过期
|
||||
await this.handleExpired();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
// 网络错误,使用本地时间判断
|
||||
const userInfo = await getUserInfo();
|
||||
return Date.now() >= (userInfo?.pluginTrialExpiresAt || 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 5.2 欢迎弹窗重复显示
|
||||
|
||||
**问题:** 每次登录都显示欢迎弹窗,用户体验不好
|
||||
|
||||
**场景:**
|
||||
- 试用用户第一次登录显示欢迎弹窗 ✅
|
||||
- 用户关闭插件后重新打开,又显示欢迎弹窗 ❌
|
||||
|
||||
**解决方案:**
|
||||
```typescript
|
||||
async onTokenReceived(token: string) {
|
||||
// ... 获取用户信息
|
||||
|
||||
if (fullUserInfo.isPluginTrial === true) {
|
||||
// 检查是否已经显示过欢迎弹窗
|
||||
const hasWelcomed = this.context.globalState.get('pluginTrialWelcomed');
|
||||
|
||||
if (!hasWelcomed) {
|
||||
await this.showWelcomePanel();
|
||||
await this.context.globalState.update('pluginTrialWelcomed', true);
|
||||
}
|
||||
} else {
|
||||
await this.checkAndShowInvitationModal();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 5.3 isPluginTrial 字段类型不一致
|
||||
|
||||
**问题:** 后端可能返回 `true`、`false`、`null`、`undefined`,前端判断时需要严格处理
|
||||
|
||||
**场景:**
|
||||
```typescript
|
||||
// ❌ 错误写法
|
||||
if (userInfo.isPluginTrial) {
|
||||
// undefined 会被判断为 false
|
||||
}
|
||||
|
||||
// ✅ 正确写法
|
||||
if (userInfo.isPluginTrial === true) {
|
||||
// 插件试用用户
|
||||
}
|
||||
```
|
||||
|
||||
**建议:**
|
||||
- 后端统一返回 `true` 或 `false`,不要返回 `null`
|
||||
- 前端使用严格相等 `===` 判断
|
||||
|
||||
|
||||
### 5.4 Token 过期但前端未清除
|
||||
|
||||
**问题:** Token 在后端已过期,但前端仍保存着过期的 Token
|
||||
|
||||
**场景:**
|
||||
- 用户 15 天后打开插件
|
||||
- 前端尝试调用 API,后端返回 401
|
||||
- 前端没有处理 401,导致功能异常
|
||||
|
||||
**解决方案:**
|
||||
```typescript
|
||||
// 在 apiClient.ts 中统一处理 401
|
||||
async function apiCall(url: string, options: any) {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (response.status === 401) {
|
||||
// Token 过期,清除本地数据
|
||||
await clearAllData();
|
||||
ExpiredPanel.render();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. 前后端对接清单
|
||||
|
||||
### 6.1 后端需要提供
|
||||
|
||||
**1. 修改 `GET /system/user/getInfo` 接口**
|
||||
- 新增字段:`isPluginTrial` (boolean)
|
||||
- 新增字段:`pluginTrialExpiresAt` (number, 毫秒时间戳)
|
||||
|
||||
**2. Token 过期时间设置**
|
||||
- 插件试用用户:JWT Token 设置 15 天过期
|
||||
- 正式用户:保持现有逻辑
|
||||
|
||||
**3. 测试账号**
|
||||
- 提供 1-2 个插件试用用户账号用于测试
|
||||
|
||||
### 6.2 前端需要修改
|
||||
|
||||
**1. 修改文件:**
|
||||
- `src/services/userService.ts` - 修改 UserInfo 接口和 onTokenReceived()
|
||||
- `src/utils/messageHandler.ts` - 添加过期检查
|
||||
|
||||
**2. 新增文件:**
|
||||
- `src/panels/WelcomePanel.ts` - 欢迎弹窗
|
||||
- `src/panels/ExpiredPanel.ts` - 过期提醒弹窗
|
||||
- `src/services/trialExpirationService.ts` - 过期检测服务
|
||||
|
||||
**3. globalState 新增存储键:**
|
||||
- `pluginTrialWelcomed` (boolean) - 是否已显示欢迎弹窗
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试计划
|
||||
|
||||
### 7.1 登录流程测试
|
||||
|
||||
**测试用例 1:插件试用用户登录**
|
||||
- 使用插件试用用户账号登录
|
||||
- 验证是否显示欢迎弹窗
|
||||
- 验证是否不显示邀请码弹窗
|
||||
- 验证 userInfo 中 isPluginTrial === true
|
||||
|
||||
**测试用例 2:正式用户登录**
|
||||
- 使用正式用户账号登录
|
||||
- 验证是否显示邀请码弹窗
|
||||
- 验证是否不显示欢迎弹窗
|
||||
- 验证 userInfo 中 isPluginTrial !== true
|
||||
|
||||
**测试用例 3:欢迎弹窗不重复显示**
|
||||
- 插件试用用户登录后显示欢迎弹窗
|
||||
- 关闭插件重新打开
|
||||
- 验证不再显示欢迎弹窗
|
||||
|
||||
|
||||
### 7.2 过期检测测试
|
||||
|
||||
**测试用例 4:未过期用户正常使用**
|
||||
- 插件试用用户登录(未过期)
|
||||
- 发送消息使用功能
|
||||
- 验证功能正常使用
|
||||
|
||||
**测试用例 5:已过期用户禁止使用**
|
||||
- 修改本地时间到 15 天后(或修改 pluginTrialExpiresAt)
|
||||
- 尝试发送消息
|
||||
- 验证显示过期弹窗
|
||||
- 验证功能被禁用
|
||||
|
||||
**测试用例 6:Token 过期处理**
|
||||
- 使用过期的 Token 调用 API
|
||||
- 验证后端返回 401
|
||||
- 验证前端显示过期弹窗
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结
|
||||
|
||||
### 8.1 核心改动点
|
||||
|
||||
**后端(最小改动):**
|
||||
1. `GET /system/user/getInfo` 接口新增 2 个字段
|
||||
2. JWT Token 根据用户类型设置不同过期时间
|
||||
|
||||
**前端(主要改动):**
|
||||
1. 修改 UserInfo 接口定义
|
||||
2. 修改 onTokenReceived() 添加判断逻辑
|
||||
3. 新增 3 个文件(欢迎面板、过期面板、过期检测服务)
|
||||
4. 在消息发送前添加过期检查
|
||||
|
||||
### 8.2 关键判断逻辑
|
||||
|
||||
```typescript
|
||||
// 登录后判断
|
||||
if (userInfo.isPluginTrial === true) {
|
||||
showWelcomePanel(); // 显示欢迎弹窗
|
||||
} else {
|
||||
showInvitationModal(); // 显示邀请码弹窗
|
||||
}
|
||||
|
||||
// 使用前判断
|
||||
if (Date.now() >= userInfo.pluginTrialExpiresAt) {
|
||||
showExpiredPanel(); // 显示过期弹窗
|
||||
return; // 禁止使用
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 8.3 必须注意的问题
|
||||
|
||||
1. **时间同步问题** - 用户本地时间不准确导致误判,建议调用后端验证
|
||||
2. **isPluginTrial 严格判断** - 必须使用 `=== true` 判断
|
||||
3. **欢迎弹窗重复显示** - 使用 globalState 标记避免重复
|
||||
4. **Token 过期处理** - 在 apiClient 中统一处理 401 响应
|
||||
|
||||
### 8.4 实现优先级
|
||||
|
||||
**P0(必须实现):**
|
||||
1. 后端接口新增字段
|
||||
2. 前端 UserInfo 接口修改
|
||||
3. 前端 onTokenReceived() 判断逻辑
|
||||
4. 过期检测逻辑
|
||||
|
||||
**P1(重要):**
|
||||
1. 欢迎弹窗
|
||||
2. 过期提醒弹窗
|
||||
|
||||
**P2(优化):**
|
||||
1. 后端验证过期(避免时间同步问题)
|
||||
2. 更友好的过期提示
|
||||
|
||||
---
|
||||
|
||||
**文档完成!** 基于你现有的代码架构,这个方案改动最小,实现最简单。
|
||||
|
||||
BIN
media/description/auto-build-architecture-copy.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
media/description/auto-build-architecture.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
media/description/auto-simulation-1.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
media/description/auto-simulation-2.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
media/description/auto-simulation-3.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
media/description/input-requirement-1.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
media/description/input-requirement-2.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
media/description/input-requirement-3.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
media/description/plan-design-doc-1.png
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
media/description/plan-design-doc-2.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
media/description/real-time-follow-1.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
media/description/real-time-follow-2.png
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
media/description/real-time-follow-3.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
media/description/real-time-follow-4.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 252 KiB |
50
package.json
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "iccoder",
|
||||
"displayName": "IC Coder",
|
||||
"displayName": "IC Coder: Agentic Verilog Platform",
|
||||
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
||||
"version": "0.0.2",
|
||||
"publisher": "ICCoder",
|
||||
"version": "1.0.11",
|
||||
"publisher": "ICCoderAgenticVerilogPlatform",
|
||||
"engines": {
|
||||
"vscode": "^1.80.0"
|
||||
},
|
||||
@ -21,9 +21,13 @@
|
||||
"assistant"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.pengyejiatu.com/pengyejiatu/IC-Coder-Plugin.git"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:ic-coder.openPanel",
|
||||
"onView:ic-coder-sidebar",
|
||||
"onView:ic-coder.mainView",
|
||||
"onLanguage:verilog",
|
||||
"onLanguage:vhdl",
|
||||
"onStartupFinished"
|
||||
@ -45,6 +49,11 @@
|
||||
"command": "ic-coder.openVCDViewer",
|
||||
"title": "打开 VCD 波形查看器",
|
||||
"category": "IC Coder"
|
||||
},
|
||||
{
|
||||
"command": "ic-coder.testNotification",
|
||||
"title": "测试系统通知",
|
||||
"category": "IC Coder"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
@ -82,7 +91,29 @@
|
||||
],
|
||||
"priority": "default"
|
||||
}
|
||||
]
|
||||
],
|
||||
"configuration": {
|
||||
"title": "IC Coder",
|
||||
"properties": {
|
||||
"ic-coder.enableSystemNotification": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "启用系统级通知(任务完成时显示操作系统通知)"
|
||||
},
|
||||
"ic-coder.notificationSound": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "通知时播放系统声音"
|
||||
},
|
||||
"ic-coder.notificationTimeout": {
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"minimum": 0,
|
||||
"maximum": 60,
|
||||
"description": "通知自动消失时间(秒),0 表示不自动消失"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "pnpm run package",
|
||||
@ -99,10 +130,12 @@
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "22.x",
|
||||
"@types/node-notifier": "^8.0.5",
|
||||
"@types/vscode": "^1.80.0",
|
||||
"@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",
|
||||
@ -110,16 +143,11 @@
|
||||
"webpack": "^5.103.0",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"media",
|
||||
"tools",
|
||||
"src/assets"
|
||||
],
|
||||
"dependencies": {
|
||||
"@wavedrom/doppler": "^1.14.0",
|
||||
"eventsource-parser": "^3.0.6",
|
||||
"iconv-lite": "^0.7.1",
|
||||
"node-notifier": "^10.0.1",
|
||||
"onml": "^2.1.0",
|
||||
"style-mod": "^4.1.3",
|
||||
"vcd-stream": "^1.5.0",
|
||||
|
||||
74
pnpm-lock.yaml
generated
@ -17,6 +17,9 @@ importers:
|
||||
iconv-lite:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
node-notifier:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
onml:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
@ -39,6 +42,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: 22.x
|
||||
version: 22.19.2
|
||||
'@types/node-notifier':
|
||||
specifier: ^8.0.5
|
||||
version: 8.0.5
|
||||
'@types/vscode':
|
||||
specifier: ^1.80.0
|
||||
version: 1.107.0
|
||||
@ -51,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
|
||||
@ -349,6 +358,9 @@ packages:
|
||||
'@types/mocha@10.0.10':
|
||||
resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==}
|
||||
|
||||
'@types/node-notifier@8.0.5':
|
||||
resolution: {integrity: sha512-LX7+8MtTsv6szumAp6WOy87nqMEdGhhry/Qfprjm1Ma6REjVzeF7SCyvPtp5RaF6IkXCS9V4ra8g5fwvf2ZAYg==}
|
||||
|
||||
'@types/node@22.19.2':
|
||||
resolution: {integrity: sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==}
|
||||
|
||||
@ -814,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==}
|
||||
|
||||
@ -1185,6 +1203,9 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
growly@1.3.0:
|
||||
resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@ -1284,6 +1305,11 @@ packages:
|
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-docker@2.2.1:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
is-docker@3.0.0:
|
||||
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
@ -1342,6 +1368,10 @@ packages:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-wsl@2.2.0:
|
||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-wsl@3.1.0:
|
||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||
engines: {node: '>=16'}
|
||||
@ -1622,6 +1652,9 @@ packages:
|
||||
node-addon-api@4.3.0:
|
||||
resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
|
||||
|
||||
node-notifier@10.0.1:
|
||||
resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==}
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
@ -1904,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==}
|
||||
|
||||
@ -1919,6 +1956,9 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shellwords@0.1.1:
|
||||
resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==}
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2702,6 +2742,10 @@ snapshots:
|
||||
|
||||
'@types/mocha@10.0.10': {}
|
||||
|
||||
'@types/node-notifier@8.0.5':
|
||||
dependencies:
|
||||
'@types/node': 22.19.2
|
||||
|
||||
'@types/node@22.19.2':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@ -3266,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:
|
||||
@ -3646,6 +3699,8 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
growly@1.3.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
@ -3737,6 +3792,8 @@ snapshots:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
||||
is-docker@2.2.1: {}
|
||||
|
||||
is-docker@3.0.0: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
@ -3771,6 +3828,10 @@ snapshots:
|
||||
|
||||
is-unicode-supported@2.1.0: {}
|
||||
|
||||
is-wsl@2.2.0:
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
|
||||
is-wsl@3.1.0:
|
||||
dependencies:
|
||||
is-inside-container: 1.0.0
|
||||
@ -4074,6 +4135,15 @@ snapshots:
|
||||
node-addon-api@4.3.0:
|
||||
optional: true
|
||||
|
||||
node-notifier@10.0.1:
|
||||
dependencies:
|
||||
growly: 1.3.0
|
||||
is-wsl: 2.2.0
|
||||
semver: 7.7.3
|
||||
shellwords: 0.1.1
|
||||
uuid: 8.3.2
|
||||
which: 2.0.2
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
node-sarif-builder@3.3.1:
|
||||
@ -4383,6 +4453,8 @@ snapshots:
|
||||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
|
||||
serialize-javascript@7.0.4: {}
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
shallow-clone@3.0.1:
|
||||
@ -4395,6 +4467,8 @@ snapshots:
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
shellwords@0.1.1: {}
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
BIN
src/assets/QRCode/wx.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
@ -8,7 +8,7 @@ import * as vscode from "vscode";
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "test";
|
||||
const CURRENT_ENV: Environment = "prod";
|
||||
|
||||
/** 服务等级类型 */
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
@ -17,6 +17,8 @@ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
export interface IccoderConfig {
|
||||
/** 后端服务地址 */
|
||||
backendUrl: string;
|
||||
/** 登录页面地址 */
|
||||
loginUrl: string;
|
||||
/** 后端服务地址(strangeLoop) */
|
||||
backendUrlStrongeLoop: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
@ -29,26 +31,29 @@ export interface IccoderConfig {
|
||||
|
||||
/** 环境配置 */
|
||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 */
|
||||
/** 本地开发环境 - 通过 Gateway 路由 */
|
||||
dev: {
|
||||
backendUrl: "http://localhost:2233",
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
backendUrl: "http://localhost:8080/iccoder",
|
||||
backendUrlStrongeLoop: "http://localhost:8080",
|
||||
loginUrl: "http://localhost/login",
|
||||
timeout: 300000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max", // 默认使用 max
|
||||
},
|
||||
/** 测试服务器环境 */
|
||||
/** 测试服务器环境 - 通过 Gateway 路由 */
|
||||
test: {
|
||||
backendUrl: "http://192.168.1.108:2233",
|
||||
backendUrl: "http://192.168.1.108:2029/iccoder",
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
loginUrl: "http://192.168.1.108:2005/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max",
|
||||
},
|
||||
/** 生产环境 */
|
||||
/** 生产环境 - 通过 Gateway 路由 */
|
||||
prod: {
|
||||
backendUrl: "https://api.iccoder.com",
|
||||
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
|
||||
backendUrlStrongeLoop: "http://192.168.1.115:2029",
|
||||
loginUrl: "https://iccoder.com/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "auto",
|
||||
|
||||
124
src/extension.ts
@ -6,13 +6,46 @@ 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";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 初始化通知服务
|
||||
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);
|
||||
|
||||
// 初始化 VCD 文件服务器
|
||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||
vcdFileServer.start().then((port) => {
|
||||
@ -26,7 +59,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
dispose: () => vcdFileServer.stop()
|
||||
});
|
||||
|
||||
// 注册 Authentication Provider
|
||||
// 注册 Authentication Provider(此时 icCoderSessions 已经被清除)
|
||||
const authProvider = new ICCoderAuthenticationProvider(context);
|
||||
context.subscriptions.push(
|
||||
vscode.authentication.registerAuthenticationProvider(
|
||||
@ -126,9 +159,32 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
// 注册命令:用户登录
|
||||
const loginCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.login",
|
||||
async () => {
|
||||
async (options?: { forceReauth?: boolean }) => {
|
||||
try {
|
||||
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
||||
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;
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
@ -142,12 +198,10 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
if (session) {
|
||||
// 通过创建新会话并清除偏好来实现登出
|
||||
await vscode.authentication.getSession("iccoder", [], {
|
||||
clearSessionPreference: true,
|
||||
forceNewSession: true
|
||||
});
|
||||
vscode.window.showInformationMessage("已退出登录");
|
||||
// 调用 authProvider 的 removeSession 方法
|
||||
await authProvider.removeSession(session.id);
|
||||
// 清除邀请码验证状态
|
||||
await InvitationService.clearVerificationStatus(context);
|
||||
} else {
|
||||
vscode.window.showInformationMessage("当前未登录");
|
||||
}
|
||||
@ -157,6 +211,45 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:更换邀请码
|
||||
const changeInvitationCodeCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.changeInvitationCode",
|
||||
async () => {
|
||||
const confirm = await vscode.window.showWarningMessage(
|
||||
'确定要更换邀请码吗?',
|
||||
'确定',
|
||||
'取消'
|
||||
);
|
||||
|
||||
if (confirm === '确定') {
|
||||
await InvitationService.clearVerificationStatus(context);
|
||||
vscode.window.showInformationMessage('已清除邀请码,请重新验证');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:测试系统通知
|
||||
const testNotificationCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.testNotification",
|
||||
() => {
|
||||
console.log('[Extension] ========== 测试通知命令被调用 ==========');
|
||||
|
||||
// 先显示 VS Code 通知确认命令执行
|
||||
vscode.window.showInformationMessage('正在测试系统通知...');
|
||||
|
||||
// 发送系统通知
|
||||
notificationService.success(
|
||||
'IC Coder - 测试通知',
|
||||
'系统通知功能正常工作!',
|
||||
() => {
|
||||
vscode.window.showInformationMessage('您点击了系统通知!');
|
||||
}
|
||||
);
|
||||
|
||||
console.log('[Extension] 测试通知命令执行完成');
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:查看会话历史
|
||||
// TODO: 这些命令需要根据新的任务架构重新实现
|
||||
// 暂时注释掉,等待重新实现
|
||||
@ -208,7 +301,12 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
const viewProvider = new ICViewProvider(context.extensionUri, context);
|
||||
const viewRegistration = vscode.window.registerWebviewViewProvider(
|
||||
"ic-coder.mainView",
|
||||
viewProvider
|
||||
viewProvider,
|
||||
{
|
||||
webviewOptions: {
|
||||
retainContextWhenHidden: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 注册 VCD 自定义编辑器
|
||||
@ -222,6 +320,10 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
openVCDViewerInBrowserCommand,
|
||||
loginCommand,
|
||||
logoutCommand,
|
||||
changeInvitationCodeCommand,
|
||||
testNotificationCommand,
|
||||
// testTrialUserCommand,
|
||||
// testExpiredUserCommand,
|
||||
// TODO: 等待重新实现这些命令
|
||||
// viewHistoryCommand,
|
||||
// newSessionCommand,
|
||||
|
||||
78
src/panels/ExpiredPanel.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 试用期到期提醒面板
|
||||
* 功能:试用期到期时显示续费提示
|
||||
* 依赖:vscode
|
||||
* 使用场景:试用用户到期时显示
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class ExpiredPanel {
|
||||
public static render() {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'icCoderExpired',
|
||||
'试用期已到期',
|
||||
vscode.ViewColumn.One,
|
||||
{ enableScripts: true }
|
||||
);
|
||||
|
||||
panel.webview.html = this.getHtmlContent();
|
||||
}
|
||||
|
||||
private static getHtmlContent(): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
color: var(--vscode-foreground);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
h1 {
|
||||
color: var(--vscode-errorForeground);
|
||||
font-size: 28px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin: 15px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.button {
|
||||
padding: 12px 30px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin: 10px;
|
||||
}
|
||||
.button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>⏰ 您的试用期已到期</h1>
|
||||
<p>感谢您使用 IC Coder!您的 15 天试用期已结束。</p>
|
||||
<p>如需继续使用,请联系我们获取正式版本。</p>
|
||||
|
||||
<button class="button" onclick="contact()">联系我们</button>
|
||||
|
||||
<script>
|
||||
function contact() {
|
||||
window.open('https://iccoder.com/contact', '_blank');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -9,16 +9,22 @@ import {
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
handlePlanAction,
|
||||
setPendingPlanExecution,
|
||||
getCurrentTaskId,
|
||||
setLastTaskId,
|
||||
handleAcceptChange,
|
||||
handleRejectChange,
|
||||
startChangeSession,
|
||||
handleOpenFileDiff,
|
||||
} from "../utils/messageHandler";
|
||||
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
|
||||
@ -26,7 +32,7 @@ import { getCachedUserInfo } from "../services/userService";
|
||||
function getTierIconUri(
|
||||
webview: vscode.Webview,
|
||||
context: vscode.ExtensionContext,
|
||||
tierCode?: string
|
||||
tierCode?: string,
|
||||
): string | undefined {
|
||||
if (!tierCode) {
|
||||
return undefined;
|
||||
@ -47,11 +53,11 @@ function getTierIconUri(
|
||||
const iconUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"titleIcon",
|
||||
iconFile
|
||||
)
|
||||
iconFile,
|
||||
),
|
||||
);
|
||||
|
||||
return iconUri.toString();
|
||||
@ -62,8 +68,36 @@ function getTierIconUri(
|
||||
*/
|
||||
export async function showICHelperPanel(
|
||||
context: vscode.ExtensionContext,
|
||||
viewColumn?: vscode.ViewColumn
|
||||
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", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户是否已登录
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
@ -74,7 +108,9 @@ export async function showICHelperPanel(
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
@ -84,7 +120,9 @@ export async function showICHelperPanel(
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
@ -100,9 +138,9 @@ 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"),
|
||||
],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 为面板生成唯一ID
|
||||
@ -110,55 +148,72 @@ export async function showICHelperPanel(
|
||||
.toString(36)
|
||||
.substr(2, 9)}`;
|
||||
(panel as any).__uniqueId = panelId;
|
||||
(panel as any).__context = context;
|
||||
|
||||
// 设置标签页图标
|
||||
panel.iconPath = vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"media",
|
||||
"icon.png"
|
||||
"icon.png",
|
||||
);
|
||||
|
||||
// 获取页面内图标URI
|
||||
const iconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png"),
|
||||
);
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Auto.png"
|
||||
)
|
||||
"Auto.png",
|
||||
),
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"lite.png"
|
||||
)
|
||||
"lite.png",
|
||||
),
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Sy.png"
|
||||
)
|
||||
"Sy.png",
|
||||
),
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Max.png"
|
||||
)
|
||||
"Max.png",
|
||||
),
|
||||
);
|
||||
|
||||
// 获取二维码图片URI
|
||||
const qrCodeUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"dist",
|
||||
"assets",
|
||||
"QRCode",
|
||||
"wx.png",
|
||||
),
|
||||
);
|
||||
|
||||
// 获取Logo URI
|
||||
const logoUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png"),
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
@ -167,7 +222,9 @@ export async function showICHelperPanel(
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString()
|
||||
maxIconUri.toString(),
|
||||
qrCodeUri.toString(),
|
||||
logoUri.toString(),
|
||||
);
|
||||
|
||||
// 获取并发送用户信息到 webview
|
||||
@ -178,20 +235,25 @@ export async function showICHelperPanel(
|
||||
if (userInfo) {
|
||||
// 使用缓存的用户信息
|
||||
console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo);
|
||||
console.log("[ICHelperPanel] Credits 余额:", userInfo.credits);
|
||||
const tierIconUrl = getTierIconUri(
|
||||
panel.webview,
|
||||
context,
|
||||
userInfo.membership?.tierCode
|
||||
userInfo.membership?.tierCode,
|
||||
);
|
||||
panel.webview.postMessage({
|
||||
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", [], {
|
||||
@ -200,7 +262,7 @@ export async function showICHelperPanel(
|
||||
if (session) {
|
||||
console.log(
|
||||
"[ICHelperPanel] 从 session 获取用户信息, account:",
|
||||
session.account
|
||||
session.account,
|
||||
);
|
||||
panel.webview.postMessage({
|
||||
command: "updateUserInfo",
|
||||
@ -216,6 +278,49 @@ export async function showICHelperPanel(
|
||||
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) {
|
||||
console.log("[ICHelperPanel] 检测到待发送消息,准备自动发送");
|
||||
|
||||
// 清除待发送消息
|
||||
await context.globalState.update("pendingMessage", undefined);
|
||||
|
||||
// 延迟发送,确保面板已完全初始化
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({
|
||||
command: "autoSendMessage",
|
||||
text: pendingMessage.text,
|
||||
mode: pendingMessage.mode,
|
||||
serviceTier: pendingMessage.serviceTier,
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (message) => {
|
||||
@ -233,12 +338,12 @@ export async function showICHelperPanel(
|
||||
try {
|
||||
const taskMeta = await historyManager.createTask(
|
||||
workspacePath,
|
||||
"新对话"
|
||||
"新对话",
|
||||
);
|
||||
historyManager.setPanelTask(
|
||||
panelId,
|
||||
taskMeta.taskId,
|
||||
workspacePath
|
||||
workspacePath,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("创建任务失败:", error);
|
||||
@ -249,6 +354,10 @@ export async function showICHelperPanel(
|
||||
// 切换到当前面板的任务上下文
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
|
||||
// 启动变更追踪会话
|
||||
const sessionId = `session_${panelId}_${Date.now()}`;
|
||||
startChangeSession(sessionId);
|
||||
|
||||
// 显示进度条
|
||||
panel.webview.postMessage({ type: "showProgress" });
|
||||
|
||||
@ -257,7 +366,8 @@ export async function showICHelperPanel(
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode,
|
||||
message.model // 传递服务等级
|
||||
message.model, // 传递服务等级
|
||||
message.contextItems, // 传递上下文项
|
||||
);
|
||||
break;
|
||||
case "readFile":
|
||||
@ -274,7 +384,7 @@ export async function showICHelperPanel(
|
||||
panel,
|
||||
message.filePath,
|
||||
message.searchText,
|
||||
message.replaceText
|
||||
message.replaceText,
|
||||
);
|
||||
break;
|
||||
case "insertCode":
|
||||
@ -288,7 +398,7 @@ export async function showICHelperPanel(
|
||||
if (message.vcdFilePath) {
|
||||
vscode.commands.executeCommand(
|
||||
"ic-coder.openVCDViewer",
|
||||
message.vcdFilePath
|
||||
message.vcdFilePath,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -307,7 +417,7 @@ export async function showICHelperPanel(
|
||||
loadConversationHistory(
|
||||
panel,
|
||||
message.offset || 0,
|
||||
message.limit || 10
|
||||
message.limit || 10,
|
||||
);
|
||||
break;
|
||||
case "selectConversation":
|
||||
@ -316,16 +426,17 @@ export async function showICHelperPanel(
|
||||
selectConversation(
|
||||
panel,
|
||||
message.conversationId,
|
||||
context.extensionPath
|
||||
context.extensionPath,
|
||||
);
|
||||
}
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
void handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput
|
||||
message.customInput,
|
||||
message.answers
|
||||
);
|
||||
break;
|
||||
// 新增:中止对话
|
||||
@ -365,29 +476,194 @@ export async function showICHelperPanel(
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "optimizePrompt":
|
||||
if (typeof message.prompt === "string") {
|
||||
void handleOptimizePrompt(panel, message.prompt);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: "提示词为空或格式错误",
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "logout":
|
||||
// 退出登录
|
||||
vscode.commands.executeCommand("ic-coder.logout");
|
||||
break;
|
||||
case "acceptChange":
|
||||
// 采纳变更
|
||||
if (message.changeId) {
|
||||
await handleAcceptChange(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
case "rejectChange":
|
||||
// 拒绝变更
|
||||
if (message.changeId) {
|
||||
await handleRejectChange(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
case "openFileDiff":
|
||||
// 打开文件 diff
|
||||
if (message.changeId) {
|
||||
await handleOpenFileDiff(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
case "checkInvitationCode":
|
||||
// 检查邀请码验证状态
|
||||
{
|
||||
// 先检查是否是试用用户
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "checkWelcomeModal":
|
||||
// 检查是否需要显示欢迎弹窗
|
||||
{
|
||||
console.log("[ICHelperPanel] 收到 checkWelcomeModal 消息");
|
||||
const userInfo = getCachedUserInfo();
|
||||
|
||||
console.log("[ICHelperPanel] 用户信息:", userInfo);
|
||||
console.log("[ICHelperPanel] isPluginTrial:", userInfo?.isPluginTrial);
|
||||
console.log("[ICHelperPanel] pluginTrialExpiresAt:", userInfo?.pluginTrialExpiresAt);
|
||||
|
||||
if (userInfo?.isPluginTrial === true) {
|
||||
// undefined 表示无效,不显示
|
||||
if (userInfo.pluginTrialExpiresAt === undefined) {
|
||||
console.log("[ICHelperPanel] pluginTrialExpiresAt 未设置,不显示欢迎弹窗");
|
||||
break;
|
||||
}
|
||||
|
||||
// null 表示长期有效,显示弹窗
|
||||
// 有值则检查是否过期
|
||||
if (userInfo.pluginTrialExpiresAt !== null) {
|
||||
const now = Date.now();
|
||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
console.log("[ICHelperPanel] 是否过期:", isExpired);
|
||||
|
||||
if (isExpired) {
|
||||
console.log("[ICHelperPanel] 试用已过期,不显示欢迎弹窗");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 未过期或长期有效(null),显示欢迎弹窗
|
||||
console.log("[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端");
|
||||
panel.webview.postMessage({
|
||||
command: "showWelcomeModal",
|
||||
});
|
||||
} else {
|
||||
console.log("[ICHelperPanel] 非试用用户");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "checkTrialExpiration":
|
||||
// 检查试用期是否过期
|
||||
{
|
||||
console.log("[ICHelperPanel] 收到 checkTrialExpiration 消息");
|
||||
const {
|
||||
TrialExpirationService,
|
||||
} = require("../services/trialExpirationService");
|
||||
const trialService = new TrialExpirationService(context, panel);
|
||||
const isExpired = await trialService.checkExpiration();
|
||||
console.log("[ICHelperPanel] 试用期过期状态:", isExpired);
|
||||
}
|
||||
break;
|
||||
case "verifyInvitationCode":
|
||||
// 验证邀请码
|
||||
{
|
||||
const {
|
||||
InvitationService,
|
||||
} = require("../services/invitationService");
|
||||
const result = await InvitationService.verifyCode(message.code);
|
||||
|
||||
if (result.success) {
|
||||
// 验证成功,保存状态
|
||||
await InvitationService.saveVerificationStatus(
|
||||
context,
|
||||
message.code,
|
||||
);
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeVerified",
|
||||
success: true,
|
||||
});
|
||||
// 延迟显示欢迎弹窗,确保邀请码弹窗已关闭
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({
|
||||
command: "showNdtWelcomeModal",
|
||||
});
|
||||
}, 300);
|
||||
} else {
|
||||
// 验证失败,返回错误信息
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeVerified",
|
||||
success: false,
|
||||
message: result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "openICCoder":
|
||||
// 跳转到 IC Coder 官网
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
case "openTutorial":
|
||||
// 打开使用教程
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse(
|
||||
"https://www.iccoder.com/guides/quick-start/first-task-plugin",
|
||||
),
|
||||
);
|
||||
break;
|
||||
case "openUserManual":
|
||||
// 打开用户手册
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
case "openUserFeedback":
|
||||
// 打开用户反馈二维码弹窗
|
||||
panel.webview.postMessage({
|
||||
command: "showFeedbackQRCode",
|
||||
});
|
||||
break;
|
||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
// 确认执行:切换到 Agent 模式
|
||||
// 确认执行:切换到 Agent 模式(UI 切换)
|
||||
panel.webview.postMessage({
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 获取当前会话的 taskId,用于复用知识图谱数据
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
||||
setPendingPlanExecution(
|
||||
panel,
|
||||
message.planTitle || "计划",
|
||||
context.extensionPath,
|
||||
taskId
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
||||
);
|
||||
}
|
||||
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
|
||||
} else if (
|
||||
message.action === "modify" ||
|
||||
message.action === "cancel"
|
||||
) {
|
||||
void handlePlanAction(
|
||||
panel,
|
||||
message.action,
|
||||
message.planTitle || "",
|
||||
context.extensionPath,
|
||||
message.model,
|
||||
);
|
||||
}
|
||||
break;
|
||||
// 添加文件上下文 - 显示工作区文件列表
|
||||
@ -402,7 +678,7 @@ export async function showICHelperPanel(
|
||||
// 获取工作区所有文件
|
||||
const files = await vscode.workspace.findFiles(
|
||||
"**/*",
|
||||
"**/node_modules/**"
|
||||
"**/node_modules/**",
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
@ -497,6 +773,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 = !!(
|
||||
@ -508,7 +801,7 @@ export async function showICHelperPanel(
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||
"打开文件夹"
|
||||
"打开文件夹",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "打开文件夹") {
|
||||
@ -522,33 +815,24 @@ export async function showICHelperPanel(
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
break;
|
||||
// 新增:处理面板宽度不足
|
||||
case "panelWidthInsufficient":
|
||||
// 关闭面板
|
||||
panel.dispose();
|
||||
vscode.window.showWarningMessage(
|
||||
"聊天面板宽度不足(最小 200px),已自动关闭"
|
||||
);
|
||||
case "openExternalUrl":
|
||||
// 打开外部链接
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
break;
|
||||
case "openICCoder":
|
||||
// 打开 IC Coder 官网
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
case "logout":
|
||||
// 退出登录(前端已有确认对话框)
|
||||
vscode.commands.executeCommand("ic-coder.logout");
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
|
||||
// 监听面板状态变化,检查宽度
|
||||
panel.onDidChangeViewState(
|
||||
(e) => {
|
||||
if (e.webviewPanel.visible) {
|
||||
// 请求前端检查宽度
|
||||
panel.webview.postMessage({
|
||||
command: "checkPanelWidth",
|
||||
minWidth: 200,
|
||||
});
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
context.subscriptions,
|
||||
);
|
||||
|
||||
// 面板关闭时清理任务映射
|
||||
@ -559,7 +843,7 @@ export async function showICHelperPanel(
|
||||
historyManager.removePanelTask(panelId);
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
context.subscriptions,
|
||||
);
|
||||
}
|
||||
|
||||
@ -569,18 +853,14 @@ export async function showICHelperPanel(
|
||||
async function getVCDFileInfo(
|
||||
panel: vscode.WebviewPanel,
|
||||
vcdFilePath: string,
|
||||
containerId: string
|
||||
containerId: string,
|
||||
) {
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
console.log(`[getVCDFileInfo] 开始解析 VCD 文件: ${vcdFilePath}`);
|
||||
console.log(`[getVCDFileInfo] containerId: ${containerId}`);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
console.error(`[getVCDFileInfo] 文件不存在: ${vcdFilePath}`);
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
@ -619,14 +899,8 @@ async function getVCDFileInfo(
|
||||
timeRange = `${minTime} - ${maxTime}`;
|
||||
}
|
||||
|
||||
// 解析信号的真实数据
|
||||
// 增加到20个信号,以便显示更多波形(平衡性能和完整性)
|
||||
const signals = parseVCDSignals(content, 20);
|
||||
|
||||
console.log(`[getVCDFileInfo] 解析到 ${signals.length} 个有效信号`);
|
||||
signals.forEach((sig, idx) => {
|
||||
console.log(`[getVCDFileInfo] 信号${idx + 1}: ${sig.name}, 值变化数: ${sig.values.length}`);
|
||||
});
|
||||
// 解析前几个信号的真实数据
|
||||
const signals = parseVCDSignals(content, 3); // 只解析前3个信号
|
||||
|
||||
// 发送信息回前端
|
||||
panel.webview.postMessage({
|
||||
@ -714,13 +988,10 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
// 解析信号值变化
|
||||
// 格式1: 单比特信号 "0!" 或 "1!"
|
||||
// 格式2: 多比特信号 "b1010 !"
|
||||
// 转义标识符中的特殊字符
|
||||
const escapedId = signalDef.identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
if (signalDef.width === 1) {
|
||||
// 单比特信号
|
||||
const singleBitMatch = trimmedLine.match(
|
||||
new RegExp(`^([01xz])${escapedId}$`)
|
||||
new RegExp(`^([01xz])${signalDef.identifier}$`),
|
||||
);
|
||||
if (singleBitMatch) {
|
||||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||
@ -728,7 +999,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
} else {
|
||||
// 多比特信号
|
||||
const multiBitMatch = trimmedLine.match(
|
||||
new RegExp(`^b([01xz]+)\\s+${escapedId}$`)
|
||||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`),
|
||||
);
|
||||
if (multiBitMatch) {
|
||||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||
@ -741,15 +1012,12 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
}
|
||||
}
|
||||
|
||||
// 只添加有值变化数据的信号
|
||||
if (values.length > 0) {
|
||||
signals.push({
|
||||
name: signalDef.name,
|
||||
identifier: signalDef.identifier,
|
||||
width: signalDef.width,
|
||||
values: values,
|
||||
});
|
||||
}
|
||||
signals.push({
|
||||
name: signalDef.name,
|
||||
identifier: signalDef.identifier,
|
||||
width: signalDef.width,
|
||||
values: values,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析 VCD 信号数据失败:", error);
|
||||
@ -764,7 +1032,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
async function loadConversationHistory(
|
||||
panel: vscode.WebviewPanel,
|
||||
offset: number = 0,
|
||||
limit: number = 10
|
||||
limit: number = 10,
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
@ -785,7 +1053,7 @@ async function loadConversationHistory(
|
||||
const result = await historyManager.getConversationHistoryList(
|
||||
workspacePath,
|
||||
offset,
|
||||
limit
|
||||
limit,
|
||||
);
|
||||
|
||||
// 发送会话历史到前端
|
||||
@ -813,7 +1081,7 @@ async function loadConversationHistory(
|
||||
async function selectConversation(
|
||||
panel: vscode.WebviewPanel,
|
||||
taskId: string,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
@ -827,12 +1095,12 @@ async function selectConversation(
|
||||
// 加载任务会话
|
||||
const taskSession = await historyManager.loadTaskSession(
|
||||
workspacePath,
|
||||
taskId
|
||||
taskId,
|
||||
);
|
||||
|
||||
if (!taskSession) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`
|
||||
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -993,7 +1261,7 @@ async function selectConversation(
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`已加载会话: ${taskSession.meta.taskName}`
|
||||
`已加载会话: ${taskSession.meta.taskName}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("选择会话失败:", error);
|
||||
|
||||
@ -6,8 +6,13 @@ import { VCDFileServer } from "../services/vcdFileServer";
|
||||
/**
|
||||
* VCD 波形查看器自定义编辑器提供者
|
||||
*/
|
||||
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider {
|
||||
public static register(context: vscode.ExtensionContext, vcdFileServer: VCDFileServer): vscode.Disposable {
|
||||
export class VCDViewerEditorProvider
|
||||
implements vscode.CustomReadonlyEditorProvider
|
||||
{
|
||||
public static register(
|
||||
context: vscode.ExtensionContext,
|
||||
vcdFileServer: VCDFileServer,
|
||||
): vscode.Disposable {
|
||||
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
|
||||
const providerRegistration = vscode.window.registerCustomEditorProvider(
|
||||
"ic-coder.vcdViewer",
|
||||
@ -16,20 +21,20 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
||||
webviewOptions: {
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
return providerRegistration;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly vcdFileServer: VCDFileServer
|
||||
private readonly vcdFileServer: VCDFileServer,
|
||||
) {}
|
||||
|
||||
async openCustomDocument(
|
||||
uri: vscode.Uri,
|
||||
openContext: vscode.CustomDocumentOpenContext,
|
||||
token: vscode.CancellationToken
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<vscode.CustomDocument> {
|
||||
return {
|
||||
uri,
|
||||
@ -40,7 +45,7 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
||||
async resolveCustomEditor(
|
||||
document: vscode.CustomDocument,
|
||||
webviewPanel: vscode.WebviewPanel,
|
||||
token: vscode.CancellationToken
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<void> {
|
||||
webviewPanel.webview.options = {
|
||||
enableScripts: true,
|
||||
@ -52,7 +57,7 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
||||
webviewPanel,
|
||||
this.context.extensionUri,
|
||||
document.uri.fsPath,
|
||||
this.vcdFileServer
|
||||
this.vcdFileServer,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -68,7 +73,11 @@ export class VCDViewerPanel {
|
||||
private _currentVcdPath: string | undefined;
|
||||
private _vcdFileServer: VCDFileServer | undefined;
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) {
|
||||
private constructor(
|
||||
panel: vscode.WebviewPanel,
|
||||
extensionUri: vscode.Uri,
|
||||
vcdFileServer?: VCDFileServer,
|
||||
) {
|
||||
this._panel = panel;
|
||||
this._extensionUri = extensionUri;
|
||||
this._vcdFileServer = vcdFileServer;
|
||||
@ -91,7 +100,10 @@ export class VCDViewerPanel {
|
||||
break;
|
||||
case "loaded":
|
||||
// Surfer iframe 加载完成,发送 VCD 文件
|
||||
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
|
||||
console.log(
|
||||
"[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:",
|
||||
this._currentVcdPath,
|
||||
);
|
||||
if (this._currentVcdPath) {
|
||||
this.sendVcdToSurfer(this._currentVcdPath);
|
||||
}
|
||||
@ -99,14 +111,18 @@ export class VCDViewerPanel {
|
||||
}
|
||||
},
|
||||
null,
|
||||
this._disposables
|
||||
this._disposables,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或显示 VCD 查看器面板
|
||||
*/
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||
public static createOrShow(
|
||||
extensionUri: vscode.Uri,
|
||||
vcdFilePath?: string,
|
||||
vcdFileServer?: VCDFileServer,
|
||||
) {
|
||||
// 在当前活动编辑器旁边打开新列
|
||||
const column = vscode.ViewColumn.Beside;
|
||||
|
||||
@ -128,10 +144,14 @@ export class VCDViewerPanel {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [extensionUri],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(
|
||||
panel,
|
||||
extensionUri,
|
||||
vcdFileServer,
|
||||
);
|
||||
|
||||
// 如果提供了 VCD 文件路径,加载它
|
||||
if (vcdFilePath) {
|
||||
@ -146,7 +166,7 @@ export class VCDViewerPanel {
|
||||
panel: vscode.WebviewPanel,
|
||||
extensionUri: vscode.Uri,
|
||||
vcdFilePath: string,
|
||||
vcdFileServer?: VCDFileServer
|
||||
vcdFileServer?: VCDFileServer,
|
||||
) {
|
||||
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||
viewer.loadVCDFile(vcdFilePath);
|
||||
@ -172,14 +192,14 @@ export class VCDViewerPanel {
|
||||
|
||||
// 更新面板标题
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
this._panel.title = `Surfer 波形查看器 - ${fileName}`;
|
||||
this._panel.title = `波形查看器 - ${fileName}`;
|
||||
|
||||
// 设置 HTML 内容
|
||||
this._panel.webview.html = this._getWebviewContent();
|
||||
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -190,8 +210,8 @@ export class VCDViewerPanel {
|
||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||
try {
|
||||
// 读取 VCD 文件
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
||||
const lines = buffer.split('\n');
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
const scopeNames: string[] = [];
|
||||
let scopeDepth = 0;
|
||||
@ -201,7 +221,7 @@ export class VCDViewerPanel {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// 遇到 $enddefinitions 就停止解析
|
||||
if (trimmed.startsWith('$enddefinitions')) {
|
||||
if (trimmed.startsWith("$enddefinitions")) {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -212,22 +232,22 @@ export class VCDViewerPanel {
|
||||
const scopeName = scopeMatch[2];
|
||||
|
||||
// 记录顶层 module (depth = 0)
|
||||
if (scopeDepth === 0 && scopeType === 'module') {
|
||||
if (scopeDepth === 0 && scopeType === "module") {
|
||||
scopeStack.push(scopeName);
|
||||
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
|
||||
}
|
||||
// 记录顶层下的直接子模块 (depth = 1)
|
||||
else if (scopeDepth === 1 && scopeType === 'module') {
|
||||
else if (scopeDepth === 1 && scopeType === "module") {
|
||||
const fullPath = [...scopeStack, scopeName];
|
||||
scopeNames.push(fullPath.join('.'));
|
||||
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.'));
|
||||
scopeNames.push(fullPath.join("."));
|
||||
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join("."));
|
||||
}
|
||||
|
||||
scopeDepth++;
|
||||
}
|
||||
|
||||
// 遇到 $upscope 减少深度
|
||||
if (trimmed.startsWith('$upscope')) {
|
||||
if (trimmed.startsWith("$upscope")) {
|
||||
scopeDepth--;
|
||||
if (scopeDepth === 0) {
|
||||
scopeStack.pop();
|
||||
@ -277,7 +297,7 @@ export class VCDViewerPanel {
|
||||
} catch (error) {
|
||||
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
|
||||
vscode.window.showErrorMessage(
|
||||
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -352,13 +372,23 @@ export class VCDViewerPanel {
|
||||
private _getWebviewContent(): string {
|
||||
// 获取 surfer 资源 URI
|
||||
const surferJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js"),
|
||||
);
|
||||
const surferWasmUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
|
||||
vscode.Uri.joinPath(
|
||||
this._extensionUri,
|
||||
"media",
|
||||
"surfer",
|
||||
"surfer_bg.wasm",
|
||||
),
|
||||
);
|
||||
const integrationJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js")
|
||||
vscode.Uri.joinPath(
|
||||
this._extensionUri,
|
||||
"media",
|
||||
"surfer",
|
||||
"integration.js",
|
||||
),
|
||||
);
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
@ -367,7 +397,7 @@ export class VCDViewerPanel {
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
|
||||
<title>Surfer 波形查看器</title>
|
||||
<title>波形查看器</title>
|
||||
|
||||
<script>
|
||||
// 获取 VS Code API(只能调用一次)
|
||||
|
||||
153
src/panels/WelcomePanel.ts
Normal file
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 欢迎引导面板
|
||||
* 功能:插件试用用户首次登录显示使用教程
|
||||
* 依赖:vscode
|
||||
* 使用场景:试用用户首次登录时显示
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class WelcomePanel {
|
||||
public static currentPanel: WelcomePanel | undefined;
|
||||
private readonly _panel: vscode.WebviewPanel;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel) {
|
||||
this._panel = panel;
|
||||
this._panel.webview.html = this.getHtmlContent();
|
||||
|
||||
// 监听来自 webview 的消息
|
||||
this._panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
if (message.command === 'close') {
|
||||
this._panel.dispose();
|
||||
}
|
||||
},
|
||||
null,
|
||||
this._disposables
|
||||
);
|
||||
|
||||
// 监听关闭事件
|
||||
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||
}
|
||||
|
||||
public static render(context: vscode.ExtensionContext) {
|
||||
// 避免重复显示
|
||||
if (WelcomePanel.currentPanel) {
|
||||
WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'icCoderWelcome',
|
||||
'欢迎使用 IC Coder',
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true
|
||||
}
|
||||
);
|
||||
|
||||
WelcomePanel.currentPanel = new WelcomePanel(panel);
|
||||
}
|
||||
|
||||
private getHtmlContent(): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>欢迎使用 IC Coder</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 40px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--vscode-foreground);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
h1 {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.welcome-message {
|
||||
font-size: 16px;
|
||||
margin-bottom: 30px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.step {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
.step h3 {
|
||||
margin-top: 0;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
.step p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.button {
|
||||
padding: 12px 24px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🎉 欢迎使用 IC Coder!</h1>
|
||||
<p class="welcome-message">
|
||||
您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
|
||||
</p>
|
||||
|
||||
<div class="step">
|
||||
<h3>📝 步骤 1:打开聊天面板</h3>
|
||||
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<h3>💬 步骤 2:输入您的需求</h3>
|
||||
<p>描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案</p>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<h3>🔬 步骤 3:运行仿真</h3>
|
||||
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
|
||||
</div>
|
||||
|
||||
<button class="button" onclick="closePanel()">开始使用</button>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
function closePanel() {
|
||||
vscode.postMessage({ command: 'close' });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
WelcomePanel.currentPanel = undefined;
|
||||
this._panel.dispose();
|
||||
while (this._disposables.length) {
|
||||
const disposable = this._disposables.pop();
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,22 +2,47 @@
|
||||
* API 客户端
|
||||
* 封装与后端的 HTTP 通信
|
||||
*/
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl, getConfig } from '../config/settings';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse, UserInfoResponse } from '../types/api';
|
||||
import * as vscode from "vscode";
|
||||
import * as https from "https";
|
||||
import * as http from "http";
|
||||
import { URL } from "url";
|
||||
import { getApiUrl, getConfig } from "../config/settings";
|
||||
import type {
|
||||
ToolCallResult,
|
||||
AnswerRequest,
|
||||
ToolResultResponse,
|
||||
AnswerResponse,
|
||||
ToolConfirmResponse,
|
||||
UserInfoResponse,
|
||||
InvitationVerifyRequest,
|
||||
InvitationVerifyResponse,
|
||||
InvitationStatusResponse,
|
||||
} from "../types/api";
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
*/
|
||||
interface RequestOptions {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录的 Token
|
||||
*/
|
||||
async function getAuthToken(): Promise<string | undefined> {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
silent: true,
|
||||
});
|
||||
return session?.accessToken;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*/
|
||||
@ -25,7 +50,10 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
const url = new URL(getApiUrl(path));
|
||||
const { timeout } = getConfig();
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
// 自动获取 Token
|
||||
const token = await getAuthToken();
|
||||
|
||||
const isHttps = url.protocol === "https:";
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
@ -34,48 +62,84 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
path: url.pathname + url.search,
|
||||
method: options.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
},
|
||||
timeout: options.timeout || timeout
|
||||
timeout: options.timeout || timeout,
|
||||
};
|
||||
|
||||
console.log("[HTTP] 请求详情:", {
|
||||
url: url.toString(),
|
||||
method: options.method,
|
||||
headers: requestOptions.headers,
|
||||
hasToken: !!token,
|
||||
body: options.body,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
let data = "";
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
// console.log('[HTTP] 响应状态码:', res.statusCode);
|
||||
// console.log('[HTTP] 响应头:', res.headers);
|
||||
|
||||
res.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
res.on("end", () => {
|
||||
console.log("[HTTP] 响应体:", data);
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
// console.log('[HTTP] 解析后的响应:', JSON.stringify(json, null, 2));
|
||||
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
console.log("[HTTP] 请求成功");
|
||||
resolve(json as T);
|
||||
} else {
|
||||
reject(new Error(json.error || json.message || `HTTP ${res.statusCode}`));
|
||||
console.error("[HTTP] 请求失败:", {
|
||||
statusCode: res.statusCode,
|
||||
error: json.error,
|
||||
message: json.message,
|
||||
msg: json.msg,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
json.error ||
|
||||
json.message ||
|
||||
json.msg ||
|
||||
`HTTP ${res.statusCode}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// console.error('[HTTP] 解析响应失败:', e);
|
||||
// console.error('[HTTP] 原始响应:', data);
|
||||
reject(new Error(`解析响应失败: ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
req.on("error", (error) => {
|
||||
// console.error('[HTTP] 请求错误:', error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.on("timeout", () => {
|
||||
// console.error('[HTTP] 请求超时');
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
reject(new Error("请求超时"));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(JSON.stringify(options.body));
|
||||
const bodyStr = JSON.stringify(options.body);
|
||||
// console.log('[HTTP] 发送请求体:', bodyStr);
|
||||
req.write(bodyStr);
|
||||
}
|
||||
|
||||
req.end();
|
||||
// console.log('[HTTP] 请求已发送');
|
||||
});
|
||||
}
|
||||
|
||||
@ -83,11 +147,13 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
* 提交工具执行结果
|
||||
* POST /api/tool/result
|
||||
*/
|
||||
export async function submitToolResult(result: ToolCallResult): Promise<ToolResultResponse> {
|
||||
export async function submitToolResult(
|
||||
result: ToolCallResult,
|
||||
): Promise<ToolResultResponse> {
|
||||
console.log(`[API] 提交工具结果: callId=${result.id}`);
|
||||
return request<ToolResultResponse>('/api/tool/result', {
|
||||
method: 'POST',
|
||||
body: result
|
||||
return request<ToolResultResponse>("/api/tool/result", {
|
||||
method: "POST",
|
||||
body: result,
|
||||
});
|
||||
}
|
||||
|
||||
@ -95,11 +161,13 @@ export async function submitToolResult(result: ToolCallResult): Promise<ToolResu
|
||||
* 提交用户回答
|
||||
* POST /api/task/answer
|
||||
*/
|
||||
export async function submitAnswer(answer: AnswerRequest): Promise<AnswerResponse> {
|
||||
export async function submitAnswer(
|
||||
answer: AnswerRequest,
|
||||
): Promise<AnswerResponse> {
|
||||
console.log(`[API] 提交用户回答: askId=${answer.askId}`);
|
||||
return request<AnswerResponse>('/api/task/answer', {
|
||||
method: 'POST',
|
||||
body: answer
|
||||
return request<AnswerResponse>("/api/task/answer", {
|
||||
method: "POST",
|
||||
body: answer,
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,11 +175,15 @@ export async function submitAnswer(answer: AnswerRequest): Promise<AnswerRespons
|
||||
* 提交工具确认响应(Ask 模式)
|
||||
* POST /api/tool/confirm
|
||||
*/
|
||||
export async function submitToolConfirm(response: ToolConfirmResponse): Promise<ToolResultResponse> {
|
||||
console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`);
|
||||
return request<ToolResultResponse>('/api/tool/confirm', {
|
||||
method: 'POST',
|
||||
body: response
|
||||
export async function submitToolConfirm(
|
||||
response: ToolConfirmResponse,
|
||||
): Promise<ToolResultResponse> {
|
||||
console.log(
|
||||
`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`,
|
||||
);
|
||||
return request<ToolResultResponse>("/api/tool/confirm", {
|
||||
method: "POST",
|
||||
body: response,
|
||||
});
|
||||
}
|
||||
|
||||
@ -120,9 +192,9 @@ export async function submitToolConfirm(response: ToolConfirmResponse): Promise<
|
||||
* GET /api/dialog/health
|
||||
*/
|
||||
export async function healthCheck(): Promise<{ status: string }> {
|
||||
return request<{ status: string }>('/api/dialog/health', {
|
||||
method: 'GET',
|
||||
timeout: 5000
|
||||
return request<{ status: string }>("/api/dialog/health", {
|
||||
method: "GET",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
@ -149,9 +221,9 @@ export interface StopDialogResponse {
|
||||
*/
|
||||
export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
|
||||
console.log(`[API] 停止对话: taskId=${taskId}`);
|
||||
return request<StopDialogResponse>('/api/dialog/stop', {
|
||||
method: 'POST',
|
||||
body: { taskId }
|
||||
return request<StopDialogResponse>("/api/dialog/stop", {
|
||||
method: "POST",
|
||||
body: { taskId },
|
||||
});
|
||||
}
|
||||
|
||||
@ -167,11 +239,13 @@ export interface CompactDialogResponse {
|
||||
* 手动压缩对话历史
|
||||
* POST /api/dialog/compact
|
||||
*/
|
||||
export async function compactDialog(taskId: string): Promise<CompactDialogResponse> {
|
||||
export async function compactDialog(
|
||||
taskId: string,
|
||||
): Promise<CompactDialogResponse> {
|
||||
console.log(`[API] 压缩对话: taskId=${taskId}`);
|
||||
return request<CompactDialogResponse>('/api/dialog/compact', {
|
||||
method: 'POST',
|
||||
body: { taskId }
|
||||
return request<CompactDialogResponse>("/api/dialog/compact", {
|
||||
method: "POST",
|
||||
body: { taskId },
|
||||
});
|
||||
}
|
||||
|
||||
@ -180,37 +254,44 @@ export async function compactDialog(taskId: string): Promise<CompactDialogRespon
|
||||
*/
|
||||
export function createSuccessResult(id: number, text: string): ToolCallResult {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
content: [{ type: 'text', text }],
|
||||
isError: false
|
||||
}
|
||||
content: [{ type: "text", text }],
|
||||
isError: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建业务错误的工具结果(如编译失败)
|
||||
*/
|
||||
export function createBusinessErrorResult(id: number, errorMessage: string): ToolCallResult {
|
||||
export function createBusinessErrorResult(
|
||||
id: number,
|
||||
errorMessage: string,
|
||||
): ToolCallResult {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
content: [{ type: 'text', text: errorMessage }],
|
||||
isError: true
|
||||
}
|
||||
content: [{ type: "text", text: errorMessage }],
|
||||
isError: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统错误的工具结果
|
||||
*/
|
||||
export function createSystemErrorResult(id: number, code: number, message: string): ToolCallResult {
|
||||
export function createSystemErrorResult(
|
||||
id: number,
|
||||
code: number,
|
||||
message: string,
|
||||
): ToolCallResult {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code, message }
|
||||
error: { code, message },
|
||||
};
|
||||
}
|
||||
|
||||
@ -219,8 +300,96 @@ export function createSystemErrorResult(id: number, code: number, message: strin
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(): Promise<UserInfoResponse> {
|
||||
console.log('[API] 获取用户信息');
|
||||
return request<UserInfoResponse>('/system/user/getInfo', {
|
||||
method: 'GET'
|
||||
console.log("[API] 获取用户信息");
|
||||
return request<UserInfoResponse>("/system/user/getInfo", {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/** 余额查询响应 */
|
||||
export interface CreditBalanceResponse {
|
||||
success: boolean;
|
||||
balance?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户资源点余额
|
||||
* GET /api/dialog/balance?userId=xxx
|
||||
*/
|
||||
export async function getCreditBalance(
|
||||
userId: string,
|
||||
): Promise<CreditBalanceResponse> {
|
||||
console.log("[API] 查询余额: userId=", userId);
|
||||
return request<CreditBalanceResponse>(
|
||||
`/api/dialog/balance?userId=${userId}`,
|
||||
{
|
||||
method: "GET",
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邀请码
|
||||
* POST /api/invitation/verify
|
||||
*/
|
||||
export async function verifyInvitationCode(
|
||||
code: string,
|
||||
): Promise<InvitationVerifyResponse> {
|
||||
// console.log('[API] 验证邀请码 - 开始');
|
||||
console.log("[API] 邀请码:", code);
|
||||
const body: InvitationVerifyRequest = { code };
|
||||
console.log("[API] 请求体:", JSON.stringify(body));
|
||||
|
||||
try {
|
||||
const response = await request<InvitationVerifyResponse>(
|
||||
"/api/invitation/verify",
|
||||
{
|
||||
method: "POST",
|
||||
body,
|
||||
},
|
||||
);
|
||||
console.log("[API] 验证邀请码 - 响应:", JSON.stringify(response));
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("[API] 验证邀请码 - 错误:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询邀请码验证状态
|
||||
* GET /api/invitation/status
|
||||
*/
|
||||
export async function checkInvitationStatus(): Promise<InvitationStatusResponse> {
|
||||
console.log("[API] 查询邀请码验证状态");
|
||||
return request<InvitationStatusResponse>("/api/invitation/status", {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置邀请码验证状态(退出登录时调用)
|
||||
* POST /api/invitation/reset
|
||||
*/
|
||||
export async function resetInvitationVerification(): Promise<{
|
||||
code: number;
|
||||
msg: string;
|
||||
}> {
|
||||
console.log("[API] 重置邀请码验证状态");
|
||||
try {
|
||||
const response = await request<{ code: number; msg: string }>(
|
||||
"/api/invitation/reset",
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
console.log("[API] 重置邀请码验证状态 - 响应:", JSON.stringify(response));
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.warn("[API] 重置邀请码验证状态 - 错误:", error);
|
||||
// 即使失败也不影响退出登录流程
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
210
src/services/changeTracker.ts
Normal file
@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 文件变更追踪服务
|
||||
* 功能:收集和管理 AI 修改文件的变更记录
|
||||
* 依赖:types/fileChanges
|
||||
* 使用场景:在文件操作时记录变更,供用户审查
|
||||
*/
|
||||
|
||||
import { FileChange, ChangeSession } from '../types/fileChanges';
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
class ChangeTrackerService {
|
||||
private currentSession: ChangeSession | null = null;
|
||||
private changeListeners: Array<(session: ChangeSession) => void> = [];
|
||||
|
||||
/**
|
||||
* 开始新的变更会话
|
||||
*/
|
||||
startSession(sessionId: string): void {
|
||||
// 如果已有 session(无论状态),重用并重置为 active
|
||||
if (this.currentSession) {
|
||||
this.currentSession.status = 'active';
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentSession = {
|
||||
sessionId,
|
||||
startTime: Date.now(),
|
||||
changes: [],
|
||||
status: 'active'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录文件变更
|
||||
*/
|
||||
trackChange(filePath: string, oldContent: string, newContent: string): string {
|
||||
if (!this.currentSession) {
|
||||
this.startSession(`session_${Date.now()}`);
|
||||
}
|
||||
|
||||
const changeId = `change_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// 判断变更类型
|
||||
let changeType: 'create' | 'modify' | 'delete';
|
||||
if (oldContent === '' && newContent !== '') {
|
||||
changeType = 'create';
|
||||
} else if (oldContent !== '' && newContent === '') {
|
||||
changeType = 'delete';
|
||||
} else {
|
||||
changeType = 'modify';
|
||||
}
|
||||
|
||||
const change: FileChange = {
|
||||
filePath,
|
||||
oldContent,
|
||||
newContent,
|
||||
timestamp: Date.now(),
|
||||
changeType,
|
||||
changeId
|
||||
};
|
||||
|
||||
this.currentSession!.changes.push(change);
|
||||
this.notifyListeners();
|
||||
|
||||
return changeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前会话
|
||||
*/
|
||||
endSession(): ChangeSession | null {
|
||||
if (this.currentSession && this.currentSession.changes.length > 0) {
|
||||
this.currentSession.status = 'completed';
|
||||
const session = this.currentSession;
|
||||
this.notifyListeners();
|
||||
return session;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话
|
||||
*/
|
||||
getCurrentSession(): ChangeSession | null {
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前会话
|
||||
*/
|
||||
clearSession(): void {
|
||||
this.currentSession = null;
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定的变更
|
||||
*/
|
||||
removeChange(changeId: string): boolean {
|
||||
if (!this.currentSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const index = this.currentSession.changes.findIndex(c => c.changeId === changeId);
|
||||
if (index !== -1) {
|
||||
this.currentSession.changes.splice(index, 1);
|
||||
this.notifyListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听变更
|
||||
*/
|
||||
onChangeUpdate(listener: (session: ChangeSession) => void): void {
|
||||
this.changeListeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有监听器
|
||||
*/
|
||||
private notifyListeners(): void {
|
||||
if (this.currentSession) {
|
||||
this.changeListeners.forEach(listener => listener(this.currentSession!));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 采纳变更(保存文件)
|
||||
*/
|
||||
async acceptChange(changeId: string): Promise<boolean> {
|
||||
if (!this.currentSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const change = this.currentSession.changes.find(c => c.changeId === changeId);
|
||||
if (!change) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath);
|
||||
|
||||
// 如果是删除操作,删除文件
|
||||
if (change.changeType === 'delete') {
|
||||
if (fs.existsSync(absolutePath)) {
|
||||
await fs.promises.unlink(absolutePath);
|
||||
}
|
||||
} else {
|
||||
// 创建或修改文件
|
||||
await fs.promises.writeFile(absolutePath, change.newContent, 'utf-8');
|
||||
}
|
||||
|
||||
this.removeChange(changeId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[ChangeTracker] 采纳变更失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝变更(恢复旧内容)
|
||||
*/
|
||||
async rejectChange(changeId: string): Promise<boolean> {
|
||||
if (!this.currentSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const change = this.currentSession.changes.find(c => c.changeId === changeId);
|
||||
if (!change) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath);
|
||||
|
||||
// 如果是新建文件,删除它
|
||||
if (change.changeType === 'create') {
|
||||
if (fs.existsSync(absolutePath)) {
|
||||
await fs.promises.unlink(absolutePath);
|
||||
}
|
||||
} else {
|
||||
// 恢复旧内容
|
||||
await fs.promises.writeFile(absolutePath, change.oldContent, 'utf-8');
|
||||
}
|
||||
|
||||
this.removeChange(changeId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[ChangeTracker] 拒绝变更失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const changeTracker = new ChangeTrackerService();
|
||||
269
src/services/creditsService.ts
Normal file
@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 资源点余额管理服务
|
||||
* 负责缓存余额、主动查询、发送前检测
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getStrangeLoopApiUrl } from '../config/settings';
|
||||
import { getCachedUserInfo } from './userService';
|
||||
|
||||
/** 低余额阈值 */
|
||||
const LOW_CREDIT_THRESHOLD = 5;
|
||||
|
||||
/** 缓存的余额 */
|
||||
let cachedBalance: number | null = null;
|
||||
|
||||
/** 最后更新时间 */
|
||||
let lastUpdateTime: number = 0;
|
||||
|
||||
/** 缓存有效期(5分钟) */
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
/** ExtensionContext 用于持久化存储 */
|
||||
let extensionContext: vscode.ExtensionContext | null = null;
|
||||
|
||||
/** 余额更新回调函数 */
|
||||
let onBalanceUpdateCallback: ((balance: number) => void) | null = null;
|
||||
|
||||
/**
|
||||
* 初始化 Credits 服务(设置 context)
|
||||
*/
|
||||
export function initCreditsService(context: vscode.ExtensionContext): void {
|
||||
extensionContext = context;
|
||||
// 从持久化存储加载余额
|
||||
const savedBalance = extensionContext.globalState.get<number>('icCoderCreditsBalance');
|
||||
if (savedBalance !== undefined) {
|
||||
cachedBalance = savedBalance;
|
||||
lastUpdateTime = Date.now();
|
||||
console.log('[CreditsService] 从持久化存储加载余额:', savedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置余额更新回调
|
||||
*/
|
||||
export function setBalanceUpdateCallback(callback: (balance: number) => void): void {
|
||||
onBalanceUpdateCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存余额到持久化存储
|
||||
*/
|
||||
async function saveBalance(balance: number): Promise<void> {
|
||||
if (extensionContext) {
|
||||
await extensionContext.globalState.update('icCoderCreditsBalance', balance);
|
||||
console.log('[CreditsService] 余额已保存到持久化存储:', balance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存的余额(从 SSE credit_update 事件调用)
|
||||
*/
|
||||
export function updateCachedBalance(balance: number): void {
|
||||
cachedBalance = balance;
|
||||
lastUpdateTime = Date.now();
|
||||
console.log('[CreditsService] 余额已更新:', balance);
|
||||
// 异步保存到持久化存储
|
||||
saveBalance(balance).catch(err => {
|
||||
console.error('[CreditsService] 保存余额失败:', err);
|
||||
});
|
||||
// 通知前端更新余额显示
|
||||
if (onBalanceUpdateCallback) {
|
||||
onBalanceUpdateCallback(balance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的余额
|
||||
*/
|
||||
export function getCachedBalance(): number | null {
|
||||
return cachedBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否有效
|
||||
*/
|
||||
function isCacheValid(): boolean {
|
||||
if (cachedBalance === null) return false;
|
||||
return Date.now() - lastUpdateTime < CACHE_TTL_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* StrangeLoop 余额响应类型
|
||||
*/
|
||||
interface StrangeLoopBalanceResponse {
|
||||
userId?: number;
|
||||
availableCredits?: number;
|
||||
totalCredits?: number;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动查询余额(直接调用 StrangeLoop 接口)
|
||||
*/
|
||||
export async function fetchBalance(): Promise<number | null> {
|
||||
try {
|
||||
// 获取 JWT token
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
if (!session?.accessToken) {
|
||||
console.warn('[CreditsService] 无法查询余额:未登录');
|
||||
return null;
|
||||
}
|
||||
|
||||
return await fetchBalanceWithToken(session.accessToken);
|
||||
} catch (error) {
|
||||
console.error('[CreditsService] 查询余额异常:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定 token 查询余额(登录过程中使用)
|
||||
*/
|
||||
export async function fetchBalanceWithToken(token: string): Promise<number | null> {
|
||||
try {
|
||||
console.log('[CreditsService] 开始查询余额,token 长度:', token.length);
|
||||
|
||||
// 直接调用 StrangeLoop 的 /api/credit/balance 接口
|
||||
const response = await callStrangeLoopBalance(token);
|
||||
|
||||
if (response.availableCredits !== undefined) {
|
||||
const balance = response.availableCredits;
|
||||
updateCachedBalance(balance);
|
||||
console.log('[CreditsService] 余额查询成功:', balance);
|
||||
return balance;
|
||||
} else {
|
||||
console.warn('[CreditsService] 查询余额失败:', response.error || response.message);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CreditsService] 查询余额异常:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 StrangeLoop 余额接口
|
||||
*/
|
||||
async function callStrangeLoopBalance(token: string): Promise<StrangeLoopBalanceResponse> {
|
||||
const urlStr = getStrangeLoopApiUrl('/strangeloop/api/credit/balance');
|
||||
const url = new URL(urlStr);
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
// 余额查询使用固定短超时,避免阻塞发送前检查
|
||||
const BALANCE_TIMEOUT_MS = 5000;
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: BALANCE_TIMEOUT_MS
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('[CreditsService] 响应状态码:', res.statusCode);
|
||||
console.log('[CreditsService] 响应内容:', data);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as StrangeLoopBalanceResponse);
|
||||
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
// 登录过期或无权限
|
||||
resolve({ error: '登录已过期,请重新登录' });
|
||||
} else {
|
||||
resolve({ error: json.error || json.message || json.msg || `HTTP ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({ error: `解析响应失败: ${data}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前余额(优先使用缓存,过期则主动查询)
|
||||
*/
|
||||
export async function getBalance(): Promise<number | null> {
|
||||
if (isCacheValid()) {
|
||||
return cachedBalance;
|
||||
}
|
||||
return await fetchBalance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查余额是否足够发送消息
|
||||
* @returns { allowed: boolean, balance: number | null, message?: string }
|
||||
*/
|
||||
export async function checkBalanceBeforeSend(): Promise<{
|
||||
allowed: boolean;
|
||||
balance: number | null;
|
||||
message?: string;
|
||||
}> {
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (!userInfo) {
|
||||
// 未登录,允许发送(后端会处理)
|
||||
return { allowed: true, balance: null };
|
||||
}
|
||||
|
||||
const balance = await getBalance();
|
||||
|
||||
if (balance === null) {
|
||||
// 无法获取余额,允许发送(后端会处理)
|
||||
console.warn('[CreditsService] 无法获取余额,允许发送');
|
||||
return { allowed: true, balance: null };
|
||||
}
|
||||
|
||||
if (balance < LOW_CREDIT_THRESHOLD) {
|
||||
return {
|
||||
allowed: false,
|
||||
balance,
|
||||
message: `资源点余额不足!当前余额 ${balance.toFixed(2)} 点,低于最低要求 ${LOW_CREDIT_THRESHOLD} 点。请充值后再试。`
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true, balance };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存(登出时调用)
|
||||
*/
|
||||
export async function clearBalanceCache(): Promise<void> {
|
||||
cachedBalance = null;
|
||||
lastUpdateTime = 0;
|
||||
if (extensionContext) {
|
||||
await extensionContext.globalState.update('icCoderCreditsBalance', undefined);
|
||||
}
|
||||
console.log('[CreditsService] 余额缓存已清除');
|
||||
}
|
||||
@ -3,6 +3,8 @@ 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";
|
||||
|
||||
/**
|
||||
* IC Coder Authentication Provider
|
||||
@ -13,7 +15,6 @@ export class ICCoderAuthenticationProvider
|
||||
{
|
||||
private static readonly AUTH_TYPE = "iccoder";
|
||||
private static readonly AUTH_NAME = "IC Coder";
|
||||
private static readonly LOGIN_URL = "http://192.168.1.108:2005/login";
|
||||
private static loginServer: http.Server | null = null;
|
||||
private static currentPort: number | null = null;
|
||||
|
||||
@ -24,8 +25,23 @@ export class ICCoderAuthenticationProvider
|
||||
private _sessions: vscode.AuthenticationSession[] = [];
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
// 从存储中恢复会话
|
||||
this.loadSessions();
|
||||
// 从存储中恢复会话(同步执行)
|
||||
this.loadSessionsSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中加载会话(同步版本)
|
||||
*/
|
||||
private loadSessionsSync(): void {
|
||||
const storedSessions = this.context.globalState.get<
|
||||
vscode.AuthenticationSession[]
|
||||
>("icCoderSessions", []);
|
||||
this._sessions = storedSessions;
|
||||
console.log("[AuthProvider] 同步加载 sessions, 数量:", this._sessions.length);
|
||||
if (this._sessions.length > 0) {
|
||||
console.log("[AuthProvider] Session ID:", this._sessions[0].id);
|
||||
console.log("[AuthProvider] Account:", this._sessions[0].account.label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,7 +58,9 @@ export class ICCoderAuthenticationProvider
|
||||
* 保存会话到存储
|
||||
*/
|
||||
private async saveSessions(): Promise<void> {
|
||||
console.log("[AuthProvider] 保存 sessions, 数量:", this._sessions.length);
|
||||
await this.context.globalState.update("icCoderSessions", this._sessions);
|
||||
console.log("[AuthProvider] sessions 已保存到 globalState");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,6 +69,7 @@ export class ICCoderAuthenticationProvider
|
||||
async getSessions(
|
||||
scopes?: readonly string[]
|
||||
): Promise<vscode.AuthenticationSession[]> {
|
||||
console.log("[AuthProvider] getSessions 被调用, 当前 sessions 数量:", this._sessions.length);
|
||||
return [...this._sessions];
|
||||
}
|
||||
|
||||
@ -61,6 +80,20 @@ export class ICCoderAuthenticationProvider
|
||||
scopes: readonly string[]
|
||||
): Promise<vscode.AuthenticationSession> {
|
||||
try {
|
||||
// 先删除旧的 session(静默删除,不弹窗、不重载窗口)
|
||||
if (this._sessions.length > 0) {
|
||||
const oldSession = this._sessions[0];
|
||||
this._sessions = [];
|
||||
await this.saveSessions();
|
||||
await clearUserInfo();
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
removed: [oldSession],
|
||||
changed: [],
|
||||
});
|
||||
console.log("🔄 已清除旧的 session");
|
||||
}
|
||||
|
||||
const token = await this.login();
|
||||
|
||||
// 获取到 token 后立即调用用户信息接口
|
||||
@ -110,13 +143,24 @@ export class ICCoderAuthenticationProvider
|
||||
const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId);
|
||||
if (sessionIndex > -1) {
|
||||
const session = this._sessions[sessionIndex];
|
||||
|
||||
// 1. 先调用后端重置邀请码验证状态
|
||||
try {
|
||||
await resetInvitationVerification();
|
||||
console.log("[AuthProvider] 邀请码验证状态已重置");
|
||||
} catch (error) {
|
||||
console.warn("[AuthProvider] 重置邀请码验证状态失败,但继续退出流程:", error);
|
||||
// 即使失败也继续退出流程
|
||||
}
|
||||
|
||||
// 2. 清除本地 session
|
||||
this._sessions.splice(sessionIndex, 1);
|
||||
await this.saveSessions();
|
||||
|
||||
// 清除用户信息缓存
|
||||
// 3. 清除用户信息缓存
|
||||
await clearUserInfo();
|
||||
|
||||
// 触发会话变化事件
|
||||
// 4. 触发会话变化事件
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
removed: [session],
|
||||
@ -132,6 +176,28 @@ export class ICCoderAuthenticationProvider
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear local authentication state without window reload.
|
||||
* Used by re-login flow when session is expired.
|
||||
*/
|
||||
async clearSessionsForRelogin(): Promise<void> {
|
||||
if (this._sessions.length === 0) {
|
||||
await clearUserInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
const removed = [...this._sessions];
|
||||
this._sessions = [];
|
||||
await this.saveSessions();
|
||||
await clearUserInfo();
|
||||
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
removed,
|
||||
changed: [],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话 ID
|
||||
*/
|
||||
@ -156,9 +222,8 @@ export class ICCoderAuthenticationProvider
|
||||
|
||||
// 构建登录 URL
|
||||
const callbackUrl = `http://localhost:${port}/callback`;
|
||||
const loginUrl = `${
|
||||
ICCoderAuthenticationProvider.LOGIN_URL
|
||||
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||||
const config = getConfig();
|
||||
const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
console.log("🔐 登录服务器已启动,监听端口:", port);
|
||||
console.log("🌐 登录 URL:", loginUrl);
|
||||
|
||||
113
src/services/invitationService.ts
Normal file
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 邀请码验证服务
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { verifyInvitationCode, checkInvitationStatus } from "./apiClient";
|
||||
|
||||
/**
|
||||
* 邀请码验证服务类
|
||||
*/
|
||||
export class InvitationService {
|
||||
/**
|
||||
* 检查用户是否已验证邀请码
|
||||
*/
|
||||
static async isVerified(context: vscode.ExtensionContext): Promise<boolean> {
|
||||
// 【临时】使用本地验证,不调用后端
|
||||
const localVerified = context.globalState.get<boolean>(
|
||||
"invitationCodeVerified",
|
||||
);
|
||||
return localVerified || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邀请码
|
||||
*/
|
||||
static async verifyCode(
|
||||
code: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// console.log('[InvitationService] ========== 开始验证邀请码 ==========');
|
||||
// console.log('[InvitationService] 邀请码:', code);
|
||||
// console.log('[InvitationService] 邀请码长度:', code.length);
|
||||
|
||||
const response = await verifyInvitationCode(code);
|
||||
|
||||
// console.log('[InvitationService] 收到响应:', JSON.stringify(response, null, 2));
|
||||
// console.log('[InvitationService] 响应代码:', response.code);
|
||||
// console.log('[InvitationService] 响应消息:', response.msg);
|
||||
// console.log('[InvitationService] 验证结果:', response.data?.verified);
|
||||
|
||||
if (response.code === 200 && response.data?.verified) {
|
||||
console.log("[InvitationService] ✓ 验证成功");
|
||||
return {
|
||||
success: true,
|
||||
message: response.msg || "验证成功",
|
||||
};
|
||||
} else {
|
||||
console.log("[InvitationService] ✗ 验证失败");
|
||||
return {
|
||||
success: false,
|
||||
message: response.msg || "验证失败",
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
// console.error('[InvitationService] ========== 验证邀请码异常 ==========');
|
||||
// console.error('[InvitationService] 错误类型:', error.constructor.name);
|
||||
// console.error('[InvitationService] 错误消息:', error.message);
|
||||
// console.error('[InvitationService] 错误堆栈:', error.stack);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || "网络连接失败,请检查网络后重试",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存验证状态到本地
|
||||
*/
|
||||
static async saveVerificationStatus(
|
||||
context: vscode.ExtensionContext,
|
||||
code: string,
|
||||
verifiedTime?: string,
|
||||
): Promise<void> {
|
||||
await context.globalState.update("invitationCodeVerified", true);
|
||||
await context.globalState.update("invitationCode", code);
|
||||
await context.globalState.update(
|
||||
"invitationVerifiedTime",
|
||||
verifiedTime || new Date().toISOString(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除验证状态(用于退出登录或更换邀请码)
|
||||
*/
|
||||
static async clearVerificationStatus(
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<void> {
|
||||
await context.globalState.update("invitationCodeVerified", undefined);
|
||||
await context.globalState.update("invitationCode", undefined);
|
||||
await context.globalState.update("invitationVerifiedTime", undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示邀请码输入弹窗
|
||||
*/
|
||||
static async showInputDialog(): Promise<string | undefined> {
|
||||
const code = await vscode.window.showInputBox({
|
||||
prompt: "请输入邀请码以继续使用 IC Coder",
|
||||
placeHolder: "例如:INVITE2024ABC",
|
||||
ignoreFocusOut: true,
|
||||
validateInput: (value) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
return "邀请码不能为空";
|
||||
}
|
||||
if (value.trim().length < 6) {
|
||||
return "邀请码格式不正确";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
return code?.trim();
|
||||
}
|
||||
}
|
||||
270
src/services/notificationService.ts
Normal file
@ -0,0 +1,270 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
// 尝试加载 node-notifier,如果失败则使用 null
|
||||
let notifier: any = null;
|
||||
try {
|
||||
notifier = require('node-notifier');
|
||||
console.log('[NotificationService] node-notifier 加载成功');
|
||||
} catch (error) {
|
||||
console.log('[NotificationService] node-notifier 加载失败,将只使用 VS Code 内置通知');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知类型枚举
|
||||
*/
|
||||
export enum NotificationType {
|
||||
INFO = 'info',
|
||||
SUCCESS = 'success',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知选项接口
|
||||
*/
|
||||
export interface NotificationOptions {
|
||||
/** 通知标题 */
|
||||
title: string;
|
||||
/** 通知消息 */
|
||||
message: string;
|
||||
/** 通知类型 */
|
||||
type?: NotificationType;
|
||||
/** 是否播放声音 */
|
||||
sound?: boolean;
|
||||
/** 超时时间(秒),0 表示不自动消失 */
|
||||
timeout?: number;
|
||||
/** 自定义图标路径 */
|
||||
icon?: string;
|
||||
/** 点击通知时的回调 */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统通知服务类
|
||||
*/
|
||||
export class NotificationService {
|
||||
private static instance: NotificationService;
|
||||
private readonly extensionPath: string;
|
||||
private readonly iconPath: string;
|
||||
private lastNotificationTime: Map<string, number> = new Map();
|
||||
private readonly DEBOUNCE_INTERVAL = 3000; // 3 秒防抖
|
||||
|
||||
private constructor(context: vscode.ExtensionContext) {
|
||||
this.extensionPath = context.extensionPath;
|
||||
this.iconPath = path.join(this.extensionPath, 'media', 'icon.png');
|
||||
console.log('[NotificationService] 初始化通知服务');
|
||||
console.log('[NotificationService] 扩展路径:', this.extensionPath);
|
||||
console.log('[NotificationService] 图标路径:', this.iconPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(context?: vscode.ExtensionContext): NotificationService {
|
||||
if (!NotificationService.instance && context) {
|
||||
NotificationService.instance = new NotificationService(context);
|
||||
}
|
||||
return NotificationService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用系统通知
|
||||
*/
|
||||
private isSystemNotificationEnabled(): boolean {
|
||||
const config = vscode.workspace.getConfiguration('ic-coder');
|
||||
return config.get<boolean>('enableSystemNotification', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该发送通知(防抖)
|
||||
*/
|
||||
private shouldSendNotification(key: string): boolean {
|
||||
const now = Date.now();
|
||||
const lastTime = this.lastNotificationTime.get(key) || 0;
|
||||
|
||||
if (now - lastTime < this.DEBOUNCE_INTERVAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.lastNotificationTime.set(key, now);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统通知
|
||||
*/
|
||||
public sendNotification(options: NotificationOptions): void {
|
||||
console.log('[NotificationService] ========== 开始发送通知 ==========');
|
||||
console.log('[NotificationService] 通知选项:', options);
|
||||
|
||||
// 检查用户配置
|
||||
if (!this.isSystemNotificationEnabled()) {
|
||||
console.log('[NotificationService] 系统通知已禁用');
|
||||
return;
|
||||
}
|
||||
console.log('[NotificationService] 系统通知已启用');
|
||||
|
||||
const {
|
||||
title,
|
||||
message,
|
||||
type = NotificationType.INFO,
|
||||
onClick
|
||||
} = options;
|
||||
|
||||
// 防抖检查
|
||||
const notificationKey = `${title}-${message}`;
|
||||
if (!this.shouldSendNotification(notificationKey)) {
|
||||
console.log('[NotificationService] 通知被防抖机制拦截');
|
||||
return;
|
||||
}
|
||||
console.log('[NotificationService] 通过防抖检查');
|
||||
|
||||
// 如果 node-notifier 不可用,直接使用 VS Code 内置通知
|
||||
if (!notifier) {
|
||||
console.log('[NotificationService] node-notifier 不可用,使用 VS Code 内置通知');
|
||||
this.showVSCodeNotification(title, message, type, onClick);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 node-notifier 发送系统通知
|
||||
console.log('[NotificationService] 使用 node-notifier 发送系统通知');
|
||||
|
||||
try {
|
||||
const notificationConfig: any = {
|
||||
title: title,
|
||||
message: message,
|
||||
sound: true,
|
||||
wait: false,
|
||||
timeout: 10,
|
||||
appID: 'IC Coder'
|
||||
};
|
||||
|
||||
// Windows 特定配置
|
||||
if (process.platform === 'win32') {
|
||||
notificationConfig.icon = this.iconPath;
|
||||
console.log('[NotificationService] Windows 平台,图标路径:', this.iconPath);
|
||||
}
|
||||
|
||||
console.log('[NotificationService] 通知配置:', notificationConfig);
|
||||
|
||||
notifier.notify(notificationConfig, (err: any, response: any, metadata: any) => {
|
||||
if (err) {
|
||||
console.error('[NotificationService] ❌ node-notifier 失败:', err);
|
||||
} else {
|
||||
console.log('[NotificationService] ✅ node-notifier 成功');
|
||||
console.log('[NotificationService] 响应:', response);
|
||||
console.log('[NotificationService] 元数据:', metadata);
|
||||
}
|
||||
});
|
||||
|
||||
if (onClick) {
|
||||
notifier.on('click', () => {
|
||||
console.log('[NotificationService] 用户点击了系统通知');
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[NotificationService] ❌ node-notifier 异常:', error);
|
||||
// 如果系统通知失败,显示 VS Code 内置通知作为备用
|
||||
console.log('[NotificationService] 系统通知失败,显示 VS Code 内置通知');
|
||||
this.showVSCodeNotification(title, message, type, onClick);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 VS Code 内置通知
|
||||
*/
|
||||
private showVSCodeNotification(
|
||||
title: string,
|
||||
message: string,
|
||||
type: NotificationType,
|
||||
onClick?: () => void
|
||||
): void {
|
||||
const fullMessage = `${title}: ${message}`;
|
||||
console.log('[NotificationService] 显示 VS Code 通知:', fullMessage);
|
||||
|
||||
let notificationPromise: Thenable<string | undefined>;
|
||||
|
||||
switch (type) {
|
||||
case NotificationType.ERROR:
|
||||
notificationPromise = vscode.window.showErrorMessage(fullMessage, '查看详情');
|
||||
break;
|
||||
case NotificationType.WARNING:
|
||||
notificationPromise = vscode.window.showWarningMessage(fullMessage, '查看详情');
|
||||
break;
|
||||
case NotificationType.SUCCESS:
|
||||
case NotificationType.INFO:
|
||||
default:
|
||||
notificationPromise = vscode.window.showInformationMessage(fullMessage, '查看详情');
|
||||
break;
|
||||
}
|
||||
|
||||
// 处理点击事件
|
||||
if (onClick) {
|
||||
notificationPromise.then((selection) => {
|
||||
if (selection === '查看详情') {
|
||||
console.log('[NotificationService] 用户点击了通知');
|
||||
onClick();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送成功通知
|
||||
*/
|
||||
public success(title: string, message: string, onClick?: () => void): void {
|
||||
this.sendNotification({
|
||||
title,
|
||||
message,
|
||||
type: NotificationType.SUCCESS,
|
||||
sound: true,
|
||||
timeout: 10,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误通知
|
||||
*/
|
||||
public error(title: string, message: string, onClick?: () => void): void {
|
||||
this.sendNotification({
|
||||
title,
|
||||
message,
|
||||
type: NotificationType.ERROR,
|
||||
sound: true,
|
||||
timeout: 15,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送警告通知
|
||||
*/
|
||||
public warning(title: string, message: string, onClick?: () => void): void {
|
||||
this.sendNotification({
|
||||
title,
|
||||
message,
|
||||
type: NotificationType.WARNING,
|
||||
sound: true,
|
||||
timeout: 10,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送信息通知
|
||||
*/
|
||||
public info(title: string, message: string, onClick?: () => void): void {
|
||||
this.sendNotification({
|
||||
title,
|
||||
message,
|
||||
type: NotificationType.INFO,
|
||||
sound: false,
|
||||
timeout: 8,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
103
src/services/promptOptimizeService.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 提示词优化服务
|
||||
* 调用后端 API 优化用户输入的提示词
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl } from '../config/settings';
|
||||
|
||||
/** 优化响应类型 */
|
||||
interface OptimizeResponse {
|
||||
success: boolean;
|
||||
optimizedPrompt?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化提示词
|
||||
* @param prompt 原始提示词
|
||||
* @returns 优化后的提示词
|
||||
*/
|
||||
export async function optimizePrompt(prompt: string): Promise<string> {
|
||||
// 获取 JWT token
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
if (!session?.accessToken) {
|
||||
throw new Error('未登录,请先登录');
|
||||
}
|
||||
|
||||
const response = await callOptimizeApi(prompt, session.accessToken);
|
||||
|
||||
if (response.success && response.optimizedPrompt) {
|
||||
return response.optimizedPrompt;
|
||||
} else {
|
||||
throw new Error(response.error || '优化失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用后端优化 API
|
||||
*/
|
||||
async function callOptimizeApi(prompt: string, token: string): Promise<OptimizeResponse> {
|
||||
const urlStr = getApiUrl('/api/prompt/optimize');
|
||||
const url = new URL(urlStr);
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const body = JSON.stringify({ prompt });
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('[PromptOptimize] 响应状态码:', res.statusCode);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as OptimizeResponse);
|
||||
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
resolve({ success: false, error: '登录已过期,请重新登录' });
|
||||
} else {
|
||||
resolve({ success: false, error: json.error || json.message || `HTTP ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({ success: false, error: `解析响应失败: ${data}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
@ -28,7 +28,8 @@ import type {
|
||||
AgentProgressEvent,
|
||||
AgentCompleteEvent,
|
||||
AgentErrorEvent,
|
||||
ContextUsageEvent
|
||||
ContextUsageEvent,
|
||||
CreditUpdateEvent
|
||||
} from '../types/api';
|
||||
import type { MemoryCompactedEvent } from '../types/memory';
|
||||
|
||||
@ -44,6 +45,16 @@ export interface SSECallbacks {
|
||||
onToolConfirm?: (data: ToolConfirmEvent) => void;
|
||||
/** 收到计划确认请求(Plan 模式) */
|
||||
onPlanConfirm?: (data: PlanConfirmEvent) => void;
|
||||
/** 阶段进度更新 */
|
||||
onPhaseProgress?: (data: import('../types/api').PhaseProgressEvent) => void;
|
||||
/** 添加计划步骤 */
|
||||
onPlanStepAdd?: (data: import('../types/api').PlanStepAddEvent) => void;
|
||||
/** 删除计划步骤 */
|
||||
onPlanStepRemove?: (data: import('../types/api').PlanStepRemoveEvent) => void;
|
||||
/** 更新计划步骤 */
|
||||
onPlanStepUpdate?: (data: import('../types/api').PlanStepUpdateEvent) => void;
|
||||
/** 更新计划摘要 */
|
||||
onPlanSummaryUpdate?: (data: import('../types/api').PlanSummaryUpdateEvent) => void;
|
||||
/** 工具开始执行 */
|
||||
onToolStart?: (data: ToolStartEvent) => void;
|
||||
/** 工具执行完成 */
|
||||
@ -74,6 +85,8 @@ export interface SSECallbacks {
|
||||
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
||||
/** 上下文使用量更新 */
|
||||
onContextUsage?: (data: ContextUsageEvent) => void;
|
||||
/** 资源点余额更新 */
|
||||
onCreditUpdate?: (data: CreditUpdateEvent) => void;
|
||||
/** 连接打开 */
|
||||
onOpen?: () => void;
|
||||
/** 连接关闭 */
|
||||
@ -160,7 +173,8 @@ export async function startStreamDialog(
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
...(request.token ? { 'Authorization': `Bearer ${request.token}` } : {})
|
||||
}
|
||||
};
|
||||
|
||||
@ -170,9 +184,20 @@ export async function startStreamDialog(
|
||||
let errorBody = '';
|
||||
res.on('data', chunk => errorBody += chunk);
|
||||
res.on('end', () => {
|
||||
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
// 检测是否是登录状态过期
|
||||
const isLoginExpired = errorBody.includes('登录状态已过期') ||
|
||||
errorBody.includes('token') && errorBody.includes('过期') ||
|
||||
res.statusCode === 401;
|
||||
|
||||
if (isLoginExpired) {
|
||||
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
} else {
|
||||
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -213,6 +238,25 @@ export async function startStreamDialog(
|
||||
res.on('data', (chunk: string) => {
|
||||
if (!controller.aborted) {
|
||||
console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200));
|
||||
|
||||
// 检查是否是业务错误码(Gateway 返回 HTTP 200 但响应体是错误 JSON)
|
||||
try {
|
||||
const trimmed = chunk.trim();
|
||||
if (trimmed.startsWith('{') && trimmed.includes('"code"')) {
|
||||
const json = JSON.parse(trimmed);
|
||||
if (json.code === 401 || json.msg?.includes('登录状态已过期')) {
|
||||
console.log('[SSE] 检测到登录过期业务错误');
|
||||
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
|
||||
callbacks.onError?.({ message: error.message });
|
||||
controller.abort();
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 不是 JSON 格式,继续正常处理
|
||||
}
|
||||
|
||||
parser.feed(chunk);
|
||||
}
|
||||
});
|
||||
@ -286,6 +330,21 @@ function dispatchEvent(
|
||||
case 'plan_confirm':
|
||||
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
|
||||
break;
|
||||
case 'phase_progress':
|
||||
callbacks.onPhaseProgress?.(data as import('../types/api').PhaseProgressEvent);
|
||||
break;
|
||||
case 'plan_step_add':
|
||||
callbacks.onPlanStepAdd?.(data as import('../types/api').PlanStepAddEvent);
|
||||
break;
|
||||
case 'plan_step_remove':
|
||||
callbacks.onPlanStepRemove?.(data as import('../types/api').PlanStepRemoveEvent);
|
||||
break;
|
||||
case 'plan_step_update':
|
||||
callbacks.onPlanStepUpdate?.(data as import('../types/api').PlanStepUpdateEvent);
|
||||
break;
|
||||
case 'plan_summary_update':
|
||||
callbacks.onPlanSummaryUpdate?.(data as import('../types/api').PlanSummaryUpdateEvent);
|
||||
break;
|
||||
case 'tool_start':
|
||||
callbacks.onToolStart?.(data as ToolStartEvent);
|
||||
break;
|
||||
@ -331,6 +390,9 @@ function dispatchEvent(
|
||||
case 'context_usage':
|
||||
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
||||
break;
|
||||
case 'credit_update':
|
||||
callbacks.onCreditUpdate?.(data as CreditUpdateEvent);
|
||||
break;
|
||||
case 'heartbeat':
|
||||
// 心跳事件:仅用于保持连接,不需要特殊处理
|
||||
// Node.js req.setTimeout 会在收到数据时自动重置计时器
|
||||
|
||||
@ -2,21 +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 { generateVCD, checkIverilogAvailable } 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,
|
||||
@ -25,11 +35,12 @@ import type {
|
||||
FileDeleteArgs,
|
||||
FileListArgs,
|
||||
SyntaxCheckArgs,
|
||||
IverilogArgs,
|
||||
SimulationArgs,
|
||||
WaveformSummaryArgs,
|
||||
KnowledgeSaveArgs,
|
||||
KnowledgeLoadArgs
|
||||
} from '../types/api';
|
||||
KnowledgeLoadArgs,
|
||||
} from "../types/api";
|
||||
|
||||
/**
|
||||
* 工具执行器上下文
|
||||
@ -48,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;
|
||||
@ -60,34 +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 'simulation':
|
||||
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
||||
case "iverilog":
|
||||
resultText = await executeIverilog(
|
||||
args as unknown as IverilogArgs,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case 'waveform_summary':
|
||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
||||
case "simulation":
|
||||
resultText = await executeSimulation(
|
||||
args as unknown as SimulationArgs,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case 'waveform_trace':
|
||||
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
|
||||
case "waveform_summary":
|
||||
resultText = await executeWaveformSummary(
|
||||
args as unknown as WaveformSummaryArgs,
|
||||
);
|
||||
break;
|
||||
case 'knowledge_save':
|
||||
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
||||
case "waveform_trace":
|
||||
resultText = await executeWaveformTrace(
|
||||
args as unknown as WaveformTraceArgs,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case 'knowledge_load':
|
||||
case "knowledge_save":
|
||||
resultText = await executeKnowledgeSave(
|
||||
args as unknown as KnowledgeSaveArgs,
|
||||
);
|
||||
break;
|
||||
case "knowledge_load":
|
||||
resultText = await executeKnowledgeLoad();
|
||||
break;
|
||||
default:
|
||||
@ -98,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);
|
||||
@ -121,10 +153,21 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
||||
* 执行 file_write 工具
|
||||
*/
|
||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||
const absolutePath = resolveWorkspaceFilePath(args.path);
|
||||
const existedBeforeWrite = fs.existsSync(absolutePath);
|
||||
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : "";
|
||||
|
||||
await createOrOverwriteFile(args.path, args.content);
|
||||
|
||||
// 记录文件变更
|
||||
try {
|
||||
changeTracker.trackChange(args.path, oldContent, args.content);
|
||||
} catch (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[提示] 如有新信号或规则,请更新知识图谱`;
|
||||
}
|
||||
@ -134,7 +177,7 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||
|
||||
/**
|
||||
* 执行 file_delete 工具
|
||||
* 删除指定路径的文件
|
||||
* 删除指定路径的文件(带用户确认)
|
||||
*/
|
||||
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||
const filePath = args.path;
|
||||
@ -142,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;
|
||||
@ -163,11 +206,60 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
fs.unlinkSync(absolutePath);
|
||||
// 验证文件路径在工作区内
|
||||
const isInWorkspace = workspaceFolders.some((folder) =>
|
||||
absolutePath.startsWith(folder.uri.fsPath),
|
||||
);
|
||||
|
||||
if (!isInWorkspace) {
|
||||
throw new Error("只能删除工作区内的文件");
|
||||
}
|
||||
|
||||
// 保护敏感文件
|
||||
const protectedFiles = [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
".git",
|
||||
"node_modules",
|
||||
];
|
||||
|
||||
const fileName = path.basename(absolutePath);
|
||||
if (protectedFiles.includes(fileName)) {
|
||||
throw new Error(`不允许删除系统文件: ${fileName}`);
|
||||
}
|
||||
|
||||
// 弹出确认对话框
|
||||
const confirmed = await vscode.window.showWarningMessage(
|
||||
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
|
||||
{
|
||||
modal: true, // 模态对话框,阻止其他操作
|
||||
detail: "⚠️ 文件将被移到回收站,可以恢复",
|
||||
},
|
||||
"确定删除",
|
||||
"取消",
|
||||
);
|
||||
|
||||
// 用户取消或关闭对话框
|
||||
if (confirmed !== "确定删除") {
|
||||
throw new Error("用户取消了删除操作");
|
||||
}
|
||||
|
||||
// 读取文件内容用于变更追踪
|
||||
const oldContent = fs.readFileSync(absolutePath, "utf-8");
|
||||
|
||||
// 记录删除变更
|
||||
const relativePath = path.relative(workspacePath, absolutePath);
|
||||
changeTracker.trackChange(relativePath, oldContent, "");
|
||||
|
||||
// 删除文件(移到回收站)
|
||||
const uri = vscode.Uri.file(absolutePath);
|
||||
await vscode.workspace.fs.delete(uri, {
|
||||
recursive: false, // 不是目录,设为 false
|
||||
useTrash: true, // 移到回收站而非永久删除
|
||||
});
|
||||
|
||||
// Verilog 文件添加知识图谱提示
|
||||
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
|
||||
const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv");
|
||||
if (isVerilogFile) {
|
||||
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
||||
}
|
||||
@ -179,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 || "(目录为空)";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -194,7 +286,7 @@ 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);
|
||||
@ -208,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);
|
||||
@ -243,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) {
|
||||
@ -258,7 +350,6 @@ async function executeSyntaxCheck(
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// 确保清理临时文件
|
||||
try {
|
||||
@ -270,22 +361,113 @@ async function executeSyntaxCheck(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 iverilog 工具
|
||||
* 直接执行 iverilog 命令
|
||||
*/
|
||||
async function executeIverilog(
|
||||
args: IverilogArgs,
|
||||
context: ToolExecutorContext,
|
||||
): Promise<string> {
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
if (!iverilogCheck.available) {
|
||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
||||
}
|
||||
|
||||
// 获取工作目录
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error("没有打开的工作区");
|
||||
}
|
||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||
const workDir = args.workDir
|
||||
? path.join(projectPath, args.workDir)
|
||||
: projectPath;
|
||||
|
||||
// 解析参数
|
||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||
const cmdArgs = args.args.split(/\s+/).filter((a) => a.length > 0);
|
||||
|
||||
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"),
|
||||
},
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout.on("data", (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code: number) => {
|
||||
const output = stderr || stdout || "(无输出)";
|
||||
if (code === 0) {
|
||||
resolve(`执行成功\n${output}`);
|
||||
} else {
|
||||
resolve(`执行失败 (exit code: ${code})\n${output}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 simulation 工具
|
||||
*/
|
||||
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;
|
||||
|
||||
// 调用现有的 generateVCD 函数
|
||||
// 检查是否有 dumpModules 参数(多 VCD 模式)
|
||||
if (args.dumpModules) {
|
||||
const modules = parseDumpModules(args.dumpModules);
|
||||
const vcdDir = args.vcdDir || "vcd";
|
||||
|
||||
const result = await generateMultiVCD(
|
||||
projectPath,
|
||||
context.extensionPath,
|
||||
args.tbPath,
|
||||
modules,
|
||||
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 : ""}`;
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 原有单 VCD 逻辑
|
||||
const result = await generateVCD(projectPath, context.extensionPath);
|
||||
|
||||
if (result.success) {
|
||||
@ -303,17 +485,30 @@ async function executeSimulation(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 dumpModules 参数
|
||||
* 格式:name:path,name:path
|
||||
*/
|
||||
function parseDumpModules(dumpModules: string): DumpModule[] {
|
||||
return dumpModules.split(",").map((item) => {
|
||||
const [name, modulePath] = item.trim().split(":");
|
||||
return { name: name.trim(), path: modulePath.trim() };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 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;
|
||||
@ -344,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`;
|
||||
}
|
||||
@ -366,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;
|
||||
}
|
||||
@ -392,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];
|
||||
}
|
||||
|
||||
@ -401,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,
|
||||
};
|
||||
}
|
||||
|
||||
62
src/services/trialExpirationService.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 试用期过期检测服务
|
||||
* 功能:检查插件试用用户是否过期
|
||||
* 依赖:vscode, userService
|
||||
* 使用场景:用户使用功能前检查是否过期
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { getCachedUserInfo } from './userService';
|
||||
|
||||
export class TrialExpirationService {
|
||||
private context: vscode.ExtensionContext;
|
||||
private panel?: vscode.WebviewPanel;
|
||||
|
||||
constructor(context: vscode.ExtensionContext, panel?: vscode.WebviewPanel) {
|
||||
this.context = context;
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期
|
||||
* @returns true=已过期,false=未过期
|
||||
*/
|
||||
public async checkExpiration(): Promise<boolean> {
|
||||
const userInfo = getCachedUserInfo();
|
||||
|
||||
// 不是插件试用用户,不需要检查
|
||||
if (!userInfo?.isPluginTrial) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 没有过期时间,不检查
|
||||
if (!userInfo.pluginTrialExpiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
const now = Date.now();
|
||||
if (now >= userInfo.pluginTrialExpiresAt) {
|
||||
// 已过期
|
||||
await this.handleExpired();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理过期逻辑
|
||||
*/
|
||||
private async handleExpired(): Promise<void> {
|
||||
// 通知前端显示过期弹窗
|
||||
if (this.panel) {
|
||||
this.panel.webview.postMessage({
|
||||
command: 'showExpiredModal'
|
||||
});
|
||||
console.log('[TrialExpirationService] 已通知前端显示过期弹窗');
|
||||
} else {
|
||||
console.warn('[TrialExpirationService] panel 未提供,无法显示过期弹窗');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,22 +78,43 @@ 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
|
||||
customInput?: string,
|
||||
answers?: { [questionIndex: string]: string[] },
|
||||
fallbackTaskId?: string
|
||||
): Promise<void> {
|
||||
const pending = this.pendingQuestions.get(askId);
|
||||
if (!pending) {
|
||||
console.warn(`[UserInteraction] 问题不存在或已超时: askId=${askId}`);
|
||||
return;
|
||||
|
||||
// 构建答案字符串
|
||||
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(', ') || '';
|
||||
}
|
||||
|
||||
// 构建答案
|
||||
const answer = customInput || selected?.join(', ') || '';
|
||||
if (!pending) {
|
||||
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
||||
if (fallbackTaskId) {
|
||||
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
||||
await this.submitUserAnswer(askId, fallbackTaskId, answer, answers);
|
||||
} else {
|
||||
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
|
||||
|
||||
@ -112,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_')) {
|
||||
@ -141,7 +161,8 @@ export class UserInteractionManager {
|
||||
const request: AnswerRequest = {
|
||||
askId,
|
||||
taskId,
|
||||
customInput: answer
|
||||
answers: answers,
|
||||
customInput: answers ? undefined : answer
|
||||
};
|
||||
|
||||
try {
|
||||
@ -173,6 +194,13 @@ export class UserInteractionManager {
|
||||
hasPendingQuestions(): boolean {
|
||||
return this.pendingQuestions.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查特定问题是否存在
|
||||
*/
|
||||
hasPendingQuestion(askId: string): boolean {
|
||||
return this.pendingQuestions.has(askId);
|
||||
}
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
|
||||
@ -8,6 +8,7 @@ import { URL } from 'url';
|
||||
import * as vscode from 'vscode';
|
||||
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
|
||||
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
|
||||
import { fetchBalanceWithToken, getCachedBalance } from './creditsService';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
@ -114,6 +115,12 @@ export interface UserInfo {
|
||||
remainingDays?: number;
|
||||
monthlyCredits?: number;
|
||||
};
|
||||
// Credits 余额
|
||||
credits?: number;
|
||||
// 插件试用用户标识(从 JWT token 中提取)
|
||||
isPluginTrial?: boolean;
|
||||
// 试用到期时间(毫秒时间戳)
|
||||
pluginTrialExpiresAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,7 +143,7 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.user) {
|
||||
const user = response.user;
|
||||
return {
|
||||
const userInfo: UserInfo = {
|
||||
userId: String(user.userId),
|
||||
username: user.userName,
|
||||
nickname: user.nickName,
|
||||
@ -148,6 +155,24 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||||
createTime: user.createTime,
|
||||
loginDate: user.loginDate
|
||||
};
|
||||
|
||||
// 从接口响应中获取企业试用标识
|
||||
if (response.isPluginTrial === true) {
|
||||
userInfo.isPluginTrial = true;
|
||||
console.log('[UserService] 从 getInfo 接口获取到 isPluginTrial: true');
|
||||
}
|
||||
|
||||
// 获取试用到期时间(null 表示长期有效)
|
||||
if (response.enterpriseTrialExpires !== undefined) {
|
||||
userInfo.pluginTrialExpiresAt = response.enterpriseTrialExpires;
|
||||
if (response.enterpriseTrialExpires === null) {
|
||||
console.log('[UserService] 试用长期有效');
|
||||
} else {
|
||||
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
console.error('[UserService] 获取用户信息失败:', response);
|
||||
@ -221,12 +246,13 @@ function getHighestTierMembership(allMemberships?: MembershipItemVO[]): Membersh
|
||||
*/
|
||||
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
try {
|
||||
console.log('[UserService] Token 已获取,正在获取用户信息和会员信息...');
|
||||
console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...');
|
||||
|
||||
// 并行获取用户信息和会员信息
|
||||
const [userInfo, membershipInfo] = await Promise.all([
|
||||
// 并行获取用户信息、会员信息和余额
|
||||
const [userInfo, membershipInfo, credits] = await Promise.all([
|
||||
getUserInfo(token),
|
||||
getMembershipInfo(token)
|
||||
getMembershipInfo(token),
|
||||
fetchBalanceWithToken(token)
|
||||
]);
|
||||
|
||||
if (!userInfo) {
|
||||
@ -234,6 +260,15 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 添加 Credits 余额到用户信息
|
||||
console.log('[UserService] 获取到的 Credits 余额:', credits);
|
||||
if (credits !== null) {
|
||||
userInfo.credits = credits;
|
||||
console.log('[UserService] Credits 已添加到用户信息');
|
||||
} else {
|
||||
console.warn('[UserService] Credits 余额为 null,未添加到用户信息');
|
||||
}
|
||||
|
||||
// 打印用户信息到控制台
|
||||
console.log('='.repeat(60));
|
||||
console.log('用户信息详情:');
|
||||
@ -286,11 +321,58 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
}
|
||||
}
|
||||
|
||||
// 打印 Credits 余额
|
||||
console.log('');
|
||||
console.log('资源点余额:');
|
||||
if (userInfo.credits !== undefined) {
|
||||
console.log(`当前余额: ${userInfo.credits} Credits`);
|
||||
} else {
|
||||
console.log('当前余额: 未获取到余额信息');
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 保存到持久化存储
|
||||
await saveUserInfo(userInfo);
|
||||
|
||||
// 判断是否是插件试用用户
|
||||
console.log('[UserService] 检查用户类型,isPluginTrial:', userInfo.isPluginTrial);
|
||||
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
|
||||
|
||||
if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
|
||||
// 检查是否过期
|
||||
const now = Date.now();
|
||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
console.log('[UserService] 试用到期时间:', new Date(userInfo.pluginTrialExpiresAt).toLocaleString());
|
||||
console.log('[UserService] 当前时间:', new Date(now).toLocaleString());
|
||||
console.log('[UserService] 是否过期:', isExpired);
|
||||
|
||||
if (isExpired) {
|
||||
// 已过期:显示邀请码弹窗
|
||||
console.log('[UserService] 试用已过期,将显示邀请码弹窗');
|
||||
} else {
|
||||
// 未过期:显示欢迎弹窗
|
||||
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
|
||||
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
|
||||
|
||||
if (!hasWelcomed && extensionContext) {
|
||||
await extensionContext.globalState.update('showWelcomeModal', true);
|
||||
await extensionContext.globalState.update('pluginTrialWelcomed', true);
|
||||
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
|
||||
|
||||
const checkMark = extensionContext.globalState.get('showWelcomeModal');
|
||||
console.log('[UserService] 验证标记:', checkMark);
|
||||
} else if (!extensionContext) {
|
||||
console.error('[UserService] ❌ extensionContext 为 null,无法设置标记');
|
||||
} else {
|
||||
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// isPluginTrial=false 或 enterpriseTrialExpires 为 null:显示邀请码弹窗
|
||||
console.log('[UserService] 非试用用户或无过期时间,将显示邀请码弹窗');
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 获取用户信息失败:', error);
|
||||
@ -329,7 +411,18 @@ export function getCachedUserInfo(): UserInfo | null {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return null;
|
||||
}
|
||||
return extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
||||
const userInfo = extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
||||
|
||||
// 从 creditsService 加载余额并合并到用户信息中
|
||||
if (userInfo) {
|
||||
const cachedCredits = getCachedBalance();
|
||||
if (cachedCredits !== null) {
|
||||
userInfo.credits = cachedCredits;
|
||||
console.log('[UserService] 从 creditsService 加载余额:', cachedCredits);
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,7 +5,7 @@ import * as vscode from "vscode";
|
||||
|
||||
/**
|
||||
* VCD 文件 HTTP 服务器
|
||||
* 用于为 Surfer 波形查看器提供 VCD 文件访问
|
||||
* 用于为 波形查看器提供 VCD 文件访问
|
||||
*/
|
||||
export class VCDFileServer {
|
||||
private server: http.Server | null = null;
|
||||
@ -98,7 +98,10 @@ export class VCDFileServer {
|
||||
/**
|
||||
* 处理 HTTP 请求
|
||||
*/
|
||||
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
private handleRequest(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
): void {
|
||||
const url = req.url || "";
|
||||
console.log(`[VCDFileServer] 收到请求: ${url}`);
|
||||
|
||||
@ -214,7 +217,12 @@ export class VCDFileServer {
|
||||
}
|
||||
|
||||
const fileName = match[1];
|
||||
const filePath = path.join(this.extensionUri.fsPath, "media", "surfer", fileName);
|
||||
const filePath = path.join(
|
||||
this.extensionUri.fsPath,
|
||||
"media",
|
||||
"surfer",
|
||||
fileName,
|
||||
);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||||
@ -257,8 +265,8 @@ export class VCDFileServer {
|
||||
*/
|
||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||
try {
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
||||
const lines = buffer.split('\n');
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
const scopeNames: string[] = [];
|
||||
let scopeDepth = 0;
|
||||
@ -267,7 +275,7 @@ export class VCDFileServer {
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith('$enddefinitions')) {
|
||||
if (trimmed.startsWith("$enddefinitions")) {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -276,17 +284,17 @@ export class VCDFileServer {
|
||||
const scopeType = scopeMatch[1];
|
||||
const scopeName = scopeMatch[2];
|
||||
|
||||
if (scopeDepth === 0 && scopeType === 'module') {
|
||||
if (scopeDepth === 0 && scopeType === "module") {
|
||||
scopeStack.push(scopeName);
|
||||
} else if (scopeDepth === 1 && scopeType === 'module') {
|
||||
} else if (scopeDepth === 1 && scopeType === "module") {
|
||||
const fullPath = [...scopeStack, scopeName];
|
||||
scopeNames.push(fullPath.join('.'));
|
||||
scopeNames.push(fullPath.join("."));
|
||||
}
|
||||
|
||||
scopeDepth++;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('$upscope')) {
|
||||
if (trimmed.startsWith("$upscope")) {
|
||||
scopeDepth--;
|
||||
if (scopeDepth === 0) {
|
||||
scopeStack.pop();
|
||||
@ -323,7 +331,7 @@ export class VCDFileServer {
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Surfer 波形查看器 - ${fileName}</title>
|
||||
<title>波形查看器 - ${fileName}</title>
|
||||
<script>
|
||||
window.surferReady = false;
|
||||
window.pendingVcdData = null;
|
||||
|
||||
161
src/types/api.ts
@ -40,6 +40,8 @@ export interface DialogRequest {
|
||||
mode: RunMode;
|
||||
/** 服务等级 */
|
||||
serviceTier?: ServiceTier;
|
||||
/** JWT Token(用于认证和扣费) */
|
||||
token?: string;
|
||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||
compactedData?: CompactedMemory;
|
||||
/** 压缩后产生的新消息 */
|
||||
@ -56,6 +58,11 @@ export type SSEEventType =
|
||||
| "tool_call" // 客户端工具调用请求
|
||||
| "tool_confirm" // 工具确认请求(Ask 模式)
|
||||
| "plan_confirm" // 计划确认请求(Plan 模式)
|
||||
| "phase_progress" // 阶段进度更新
|
||||
| "plan_step_add" // 添加计划步骤
|
||||
| "plan_step_remove" // 删除计划步骤
|
||||
| "plan_step_update" // 更新计划步骤
|
||||
| "plan_summary_update" // 更新计划摘要
|
||||
| "tool_start" // 工具开始执行
|
||||
| "tool_complete" // 工具执行完成
|
||||
| "tool_error" // 工具执行错误
|
||||
@ -66,6 +73,7 @@ export type SSEEventType =
|
||||
| "agent_error" // 子智能体错误
|
||||
| "memory_compacted" // 记忆压缩完成
|
||||
| "context_usage" // 上下文使用量
|
||||
| "credit_update" // 资源点余额更新
|
||||
| "complete" // 对话完成
|
||||
| "error" // 错误
|
||||
| "warning" // 警告
|
||||
@ -88,6 +96,7 @@ export interface ToolStartEvent {
|
||||
export interface ToolCompleteEvent {
|
||||
tool_name: string;
|
||||
result: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** tool_error 事件数据 */
|
||||
@ -108,25 +117,94 @@ export interface ToolConfirmEvent {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** 计划步骤 */
|
||||
export interface PlanStep {
|
||||
/** 步骤名称 */
|
||||
name: string;
|
||||
/** 步骤描述 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 计划阶段 */
|
||||
export interface PlanPhase {
|
||||
/** 阶段ID: spec/design/sim/done */
|
||||
id: string;
|
||||
/** 阶段名称 */
|
||||
name: string;
|
||||
/** 阶段状态: skipped/completed/current/pending */
|
||||
status: string;
|
||||
/** 跳过原因 */
|
||||
reason?: string;
|
||||
/** 阶段内的步骤 */
|
||||
steps: PlanStep[];
|
||||
}
|
||||
|
||||
/** plan_confirm 事件数据(Plan 模式计划确认) */
|
||||
export interface PlanConfirmEvent {
|
||||
/** 确认ID */
|
||||
confirmId: number;
|
||||
/** 计划标题 */
|
||||
title: string;
|
||||
/** 执行步骤列表 */
|
||||
steps: string[];
|
||||
/** 四阶段计划列表(新格式) */
|
||||
phases?: PlanPhase[];
|
||||
/** 执行步骤列表(旧格式,兼容) */
|
||||
steps?: string[];
|
||||
/** 计划摘要 */
|
||||
summary: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** phase_progress 事件数据(阶段进度更新) */
|
||||
export interface PhaseProgressEvent {
|
||||
/** 阶段ID: spec/design/sim/done */
|
||||
phaseId: string;
|
||||
/** 状态: current/completed */
|
||||
status: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_step_add 事件数据(添加计划步骤) */
|
||||
export interface PlanStepAddEvent {
|
||||
phaseId: string;
|
||||
step: PlanStep;
|
||||
index: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_step_remove 事件数据(删除计划步骤) */
|
||||
export interface PlanStepRemoveEvent {
|
||||
phaseId: string;
|
||||
stepIndex: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_step_update 事件数据(更新计划步骤) */
|
||||
export interface PlanStepUpdateEvent {
|
||||
phaseId: string;
|
||||
stepIndex: number;
|
||||
step: PlanStep;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_summary_update 事件数据(更新计划摘要) */
|
||||
export interface PlanSummaryUpdateEvent {
|
||||
summary: string;
|
||||
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 事件数据 */
|
||||
@ -201,6 +279,12 @@ export interface ContextUsageEvent {
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
/** credit_update 事件数据 */
|
||||
export interface CreditUpdateEvent {
|
||||
deductedCredits: number;
|
||||
remainingCredits: number;
|
||||
}
|
||||
|
||||
// ============== 工具调用协议 (MCP 格式) ==============
|
||||
|
||||
/**
|
||||
@ -273,10 +357,12 @@ export interface AnswerRequest {
|
||||
askId: string;
|
||||
/** 任务ID */
|
||||
taskId: string;
|
||||
/** 选中的选项列表 */
|
||||
/** 选中的选项列表(旧格式,兼容) */
|
||||
selected?: string[];
|
||||
/** 自定义输入内容 */
|
||||
/** 自定义输入内容(旧格式,兼容) */
|
||||
customInput?: string;
|
||||
/** 新格式:按问题索引的答案 */
|
||||
answers?: { [questionIndex: string]: string[] };
|
||||
}
|
||||
|
||||
/** 用户回答响应 */
|
||||
@ -329,6 +415,10 @@ export interface UserInfoResponse {
|
||||
isDefaultModifyPwd: boolean;
|
||||
/** 密码是否过期 */
|
||||
isPasswordExpired: boolean;
|
||||
/** 是否为插件试用用户 */
|
||||
isPluginTrial?: boolean;
|
||||
/** 企业试用到期时间(毫秒时间戳) */
|
||||
enterpriseTrialExpires?: number;
|
||||
/** 用户信息 */
|
||||
user: {
|
||||
userId: number;
|
||||
@ -341,6 +431,7 @@ export interface UserInfoResponse {
|
||||
status?: string;
|
||||
createTime?: string;
|
||||
loginDate?: string;
|
||||
remark?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
@ -409,6 +500,7 @@ export type ToolName =
|
||||
| "file_delete"
|
||||
| "file_list"
|
||||
| "syntax_check"
|
||||
| "iverilog"
|
||||
| "simulation"
|
||||
| "waveform_summary"
|
||||
| "waveform_trace"
|
||||
@ -443,11 +535,21 @@ export interface SyntaxCheckArgs {
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** iverilog 工具参数 */
|
||||
export interface IverilogArgs {
|
||||
args: string;
|
||||
workDir?: string;
|
||||
}
|
||||
|
||||
/** simulation 工具参数 */
|
||||
export interface SimulationArgs {
|
||||
rtlPath: string;
|
||||
tbPath: string;
|
||||
duration?: string;
|
||||
/** 要dump的模块列表,格式:name:path,name:path */
|
||||
dumpModules?: string;
|
||||
/** VCD输出目录,默认'vcd' */
|
||||
vcdDir?: string;
|
||||
}
|
||||
|
||||
/** waveform_summary 工具参数 */
|
||||
@ -487,8 +589,55 @@ export type ToolArgs =
|
||||
| FileDeleteArgs
|
||||
| FileListArgs
|
||||
| SyntaxCheckArgs
|
||||
| IverilogArgs
|
||||
| SimulationArgs
|
||||
| WaveformSummaryArgs
|
||||
| WaveformTraceArgs
|
||||
| KnowledgeSaveArgs
|
||||
| KnowledgeLoadArgs;
|
||||
|
||||
// ============== 邀请码验证 ==============
|
||||
|
||||
/**
|
||||
* 邀请码验证请求
|
||||
* POST /api/invitation/verify
|
||||
*/
|
||||
export interface InvitationVerifyRequest {
|
||||
/** 邀请码 */
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请码验证响应
|
||||
*/
|
||||
export interface InvitationVerifyResponse {
|
||||
/** 响应代码 */
|
||||
code: number;
|
||||
/** 响应消息 */
|
||||
msg: string;
|
||||
/** 验证结果数据 */
|
||||
data?: {
|
||||
/** 是否验证成功 */
|
||||
verified: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请码状态响应
|
||||
* GET /api/invitation/status
|
||||
*/
|
||||
export interface InvitationStatusResponse {
|
||||
/** 响应代码 */
|
||||
code: number;
|
||||
/** 响应消息 */
|
||||
msg?: string;
|
||||
/** 状态数据 */
|
||||
data?: {
|
||||
/** 是否已验证 */
|
||||
verified: boolean;
|
||||
/** 使用的邀请码 */
|
||||
invitationCode?: string;
|
||||
/** 验证时间 */
|
||||
verifiedTime?: string;
|
||||
};
|
||||
}
|
||||
|
||||
47
src/types/fileChanges.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 文件变更追踪类型定义
|
||||
* 功能:定义代码变更的数据结构
|
||||
* 依赖:无
|
||||
* 使用场景:AI 修改文件后的变更审查
|
||||
*/
|
||||
|
||||
/**
|
||||
* 单个文件的变更记录
|
||||
*/
|
||||
export interface FileChange {
|
||||
/** 文件相对路径 */
|
||||
filePath: string;
|
||||
/** 修改前的内容 */
|
||||
oldContent: string;
|
||||
/** 修改后的内容 */
|
||||
newContent: string;
|
||||
/** 变更时间戳 */
|
||||
timestamp: number;
|
||||
/** 变更类型 */
|
||||
changeType: 'create' | 'modify' | 'delete';
|
||||
/** 变更 ID(唯一标识) */
|
||||
changeId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更会话(一次对话的所有变更)
|
||||
*/
|
||||
export interface ChangeSession {
|
||||
/** 会话 ID */
|
||||
sessionId: string;
|
||||
/** 会话开始时间 */
|
||||
startTime: number;
|
||||
/** 所有文件变更 */
|
||||
changes: FileChange[];
|
||||
/** 会话状态 */
|
||||
status: 'active' | 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更操作结果
|
||||
*/
|
||||
export interface ChangeActionResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
changeId: string;
|
||||
}
|
||||
@ -715,6 +715,10 @@ export class ChatHistoryManager {
|
||||
|
||||
if (!projectPath) {
|
||||
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
||||
// 通知用户压缩数据保存失败
|
||||
vscode.window.showWarningMessage(
|
||||
'对话历史压缩数据保存失败:无法确定项目路径。后端重启后可能无法恢复完整对话历史。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -731,6 +735,19 @@ export class ChatHistoryManager {
|
||||
// 文件不存在,使用空数组
|
||||
}
|
||||
|
||||
// 版本检查:防止旧版本覆盖新版本(从尾部扫描,与加载逻辑一致)
|
||||
let existingSummary: CompactionSummaryMessage | null = null;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||
existingSummary = messages[i] as CompactionSummaryMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existingSummary && existingSummary.version >= compacted.version) {
|
||||
console.log(`[ChatHistoryManager] 跳过旧版本压缩数据: 现有版本=${existingSummary.version}, 新版本=${compacted.version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建压缩摘要消息
|
||||
const summaryMessage: CompactionSummaryMessage = {
|
||||
type: MessageType.COMPACTION_SUMMARY,
|
||||
@ -893,4 +910,14 @@ export class ChatHistoryManager {
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(工具执行结果)
|
||||
*/
|
||||
public trackToolResult(toolName: string, result: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'TOOL_RESULT',
|
||||
content: `[${toolName}] ${result}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
203
src/utils/diffRenderer.ts
Normal file
@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Diff 渲染工具
|
||||
* 功能:生成代码差异的 HTML 展示
|
||||
* 依赖:无
|
||||
* 使用场景:在变更面板中展示文件修改的 diff
|
||||
*/
|
||||
|
||||
interface DiffLine {
|
||||
type: 'add' | 'remove' | 'context';
|
||||
content: string;
|
||||
oldLineNumber?: number;
|
||||
newLineNumber?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的 diff 算法(基于行)
|
||||
*/
|
||||
export function generateDiff(oldContent: string, newContent: string): DiffLine[] {
|
||||
const oldLines = oldContent.split('\n');
|
||||
const newLines = newContent.split('\n');
|
||||
const result: DiffLine[] = [];
|
||||
|
||||
let oldIndex = 0;
|
||||
let newIndex = 0;
|
||||
|
||||
while (oldIndex < oldLines.length || newIndex < newLines.length) {
|
||||
const oldLine = oldLines[oldIndex];
|
||||
const newLine = newLines[newIndex];
|
||||
|
||||
if (oldIndex >= oldLines.length) {
|
||||
// 只剩新行
|
||||
result.push({
|
||||
type: 'add',
|
||||
content: newLine,
|
||||
newLineNumber: newIndex + 1
|
||||
});
|
||||
newIndex++;
|
||||
} else if (newIndex >= newLines.length) {
|
||||
// 只剩旧行
|
||||
result.push({
|
||||
type: 'remove',
|
||||
content: oldLine,
|
||||
oldLineNumber: oldIndex + 1
|
||||
});
|
||||
oldIndex++;
|
||||
} else if (oldLine === newLine) {
|
||||
// 相同行
|
||||
result.push({
|
||||
type: 'context',
|
||||
content: oldLine,
|
||||
oldLineNumber: oldIndex + 1,
|
||||
newLineNumber: newIndex + 1
|
||||
});
|
||||
oldIndex++;
|
||||
newIndex++;
|
||||
} else {
|
||||
// 不同行,标记为删除和添加
|
||||
result.push({
|
||||
type: 'remove',
|
||||
content: oldLine,
|
||||
oldLineNumber: oldIndex + 1
|
||||
});
|
||||
result.push({
|
||||
type: 'add',
|
||||
content: newLine,
|
||||
newLineNumber: newIndex + 1
|
||||
});
|
||||
oldIndex++;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 diff 结果渲染为 HTML
|
||||
*/
|
||||
export function renderDiffHtml(diffLines: DiffLine[]): string {
|
||||
let html = '<div class="diff-viewer">';
|
||||
|
||||
for (const line of diffLines) {
|
||||
const lineClass = `diff-line diff-line-${line.type}`;
|
||||
const oldNum = line.oldLineNumber ? `<span class="line-num">${line.oldLineNumber}</span>` : '<span class="line-num"></span>';
|
||||
const newNum = line.newLineNumber ? `<span class="line-num">${line.newLineNumber}</span>` : '<span class="line-num"></span>';
|
||||
const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' ';
|
||||
const escapedContent = escapeHtml(line.content);
|
||||
|
||||
html += `
|
||||
<div class="${lineClass}">
|
||||
${oldNum}
|
||||
${newNum}
|
||||
<span class="line-prefix">${prefix}</span>
|
||||
<span class="line-content">${escapedContent}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义 HTML 特殊字符
|
||||
*/
|
||||
function escapeHtml(text: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 diff 样式
|
||||
*/
|
||||
export function getDiffStyles(): string {
|
||||
return `
|
||||
.diff-viewer {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
white-space: pre;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.diff-line:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.diff-line-add {
|
||||
background: rgba(40, 167, 69, 0.2);
|
||||
border-left: 3px solid #28a745;
|
||||
}
|
||||
|
||||
.diff-line-add:hover {
|
||||
background: rgba(40, 167, 69, 0.25);
|
||||
}
|
||||
|
||||
.diff-line-remove {
|
||||
background: rgba(220, 53, 69, 0.2);
|
||||
border-left: 3px solid #dc3545;
|
||||
}
|
||||
|
||||
.diff-line-remove:hover {
|
||||
background: rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
.diff-line-context {
|
||||
background: transparent;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.line-num {
|
||||
display: inline-block;
|
||||
width: 45px;
|
||||
text-align: right;
|
||||
padding: 0 10px;
|
||||
color: var(--vscode-editorLineNumber-foreground);
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.line-prefix {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.diff-line-add .line-prefix {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.diff-line-remove .line-prefix {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.line-content {
|
||||
flex: 1;
|
||||
padding: 0 12px 0 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
51
src/utils/fileDiff.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
|
||||
/**
|
||||
* 将相对路径解析为工作区绝对路径
|
||||
*/
|
||||
export function resolveWorkspaceFilePath(filePath: string): string {
|
||||
if (path.isAbsolute(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error("请先打开一个文件夹作为工作区,这样我就能为您修改文件了");
|
||||
}
|
||||
|
||||
return path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 VS Code 原生 diff 视图展示文件修改前后对比
|
||||
*/
|
||||
export async function showFileDiff(
|
||||
filePath: string,
|
||||
oldContent: string,
|
||||
title?: string
|
||||
): Promise<void> {
|
||||
const absolutePath = resolveWorkspaceFilePath(filePath);
|
||||
const fileUri = vscode.Uri.file(absolutePath);
|
||||
const newBytes = await vscode.workspace.fs.readFile(fileUri);
|
||||
const newContent = Buffer.from(newBytes).toString("utf-8");
|
||||
|
||||
if (oldContent === newContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const language = (await vscode.workspace.openTextDocument(fileUri)).languageId;
|
||||
const oldDoc = await vscode.workspace.openTextDocument({
|
||||
content: oldContent,
|
||||
language,
|
||||
});
|
||||
|
||||
const diffTitle = title || `${path.basename(absolutePath)} (修改前 <-> 修改后)`;
|
||||
await vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
oldDoc.uri,
|
||||
fileUri,
|
||||
diffTitle,
|
||||
{ preview: false }
|
||||
);
|
||||
}
|
||||
@ -413,3 +413,193 @@ export async function checkIverilogAvailable(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 要 dump 的模块定义
|
||||
*/
|
||||
export interface DumpModule {
|
||||
name: string; // 模块名(用于 VCD 文件名和宏名)
|
||||
path: string; // 实例路径(如 dut.u_tx)
|
||||
}
|
||||
|
||||
/**
|
||||
* 多 VCD 生成结果
|
||||
*/
|
||||
export interface MultiVCDResult {
|
||||
success: boolean;
|
||||
vcdFiles: Array<{
|
||||
moduleName: string;
|
||||
vcdPath: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
message: string;
|
||||
stdout?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 testbench 中注入条件编译代码
|
||||
* 将原有的 $dumpfile/$dumpvars 替换为条件编译版本
|
||||
*/
|
||||
function injectConditionalDump(
|
||||
tbContent: string,
|
||||
dumpModules: DumpModule[],
|
||||
vcdDir: string
|
||||
): string {
|
||||
// 匹配 $dumpfile 和 $dumpvars 语句(可能跨多行)
|
||||
const dumpPattern = /(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
|
||||
|
||||
// 生成条件编译代码
|
||||
const conditionalCode = generateConditionalDumpCode(dumpModules, vcdDir);
|
||||
|
||||
// 替换原有的 dump 语句
|
||||
const modified = tbContent.replace(dumpPattern, conditionalCode);
|
||||
|
||||
// 如果没有找到匹配,尝试单独匹配 $dumpfile
|
||||
if (modified === tbContent) {
|
||||
const singleDumpPattern = /\$dumpfile\s*\([^)]+\)\s*;/g;
|
||||
return tbContent.replace(singleDumpPattern, conditionalCode);
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成条件编译的 dump 代码
|
||||
*/
|
||||
function generateConditionalDumpCode(
|
||||
dumpModules: DumpModule[],
|
||||
vcdDir: string
|
||||
): string {
|
||||
if (dumpModules.length === 0) {
|
||||
return '$dumpfile("output.vcd");\n $dumpvars(0, dut);';
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
dumpModules.forEach((module, index) => {
|
||||
const macroName = `DUMP_${module.name.toUpperCase()}`;
|
||||
const vcdPath = `${vcdDir}/${module.name}.vcd`;
|
||||
const directive = index === 0 ? '`ifdef' : '`elsif';
|
||||
|
||||
lines.push(`${directive} ${macroName}`);
|
||||
lines.push(` $dumpfile("${vcdPath}");`);
|
||||
lines.push(` $dumpvars(1, ${module.path});`);
|
||||
});
|
||||
|
||||
// 添加默认分支(使用第一个模块)
|
||||
lines.push('`else');
|
||||
lines.push(` $dumpfile("${vcdDir}/${dumpModules[0].name}.vcd");`);
|
||||
lines.push(` $dumpvars(1, ${dumpModules[0].path});`);
|
||||
lines.push('`endif');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成多个 VCD 文件(为不同子模块)
|
||||
*/
|
||||
export async function generateMultiVCD(
|
||||
projectPath: string,
|
||||
extensionPath: string,
|
||||
tbPath: string,
|
||||
dumpModules: DumpModule[],
|
||||
vcdDir: string = 'vcd'
|
||||
): Promise<MultiVCDResult> {
|
||||
const results: MultiVCDResult['vcdFiles'] = [];
|
||||
let allStdout = '';
|
||||
|
||||
try {
|
||||
// 1. 创建 vcd 目录
|
||||
const vcdDirPath = path.join(projectPath, vcdDir);
|
||||
const vcdDirUri = vscode.Uri.file(vcdDirPath);
|
||||
try {
|
||||
await vscode.workspace.fs.createDirectory(vcdDirUri);
|
||||
} catch {
|
||||
// 目录可能已存在
|
||||
}
|
||||
|
||||
// 2. 读取原始 testbench
|
||||
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');
|
||||
|
||||
// 3. 注入条件编译代码
|
||||
const modifiedTb = injectConditionalDump(originalTb, dumpModules, vcdDir);
|
||||
await vscode.workspace.fs.writeFile(tbUri, Buffer.from(modifiedTb, 'utf-8'));
|
||||
|
||||
console.log('[generateMultiVCD] Testbench 已修改,开始多次仿真...');
|
||||
|
||||
// 4. 获取工具路径
|
||||
const iverilogPath = await getIverilogPath(extensionPath);
|
||||
const vvpPath = await getVvpPath(extensionPath);
|
||||
const env = {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
|
||||
};
|
||||
|
||||
// 5. 获取所有 Verilog 文件
|
||||
const projectCheck = await checkVerilogProject(projectPath);
|
||||
const outputFile = path.join(projectPath, "simulation.vvp");
|
||||
|
||||
// 6. 循环执行仿真
|
||||
for (const module of dumpModules) {
|
||||
const macroName = `DUMP_${module.name.toUpperCase()}`;
|
||||
const vcdPath = path.join(vcdDirPath, `${module.name}.vcd`);
|
||||
|
||||
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
|
||||
|
||||
try {
|
||||
// 编译(带宏定义)
|
||||
const compileArgs = [
|
||||
`-D${macroName}`,
|
||||
"-o", outputFile,
|
||||
...projectCheck.allVerilogFiles
|
||||
];
|
||||
await execCommand(iverilogPath, compileArgs, { 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
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(`[generateMultiVCD] 模块 ${module.name} 仿真失败:`, error.message);
|
||||
results.push({
|
||||
moduleName: module.name,
|
||||
vcdPath: vcdPath,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
// 继续执行其他模块
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 清理中间文件
|
||||
try {
|
||||
await vscode.workspace.fs.delete(vscode.Uri.file(outputFile));
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
return {
|
||||
success: successCount > 0,
|
||||
vcdFiles: results,
|
||||
message: `生成完成:${successCount}/${dumpModules.length} 个 VCD 文件成功`,
|
||||
stdout: allStdout
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
vcdFiles: results,
|
||||
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
126
src/utils/jwtUtils.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* JWT 工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* JWT Payload 接口
|
||||
*/
|
||||
export interface JwtPayload {
|
||||
sub?: string; // subject (通常是 userId)
|
||||
userId?: number; // 用户ID (驼峰命名)
|
||||
user_id?: number; // 用户ID (下划线命名)
|
||||
exp?: number; // 过期时间
|
||||
iat?: number; // 签发时间
|
||||
ispluginTrial?: boolean; // 是否是插件试用用户
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JWT token 的 payload
|
||||
* @param token JWT token
|
||||
* @returns 解析后的 payload,解析失败返回 null
|
||||
*/
|
||||
export function parseJwtPayload(token: string): JwtPayload | null {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
console.warn("[JWT] token 格式不正确,期望3部分,实际:", parts.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
// payload 是第二部分,base64url 编码
|
||||
const payload = parts[1];
|
||||
|
||||
// base64url 转 base64
|
||||
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
// 解码
|
||||
const jsonStr = Buffer.from(base64, "base64").toString("utf-8");
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
|
||||
console.log("[JWT] 解析成功, payload 字段:", Object.keys(parsed));
|
||||
console.log("[JWT] payload 内容:", JSON.stringify(parsed));
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error("[JWT] 解析失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JWT token 中获取用户ID
|
||||
* @param token JWT token
|
||||
* @returns 用户ID字符串,获取失败返回 null
|
||||
*/
|
||||
export function getUserIdFromToken(token: string): string | null {
|
||||
const payload = parseJwtPayload(token);
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 支持多种字段名:user_id, userId, sub
|
||||
if (payload.user_id !== undefined) {
|
||||
return String(payload.user_id);
|
||||
}
|
||||
if (payload.userId !== undefined) {
|
||||
return String(payload.userId);
|
||||
}
|
||||
if (payload.sub !== undefined) {
|
||||
return String(payload.sub);
|
||||
}
|
||||
|
||||
console.warn("[JWT] payload 中没有 user_id, userId 或 sub 字段");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测 JWT token 是否已过期
|
||||
* @param token JWT token
|
||||
* @param bufferSeconds 提前多少秒判定为过期(默认60秒)
|
||||
* @returns true 表示已过期,false 表示未过期,null 表示无法判断
|
||||
*/
|
||||
export function isTokenExpired(
|
||||
token: string,
|
||||
bufferSeconds: number = 60,
|
||||
): boolean | null {
|
||||
const payload = parseJwtPayload(token);
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.exp === undefined) {
|
||||
console.warn("[JWT] payload 中没有 exp 字段,无法判断过期");
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const expTime = payload.exp - bufferSeconds;
|
||||
const isExpired = now >= expTime;
|
||||
|
||||
if (isExpired) {
|
||||
console.warn("[JWT] token 已过期,exp:", payload.exp, "当前:", now);
|
||||
}
|
||||
|
||||
return isExpired;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JWT token 中获取 ispluginTrial 标识
|
||||
* @param token JWT token
|
||||
* @returns true=插件试用用户,false=正式用户,null=无法判断
|
||||
*/
|
||||
export function getIsPluginTrialFromToken(token: string): boolean | null {
|
||||
const payload = parseJwtPayload(token);
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查 ispluginTrial 字段
|
||||
if (payload.ispluginTrial !== undefined) {
|
||||
console.log("[JWT] 从 token 中获取到 ispluginTrial:", payload.ispluginTrial);
|
||||
return payload.ispluginTrial === true;
|
||||
}
|
||||
|
||||
console.log("[JWT] token 中没有 ispluginTrial 字段,判定为正式用户");
|
||||
return false;
|
||||
}
|
||||
@ -18,6 +18,17 @@ import { ChatHistoryManager } from "./chatHistoryManager";
|
||||
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";
|
||||
import { showFileDiff } from "./fileDiff";
|
||||
import { changeTracker } from "../services/changeTracker";
|
||||
import { generateDiff, renderDiffHtml } from "./diffRenderer";
|
||||
|
||||
import type { RunMode, ServiceTier } from "../types/api";
|
||||
|
||||
@ -30,25 +41,12 @@ let currentSession: DialogSession | null = null;
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||
let pendingPlanExecution: {
|
||||
panel: vscode.WebviewPanel;
|
||||
planTitle: string;
|
||||
extensionPath: string;
|
||||
taskId: string; // 保存 taskId 以便复用
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* 设置待执行的计划(由 ICHelperPanel 调用)
|
||||
*/
|
||||
export function setPendingPlanExecution(
|
||||
panel: vscode.WebviewPanel,
|
||||
planTitle: string,
|
||||
extensionPath: string,
|
||||
taskId: string
|
||||
): void {
|
||||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
||||
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId);
|
||||
async function trackFileChange(filePath: string, oldContent: string, newContent: string): Promise<void> {
|
||||
try {
|
||||
changeTracker.trackChange(filePath, oldContent, newContent);
|
||||
} catch (error) {
|
||||
console.warn("[MessageHandler] 记录文件变更失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,10 +57,107 @@ export async function handleUserMessage(
|
||||
text: string,
|
||||
extensionPath?: string,
|
||||
mode?: RunMode,
|
||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||
serviceTier?: ServiceTier, // 服务等级参数
|
||||
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
|
||||
) {
|
||||
console.log("收到用户消息:", text);
|
||||
|
||||
// 检查 token 是否过期
|
||||
const context = (panel as any).__context;
|
||||
if (context) {
|
||||
// 从 session 中获取 token
|
||||
let token: string | undefined;
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
token = session?.accessToken;
|
||||
} catch (error) {
|
||||
console.warn("[MessageHandler] 获取 session 失败:", error);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.warn("[MessageHandler] 未登录,阻止发送");
|
||||
|
||||
// 保存待发送的消息
|
||||
await context.globalState.update('pendingMessage', {
|
||||
text,
|
||||
mode,
|
||||
serviceTier,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 显示弹窗提示
|
||||
const action = await vscode.window.showWarningMessage(
|
||||
'请先登录后再发送消息',
|
||||
'立即登录'
|
||||
);
|
||||
|
||||
if (action === '立即登录') {
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTokenExpired(token)) {
|
||||
console.warn("[MessageHandler] Token 已过期,阻止发送");
|
||||
|
||||
// 保存待发送的消息
|
||||
await context.globalState.update('pendingMessage', {
|
||||
text,
|
||||
mode,
|
||||
serviceTier,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 清除过期的 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", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查试用期是否过期
|
||||
const trialService = new TrialExpirationService(context, panel);
|
||||
const isExpired = await trialService.checkExpiration();
|
||||
if (isExpired) {
|
||||
console.warn("[MessageHandler] 试用期已过期,阻止发送");
|
||||
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 记录用户消息到历史(允许失败,不阻塞主流程)
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
@ -88,6 +183,29 @@ 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 {
|
||||
@ -97,7 +215,8 @@ export async function handleUserMessage(
|
||||
extensionPath,
|
||||
mode,
|
||||
undefined,
|
||||
serviceTier
|
||||
serviceTier,
|
||||
contextItems
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
@ -134,29 +253,36 @@ async function handleUserMessageWithBackend(
|
||||
extensionPath: string,
|
||||
mode?: RunMode,
|
||||
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||
serviceTier?: ServiceTier, // 服务等级参数
|
||||
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
|
||||
): Promise<void> {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
|
||||
// 处理上下文项:在消息前附加文件/文件夹路径
|
||||
let enhancedText = text;
|
||||
if (contextItems && contextItems.length > 0) {
|
||||
console.log("[MessageHandler] 处理上下文项:", contextItems.length);
|
||||
const paths = contextItems.map(item => item.path).join('\n');
|
||||
enhancedText = `${paths}\n\n${text}`;
|
||||
}
|
||||
|
||||
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
||||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(
|
||||
extensionPath,
|
||||
taskIdToUse || undefined
|
||||
);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
console.log(
|
||||
"[MessageHandler] 创建会话: taskId=",
|
||||
lastTaskId,
|
||||
"来源=",
|
||||
taskIdToUse ? "historyManager" : "新生成"
|
||||
);
|
||||
}
|
||||
// 创建会话(dialogManager 会自动处理旧会话的中止)
|
||||
currentSession = dialogManager.createSession(
|
||||
extensionPath,
|
||||
taskIdToUse || undefined
|
||||
);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
console.log(
|
||||
"[MessageHandler] 创建会话: taskId=",
|
||||
lastTaskId,
|
||||
"来源=",
|
||||
taskIdToUse ? "historyManager" : "新生成"
|
||||
);
|
||||
|
||||
// 显示状态栏
|
||||
panel.webview.postMessage({
|
||||
@ -167,7 +293,7 @@ async function handleUserMessageWithBackend(
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
currentSession!.sendMessage(
|
||||
text,
|
||||
enhancedText,
|
||||
{
|
||||
onText: (fullText, isStreaming) => {
|
||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||
@ -198,7 +324,7 @@ async function handleUserMessageWithBackend(
|
||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onQuestion: (askId, question, options) => {
|
||||
onQuestion: (askId: string, questions: import("../types/api").QuestionItem[]) => {
|
||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
@ -208,22 +334,9 @@ async function handleUserMessageWithBackend(
|
||||
},
|
||||
|
||||
onComplete: async (segments) => {
|
||||
// 隐藏状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "hideStatus",
|
||||
});
|
||||
|
||||
// 最后一次发送完整的段落
|
||||
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
||||
|
||||
const result = await panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
isComplete: true,
|
||||
});
|
||||
console.log("[MessageHandler] postMessage 返回值:", result);
|
||||
|
||||
// 保存完整的 segments 到历史记录
|
||||
// 先保存到历史记录(优先级最高,确保数据不丢失)
|
||||
try {
|
||||
// 将完整的 segments 保存到一条 AI 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
@ -233,41 +346,51 @@ async function handleUserMessageWithBackend(
|
||||
.join("\n");
|
||||
|
||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||
console.log("[MessageHandler] AI响应已保存到历史记录");
|
||||
} catch (error) {
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
console.error("[MessageHandler] 保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||||
if (pendingPlanExecution) {
|
||||
const {
|
||||
panel: execPanel,
|
||||
planTitle,
|
||||
extensionPath: execPath,
|
||||
taskId: reuseTaskId,
|
||||
} = pendingPlanExecution;
|
||||
pendingPlanExecution = null;
|
||||
console.log(
|
||||
"[MessageHandler] 自动执行计划:",
|
||||
planTitle,
|
||||
"复用 taskId:",
|
||||
reuseTaskId
|
||||
// 对话完成后重新获取余额(因为已经消耗了 Credits)
|
||||
try {
|
||||
console.log("[MessageHandler] 对话完成,重新获取余额...");
|
||||
const newBalance = await fetchBalance();
|
||||
if (newBalance !== null) {
|
||||
console.log("[MessageHandler] 余额已更新:", newBalance);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[MessageHandler] 获取余额失败:", error);
|
||||
}
|
||||
|
||||
// 尝试更新面板(如果面板已关闭,这些操作会失败,但不影响数据保存)
|
||||
try {
|
||||
// 隐藏状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "hideStatus",
|
||||
});
|
||||
|
||||
// 发送完成标记(不再重复发送 segments,避免内容重复显示)
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
|
||||
// 发送系统通知 - AI 响应完成
|
||||
const notificationService = NotificationService.getInstance();
|
||||
notificationService.success(
|
||||
'IC Coder - AI 响应完成',
|
||||
'您的问题已得到回复,点击查看详情',
|
||||
() => {
|
||||
// 点击通知时聚焦到面板
|
||||
panel.reveal();
|
||||
}
|
||||
);
|
||||
|
||||
// 延迟一小段时间确保当前对话完全结束
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||||
await handleUserMessageWithBackend(
|
||||
execPanel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
execPath,
|
||||
"agent",
|
||||
reuseTaskId // 复用 Plan 模式的 taskId
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[MessageHandler] 自动执行计划失败:", err);
|
||||
}
|
||||
}, 500);
|
||||
// 发送代码变更到前端
|
||||
sendChangesToWebview(panel);
|
||||
} catch (error) {
|
||||
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
|
||||
}
|
||||
|
||||
resolve();
|
||||
@ -303,6 +426,36 @@ async function handleUserMessageWithBackend(
|
||||
percentage: data.percentage,
|
||||
});
|
||||
},
|
||||
|
||||
onPhaseProgress: (phaseId, status) => {
|
||||
// 发送阶段进度更新到 WebView
|
||||
// 映射 phaseId: sim -> simulation
|
||||
const stepMap: Record<string, string> = {
|
||||
spec: "spec",
|
||||
design: "design",
|
||||
sim: "simulation",
|
||||
done: "done",
|
||||
};
|
||||
const step = stepMap[phaseId] || phaseId;
|
||||
|
||||
if (status === "current") {
|
||||
// 显示进度条并更新到当前步骤
|
||||
panel.webview.postMessage({ type: "showProgress" });
|
||||
panel.webview.postMessage({ type: "updateProgress", step });
|
||||
} else if (status === "completed") {
|
||||
// 更新到下一步(或完成)
|
||||
const steps = ["spec", "design", "simulation", "done"];
|
||||
const currentIndex = steps.indexOf(step);
|
||||
if (currentIndex < steps.length - 1) {
|
||||
panel.webview.postMessage({
|
||||
type: "updateProgress",
|
||||
step: steps[currentIndex + 1],
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({ type: "completeProgress" });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mode,
|
||||
serviceTier // 传递服务等级
|
||||
@ -316,10 +469,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,9 +539,17 @@ export async function handlePlanAction(
|
||||
panel: vscode.WebviewPanel,
|
||||
action: string,
|
||||
planTitle: string,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
serviceTier?: ServiceTier
|
||||
): Promise<void> {
|
||||
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle);
|
||||
console.log(
|
||||
"[handlePlanAction] action:",
|
||||
action,
|
||||
"planTitle:",
|
||||
planTitle,
|
||||
"serviceTier:",
|
||||
serviceTier
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
case "confirm":
|
||||
@ -401,7 +563,8 @@ export async function handlePlanAction(
|
||||
panel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
extensionPath,
|
||||
"agent"
|
||||
"agent",
|
||||
serviceTier
|
||||
);
|
||||
break;
|
||||
|
||||
@ -417,7 +580,8 @@ export async function handlePlanAction(
|
||||
panel,
|
||||
`请根据以下建议修改计划:${modification}`,
|
||||
extensionPath,
|
||||
"plan"
|
||||
"plan",
|
||||
serviceTier
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -628,11 +792,14 @@ async function handleFileOperation(
|
||||
if (!operation.searchText || !operation.replaceText) {
|
||||
throw new Error("缺少替换内容");
|
||||
}
|
||||
const oldContentBeforeReplace = await readFileContent(operation.filePath);
|
||||
await replaceFile(
|
||||
operation.filePath,
|
||||
operation.searchText,
|
||||
operation.replaceText
|
||||
);
|
||||
const newContentAfterReplace = await readFileContent(operation.filePath);
|
||||
await trackFileChange(operation.filePath, oldContentBeforeReplace, newContentAfterReplace);
|
||||
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
@ -738,6 +905,16 @@ export async function handleCreateFile(
|
||||
message: " 文件创建成功",
|
||||
});
|
||||
vscode.window.showInformationMessage(`文件创建成功: ${filePath}`);
|
||||
|
||||
// 发送系统通知
|
||||
const notificationService = NotificationService.getInstance();
|
||||
notificationService.success(
|
||||
'IC Coder - 文件创建',
|
||||
`文件已创建: ${path.basename(filePath)}`,
|
||||
() => {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
panel.webview.postMessage({
|
||||
command: "fileCreateError",
|
||||
@ -758,13 +935,22 @@ export async function handleUpdateFile(
|
||||
content: string
|
||||
) {
|
||||
try {
|
||||
const oldContent = await readFileContent(filePath);
|
||||
await updateFile(filePath, content);
|
||||
await trackFileChange(filePath, oldContent, content);
|
||||
panel.webview.postMessage({
|
||||
command: "fileUpdated",
|
||||
filePath: filePath,
|
||||
message: " 文件更新成功",
|
||||
});
|
||||
vscode.window.showInformationMessage(`文件更新成功: ${filePath}`);
|
||||
|
||||
// 发送系统通知
|
||||
const notificationService = NotificationService.getInstance();
|
||||
notificationService.info(
|
||||
'IC Coder - 文件更新',
|
||||
`文件已更新: ${path.basename(filePath)}`
|
||||
);
|
||||
} catch (error) {
|
||||
panel.webview.postMessage({
|
||||
command: "fileUpdateError",
|
||||
@ -816,7 +1002,10 @@ export async function handleReplaceInFile(
|
||||
replaceText: string
|
||||
) {
|
||||
try {
|
||||
const oldContent = await readFileContent(filePath);
|
||||
await replaceFile(filePath, searchText, replaceText);
|
||||
const newContent = await readFileContent(filePath);
|
||||
await trackFileChange(filePath, oldContent, newContent);
|
||||
panel.webview.postMessage({
|
||||
command: "fileReplaced",
|
||||
filePath: filePath,
|
||||
@ -972,6 +1161,17 @@ async function handleVCDGeneration(
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
|
||||
|
||||
// 发送系统通知
|
||||
const notificationService = NotificationService.getInstance();
|
||||
notificationService.success(
|
||||
'IC Coder - 仿真完成',
|
||||
`VCD 文件已生成: ${fileName}`,
|
||||
() => {
|
||||
// 点击通知时打开 VCD 查看器
|
||||
vscode.commands.executeCommand('ic-coder.openVCDViewer', result.vcdFilePath);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
@ -995,6 +1195,17 @@ async function handleVCDGeneration(
|
||||
});
|
||||
|
||||
vscode.window.showErrorMessage("VCD 文件生成失败");
|
||||
|
||||
// 发送系统通知
|
||||
const notificationService = NotificationService.getInstance();
|
||||
notificationService.error(
|
||||
'IC Coder - 仿真失败',
|
||||
'VCD 文件生成失败,请查看错误信息',
|
||||
() => {
|
||||
// 点击通知时聚焦到面板
|
||||
panel.reveal();
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `❌ 生成 VCD 文件时出错: ${
|
||||
@ -1007,5 +1218,209 @@ async function handleVCDGeneration(
|
||||
});
|
||||
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
|
||||
// 发送系统通知
|
||||
const notificationService = NotificationService.getInstance();
|
||||
notificationService.error(
|
||||
'IC Coder - 仿真错误',
|
||||
error instanceof Error ? error.message : '生成 VCD 文件时出错',
|
||||
() => {
|
||||
panel.reveal();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提示词优化请求
|
||||
*/
|
||||
export async function handleOptimizePrompt(
|
||||
panel: vscode.WebviewPanel,
|
||||
prompt: string
|
||||
): Promise<void> {
|
||||
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
|
||||
console.log("[MessageHandler] prompt:", prompt);
|
||||
console.log("[MessageHandler] prompt 长度:", prompt?.length);
|
||||
|
||||
try {
|
||||
console.log("[MessageHandler] 开始调用 optimizePrompt...");
|
||||
const optimized = await optimizePrompt(prompt);
|
||||
console.log("[MessageHandler] 优化成功,结果:", optimized);
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: true,
|
||||
optimizedPrompt: optimized,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : "优化失败";
|
||||
console.error("[MessageHandler] 提示词优化失败:", errorMsg);
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: errorMsg,
|
||||
});
|
||||
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理采纳变更
|
||||
*/
|
||||
export async function handleAcceptChange(
|
||||
panel: vscode.WebviewPanel,
|
||||
changeId: string
|
||||
) {
|
||||
try {
|
||||
const success = await changeTracker.acceptChange(changeId);
|
||||
if (success) {
|
||||
panel.webview.postMessage({
|
||||
command: "changeAccepted",
|
||||
changeId: changeId,
|
||||
success: true
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "changeAccepted",
|
||||
changeId: changeId,
|
||||
success: false,
|
||||
error: "采纳变更失败"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[MessageHandler] 采纳变更失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "changeAccepted",
|
||||
changeId: changeId,
|
||||
success: false,
|
||||
error: String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拒绝变更
|
||||
*/
|
||||
export async function handleRejectChange(
|
||||
panel: vscode.WebviewPanel,
|
||||
changeId: string
|
||||
) {
|
||||
try {
|
||||
const success = await changeTracker.rejectChange(changeId);
|
||||
if (success) {
|
||||
panel.webview.postMessage({
|
||||
command: "changeRejected",
|
||||
changeId: changeId,
|
||||
success: true
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "changeRejected",
|
||||
changeId: changeId,
|
||||
success: false,
|
||||
error: "拒绝变更失败"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[MessageHandler] 拒绝变更失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "changeRejected",
|
||||
changeId: changeId,
|
||||
success: false,
|
||||
error: String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在对话结束时发送变更列表到前端
|
||||
*/
|
||||
export function sendChangesToWebview(panel: vscode.WebviewPanel) {
|
||||
const session = changeTracker.endSession();
|
||||
if (session && session.changes.length > 0) {
|
||||
const changesWithDiff = session.changes.map(change => {
|
||||
const diffLines = generateDiff(change.oldContent, change.newContent);
|
||||
const diffHtml = renderDiffHtml(diffLines);
|
||||
return {
|
||||
...change,
|
||||
diffHtml
|
||||
};
|
||||
});
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showChanges",
|
||||
changes: changesWithDiff
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始新的变更会话
|
||||
*/
|
||||
export function startChangeSession(sessionId: string) {
|
||||
changeTracker.startSession(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文件 diff 编辑器
|
||||
*/
|
||||
export async function handleOpenFileDiff(
|
||||
panel: vscode.WebviewPanel,
|
||||
changeId: string
|
||||
) {
|
||||
try {
|
||||
const session = changeTracker.getCurrentSession();
|
||||
if (!session) {
|
||||
vscode.window.showErrorMessage('没有找到变更会话');
|
||||
return;
|
||||
}
|
||||
|
||||
const change = session.changes.find(c => c.changeId === changeId);
|
||||
if (!change) {
|
||||
vscode.window.showErrorMessage('没有找到该变更');
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showErrorMessage('没有打开的工作区');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建临时文件用于对比
|
||||
const filePath = change.filePath;
|
||||
const absolutePath = vscode.Uri.file(
|
||||
path.join(workspaceFolder.uri.fsPath, filePath)
|
||||
);
|
||||
|
||||
// 创建虚拟文档显示旧内容
|
||||
const oldUri = vscode.Uri.parse(
|
||||
`ic-coder-diff:${filePath}.old?${changeId}`
|
||||
).with({ scheme: 'ic-coder-diff' });
|
||||
|
||||
// 注册文档内容提供者(如果还没注册)
|
||||
if (!(global as any).__diffProviderRegistered) {
|
||||
const provider = new (class implements vscode.TextDocumentContentProvider {
|
||||
provideTextDocumentContent(uri: vscode.Uri): string {
|
||||
const changeId = uri.query;
|
||||
const session = changeTracker.getCurrentSession();
|
||||
const change = session?.changes.find(c => c.changeId === changeId);
|
||||
return change?.oldContent || '';
|
||||
}
|
||||
})();
|
||||
|
||||
vscode.workspace.registerTextDocumentContentProvider('ic-coder-diff', provider);
|
||||
(global as any).__diffProviderRegistered = true;
|
||||
}
|
||||
|
||||
// 打开 diff 编辑器
|
||||
await vscode.commands.executeCommand(
|
||||
'vscode.diff',
|
||||
oldUri,
|
||||
absolutePath,
|
||||
`${filePath} (变更对比)`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[MessageHandler] 打开 diff 失败:', error);
|
||||
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}`
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import * as vscode from "vscode";
|
||||
import { getWebviewContent } from "./webviewContent";
|
||||
import { isTokenExpired } from "../utils/jwtUtils";
|
||||
import {
|
||||
handleUserMessage,
|
||||
insertCodeToEditor,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
} from "../utils/messageHandler";
|
||||
|
||||
/**
|
||||
@ -26,7 +28,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")
|
||||
],
|
||||
}
|
||||
);
|
||||
@ -45,16 +47,26 @@ 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, "dist", "assets", "QRCode", "wx.png")
|
||||
);
|
||||
|
||||
// 获取Logo URI
|
||||
const logoUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png")
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
@ -63,12 +75,17 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString()
|
||||
maxIconUri.toString(),
|
||||
qrCodeUri.toString(),
|
||||
logoUri.toString()
|
||||
);
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
console.log("[ICViewProvider] ====== 收到 WebView 消息 ======");
|
||||
console.log("[ICViewProvider] command:", message.command);
|
||||
console.log("[ICViewProvider] 完整消息:", JSON.stringify(message));
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
||||
@ -104,18 +121,26 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "showWarning":
|
||||
vscode.window.showWarningMessage(message.message);
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput
|
||||
message.customInput,
|
||||
message.answers
|
||||
);
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
// 新增:优化提示词
|
||||
case "optimizePrompt":
|
||||
handleOptimizePrompt(panel, message.prompt);
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
@ -127,10 +152,34 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
* 侧边栏视图提供者
|
||||
*/
|
||||
export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
private _view?: vscode.WebviewView;
|
||||
|
||||
constructor(
|
||||
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)
|
||||
@ -138,26 +187,66 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
private async checkLoginStatus(): Promise<boolean> {
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
return !!session;
|
||||
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("检查登录状态失败:", error);
|
||||
console.log("[ICViewProvider] 检查登录状态失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||
console.log('[ICViewProvider] ========== resolveWebviewView 被调用 ==========');
|
||||
|
||||
// 保存引用以便后续刷新
|
||||
this._view = webviewView;
|
||||
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(this.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(this.extensionUri, "src", "assets")
|
||||
],
|
||||
};
|
||||
|
||||
// 检查是否已登录(使用 Authentication API)
|
||||
this.checkLoginStatus().then((isLoggedIn) => {
|
||||
webviewView.webview.html = this.getWebviewContent(
|
||||
webviewView.webview,
|
||||
isLoggedIn
|
||||
);
|
||||
});
|
||||
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.onDidReceiveMessage(
|
||||
@ -166,6 +255,17 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
vscode.commands.executeCommand("ic-coder.openChat");
|
||||
} else if (message.command === "login") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
} else if (message.command === "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 === "openExternalUrl") {
|
||||
// 打开外部链接
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
@ -177,187 +277,88 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
webview: vscode.Webview,
|
||||
isLoggedIn: boolean
|
||||
): string {
|
||||
console.log('[ICViewProvider] 开始生成 HTML 内容, isLoggedIn:', isLoggedIn);
|
||||
|
||||
const logoUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this.extensionUri, "media", "icon.png")
|
||||
);
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--vscode-font-family);
|
||||
color: var(--vscode-foreground);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg,
|
||||
var(--vscode-editor-background) 0%,
|
||||
color-mix(in srgb, var(--vscode-editor-background) 85%, var(--vscode-button-background) 15%) 50%,
|
||||
color-mix(in srgb, var(--vscode-editor-background) 90%, var(--vscode-button-background) 10%) 100%);
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container img {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.container h2 {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
.btn {
|
||||
width: 200px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
.btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="${logoUri}" alt="IC Coder" width="120" />
|
||||
<h2>欢迎使用 IC Coder</h2>
|
||||
${
|
||||
isLoggedIn
|
||||
? '<button class="btn" onclick="openChat()">开始创作</button>'
|
||||
: '<button class="btn" onclick="login()">登录账户</button>'
|
||||
}
|
||||
</div>
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--vscode-font-family);
|
||||
color: var(--vscode-foreground);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg,
|
||||
var(--vscode-editor-background) 0%,
|
||||
color-mix(in srgb, var(--vscode-editor-background) 85%, var(--vscode-button-background) 15%) 50%,
|
||||
color-mix(in srgb, var(--vscode-editor-background) 90%, var(--vscode-button-background) 10%) 100%);
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container img {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.container h2 {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
.btn {
|
||||
width: 200px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #005a9e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="${logoUri}" alt="IC Coder" width="120" />
|
||||
<h2>欢迎使用 IC Coder</h2>
|
||||
${isLoggedIn
|
||||
? '<button class="btn" onclick="openChat()">开始创作</button>'
|
||||
: '<button class="btn" onclick="login()">登录账户</button>'
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
console.log('[Webview] 脚本已加载');
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
function openChat() {
|
||||
console.log('[Webview] 点击开始创作');
|
||||
vscode.postMessage({ command: 'openChat' });
|
||||
}
|
||||
|
||||
function openChat() {
|
||||
vscode.postMessage({ command: 'openChat' });
|
||||
}
|
||||
function login() {
|
||||
console.log('[Webview] 点击登录');
|
||||
vscode.postMessage({ command: 'login' });
|
||||
}
|
||||
|
||||
// 登录功能
|
||||
function login() {
|
||||
vscode.postMessage({ command: 'login' });
|
||||
}
|
||||
|
||||
function generateCode(type) {
|
||||
const code = getCodeTemplate(type);
|
||||
vscode.postMessage({
|
||||
command: 'insertCode',
|
||||
code: code
|
||||
});
|
||||
}
|
||||
|
||||
function getCodeTemplate(type) {
|
||||
const templates = {
|
||||
counter: \`module counter #(
|
||||
parameter WIDTH = 4
|
||||
)(
|
||||
input wire clk,
|
||||
input wire rst_n,
|
||||
input wire enable,
|
||||
output reg [WIDTH-1:0] count
|
||||
);
|
||||
always @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
count <= 0;
|
||||
end else if (enable) begin
|
||||
count <= count + 1;
|
||||
end
|
||||
end
|
||||
endmodule\`,
|
||||
fsm: \`module fsm (
|
||||
input wire clk,
|
||||
input wire rst_n,
|
||||
input wire start,
|
||||
output reg done
|
||||
);
|
||||
parameter IDLE = 2'b00;
|
||||
parameter STATE1 = 2'b01;
|
||||
parameter STATE2 = 2'b10;
|
||||
|
||||
reg [1:0] state, next_state;
|
||||
|
||||
always @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
state <= IDLE;
|
||||
end else begin
|
||||
state <= next_state;
|
||||
end
|
||||
end
|
||||
|
||||
always @(*) begin
|
||||
case (state)
|
||||
IDLE: next_state = start ? STATE1 : IDLE;
|
||||
STATE1: next_state = STATE2;
|
||||
STATE2: next_state = IDLE;
|
||||
default: next_state = IDLE;
|
||||
endcase
|
||||
end
|
||||
|
||||
assign done = (state == STATE2);
|
||||
endmodule\`,
|
||||
fifo: \`module sync_fifo #(
|
||||
parameter DATA_WIDTH = 8,
|
||||
parameter DEPTH = 16
|
||||
)(
|
||||
input wire clk,
|
||||
input wire rst_n,
|
||||
input wire wr_en,
|
||||
input wire [DATA_WIDTH-1:0] din,
|
||||
input wire rd_en,
|
||||
output reg [DATA_WIDTH-1:0] dout,
|
||||
output wire full,
|
||||
output wire empty
|
||||
);
|
||||
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
|
||||
reg [$clog2(DEPTH):0] wr_ptr, rd_ptr;
|
||||
|
||||
assign full = (wr_ptr == rd_ptr + DEPTH);
|
||||
assign empty = (wr_ptr == rd_ptr);
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (!rst_n) wr_ptr <= 0;
|
||||
else if (wr_en && !full) begin
|
||||
mem[wr_ptr] <= din;
|
||||
wr_ptr <= wr_ptr + 1;
|
||||
end
|
||||
end
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (!rst_n) begin
|
||||
rd_ptr <= 0;
|
||||
dout <= 0;
|
||||
end else if (rd_en && !empty) begin
|
||||
dout <= mem[rd_ptr];
|
||||
rd_ptr <= rd_ptr + 1;
|
||||
end
|
||||
end
|
||||
endmodule\`
|
||||
};
|
||||
return templates[type] || '// 代码模板';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
console.log('[Webview] 初始化完成');
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ export function getAgentCardStyles(): string {
|
||||
.agent-name {
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
font-size:14px
|
||||
}
|
||||
.agent-status {
|
||||
font-size: 11px;
|
||||
@ -99,14 +100,14 @@ export function getAgentCardStyles(): string {
|
||||
/* 低调显示的工具调用样式 */
|
||||
.agent-step.low-profile {
|
||||
opacity: 0.85;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.agent-step.low-profile .step-icon {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.agent-step.low-profile .step-name {
|
||||
font-weight: 400;
|
||||
@ -115,7 +116,7 @@ export function getAgentCardStyles(): string {
|
||||
}
|
||||
.agent-step.low-profile .step-result {
|
||||
opacity: 0.85;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
489
src/views/changePanel.ts
Normal file
@ -0,0 +1,489 @@
|
||||
/**
|
||||
* 代码变更面板组件
|
||||
* 功能:显示 AI 修改的文件列表和 diff 对比
|
||||
* 依赖:utils/diffRenderer
|
||||
* 使用场景:对话结束后展示代码变更供用户审查
|
||||
*/
|
||||
|
||||
import { getDiffStyles } from "../utils/diffRenderer";
|
||||
|
||||
/**
|
||||
* 获取变更面板的 HTML 内容
|
||||
*/
|
||||
export function getChangePanelContent(): string {
|
||||
return `
|
||||
<div class="change-panel" id="changePanel" style="display: none;">
|
||||
<div class="change-panel-header" onclick="toggleChangePanel()">
|
||||
<div class="change-panel-title">
|
||||
<span>代码变更</span>
|
||||
<span class="change-count" id="changeCount">0</span>
|
||||
</div>
|
||||
<div class="change-panel-actions" onclick="event.stopPropagation()">
|
||||
<button class="batch-action-btn accept-all-btn" onclick="acceptAllChanges()" title="采纳全部">
|
||||
<span>✓ 全部采纳</span>
|
||||
</button>
|
||||
<button class="batch-action-btn reject-all-btn" onclick="rejectAllChanges()" title="拒绝全部">
|
||||
<span>✕ 全部拒绝</span>
|
||||
</button>
|
||||
<button class="change-toggle-btn" id="changePanelToggle">
|
||||
<span class="toggle-icon">▼</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="change-panel-body" id="changePanelBody" style="display: none;">
|
||||
<div class="change-list" id="changeList">
|
||||
<!-- 变更列表将动态插入 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变更面板的样式
|
||||
*/
|
||||
export function getChangePanelStyles(): string {
|
||||
return `
|
||||
.change-panel {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-editor-background);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.change-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
user-select: none;
|
||||
border-bottom: 2px solid var(--vscode-panel-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.change-panel-header:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.change-panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.change-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.change-count {
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.change-panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.batch-action-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.batch-action-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.accept-all-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.accept-all-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.reject-all-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reject-all-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.change-toggle-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.change-toggle-btn:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
display: inline-block;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.change-panel-body {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.change-list {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.change-item {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.change-item:hover {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.change-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.change-item-header:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.change-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.change-type-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.change-type-create {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.change-type-modify {
|
||||
background: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.change-type-delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.change-file-path {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.change-item-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.change-action-btn {
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.change-action-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.accept-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.accept-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.reject-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reject-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.change-item-diff {
|
||||
padding: 12px;
|
||||
background: var(--vscode-editor-background);
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
display: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.change-item-diff.expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
${getDiffStyles()}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变更面板的脚本
|
||||
*/
|
||||
export function getChangePanelScript(): string {
|
||||
return `
|
||||
// 切换变更面板展开/收起
|
||||
function toggleChangePanel() {
|
||||
const body = document.getElementById('changePanelBody');
|
||||
const toggleIcon = document.querySelector('.toggle-icon');
|
||||
|
||||
if (body.style.display === 'none') {
|
||||
body.style.display = 'block';
|
||||
toggleIcon.classList.add('expanded');
|
||||
} else {
|
||||
body.style.display = 'none';
|
||||
toggleIcon.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// 全部采纳
|
||||
window.acceptAllChanges = function() {
|
||||
const changeList = document.getElementById('changeList');
|
||||
if (!changeList) {
|
||||
console.error('changeList not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const items = Array.from(changeList.querySelectorAll('.change-item'));
|
||||
console.log('Found items:', items.length);
|
||||
|
||||
if (items.length === 0) {
|
||||
alert('没有待处理的变更');
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
const changeId = item.id.replace('change-item-', '');
|
||||
console.log('Accepting change:', changeId);
|
||||
vscode.postMessage({
|
||||
command: 'acceptChange',
|
||||
changeId: changeId
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 全部拒绝
|
||||
window.rejectAllChanges = function() {
|
||||
const changeList = document.getElementById('changeList');
|
||||
if (!changeList) {
|
||||
console.error('changeList not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const items = Array.from(changeList.querySelectorAll('.change-item'));
|
||||
console.log('Found items:', items.length);
|
||||
|
||||
if (items.length === 0) {
|
||||
alert('没有待处理的变更');
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
const changeId = item.id.replace('change-item-', '');
|
||||
console.log('Rejecting change:', changeId);
|
||||
vscode.postMessage({
|
||||
command: 'rejectChange',
|
||||
changeId: changeId
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 打开文件 diff(在 VS Code 中打开)
|
||||
function openFileDiff(changeId) {
|
||||
vscode.postMessage({
|
||||
command: 'openFileDiff',
|
||||
changeId: changeId
|
||||
});
|
||||
}
|
||||
|
||||
// 采纳变更
|
||||
function acceptChange(changeId) {
|
||||
vscode.postMessage({
|
||||
command: 'acceptChange',
|
||||
changeId: changeId
|
||||
});
|
||||
}
|
||||
|
||||
// 拒绝变更
|
||||
function rejectChange(changeId) {
|
||||
vscode.postMessage({
|
||||
command: 'rejectChange',
|
||||
changeId: changeId
|
||||
});
|
||||
}
|
||||
|
||||
// 显示变更面板(从后端接收变更列表)
|
||||
window.showChangesPanel = function(changes) {
|
||||
const changePanel = document.getElementById('changePanel');
|
||||
const changeList = document.getElementById('changeList');
|
||||
const changeCount = document.getElementById('changeCount');
|
||||
|
||||
if (!changePanel || !changeList || !changeCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新变更数量
|
||||
changeCount.textContent = changes.length;
|
||||
|
||||
// 清空现有列表
|
||||
changeList.innerHTML = '';
|
||||
|
||||
// 渲染每个变更项
|
||||
changes.forEach(change => {
|
||||
const changeItem = createChangeItem(change);
|
||||
changeList.appendChild(changeItem);
|
||||
});
|
||||
|
||||
// 显示面板
|
||||
changePanel.style.display = 'block';
|
||||
};
|
||||
|
||||
// 创建单个变更项的 DOM 元素
|
||||
function createChangeItem(change) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'change-item';
|
||||
item.id = 'change-item-' + change.changeId;
|
||||
|
||||
const typeLabel = change.changeType === 'create' ? '新建' :
|
||||
change.changeType === 'modify' ? '修改' : '删除';
|
||||
|
||||
item.innerHTML = \`
|
||||
<div class="change-item-header" onclick="openFileDiff('\${change.changeId}')">
|
||||
<div class="change-item-info">
|
||||
<span class="change-type-badge change-type-\${change.changeType}">\${typeLabel}</span>
|
||||
<span class="change-file-path">\${change.filePath}</span>
|
||||
</div>
|
||||
<div class="change-item-actions">
|
||||
<button class="change-action-btn accept-btn" onclick="event.stopPropagation(); acceptChange('\${change.changeId}')">采纳</button>
|
||||
<button class="change-action-btn reject-btn" onclick="event.stopPropagation(); rejectChange('\${change.changeId}')">拒绝</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// 处理采纳变更的响应
|
||||
window.handleChangeAccepted = function(changeId, success, error) {
|
||||
if (success) {
|
||||
// 从列表中移除该变更项
|
||||
const item = document.getElementById('change-item-' + changeId);
|
||||
if (item) {
|
||||
item.remove();
|
||||
}
|
||||
|
||||
// 更新变更数量
|
||||
updateChangeCount();
|
||||
} else {
|
||||
console.error('采纳变更失败:', error);
|
||||
alert('采纳变更失败: ' + (error || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理拒绝变更的响应
|
||||
window.handleChangeRejected = function(changeId, success, error) {
|
||||
if (success) {
|
||||
// 从列表中移除该变更项
|
||||
const item = document.getElementById('change-item-' + changeId);
|
||||
if (item) {
|
||||
item.remove();
|
||||
}
|
||||
|
||||
// 更新变更数量
|
||||
updateChangeCount();
|
||||
} else {
|
||||
console.error('拒绝变更失败:', error);
|
||||
alert('拒绝变更失败: ' + (error || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新变更数量
|
||||
function updateChangeCount() {
|
||||
const changeList = document.getElementById('changeList');
|
||||
const changeCount = document.getElementById('changeCount');
|
||||
const changePanel = document.getElementById('changePanel');
|
||||
|
||||
if (changeList && changeCount) {
|
||||
const count = changeList.children.length;
|
||||
changeCount.textContent = count;
|
||||
|
||||
// 如果没有变更了,隐藏面板
|
||||
if (count === 0 && changePanel) {
|
||||
changePanel.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -14,11 +14,9 @@ export function getContextButtonContent(): string {
|
||||
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
|
||||
</svg>
|
||||
<span class="add-context-label">添加上下文</span>
|
||||
<svg class="dropdown-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 714.666667L213.333333 416l42.666667-42.666667L512 629.333333l256-256 42.666667 42.666667z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
</button>
|
||||
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
||||
<span class="tooltiptext">添加文件、文件夹作为上下文</span>
|
||||
</div>
|
||||
|
||||
<!-- 上拉菜单 -->
|
||||
@ -43,18 +41,18 @@ export function getContextButtonContent(): string {
|
||||
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="handleAddImage()">
|
||||
<!-- <div class="context-menu-item" onclick="handleAddImage()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>图片</span>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="handleAddDocument()">
|
||||
</div> -->
|
||||
<!-- <div class="context-menu-item" onclick="handleAddDocument()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>文档库</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 文件/文件夹列表视图 -->
|
||||
@ -273,6 +271,7 @@ export function getContextButtonStyles(): string {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.context-menu-list-item label {
|
||||
@ -337,6 +336,7 @@ export function getContextButtonScript(): string {
|
||||
return `
|
||||
// 上下文菜单状态
|
||||
let currentListData = [];
|
||||
let filteredListData = [];
|
||||
let currentListType = '';
|
||||
let selectedItems = new Set();
|
||||
|
||||
@ -394,6 +394,15 @@ export function getContextButtonScript(): string {
|
||||
|
||||
selectedItems.clear();
|
||||
currentListData = [];
|
||||
filteredListData = [];
|
||||
clearContextSearchInput();
|
||||
}
|
||||
|
||||
function clearContextSearchInput() {
|
||||
const searchInput = document.getElementById('contextMenuSearch');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到列表视图
|
||||
@ -408,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();
|
||||
}
|
||||
}
|
||||
@ -421,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();
|
||||
}
|
||||
|
||||
// 更新选中数量
|
||||
@ -459,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();
|
||||
}
|
||||
|
||||
// 添加图片
|
||||
@ -486,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);
|
||||
});
|
||||
|
||||
@ -137,9 +137,12 @@ export function getContextDisplayScript(): string {
|
||||
}
|
||||
|
||||
// 添加上下文项
|
||||
function addContextItem(type, path) {
|
||||
function addContextItem(type, path, displayPath) {
|
||||
const exists = contextItems.some(item => item.type === type && item.path === path);
|
||||
if (exists) return;
|
||||
|
||||
const id = Date.now() + Math.random();
|
||||
contextItems.push({ id, type, path });
|
||||
contextItems.push({ id, type, path, displayPath: displayPath || '' });
|
||||
renderContextItems();
|
||||
}
|
||||
|
||||
@ -174,7 +177,7 @@ export function getContextDisplayScript(): string {
|
||||
return \`
|
||||
<div class="context-item" title="\${item.path}">
|
||||
\${icon}
|
||||
<span class="context-item-name">\${getFileName(item.path)}</span>
|
||||
<span class="context-item-name">\${item.displayPath || getFileName(item.path)}</span>
|
||||
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
||||
\${getRemoveIcon()}
|
||||
</span>
|
||||
|
||||
@ -3,7 +3,21 @@ import {
|
||||
getUserInfoComponentStyles,
|
||||
getUserInfoComponentScript,
|
||||
} from "./userInfoComponent";
|
||||
import { userAvatarIconSvg } from "../constants/toolIcons";
|
||||
import {
|
||||
getMoreOptionsComponentContent,
|
||||
getMoreOptionsComponentStyles,
|
||||
getMoreOptionsComponentScript,
|
||||
} from "./moreOptionsComponent";
|
||||
import {
|
||||
getSettingsComponentContent,
|
||||
getSettingsComponentStyles,
|
||||
getSettingsComponentScript,
|
||||
} from "./settingsComponent";
|
||||
import {
|
||||
userAvatarIconSvg,
|
||||
moreIconSvg,
|
||||
setting,
|
||||
} from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取会话历史栏的 HTML 内容
|
||||
@ -39,8 +53,23 @@ export function getConversationHistoryBarContent(): string {
|
||||
</button>
|
||||
${getUserInfoComponentContent()}
|
||||
</div>
|
||||
|
||||
<div class='setting'>
|
||||
<button class="setting-btn" title="设置" onclick="openSettingsModal()">
|
||||
${setting}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class='more-container'>
|
||||
<button class="more-button" title="更多选项" onclick="toggleMoreOptionsDropdown()">
|
||||
${moreIconSvg}
|
||||
</button>
|
||||
${getMoreOptionsComponentContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${getSettingsComponentContent()}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -76,8 +105,8 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.user-avatar-icon-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
@ -111,6 +140,72 @@ export function getConversationHistoryBarStyles(): string {
|
||||
|
||||
${getUserInfoComponentStyles()}
|
||||
|
||||
${getSettingsComponentStyles()}
|
||||
|
||||
.setting {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.setting-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.setting-btn:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.setting-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.more-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.more-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.more-button:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.more-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.more-button.active {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
${getMoreOptionsComponentStyles()}
|
||||
|
||||
.history-dropdown-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -219,8 +314,8 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
@ -235,7 +330,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
background: #007ACC;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@ -275,6 +370,10 @@ export function getConversationHistoryBarScript(): string {
|
||||
return `
|
||||
${getUserInfoComponentScript()}
|
||||
|
||||
${getMoreOptionsComponentScript()}
|
||||
|
||||
${getSettingsComponentScript()}
|
||||
|
||||
// 更新用户头像图标按钮显示
|
||||
function updateUserAvatarIconButton(userInfo) {
|
||||
const userAvatarIconButton = document.getElementById('userAvatarIconButton');
|
||||
@ -303,6 +402,7 @@ export function getConversationHistoryBarScript(): string {
|
||||
let totalHistory = 0;
|
||||
let hasMoreHistory = false;
|
||||
let isLoadingHistory = false;
|
||||
let currentLoadRequestId = 0; // 请求 ID,用于防止并发加载
|
||||
const HISTORY_PAGE_SIZE = 10;
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
@ -346,11 +446,15 @@ export function getConversationHistoryBarScript(): string {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成新的请求 ID,用于防止并发加载
|
||||
const requestId = ++currentLoadRequestId;
|
||||
|
||||
isLoadingHistory = true;
|
||||
vscode.postMessage({
|
||||
command: 'loadConversationHistory',
|
||||
offset: currentOffset,
|
||||
limit: HISTORY_PAGE_SIZE
|
||||
limit: HISTORY_PAGE_SIZE,
|
||||
requestId: requestId
|
||||
});
|
||||
}
|
||||
|
||||
@ -362,11 +466,19 @@ export function getConversationHistoryBarScript(): string {
|
||||
return;
|
||||
}
|
||||
|
||||
// 追加新数据
|
||||
conversationHistory = conversationHistory.concat(data.items);
|
||||
// 追加新数据(去重)
|
||||
const existingIds = new Set(conversationHistory.map(item => item.id));
|
||||
const newItems = [];
|
||||
for (const item of data.items) {
|
||||
if (!existingIds.has(item.id)) {
|
||||
existingIds.add(item.id);
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
conversationHistory = conversationHistory.concat(newItems);
|
||||
totalHistory = data.total;
|
||||
hasMoreHistory = data.hasMore;
|
||||
currentOffset += data.items.length;
|
||||
currentOffset = conversationHistory.length;
|
||||
|
||||
const historyList = document.getElementById('historyList');
|
||||
if (!historyList) {
|
||||
@ -454,9 +566,10 @@ export function getConversationHistoryBarScript(): string {
|
||||
});
|
||||
}
|
||||
|
||||
// 监听下拉菜单滚动事件
|
||||
// 监听下拉菜单滚动事件(防止重复注册)
|
||||
const historyDropdownMenu = document.getElementById('historyDropdownMenu');
|
||||
if (historyDropdownMenu) {
|
||||
if (historyDropdownMenu && !historyDropdownMenu._scrollListenerAdded) {
|
||||
historyDropdownMenu._scrollListenerAdded = true;
|
||||
historyDropdownMenu.addEventListener('scroll', () => {
|
||||
const menu = historyDropdownMenu;
|
||||
const scrollTop = menu.scrollTop;
|
||||
|
||||
284
src/views/exampleShowcase.ts
Normal file
@ -0,0 +1,284 @@
|
||||
/**
|
||||
* 获取展示区域的 HTML 内容
|
||||
*/
|
||||
export function getExampleShowcaseContent(): string {
|
||||
return `
|
||||
<div class="example-showcase" id="exampleShowcase">
|
||||
<div class="showcase-title">示例</div>
|
||||
<div class="example-cards">
|
||||
<div class="example-card" onclick="sendExample(0)">
|
||||
<div class="example-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 9H9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="example-content">
|
||||
<div class="example-title">生成一个SPI控制器</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-card" onclick="sendExample(1)">
|
||||
<div class="example-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="example-content">
|
||||
<div class="example-title">生成一个GMII接口的以太网UDP通信模块</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="web-link">
|
||||
<a href="https://iccoder.com" target="_blank" class="web-link-button">
|
||||
<span class="link-icon">🌐</span>
|
||||
<span>IC Coder Web端</span>
|
||||
<span class="link-arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取展示区域的样式
|
||||
*/
|
||||
export function getExampleShowcaseStyles(): string {
|
||||
return `
|
||||
.example-showcase {
|
||||
margin-top: 24px;
|
||||
padding: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.example-showcase.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.showcase-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin-bottom: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.example-cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.example-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.example-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(79, 172, 254, 0.1) 0%, rgba(0, 242, 254, 0.1) 50%, rgba(168, 85, 247, 0.1) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.example-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.example-card:hover {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.example-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.example-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.example-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.example-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.example-card:hover .example-title {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.example-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-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;
|
||||
}
|
||||
|
||||
.link-arrow {
|
||||
font-size: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.web-link-button:hover .link-arrow {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取展示区域的脚本
|
||||
*/
|
||||
export function getExampleShowcaseScript(): string {
|
||||
return `
|
||||
// 示例文本数组
|
||||
const exampleTexts = [
|
||||
'生成一个SPI控制器',
|
||||
'生成一个GMII接口的以太网UDP通信模块'
|
||||
];
|
||||
|
||||
// 存储待发送的示例索引
|
||||
let pendingExampleIndex = -1;
|
||||
|
||||
// 直接发送示例消息
|
||||
function sendExample(index) {
|
||||
// 先检查邀请码验证状态
|
||||
pendingExampleIndex = index;
|
||||
vscode.postMessage({
|
||||
command: 'checkInvitationCode'
|
||||
});
|
||||
}
|
||||
|
||||
// 实际发送示例消息
|
||||
function doSendExample(index) {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const sendButton = document.getElementById('sendButton');
|
||||
|
||||
if (messageInput && exampleTexts[index]) {
|
||||
messageInput.value = exampleTexts[index];
|
||||
|
||||
// 触发自动调整高度
|
||||
if (typeof autoResizeTextarea === 'function') {
|
||||
autoResizeTextarea();
|
||||
}
|
||||
|
||||
// 直接触发发送
|
||||
if (sendButton && typeof sendButton.click === 'function') {
|
||||
sendButton.click();
|
||||
} else if (typeof sendMessage === 'function') {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听消息变化,自动隐藏/显示展示区域
|
||||
function updateShowcaseVisibility() {
|
||||
const showcase = document.getElementById('exampleShowcase');
|
||||
if (showcase) {
|
||||
if (hasMessages) {
|
||||
showcase.classList.add('hidden');
|
||||
} else {
|
||||
showcase.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展原有的布局更新函数
|
||||
const originalUpdateInputAreaLayout = updateInputAreaLayout;
|
||||
updateInputAreaLayout = function() {
|
||||
if (originalUpdateInputAreaLayout) {
|
||||
originalUpdateInputAreaLayout();
|
||||
}
|
||||
updateShowcaseVisibility();
|
||||
};
|
||||
`;
|
||||
}
|
||||
218
src/views/expiredModal.ts
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 试用期过期弹窗
|
||||
* 功能:在聊天面板内显示过期提醒模态窗口
|
||||
* 依赖:无
|
||||
* 使用场景:试用用户过期时在聊天面板内显示
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取过期弹窗的 HTML 内容
|
||||
*/
|
||||
export function getExpiredModalContent(logoUri?: string): string {
|
||||
return `
|
||||
<!-- 过期弹窗 -->
|
||||
<div id="expiredModal" class="expired-modal" style="display: none;">
|
||||
<div class="expired-modal-overlay"></div>
|
||||
<div class="expired-modal-content">
|
||||
${logoUri ? `<img src="${logoUri}" class="expired-logo-corner" alt="IC Coder" />` : ""}
|
||||
|
||||
<div class="expired-modal-header">
|
||||
<div class="expired-icon">⏰</div>
|
||||
<h2>您的试用期已到期</h2>
|
||||
<p class="expired-modal-subtitle">感谢您使用 IC Coder!您的 15 天试用期已结束。</p>
|
||||
</div>
|
||||
|
||||
<div class="expired-modal-body">
|
||||
<p class="expired-message">如需继续使用,请联系我们获取正式版本。</p>
|
||||
|
||||
<button id="expiredContactBtn" class="expired-btn expired-btn-primary">
|
||||
<span>联系我们</span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期弹窗的 CSS 样式
|
||||
*/
|
||||
export function getExpiredModalStyles(): string {
|
||||
return `
|
||||
/* 过期弹窗样式 */
|
||||
.expired-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||
}
|
||||
|
||||
.expired-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.expired-modal-content {
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.expired-logo-corner {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 24px;
|
||||
height: 40px;
|
||||
width: auto;
|
||||
opacity: 0.9;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.expired-modal-header {
|
||||
padding: 60px 32px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.expired-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.expired-modal-header h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-errorForeground);
|
||||
}
|
||||
|
||||
.expired-modal-subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.expired-modal-body {
|
||||
padding: 0 32px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.expired-message {
|
||||
font-size: 14px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin: 20px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.expired-btn {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
transition: all 0.2s;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.expired-btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.expired-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期弹窗的 JavaScript 逻辑
|
||||
*/
|
||||
export function getExpiredModalScript(): string {
|
||||
return `
|
||||
// 过期弹窗逻辑
|
||||
(function() {
|
||||
const modal = document.getElementById('expiredModal');
|
||||
const contactBtn = document.getElementById('expiredContactBtn');
|
||||
const overlay = modal?.querySelector('.expired-modal-overlay');
|
||||
|
||||
// 显示过期弹窗
|
||||
window.showExpiredModal = function() {
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏过期弹窗
|
||||
window.hideExpiredModal = function() {
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
// 点击"联系我们"按钮
|
||||
if (contactBtn) {
|
||||
contactBtn.addEventListener('click', function() {
|
||||
// 可以打开联系页面
|
||||
// window.open('https://iccoder.com/contact', '_blank');
|
||||
hideExpiredModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 点击遮罩层关闭弹窗
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function() {
|
||||
hideExpiredModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 阻止点击弹窗内容时关闭
|
||||
const content = modal?.querySelector('.expired-modal-content');
|
||||
if (content) {
|
||||
content.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// 监听来自后端的消息
|
||||
window.addEventListener('message', function(event) {
|
||||
const message = event.data;
|
||||
if (message.command === 'showExpiredModal') {
|
||||
showExpiredModal();
|
||||
}
|
||||
});
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
62
src/views/filePathTag.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 文件路径标签组件
|
||||
* 功能:显示可点击的文件路径标签
|
||||
* 使用场景:在用户消息中显示上下文文件
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取文件路径标签的样式
|
||||
*/
|
||||
export function getFilePathTagStyles(): string {
|
||||
return `
|
||||
/* 文件路径标签 */
|
||||
.file-path-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-right: 6px;
|
||||
background: rgba(0, 122, 204, 0.15);
|
||||
border: 1px solid rgba(0, 122, 204, 0.3);
|
||||
border-radius: 4px;
|
||||
color: #4fc3f7;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.file-path-tag:hover {
|
||||
background: rgba(0, 122, 204, 0.25);
|
||||
border-color: rgba(0, 122, 204, 0.5);
|
||||
}
|
||||
|
||||
.file-path-tag svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件路径标签的脚本
|
||||
*/
|
||||
export function getFilePathTagScript(): string {
|
||||
return `
|
||||
// 处理文件路径标签点击
|
||||
function handleFilePathClick(filePath) {
|
||||
vscode.postMessage({
|
||||
command: 'openFile',
|
||||
filePath: filePath
|
||||
});
|
||||
}
|
||||
|
||||
// 创建文件路径标签
|
||||
window.createFilePathTag = function(filePath) {
|
||||
const fileIcon = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
|
||||
const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
|
||||
return '<span class="file-path-tag" onclick="handleFilePathClick(\\'' + escapedPath + '\\')">' + fileIcon + filePath + '</span>';
|
||||
};
|
||||
`;
|
||||
}
|
||||
327
src/views/generalSettingsComponent.ts
Normal file
@ -0,0 +1,327 @@
|
||||
/**
|
||||
* 获取通用设置组件的 HTML 内容
|
||||
*/
|
||||
export function getGeneralSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="general-settings">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="saveGeneralSettings()">
|
||||
保存设置
|
||||
</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="resetGeneralSettings()">
|
||||
重置为默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用设置组件的 CSS 样式
|
||||
*/
|
||||
export function getGeneralSettingsComponentStyles(): string {
|
||||
return `
|
||||
.general-settings {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.settings-subsection-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.settings-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-item-header {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.settings-item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.settings-item-description {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.settings-select {
|
||||
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;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-select:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.settings-input {
|
||||
width: 80px;
|
||||
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:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.settings-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.settings-switch-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 24px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-switch-slider:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: var(--vscode-foreground);
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-switch input:checked + .settings-switch-slider {
|
||||
background: var(--vscode-button-background);
|
||||
border-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.settings-switch input:checked + .settings-switch-slider:before {
|
||||
transform: translateX(20px);
|
||||
background: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-button-primary {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.settings-button-primary:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.settings-button-secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.settings-button-secondary:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用设置组件的 JavaScript 脚本
|
||||
*/
|
||||
export function getGeneralSettingsComponentScript(): string {
|
||||
return `
|
||||
// 保存通用设置
|
||||
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,
|
||||
};
|
||||
|
||||
// 发送消息到扩展
|
||||
vscode.postMessage({
|
||||
command: 'saveGeneralSettings',
|
||||
settings: settings
|
||||
});
|
||||
|
||||
// 显示保存成功提示
|
||||
console.log('通用设置已保存', settings);
|
||||
}
|
||||
|
||||
// 重置通用设置
|
||||
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;
|
||||
|
||||
console.log('通用设置已重置为默认值');
|
||||
}
|
||||
|
||||
// 加载通用设置
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -24,26 +24,42 @@ import {
|
||||
getContextCompressStyles,
|
||||
getContextCompressScript,
|
||||
} from "./contextCompress";
|
||||
import {
|
||||
getFilePathTagStyles,
|
||||
getFilePathTagScript,
|
||||
} from "./filePathTag";
|
||||
import {
|
||||
getOptimizeButtonContent,
|
||||
getOptimizeButtonStyles,
|
||||
getOptimizeButtonScript,
|
||||
} from "./optimizeButton";
|
||||
import {
|
||||
getExampleShowcaseContent,
|
||||
getExampleShowcaseStyles,
|
||||
getExampleShowcaseScript,
|
||||
} from "./exampleShowcase";
|
||||
import {
|
||||
getChangePanelContent,
|
||||
getChangePanelStyles,
|
||||
getChangePanelScript,
|
||||
} from "./changePanel";
|
||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取输入区域的 HTML 内容
|
||||
*/
|
||||
export function getInputAreaContent(
|
||||
autoIcon: string = '',
|
||||
liteIcon: string = '',
|
||||
syIcon: string = '',
|
||||
maxIcon: string = ''
|
||||
autoIcon: string = "",
|
||||
liteIcon: string = "",
|
||||
syIcon: string = "",
|
||||
maxIcon: string = ""
|
||||
): string {
|
||||
return `
|
||||
<div class="input-area centered" id="inputArea">
|
||||
<div class="input-group">
|
||||
<div class="input-wrapper">
|
||||
<!-- 代码变更面板 -->
|
||||
${getChangePanelContent()}
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="input-top-toolbar">
|
||||
${getContextButtonContent()}
|
||||
@ -71,6 +87,8 @@ export function getInputAreaContent(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 展示区域:案例和 Web 端链接 -->
|
||||
${getExampleShowcaseContent()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -84,8 +102,11 @@ export function getInputAreaStyles(): string {
|
||||
${getModelSelectorStyles()}
|
||||
${getContextButtonStyles()}
|
||||
${getContextDisplayStyles()}
|
||||
${getFilePathTagStyles()}
|
||||
${getContextCompressStyles()}
|
||||
${getOptimizeButtonStyles()}
|
||||
${getExampleShowcaseStyles()}
|
||||
${getChangePanelStyles()}
|
||||
.input-area {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
padding-top: 15px;
|
||||
@ -95,7 +116,7 @@ export function getInputAreaStyles(): string {
|
||||
/* 居中模式:未发起对话时 */
|
||||
.input-area.centered {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% - 40px);
|
||||
@ -288,10 +309,12 @@ export function getInputAreaScript(): string {
|
||||
return `
|
||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||
${getModelSelectorScript()}
|
||||
${getContextButtonScript()}
|
||||
${getContextDisplayScript()}
|
||||
${getContextButtonScript()}
|
||||
${getContextCompressScript()}
|
||||
${getOptimizeButtonScript()}
|
||||
${getChangePanelScript()}
|
||||
${getFilePathTagScript()}
|
||||
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
@ -301,6 +324,8 @@ export function getInputAreaScript(): string {
|
||||
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
||||
let hasWorkspace = true; // 工作区状态
|
||||
|
||||
${getExampleShowcaseScript()}
|
||||
|
||||
// 切换输入框布局模式
|
||||
function updateInputAreaLayout() {
|
||||
const inputArea = document.getElementById('inputArea');
|
||||
@ -329,12 +354,16 @@ export function getInputAreaScript(): string {
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('input', autoResizeTextarea);
|
||||
|
||||
// 监听点击事件,检测工作区状态
|
||||
// 监听点击事件,检测工作区状态、试用期过期和邀请码验证状态
|
||||
messageInput.addEventListener('focus', () => {
|
||||
if (!hasCheckedWorkspace) {
|
||||
hasCheckedWorkspace = true;
|
||||
vscode.postMessage({ command: 'checkWorkspace' });
|
||||
}
|
||||
// 检查试用期是否过期
|
||||
vscode.postMessage({ command: 'checkTrialExpiration' });
|
||||
// 检查邀请码验证状态
|
||||
vscode.postMessage({ command: 'checkInvitationCode' });
|
||||
});
|
||||
|
||||
// 初始化时调整一次高度
|
||||
@ -403,7 +432,22 @@ export function getInputAreaScript(): string {
|
||||
// 获取上下文项
|
||||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||
|
||||
addMessage(text, 'user');
|
||||
// 构建显示消息:如果有上下文文件,添加文件路径前缀
|
||||
let displayText = text;
|
||||
if (contextItems.length > 0) {
|
||||
const filePaths = contextItems
|
||||
.filter(item => item.type === 'file')
|
||||
.map(item => item.displayPath || item.path)
|
||||
.join(' ');
|
||||
if (filePaths) {
|
||||
displayText = filePaths + ' ' + text;
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(displayText, 'user');
|
||||
|
||||
// 重置分段消息容器,强制下次创建新容器
|
||||
currentSegmentedMessage = null;
|
||||
|
||||
// 标记已有消息,切换布局到底部
|
||||
hasMessages = true;
|
||||
@ -424,6 +468,11 @@ export function getInputAreaScript(): string {
|
||||
autoResizeTextarea(); // 重置输入框高度
|
||||
messageInput.focus();
|
||||
|
||||
// 清空上下文项
|
||||
if (window.clearContextItems) {
|
||||
window.clearContextItems();
|
||||
}
|
||||
|
||||
// 重置优化状态
|
||||
resetOptimizeButton();
|
||||
}
|
||||
|
||||
457
src/views/invitationModal.ts
Normal file
@ -0,0 +1,457 @@
|
||||
/**
|
||||
* 邀请码验证弹窗
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取邀请码弹窗的 HTML 内容
|
||||
*/
|
||||
export function getInvitationModalContent(
|
||||
qrCodeUri?: string,
|
||||
logoUri?: string,
|
||||
): string {
|
||||
return `
|
||||
<!-- 邀请码验证弹窗 -->
|
||||
<div id="invitationModal" class="invitation-modal" style="display: none;">
|
||||
<div class="invitation-modal-overlay"></div>
|
||||
<div class="invitation-modal-content">
|
||||
${logoUri ? `<img src="${logoUri}" class="invitation-logo-corner" alt="IC Coder" />` : ""}
|
||||
<button id="invitationCloseBtn" class="invitation-close-btn" title="关闭">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M1 1L13 13M13 1L1 13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="invitation-modal-header">
|
||||
<!-- <div class="invitation-icon">🔐</div> -->
|
||||
<h2>欢迎使用 IC Coder</h2>
|
||||
<p class="invitation-modal-subtitle">目前IC Coder插件端仅供企业端付费用户使用,2026年3月起会逐步开放给所有用户使用~</p>
|
||||
</div>
|
||||
|
||||
<div class="invitation-modal-body">
|
||||
<div class="invitation-qrcode-section">
|
||||
<div class="invitation-qrcode-wrapper">
|
||||
<img src="${qrCodeUri}" alt="微信二维码" class="invitation-qrcode-image" />
|
||||
</div>
|
||||
<p class="invitation-qrcode-text">欢迎扫码添加微信,填写《企业试用申请表》获取邀请码,与我们一起加速芯片设计与验证吧!</p>
|
||||
</div>
|
||||
|
||||
<div class="invitation-divider">
|
||||
</div>
|
||||
|
||||
<div class="invitation-input-section">
|
||||
<label class="invitation-input-label">邀请码</label>
|
||||
<input
|
||||
type="text"
|
||||
id="invitationCodeInput"
|
||||
class="invitation-code-input"
|
||||
placeholder="请输入您的邀请码"
|
||||
maxlength="20"
|
||||
/>
|
||||
<div id="invitationError" class="invitation-error" style="display: none;"></div>
|
||||
<button id="invitationSubmitBtn" class="invitation-btn invitation-btn-primary">
|
||||
<span>立即验证</span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请码弹窗的 CSS 样式
|
||||
*/
|
||||
export function getInvitationModalStyles(): string {
|
||||
return `
|
||||
/* 邀请码弹窗样式 */
|
||||
.invitation-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||
}
|
||||
|
||||
.invitation-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.invitation-modal-content {
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.invitation-close-btn {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.invitation-logo-corner {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 24px;
|
||||
height: 40px;
|
||||
width: auto;
|
||||
opacity: 0.9;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.invitation-close-btn:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.invitation-close-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.invitation-modal-header {
|
||||
padding: 60px 32px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invitation-modal-header h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.invitation-modal-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.invitation-modal-body {
|
||||
padding: 0 32px 32px;
|
||||
}
|
||||
|
||||
.invitation-qrcode-section {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.invitation-qrcode-wrapper {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.invitation-qrcode-image {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.invitation-qrcode-text {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.invitation-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.invitation-divider::before,
|
||||
.invitation-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--vscode-widget-border);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.invitation-divider span {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.invitation-input-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.invitation-input-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.invitation-code-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.invitation-code-input:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
/* box-shadow: 0 0 0 2px var(--vscode-focusBorder); */
|
||||
}
|
||||
|
||||
.invitation-code-input::placeholder {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
}
|
||||
|
||||
.invitation-error {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-errorForeground);
|
||||
background: var(--vscode-inputValidation-errorBackground);
|
||||
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||
border-radius: 6px;
|
||||
animation: shakeError 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes shakeError {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-6px); }
|
||||
75% { transform: translateX(6px); }
|
||||
}
|
||||
|
||||
.invitation-btn {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.invitation-btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.invitation-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.invitation-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.invitation-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请码弹窗的 JavaScript 逻辑
|
||||
*/
|
||||
export function getInvitationModalScript(): string {
|
||||
return `
|
||||
// 邀请码弹窗逻辑
|
||||
(function() {
|
||||
const modal = document.getElementById('invitationModal');
|
||||
const input = document.getElementById('invitationCodeInput');
|
||||
const submitBtn = document.getElementById('invitationSubmitBtn');
|
||||
const closeBtn = document.getElementById('invitationCloseBtn');
|
||||
const errorDiv = document.getElementById('invitationError');
|
||||
|
||||
// 显示邀请码弹窗
|
||||
window.showInvitationModal = function() {
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 隐藏邀请码弹窗
|
||||
window.hideInvitationModal = function() {
|
||||
modal.style.display = 'none';
|
||||
input.value = '';
|
||||
errorDiv.style.display = 'none';
|
||||
errorDiv.textContent = '';
|
||||
submitBtn.disabled = false;
|
||||
};
|
||||
|
||||
// 显示错误信息
|
||||
window.showInvitationError = function(message) {
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
submitBtn.disabled = false;
|
||||
};
|
||||
|
||||
// 提交邀请码
|
||||
function submitInvitationCode() {
|
||||
const code = input.value.trim();
|
||||
|
||||
if (!code) {
|
||||
showInvitationError('邀请码不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.length < 6) {
|
||||
showInvitationError('邀请码格式不正确');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮,防止重复提交
|
||||
submitBtn.disabled = true;
|
||||
errorDiv.style.display = 'none';
|
||||
|
||||
// 发送验证请求到后端
|
||||
vscode.postMessage({
|
||||
command: 'verifyInvitationCode',
|
||||
code: code
|
||||
});
|
||||
}
|
||||
|
||||
// 点击提交按钮
|
||||
submitBtn.addEventListener('click', submitInvitationCode);
|
||||
|
||||
// 点击关闭按钮
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', function(e) {
|
||||
console.log('[InvitationModal] Close button clicked');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hideInvitationModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 回车键提交
|
||||
input.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
submitInvitationCode();
|
||||
}
|
||||
});
|
||||
|
||||
// 点击遮罩层关闭弹窗
|
||||
document.querySelector('.invitation-modal-overlay').addEventListener('click', function() {
|
||||
hideInvitationModal();
|
||||
});
|
||||
|
||||
// 阻止点击弹窗内容时关闭
|
||||
document.querySelector('.invitation-modal-content').addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// 监听来自后端的消息
|
||||
window.addEventListener('message', function(event) {
|
||||
const message = event.data;
|
||||
|
||||
// 处理邀请码验证状态
|
||||
if (message.command === 'invitationCodeStatus') {
|
||||
if (!message.verified) {
|
||||
// 未验证,显示弹窗
|
||||
showInvitationModal();
|
||||
} else {
|
||||
// 已验证,继续执行待处理的操作
|
||||
if (typeof pendingExampleIndex !== 'undefined' && pendingExampleIndex >= 0) {
|
||||
// 如果有待发送的示例,先检查工作区
|
||||
vscode.postMessage({ command: 'checkWorkspace' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理邀请码验证结果
|
||||
if (message.command === 'invitationCodeVerified') {
|
||||
if (message.success) {
|
||||
// 验证成功,隐藏弹窗
|
||||
hideInvitationModal();
|
||||
// 继续执行待处理的操作
|
||||
if (typeof pendingExampleIndex !== 'undefined' && pendingExampleIndex >= 0) {
|
||||
// 如果有待发送的示例,先检查工作区
|
||||
vscode.postMessage({ command: 'checkWorkspace' });
|
||||
}
|
||||
} else {
|
||||
// 验证失败,显示错误信息
|
||||
showInvitationError(message.message || '验证失败,请重试');
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
`;
|
||||
}
|
||||
@ -24,6 +24,8 @@ import {
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
userQuestionIconSvg,
|
||||
updateStageIconSvg,
|
||||
successIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
import {
|
||||
getWaveformPreviewContent,
|
||||
@ -246,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;
|
||||
@ -378,7 +382,7 @@ export function getMessageAreaStyles(): string {
|
||||
}
|
||||
/* 低调显示的工具调用 - 移除边距和背景 */
|
||||
.segment-tool.low-profile {
|
||||
margin: 2px 0px;
|
||||
margin: 5px 0px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
@ -418,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);
|
||||
}
|
||||
@ -541,6 +548,12 @@ export function getMessageAreaStyles(): string {
|
||||
.tool-segment-content.collapsed {
|
||||
max-height: 0;
|
||||
}
|
||||
.tool-segment-description {
|
||||
margin: 6px 0 0 0px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.4;
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
.segment-tool.low-profile .tool-segment-header {
|
||||
opacity: 0.65;
|
||||
@ -556,7 +569,7 @@ export function getMessageAreaStyles(): string {
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-result {
|
||||
opacity: 0.7;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segment-question {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
@ -577,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: #007ACC;
|
||||
color: #ffffff;
|
||||
border: 1px solid #007ACC;
|
||||
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;
|
||||
@ -670,11 +685,36 @@ export function getMessageAreaScript(): string {
|
||||
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
||||
const updateStageIconSvg = \`${updateStageIconSvg}\`;
|
||||
const successIconSvg = \`${successIconSvg}\`;
|
||||
|
||||
${getAgentCardScript()}
|
||||
|
||||
${getPlanCardScript()}
|
||||
|
||||
// 解析多 VCD 文件路径
|
||||
function parseMultiVcdPaths(toolResult) {
|
||||
if (!toolResult) return [];
|
||||
const result = String(toolResult);
|
||||
|
||||
// 匹配 "- moduleName: path" 格式
|
||||
const vcdListMatch = result.match(/VCD 文件列表:[\\s\\S]*?(?=\\n\\n|$)/);
|
||||
if (!vcdListMatch) return [];
|
||||
|
||||
const paths = [];
|
||||
const lineRegex = /- (\\w+): ([^\\n]+)/g;
|
||||
let match;
|
||||
while ((match = lineRegex.exec(vcdListMatch[0])) !== null) {
|
||||
const name = match[1];
|
||||
const pathOrError = match[2].trim();
|
||||
// 跳过失败的条目
|
||||
if (!pathOrError.startsWith('失败')) {
|
||||
paths.push({ name: name + '.vcd', path: pathOrError });
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
// 获取工具图标
|
||||
function getToolIcon(toolName) {
|
||||
const iconMap = {
|
||||
@ -701,6 +741,8 @@ export function getMessageAreaScript(): string {
|
||||
'updateNode': fileWriteIconSvg,
|
||||
'addStateTransition': stateTransitionIconSvg,
|
||||
'askUser': userQuestionIconSvg,
|
||||
'updatePhase': updateStageIconSvg,
|
||||
'iverilog': successIconSvg,
|
||||
};
|
||||
return iconMap[toolName] || '';
|
||||
}
|
||||
@ -733,6 +775,8 @@ export function getMessageAreaScript(): string {
|
||||
'spawnExplorer': '代码探索',
|
||||
'spawnDebugger': '波形调试',
|
||||
'askUser': '用户提问',
|
||||
'updatePhase': '已更新阶段',
|
||||
'iverilog': '已完成编译',
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
@ -813,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)) {
|
||||
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();
|
||||
}
|
||||
@ -951,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;
|
||||
}
|
||||
|
||||
// 保存当前所有工具的展开/折叠状态
|
||||
@ -1036,6 +1107,7 @@ export function getMessageAreaScript(): string {
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
const toolDescription = segment.toolDescription || '';
|
||||
|
||||
// 检查工具结果是否过长(超过一行显示不下)
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
@ -1053,23 +1125,35 @@ export function getMessageAreaScript(): string {
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\${toolDescription ? \`<p class="tool-segment-description">\${toolDescription}</p>\` : ''}
|
||||
\`;
|
||||
|
||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
// 优先使用显式提供的路径,否则从结果文本中解析
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||
if (match && match[1]) {
|
||||
vcdPath = match[1].trim();
|
||||
}
|
||||
}
|
||||
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
|
||||
if (vcdPath) {
|
||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
if (vcdPaths.length > 0) {
|
||||
// 多 VCD 模式:为每个文件创建预览
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
});
|
||||
} else {
|
||||
// 单 VCD 模式(兼容旧逻辑)
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||
if (match && match[1]) {
|
||||
vcdPath = match[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (vcdPath) {
|
||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1104,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 style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 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);
|
||||
@ -1177,7 +1257,7 @@ export function getMessageAreaScript(): string {
|
||||
currentSegmentedMessage.appendChild(segmentDiv);
|
||||
});
|
||||
|
||||
// 如果对话完成,添加操作按钮
|
||||
// 如果对话完成,添加操作按钮并重置容器
|
||||
if (isComplete) {
|
||||
console.log('[WebView] 对话完成,添加操作按钮');
|
||||
const actionsDiv = document.createElement('div');
|
||||
@ -1212,7 +1292,7 @@ export function getMessageAreaScript(): string {
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
currentSegmentedMessage.appendChild(actionsDiv);
|
||||
|
||||
// 重置当前分段消息容器
|
||||
// 重置当前分段消息容器(继续对话时创建新容器)
|
||||
currentSegmentedMessage = null;
|
||||
}
|
||||
|
||||
@ -1289,6 +1369,7 @@ export function getMessageAreaScript(): string {
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
const toolDescription = segment.toolDescription || '';
|
||||
|
||||
// 检查工具结果是否过长(超过一行显示不下)
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
@ -1300,23 +1381,35 @@ export function getMessageAreaScript(): string {
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\${toolDescription ? \`<p class="tool-segment-description">\${toolDescription}</p>\` : ''}
|
||||
\`;
|
||||
|
||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
// 优先使用显式提供的路径,否则从结果文本中解析
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||
if (match && match[1]) {
|
||||
vcdPath = match[1].trim();
|
||||
}
|
||||
}
|
||||
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
|
||||
if (vcdPath) {
|
||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
if (vcdPaths.length > 0) {
|
||||
// 多 VCD 模式:为每个文件创建预览
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
});
|
||||
} else {
|
||||
// 单 VCD 模式(兼容旧逻辑)
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||
if (match && match[1]) {
|
||||
vcdPath = match[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (vcdPath) {
|
||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1626,6 +1719,34 @@ export function getMessageAreaScript(): string {
|
||||
});
|
||||
}
|
||||
|
||||
// 处理多问题答案提交
|
||||
function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
|
||||
console.log('[WebView] 多问题答案提交:', askId, answers);
|
||||
|
||||
// 保存答案到 Map 中
|
||||
answeredQuestions.set(askId, answers);
|
||||
|
||||
// 标记问题已回答
|
||||
segmentDiv.classList.add('answered');
|
||||
|
||||
// 禁用所有输入
|
||||
const inputs = segmentDiv.querySelectorAll('input');
|
||||
inputs.forEach(input => input.disabled = true);
|
||||
|
||||
// 隐藏提交按钮
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
if (submitBtn) {
|
||||
submitBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// 发送答案到后端
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
answers: answers
|
||||
});
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
|
||||
${getCodeHighlightScript()}
|
||||
|
||||
394
src/views/moreOptionsComponent.ts
Normal file
@ -0,0 +1,394 @@
|
||||
/**
|
||||
* 更多选项组件
|
||||
* 包含用户手册和用户反馈入口
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取更多选项组件的 HTML 内容
|
||||
*/
|
||||
export function getMoreOptionsComponentContent(): string {
|
||||
return `
|
||||
<div class="more-options-wrapper">
|
||||
<!-- 更多选项下拉面板 -->
|
||||
<div class="more-options-dropdown" id="moreOptionsDropdown">
|
||||
<div class="more-options-content">
|
||||
<div class="more-options-header">
|
||||
<span class="more-options-title">更多选项</span>
|
||||
</div>
|
||||
|
||||
<div class="more-options-body">
|
||||
<div class="more-option-item" id="userManualOption">
|
||||
<div class="option-icon">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<div class="option-label">用户手册</div>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更多选项组件的 CSS 样式
|
||||
*/
|
||||
export function getMoreOptionsComponentStyles(): string {
|
||||
return `
|
||||
.more-options-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 更多选项下拉面板 */
|
||||
.more-options-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.more-options-dropdown.active {
|
||||
display: block;
|
||||
animation: dropdownSlideIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.more-options-content {
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.more-options-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.more-options-body {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.more-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.more-option-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.more-option-item:active {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.option-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更多选项组件的 JavaScript 脚本
|
||||
*/
|
||||
export function getMoreOptionsComponentScript(): string {
|
||||
return `
|
||||
// 切换更多选项下拉面板
|
||||
function toggleMoreOptionsDropdown() {
|
||||
const dropdown = document.getElementById('moreOptionsDropdown');
|
||||
const moreButton = document.querySelector('.more-button');
|
||||
|
||||
if (dropdown) {
|
||||
const isActive = dropdown.classList.contains('active');
|
||||
if (isActive) {
|
||||
dropdown.classList.remove('active');
|
||||
if (moreButton) {
|
||||
moreButton.classList.remove('active');
|
||||
}
|
||||
} else {
|
||||
dropdown.classList.add('active');
|
||||
if (moreButton) {
|
||||
moreButton.classList.add('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭更多选项下拉面板
|
||||
function closeMoreOptionsDropdown() {
|
||||
const dropdown = document.getElementById('moreOptionsDropdown');
|
||||
const moreButton = document.querySelector('.more-button');
|
||||
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('active');
|
||||
}
|
||||
if (moreButton) {
|
||||
moreButton.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 打开用户手册
|
||||
function openUserManual() {
|
||||
console.log('打开用户手册');
|
||||
vscode.postMessage({ command: 'openUserManual' });
|
||||
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', () => {
|
||||
// 绑定用户手册选项
|
||||
const userManualOption = document.getElementById('userManualOption');
|
||||
if (userManualOption) {
|
||||
userManualOption.addEventListener('click', openUserManual);
|
||||
}
|
||||
|
||||
// 绑定用户反馈选项
|
||||
const userFeedbackOption = document.getElementById('userFeedbackOption');
|
||||
if (userFeedbackOption) {
|
||||
userFeedbackOption.addEventListener('click', openUserFeedback);
|
||||
}
|
||||
|
||||
// 点击页面其他地方关闭下拉面板
|
||||
document.addEventListener('click', (e) => {
|
||||
const dropdown = document.getElementById('moreOptionsDropdown');
|
||||
const moreButton = document.querySelector('.more-button');
|
||||
const moreContainer = document.querySelector('.more-container');
|
||||
|
||||
if (dropdown && dropdown.classList.contains('active')) {
|
||||
// 如果点击的不是更多按钮和下拉面板内容,则关闭
|
||||
if (!moreContainer?.contains(e.target)) {
|
||||
closeMoreOptionsDropdown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止下拉面板内容点击事件冒泡
|
||||
const dropdownContent = document.querySelector('.more-options-content');
|
||||
if (dropdownContent) {
|
||||
dropdownContent.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
329
src/views/ndtWelcomeModal.ts
Normal file
@ -0,0 +1,329 @@
|
||||
/**
|
||||
* 宁德时代欢迎弹窗
|
||||
* 功能:邀请码验证成功后显示欢迎信息
|
||||
* 依赖:无
|
||||
* 使用场景:宁德时代用户首次验证邀请码成功后显示
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取宁德时代欢迎弹窗的 HTML 内容
|
||||
*/
|
||||
export function getNdtWelcomeModalContent(logoUri?: string): string {
|
||||
return `
|
||||
<!-- 宁德时代欢迎弹窗 -->
|
||||
<div id="ndtWelcomeModal" class="ndt-welcome-modal" style="display: none;">
|
||||
<div class="ndt-welcome-modal-overlay"></div>
|
||||
<div class="ndt-welcome-modal-content">
|
||||
${logoUri ? `<img src="${logoUri}" class="ndt-welcome-logo-corner" alt="IC Coder" />` : ""}
|
||||
|
||||
<div class="ndt-welcome-modal-header">
|
||||
<div class="ndt-welcome-icon">🎉</div>
|
||||
<h2>欢迎企业<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder!</h2>
|
||||
</div>
|
||||
|
||||
<div class="ndt-welcome-modal-body">
|
||||
<!-- 试用期提示 -->
|
||||
<div class="ndt-trial-banner">
|
||||
<span>您已获得 <strong>5 天企业版试用期</strong>,企业版试用期内Credits用量无限,并可无限制使用所有功能</span>
|
||||
</div>
|
||||
|
||||
<!-- IC Coder 简介 -->
|
||||
<div class="ndt-intro-section">
|
||||
<h3 class="ndt-section-title">关于 IC Coder</h3>
|
||||
<p class="ndt-intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform(自主式人工智能 Verilog 编码平台)。我们采用全球顶尖的IC Coder自研芯片设计微调模型,为代码生成提供强大的AI能力支撑。</p>
|
||||
|
||||
<div class="ndt-features">
|
||||
<div class="ndt-feature-item">
|
||||
<span class="ndt-feature-text">多智能体架构(Multi-Agent System):多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节</span>
|
||||
</div>
|
||||
<div class="ndt-feature-item">
|
||||
<span class="ndt-feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
|
||||
</div>
|
||||
<div class="ndt-feature-item">
|
||||
<span class="ndt-feature-text">AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮组 -->
|
||||
<div class="ndt-button-group">
|
||||
<button id="ndtTutorialBtn" class="ndt-welcome-btn ndt-welcome-btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M8 2C4.7 2 2 4.7 2 8s2.7 6 6 6 6-2.7 6-6-2.7-6-6-6zm0 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z" fill="currentColor"/>
|
||||
<path d="M8 5v3l2 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span>查看使用教程</span>
|
||||
</button>
|
||||
<button id="ndtWelcomeStartBtn" class="ndt-welcome-btn ndt-welcome-btn-primary">
|
||||
<span>开始使用</span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取宁德时代欢迎弹窗的 CSS 样式
|
||||
*/
|
||||
export function getNdtWelcomeModalStyles(): string {
|
||||
return `
|
||||
/* 宁德时代欢迎弹窗样式 */
|
||||
.ndt-welcome-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||
}
|
||||
|
||||
.ndt-welcome-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.ndt-welcome-modal-content {
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ndt-welcome-logo-corner {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 24px;
|
||||
height: 40px;
|
||||
width: auto;
|
||||
opacity: 0.9;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.ndt-welcome-modal-header {
|
||||
padding: 60px 32px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ndt-welcome-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ndt-welcome-modal-header h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ndt-welcome-modal-subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ndt-welcome-modal-body {
|
||||
padding: 0 32px 32px;
|
||||
}
|
||||
|
||||
/* 试用期横幅 */
|
||||
.ndt-trial-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 16px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border-left: 3px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.ndt-trial-banner strong {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* IC Coder 简介区域 */
|
||||
.ndt-intro-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ndt-section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.ndt-intro-text {
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.ndt-features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ndt-feature-item {
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
border-left: 2px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.ndt-feature-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.ndt-button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ndt-welcome-btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.ndt-welcome-btn-primary {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.ndt-welcome-btn-primary:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.ndt-welcome-btn-secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
}
|
||||
|
||||
.ndt-welcome-btn-secondary:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.ndt-welcome-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取宁德时代欢迎弹窗的 JavaScript 逻辑
|
||||
*/
|
||||
export function getNdtWelcomeModalScript(): string {
|
||||
return `
|
||||
// 宁德时代欢迎弹窗逻辑
|
||||
(function() {
|
||||
const modal = document.getElementById('ndtWelcomeModal');
|
||||
const startBtn = document.getElementById('ndtWelcomeStartBtn');
|
||||
const tutorialBtn = document.getElementById('ndtTutorialBtn');
|
||||
const overlay = modal?.querySelector('.ndt-welcome-modal-overlay');
|
||||
|
||||
// 显示宁德时代欢迎弹窗
|
||||
window.showNdtWelcomeModal = function() {
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏宁德时代欢迎弹窗
|
||||
window.hideNdtWelcomeModal = function() {
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
// 点击"查看使用教程"按钮
|
||||
if (tutorialBtn) {
|
||||
tutorialBtn.addEventListener('click', function() {
|
||||
// 打开使用教程链接
|
||||
vscode.postMessage({
|
||||
command: 'openTutorial'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 点击"开始使用"按钮
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', function() {
|
||||
hideNdtWelcomeModal();
|
||||
// 通知后端用户已查看欢迎弹窗
|
||||
vscode.postMessage({ command: 'ndtWelcomeModalViewed' });
|
||||
});
|
||||
}
|
||||
|
||||
// 点击遮罩层关闭弹窗
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function() {
|
||||
hideNdtWelcomeModal();
|
||||
vscode.postMessage({ command: 'ndtWelcomeModalViewed' });
|
||||
});
|
||||
}
|
||||
|
||||
// 阻止点击弹窗内容时关闭
|
||||
const content = modal?.querySelector('.ndt-welcome-modal-content');
|
||||
if (content) {
|
||||
content.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// 监听来自后端的消息
|
||||
window.addEventListener('message', function(event) {
|
||||
const message = event.data;
|
||||
if (message.command === 'showNdtWelcomeModal') {
|
||||
showNdtWelcomeModal();
|
||||
}
|
||||
});
|
||||
})();
|
||||
`;
|
||||
}
|
||||
@ -60,35 +60,97 @@ export function getOptimizeButtonScript(): string {
|
||||
return `
|
||||
let isOptimized = false; // 标记是否已优化
|
||||
let originalText = ''; // 保存原始文本用于撤回
|
||||
let isOptimizing = false; // 标记是否正在优化中
|
||||
|
||||
function handleOptimize() {
|
||||
console.log('[Optimize] handleOptimize 被调用');
|
||||
console.log('[Optimize] isOptimizing:', isOptimizing);
|
||||
console.log('[Optimize] isOptimized:', isOptimized);
|
||||
console.log('[Optimize] messageInput:', messageInput);
|
||||
|
||||
if (isOptimizing) {
|
||||
console.log('[Optimize] 正在优化中,忽略点击');
|
||||
return; // 正在优化中,忽略点击
|
||||
}
|
||||
|
||||
if (isOptimized) {
|
||||
// 撤回操作
|
||||
console.log('[Optimize] 执行撤回操作');
|
||||
messageInput.value = originalText;
|
||||
resetOptimizeButton();
|
||||
} else {
|
||||
// 优化操作
|
||||
const currentText = messageInput.value.trim();
|
||||
console.log('[Optimize] 当前输入内容:', currentText);
|
||||
console.log('[Optimize] 内容长度:', currentText.length);
|
||||
|
||||
if (!currentText) {
|
||||
console.log('[Optimize] 输入框为空,不执行优化');
|
||||
return; // 输入框为空,不执行优化
|
||||
}
|
||||
|
||||
originalText = messageInput.value; // 保存原始文本
|
||||
isOptimizing = true;
|
||||
console.log('[Optimize] 开始优化,显示加载状态');
|
||||
|
||||
// 使用死数据替换输入框内容
|
||||
const optimizedTexts = [
|
||||
'请帮我优化这段代码,提高性能和可读性',
|
||||
'请分析这个问题并给出最佳解决方案',
|
||||
'请帮我重构这段代码,使其更加简洁高效',
|
||||
'请检查代码中的潜在问题并提供改进建议'
|
||||
];
|
||||
const randomText = optimizedTexts[Math.floor(Math.random() * optimizedTexts.length)];
|
||||
messageInput.value = randomText;
|
||||
// 显示加载状态
|
||||
showOptimizeLoading();
|
||||
|
||||
// 切换到撤回状态
|
||||
isOptimized = true;
|
||||
updateOptimizeButton();
|
||||
// 发送优化请求到扩展
|
||||
console.log('[Optimize] 发送 optimizePrompt 消息');
|
||||
vscode.postMessage({
|
||||
command: 'optimizePrompt',
|
||||
prompt: currentText
|
||||
});
|
||||
console.log('[Optimize] postMessage 已发送');
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
autoResizeTextarea();
|
||||
}
|
||||
|
||||
// 处理优化结果
|
||||
function handleOptimizeResult(success, optimizedPrompt, error) {
|
||||
isOptimizing = false;
|
||||
hideOptimizeLoading();
|
||||
|
||||
if (success && optimizedPrompt) {
|
||||
messageInput.value = optimizedPrompt;
|
||||
isOptimized = true;
|
||||
updateOptimizeButton();
|
||||
} else {
|
||||
// 优化失败,恢复原始文本
|
||||
messageInput.value = originalText;
|
||||
console.error('优化失败:', error);
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
autoResizeTextarea();
|
||||
}
|
||||
|
||||
function showOptimizeLoading() {
|
||||
const optimizeButton = document.getElementById('optimizeButton');
|
||||
const optimizeIcon = document.getElementById('optimizeIcon');
|
||||
if (optimizeButton && optimizeIcon) {
|
||||
optimizeButton.disabled = true;
|
||||
optimizeButton.style.opacity = '0.5';
|
||||
// 显示加载动画
|
||||
optimizeIcon.innerHTML = '<circle cx="512" cy="512" r="400" fill="none" stroke="#409eff" stroke-width="60" stroke-dasharray="1200" stroke-dashoffset="0"><animateTransform attributeName="transform" type="rotate" from="0 512 512" to="360 512 512" dur="1s" repeatCount="indefinite"/></circle>';
|
||||
}
|
||||
}
|
||||
|
||||
function hideOptimizeLoading() {
|
||||
const optimizeButton = document.getElementById('optimizeButton');
|
||||
if (optimizeButton) {
|
||||
optimizeButton.disabled = false;
|
||||
optimizeButton.style.opacity = '1';
|
||||
}
|
||||
// 恢复图标会在 updateOptimizeButton 或 resetOptimizeButton 中处理
|
||||
if (!isOptimized) {
|
||||
resetOptimizeButton();
|
||||
}
|
||||
}
|
||||
|
||||
function updateOptimizeButton() {
|
||||
const optimizeIcon = document.getElementById('optimizeIcon');
|
||||
const optimizeTooltip = document.getElementById('optimizeTooltip');
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
* 功能说明:
|
||||
* - 显示执行计划的卡片界面
|
||||
* - 包含计划标题、摘要和步骤列表
|
||||
* - 摘要支持 Markdown 格式渲染
|
||||
* - 提供确认执行、修改计划、取消等操作按钮
|
||||
*/
|
||||
|
||||
@ -43,11 +44,62 @@ export function getPlanCardStyles(): string {
|
||||
padding: 16px;
|
||||
}
|
||||
.plan-summary {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
color: var(--vscode-foreground);
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* Markdown 渲染样式 */
|
||||
.plan-summary h1, .plan-summary h2, .plan-summary h3, .plan-summary h4 {
|
||||
margin: 16px 0 8px 0;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.plan-summary h1 { font-size: 18px; border-bottom: 1px solid var(--vscode-input-border); padding-bottom: 6px; }
|
||||
.plan-summary h2 { font-size: 16px; }
|
||||
.plan-summary h3 { font-size: 14px; }
|
||||
.plan-summary h4 { font-size: 13px; }
|
||||
.plan-summary p { margin: 8px 0; }
|
||||
.plan-summary ul, .plan-summary ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.plan-summary li { margin: 4px 0 4px 27px; }
|
||||
.plan-summary code {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 12px;
|
||||
}
|
||||
.plan-summary pre {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.plan-summary pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.plan-summary table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.plan-summary th, .plan-summary td {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.plan-summary th {
|
||||
background: var(--vscode-sideBar-background);
|
||||
font-weight: 600;
|
||||
}
|
||||
.plan-summary strong { font-weight: 600; }
|
||||
.plan-summary em { font-style: italic; }
|
||||
.plan-steps {
|
||||
font-size: 13px;
|
||||
}
|
||||
@ -58,6 +110,15 @@ export function getPlanCardStyles(): string {
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.plan-step strong {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
.step-details {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.plan-step:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -89,66 +150,239 @@ export function getPlanCardStyles(): string {
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-sideBar-background);
|
||||
}
|
||||
.plan-actions .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.plan-btn {
|
||||
padding: 8px 18px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.plan-btn-confirm {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
.plan-btn-confirm:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.plan-btn-modify {
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.plan-actions .custom-input-container {
|
||||
.plan-input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.plan-actions .custom-input {
|
||||
.plan-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.plan-actions .custom-submit {
|
||||
padding: 8px 18px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
.plan-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
.plan-btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.plan-btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.plan-actions .custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
.plan-btn-submit {
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-submit:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.plan-btn-confirm {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
}
|
||||
.plan-btn-confirm:hover {
|
||||
background: #005a9e;
|
||||
}
|
||||
.plan-btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-cancel:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.plan-answered {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-sideBar-background);
|
||||
font-size: 13px;
|
||||
}
|
||||
.answered-label {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.answered-value {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 阶段进度条样式 */
|
||||
.phase-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-bottom: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.phase-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.phase-item.current {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
.phase-item.completed {
|
||||
color: #4caf50;
|
||||
}
|
||||
.phase-item.skipped {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.phase-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-input-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.phase-dot.current {
|
||||
background: var(--vscode-textLink-foreground);
|
||||
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.2);
|
||||
}
|
||||
.phase-dot.completed {
|
||||
background: #4caf50;
|
||||
}
|
||||
.phase-dot.skipped {
|
||||
background: var(--vscode-descriptionForeground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.phase-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: var(--vscode-input-border);
|
||||
margin: 0 8px;
|
||||
}
|
||||
.phase-line.completed {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
/* 阶段列表样式 */
|
||||
.plan-phases {
|
||||
font-size: 13px;
|
||||
}
|
||||
.plan-phase {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.plan-phase:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.phase-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.phase-header:hover {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
.phase-toggle {
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.phase-toggle.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.phase-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
.phase-status {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
}
|
||||
.phase-status.current {
|
||||
background: var(--vscode-textLink-foreground);
|
||||
color: white;
|
||||
}
|
||||
.phase-status.skipped {
|
||||
background: var(--vscode-descriptionForeground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.phase-status.completed {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
.phase-content {
|
||||
padding: 0 12px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
}
|
||||
.phase-content.expanded {
|
||||
padding: 12px;
|
||||
max-height: 500px;
|
||||
}
|
||||
.phase-reason {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.phase-steps {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.phase-step-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.phase-step-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.phase-step-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid var(--vscode-textLink-foreground);
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.phase-step-text {
|
||||
flex: 1;
|
||||
}
|
||||
.phase-step-name {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.phase-step-desc {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-top: 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -158,6 +392,200 @@ export function getPlanCardStyles(): string {
|
||||
*/
|
||||
export function getPlanCardScript(): string {
|
||||
return `
|
||||
// 简单的 Markdown 渲染函数
|
||||
function renderPlanMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
let html = text;
|
||||
|
||||
// 转义 HTML 特殊字符(保留换行)
|
||||
html = html.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 标题(必须在转义之后、其他处理之前)
|
||||
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
||||
|
||||
// 代码块 (\`\`\`code\`\`\`)
|
||||
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
|
||||
|
||||
// 行内代码 (\`code\`)
|
||||
html = html.replace(/\\x60([^\\x60]+)\\x60/g, '<code>$1</code>');
|
||||
|
||||
// 表格处理
|
||||
html = html.replace(/^\\|(.+)\\|\\s*\\n\\|[-:\\s|]+\\|\\s*\\n((?:\\|.+\\|\\s*\\n?)+)/gm, function(match, header, body) {
|
||||
const headers = header.split('|').map(h => h.trim()).filter(h => h);
|
||||
const rows = body.trim().split('\\n').map(row =>
|
||||
row.split('|').map(cell => cell.trim()).filter(cell => cell)
|
||||
);
|
||||
|
||||
let table = '<table><thead><tr>';
|
||||
headers.forEach(h => table += '<th>' + h + '</th>');
|
||||
table += '</tr></thead><tbody>';
|
||||
rows.forEach(row => {
|
||||
table += '<tr>';
|
||||
row.forEach(cell => table += '<td>' + cell + '</td>');
|
||||
table += '</tr>';
|
||||
});
|
||||
table += '</tbody></table>';
|
||||
return table;
|
||||
});
|
||||
|
||||
// 粗体和斜体
|
||||
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
||||
|
||||
// 无序列表
|
||||
html = html.replace(/^[\\s]*[-*] (.+)$/gm, '<li>$1</li>');
|
||||
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
|
||||
|
||||
// 有序列表
|
||||
html = html.replace(/^[\\s]*\\d+\\. (.+)$/gm, '<li>$1</li>');
|
||||
|
||||
// 段落(连续的非空行)
|
||||
html = html.replace(/^(?!<[hupolt]|$)(.+)$/gm, '<p>$1</p>');
|
||||
|
||||
// 清理多余的空行
|
||||
html = html.replace(/<p><\\/p>/g, '');
|
||||
html = html.replace(/\\n{2,}/g, '\\n');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 解析并渲染步骤列表
|
||||
function renderPlanSteps(steps) {
|
||||
if (!steps || steps.length === 0) return '';
|
||||
|
||||
// 尝试解析 JSON 格式的步骤
|
||||
let parsedSteps = steps;
|
||||
|
||||
// 如果是单个字符串且看起来像 JSON 数组,尝试解析
|
||||
if (steps.length === 1 && typeof steps[0] === 'string') {
|
||||
const str = steps[0].trim();
|
||||
if (str.startsWith('[') && str.endsWith(']')) {
|
||||
try {
|
||||
parsedSteps = JSON.parse(str);
|
||||
} catch (e) {
|
||||
// 解析失败,保持原样
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsedSteps.map((step, i) => {
|
||||
// 如果是对象,格式化显示
|
||||
if (typeof step === 'object' && step !== null) {
|
||||
const name = step.name || step.id || ('步骤 ' + (i + 1));
|
||||
const desc = step.description || '';
|
||||
const inputs = step.inputs || '';
|
||||
const outputs = step.outputs || '';
|
||||
const logic = step.logic || '';
|
||||
|
||||
let content = '<strong>' + name + '</strong>';
|
||||
if (desc) content += ':' + desc;
|
||||
|
||||
let details = [];
|
||||
if (inputs) details.push('输入: ' + inputs);
|
||||
if (outputs) details.push('输出: ' + outputs);
|
||||
if (logic) details.push('逻辑: ' + logic);
|
||||
|
||||
if (details.length > 0) {
|
||||
content += '<div class="step-details">' + details.join(' | ') + '</div>';
|
||||
}
|
||||
|
||||
return '<div class="plan-step"><span class="step-checkbox"></span>' + content + '</div>';
|
||||
}
|
||||
|
||||
// 普通字符串
|
||||
return '<div class="plan-step"><span class="step-checkbox"></span> ' + step + '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 渲染阶段进度条
|
||||
function renderPhaseProgress(phases) {
|
||||
if (!phases || phases.length === 0) return '';
|
||||
|
||||
const phaseNames = { spec: 'Spec', design: 'Design', sim: 'Sim', done: 'Done' };
|
||||
let html = '<div class="phase-progress">';
|
||||
|
||||
phases.forEach((phase, i) => {
|
||||
const name = phaseNames[phase.id] || phase.name || phase.id;
|
||||
const status = phase.status || 'pending';
|
||||
|
||||
html += \`<div class="phase-item \${status}">
|
||||
<span class="phase-dot \${status}"></span>
|
||||
<span>\${name}</span>
|
||||
</div>\`;
|
||||
|
||||
// 添加连接线(最后一个不加)
|
||||
if (i < phases.length - 1) {
|
||||
const lineStatus = (status === 'completed' || status === 'skipped') ? 'completed' : '';
|
||||
html += \`<div class="phase-line \${lineStatus}"></div>\`;
|
||||
}
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// 渲染阶段列表(两级结构)
|
||||
function renderPlanPhases(phases) {
|
||||
if (!phases || phases.length === 0) return '';
|
||||
|
||||
const statusLabels = {
|
||||
skipped: '跳过',
|
||||
completed: '已完成',
|
||||
current: '当前',
|
||||
pending: '待执行'
|
||||
};
|
||||
|
||||
return phases.map((phase, i) => {
|
||||
const status = phase.status || 'pending';
|
||||
const statusLabel = statusLabels[status] || status;
|
||||
const isExpanded = status === 'current';
|
||||
const hasSteps = phase.steps && phase.steps.length > 0;
|
||||
const hasReason = phase.reason && status === 'skipped';
|
||||
|
||||
let stepsHtml = '';
|
||||
if (phase.steps && phase.steps.length > 0) {
|
||||
stepsHtml = phase.steps.map(step => \`
|
||||
<li class="phase-step-item">
|
||||
<span class="phase-step-checkbox"></span>
|
||||
<div class="phase-step-text">
|
||||
<div class="phase-step-name">\${step.name || ''}</div>
|
||||
\${step.description ? \`<div class="phase-step-desc">\${step.description}</div>\` : ''}
|
||||
</div>
|
||||
</li>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
return \`
|
||||
<div class="plan-phase" data-phase-id="\${phase.id}">
|
||||
<div class="phase-header" onclick="togglePhase(this)">
|
||||
<span class="phase-toggle \${isExpanded ? 'expanded' : ''}">▶</span>
|
||||
<span class="phase-name">\${phase.name || phase.id}</span>
|
||||
<span class="phase-status \${status}">\${statusLabel}</span>
|
||||
</div>
|
||||
<div class="phase-content \${isExpanded ? 'expanded' : ''}">
|
||||
\${hasReason ? \`<div class="phase-reason">\${phase.reason}</div>\` : ''}
|
||||
\${hasSteps ? \`<ul class="phase-steps">\${stepsHtml}</ul>\` : ''}
|
||||
\${!hasSteps && !hasReason ? '<div class="phase-reason">暂无步骤</div>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 切换阶段展开/折叠
|
||||
function togglePhase(header) {
|
||||
const toggle = header.querySelector('.phase-toggle');
|
||||
const content = header.nextElementSibling;
|
||||
toggle.classList.toggle('expanded');
|
||||
content.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
// 渲染计划卡片(在 updateSegmentsRealtime 中使用)
|
||||
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
@ -170,16 +598,26 @@ export function getPlanCardScript(): string {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||
).join('');
|
||||
// 判断是否有 phases(新格式)还是 steps(旧格式)
|
||||
const hasPhases = segment.planPhases && segment.planPhases.length > 0;
|
||||
|
||||
// 选项按钮
|
||||
const options = ['确认执行', '修改计划', '取消'];
|
||||
const optionsHtml = options.map(opt => {
|
||||
const isSelected = isAnswered && opt === selectedAnswer;
|
||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||
}).join('');
|
||||
// 渲染阶段进度条和阶段列表(新格式)
|
||||
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
|
||||
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
|
||||
|
||||
// 兼容旧格式:渲染步骤列表
|
||||
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
|
||||
|
||||
// 渲染 Markdown 格式的摘要
|
||||
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
||||
|
||||
// 已回答时显示用户的选择
|
||||
const answeredHtml = isAnswered ? \`
|
||||
<div class="plan-answered">
|
||||
<span class="answered-label">已回复:</span>
|
||||
<span class="answered-value">\${selectedAnswer}</span>
|
||||
</div>
|
||||
\` : '';
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
@ -187,62 +625,77 @@ export function getPlanCardScript(): string {
|
||||
<span class="plan-icon">${plannerIconSvg}</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
\${progressHtml}
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||
<div class="plan-steps">\${stepsHtml}</div>
|
||||
<div class="plan-summary">\${summaryHtml}</div>
|
||||
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<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="输入修改建议..." />
|
||||
<button class="custom-submit">提交</button>
|
||||
<div class="plan-actions" data-ask-id="\${segment.askId}" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<div class="plan-input-row">
|
||||
<input type="text" class="plan-input" placeholder="输入修改建议..." />
|
||||
<button class="plan-btn plan-btn-submit">提交修改</button>
|
||||
</div>
|
||||
<div class="plan-btn-row">
|
||||
<button class="plan-btn plan-btn-confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
\${answeredHtml}
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 只在未回答时添加事件监听
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||||
optionButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const option = this.getAttribute('data-option');
|
||||
// 发送答案到后端
|
||||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||||
// 同时发送 planAction 用于模式切换
|
||||
const actionMap = {
|
||||
'确认执行': 'confirm',
|
||||
'修改计划': 'modify',
|
||||
'取消': 'cancel'
|
||||
};
|
||||
vscode.postMessage({
|
||||
command: 'planAction',
|
||||
action: actionMap[option] || option,
|
||||
planTitle: segment.planTitle
|
||||
});
|
||||
});
|
||||
});
|
||||
const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
|
||||
const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
|
||||
const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
|
||||
const planInput = segmentDiv.querySelector('.plan-input');
|
||||
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
const customInput = segmentDiv.querySelector('.custom-input');
|
||||
if (submitBtn && customInput) {
|
||||
// 提交修改按钮
|
||||
if (submitBtn && planInput) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
|
||||
}
|
||||
});
|
||||
|
||||
customInput.addEventListener('keypress', function(e) {
|
||||
// 回车键提交修改
|
||||
planInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认执行按钮
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
handleQuestionAnswerInSegment(segment.askId, '确认执行', segmentDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// 取消按钮 - 直接中止对话,不发送给智能体
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
// 标记问题已回答
|
||||
answeredQuestions.set(segment.askId, '取消');
|
||||
segmentDiv.classList.add('answered');
|
||||
|
||||
// 隐藏操作按钮
|
||||
const actionsDiv = segmentDiv.querySelector('.plan-actions');
|
||||
if (actionsDiv) {
|
||||
actionsDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// 发送中止对话命令
|
||||
vscode.postMessage({ command: 'abortDialog' });
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@ -250,43 +703,90 @@ export function getPlanCardScript(): string {
|
||||
// 渲染计划卡片(在 renderSegments 中使用)
|
||||
function renderPlanCardStatic(segment, segmentDiv) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||
).join('');
|
||||
|
||||
// 判断是否有 phases(新格式)还是 steps(旧格式)
|
||||
const hasPhases = segment.planPhases && segment.planPhases.length > 0;
|
||||
|
||||
// 渲染阶段进度条和阶段列表(新格式)
|
||||
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
|
||||
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
|
||||
|
||||
// 兼容旧格式:渲染步骤列表
|
||||
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
|
||||
|
||||
// 渲染 Markdown 格式的摘要
|
||||
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
<div class="plan-header">
|
||||
<span class="plan-icon">📋</span>
|
||||
<!-- <span class="plan-icon">📋</span> -->
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
\${progressHtml}
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||
<div class="plan-steps">\${stepsHtml}</div>
|
||||
<div class="plan-summary">\${summaryHtml}</div>
|
||||
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
|
||||
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
|
||||
<div class="plan-actions" data-ask-id="\${segment.askId}">
|
||||
<div class="plan-input-row">
|
||||
<input type="text" class="plan-input" placeholder="输入修改建议..." />
|
||||
<button class="plan-btn plan-btn-submit">提交修改</button>
|
||||
</div>
|
||||
<div class="plan-btn-row">
|
||||
<button class="plan-btn plan-btn-confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 绑定按钮事件
|
||||
// 绑定按钮事件(静态渲染时也需要能响应)
|
||||
setTimeout(() => {
|
||||
const planCard = segmentDiv.querySelector('.plan-card');
|
||||
if (planCard) {
|
||||
planCard.querySelectorAll('.plan-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const action = e.currentTarget?.dataset?.action;
|
||||
const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
|
||||
const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
|
||||
const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
|
||||
const planInput = segmentDiv.querySelector('.plan-input');
|
||||
|
||||
// 提交修改按钮
|
||||
if (submitBtn && planInput) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const inputValue = planInput.value.trim();
|
||||
if (inputValue) {
|
||||
vscode.postMessage({
|
||||
command: 'planAction',
|
||||
action: action,
|
||||
planTitle: segment.planTitle
|
||||
command: 'submitAnswer',
|
||||
askId: segment.askId,
|
||||
selected: [inputValue],
|
||||
customInput: inputValue
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认执行按钮
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: segment.askId,
|
||||
selected: ['确认执行'],
|
||||
customInput: '确认执行'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 取消按钮 - 直接中止对话
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
// 隐藏操作按钮
|
||||
const actionsDiv = segmentDiv.querySelector('.plan-actions');
|
||||
if (actionsDiv) {
|
||||
actionsDiv.style.display = 'none';
|
||||
}
|
||||
// 发送中止对话命令
|
||||
vscode.postMessage({ command: 'abortDialog' });
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
`;
|
||||
|
||||
@ -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)';
|
||||
}
|
||||
|
||||
177
src/views/rulesSettingsComponent.ts
Normal file
@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 获取规则设置组件的 HTML 内容
|
||||
*/
|
||||
export function getRulesSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="rules-settings">
|
||||
<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">使用自定义规则来控制 AI 行为</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="enableCustomRulesCheckbox" checked>
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">系统规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="systemRulesTextarea"
|
||||
placeholder="在此输入系统规则,例如: - 始终使用中文回复 - 代码注释要详细 - 遵循项目编码规范"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
系统规则会在每次对话开始时应用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">代码生成规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="codeRulesTextarea"
|
||||
placeholder="在此输入代码生成规则,例如: - 使用 TypeScript 严格模式 - 函数命名使用驼峰命名法 - 添加必要的错误处理"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
这些规则会在生成代码时应用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">Verilog 规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="verilogRulesTextarea"
|
||||
placeholder="在此输入 Verilog 代码规则,例如: - 使用非阻塞赋值 (<=) 在时序逻辑中 - 模块命名使用小写加下划线 - 添加详细的端口注释"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
这些规则会在生成 Verilog 代码时应用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="saveRulesSettings()">
|
||||
保存规则
|
||||
</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="resetRulesSettings()">
|
||||
重置为默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则设置组件的 CSS 样式
|
||||
*/
|
||||
export function getRulesSettingsComponentStyles(): string {
|
||||
return `
|
||||
.rules-settings {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.rules-textarea-container {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.rules-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rules-textarea:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.rules-textarea::placeholder {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.rules-textarea-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则设置组件的 JavaScript 脚本
|
||||
*/
|
||||
export function getRulesSettingsComponentScript(): string {
|
||||
return `
|
||||
// 保存规则设置
|
||||
function saveRulesSettings() {
|
||||
const settings = {
|
||||
enableCustomRules: document.getElementById('enableCustomRulesCheckbox').checked,
|
||||
systemRules: document.getElementById('systemRulesTextarea').value,
|
||||
codeRules: document.getElementById('codeRulesTextarea').value,
|
||||
verilogRules: document.getElementById('verilogRulesTextarea').value,
|
||||
};
|
||||
|
||||
// 发送消息到扩展
|
||||
vscode.postMessage({
|
||||
command: 'saveRulesSettings',
|
||||
settings: settings
|
||||
});
|
||||
|
||||
// 显示保存成功提示
|
||||
console.log('规则设置已保存', settings);
|
||||
}
|
||||
|
||||
// 重置规则设置
|
||||
function resetRulesSettings() {
|
||||
document.getElementById('enableCustomRulesCheckbox').checked = true;
|
||||
document.getElementById('systemRulesTextarea').value = '';
|
||||
document.getElementById('codeRulesTextarea').value = '';
|
||||
document.getElementById('verilogRulesTextarea').value = '';
|
||||
|
||||
console.log('规则设置已重置为默认值');
|
||||
}
|
||||
|
||||
// 加载规则设置
|
||||
function loadRulesSettings(settings) {
|
||||
if (!settings) return;
|
||||
|
||||
if (settings.enableCustomRules !== undefined) {
|
||||
document.getElementById('enableCustomRulesCheckbox').checked = settings.enableCustomRules;
|
||||
}
|
||||
if (settings.systemRules) {
|
||||
document.getElementById('systemRulesTextarea').value = settings.systemRules;
|
||||
}
|
||||
if (settings.codeRules) {
|
||||
document.getElementById('codeRulesTextarea').value = settings.codeRules;
|
||||
}
|
||||
if (settings.verilogRules) {
|
||||
document.getElementById('verilogRulesTextarea').value = settings.verilogRules;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
248
src/views/settingsComponent.ts
Normal file
@ -0,0 +1,248 @@
|
||||
import {
|
||||
getGeneralSettingsComponentContent,
|
||||
getGeneralSettingsComponentStyles,
|
||||
getGeneralSettingsComponentScript,
|
||||
} from "./generalSettingsComponent";
|
||||
import {
|
||||
getRulesSettingsComponentContent,
|
||||
getRulesSettingsComponentStyles,
|
||||
getRulesSettingsComponentScript,
|
||||
} from "./rulesSettingsComponent";
|
||||
|
||||
/**
|
||||
* 获取设置面板的 HTML 内容
|
||||
*/
|
||||
export function getSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="settings-modal" id="settingsModal">
|
||||
<div class="settings-modal-overlay" onclick="closeSettingsModal()"></div>
|
||||
<div class="settings-modal-content">
|
||||
<div class="settings-modal-header">
|
||||
<h2 class="settings-modal-title">设置</h2>
|
||||
<button class="settings-modal-close" onclick="closeSettingsModal()" title="关闭">
|
||||
<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="settings-modal-body">
|
||||
<div class="settings-nav">
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设置面板的 CSS 样式
|
||||
*/
|
||||
export function getSettingsComponentStyles(): string {
|
||||
return `
|
||||
.settings-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.settings-modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.settings-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.settings-modal-content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
height: 80%;
|
||||
max-height: 600px;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.settings-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-modal-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.settings-modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-foreground);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-modal-close:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.settings-modal-close svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.settings-modal-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
width: 180px;
|
||||
padding: 16px 0;
|
||||
border-right: 1px solid var(--vscode-panel-border);
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.settings-nav-item {
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.settings-nav-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.settings-nav-item.active {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
border-left-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
${getGeneralSettingsComponentStyles()}
|
||||
${getRulesSettingsComponentStyles()}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设置面板的 JavaScript 脚本
|
||||
*/
|
||||
export function getSettingsComponentScript(): string {
|
||||
return `
|
||||
${getGeneralSettingsComponentScript()}
|
||||
${getRulesSettingsComponentScript()}
|
||||
|
||||
// 打开设置面板
|
||||
function openSettingsModal() {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭设置面板
|
||||
function closeSettingsModal() {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换设置标签页
|
||||
function switchSettingsTab(tabName) {
|
||||
// 更新导航项状态
|
||||
const navItems = document.querySelectorAll('.settings-nav-item');
|
||||
navItems.forEach(item => {
|
||||
if (item.dataset.tab === tabName) {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 更新内容区域
|
||||
const tabContents = document.querySelectorAll('.settings-tab-content');
|
||||
tabContents.forEach(content => {
|
||||
if (content.id === tabName + 'Settings') {
|
||||
content.classList.add('active');
|
||||
} else {
|
||||
content.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 阻止点击模态框内容时关闭
|
||||
document.addEventListener('click', (event) => {
|
||||
const modalContent = document.querySelector('.settings-modal-content');
|
||||
if (modalContent && modalContent.contains(event.target)) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
@ -14,26 +14,53 @@ export function getUserInfoComponentContent(): string {
|
||||
<div class="user-detail-dropdown" id="userDetailDropdown">
|
||||
<div class="user-detail-content">
|
||||
<div class="user-detail-header">
|
||||
<div class="user-avatar-small">
|
||||
<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" id="userDetailName">加载中...</div>
|
||||
<img class="tier-icon-inline" id="tierIconInline" style="display: none;" />
|
||||
<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">
|
||||
<!-- <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>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -84,12 +111,18 @@ export function getUserInfoComponentStyles(): string {
|
||||
.user-detail-header {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background: linear-gradient(135deg, rgba(0, 122, 204, 0.1) 0%, rgba(88, 166, 255, 0.05) 100%);
|
||||
border-bottom: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.user-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
@ -100,6 +133,16 @@ export function getUserInfoComponentStyles(): string {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-avatar-small.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-avatar-small.clickable:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.5);
|
||||
}
|
||||
|
||||
.user-avatar-small svg {
|
||||
@ -119,6 +162,16 @@ export function getUserInfoComponentStyles(): string {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-detail-name.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-detail-name.clickable:hover {
|
||||
color: #007acc;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tier-icon-inline {
|
||||
@ -152,6 +205,19 @@ export function getUserInfoComponentStyles(): string {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.logout-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logout-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-color: rgba(204, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.logout-item:hover .logout-link {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
@ -168,6 +234,11 @@ export function getUserInfoComponentStyles(): string {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
color: var(--vscode-foreground);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tier-icon-large {
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
@ -180,6 +251,136 @@ export function getUserInfoComponentStyles(): string {
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.upgrade-pro-wrapper {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.upgrade-pro-btn {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(135deg, #007acc 0%, #58a6ff 100%);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 4px rgba(0, 122, 204, 0.2);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.upgrade-pro-btn:hover {
|
||||
background: linear-gradient(135deg, #0098ff 0%, #6bb6ff 100%);
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.upgrade-pro-btn:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
/* 退出登录确认对话框 */
|
||||
.logout-confirm-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
.logout-confirm-modal.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logout-confirm-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.logout-confirm-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 8px;
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.logout-confirm-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.logout-confirm-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.logout-confirm-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.logout-confirm-body p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logout-confirm-footer {
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
border-top: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.logout-confirm-btn {
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-cancel-btn {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.logout-cancel-btn:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.logout-ok-btn {
|
||||
background: #f48771;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.logout-ok-btn:hover {
|
||||
background: #e67361;
|
||||
}
|
||||
|
||||
.logout-confirm-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -214,6 +415,44 @@ export function getUserInfoComponentScript(): string {
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
console.log("显示退出登录确认对话框");
|
||||
// 显示确认对话框
|
||||
const modal = document.getElementById('logoutConfirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 确认退出登录
|
||||
function confirmLogout() {
|
||||
console.log("确认退出登录");
|
||||
vscode.postMessage({ command: 'logout' });
|
||||
// 关闭确认对话框
|
||||
closeLogoutConfirmModal();
|
||||
}
|
||||
|
||||
// 关闭退出登录确认对话框
|
||||
function closeLogoutConfirmModal() {
|
||||
const modal = document.getElementById('logoutConfirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到 IC Coder 官网
|
||||
function openICCoder() {
|
||||
console.log("跳转到 IC Coder 官网");
|
||||
vscode.postMessage({ command: 'openICCoder' });
|
||||
}
|
||||
|
||||
// 升级到Pro
|
||||
function upgradeToPro() {
|
||||
console.log("升级到 Pro - 跳转到 IC Coder 官网");
|
||||
vscode.postMessage({ command: 'openExternalUrl', url: 'https://www.iccoder.com' });
|
||||
}
|
||||
|
||||
// 关闭用户详情下拉面板
|
||||
function closeUserDetailModal() {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
@ -250,18 +489,104 @@ export function getUserInfoComponentScript(): string {
|
||||
|
||||
// 更新剩余 Credits
|
||||
const creditsDetail = document.getElementById('creditsDetail');
|
||||
console.log('[UserInfoComponent] 更新 Credits 显示');
|
||||
console.log('[UserInfoComponent] currentUserInfo.credits:', currentUserInfo.credits);
|
||||
console.log('[UserInfoComponent] creditsDetail 元素:', creditsDetail);
|
||||
if (creditsDetail) {
|
||||
creditsDetail.textContent = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-';
|
||||
const creditsText = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-';
|
||||
creditsDetail.textContent = creditsText;
|
||||
console.log('[UserInfoComponent] Credits 已更新为:', creditsText);
|
||||
} else {
|
||||
console.warn('[UserInfoComponent] creditsDetail 元素未找到');
|
||||
}
|
||||
|
||||
// 显示或隐藏升级到Pro按钮 (仅BASIC会员显示)
|
||||
const upgradeProWrapper = document.getElementById('upgradeProWrapper');
|
||||
const tierCode = currentUserInfo.membership?.tierCode;
|
||||
if (upgradeProWrapper) {
|
||||
if (tierCode === 'BASIC') {
|
||||
upgradeProWrapper.style.display = 'block';
|
||||
} else {
|
||||
upgradeProWrapper.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
console.warn('[UserInfoComponent] upgradeProWrapper 元素未找到');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息显示
|
||||
function updateUserInfoDisplay(userInfo) {
|
||||
currentUserInfo = userInfo;
|
||||
console.log('[UserInfoComponent] 更新用户信息:', userInfo);
|
||||
// 如果下拉面板已打开,立即更新显示
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
if (dropdown && dropdown.classList.contains('active')) {
|
||||
updateUserDetailModal();
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定下拉面板事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 绑定退出登录卡片点击事件
|
||||
const logoutItem = document.getElementById('logoutItem');
|
||||
if (logoutItem) {
|
||||
logoutItem.addEventListener('click', () => {
|
||||
logout();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定退出登录确认对话框按钮
|
||||
const logoutOkBtn = document.getElementById('logoutOkBtn');
|
||||
if (logoutOkBtn) {
|
||||
logoutOkBtn.addEventListener('click', () => {
|
||||
confirmLogout();
|
||||
});
|
||||
}
|
||||
|
||||
const logoutCancelBtn = document.getElementById('logoutCancelBtn');
|
||||
if (logoutCancelBtn) {
|
||||
logoutCancelBtn.addEventListener('click', () => {
|
||||
closeLogoutConfirmModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 点击遮罩层关闭对话框
|
||||
const logoutConfirmModal = document.getElementById('logoutConfirmModal');
|
||||
if (logoutConfirmModal) {
|
||||
const overlay = logoutConfirmModal.querySelector('.logout-confirm-overlay');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', () => {
|
||||
closeLogoutConfirmModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定升级到Pro按钮
|
||||
const upgradeProBtn = document.getElementById('upgradeProBtn');
|
||||
if (upgradeProBtn) {
|
||||
upgradeProBtn.addEventListener('click', () => {
|
||||
upgradeToPro();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定头像点击事件
|
||||
const userAvatar = document.getElementById('userAvatarClickable');
|
||||
if (userAvatar) {
|
||||
userAvatar.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
openICCoder();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定用户名点击事件
|
||||
const userName = document.getElementById('userDetailName');
|
||||
if (userName) {
|
||||
userName.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
openICCoder();
|
||||
});
|
||||
}
|
||||
|
||||
// 点击页面其他地方关闭下拉面板
|
||||
document.addEventListener('click', (e) => {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
|
||||
@ -174,7 +174,7 @@ export function getWaveformPreviewScript(): string {
|
||||
const content = document.createElement('div');
|
||||
content.className = 'waveform-preview-content';
|
||||
|
||||
const miniViewerId = 'waveform-mini-' + Date.now();
|
||||
const miniViewerId = 'waveform-mini-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
const miniViewer = document.createElement('div');
|
||||
miniViewer.id = miniViewerId;
|
||||
miniViewer.className = 'waveform-mini-viewer';
|
||||
@ -218,16 +218,8 @@ export function getWaveformPreviewScript(): string {
|
||||
* 渲染波形预览信息
|
||||
*/
|
||||
function renderWaveformInfo(containerId, vcdInfo) {
|
||||
console.log('[renderWaveformInfo] 开始渲染波形, containerId:', containerId);
|
||||
console.log('[renderWaveformInfo] vcdInfo:', vcdInfo);
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error('[renderWaveformInfo] 找不到容器:', containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[renderWaveformInfo] 找到容器,信号数量:', vcdInfo.signals?.length || 0);
|
||||
if (!container) return;
|
||||
|
||||
// 清空容器
|
||||
container.innerHTML = '';
|
||||
@ -237,7 +229,6 @@ export function getWaveformPreviewScript(): string {
|
||||
waveformSvg.innerHTML = drawRealWaveform(vcdInfo.signals || []);
|
||||
|
||||
container.appendChild(waveformSvg);
|
||||
console.log('[renderWaveformInfo] 波形渲染完成');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -25,6 +25,26 @@ import {
|
||||
} from "./progressBar";
|
||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||
import { getCurrentEnv } from "../config/settings";
|
||||
import {
|
||||
getInvitationModalContent,
|
||||
getInvitationModalStyles,
|
||||
getInvitationModalScript,
|
||||
} from "./invitationModal";
|
||||
import {
|
||||
getWelcomeModalContent,
|
||||
getWelcomeModalStyles,
|
||||
getWelcomeModalScript,
|
||||
} from "./welcomeModal";
|
||||
import {
|
||||
getNdtWelcomeModalContent,
|
||||
getNdtWelcomeModalStyles,
|
||||
getNdtWelcomeModalScript,
|
||||
} from "./ndtWelcomeModal";
|
||||
import {
|
||||
getExpiredModalContent,
|
||||
getExpiredModalStyles,
|
||||
getExpiredModalScript,
|
||||
} from "./expiredModal";
|
||||
/**
|
||||
* 获取 WebView 面板的 HTML 内容
|
||||
*/
|
||||
@ -33,7 +53,9 @@ export function getWebviewContent(
|
||||
autoIconUri?: string,
|
||||
liteIconUri?: string,
|
||||
syIconUri?: string,
|
||||
maxIconUri?: string
|
||||
maxIconUri?: string,
|
||||
qrCodeUri?: string,
|
||||
logoUri?: string,
|
||||
): string {
|
||||
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
|
||||
const currentEnv = getCurrentEnv();
|
||||
@ -72,7 +94,10 @@ export function getWebviewContent(
|
||||
display: none;
|
||||
}
|
||||
.header h1 {
|
||||
color: var(--vscode-button-background);
|
||||
background: linear-gradient(to right, #4A9EFF, #7CB8FF, #A8D0FF);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.chat-container {
|
||||
@ -89,6 +114,10 @@ export function getWebviewContent(
|
||||
${getConversationHistoryBarStyles()}
|
||||
${getProgressBarStyles()}
|
||||
${getInputAreaStyles()}
|
||||
${getInvitationModalStyles()}
|
||||
${getWelcomeModalStyles()}
|
||||
${getNdtWelcomeModalStyles()}
|
||||
${getExpiredModalStyles()}
|
||||
|
||||
.file-editor-section {
|
||||
margin-bottom: 15px;
|
||||
@ -275,6 +304,7 @@ export function getWebviewContent(
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
font-size:0.9rem
|
||||
}
|
||||
.segment-tool {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
@ -296,7 +326,6 @@ export function getWebviewContent(
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.tool-segment-result {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
padding-left: 22px;
|
||||
@ -389,17 +418,83 @@ export function getWebviewContent(
|
||||
.quick-btn:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-height: 600px) {
|
||||
.header {
|
||||
/* 使用 clamp 动态调整内边距: 最小值 5px, 理想值 2vh, 最大值 20px */
|
||||
padding: clamp(5px, 2vh, 20px) 20px;
|
||||
flex: 0 0 auto;
|
||||
min-height: auto;
|
||||
}
|
||||
.header img {
|
||||
/* 使用 clamp 动态调整图片高度: 最小值 40px, 理想值 10vh, 最大值 60px */
|
||||
max-height: clamp(40px, 10vh, 60px) !important;
|
||||
}
|
||||
.header p {
|
||||
/* 使用 clamp 动态调整字体大小 */
|
||||
font-size: clamp(12px, 2.5vh, 14px) !important;
|
||||
margin-top: clamp(4px, 1.5vh, 8px) !important;
|
||||
line-height: 1.2 !important;
|
||||
margin-bottom: clamp(4px, 1.5vh, 8px) !important;
|
||||
}
|
||||
.quick-actions {
|
||||
margin-bottom: 5px;
|
||||
gap: 5px;
|
||||
}
|
||||
.quick-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.chat-container {
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高度极小时隐藏描述文本 */
|
||||
@media (max-height: 450px) {
|
||||
.header p {
|
||||
display: none !important;
|
||||
}
|
||||
.header {
|
||||
padding: 4px;
|
||||
}
|
||||
.quick-actions {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
}
|
||||
.quick-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
.chat-container {
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${getConversationHistoryBarContent()}
|
||||
${getProgressBarContent()}
|
||||
${getInvitationModalContent(qrCodeUri, logoUri)}
|
||||
${getWelcomeModalContent(logoUri)}
|
||||
${getNdtWelcomeModalContent(logoUri)}
|
||||
${getExpiredModalContent(logoUri)}
|
||||
<div class="header">
|
||||
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
|
||||
<h1 style="margin: 0;">IC Coder</h1>
|
||||
<div style="display: flex; align-items: center; justify-content: center;">
|
||||
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
|
||||
</div>
|
||||
<p>专注于真实FPGA研发的Verilog智能体编程平台</p>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
@ -428,6 +523,7 @@ export function getWebviewContent(
|
||||
<script>
|
||||
console.log('[WebView] 脚本开始执行');
|
||||
const vscode = acquireVsCodeApi();
|
||||
window.vscode = vscode; // 确保全局可访问
|
||||
console.log('[WebView] vscode API 已获取');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
@ -438,6 +534,12 @@ export function getWebviewContent(
|
||||
let loadingIndicator = null;
|
||||
let currentSegmentedMessage = null; // 当前分段消息容器
|
||||
|
||||
// 设置二维码图片
|
||||
const feedbackQRCodeImage = document.getElementById('feedbackQRCodeImage');
|
||||
if (feedbackQRCodeImage && '${qrCodeUri}') {
|
||||
feedbackQRCodeImage.src = '${qrCodeUri}';
|
||||
}
|
||||
|
||||
// ========== 模式选择器脚本(直接内联,避免模板字符串嵌套问题)==========
|
||||
let currentMode = 'agent';
|
||||
|
||||
@ -588,24 +690,52 @@ export function getWebviewContent(
|
||||
case 'updateUserInfo':
|
||||
// 更新用户信息
|
||||
console.log('[WebView] 收到用户信息:', message.userInfo);
|
||||
console.log('[WebView] Credits 字段值:', message.userInfo?.credits);
|
||||
if (message.userInfo) {
|
||||
const userInfoData = {
|
||||
nickname: message.userInfo.nickname || message.userInfo.username || '用户',
|
||||
userId: message.userInfo.userId || message.userInfo.id,
|
||||
tierName: message.userInfo.tierName,
|
||||
tierIconUrl: message.tierIconUrl,
|
||||
registerTime: message.userInfo.registerTime || message.userInfo.createdAt
|
||||
registerTime: message.userInfo.registerTime || message.userInfo.createdAt,
|
||||
credits: message.userInfo.credits,
|
||||
membership: message.userInfo.membership
|
||||
};
|
||||
|
||||
console.log('[WebView] 显示用户信息:', userInfoData);
|
||||
console.log('[WebView] userInfoData.credits:', userInfoData.credits);
|
||||
console.log('[WebView] userInfoData.membership:', userInfoData.membership);
|
||||
|
||||
// 调用更新用户头像图标按钮的函数
|
||||
if (typeof updateUserAvatarIconButton === 'function') {
|
||||
updateUserAvatarIconButton(userInfoData);
|
||||
} else {
|
||||
console.warn('[WebView] updateUserAvatarIconButton 函数不存在');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'autoSendMessage':
|
||||
// 自动发送待发送的消息(登录后)
|
||||
console.log('[WebView] 自动发送待发送消息:', message.text);
|
||||
const inputElement = document.getElementById('userInput');
|
||||
if (inputElement) {
|
||||
inputElement.value = message.text;
|
||||
// 触发发送
|
||||
if (typeof sendMessage === 'function') {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'showFeedbackQRCode':
|
||||
// 显示用户反馈二维码弹窗
|
||||
console.log('[WebView] 显示用户反馈二维码弹窗');
|
||||
if (typeof showFeedbackQRCode === 'function') {
|
||||
showFeedbackQRCode();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'resetSegmentedMessage':
|
||||
// 重置分段消息容器(停止对话时调用)
|
||||
console.log('[WebView] 重置分段消息容器');
|
||||
@ -629,21 +759,14 @@ export function getWebviewContent(
|
||||
if (typeof hasWorkspace !== 'undefined') {
|
||||
hasWorkspace = message.hasWorkspace;
|
||||
console.log('[WebView] 工作区状态:', hasWorkspace);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'checkPanelWidth':
|
||||
// 检查面板宽度
|
||||
const minWidth = message.minWidth || 200;
|
||||
const currentWidth = document.body.clientWidth;
|
||||
console.log('[WebView] 检查面板宽度:', currentWidth, '最小宽度:', minWidth);
|
||||
if (currentWidth < minWidth) {
|
||||
// 宽度不足,通知后端关闭面板
|
||||
vscode.postMessage({
|
||||
command: 'panelWidthInsufficient',
|
||||
currentWidth: currentWidth,
|
||||
minWidth: minWidth
|
||||
});
|
||||
// 如果有待发送的示例,且工作区存在,则发送
|
||||
if (hasWorkspace && typeof pendingExampleIndex !== 'undefined' && pendingExampleIndex >= 0) {
|
||||
if (typeof doSendExample === 'function') {
|
||||
doSendExample(pendingExampleIndex);
|
||||
pendingExampleIndex = -1; // 重置
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@ -752,46 +875,49 @@ export function getWebviewContent(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'optimizeResult':
|
||||
// 处理提示词优化结果
|
||||
if (typeof handleOptimizeResult === 'function') {
|
||||
handleOptimizeResult(message.success, message.optimizedPrompt, message.error);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'showChanges':
|
||||
// 显示代码变更
|
||||
if (typeof showChangesPanel === 'function') {
|
||||
showChangesPanel(message.changes);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'changeAccepted':
|
||||
// 变更已采纳
|
||||
if (typeof handleChangeAccepted === 'function') {
|
||||
handleChangeAccepted(message.changeId, message.success, message.error);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'changeRejected':
|
||||
// 变更已拒绝
|
||||
if (typeof handleChangeRejected === 'function') {
|
||||
handleChangeRejected(message.changeId, message.success, message.error);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听窗口大小变化,检查面板宽度
|
||||
let resizeTimer;
|
||||
const MIN_PANEL_WIDTH = 500;
|
||||
|
||||
function checkPanelWidth() {
|
||||
const currentWidth = document.body.clientWidth;
|
||||
if (currentWidth < MIN_PANEL_WIDTH) {
|
||||
console.log('[WebView] 面板宽度不足:', currentWidth, 'px,最小要求:', MIN_PANEL_WIDTH, 'px');
|
||||
vscode.postMessage({
|
||||
command: 'panelWidthInsufficient',
|
||||
currentWidth: currentWidth,
|
||||
minWidth: MIN_PANEL_WIDTH
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
// 使用防抖,避免频繁检查
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
checkPanelWidth();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 初始加载时也检查一次
|
||||
setTimeout(() => {
|
||||
checkPanelWidth();
|
||||
}, 500);
|
||||
|
||||
${getMessageAreaScript()}
|
||||
${getAgentCardScript()}
|
||||
${getWaveformPreviewScript()}
|
||||
${getConversationHistoryBarScript()}
|
||||
${getProgressBarScript()}
|
||||
${getInputAreaScript()}
|
||||
${getInvitationModalScript()}
|
||||
${getWelcomeModalScript()}
|
||||
${getNdtWelcomeModalScript()}
|
||||
${getExpiredModalScript()}
|
||||
</script></body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
310
src/views/welcomeModal.ts
Normal file
@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 欢迎弹窗(试用用户)
|
||||
* 功能:在聊天面板内显示欢迎模态窗口
|
||||
* 依赖:无
|
||||
* 使用场景:试用用户首次登录时在聊天面板内显示
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取欢迎弹窗的 HTML 内容
|
||||
*/
|
||||
export function getWelcomeModalContent(logoUri?: string): string {
|
||||
return `
|
||||
<!-- 欢迎弹窗 -->
|
||||
<div id="welcomeModal" class="welcome-modal" style="display: none;">
|
||||
<div class="welcome-modal-overlay"></div>
|
||||
<div class="welcome-modal-content">
|
||||
${logoUri ? `<img src="${logoUri}" class="welcome-logo-corner" alt="IC Coder" />` : ""}
|
||||
|
||||
<div class="welcome-modal-header">
|
||||
<div class="welcome-icon">🎉</div>
|
||||
<h2>欢迎使用 IC Coder!</h2>
|
||||
</div>
|
||||
|
||||
<div class="welcome-modal-body">
|
||||
<!-- 试用期提示 -->
|
||||
<div class="trial-banner">
|
||||
<span>您已获得 <strong>5 天企业版试用期</strong>,企业版试用期内Credits用量无限,并可无限制使用所有功能</span>
|
||||
</div>
|
||||
|
||||
<!-- IC Coder 简介 -->
|
||||
<div class="intro-section">
|
||||
<h3 class="section-title">关于 IC Coder</h3>
|
||||
<p class="intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform(自主式人工智能 Verilog 编码平台)。我们采用全球顶尖的IC Coder自研芯片设计微调模型,为代码生成提供强大的AI能力支撑。</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-item">
|
||||
<span class="feature-text">多智能体架构(Multi-Agent System):多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-text">AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮组 -->
|
||||
<div class="button-group">
|
||||
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
|
||||
<span>开始使用</span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取欢迎弹窗的 CSS 样式
|
||||
*/
|
||||
export function getWelcomeModalStyles(): string {
|
||||
return `
|
||||
/* 欢迎弹窗样式 */
|
||||
.welcome-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||
}
|
||||
|
||||
.welcome-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.welcome-modal-content {
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.welcome-logo-corner {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 24px;
|
||||
height: 40px;
|
||||
width: auto;
|
||||
opacity: 0.9;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.welcome-modal-header {
|
||||
padding: 60px 32px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.welcome-modal-header h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.welcome-modal-subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.welcome-modal-body {
|
||||
padding: 0 32px 32px;
|
||||
}
|
||||
|
||||
/* 试用期横幅 */
|
||||
.trial-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 16px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border-left: 3px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.trial-banner strong {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* IC Coder 简介区域 */
|
||||
.intro-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
border-left: 2px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.button-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.welcome-btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.welcome-btn-primary {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.welcome-btn-primary:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.welcome-btn-secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
}
|
||||
|
||||
.welcome-btn-secondary:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.welcome-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取欢迎弹窗的 JavaScript 逻辑
|
||||
*/
|
||||
export function getWelcomeModalScript(): string {
|
||||
return `
|
||||
// 欢迎弹窗逻辑
|
||||
(function() {
|
||||
const modal = document.getElementById('welcomeModal');
|
||||
const startBtn = document.getElementById('welcomeStartBtn');
|
||||
const overlay = modal?.querySelector('.welcome-modal-overlay');
|
||||
|
||||
// 显示欢迎弹窗
|
||||
window.showWelcomeModal = function() {
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏欢迎弹窗
|
||||
window.hideWelcomeModal = function() {
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
// 点击"开始使用"按钮
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', function() {
|
||||
hideWelcomeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 点击遮罩层关闭弹窗
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function() {
|
||||
hideWelcomeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 阻止点击弹窗内容时关闭
|
||||
const content = modal?.querySelector('.welcome-modal-content');
|
||||
if (content) {
|
||||
content.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// 监听来自后端的消息
|
||||
window.addEventListener('message', function(event) {
|
||||
const message = event.data;
|
||||
if (message.command === 'showWelcomeModal') {
|
||||
showWelcomeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时检查是否需要显示欢迎弹窗
|
||||
vscode.postMessage({ command: 'checkWelcomeModal' });
|
||||
})();
|
||||
`;
|
||||
}
|
||||
BIN
tools/waveform_trace/bin/waveform_trace.exe
Normal file
@ -3,6 +3,7 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
//@ts-check
|
||||
/** @typedef {import('webpack').Configuration} WebpackConfig **/
|
||||
@ -20,7 +21,8 @@ const extensionConfig = {
|
||||
libraryTarget: 'commonjs2'
|
||||
},
|
||||
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/
|
||||
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
|
||||
},
|
||||
resolve: {
|
||||
@ -44,5 +46,12 @@ const extensionConfig = {
|
||||
infrastructureLogging: {
|
||||
level: "log", // enables logging required for problem matchers
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: 'src/assets', to: 'assets' }
|
||||
]
|
||||
})
|
||||
]
|
||||
};
|
||||
module.exports = [ extensionConfig ];
|
||||