Compare commits
169 Commits
5c19be22d3
...
feat/eda
| Author | SHA1 | Date | |
|---|---|---|---|
| ccbc40f5fb | |||
| 5adde3d40a | |||
| fa5c2cdafd | |||
| aa80088abc | |||
| 0ae627ca7c | |||
| 7732b11d37 | |||
| 81717dc84f | |||
| 11c408ce0f | |||
| c138406217 | |||
| 2a280aaa93 | |||
| 2f6eae9f2b | |||
| d0ff876ba2 | |||
| 7fe87e515b | |||
| 790110ba7e | |||
| 29e80ce296 | |||
| c244a308d7 | |||
| 7cde4fa138 | |||
| 1b7259d1c1 | |||
| 09ff812562 | |||
| e7c631d532 | |||
| 06573e37d7 | |||
| d740f4da44 | |||
| f24bd38ec7 | |||
| 45934baf0a | |||
| 4384ee53c5 | |||
| d89c326be5 | |||
| 2dccb4f871 | |||
| a9ddf3074e | |||
| db087bb184 | |||
| 5e9083041f | |||
| be0555d6bc | |||
| 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 | |||
| 4687c3faa6 | |||
| feff8ea4d3 | |||
| 6abec8c7b7 | |||
| f9b3699bda | |||
| 8da1177bf3 | |||
| a85a044a9b | |||
| c58e3603de | |||
| 940584e1ea |
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
|
||||
|
||||
4
.npmrc
@ -1 +1,3 @@
|
||||
enable-pre-post-scripts = true
|
||||
enable-pre-post-scripts = true
|
||||
shamefully-hoist = true
|
||||
public-hoist-pattern[] = *
|
||||
@ -1,29 +1,33 @@
|
||||
# 排除开发文件
|
||||
# 开发文件
|
||||
.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)
|
||||
# 文档(避免中文文件名打包问题)
|
||||
docs/**
|
||||
CLAUDE.md
|
||||
|
||||
# 只排除 waveform_trace 的 src/dist 目录
|
||||
tools/waveform_trace/src/**
|
||||
tools/waveform_trace/build/**
|
||||
tools/waveform_trace/dist/**
|
||||
tools/waveform_trace/build.bat
|
||||
tools/waveform_trace/build.sh
|
||||
tools/iverilog/examples/**
|
||||
tools/iverilog/INSTALL.md
|
||||
tools/iverilog/README.md
|
||||
tools/iverilog/DOWNLOAD_INSTRUCTIONS.md
|
||||
|
||||
# 排除打包临时文件
|
||||
**/__pycache__/**
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
**/*.spec
|
||||
|
||||
# Git 相关
|
||||
.git/**
|
||||
.github/**
|
||||
|
||||
69
CHANGELOG.md
@ -1,9 +1,68 @@
|
||||
# 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.12] - 2026-03-06
|
||||
|
||||
## [Unreleased]
|
||||
### 新增
|
||||
|
||||
- Initial release
|
||||
- 支持 AskUserQuestion 多问题和多选功能
|
||||
|
||||
## [1.0.9] - 2026-03-04
|
||||
|
||||
### 优化
|
||||
|
||||
- 将工具折叠图标颜色从蓝色改为灰色
|
||||
- 统一使用蓝色主题色
|
||||
- 优化打包配置,排除重复的 exe 文件
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复代码变更继续对话查找不到之前的代码变更信息的 bug
|
||||
- 修复对话展示两遍的问题
|
||||
|
||||
## [1.0.8] - 2026-03-03
|
||||
|
||||
### 新增
|
||||
|
||||
- 删除文件确认功能
|
||||
- 文件路径标签显示
|
||||
- 企业试用用户欢迎弹窗优化
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复继续对话时消息覆盖问题
|
||||
- 修复试用用户欢迎弹窗显示逻辑
|
||||
- 修复企业试用用户仍弹出邀请码的问题
|
||||
- 修复登录过期点击重新登录失败的问题
|
||||
|
||||
## [1.0.7] - 2026-03-02
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 AI 响应内容重复显示问题
|
||||
|
||||
## [1.0.6] - 2026-03-02
|
||||
|
||||
### 新增
|
||||
|
||||
- Git Diff 功能:支持查看当前文件的 Git 差异对比
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复添加上下文搜索选择文件不匹配的问题
|
||||
- 修复过期认证状态未清除导致重新登录失败的问题
|
||||
|
||||
## [1.0.4] - 2026-01-28
|
||||
|
||||
IC Coder插件端正式上线。
|
||||
|
||||
IC Coder 插件端是一个是一个自主式人工智能 Verilog 编码平台,可以将芯片设计与验证的效率提升至少20倍!
|
||||
|
||||
主要功能:
|
||||
|
||||
- 自动搭建电路架构:够根据自然语言描述的设计需求,自动生成完整的电路架构
|
||||
- AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码
|
||||
- AI自主代码迭代:实现了真正的自主式开发循环,能够持续优化代码直到满足设计要求
|
||||
- 随时可掌控:提供透明化的开发过程,让用户始终掌握AI的工作状态
|
||||
- 多层次安全保障:将数据安全和隐私保护作为核心设计原则,提供企业级的安全保障
|
||||
|
||||
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. ✅ 向后兼容(可以只有一个问题)
|
||||
526
docs/EDA联动功能需求文档.md
Normal file
@ -0,0 +1,526 @@
|
||||
# Vivado 联动功能需求文档
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
### 1.1 当前状态
|
||||
|
||||
IC Coder Plugin 目前支持:
|
||||
|
||||
- iverilog 仿真(内置 Windows 版本)
|
||||
- VCD 波形查看
|
||||
- Verilog 代码生成和文件操作
|
||||
|
||||
### 1.2 需求来源
|
||||
|
||||
用户需要在 VS Code 中直接调用本地 Vivado 工具,并将产出文件自动导入到项目中,完成从仿真到 FPGA 部署的完整流程。
|
||||
|
||||
### 1.3 Vivado 是什么?
|
||||
|
||||
**Vivado** 是 Xilinx(现 AMD)的 FPGA 开发工具,用于将 Verilog 代码部署到 FPGA 硬件:
|
||||
|
||||
- **综合(Synthesis)**:将 RTL 代码转换为门级网表
|
||||
- **实现(Implementation)**:布局布线,映射到具体 FPGA 芯片
|
||||
- **生成比特流(Bitstream)**:生成 .bit 配置文件用于烧录
|
||||
|
||||
**与 iverilog 的区别**:
|
||||
|
||||
- iverilog:只做**仿真验证**(软件层面验证逻辑)
|
||||
- Vivado:做**综合+实现+生成配置文件**(真正部署到硬件)
|
||||
|
||||
**典型开发流程**:
|
||||
|
||||
```
|
||||
编写 Verilog → iverilog 仿真验证 → Vivado 综合 → Vivado 实现 → 生成 .bit 文件 → 烧录到 FPGA
|
||||
```
|
||||
|
||||
## 2. 功能目标
|
||||
|
||||
### 2.1 核心目标
|
||||
|
||||
- **前端提供原子工具**:前端只提供独立的 Vivado 命令工具,不控制流程
|
||||
- **后端AI控制流程**:所有执行顺序、依赖检查由后端AI决策
|
||||
- **工具职责单一**:每个工具只负责执行一个具体命令
|
||||
- **结果透明返回**:执行结果完整返回给后端,由后端决定下一步
|
||||
|
||||
### 2.2 设计原则
|
||||
|
||||
- 前端不做流程判断,只执行命令
|
||||
- 前端不检查依赖关系,由后端保证顺序
|
||||
- 前端返回详细的执行结果,包括成功/失败、输出、报告等
|
||||
- 后端AI根据结果智能决策是否继续
|
||||
|
||||
## 3. 功能详细需求
|
||||
|
||||
### 3.1 前端提供的工具
|
||||
|
||||
前端提供 4 个独立的工具,每个工具只负责执行一个命令:
|
||||
|
||||
#### 3.1.1 createVivadoProject - 创建工程
|
||||
|
||||
- **输入**:项目名称、芯片型号、源文件列表、约束文件(可选)
|
||||
- **输出**:工程文件(.xpr)
|
||||
- **说明**:创建 Vivado 工程,不执行任何构建操作
|
||||
|
||||
#### 3.1.2 runVivadoSynthesis - 综合
|
||||
|
||||
- **输入**:工程路径或源文件、芯片型号、顶层模块、约束文件(可选)
|
||||
- **输出**:.dcp 文件、综合报告
|
||||
- **说明**:执行综合,前端不检查工程是否存在。约束文件在此阶段可选,主要用于时序约束
|
||||
|
||||
#### 3.1.3 runVivadoImplementation - 实现
|
||||
|
||||
- **输入**:综合后的 .dcp 文件路径、约束文件(必需,包含管脚约束)
|
||||
- **输出**:实现后的 .dcp 文件、时序报告
|
||||
- **说明**:执行实现,前端不检查 .dcp 是否存在。**管脚约束是必需的**,否则无法完成布局布线
|
||||
|
||||
#### 3.1.4 runVivadoBitstream - 生成比特流
|
||||
|
||||
- **输入**:实现后的 .dcp 文件路径
|
||||
- **输出**:.bit 文件(可下载到 FPGA 的配置文件)
|
||||
- **核心依赖**:
|
||||
1. 实现已完成
|
||||
2. 工程指定目标芯片型号
|
||||
3. 已完成管脚约束(无管脚约束无法生成)
|
||||
- **说明**:生成比特流,前端不检查 .dcp 是否存在
|
||||
|
||||
### 3.2 配置管理
|
||||
|
||||
#### 3.2.1 配置项
|
||||
|
||||
```json
|
||||
{
|
||||
"vivado": {
|
||||
"enabled": true,
|
||||
"executablePath": "C:/Xilinx/Vivado/2023.1/bin/vivado.bat",
|
||||
"workingDir": "${workspaceFolder}/vivado_project",
|
||||
"part": "xc7a35tcpg236-1", // FPGA 型号
|
||||
"commands": {
|
||||
"synthesis": "vivado -mode batch -source synth.tcl",
|
||||
"implementation": "vivado -mode batch -source impl.tcl",
|
||||
"bitstream": "vivado -mode batch -source bitstream.tcl"
|
||||
},
|
||||
"outputFiles": {
|
||||
"synthesis": ["*.dcp", "*_synth.rpt"],
|
||||
"implementation": ["*.dcp", "*_timing.rpt", "*_utilization.rpt"],
|
||||
"bitstream": ["*.bit"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 存储位置
|
||||
|
||||
- 全局配置:VS Code Settings(`settings.json`)
|
||||
- 项目配置:`.vscode/ic-coder-vivado.json`(优先级更高)
|
||||
|
||||
### 3.3 工具调用接口
|
||||
|
||||
#### 3.3.1 通用响应格式
|
||||
|
||||
所有工具返回统一的响应格式:
|
||||
|
||||
```typescript
|
||||
interface VivadoToolResponse {
|
||||
success: boolean; // 是否成功
|
||||
command: string; // 执行的命令
|
||||
executionTime: number; // 执行时间(毫秒)
|
||||
output: string; // 完整输出日志
|
||||
error?: string; // 错误信息(如果失败)
|
||||
outputFiles?: string[]; // 产出文件路径列表
|
||||
reports?: {
|
||||
resources?: string; // 资源使用摘要
|
||||
timing?: string; // 时序信息摘要
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.2 各工具的参数定义
|
||||
|
||||
**createVivadoProject**
|
||||
```typescript
|
||||
{
|
||||
projectName: string; // 项目名称
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files: string[]; // 源文件列表
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoSynthesis**
|
||||
```typescript
|
||||
{
|
||||
projectPath?: string; // 工程路径(可选,如果有工程)
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files?: string[]; // 源文件(如果没有工程)
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoImplementation**
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||
constraints: string; // 约束文件(必需,包含管脚约束)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoBitstream**
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 后端AI的职责
|
||||
|
||||
后端AI负责:
|
||||
1. 询问用户必要参数(芯片型号、执行模式等)
|
||||
2. 理解用户意图,决定调用哪些工具
|
||||
3. 按正确顺序调用工具(遵循依赖关系)
|
||||
4. 检查每步执行结果,决定是否继续
|
||||
5. 汇总结果并展示给用户
|
||||
|
||||
#### 3.4.1 询问用户参数
|
||||
|
||||
后端必须询问:
|
||||
- **芯片型号**(必需):"请提供 FPGA 芯片型号(例如:xc7a35tcpg236-1)"
|
||||
- **执行模式**(必需):"选择执行模式:1) 图形化 2) 后端执行"
|
||||
- **约束文件**(必需):"请提供约束文件(.xdc),包含管脚约束和时序约束"
|
||||
|
||||
#### 3.4.2 理解依赖关系
|
||||
|
||||
后端AI需要理解:
|
||||
```
|
||||
创建工程 → 综合 → 实现 → 生成比特流
|
||||
```
|
||||
|
||||
如果用户说"做实现",后端应该:
|
||||
1. 先调用 `createVivadoProject` 创建工程
|
||||
2. 再调用 `runVivadoSynthesis` 执行综合
|
||||
3. 最后调用 `runVivadoImplementation` 执行实现
|
||||
|
||||
#### 3.4.3 逐步调用工具
|
||||
|
||||
```
|
||||
步骤1: 调用 createVivadoProject
|
||||
检查 response.success
|
||||
如果失败 → 停止并报错
|
||||
|
||||
步骤2: 调用 runVivadoSynthesis
|
||||
检查 response.success
|
||||
如果失败 → 停止并报错
|
||||
|
||||
步骤3: 调用 runVivadoImplementation
|
||||
检查 response.success
|
||||
返回最终结果
|
||||
```
|
||||
|
||||
### 3.5 UI 交互
|
||||
|
||||
#### 3.5.1 配置界面
|
||||
|
||||
- 在设置页面添加 "Vivado 配置" 选项
|
||||
- 支持配置 Vivado 路径、FPGA 型号
|
||||
- 支持测试 Vivado 可用性(点击按钮测试)
|
||||
|
||||
#### 3.5.2 调用界面
|
||||
|
||||
- 在聊天面板中,AI 可以建议使用 Vivado
|
||||
- 用户确认后,显示执行进度对话框
|
||||
- 实时显示日志输出(可折叠)
|
||||
- 显示执行状态:准备中 → 执行中 → 完成/失败
|
||||
|
||||
#### 3.5.3 结果展示
|
||||
|
||||
- 执行成功:显示执行时间、资源使用、时序信息
|
||||
- 执行失败:显示错误信息、建议解决方案
|
||||
- 导入文件:高亮显示已导入的文件,支持点击打开报告
|
||||
|
||||
### 3.6 后端集成
|
||||
|
||||
#### 3.6.1 工具定义
|
||||
|
||||
后端注册 4 个独立工具:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "createVivadoProject",
|
||||
"description": "创建 Vivado 工程。需要先询问用户芯片型号和执行模式。",
|
||||
"parameters": {
|
||||
"projectName": "项目名称",
|
||||
"part": "芯片型号(必须从用户获取)",
|
||||
"topModule": "顶层模块名",
|
||||
"files": "源文件列表",
|
||||
"constraints": "约束文件(可选)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoSynthesis",
|
||||
"description": "执行 Vivado 综合。前端不检查依赖,后端需确保工程已创建。",
|
||||
"parameters": {
|
||||
"projectPath": "工程路径(可选)",
|
||||
"part": "芯片型号",
|
||||
"topModule": "顶层模块",
|
||||
"files": "源文件(如果没有工程)",
|
||||
"constraints": "约束文件(可选)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoImplementation",
|
||||
"description": "执行 Vivado 实现。前端不检查依赖,后端需确保综合已完成且提供约束文件。",
|
||||
"parameters": {
|
||||
"dcpFile": "综合后的 .dcp 文件路径",
|
||||
"constraints": "约束文件(必需,包含管脚约束)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoBitstream",
|
||||
"description": "生成比特流。前端不检查依赖,后端需确保实现已完成。",
|
||||
"parameters": {
|
||||
"dcpFile": "实现后的 .dcp 文件路径",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.6.2 后端调用示例
|
||||
|
||||
**场景:用户要求完整流程**
|
||||
|
||||
```
|
||||
用户: 用 Vivado 跑完整流程
|
||||
|
||||
AI: 请提供芯片型号和执行模式
|
||||
用户: xc7a35tcpg236-1,后端执行
|
||||
|
||||
AI 执行:
|
||||
1. [调用] createVivadoProject({ projectName: "counter", part: "xc7a35tcpg236-1", ... })
|
||||
[结果] { success: true, outputFiles: ["counter.xpr"] }
|
||||
|
||||
2. [调用] runVivadoSynthesis({ projectPath: "counter.xpr", ... })
|
||||
[结果] { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||
|
||||
3. [调用] runVivadoImplementation({ dcpFile: "counter_synth.dcp", constraints: "counter.xdc", ... })
|
||||
[结果] { success: true, outputFiles: ["counter_impl.dcp"], reports: {...} }
|
||||
|
||||
4. [调用] runVivadoBitstream({ dcpFile: "counter_impl.dcp", ... })
|
||||
[结果] { success: true, outputFiles: ["counter.bit"] }
|
||||
|
||||
AI 回复: 完成!已生成 counter.bit 文件
|
||||
```
|
||||
|
||||
## 4. 用户场景
|
||||
|
||||
### 4.1 场景一:完整流程执行
|
||||
|
||||
**用户输入**:"用 Vivado 跑完整个 FPGA 流程"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 好的,我将使用 Vivado 执行完整的 FPGA 开发流程。请提供以下信息:
|
||||
|
||||
1. FPGA 芯片型号(例如:xc7a35tcpg236-1)
|
||||
2. 请选择要执行的步骤(可多选):
|
||||
- 综合(Synthesis)
|
||||
- 实现(Implementation)
|
||||
- 生成比特流(Bitstream)
|
||||
3. 执行模式:
|
||||
- 图形化模式:打开 Vivado GUI
|
||||
- 后端执行:自动在后台执行
|
||||
4. 约束文件路径(.xdc,必需)
|
||||
|
||||
用户: xc7a35tcpg236-1,全部步骤,后端执行,约束文件是 counter.xdc
|
||||
|
||||
AI: 收到!执行计划:
|
||||
1. 创建工程
|
||||
2. 综合
|
||||
3. 实现
|
||||
4. 生成比特流
|
||||
|
||||
开始执行...
|
||||
|
||||
[步骤 1/4] 创建工程...
|
||||
✓ 工程创建成功(耗时 2s)
|
||||
|
||||
[步骤 2/4] 综合中...
|
||||
✓ 综合完成(耗时 45s)
|
||||
资源使用:LUT 32/20800 (0.15%), FF 8/41600 (0.02%)
|
||||
|
||||
[步骤 3/4] 实现中...
|
||||
✓ 实现完成(耗时 120s)
|
||||
时序:WNS 5.234ns, TNS 0.000ns(满足要求)
|
||||
|
||||
[步骤 4/4] 生成比特流...
|
||||
✓ 比特流生成完成(耗时 15s)
|
||||
|
||||
所有步骤执行完毕!产出文件已导入到 vivado_output/ 目录。
|
||||
```
|
||||
|
||||
### 4.2 场景二:只执行实现(自动补充依赖)
|
||||
|
||||
**用户输入**:"用 Vivado 做实现"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 请提供以下信息:
|
||||
1. FPGA 芯片型号
|
||||
2. 执行模式(图形化/后端执行)
|
||||
|
||||
用户: xc7a35tcpg236-1,后端执行
|
||||
|
||||
AI: 注意:实现依赖综合,我将自动执行以下步骤:
|
||||
1. 创建工程
|
||||
2. 综合(前置依赖)
|
||||
3. 实现(您选择的步骤)
|
||||
|
||||
是否继续?
|
||||
|
||||
用户: 是
|
||||
|
||||
AI: 开始执行...
|
||||
[自动执行创建工程 → 综合 → 实现]
|
||||
```
|
||||
|
||||
### 4.3 场景三:图形化模式
|
||||
|
||||
**用户输入**:"打开 Vivado 图形界面做综合"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 请提供芯片型号:
|
||||
用户: xc7a35tcpg236-1
|
||||
|
||||
AI: 正在创建工程并打开 Vivado GUI...
|
||||
✓ 工程已创建
|
||||
✓ Vivado 图形界面已启动
|
||||
|
||||
您现在可以在 Vivado 中手动操作。完成后,产出文件将保存在工程目录中。
|
||||
```
|
||||
|
||||
### 4.4 场景四:执行中断处理
|
||||
|
||||
**执行过程**:
|
||||
```
|
||||
[步骤 1/3] 创建工程...
|
||||
✓ 成功
|
||||
|
||||
[步骤 2/3] 综合中...
|
||||
✗ 综合失败
|
||||
错误:ERROR: [Synth 8-439] module 'counter' not found
|
||||
|
||||
AI: 综合失败,发现以下问题:
|
||||
- 找不到模块 'counter'
|
||||
|
||||
建议检查:
|
||||
1. 模块名是否正确
|
||||
2. 文件中是否定义了该模块
|
||||
3. 是否有语法错误
|
||||
|
||||
执行已停止,请修复错误后重试。
|
||||
```
|
||||
|
||||
## 5. 技术约束
|
||||
|
||||
### 5.1 平台兼容性
|
||||
|
||||
- Windows:支持 `.bat` 可执行文件
|
||||
- Linux:支持 shell 脚本
|
||||
- 路径分隔符自动适配
|
||||
|
||||
### 5.2 性能要求
|
||||
|
||||
- 命令执行不阻塞 UI
|
||||
- 综合时间可能较长(分钟级),需要进度提示
|
||||
- 日志输出实时更新,限制缓冲区大小
|
||||
|
||||
### 5.3 安全性
|
||||
|
||||
- 工作目录限制在项目范围内
|
||||
- 许可证路径不记录到日志
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
### 6.1 功能验收
|
||||
|
||||
- [ ] 用户可以配置 Vivado 路径和 FPGA 型号
|
||||
- [ ] AI 可以通过工具调用成功执行 Vivado 综合
|
||||
- [ ] 产出文件自动导入到指定目录
|
||||
- [ ] 执行过程有清晰的进度提示
|
||||
- [ ] 报告文件可以正常打开查看
|
||||
|
||||
### 6.2 性能验收
|
||||
|
||||
- [ ] 小型项目综合时间 < 1 分钟
|
||||
- [ ] UI 响应流畅,不卡顿
|
||||
- [ ] 日志输出实时更新(延迟 < 500ms)
|
||||
|
||||
### 6.3 用户体验验收
|
||||
|
||||
- [ ] 配置界面直观易用
|
||||
- [ ] 首次使用有引导提示
|
||||
- [ ] 错误提示清晰,有解决建议
|
||||
- [ ] 导入的文件可以直接打开查看
|
||||
|
||||
## 7. 风险和依赖
|
||||
|
||||
### 7.1 风险
|
||||
|
||||
- **Vivado 版本差异**:不同版本的命令行参数可能不同
|
||||
- **许可证问题**:Vivado 需要许可证才能运行
|
||||
- **路径问题**:Windows 路径中的空格和特殊字符
|
||||
- **执行时间长**:大型项目可能需要数十分钟
|
||||
|
||||
### 7.2 依赖
|
||||
|
||||
- 用户需要自行安装 Vivado
|
||||
- 用户需要配置正确的 Vivado 路径
|
||||
- 需要设置环境变量(如 `XILINX_VIVADO`)
|
||||
- 需要有效的 Vivado 许可证
|
||||
- **需要提供 .xdc 约束文件**:
|
||||
- **管脚约束**(必需):定义信号与 FPGA 引脚的映射关系,实现阶段必须提供
|
||||
- **时序约束**(强烈推荐):定义时钟频率和时序要求,确保设计满足性能指标
|
||||
|
||||
## 8. 后续扩展
|
||||
|
||||
### 8.1 短期扩展
|
||||
|
||||
- 支持自定义 TCL 脚本模板
|
||||
- 支持批量处理多个设计
|
||||
- 支持时序约束编辑器
|
||||
|
||||
### 8.2 长期扩展
|
||||
|
||||
- 支持其他 FPGA 工具(Quartus)
|
||||
- 云端 Vivado 服务集成
|
||||
- 结果对比和版本管理
|
||||
- 性能分析和优化建议
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. Vivado 命令行参考
|
||||
|
||||
- 官方文档:https://docs.xilinx.com/
|
||||
- TCL 命令参考:UG835
|
||||
- 设计流程参考:UG892
|
||||
|
||||
### B. 术语表
|
||||
|
||||
- **RTL**:Register Transfer Level,寄存器传输级
|
||||
- **综合**:Synthesis,将 RTL 代码转换为门级网表
|
||||
- **实现**:Implementation,布局布线
|
||||
- **比特流**:Bitstream,FPGA 配置文件
|
||||
- **DCP**:Design Checkpoint,Vivado 设计检查点文件
|
||||
- **XDC**:Xilinx Design Constraints,约束文件
|
||||
- **LUT**:Look-Up Table,查找表(FPGA 基本逻辑单元)
|
||||
- **FF**:Flip-Flop,触发器
|
||||
@ -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...**
|
||||
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)
|
||||
166
docs/Vivado联动前后端对接文档.md
Normal file
@ -0,0 +1,166 @@
|
||||
# Vivado 联动前后端对接文档
|
||||
|
||||
## 1. 前端提供的工具
|
||||
|
||||
前端提供 4 个独立工具,每个工具执行一个 Vivado 命令。
|
||||
|
||||
### 1.1 createVivadoProject - 创建工程
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
projectName: string; // 项目名称
|
||||
part: string; // 芯片型号(如 xc7a35tcpg236-1)
|
||||
topModule: string; // 顶层模块名
|
||||
files: string[]; // 源文件路径列表
|
||||
constraints?: string; // 约束文件路径(可选)
|
||||
mode: 'gui' | 'batch'; // gui=打开图形界面,batch=后台执行
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean; // 是否成功
|
||||
command: "create_project";
|
||||
executionTime: number; // 执行时间(毫秒)
|
||||
output: string; // 完整日志
|
||||
error?: string; // 失败原因(如果失败)
|
||||
outputFiles?: string[]; // 产出的工程文件路径
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 runVivadoSynthesis - 综合
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
projectPath?: string; // 工程路径(可选)
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files?: string[]; // 源文件(如果没有工程)
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "synthesis";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // .dcp 文件等
|
||||
reports?: {
|
||||
resources?: string; // 资源使用摘要
|
||||
timing?: string; // 时序摘要
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 runVivadoImplementation - 实现
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "implementation";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // 实现后的 .dcp 文件等
|
||||
reports?: {
|
||||
resources?: string;
|
||||
timing?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 runVivadoBitstream - 生成比特流
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "bitstream";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // .bit 文件路径
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 前端职责
|
||||
|
||||
- 接收后端工具调用
|
||||
- 生成对应的 TCL 脚本
|
||||
- 执行 Vivado 命令
|
||||
- 捕获输出日志
|
||||
- 解析报告文件(提取资源和时序摘要)
|
||||
- 返回执行结果
|
||||
|
||||
**前端不做**:
|
||||
- 不检查依赖关系
|
||||
- 不验证执行顺序
|
||||
- 不控制流程
|
||||
|
||||
## 3. 后端职责
|
||||
|
||||
- 询问用户参数(芯片型号、执行模式等)
|
||||
- 理解依赖关系(创建工程 → 综合 → 实现 → 生成比特流)
|
||||
- 按正确顺序调用工具
|
||||
- 检查每步的 `success` 字段
|
||||
- 如果失败,读取 `error` 字段并提示用户
|
||||
- 汇总结果展示给用户
|
||||
|
||||
## 4. 调用示例
|
||||
|
||||
```
|
||||
用户: 用 Vivado 做综合
|
||||
|
||||
后端:
|
||||
1. 询问芯片型号 → xc7a35tcpg236-1
|
||||
2. 询问执行模式 → batch
|
||||
|
||||
3. 调用 createVivadoProject(...)
|
||||
返回: { success: true, outputFiles: ["counter.xpr"] }
|
||||
|
||||
4. 调用 runVivadoSynthesis(...)
|
||||
返回: { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||
|
||||
5. 展示结果给用户
|
||||
```
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
如果某步失败:
|
||||
```typescript
|
||||
{
|
||||
success: false,
|
||||
error: "ERROR: [Synth 8-439] module 'counter' not found",
|
||||
output: "详细日志..."
|
||||
}
|
||||
```
|
||||
|
||||
后端应该:
|
||||
1. 停止后续步骤
|
||||
2. 提取 `error` 字段
|
||||
3. 给用户提示和建议
|
||||
153
docs/Vivado联动功能技术设计文档.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Vivado 联动功能技术设计文档
|
||||
|
||||
## 1. 架构设计
|
||||
|
||||
```
|
||||
后端 AI
|
||||
↓ 调用工具
|
||||
前端 Extension (messageHandler.ts)
|
||||
↓ 调用
|
||||
VivadoRunner (utils/vivadoRunner.ts)
|
||||
↓ 生成 TCL 脚本并执行
|
||||
本地 Vivado
|
||||
```
|
||||
|
||||
## 2. 核心模块
|
||||
|
||||
### 2.1 VivadoRunner
|
||||
|
||||
**职责**:执行单个 Vivado 命令
|
||||
|
||||
**主要方法**:
|
||||
- `createProject()` - 创建工程
|
||||
- `runSynthesis()` - 执行综合
|
||||
- `runImplementation()` - 执行实现
|
||||
- `runBitstream()` - 生成比特流
|
||||
|
||||
**实现要点**:
|
||||
- 根据参数生成 TCL 脚本
|
||||
- 启动子进程执行 Vivado
|
||||
- 捕获输出日志
|
||||
- 解析报告文件
|
||||
- 返回结果
|
||||
|
||||
### 2.2 TCL 脚本生成
|
||||
|
||||
**创建工程**:
|
||||
```tcl
|
||||
create_project {projectName} {workDir} -part {part} -force
|
||||
add_files -norecurse {files}
|
||||
set_property top {topModule} [current_fileset]
|
||||
```
|
||||
|
||||
**综合**:
|
||||
```tcl
|
||||
synth_design -part {part} -top {topModule}
|
||||
report_utilization -file utilization.rpt
|
||||
write_checkpoint -force synth.dcp
|
||||
```
|
||||
|
||||
**实现**:
|
||||
```tcl
|
||||
open_checkpoint {dcpFile}
|
||||
opt_design
|
||||
place_design
|
||||
route_design
|
||||
report_timing_summary -file timing.rpt
|
||||
write_checkpoint -force impl.dcp
|
||||
```
|
||||
|
||||
**生成比特流**:
|
||||
```tcl
|
||||
open_checkpoint {dcpFile}
|
||||
write_bitstream -force output.bit
|
||||
```
|
||||
|
||||
### 2.3 MessageHandler 集成
|
||||
|
||||
```typescript
|
||||
// 处理工具调用
|
||||
async function handleToolCall(toolName: string, params: any) {
|
||||
switch(toolName) {
|
||||
case 'createVivadoProject':
|
||||
return await vivadoRunner.createProject(params);
|
||||
case 'runVivadoSynthesis':
|
||||
return await vivadoRunner.runSynthesis(params);
|
||||
case 'runVivadoImplementation':
|
||||
return await vivadoRunner.runImplementation(params);
|
||||
case 'runVivadoBitstream':
|
||||
return await vivadoRunner.runBitstream(params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 配置管理
|
||||
|
||||
**配置文件位置**:
|
||||
- 全局:`settings.json` 中的 `ic-coder.vivado`
|
||||
- 项目:`.vscode/ic-coder-vivado.json`
|
||||
|
||||
**配置项**:
|
||||
```json
|
||||
{
|
||||
"vivado": {
|
||||
"enabled": true,
|
||||
"executablePath": "C:/Xilinx/Vivado/2023.1/bin/vivado.bat",
|
||||
"workingDir": "${workspaceFolder}/vivado_project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── utils/
|
||||
│ ├── vivadoRunner.ts # Vivado 执行器
|
||||
│ ├── vivadoConfig.ts # 配置读取
|
||||
│ └── tclGenerator.ts # TCL 脚本生成
|
||||
└── utils/
|
||||
└── messageHandler.ts # 工具调用处理(新增部分)
|
||||
```
|
||||
|
||||
## 5. 实现要点
|
||||
|
||||
### 5.1 子进程执行
|
||||
|
||||
```typescript
|
||||
const process = spawn(vivadoPath, ['-mode', 'batch', '-source', tclPath]);
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
resolve({ success: code === 0, output });
|
||||
});
|
||||
```
|
||||
|
||||
### 5.2 报告解析
|
||||
|
||||
从 `.rpt` 文件中提取关键信息:
|
||||
- 资源使用:LUT、FF、BRAM 数量
|
||||
- 时序信息:WNS、TNS
|
||||
|
||||
### 5.3 错误处理
|
||||
|
||||
捕获常见错误:
|
||||
- Vivado 未配置
|
||||
- 文件不存在
|
||||
- 综合/实现失败
|
||||
- 时序不满足
|
||||
|
||||
返回清晰的错误信息给后端。
|
||||
|
||||
## 6. 测试
|
||||
|
||||
**单元测试**:
|
||||
- TCL 脚本生成正确性
|
||||
- 配置读取
|
||||
|
||||
**集成测试**:
|
||||
- 完整流程测试(需要本地 Vivado)
|
||||
- 错误处理测试
|
||||
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. 处理前端的采纳/拒绝响应
|
||||
42
docs/code-to-chat-feature.md
Normal file
@ -0,0 +1,42 @@
|
||||
# 代码快速添加到对话功能
|
||||
|
||||
## 功能说明
|
||||
|
||||
选中代码后,通过右键菜单/小灯泡/快捷键(Ctrl+Shift+I),将代码作为上下文添加到聊天面板输入框上方。
|
||||
|
||||
## 实现方式
|
||||
|
||||
### 1. Code Action Provider
|
||||
`src/providers/codeActionProvider.ts` - 提供小灯泡菜单选项
|
||||
|
||||
### 2. 命令注册
|
||||
`src/extension.ts` - 注册 `ic-coder.addCodeToChat` 命令,发送消息到 webview
|
||||
|
||||
### 3. 全局引用
|
||||
`src/panels/ICHelperPanel.ts` - 保存 panel 到 `(global as any).currentICHelperPanel`
|
||||
|
||||
### 4. 上下文显示
|
||||
`src/views/contextDisplay.ts` - 添加 `code` 类型支持和 `addCodeContext` 消息处理
|
||||
|
||||
### 5. 配置
|
||||
`package.json` - 配置命令、右键菜单、快捷键
|
||||
|
||||
## 用户体验
|
||||
|
||||
1. 选中代码
|
||||
2. 右键/小灯泡/Ctrl+Shift+I
|
||||
3. 代码显示为上下文项:`文件名.v:10-25` 📄
|
||||
4. 输入问题发送(代码自动作为上下文)
|
||||
|
||||
## 数据结构
|
||||
|
||||
代码上下文存储为 JSON:
|
||||
```json
|
||||
{
|
||||
"fileName": "路径",
|
||||
"startLine": 10,
|
||||
"endLine": 25,
|
||||
"code": "代码内容",
|
||||
"languageId": "verilog"
|
||||
}
|
||||
```
|
||||
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` - 侧边栏加载时检查
|
||||
|
||||
55
docs/webpack-optimization.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Webpack 打包优化说明
|
||||
|
||||
## 优化内容
|
||||
|
||||
### 1. 自动模式切换
|
||||
- 开发模式:保持源码可读性
|
||||
- 生产模式:自动压缩代码
|
||||
|
||||
### 2. 性能优化
|
||||
- **Tree Shaking**:移除未使用的代码
|
||||
- **transpileOnly**:跳过类型检查,加快编译速度
|
||||
- **自动清理**:每次打包自动删除旧文件
|
||||
|
||||
### 3. 体积监控
|
||||
- 单文件超过 2MB 会发出警告
|
||||
- 帮助及时发现打包体积问题
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 开发模式
|
||||
```bash
|
||||
# 编译(不压缩)
|
||||
pnpm run compile
|
||||
|
||||
# 监听模式(自动重新编译)
|
||||
pnpm run watch
|
||||
```
|
||||
|
||||
### 生产模式
|
||||
```bash
|
||||
# Windows
|
||||
set NODE_ENV=production && pnpm run package
|
||||
|
||||
# macOS/Linux
|
||||
NODE_ENV=production pnpm run package
|
||||
```
|
||||
|
||||
## 打包结果
|
||||
|
||||
- **输出目录**:`dist/`
|
||||
- **入口文件**:`dist/extension.js`
|
||||
- **静态资源**:`dist/assets/`
|
||||
|
||||
## 性能对比
|
||||
|
||||
| 模式 | 体积 | 编译速度 | Source Map |
|
||||
|------|------|----------|------------|
|
||||
| 开发 | 较大 | 快 | 完整 |
|
||||
| 生产 | 小 | 较慢 | 隐藏 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 开发时使用 `pnpm run watch`,修改代码自动重新编译
|
||||
2. 发布前必须使用生产模式打包
|
||||
3. 如果打包体积超过 2MB,检查是否引入了不必要的依赖
|
||||
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 |
72
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.12",
|
||||
"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,33 @@
|
||||
"command": "ic-coder.openVCDViewer",
|
||||
"title": "打开 VCD 波形查看器",
|
||||
"category": "IC Coder"
|
||||
},
|
||||
{
|
||||
"command": "ic-coder.testNotification",
|
||||
"title": "测试系统通知",
|
||||
"category": "IC Coder"
|
||||
},
|
||||
{
|
||||
"command": "ic-coder.addCodeToChat",
|
||||
"title": "添加到 IC Coder 对话",
|
||||
"category": "IC Coder"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"editor/context": [
|
||||
{
|
||||
"command": "ic-coder.addCodeToChat",
|
||||
"when": "editorHasSelection",
|
||||
"group": "9_cutcopypaste"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "ic-coder.addCodeToChat",
|
||||
"key": "ctrl+l",
|
||||
"mac": "cmd+l",
|
||||
"when": "editorTextFocus && editorHasSelection"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
@ -82,7 +113,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 +152,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 +165,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 |
BIN
src/assets/titleIcon/PRO+.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
src/assets/titleIcon/PRO.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/titleIcon/free.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
@ -8,7 +8,7 @@ import * as vscode from "vscode";
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "dev";
|
||||
const CURRENT_ENV: Environment = "prod";
|
||||
|
||||
/** 服务等级类型 */
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
@ -19,6 +19,8 @@ export interface IccoderConfig {
|
||||
backendUrl: string;
|
||||
/** 登录页面地址 */
|
||||
loginUrl: string;
|
||||
/** 后端服务地址(strangeLoop) */
|
||||
backendUrlStrongeLoop: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
timeout: number;
|
||||
/** 用户ID(临时使用,后续对接认证) */
|
||||
@ -29,25 +31,28 @@ export interface IccoderConfig {
|
||||
|
||||
/** 环境配置 */
|
||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 */
|
||||
/** 本地开发环境 - 通过 Gateway 路由 */
|
||||
dev: {
|
||||
backendUrl: "http://localhost:2233",
|
||||
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://192.168.1.115:2029",
|
||||
loginUrl: "https://iccoder.com/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
@ -80,3 +85,15 @@ export function getApiUrl(path: string): string {
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 StrangeLoop 服务 API 地址(用于用户信息等)
|
||||
*/
|
||||
export function getStrangeLoopApiUrl(path: string): string {
|
||||
const { backendUrlStrongeLoop } = getConfig();
|
||||
const baseUrl = backendUrlStrongeLoop.endsWith("/")
|
||||
? backendUrlStrongeLoop.slice(0, -1)
|
||||
: backendUrlStrongeLoop;
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
|
||||
286
src/extension.ts
@ -5,12 +5,85 @@ import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel
|
||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||
import { VCDFileServer } from "./services/vcdFileServer";
|
||||
import { initUserService } from "./services/userService";
|
||||
import { initCreditsService } from "./services/creditsService";
|
||||
import { isTokenExpired } from "./utils/jwtUtils";
|
||||
import { NotificationService } from "./services/notificationService";
|
||||
import { InvitationService } from "./services/invitationService";
|
||||
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 创建装饰类型(代码旁边的提示)
|
||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||
after: {
|
||||
contentText: ' Ctrl+L 添加到 IC Coder 对话',
|
||||
color: '#888',
|
||||
fontStyle: 'italic',
|
||||
margin: '0 0 0 1em'
|
||||
}
|
||||
});
|
||||
|
||||
// 更新装饰
|
||||
const updateDecorations = () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
if (!editor.selection.isEmpty) {
|
||||
// 找到选区末尾所在的行,并将提示放在该行的末尾
|
||||
const { anchor, active } = editor.selection;
|
||||
const endPos = anchor.isAfter(active) ? anchor : active;
|
||||
const lineEndPos = editor.document.lineAt(endPos.line).range.end;
|
||||
const range = new vscode.Range(lineEndPos, lineEndPos);
|
||||
const decoration = { range };
|
||||
editor.setDecorations(decorationType, [decoration]);
|
||||
} else {
|
||||
editor.setDecorations(decorationType, []);
|
||||
}
|
||||
};
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeTextEditorSelection(updateDecorations),
|
||||
vscode.window.onDidChangeActiveTextEditor(updateDecorations)
|
||||
);
|
||||
|
||||
updateDecorations();
|
||||
|
||||
// 初始化通知服务
|
||||
const notificationService = NotificationService.getInstance(context);
|
||||
console.log('[Extension] 通知服务已初始化');
|
||||
|
||||
// 【关键】在创建 AuthProvider 之前,先检查并清除过期的 session
|
||||
const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
|
||||
console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
|
||||
|
||||
if (storedSessions.length > 0) {
|
||||
const session = storedSessions[0];
|
||||
const token = session.accessToken;
|
||||
console.log('[Extension] 检查 token 是否过期...');
|
||||
|
||||
if (token) {
|
||||
const expired = isTokenExpired(token);
|
||||
console.log('[Extension] token 过期检查结果:', expired);
|
||||
|
||||
if (expired) {
|
||||
// 必须等待清除完成后再创建 AuthProvider
|
||||
await context.globalState.update('icCoderSessions', []);
|
||||
await context.globalState.update('icCoderUserInfo', undefined);
|
||||
console.log('[Extension] Token 已过期,已清除所有登录状态');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化用户服务
|
||||
initUserService(context);
|
||||
|
||||
// 初始化 Credits 服务
|
||||
initCreditsService(context);
|
||||
|
||||
// 初始化 VCD 文件服务器
|
||||
const vcdFileServer = new VCDFileServer();
|
||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||
vcdFileServer.start().then((port) => {
|
||||
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
||||
}).catch((error) => {
|
||||
@ -22,7 +95,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(
|
||||
@ -86,12 +159,68 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:在浏览器中打开 VCD 波形查看器
|
||||
const openVCDViewerInBrowserCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.openVCDViewerInBrowser",
|
||||
async (vcdFilePath?: string) => {
|
||||
if (!vcdFilePath) {
|
||||
const fileUri = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
filters: {
|
||||
"VCD 文件": ["vcd"],
|
||||
"所有文件": ["*"],
|
||||
},
|
||||
title: "选择 VCD 文件",
|
||||
});
|
||||
|
||||
if (fileUri && fileUri[0]) {
|
||||
vcdFilePath = fileUri[0].fsPath;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册文件到服务器
|
||||
const fileId = vcdFileServer.registerFile(vcdFilePath);
|
||||
const viewerUrl = vcdFileServer.getViewerUrl(fileId);
|
||||
|
||||
// 在默认浏览器中打开
|
||||
vscode.env.openExternal(vscode.Uri.parse(viewerUrl));
|
||||
vscode.window.showInformationMessage(`波形查看器已在浏览器中打开`);
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:用户登录
|
||||
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}`);
|
||||
}
|
||||
@ -105,12 +234,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("当前未登录");
|
||||
}
|
||||
@ -120,6 +247,120 @@ 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] 测试通知命令执行完成');
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:将选中代码添加到对话
|
||||
const addCodeToChat = vscode.commands.registerCommand(
|
||||
"ic-coder.addCodeToChat",
|
||||
async () => {
|
||||
console.log('[addCodeToChat] 命令触发');
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
console.log('[addCodeToChat] 没有活动编辑器');
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = editor.selection;
|
||||
const selectedText = editor.document.getText(selection);
|
||||
|
||||
if (!selectedText) {
|
||||
vscode.window.showWarningMessage("请先选择代码");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = editor.document.fileName;
|
||||
const startLine = selection.start.line + 1;
|
||||
const endLine = selection.end.line + 1;
|
||||
|
||||
// 检查是否已有打开的面板
|
||||
let panel = (global as any).currentICHelperPanel;
|
||||
let needCreatePanel = false;
|
||||
|
||||
if (!panel) {
|
||||
needCreatePanel = true;
|
||||
} else {
|
||||
// 尝试访问 webview,如果抛出异常说明已销毁
|
||||
try {
|
||||
const _ = panel.webview;
|
||||
} catch (e) {
|
||||
needCreatePanel = true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[addCodeToChat] 需要创建面板:', needCreatePanel);
|
||||
|
||||
if (needCreatePanel) {
|
||||
console.log('[addCodeToChat] 正在打开面板...');
|
||||
await showICHelperPanel(context);
|
||||
panel = (global as any).currentICHelperPanel;
|
||||
console.log('[addCodeToChat] 面板打开后状态:', panel ? '成功' : '失败');
|
||||
|
||||
// 如果面板仍未创建(如未登录),直接返回
|
||||
if (!panel) {
|
||||
console.log('[addCodeToChat] 面板创建失败,退出');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送代码上下文
|
||||
console.log('[addCodeToChat] 准备发送代码到面板');
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (panel?.webview) {
|
||||
console.log('[addCodeToChat] 发送 addCodeContext 消息');
|
||||
panel.webview.postMessage({
|
||||
command: 'addCodeContext',
|
||||
fileName,
|
||||
startLine,
|
||||
endLine,
|
||||
code: selectedText,
|
||||
languageId: editor.document.languageId
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[addCodeToChat] 发送消息失败:', e);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:查看会话历史
|
||||
// TODO: 这些命令需要根据新的任务架构重新实现
|
||||
// 暂时注释掉,等待重新实现
|
||||
@ -171,19 +412,37 @@ 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 自定义编辑器
|
||||
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
|
||||
|
||||
// 注册 Code Action Provider
|
||||
const codeActionProvider = vscode.languages.registerCodeActionsProvider(
|
||||
{ scheme: 'file' },
|
||||
new ICCoderCodeActionProvider(),
|
||||
{ providedCodeActionKinds: [vscode.CodeActionKind.RefactorRewrite] }
|
||||
);
|
||||
|
||||
// 添加到订阅
|
||||
context.subscriptions.push(
|
||||
openPanelCommand,
|
||||
openChatCommand,
|
||||
openVCDViewerCommand,
|
||||
openVCDViewerInBrowserCommand,
|
||||
loginCommand,
|
||||
logoutCommand,
|
||||
changeInvitationCodeCommand,
|
||||
testNotificationCommand,
|
||||
addCodeToChat,
|
||||
// testTrialUserCommand,
|
||||
// testExpiredUserCommand,
|
||||
// TODO: 等待重新实现这些命令
|
||||
// viewHistoryCommand,
|
||||
// newSessionCommand,
|
||||
@ -192,7 +451,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
// clearHistoryCommand,
|
||||
// searchSessionCommand,
|
||||
viewRegistration,
|
||||
vcdEditorProvider
|
||||
vcdEditorProvider,
|
||||
codeActionProvider
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -1,420 +1,149 @@
|
||||
/**
|
||||
* IC 助手面板主组件
|
||||
* 功能:创建和管理 IC Coder 聊天面板
|
||||
* 依赖:vscode, webviewContent, helpers
|
||||
* 使用场景:用户打开 IC Coder 聊天界面
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { getWebviewContent } from "../views/webviewContent";
|
||||
import {
|
||||
handleUserMessage,
|
||||
insertCodeToEditor,
|
||||
handleReadFile,
|
||||
handleUpdateFile,
|
||||
handleRenameFile,
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handlePlanAction,
|
||||
setPendingPlanExecution,
|
||||
getCurrentTaskId,
|
||||
setLastTaskId,
|
||||
} from "../utils/messageHandler";
|
||||
import { compactDialog } from "../services/apiClient";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { MessageType } from "../types/chatHistory";
|
||||
import { checkAuthAndPromptLogin } from "./helpers/authHelper";
|
||||
import {
|
||||
sendUserInfoToWebview,
|
||||
setupBalanceUpdateCallback,
|
||||
} from "./helpers/userInfoHelper";
|
||||
import { handleWebviewMessage } from "./helpers/messageRouter";
|
||||
|
||||
function getIconUris(
|
||||
webview: vscode.Webview,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
return {
|
||||
icon: webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png"),
|
||||
),
|
||||
auto: webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Auto.png",
|
||||
),
|
||||
),
|
||||
lite: webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"lite.png",
|
||||
),
|
||||
),
|
||||
sy: webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Sy.png",
|
||||
),
|
||||
),
|
||||
max: webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Max.png",
|
||||
),
|
||||
),
|
||||
qrCode: webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"dist",
|
||||
"assets",
|
||||
"QRCode",
|
||||
"wx.png",
|
||||
),
|
||||
),
|
||||
logo: webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并显示 IC 助手面板
|
||||
*/
|
||||
export async function showICHelperPanel(
|
||||
context: vscode.ExtensionContext,
|
||||
viewColumn?: vscode.ViewColumn
|
||||
viewColumn?: vscode.ViewColumn,
|
||||
) {
|
||||
// 检查用户是否已登录
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (!session) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
if (!(await checkAuthAndPromptLogin(context))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建WebView面板
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"icCoder", // 面板ID
|
||||
"IC Coder", // 面板标题
|
||||
viewColumn || vscode.ViewColumn.Beside, // 默认显示在旁边,但可以指定
|
||||
"icCoder",
|
||||
"IC Coder",
|
||||
viewColumn || vscode.ViewColumn.Beside,
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||
vscode.Uri.joinPath(context.extensionUri, "dist", "assets"),
|
||||
],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 为面板生成唯一ID
|
||||
(global as any).currentICHelperPanel = panel;
|
||||
|
||||
const panelId = `panel_${Date.now()}_${Math.random()
|
||||
.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")
|
||||
);
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
const icons = getIconUris(panel.webview, context);
|
||||
panel.webview.html = getWebviewContent(
|
||||
iconUri.toString(),
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString()
|
||||
icons.icon.toString(),
|
||||
icons.auto.toString(),
|
||||
icons.lite.toString(),
|
||||
icons.sy.toString(),
|
||||
icons.max.toString(),
|
||||
icons.qrCode.toString(),
|
||||
icons.logo.toString(),
|
||||
);
|
||||
|
||||
// 处理消息
|
||||
await sendUserInfoToWebview(panel, context);
|
||||
setupBalanceUpdateCallback(panel, context);
|
||||
|
||||
const pendingMessage = context.globalState.get("pendingMessage") as any;
|
||||
if (pendingMessage) {
|
||||
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) => {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
// 仅在用户发送消息时,确保面板有任务上下文
|
||||
// 如果没有,则创建新任务(仅在首次发送消息时)
|
||||
if (!historyManager.getPanelTask(panelId)) {
|
||||
const workspacePath =
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (workspacePath) {
|
||||
try {
|
||||
const taskMeta = await historyManager.createTask(
|
||||
workspacePath,
|
||||
"新对话"
|
||||
);
|
||||
historyManager.setPanelTask(
|
||||
panelId,
|
||||
taskMeta.taskId,
|
||||
workspacePath
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("创建任务失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到当前面板的任务上下文
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
|
||||
// 显示进度条
|
||||
panel.webview.postMessage({ type: 'showProgress' });
|
||||
|
||||
handleUserMessage(
|
||||
panel,
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode,
|
||||
message.model // 传递服务等级
|
||||
);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
break;
|
||||
case "updateFile":
|
||||
handleUpdateFile(panel, message.filePath, message.content);
|
||||
break;
|
||||
case "renameFile":
|
||||
handleRenameFile(panel, message.oldPath, message.newPath);
|
||||
break;
|
||||
case "replaceInFile":
|
||||
handleReplaceInFile(
|
||||
panel,
|
||||
message.filePath,
|
||||
message.searchText,
|
||||
message.replaceText
|
||||
);
|
||||
break;
|
||||
case "insertCode":
|
||||
insertCodeToEditor(message.code);
|
||||
break;
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "openWaveformViewer":
|
||||
// 打开波形查看器 - 使用 vscode.open 触发自定义编辑器
|
||||
if (message.vcdFilePath) {
|
||||
const vcdUri = vscode.Uri.file(message.vcdFilePath);
|
||||
vscode.commands.executeCommand('vscode.open', vcdUri);
|
||||
}
|
||||
break;
|
||||
case "getVCDInfo":
|
||||
// 获取 VCD 文件信息
|
||||
if (message.vcdFilePath && message.containerId) {
|
||||
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
|
||||
}
|
||||
break;
|
||||
case "createNewConversation":
|
||||
// 创建新会话 - 在当前编辑器组中打开新标签页
|
||||
showICHelperPanel(context, panel.viewColumn);
|
||||
break;
|
||||
case "loadConversationHistory":
|
||||
// 加载会话历史(支持分页)
|
||||
loadConversationHistory(
|
||||
panel,
|
||||
message.offset || 0,
|
||||
message.limit || 10
|
||||
);
|
||||
break;
|
||||
case "selectConversation":
|
||||
// 选择会话
|
||||
if (message.conversationId) {
|
||||
selectConversation(
|
||||
panel,
|
||||
message.conversationId,
|
||||
context.extensionPath
|
||||
);
|
||||
}
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput
|
||||
);
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
// 新增:压缩会话
|
||||
case "compressConversation":
|
||||
{
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
compactDialog(taskId)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "✅ 会话压缩完成",
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${result.error || "未知错误"}`,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "❌ 没有活跃的会话",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
// 确认执行:切换到 Agent 模式
|
||||
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,知识图谱数据可能丢失"
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 添加文件上下文 - 显示工作区文件列表
|
||||
case "addContextFile":
|
||||
{
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
break;
|
||||
}
|
||||
|
||||
// 获取工作区所有文件
|
||||
const files = await vscode.workspace.findFiles(
|
||||
"**/*",
|
||||
"**/node_modules/**"
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFileList",
|
||||
files: files.map((uri) => ({
|
||||
path: uri.fsPath,
|
||||
relativePath: vscode.workspace.asRelativePath(uri),
|
||||
})),
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 添加文件夹上下文 - 显示工作区文件夹列表
|
||||
case "addContextFolder":
|
||||
{
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
break;
|
||||
}
|
||||
|
||||
// 获取工作区所有文件夹
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const folders: Array<{ path: string; relativePath: string }> = [];
|
||||
|
||||
function scanFolders(dir: string, baseDir: string) {
|
||||
try {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const item of items) {
|
||||
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
|
||||
const fullPath = path.join(dir, item.name);
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
folders.push({ path: fullPath, relativePath });
|
||||
scanFolders(fullPath, baseDir);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("扫描文件夹失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFolderList",
|
||||
folders: folders,
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 添加图片上下文
|
||||
case "addContextImage":
|
||||
{
|
||||
const imageUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择图片",
|
||||
filters: {
|
||||
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||
},
|
||||
});
|
||||
if (imageUris && imageUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextImagesSelected",
|
||||
images: imageUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 添加文档库上下文
|
||||
case "addContextDocument":
|
||||
{
|
||||
const docUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择文档",
|
||||
filters: {
|
||||
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
|
||||
"所有文件": ["*"],
|
||||
},
|
||||
});
|
||||
if (docUris && docUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextDocumentsSelected",
|
||||
documents: docUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 新增:检查工作区状态
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
vscode.workspace.workspaceFolders &&
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
);
|
||||
if (!hasWorkspace) {
|
||||
// 弹窗提示用户需要打开工作区
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||
"打开文件夹"
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "打开文件夹") {
|
||||
vscode.commands.executeCommand("vscode.openFolder");
|
||||
}
|
||||
});
|
||||
}
|
||||
// 返回工作区状态给前端
|
||||
panel.webview.postMessage({
|
||||
command: "workspaceStatus",
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
break;
|
||||
}
|
||||
await handleWebviewMessage(message, panel, context);
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
context.subscriptions,
|
||||
);
|
||||
|
||||
// 面板关闭时清理任务映射
|
||||
panel.onDidDispose(
|
||||
() => {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
@ -422,428 +151,6 @@ export async function showICHelperPanel(
|
||||
historyManager.removePanelTask(panelId);
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
context.subscriptions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 VCD 文件信息
|
||||
*/
|
||||
async function getVCDFileInfo(
|
||||
panel: vscode.WebviewPanel,
|
||||
vcdFilePath: string,
|
||||
containerId: string
|
||||
) {
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: "文件不存在",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
const stats = fs.statSync(vcdFilePath);
|
||||
const fileSizeKB = stats.size / 1024;
|
||||
const fileSize =
|
||||
fileSizeKB < 1024
|
||||
? `${fileSizeKB.toFixed(2)} KB`
|
||||
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
|
||||
|
||||
// 读取 VCD 文件内容
|
||||
const content = fs.readFileSync(vcdFilePath, "utf-8");
|
||||
|
||||
// 解析信号数量
|
||||
const varMatches = content.match(/\$var/g);
|
||||
const signalCount = varMatches ? varMatches.length : 0;
|
||||
|
||||
// 解析时间范围
|
||||
let timeRange = "N/A";
|
||||
const timeMatch = content.match(/#(\d+)/g);
|
||||
if (timeMatch && timeMatch.length > 0) {
|
||||
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
|
||||
const minTime = Math.min(...times);
|
||||
const maxTime = Math.max(...times);
|
||||
timeRange = `${minTime} - ${maxTime}`;
|
||||
}
|
||||
|
||||
// 解析前几个信号的真实数据
|
||||
const signals = parseVCDSignals(content, 3); // 只解析前3个信号
|
||||
|
||||
// 发送信息回前端
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: signalCount.toString(),
|
||||
timeRange: timeRange,
|
||||
fileSize: fileSize,
|
||||
signals: signals, // 添加真实信号数据
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取 VCD 文件信息失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件中的信号数据
|
||||
*/
|
||||
function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
const signals: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
values: Array<{ time: number; value: string }>;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
// 1. 解析信号定义部分
|
||||
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
|
||||
let match;
|
||||
const signalDefs: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
}> = [];
|
||||
|
||||
while (
|
||||
(match = varRegex.exec(content)) !== null &&
|
||||
signalDefs.length < maxSignals
|
||||
) {
|
||||
const width = parseInt(match[2]);
|
||||
const identifier = match[3];
|
||||
const name = match[4].trim();
|
||||
|
||||
signalDefs.push({ name, identifier, width });
|
||||
}
|
||||
|
||||
// 2. 找到数据变化部分的起始位置
|
||||
const dumpvarsIndex = content.indexOf("$dumpvars");
|
||||
if (dumpvarsIndex === -1) {
|
||||
return signals;
|
||||
}
|
||||
|
||||
const dataSection = content.substring(dumpvarsIndex);
|
||||
|
||||
// 3. 解析每个信号的值变化
|
||||
for (const signalDef of signalDefs) {
|
||||
const values: Array<{ time: number; value: string }> = [];
|
||||
let currentTime = 0;
|
||||
|
||||
// 分行处理数据
|
||||
const lines = dataSection.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// 解析时间戳
|
||||
if (trimmedLine.startsWith("#")) {
|
||||
currentTime = parseInt(trimmedLine.substring(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析信号值变化
|
||||
// 格式1: 单比特信号 "0!" 或 "1!"
|
||||
// 格式2: 多比特信号 "b1010 !"
|
||||
if (signalDef.width === 1) {
|
||||
// 单比特信号
|
||||
const singleBitMatch = trimmedLine.match(
|
||||
new RegExp(`^([01xz])${signalDef.identifier}$`)
|
||||
);
|
||||
if (singleBitMatch) {
|
||||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||
}
|
||||
} else {
|
||||
// 多比特信号
|
||||
const multiBitMatch = trimmedLine.match(
|
||||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`)
|
||||
);
|
||||
if (multiBitMatch) {
|
||||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||
}
|
||||
}
|
||||
|
||||
// 限制采样点数量,避免数据过多
|
||||
if (values.length >= 50) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
signals.push({
|
||||
name: signalDef.name,
|
||||
identifier: signalDef.identifier,
|
||||
width: signalDef.width,
|
||||
values: values,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析 VCD 信号数据失败:", error);
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载会话历史(支持分页)
|
||||
*/
|
||||
async function loadConversationHistory(
|
||||
panel: vscode.WebviewPanel,
|
||||
offset: number = 0,
|
||||
limit: number = 10
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
// 没有打开的工作区,返回空历史
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取会话历史列表(支持分页)
|
||||
const result = await historyManager.getConversationHistoryList(
|
||||
workspacePath,
|
||||
offset,
|
||||
limit
|
||||
);
|
||||
|
||||
// 发送会话历史到前端
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: result.items,
|
||||
total: result.total,
|
||||
hasMore: result.hasMore,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载会话历史失败:", error);
|
||||
// 发生错误时返回空历史
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择并加载指定的会话
|
||||
*/
|
||||
async function selectConversation(
|
||||
panel: vscode.WebviewPanel,
|
||||
taskId: string,
|
||||
extensionPath: string
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
vscode.window.showErrorMessage("没有打开的工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载任务会话
|
||||
const taskSession = await historyManager.loadTaskSession(
|
||||
workspacePath,
|
||||
taskId
|
||||
);
|
||||
|
||||
if (!taskSession) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换到该任务
|
||||
const switched = await historyManager.switchTask(workspacePath, taskId);
|
||||
if (!switched) {
|
||||
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 lastTaskId,用于压缩等操作
|
||||
setLastTaskId(taskId);
|
||||
|
||||
// 更新面板的任务映射,确保后续对话保存到正确的任务中
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||
|
||||
// 清空当前聊天界面
|
||||
panel.webview.postMessage({
|
||||
command: "clearChat",
|
||||
});
|
||||
|
||||
// 将会话历史消息转换为 segments 格式并发送到前端显示
|
||||
const segments: any[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < taskSession.messages.length) {
|
||||
const message = taskSession.messages[i];
|
||||
|
||||
if (message.type === MessageType.USER) {
|
||||
// 用户消息 - 如果有累积的 segments,先发送
|
||||
if (segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: [...segments],
|
||||
});
|
||||
segments.length = 0;
|
||||
}
|
||||
|
||||
// 发送用户消息
|
||||
const textContent = message.contents?.find((c) => c.type === "TEXT");
|
||||
if (textContent && "text" in textContent) {
|
||||
panel.webview.postMessage({
|
||||
command: "addUserMessage",
|
||||
text: textContent.text,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
} else if (message.type === MessageType.AI) {
|
||||
// AI消息 - 如果有 segments,直接使用
|
||||
if (message.segments && message.segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: message.segments,
|
||||
});
|
||||
i++;
|
||||
} else {
|
||||
// 旧格式:需要转换为 segments
|
||||
// 收集连续的 AI 消息、工具调用和工具结果
|
||||
if (message.text) {
|
||||
segments.push({
|
||||
type: "text",
|
||||
content: message.text,
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否有工具调用
|
||||
if (
|
||||
message.toolExecutionRequests &&
|
||||
message.toolExecutionRequests.length > 0
|
||||
) {
|
||||
for (const toolReq of message.toolExecutionRequests) {
|
||||
// 查找对应的工具执行结果
|
||||
let toolResult = "";
|
||||
if (i + 1 < taskSession.messages.length) {
|
||||
const nextMsg = taskSession.messages[i + 1];
|
||||
if (
|
||||
nextMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||
nextMsg.id === toolReq.id
|
||||
) {
|
||||
toolResult = nextMsg.text;
|
||||
i++; // 跳过工具结果消息
|
||||
}
|
||||
}
|
||||
|
||||
segments.push({
|
||||
type: "tool",
|
||||
toolName: toolReq.name,
|
||||
askId: toolReq.id,
|
||||
toolResult: toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
// 继续收集后续的 AI 消息,直到遇到用户消息或有 segments 的 AI 消息
|
||||
while (i < taskSession.messages.length) {
|
||||
const nextMsg = taskSession.messages[i];
|
||||
if (nextMsg.type === MessageType.USER) {
|
||||
break;
|
||||
}
|
||||
if (nextMsg.type === MessageType.AI) {
|
||||
if (nextMsg.segments && nextMsg.segments.length > 0) {
|
||||
break;
|
||||
}
|
||||
if (nextMsg.text) {
|
||||
segments.push({
|
||||
type: "text",
|
||||
content: nextMsg.text,
|
||||
});
|
||||
}
|
||||
if (
|
||||
nextMsg.toolExecutionRequests &&
|
||||
nextMsg.toolExecutionRequests.length > 0
|
||||
) {
|
||||
for (const toolReq of nextMsg.toolExecutionRequests) {
|
||||
let toolResult = "";
|
||||
if (i + 1 < taskSession.messages.length) {
|
||||
const resultMsg = taskSession.messages[i + 1];
|
||||
if (
|
||||
resultMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||
resultMsg.id === toolReq.id
|
||||
) {
|
||||
toolResult = resultMsg.text;
|
||||
i++; // 跳过工具结果消息
|
||||
}
|
||||
}
|
||||
segments.push({
|
||||
type: "tool",
|
||||
toolName: toolReq.name,
|
||||
askId: toolReq.id,
|
||||
toolResult: toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
i++;
|
||||
} else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) {
|
||||
// 独立的工具结果(没有被上面处理的)
|
||||
i++;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送剩余的 segments
|
||||
if (segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: segments,
|
||||
});
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`已加载会话: ${taskSession.meta.taskName}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("选择会话失败:", error);
|
||||
vscode.window.showErrorMessage(`加载会话失败: ${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,15 +111,20 @@ export class VCDViewerPanel {
|
||||
}
|
||||
},
|
||||
null,
|
||||
this._disposables
|
||||
this._disposables,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或显示 VCD 查看器面板
|
||||
*/
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||
const column = vscode.ViewColumn.One;
|
||||
public static createOrShow(
|
||||
extensionUri: vscode.Uri,
|
||||
vcdFilePath?: string,
|
||||
vcdFileServer?: VCDFileServer,
|
||||
) {
|
||||
// 在当前活动编辑器旁边打开新列
|
||||
const column = vscode.ViewColumn.Beside;
|
||||
|
||||
// 如果已经有面板打开,则显示它
|
||||
if (VCDViewerPanel.currentPanel) {
|
||||
@ -127,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) {
|
||||
@ -145,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);
|
||||
@ -171,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 : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -189,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;
|
||||
@ -200,7 +221,7 @@ export class VCDViewerPanel {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// 遇到 $enddefinitions 就停止解析
|
||||
if (trimmed.startsWith('$enddefinitions')) {
|
||||
if (trimmed.startsWith("$enddefinitions")) {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -211,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();
|
||||
@ -276,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 : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -351,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>
|
||||
@ -366,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/panels/helpers/authHelper.ts
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 认证辅助模块
|
||||
* 功能:处理用户登录状态检查和 token 验证
|
||||
* 依赖:vscode, jwtUtils
|
||||
* 使用场景:面板初始化时验证用户登录状态
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { isTokenExpired } from "../../utils/jwtUtils";
|
||||
|
||||
export async function checkAuthAndPromptLogin(
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<boolean> {
|
||||
let token: string | undefined;
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
token = session?.accessToken;
|
||||
} catch (error) {
|
||||
console.warn("[AuthHelper] 获取 session 失败:", error);
|
||||
}
|
||||
|
||||
if (token && isTokenExpired(token)) {
|
||||
await context.globalState.update("icCoderSessions", []);
|
||||
await context.globalState.update("icCoderUserInfo", undefined);
|
||||
const action = await vscode.window.showWarningMessage(
|
||||
"登录已过期,请重新登录",
|
||||
"立即登录",
|
||||
);
|
||||
if (action === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", { forceReauth: true });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (!session) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
104
src/panels/helpers/contextHelper.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 上下文管理模块
|
||||
* 功能:处理文件、文件夹、图片、文档上下文添加
|
||||
* 依赖:vscode, fs, path
|
||||
* 使用场景:用户添加上下文项时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function handleAddContextFile(panel: vscode.WebviewPanel) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await vscode.workspace.findFiles(
|
||||
"**/*",
|
||||
"**/node_modules/**",
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFileList",
|
||||
files: files.map((uri) => ({
|
||||
path: uri.fsPath,
|
||||
relativePath: vscode.workspace.asRelativePath(uri),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleAddContextFolder(panel: vscode.WebviewPanel) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const folders: Array<{ path: string; relativePath: string }> = [];
|
||||
|
||||
function scanFolders(dir: string, baseDir: string) {
|
||||
try {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const item of items) {
|
||||
if (
|
||||
item.isDirectory() &&
|
||||
item.name !== "node_modules" &&
|
||||
!item.name.startsWith(".")
|
||||
) {
|
||||
const fullPath = path.join(dir, item.name);
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
folders.push({ path: fullPath, relativePath });
|
||||
scanFolders(fullPath, baseDir);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("扫描文件夹失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFolderList",
|
||||
folders: folders,
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleAddContextImage(panel: vscode.WebviewPanel) {
|
||||
const imageUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择图片",
|
||||
filters: {
|
||||
图片文件: ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||
},
|
||||
});
|
||||
if (imageUris && imageUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextImagesSelected",
|
||||
images: imageUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleAddContextDocument(panel: vscode.WebviewPanel) {
|
||||
const docUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择文档",
|
||||
filters: {
|
||||
文档文件: ["pdf", "doc", "docx", "txt", "md"],
|
||||
所有文件: ["*"],
|
||||
},
|
||||
});
|
||||
if (docUris && docUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextDocumentsSelected",
|
||||
documents: docUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
224
src/panels/helpers/conversationHelper.ts
Normal file
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 会话历史管理模块
|
||||
* 功能:加载和选择会话历史
|
||||
* 依赖:vscode, chatHistoryManager, messageHandler
|
||||
* 使用场景:会话历史列表和切换
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||
import { MessageType } from "../../types/chatHistory";
|
||||
import { setLastTaskId } from "../../utils/messageHandler";
|
||||
|
||||
export async function loadConversationHistory(
|
||||
panel: vscode.WebviewPanel,
|
||||
offset: number = 0,
|
||||
limit: number = 10,
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await historyManager.getConversationHistoryList(
|
||||
workspacePath,
|
||||
offset,
|
||||
limit,
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: result.items,
|
||||
total: result.total,
|
||||
hasMore: result.hasMore,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载会话历史失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectConversation(
|
||||
panel: vscode.WebviewPanel,
|
||||
taskId: string,
|
||||
extensionPath: string,
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
vscode.window.showErrorMessage("没有打开的工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const taskSession = await historyManager.loadTaskSession(
|
||||
workspacePath,
|
||||
taskId,
|
||||
);
|
||||
|
||||
if (!taskSession) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const switched = await historyManager.switchTask(workspacePath, taskId);
|
||||
if (!switched) {
|
||||
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
|
||||
return;
|
||||
}
|
||||
|
||||
setLastTaskId(taskId);
|
||||
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||
|
||||
panel.webview.postMessage({ command: "clearChat" });
|
||||
|
||||
const segments: any[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < taskSession.messages.length) {
|
||||
const message = taskSession.messages[i];
|
||||
|
||||
if (message.type === MessageType.USER) {
|
||||
if (segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: [...segments],
|
||||
});
|
||||
segments.length = 0;
|
||||
}
|
||||
|
||||
const textContent = message.contents?.find((c) => c.type === "TEXT");
|
||||
if (textContent && "text" in textContent) {
|
||||
panel.webview.postMessage({
|
||||
command: "addUserMessage",
|
||||
text: textContent.text,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
} else if (message.type === MessageType.AI) {
|
||||
if (message.segments && message.segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: message.segments,
|
||||
});
|
||||
i++;
|
||||
} else {
|
||||
if (message.text) {
|
||||
segments.push({ type: "text", content: message.text });
|
||||
}
|
||||
|
||||
if (
|
||||
message.toolExecutionRequests &&
|
||||
message.toolExecutionRequests.length > 0
|
||||
) {
|
||||
for (const toolReq of message.toolExecutionRequests) {
|
||||
let toolResult = "";
|
||||
if (i + 1 < taskSession.messages.length) {
|
||||
const nextMsg = taskSession.messages[i + 1];
|
||||
if (
|
||||
nextMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||
nextMsg.id === toolReq.id
|
||||
) {
|
||||
toolResult = nextMsg.text;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
segments.push({
|
||||
type: "tool",
|
||||
toolName: toolReq.name,
|
||||
askId: toolReq.id,
|
||||
toolResult: toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
while (i < taskSession.messages.length) {
|
||||
const nextMsg = taskSession.messages[i];
|
||||
if (nextMsg.type === MessageType.USER) {
|
||||
break;
|
||||
}
|
||||
if (nextMsg.type === MessageType.AI) {
|
||||
if (nextMsg.segments && nextMsg.segments.length > 0) {
|
||||
break;
|
||||
}
|
||||
if (nextMsg.text) {
|
||||
segments.push({ type: "text", content: nextMsg.text });
|
||||
}
|
||||
if (
|
||||
nextMsg.toolExecutionRequests &&
|
||||
nextMsg.toolExecutionRequests.length > 0
|
||||
) {
|
||||
for (const toolReq of nextMsg.toolExecutionRequests) {
|
||||
let toolResult = "";
|
||||
if (i + 1 < taskSession.messages.length) {
|
||||
const resultMsg = taskSession.messages[i + 1];
|
||||
if (
|
||||
resultMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||
resultMsg.id === toolReq.id
|
||||
) {
|
||||
toolResult = resultMsg.text;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
segments.push({
|
||||
type: "tool",
|
||||
toolName: toolReq.name,
|
||||
askId: toolReq.id,
|
||||
toolResult: toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
i++;
|
||||
} else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) {
|
||||
i++;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: segments,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送任务完成消息(历史记录)
|
||||
panel.webview.postMessage({
|
||||
command: "taskCompleteHistory",
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`已加载会话: ${taskSession.meta.taskName}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("选择会话失败:", error);
|
||||
vscode.window.showErrorMessage(`加载会话失败: ${error}`);
|
||||
}
|
||||
}
|
||||
78
src/panels/helpers/fileHelper.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 文件操作辅助模块
|
||||
* 功能:处理文件打开、选择等操作
|
||||
* 依赖:vscode, fs, path
|
||||
* 使用场景:打开文件、跳转到代码位置
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function openFile(filePath: string) {
|
||||
const path = require("path");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const fullPath =
|
||||
path.isAbsolute(filePath) || !workspaceFolder
|
||||
? filePath
|
||||
: vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath;
|
||||
const doc = await vscode.workspace.openTextDocument(fullPath);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
}
|
||||
|
||||
export async function openFileWithSelection(
|
||||
filePath: string,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
) {
|
||||
const path = require("path");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const fullPath =
|
||||
path.isAbsolute(filePath) || !workspaceFolder
|
||||
? filePath
|
||||
: vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath;
|
||||
const doc = await vscode.workspace.openTextDocument(fullPath);
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
const start = new vscode.Position(startLine - 1, 0);
|
||||
const end = new vscode.Position(
|
||||
endLine - 1,
|
||||
doc.lineAt(endLine - 1).text.length,
|
||||
);
|
||||
editor.selection = new vscode.Selection(start, end);
|
||||
editor.revealRange(new vscode.Range(start, end));
|
||||
}
|
||||
|
||||
export async function openFilePathTag(
|
||||
filePath: string,
|
||||
startLine?: number,
|
||||
endLine?: number,
|
||||
) {
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
|
||||
let fullPath = filePath;
|
||||
|
||||
if (!path.isAbsolute(filePath) && workspaceFolder) {
|
||||
const candidatePath = vscode.Uri.joinPath(
|
||||
workspaceFolder.uri,
|
||||
filePath,
|
||||
).fsPath;
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
fullPath = candidatePath;
|
||||
} else {
|
||||
const fileName = path.basename(filePath);
|
||||
const files = await vscode.workspace.findFiles(
|
||||
`**/${fileName}`,
|
||||
"**/node_modules/**",
|
||||
1,
|
||||
);
|
||||
if (files.length > 0) {
|
||||
fullPath = files[0].fsPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startLine && endLine) {
|
||||
await openFileWithSelection(fullPath, startLine, endLine);
|
||||
} else {
|
||||
await openFile(fullPath);
|
||||
}
|
||||
}
|
||||
396
src/panels/helpers/messageRouter.ts
Normal file
@ -0,0 +1,396 @@
|
||||
/**
|
||||
* 消息路由处理模块
|
||||
* 功能:处理 webview 消息的路由分发
|
||||
* 依赖:各个 helper 模块和 messageHandler
|
||||
* 使用场景:webview 消息接收时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
handleUserMessage,
|
||||
insertCodeToEditor,
|
||||
handleReadFile,
|
||||
handleUpdateFile,
|
||||
handleRenameFile,
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
handlePlanAction,
|
||||
getCurrentTaskId,
|
||||
handleAcceptChange,
|
||||
handleRejectChange,
|
||||
handleOpenFileDiff,
|
||||
startChangeSession,
|
||||
} from "../../utils/messageHandler";
|
||||
import { compactDialog } from "../../services/apiClient";
|
||||
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||
import { getCachedUserInfo } from "../../services/userService";
|
||||
import { loadConversationHistory, selectConversation } from "./conversationHelper";
|
||||
import { getVCDFileInfo } from "./vcdHelper";
|
||||
import {
|
||||
handleAddContextFile,
|
||||
handleAddContextFolder,
|
||||
handleAddContextImage,
|
||||
handleAddContextDocument,
|
||||
} from "./contextHelper";
|
||||
import { openFile, openFileWithSelection, openFilePathTag } from "./fileHelper";
|
||||
|
||||
export async function handleWebviewMessage(
|
||||
message: any,
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
if (!historyManager.getPanelTask(panelId)) {
|
||||
const workspacePath =
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (workspacePath) {
|
||||
try {
|
||||
const taskMeta = await historyManager.createTask(
|
||||
workspacePath,
|
||||
"新对话",
|
||||
);
|
||||
historyManager.setPanelTask(
|
||||
panelId,
|
||||
taskMeta.taskId,
|
||||
workspacePath,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("创建任务失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
const sessionId = `session_${panelId}_${Date.now()}`;
|
||||
startChangeSession(sessionId);
|
||||
panel.webview.postMessage({ type: "showProgress" });
|
||||
|
||||
handleUserMessage(
|
||||
panel,
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode,
|
||||
message.model,
|
||||
message.contextItems,
|
||||
);
|
||||
break;
|
||||
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
break;
|
||||
|
||||
case "updateFile":
|
||||
handleUpdateFile(panel, message.filePath, message.content);
|
||||
break;
|
||||
|
||||
case "renameFile":
|
||||
handleRenameFile(panel, message.oldPath, message.newPath);
|
||||
break;
|
||||
|
||||
case "replaceInFile":
|
||||
handleReplaceInFile(
|
||||
panel,
|
||||
message.filePath,
|
||||
message.searchText,
|
||||
message.replaceText,
|
||||
);
|
||||
break;
|
||||
|
||||
case "insertCode":
|
||||
insertCodeToEditor(message.code);
|
||||
break;
|
||||
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
|
||||
case "openWaveformViewer":
|
||||
if (message.vcdFilePath) {
|
||||
vscode.commands.executeCommand(
|
||||
"ic-coder.openVCDViewer",
|
||||
message.vcdFilePath,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "getVCDInfo":
|
||||
if (message.vcdFilePath && message.containerId) {
|
||||
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "createNewConversation":
|
||||
const { showICHelperPanel } = require("../ICHelperPanel");
|
||||
showICHelperPanel(context, panel.viewColumn);
|
||||
break;
|
||||
|
||||
case "loadConversationHistory":
|
||||
loadConversationHistory(
|
||||
panel,
|
||||
message.offset || 0,
|
||||
message.limit || 10,
|
||||
);
|
||||
break;
|
||||
|
||||
case "selectConversation":
|
||||
if (message.conversationId) {
|
||||
selectConversation(panel, message.conversationId, context.extensionPath);
|
||||
}
|
||||
break;
|
||||
|
||||
case "submitAnswer":
|
||||
void handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput,
|
||||
message.answers,
|
||||
);
|
||||
break;
|
||||
|
||||
case "abortDialog":
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
|
||||
case "compressConversation":
|
||||
{
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
compactDialog(taskId)
|
||||
.then((result) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: result.success
|
||||
? "✅ 会话压缩完成"
|
||||
: `❌ 压缩失败: ${result.error || "未知错误"}`,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "❌ 没有活跃的会话",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "optimizePrompt":
|
||||
if (typeof message.prompt === "string") {
|
||||
void handleOptimizePrompt(panel, message.prompt);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: "提示词为空或格式错误",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "logout":
|
||||
vscode.commands.executeCommand("ic-coder.logout");
|
||||
break;
|
||||
|
||||
case "openFile":
|
||||
if (message.filePath) {
|
||||
await openFile(message.filePath);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFileWithSelection":
|
||||
if (message.filePath) {
|
||||
await openFileWithSelection(
|
||||
message.filePath,
|
||||
message.startLine,
|
||||
message.endLine,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFilePathTag":
|
||||
if (message.filePath) {
|
||||
await openFilePathTag(
|
||||
message.filePath,
|
||||
message.startLine,
|
||||
message.endLine,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "acceptChange":
|
||||
if (message.changeId) {
|
||||
await handleAcceptChange(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "rejectChange":
|
||||
if (message.changeId) {
|
||||
await handleRejectChange(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFileDiff":
|
||||
if (message.changeId) {
|
||||
await handleOpenFileDiff(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkInvitationCode":
|
||||
{
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (userInfo?.isPluginTrial === true) {
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeStatus",
|
||||
verified: true,
|
||||
});
|
||||
} else {
|
||||
const { InvitationService } = require("../../services/invitationService");
|
||||
const isVerified = await InvitationService.isVerified(context);
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeStatus",
|
||||
verified: isVerified,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkWelcomeModal":
|
||||
{
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (userInfo?.isPluginTrial === true) {
|
||||
if (userInfo.pluginTrialExpiresAt === undefined) {
|
||||
break;
|
||||
}
|
||||
if (userInfo.pluginTrialExpiresAt !== null) {
|
||||
const now = Date.now();
|
||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
if (isExpired) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
panel.webview.postMessage({ command: "showWelcomeModal" });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkTrialExpiration":
|
||||
{
|
||||
const { TrialExpirationService } = require("../../services/trialExpirationService");
|
||||
const trialService = new TrialExpirationService(context, panel);
|
||||
await trialService.checkExpiration();
|
||||
}
|
||||
break;
|
||||
|
||||
case "verifyInvitationCode":
|
||||
{
|
||||
const { InvitationService } = require("../../services/invitationService");
|
||||
const result = await InvitationService.verifyCode(message.code);
|
||||
|
||||
if (result.success) {
|
||||
await InvitationService.saveVerificationStatus(context, message.code);
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeVerified",
|
||||
success: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({ command: "showNdtWelcomeModal" });
|
||||
}, 300);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "invitationCodeVerified",
|
||||
success: false,
|
||||
message: result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "openICCoder":
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
|
||||
case "openTutorial":
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse(
|
||||
"https://www.iccoder.com/guides/quick-start/first-task-plugin",
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
||||
case "openUserManual":
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
|
||||
case "openUserFeedback":
|
||||
panel.webview.postMessage({ command: "showFeedbackQRCode" });
|
||||
break;
|
||||
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
panel.webview.postMessage({ command: "switchMode", mode: "agent" });
|
||||
} else if (message.action === "modify" || message.action === "cancel") {
|
||||
void handlePlanAction(
|
||||
panel,
|
||||
message.action,
|
||||
message.planTitle || "",
|
||||
context.extensionPath,
|
||||
message.model,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "addContextFile":
|
||||
await handleAddContextFile(panel);
|
||||
break;
|
||||
|
||||
case "addContextFolder":
|
||||
await handleAddContextFolder(panel);
|
||||
break;
|
||||
|
||||
case "addContextImage":
|
||||
await handleAddContextImage(panel);
|
||||
break;
|
||||
|
||||
case "addContextDocument":
|
||||
await handleAddContextDocument(panel);
|
||||
break;
|
||||
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
vscode.workspace.workspaceFolders &&
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
);
|
||||
if (!hasWorkspace) {
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||
"打开文件夹",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "打开文件夹") {
|
||||
vscode.commands.executeCommand("vscode.openFolder");
|
||||
}
|
||||
});
|
||||
}
|
||||
panel.webview.postMessage({
|
||||
command: "workspaceStatus",
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
break;
|
||||
|
||||
case "openExternalUrl":
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
116
src/panels/helpers/userInfoHelper.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 用户信息辅助模块
|
||||
* 功能:管理用户信息的获取、更新和发送
|
||||
* 依赖:vscode, userService, creditsService
|
||||
* 使用场景:面板初始化和余额更新时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { getCachedUserInfo } from "../../services/userService";
|
||||
import { setBalanceUpdateCallback } from "../../services/creditsService";
|
||||
|
||||
export function getTierIconUri(
|
||||
webview: vscode.Webview,
|
||||
context: vscode.ExtensionContext,
|
||||
tierCode?: string,
|
||||
): string | undefined {
|
||||
if (!tierCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tierIconMap: Record<string, string> = {
|
||||
BASIC: "free.png",
|
||||
TRIAL: "PRO-Try.png",
|
||||
ADVANCED: "PRO.png",
|
||||
PROFESSIONAL: "PRO+.png",
|
||||
};
|
||||
|
||||
const iconFile = tierIconMap[tierCode];
|
||||
if (!iconFile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iconUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"dist",
|
||||
"assets",
|
||||
"titleIcon",
|
||||
iconFile,
|
||||
),
|
||||
);
|
||||
|
||||
return iconUri.toString();
|
||||
}
|
||||
|
||||
export async function sendUserInfoToWebview(
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
try {
|
||||
let userInfo = getCachedUserInfo();
|
||||
|
||||
if (userInfo) {
|
||||
console.log("[UserInfoHelper] 使用缓存的用户信息:", userInfo);
|
||||
const tierIconUrl = getTierIconUri(
|
||||
panel.webview,
|
||||
context,
|
||||
userInfo.membership?.tierCode,
|
||||
);
|
||||
panel.webview.postMessage({
|
||||
command: "updateUserInfo",
|
||||
userInfo: {
|
||||
userId: userInfo.userId,
|
||||
nickname: userInfo.nickname,
|
||||
username: userInfo.username,
|
||||
credits: userInfo.credits,
|
||||
membership: userInfo.membership,
|
||||
},
|
||||
tierIconUrl: tierIconUrl,
|
||||
});
|
||||
} else {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (session) {
|
||||
panel.webview.postMessage({
|
||||
command: "updateUserInfo",
|
||||
userInfo: {
|
||||
userId: session.account.id,
|
||||
nickname: session.account.label,
|
||||
username: session.account.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[UserInfoHelper] 获取用户信息失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function setupBalanceUpdateCallback(
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
setBalanceUpdateCallback((balance: number) => {
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (userInfo) {
|
||||
userInfo.credits = balance;
|
||||
const tierIconUrl = getTierIconUri(
|
||||
panel.webview,
|
||||
context,
|
||||
userInfo.membership?.tierCode,
|
||||
);
|
||||
panel.webview.postMessage({
|
||||
command: "updateUserInfo",
|
||||
userInfo: {
|
||||
userId: userInfo.userId,
|
||||
nickname: userInfo.nickname,
|
||||
username: userInfo.username,
|
||||
credits: balance,
|
||||
membership: userInfo.membership,
|
||||
},
|
||||
tierIconUrl: tierIconUrl,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
158
src/panels/helpers/vcdHelper.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* VCD 文件处理模块
|
||||
* 功能:VCD 文件信息获取和信号解析
|
||||
* 依赖:vscode, fs
|
||||
* 使用场景:波形查看器相关功能
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function getVCDFileInfo(
|
||||
panel: vscode.WebviewPanel,
|
||||
vcdFilePath: string,
|
||||
containerId: string,
|
||||
) {
|
||||
try {
|
||||
const fs = require("fs");
|
||||
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: "文件不存在",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(vcdFilePath);
|
||||
const fileSizeKB = stats.size / 1024;
|
||||
const fileSize =
|
||||
fileSizeKB < 1024
|
||||
? `${fileSizeKB.toFixed(2)} KB`
|
||||
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
|
||||
|
||||
const content = fs.readFileSync(vcdFilePath, "utf-8");
|
||||
const varMatches = content.match(/\$var/g);
|
||||
const signalCount = varMatches ? varMatches.length : 0;
|
||||
|
||||
let timeRange = "N/A";
|
||||
const timeMatch = content.match(/#(\d+)/g);
|
||||
if (timeMatch && timeMatch.length > 0) {
|
||||
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
|
||||
const minTime = Math.min(...times);
|
||||
const maxTime = Math.max(...times);
|
||||
timeRange = `${minTime} - ${maxTime}`;
|
||||
}
|
||||
|
||||
const signals = parseVCDSignals(content, 3);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: signalCount.toString(),
|
||||
timeRange: timeRange,
|
||||
fileSize: fileSize,
|
||||
signals: signals,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取 VCD 文件信息失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
const signals: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
values: Array<{ time: number; value: string }>;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
|
||||
let match;
|
||||
const signalDefs: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
}> = [];
|
||||
|
||||
while (
|
||||
(match = varRegex.exec(content)) !== null &&
|
||||
signalDefs.length < maxSignals
|
||||
) {
|
||||
const width = parseInt(match[2]);
|
||||
const identifier = match[3];
|
||||
const name = match[4].trim();
|
||||
signalDefs.push({ name, identifier, width });
|
||||
}
|
||||
|
||||
const dumpvarsIndex = content.indexOf("$dumpvars");
|
||||
if (dumpvarsIndex === -1) {
|
||||
return signals;
|
||||
}
|
||||
|
||||
const dataSection = content.substring(dumpvarsIndex);
|
||||
|
||||
for (const signalDef of signalDefs) {
|
||||
const values: Array<{ time: number; value: string }> = [];
|
||||
let currentTime = 0;
|
||||
const lines = dataSection.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.startsWith("#")) {
|
||||
currentTime = parseInt(trimmedLine.substring(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signalDef.width === 1) {
|
||||
const singleBitMatch = trimmedLine.match(
|
||||
new RegExp(`^([01xz])${signalDef.identifier}$`),
|
||||
);
|
||||
if (singleBitMatch) {
|
||||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||
}
|
||||
} else {
|
||||
const multiBitMatch = trimmedLine.match(
|
||||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`),
|
||||
);
|
||||
if (multiBitMatch) {
|
||||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||
}
|
||||
}
|
||||
|
||||
if (values.length >= 50) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
signals.push({
|
||||
name: signalDef.name,
|
||||
identifier: signalDef.identifier,
|
||||
width: signalDef.width,
|
||||
values: values,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析 VCD 信号数据失败:", error);
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
26
src/providers/codeActionProvider.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Code Action Provider - 为选中代码提供快捷操作
|
||||
* 功能:在小灯泡菜单中显示"添加到 IC Coder 对话"选项
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class ICCoderCodeActionProvider implements vscode.CodeActionProvider {
|
||||
provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range
|
||||
): vscode.CodeAction[] {
|
||||
const selectedText = document.getText(range);
|
||||
if (!selectedText) return [];
|
||||
|
||||
const action = new vscode.CodeAction(
|
||||
'💬 添加到 IC Coder 对话',
|
||||
vscode.CodeActionKind.RefactorRewrite
|
||||
);
|
||||
action.command = {
|
||||
command: 'ic-coder.addCodeToChat',
|
||||
title: '添加到对话'
|
||||
};
|
||||
|
||||
return [action];
|
||||
}
|
||||
}
|
||||
@ -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 } 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,36 +254,142 @@ 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 },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(): Promise<UserInfoResponse> {
|
||||
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] 余额缓存已清除');
|
||||
}
|
||||
@ -2,7 +2,9 @@ import * as vscode from "vscode";
|
||||
import * as http from "http";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||
import { getConfig } from "../config/settings";
|
||||
import { resetInvitationVerification } from "./apiClient";
|
||||
|
||||
/**
|
||||
* IC Coder Authentication Provider
|
||||
@ -23,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,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");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,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];
|
||||
}
|
||||
|
||||
@ -60,15 +80,32 @@ 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 后立即调用用户信息接口
|
||||
const userInfo = await onTokenReceived(token);
|
||||
|
||||
// 创建会话
|
||||
const session: vscode.AuthenticationSession = {
|
||||
id: this.generateSessionId(),
|
||||
accessToken: token,
|
||||
account: {
|
||||
id: "iccoder-user",
|
||||
label: "IC Coder 用户",
|
||||
id: userInfo?.userId || "iccoder-user",
|
||||
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
|
||||
},
|
||||
scopes: [...scopes],
|
||||
};
|
||||
@ -106,10 +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],
|
||||
@ -125,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
|
||||
*/
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
@ -173,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}` } : {})
|
||||
}
|
||||
};
|
||||
|
||||
@ -183,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;
|
||||
}
|
||||
@ -226,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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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,12 +286,12 @@ async function executeFileList(args: FileListArgs): Promise<string> {
|
||||
*/
|
||||
async function executeSyntaxCheck(
|
||||
args: SyntaxCheckArgs,
|
||||
context: ToolExecutorContext
|
||||
context: ToolExecutorContext,
|
||||
): Promise<string> {
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
if (!iverilogCheck.available) {
|
||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
||||
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
|
||||
}
|
||||
|
||||
// 创建临时文件
|
||||
@ -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(`IC Coder编译器不可用: ${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);
|
||||
}
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
|
||||
438
src/services/userService.ts
Normal file
@ -0,0 +1,438 @@
|
||||
/**
|
||||
* 用户服务
|
||||
* 管理用户信息和认证相关的 API 调用
|
||||
*/
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
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 请求选项
|
||||
*/
|
||||
interface RequestOptions {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
timeout?: number;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求(带 token)
|
||||
*/
|
||||
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
const url = new URL(getStrangeLoopApiUrl(path));
|
||||
const { timeout } = getConfig();
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
// 如果有 token,添加到请求头
|
||||
if (options.token) {
|
||||
headers['Authorization'] = `Bearer ${options.token}`;
|
||||
}
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: options.method,
|
||||
headers,
|
||||
timeout: options.timeout || timeout
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log(`[HTTP] 响应状态码: ${res.statusCode}`);
|
||||
console.log(`[HTTP] 响应内容: ${data}`);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as T);
|
||||
} else {
|
||||
reject(new Error(json.error || json.message || json.msg || `HTTP ${res.statusCode}`));
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果不是 JSON,直接返回原始内容
|
||||
reject(new Error(`解析响应失败 (${res.statusCode}): ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(JSON.stringify(options.body));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息数据结构(实际返回的数据)
|
||||
*/
|
||||
export 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 余额
|
||||
credits?: number;
|
||||
// 插件试用用户标识(从 JWT token 中提取)
|
||||
isPluginTrial?: boolean;
|
||||
// 试用到期时间(毫秒时间戳)
|
||||
pluginTrialExpiresAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||||
const apiPath = '/system/user/getInfo';
|
||||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||
console.log('[UserService] 获取用户信息');
|
||||
console.log('[UserService] 请求地址:', fullUrl);
|
||||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||
|
||||
try {
|
||||
const response = await request<UserInfoResponse>(apiPath, {
|
||||
method: 'GET',
|
||||
token
|
||||
});
|
||||
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.user) {
|
||||
const user = response.user;
|
||||
const userInfo: UserInfo = {
|
||||
userId: String(user.userId),
|
||||
username: user.userName,
|
||||
nickname: user.nickName,
|
||||
email: user.email,
|
||||
phonenumber: user.phonenumber,
|
||||
avatar: user.avatar,
|
||||
roles: response.roles,
|
||||
permissions: response.permissions,
|
||||
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);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户会员信息
|
||||
* GET /strangeloop/api/membership/current
|
||||
*/
|
||||
export async function getMembershipInfo(token: string): Promise<MultiMembershipVO | null> {
|
||||
const apiPath = '/strangeloop/api/membership/current';
|
||||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||
console.log('[UserService] 获取会员信息');
|
||||
console.log('[UserService] 请求地址:', fullUrl);
|
||||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||
|
||||
try {
|
||||
const response = await request<MembershipResponse>(apiPath, {
|
||||
method: 'GET',
|
||||
token
|
||||
});
|
||||
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.data) {
|
||||
console.log('[UserService] 会员信息获取成功:', response.data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.error('[UserService] 获取会员信息失败:', response);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 请求会员信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级映射
|
||||
*/
|
||||
const TIER_LEVEL_MAP: Record<string, number> = {
|
||||
'BASIC': 1,
|
||||
'TRIAL': 2,
|
||||
'ADVANCED': 3,
|
||||
'PROFESSIONAL': 4
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取最高等级的会员信息
|
||||
*/
|
||||
function getHighestTierMembership(allMemberships?: MembershipItemVO[]): MembershipItemVO | null {
|
||||
if (!allMemberships || allMemberships.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 按等级排序,获取最高等级
|
||||
return allMemberships.reduce((highest, current) => {
|
||||
const currentLevel = TIER_LEVEL_MAP[current.tierCode] || 0;
|
||||
const highestLevel = TIER_LEVEL_MAP[highest.tierCode] || 0;
|
||||
return currentLevel > highestLevel ? current : highest;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当获取到 token 时自动调用此函数
|
||||
* 用于在登录成功后立即获取用户信息
|
||||
*/
|
||||
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
try {
|
||||
console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...');
|
||||
|
||||
// 并行获取用户信息、会员信息和余额
|
||||
const [userInfo, membershipInfo, credits] = await Promise.all([
|
||||
getUserInfo(token),
|
||||
getMembershipInfo(token),
|
||||
fetchBalanceWithToken(token)
|
||||
]);
|
||||
|
||||
if (!userInfo) {
|
||||
console.warn('[UserService] 未能获取到用户信息');
|
||||
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('用户信息详情:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`用户ID: ${userInfo.userId}`);
|
||||
console.log(`用户名: ${userInfo.username}`);
|
||||
console.log(`昵称: ${userInfo.nickname}`);
|
||||
if (userInfo.email) {
|
||||
console.log(`邮箱: ${userInfo.email}`);
|
||||
}
|
||||
if (userInfo.phonenumber) {
|
||||
console.log(`手机号: ${userInfo.phonenumber}`);
|
||||
}
|
||||
if (userInfo.avatar) {
|
||||
console.log(`头像: ${userInfo.avatar}`);
|
||||
}
|
||||
if (userInfo.roles && userInfo.roles.length > 0) {
|
||||
console.log(`角色: ${userInfo.roles.join(', ')}`);
|
||||
}
|
||||
if (userInfo.permissions && userInfo.permissions.length > 0) {
|
||||
console.log(`权限: ${userInfo.permissions.join(', ')}`);
|
||||
}
|
||||
if (userInfo.createTime) {
|
||||
console.log(`创建时间: ${userInfo.createTime}`);
|
||||
}
|
||||
if (userInfo.loginDate) {
|
||||
console.log(`最后登录: ${userInfo.loginDate}`);
|
||||
}
|
||||
|
||||
// 打印会员信息 - 从 allMemberships 中获取最高等级
|
||||
if (membershipInfo && membershipInfo.allMemberships) {
|
||||
const highestTier = getHighestTierMembership(membershipInfo.allMemberships);
|
||||
|
||||
if (highestTier) {
|
||||
console.log('');
|
||||
console.log('会员信息:');
|
||||
console.log(`会员等级: ${highestTier.tierName} (${highestTier.tierCode})`);
|
||||
console.log(`等级层级: ${highestTier.tierLevel}`);
|
||||
console.log(`剩余天数: ${highestTier.remainingDays === -1 ? '永久' : highestTier.remainingDays + '天'}`);
|
||||
console.log(`月度积分: ${highestTier.monthlyCredits}`);
|
||||
|
||||
// 将最高等级会员信息合并到用户信息中
|
||||
userInfo.membership = {
|
||||
tierCode: highestTier.tierCode,
|
||||
tierName: highestTier.tierName,
|
||||
tierLevel: highestTier.tierLevel,
|
||||
remainingDays: highestTier.remainingDays,
|
||||
monthlyCredits: highestTier.monthlyCredits
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 打印 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);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 持久化存储 ==============
|
||||
|
||||
let extensionContext: vscode.ExtensionContext | null = null;
|
||||
|
||||
/**
|
||||
* 初始化用户服务(设置 context)
|
||||
*/
|
||||
export function initUserService(context: vscode.ExtensionContext): void {
|
||||
extensionContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户信息到持久化存储
|
||||
*/
|
||||
export async function saveUserInfo(userInfo: UserInfo): Promise<void> {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return;
|
||||
}
|
||||
await extensionContext.globalState.update('icCoderUserInfo', userInfo);
|
||||
console.log('[UserService] 用户信息已保存到持久化存储');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从持久化存储获取用户信息
|
||||
*/
|
||||
export function getCachedUserInfo(): UserInfo | null {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除持久化存储的用户信息
|
||||
*/
|
||||
export async function clearUserInfo(): Promise<void> {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return;
|
||||
}
|
||||
await extensionContext.globalState.update('icCoderUserInfo', undefined);
|
||||
console.log('[UserService] 用户信息已清除');
|
||||
}
|
||||
@ -1,15 +1,21 @@
|
||||
import * as http from "http";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
|
||||
/**
|
||||
* VCD 文件 HTTP 服务器
|
||||
* 用于为 Surfer 波形查看器提供 VCD 文件访问
|
||||
* 用于为 波形查看器提供 VCD 文件访问
|
||||
*/
|
||||
export class VCDFileServer {
|
||||
private server: http.Server | null = null;
|
||||
private port: number = 0;
|
||||
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
||||
private extensionUri: vscode.Uri;
|
||||
|
||||
constructor(extensionUri: vscode.Uri) {
|
||||
this.extensionUri = extensionUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
@ -73,6 +79,13 @@ export class VCDFileServer {
|
||||
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取波形查看器 URL
|
||||
*/
|
||||
public getViewerUrl(fileId: string): string {
|
||||
return `http://127.0.0.1:${this.port}/viewer/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件 ID
|
||||
*/
|
||||
@ -85,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}`);
|
||||
|
||||
@ -101,7 +117,53 @@ export class VCDFileServer {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 URL,提取文件 ID
|
||||
// 路由处理
|
||||
if (url.startsWith("/viewer/")) {
|
||||
this.handleViewerRequest(url, res);
|
||||
} else if (url.startsWith("/vcd/")) {
|
||||
this.handleVcdFileRequest(url, res);
|
||||
} else if (url.startsWith("/static/")) {
|
||||
this.handleStaticFileRequest(url, res);
|
||||
} else {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查看器页面请求
|
||||
*/
|
||||
private handleViewerRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/viewer\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = match[1];
|
||||
const filePath = this.vcdFiles.get(fileId);
|
||||
|
||||
if (!filePath) {
|
||||
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成 HTML 页面
|
||||
const html = this.generateViewerHtml(fileId, filePath);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Content-Length": Buffer.byteLength(html),
|
||||
});
|
||||
res.end(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 VCD 文件请求
|
||||
*/
|
||||
private handleVcdFileRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/vcd\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
@ -142,4 +204,305 @@ export class VCDFileServer {
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理静态文件请求(Surfer 资源)
|
||||
*/
|
||||
private handleStaticFileRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/static\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = match[1];
|
||||
const filePath = path.join(
|
||||
this.extensionUri.fsPath,
|
||||
"media",
|
||||
"surfer",
|
||||
fileName,
|
||||
);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
const contentType = this.getContentType(fileName);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": fileContent.length,
|
||||
});
|
||||
res.end(fileContent);
|
||||
} catch (error) {
|
||||
console.error(`[VCDFileServer] 读取静态文件失败:`, error);
|
||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 Content-Type
|
||||
*/
|
||||
private getContentType(fileName: string): string {
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
const contentTypes: { [key: string]: string } = {
|
||||
".js": "application/javascript",
|
||||
".wasm": "application/wasm",
|
||||
".html": "text/html",
|
||||
".css": "text/css",
|
||||
};
|
||||
return contentTypes[ext] || "application/octet-stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||||
*/
|
||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||
try {
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
const scopeNames: string[] = [];
|
||||
let scopeDepth = 0;
|
||||
const scopeStack: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith("$enddefinitions")) {
|
||||
break;
|
||||
}
|
||||
|
||||
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||||
if (scopeMatch) {
|
||||
const scopeType = scopeMatch[1];
|
||||
const scopeName = scopeMatch[2];
|
||||
|
||||
if (scopeDepth === 0 && scopeType === "module") {
|
||||
scopeStack.push(scopeName);
|
||||
} else if (scopeDepth === 1 && scopeType === "module") {
|
||||
const fullPath = [...scopeStack, scopeName];
|
||||
scopeNames.push(fullPath.join("."));
|
||||
}
|
||||
|
||||
scopeDepth++;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("$upscope")) {
|
||||
scopeDepth--;
|
||||
if (scopeDepth === 0) {
|
||||
scopeStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scopeNames;
|
||||
} catch (error) {
|
||||
console.error("[VCDFileServer] 解析 VCD 文件失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成波形查看器 HTML 页面
|
||||
*/
|
||||
private generateViewerHtml(fileId: string, vcdFilePath: string): string {
|
||||
const vcdUrl = this.getFileUrl(fileId);
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||||
const scopeNamesJson = JSON.stringify(scopeNames);
|
||||
|
||||
const htmlPart1 = this.getHtmlPart1(fileName);
|
||||
const htmlPart2 = this.getHtmlPart2(vcdUrl, scopeNamesJson);
|
||||
const htmlPart3 = this.getHtmlPart3();
|
||||
|
||||
return htmlPart1 + htmlPart2 + htmlPart3;
|
||||
}
|
||||
|
||||
private getHtmlPart1(fileName: string): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>波形查看器 - ${fileName}</title>
|
||||
<script>
|
||||
window.surferReady = false;
|
||||
window.pendingVcdData = null;
|
||||
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Surfer error:", msg);
|
||||
document.getElementById("error_message").innerHTML = msg;
|
||||
document.getElementById("error_container").style.display = "block";
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
</script>
|
||||
<script type="module">
|
||||
console.log('[Browser] 开始初始化 Surfer...');
|
||||
import init from '/static/surfer.js';
|
||||
await init({module_or_path: '/static/surfer_bg.wasm'});
|
||||
console.log('[Browser] Surfer WASM 已加载');
|
||||
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/static/surfer.js';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
window.surferReady = true;
|
||||
console.log('[Browser] Surfer 已完全初始化并准备就绪');
|
||||
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ToggleLogs"));
|
||||
console.log('[Browser] 已发送关闭日志面板命令');
|
||||
} catch (e) {
|
||||
console.log('[Browser] 关闭日志面板失败:', e);
|
||||
}
|
||||
|
||||
if (window.pendingVcdData) {
|
||||
console.log('[Browser] 发现待处理的 VCD 数据,立即加载');
|
||||
loadVcdUrl(window.pendingVcdData);
|
||||
window.pendingVcdData = null;
|
||||
}
|
||||
</script>`;
|
||||
}
|
||||
|
||||
private getHtmlPart2(vcdUrl: string, scopeNamesJson: string): string {
|
||||
return `
|
||||
<script>
|
||||
function loadVcdUrl(data) {
|
||||
try {
|
||||
console.log('[Browser] ========== 开始加载 VCD URL ==========');
|
||||
console.log('[Browser] URL:', data.url);
|
||||
console.log('[Browser] Scope names from VCD:', data.scopeNames);
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('[Browser] 通过 postMessage 发送 LoadUrl 命令');
|
||||
window.postMessage({
|
||||
command: 'LoadUrl',
|
||||
url: data.url
|
||||
}, '*');
|
||||
console.log('[Browser] ✅ 已发送 LoadUrl 命令');
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('[Browser] 尝试自动添加所有信号');
|
||||
let scopeNamesToTry = [];
|
||||
|
||||
if (data.scopeNames && data.scopeNames.length > 0) {
|
||||
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||||
console.log('[Browser] 使用解析的作用域名称:', scopeNamesToTry);
|
||||
} else {
|
||||
scopeNamesToTry = [['top'], ['testbench'], ['tb'], ['test'], ['dut']];
|
||||
console.log('[Browser] 使用回退作用域名称');
|
||||
}
|
||||
|
||||
for (let i = 0; i < scopeNamesToTry.length; i++) {
|
||||
const scopeName = scopeNamesToTry[i];
|
||||
try {
|
||||
const addScopeMsg = {
|
||||
"AddScope": [
|
||||
{"strs": scopeName, "id": {"Wellen": i + 1}},
|
||||
true
|
||||
]
|
||||
};
|
||||
window.inject_message(JSON.stringify(addScopeMsg));
|
||||
console.log('[Browser] 已发送 AddScope: ' + scopeName.join('.'));
|
||||
} catch (e) {
|
||||
console.log('[Browser] AddScope 失败: ' + scopeName.join('.'), e);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ZoomToFit"));
|
||||
console.log('[Browser] 已发送 ZoomToFit 命令');
|
||||
} catch (e) {
|
||||
console.log('[Browser] ZoomToFit 失败:', e);
|
||||
}
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
console.error('[Browser] 添加信号失败:', e);
|
||||
}
|
||||
}, 1500);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('[Browser] ❌ 加载 VCD 失败:', error);
|
||||
on_surfer_error(error.message + '\\n' + error.stack);
|
||||
}
|
||||
}
|
||||
window.loadVcdUrl = loadVcdUrl;
|
||||
|
||||
// 页面加载完成后自动加载 VCD
|
||||
window.addEventListener('load', () => {
|
||||
const vcdData = {
|
||||
url: '${vcdUrl}',
|
||||
scopeNames: ${scopeNamesJson}
|
||||
};
|
||||
if (window.surferReady) {
|
||||
loadVcdUrl(vcdData);
|
||||
} else {
|
||||
window.pendingVcdData = vcdData;
|
||||
}
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
|
||||
private getHtmlPart3(): string {
|
||||
return `
|
||||
<style>
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: #f48771;
|
||||
background-color: #5a1d1d;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
<div id="error_container" style="display: none;">
|
||||
<h3>❌ Surfer 加载失败</h3>
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
<script src="/static/integration.js"></script>
|
||||
<script>
|
||||
register_message_listener();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
172
src/types/api.ts
@ -40,6 +40,8 @@ export interface DialogRequest {
|
||||
mode: RunMode;
|
||||
/** 服务等级 */
|
||||
serviceTier?: ServiceTier;
|
||||
/** JWT Token(用于认证和扣费) */
|
||||
token?: string;
|
||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||
compactedData?: CompactedMemory;
|
||||
/** 压缩后产生的新消息 */
|
||||
@ -94,6 +96,7 @@ export interface ToolStartEvent {
|
||||
export interface ToolCompleteEvent {
|
||||
tool_name: string;
|
||||
result: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** tool_error 事件数据 */
|
||||
@ -191,11 +194,17 @@ export interface PlanSummaryUpdateEvent {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** 单个问题项 */
|
||||
export interface QuestionItem {
|
||||
question: string;
|
||||
options: string[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
/** ask_user 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
questions: QuestionItem[];
|
||||
}
|
||||
|
||||
/** complete 事件数据 */
|
||||
@ -348,10 +357,12 @@ export interface AnswerRequest {
|
||||
askId: string;
|
||||
/** 任务ID */
|
||||
taskId: string;
|
||||
/** 选中的选项列表 */
|
||||
/** 选中的选项列表(旧格式,兼容) */
|
||||
selected?: string[];
|
||||
/** 自定义输入内容 */
|
||||
/** 自定义输入内容(旧格式,兼容) */
|
||||
customInput?: string;
|
||||
/** 新格式:按问题索引的答案 */
|
||||
answers?: { [questionIndex: string]: string[] };
|
||||
}
|
||||
|
||||
/** 用户回答响应 */
|
||||
@ -385,6 +396,101 @@ export interface ToolConfirmResponse {
|
||||
approved: boolean;
|
||||
}
|
||||
|
||||
// ============== 用户信息 ==============
|
||||
|
||||
/**
|
||||
* 用户信息响应
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export interface UserInfoResponse {
|
||||
/** 响应消息 */
|
||||
msg: string;
|
||||
/** 响应代码 (200 表示成功) */
|
||||
code: number;
|
||||
/** 权限列表 */
|
||||
permissions: string[];
|
||||
/** 角色列表 */
|
||||
roles: string[];
|
||||
/** 是否默认修改密码 */
|
||||
isDefaultModifyPwd: boolean;
|
||||
/** 密码是否过期 */
|
||||
isPasswordExpired: boolean;
|
||||
/** 是否为插件试用用户 */
|
||||
isPluginTrial?: boolean;
|
||||
/** 企业试用到期时间(毫秒时间戳) */
|
||||
enterpriseTrialExpires?: number;
|
||||
/** 用户信息 */
|
||||
user: {
|
||||
userId: number;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
email?: string;
|
||||
phonenumber?: string;
|
||||
sex?: string;
|
||||
avatar?: string;
|
||||
status?: string;
|
||||
createTime?: string;
|
||||
loginDate?: string;
|
||||
remark?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
// ============== 会员信息 ==============
|
||||
|
||||
/**
|
||||
* 会员单条记录
|
||||
*/
|
||||
export interface MembershipItemVO {
|
||||
membershipId: number | null;
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
expireTime: string | null;
|
||||
remainingDays: number;
|
||||
permanent: boolean;
|
||||
nextGrantTime: string | null;
|
||||
lastGrantTime: string | null;
|
||||
grantCycle: number;
|
||||
totalGranted: number;
|
||||
monthlyCredits: number;
|
||||
teamSeat: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户会员信息
|
||||
*/
|
||||
export interface UserMembershipVO {
|
||||
userId: number;
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
allowedModelCombinations: string[];
|
||||
description?: string;
|
||||
createdTime?: string;
|
||||
updatedTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多会员信息响应
|
||||
*/
|
||||
export interface MultiMembershipVO extends UserMembershipVO {
|
||||
displayTier?: MembershipItemVO;
|
||||
allMemberships?: MembershipItemVO[];
|
||||
totalMonthlyCredits?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员信息响应
|
||||
* GET /strangeloop/api/membership/current
|
||||
*/
|
||||
export interface MembershipResponse {
|
||||
code: number;
|
||||
msg?: string;
|
||||
message?: string;
|
||||
data?: MultiMembershipVO;
|
||||
}
|
||||
|
||||
// ============== 辅助类型 ==============
|
||||
|
||||
/** 后端工具名称 */
|
||||
@ -394,6 +500,7 @@ export type ToolName =
|
||||
| "file_delete"
|
||||
| "file_list"
|
||||
| "syntax_check"
|
||||
| "iverilog"
|
||||
| "simulation"
|
||||
| "waveform_summary"
|
||||
| "waveform_trace"
|
||||
@ -428,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 工具参数 */
|
||||
@ -472,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 }
|
||||
);
|
||||
}
|
||||
@ -7,7 +7,7 @@ import { promisify } from "util";
|
||||
function execCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { cwd: string; env?: any }
|
||||
options: { cwd: string; env?: any },
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 在 Windows 上,如果路径包含空格,不使用 shell,直接用 spawn
|
||||
@ -23,25 +23,25 @@ function execCommand(
|
||||
let stderr = "";
|
||||
|
||||
// 在 Windows 上使用 GBK 编码解码输出
|
||||
const encoding = process.platform === 'win32' ? 'gbk' : 'utf8';
|
||||
const encoding = process.platform === "win32" ? "gbk" : "utf8";
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
try {
|
||||
// 尝试使用 iconv-lite 解码(如果可用)
|
||||
const iconv = require('iconv-lite');
|
||||
const iconv = require("iconv-lite");
|
||||
stdout += iconv.decode(data, encoding);
|
||||
} catch {
|
||||
// 如果 iconv-lite 不可用,使用默认解码
|
||||
stdout += data.toString('utf8');
|
||||
stdout += data.toString("utf8");
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
try {
|
||||
const iconv = require('iconv-lite');
|
||||
const iconv = require("iconv-lite");
|
||||
stderr += iconv.decode(data, encoding);
|
||||
} catch {
|
||||
stderr += data.toString('utf8');
|
||||
stderr += data.toString("utf8");
|
||||
}
|
||||
});
|
||||
|
||||
@ -93,7 +93,7 @@ export interface VCDGenerationResult {
|
||||
* 检查项目中的 Verilog 文件完整性
|
||||
*/
|
||||
export async function checkVerilogProject(
|
||||
projectPath: string
|
||||
projectPath: string,
|
||||
): Promise<VerilogProjectCheck> {
|
||||
const result: VerilogProjectCheck = {
|
||||
isComplete: false,
|
||||
@ -164,7 +164,7 @@ export async function checkVerilogProject(
|
||||
return result;
|
||||
} catch (error) {
|
||||
result.errors.push(
|
||||
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
@ -209,12 +209,30 @@ async function getIverilogPath(extensionPath: string): Promise<string> {
|
||||
let iverilogBin = "";
|
||||
|
||||
if (platform === "win32") {
|
||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
|
||||
iverilogBin = path.join(
|
||||
extensionPath,
|
||||
"tools",
|
||||
"iverilog",
|
||||
"bin",
|
||||
"iverilog.exe",
|
||||
);
|
||||
} else if (platform === "darwin") {
|
||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
|
||||
iverilogBin = path.join(
|
||||
extensionPath,
|
||||
"tools",
|
||||
"iverilog",
|
||||
"bin",
|
||||
"iverilog",
|
||||
);
|
||||
} else {
|
||||
// Linux
|
||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
|
||||
iverilogBin = path.join(
|
||||
extensionPath,
|
||||
"tools",
|
||||
"iverilog",
|
||||
"bin",
|
||||
"iverilog",
|
||||
);
|
||||
}
|
||||
|
||||
// 如果插件包中没有,尝试使用系统安装的 iverilog
|
||||
@ -258,7 +276,7 @@ async function getVvpPath(extensionPath: string): Promise<string> {
|
||||
*/
|
||||
export async function generateVCD(
|
||||
projectPath: string,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
): Promise<VCDGenerationResult> {
|
||||
try {
|
||||
// 1. 检查项目完整性
|
||||
@ -302,12 +320,27 @@ export async function generateVCD(
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `iverilog 编译失败:\n${error.message}`,
|
||||
message: `IC Coder编译器编译失败:\n${error.message}`,
|
||||
stderr: error.stderr,
|
||||
stdout: error.stdout,
|
||||
};
|
||||
}
|
||||
|
||||
// 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误)
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const vvpContent = fs.readFileSync(outputFile, "utf8");
|
||||
const lines = vvpContent.split("\n");
|
||||
|
||||
if (lines.length > 0 && lines[0].startsWith("#!")) {
|
||||
const cleanedContent = lines.slice(1).join("\n");
|
||||
fs.writeFileSync(outputFile, cleanedContent, "utf8");
|
||||
console.log("已删除 .vvp 文件的 shebang 行");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("删除 shebang 失败,继续执行:", error);
|
||||
}
|
||||
|
||||
// 7. 执行仿真生成 VCD
|
||||
const simArgs = [outputFile];
|
||||
console.log("执行仿真命令:", vvpPath, simArgs.join(" "));
|
||||
@ -331,13 +364,17 @@ export async function generateVCD(
|
||||
const projectUri = vscode.Uri.file(projectPath);
|
||||
const entries = await vscode.workspace.fs.readDirectory(projectUri);
|
||||
const vcdFiles = entries
|
||||
.filter(([fileName, fileType]) => fileType === vscode.FileType.File && fileName.endsWith('.vcd'))
|
||||
.filter(
|
||||
([fileName, fileType]) =>
|
||||
fileType === vscode.FileType.File && fileName.endsWith(".vcd"),
|
||||
)
|
||||
.map(([fileName]) => fileName);
|
||||
|
||||
if (vcdFiles.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
|
||||
message:
|
||||
"VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
|
||||
stdout: simResult.stdout,
|
||||
};
|
||||
}
|
||||
@ -373,7 +410,7 @@ export async function generateVCD(
|
||||
* 检查 iverilog 是否可用
|
||||
*/
|
||||
export async function checkIverilogAvailable(
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
): Promise<{ available: boolean; version?: string; message: string }> {
|
||||
try {
|
||||
const iverilogPath = await getIverilogPath(extensionPath);
|
||||
@ -385,7 +422,7 @@ export async function checkIverilogAvailable(
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
message: `iverilog 不可用。未找到文件: ${iverilogPath}`,
|
||||
message: `IC Coder编译器不可用。未找到文件: ${iverilogPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -404,12 +441,214 @@ export async function checkIverilogAvailable(
|
||||
return {
|
||||
available: true,
|
||||
version: version,
|
||||
message: `iverilog 可用: ${version}`,
|
||||
message: `IC Coder编译器可用: ${version}`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
available: false,
|
||||
message: `iverilog 执行失败: ${error.message}\n${error.stderr || ""}`,
|
||||
message: `IC Coder编译器执行失败: ${error.message}\n${error.stderr || ""}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 要 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 : "未知错误"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,12 @@
|
||||
* JWT Payload 接口
|
||||
*/
|
||||
export interface JwtPayload {
|
||||
sub?: string; // subject (通常是 userId)
|
||||
userId?: number; // 用户ID (驼峰命名)
|
||||
user_id?: number; // 用户ID (下划线命名)
|
||||
exp?: number; // 过期时间
|
||||
iat?: number; // 签发时间
|
||||
sub?: string; // subject (通常是 userId)
|
||||
userId?: number; // 用户ID (驼峰命名)
|
||||
user_id?: number; // 用户ID (下划线命名)
|
||||
exp?: number; // 过期时间
|
||||
iat?: number; // 签发时间
|
||||
ispluginTrial?: boolean; // 是否是插件试用用户
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@ -21,9 +22,9 @@ export interface JwtPayload {
|
||||
*/
|
||||
export function parseJwtPayload(token: string): JwtPayload | null {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
console.warn('[JWT] token 格式不正确,期望3部分,实际:', parts.length);
|
||||
console.warn("[JWT] token 格式不正确,期望3部分,实际:", parts.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -31,17 +32,17 @@ export function parseJwtPayload(token: string): JwtPayload | null {
|
||||
const payload = parts[1];
|
||||
|
||||
// base64url 转 base64
|
||||
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
// 解码
|
||||
const jsonStr = Buffer.from(base64, 'base64').toString('utf-8');
|
||||
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));
|
||||
console.log("[JWT] 解析成功, payload 字段:", Object.keys(parsed));
|
||||
console.log("[JWT] payload 内容:", JSON.stringify(parsed));
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error('[JWT] 解析失败:', error);
|
||||
console.error("[JWT] 解析失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -68,6 +69,58 @@ export function getUserIdFromToken(token: string): string | null {
|
||||
return String(payload.sub);
|
||||
}
|
||||
|
||||
console.warn('[JWT] payload 中没有 user_id, userId 或 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;
|
||||
}
|
||||
|
||||
@ -14,10 +14,22 @@ import {
|
||||
checkVerilogProject,
|
||||
checkIverilogAvailable,
|
||||
} from "./iverilogRunner";
|
||||
import { createVivadoProject, runVivadoSynthesis } from "./vivadoRunner";
|
||||
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 +42,16 @@ 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 +62,109 @@ 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,16 +190,47 @@ 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 {
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier);
|
||||
await handleUserMessageWithBackend(
|
||||
panel,
|
||||
text,
|
||||
extensionPath,
|
||||
mode,
|
||||
undefined,
|
||||
serviceTier,
|
||||
contextItems,
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("后端服务不可用:", error);
|
||||
console.error("当前访问人数过多,请稍后重试:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "后端服务不可用",
|
||||
text: "当前访问人数过多,请稍后重试",
|
||||
type: "error",
|
||||
});
|
||||
// 恢复输入状态
|
||||
@ -127,21 +260,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({
|
||||
@ -152,17 +300,25 @@ async function handleUserMessageWithBackend(
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
currentSession!.sendMessage(
|
||||
text,
|
||||
enhancedText,
|
||||
{
|
||||
onText: (fullText, isStreaming) => {
|
||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||
},
|
||||
|
||||
onSegmentUpdate: (segments) => {
|
||||
// 过滤掉包含 [调用工具:xxx] 的段落
|
||||
const filteredSegments = segments.filter(seg => {
|
||||
if (seg.type === 'text' && typeof seg.content === 'string') {
|
||||
return !/\[调用工具:.+?\]/.test(seg.content);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 实时发送段落更新,按后端返回顺序展示
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
segments: filteredSegments,
|
||||
});
|
||||
},
|
||||
|
||||
@ -183,7 +339,10 @@ async function handleUserMessageWithBackend(
|
||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onQuestion: (askId, question, options) => {
|
||||
onQuestion: (
|
||||
askId: string,
|
||||
questions: import("../types/api").QuestionItem[],
|
||||
) => {
|
||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
@ -193,22 +352,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 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
@ -218,41 +364,59 @@ 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,
|
||||
});
|
||||
|
||||
// 发送任务完成消息
|
||||
panel.webview.postMessage({
|
||||
command: "taskComplete",
|
||||
});
|
||||
|
||||
// 发送系统通知 - 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();
|
||||
@ -320,7 +484,7 @@ async function handleUserMessageWithBackend(
|
||||
},
|
||||
},
|
||||
mode,
|
||||
serviceTier // 传递服务等级
|
||||
serviceTier, // 传递服务等级
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -331,10 +495,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,9 +565,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":
|
||||
@ -416,7 +589,8 @@ export async function handlePlanAction(
|
||||
panel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
extensionPath,
|
||||
"agent"
|
||||
"agent",
|
||||
serviceTier,
|
||||
);
|
||||
break;
|
||||
|
||||
@ -432,7 +606,8 @@ export async function handlePlanAction(
|
||||
panel,
|
||||
`请根据以下建议修改计划:${modification}`,
|
||||
extensionPath,
|
||||
"plan"
|
||||
"plan",
|
||||
serviceTier,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -487,7 +662,7 @@ function parseFileOperation(text: string): {
|
||||
|
||||
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
||||
const renameMatch = lowerText.match(
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/,
|
||||
);
|
||||
if (renameMatch) {
|
||||
const oldPath = renameMatch[1].trim();
|
||||
@ -504,7 +679,7 @@ function parseFileOperation(text: string): {
|
||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
||||
const replaceMatch1 = lowerText.match(
|
||||
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/,
|
||||
);
|
||||
if (replaceMatch1) {
|
||||
const filePath = replaceMatch1[1].trim();
|
||||
@ -520,7 +695,7 @@ function parseFileOperation(text: string): {
|
||||
|
||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||
const replaceMatch2 = lowerText.match(
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/,
|
||||
);
|
||||
if (replaceMatch2) {
|
||||
const filePath = replaceMatch2[1].trim();
|
||||
@ -569,7 +744,7 @@ async function handleFileOperation(
|
||||
newPath?: string;
|
||||
searchText?: string;
|
||||
replaceText?: string;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
|
||||
@ -585,7 +760,7 @@ async function handleFileOperation(
|
||||
text: responseText,
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件创建成功: ${operation.filePath}`
|
||||
`文件创建成功: ${operation.filePath}`,
|
||||
);
|
||||
await historyManager.addAiMessage(responseText);
|
||||
break;
|
||||
@ -598,7 +773,7 @@ async function handleFileOperation(
|
||||
text: responseText,
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件删除成功: ${operation.filePath}`
|
||||
`文件删除成功: ${operation.filePath}`,
|
||||
);
|
||||
await historyManager.addAiMessage(responseText);
|
||||
break;
|
||||
@ -634,7 +809,7 @@ async function handleFileOperation(
|
||||
text: responseText,
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件重命名成功: ${operation.filePath} → ${operation.newPath}`
|
||||
`文件重命名成功: ${operation.filePath} → ${operation.newPath}`,
|
||||
);
|
||||
await historyManager.addAiMessage(responseText);
|
||||
break;
|
||||
@ -643,10 +818,21 @@ 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
|
||||
operation.replaceText,
|
||||
);
|
||||
const newContentAfterReplace = await readFileContent(
|
||||
operation.filePath,
|
||||
);
|
||||
await trackFileChange(
|
||||
operation.filePath,
|
||||
oldContentBeforeReplace,
|
||||
newContentAfterReplace,
|
||||
);
|
||||
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
||||
panel.webview.postMessage({
|
||||
@ -654,7 +840,7 @@ async function handleFileOperation(
|
||||
text: responseText,
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件内容替换成功: ${operation.filePath}`
|
||||
`文件内容替换成功: ${operation.filePath}`,
|
||||
);
|
||||
await historyManager.addAiMessage(responseText);
|
||||
break;
|
||||
@ -714,7 +900,7 @@ function getDefaultContent(filePath: string): string {
|
||||
*/
|
||||
export async function handleReadFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
filePath: string
|
||||
filePath: string,
|
||||
) {
|
||||
try {
|
||||
const content = await readFileContent(filePath);
|
||||
@ -738,7 +924,7 @@ export async function handleCreateFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
filePath: string,
|
||||
content: string,
|
||||
overwrite: boolean = false //是否覆盖
|
||||
overwrite: boolean = false, //是否覆盖
|
||||
) {
|
||||
try {
|
||||
if (overwrite) {
|
||||
@ -753,13 +939,26 @@ 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",
|
||||
error: error instanceof Error ? error.message : "创建文件失败",
|
||||
});
|
||||
vscode.window.showErrorMessage(
|
||||
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -770,23 +969,32 @@ export async function handleCreateFile(
|
||||
export async function handleUpdateFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
filePath: string,
|
||||
content: string
|
||||
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",
|
||||
error: error instanceof Error ? error.message : "更新文件失败",
|
||||
});
|
||||
vscode.window.showErrorMessage(
|
||||
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -797,7 +1005,7 @@ export async function handleUpdateFile(
|
||||
export async function handleRenameFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
oldPath: string,
|
||||
newPath: string
|
||||
newPath: string,
|
||||
) {
|
||||
try {
|
||||
await renameFile(oldPath, newPath);
|
||||
@ -808,7 +1016,7 @@ export async function handleRenameFile(
|
||||
message: "文件重命名成功",
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件重命名成功: ${oldPath} → ${newPath}`
|
||||
`文件重命名成功: ${oldPath} → ${newPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
panel.webview.postMessage({
|
||||
@ -816,7 +1024,7 @@ export async function handleRenameFile(
|
||||
error: error instanceof Error ? error.message : "重命名文件失败",
|
||||
});
|
||||
vscode.window.showErrorMessage(
|
||||
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -828,10 +1036,13 @@ export async function handleReplaceInFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
filePath: string,
|
||||
searchText: string,
|
||||
replaceText: string
|
||||
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,
|
||||
@ -844,7 +1055,7 @@ export async function handleReplaceInFile(
|
||||
error: error instanceof Error ? error.message : "替换文件内容失败",
|
||||
});
|
||||
vscode.window.showErrorMessage(
|
||||
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -889,7 +1100,7 @@ function isVCDGenerationCommand(text: string): boolean {
|
||||
*/
|
||||
async function handleVCDGeneration(
|
||||
panel: vscode.WebviewPanel,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
) {
|
||||
try {
|
||||
// 获取当前工作区路径
|
||||
@ -916,7 +1127,7 @@ async function handleVCDGeneration(
|
||||
if (!iverilogCheck.available) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ ${iverilogCheck.message}\n\n请参考插件文档安装 iverilog 工具。`,
|
||||
text: `❌ ${iverilogCheck.message}。`,
|
||||
});
|
||||
vscode.window.showErrorMessage(iverilogCheck.message);
|
||||
return;
|
||||
@ -987,6 +1198,20 @@ 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",
|
||||
@ -1010,6 +1235,17 @@ async function handleVCDGeneration(
|
||||
});
|
||||
|
||||
vscode.window.showErrorMessage("VCD 文件生成失败");
|
||||
|
||||
// 发送系统通知
|
||||
const notificationService = NotificationService.getInstance();
|
||||
notificationService.error(
|
||||
"IC Coder - 仿真失败",
|
||||
"VCD 文件生成失败,请查看错误信息",
|
||||
() => {
|
||||
// 点击通知时聚焦到面板
|
||||
panel.reveal();
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `❌ 生成 VCD 文件时出错: ${
|
||||
@ -1022,5 +1258,249 @@ 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Vivado 工具调用
|
||||
*/
|
||||
export async function handleVivadoToolCall(
|
||||
toolName: string,
|
||||
params: any
|
||||
): Promise<any> {
|
||||
try {
|
||||
switch (toolName) {
|
||||
case 'createVivadoProject':
|
||||
return await createVivadoProject(params);
|
||||
|
||||
case 'runVivadoSynthesis':
|
||||
return await runVivadoSynthesis(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
command: toolName,
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: `未知的工具: ${toolName}`
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
command: toolName,
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: error.message || String(error)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
104
src/utils/tclGenerator.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* TCL 脚本生成器
|
||||
* 功能:生成 Vivado TCL 脚本
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 生成创建工程的 TCL 脚本
|
||||
*/
|
||||
export function generateCreateProjectTcl(
|
||||
projectName: string,
|
||||
projectDir: string,
|
||||
part: string,
|
||||
topModule: string,
|
||||
files: string[],
|
||||
constraints?: string,
|
||||
runSynthesis?: boolean
|
||||
): string {
|
||||
// 转换路径为 TCL 格式(正斜杠)
|
||||
const tclPath = (p: string) => p.replace(/\\/g, '/');
|
||||
|
||||
let tcl = `# 创建 Vivado 工程\n\n`;
|
||||
|
||||
tcl += `create_project ${projectName} {${tclPath(projectDir)}} -part ${part} -force\n\n`;
|
||||
|
||||
// 添加源文件
|
||||
tcl += `# 添加源文件\n`;
|
||||
files.forEach(file => {
|
||||
tcl += `add_files -norecurse {${tclPath(file)}}\n`;
|
||||
});
|
||||
tcl += `\n`;
|
||||
|
||||
// 添加约束文件
|
||||
if (constraints) {
|
||||
tcl += `# 添加约束文件\n`;
|
||||
tcl += `add_files -fileset constrs_1 -norecurse {${tclPath(constraints)}}\n\n`;
|
||||
}
|
||||
|
||||
// 设置顶层模块
|
||||
tcl += `# 设置顶层模块\n`;
|
||||
tcl += `set_property top ${topModule} [current_fileset]\n\n`;
|
||||
|
||||
if (runSynthesis) {
|
||||
tcl += `# 执行综合\n`;
|
||||
tcl += `launch_runs synth_1\n`;
|
||||
tcl += `wait_on_run synth_1\n\n`;
|
||||
tcl += `# 打开综合结果\n`;
|
||||
tcl += `open_run synth_1\n\n`;
|
||||
tcl += `# 生成报告\n`;
|
||||
tcl += `report_utilization -file {${tclPath(path.join(projectDir, `${projectName}_utilization.rpt`))}}\n`;
|
||||
tcl += `report_timing_summary -file {${tclPath(path.join(projectDir, `${projectName}_timing.rpt`))}}\n\n`;
|
||||
}
|
||||
|
||||
return tcl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成综合的 TCL 脚本
|
||||
*/
|
||||
export function generateSynthesisTcl(
|
||||
projectPath: string | undefined,
|
||||
part: string,
|
||||
topModule: string,
|
||||
files?: string[],
|
||||
constraints?: string,
|
||||
outputDir?: string
|
||||
): string {
|
||||
const tclPath = (p: string) => p.replace(/\\/g, '/');
|
||||
let tcl = `# Vivado 综合\n\n`;
|
||||
|
||||
if (projectPath) {
|
||||
// 使用现有工程
|
||||
tcl += `open_project {${tclPath(projectPath)}}\n\n`;
|
||||
} else {
|
||||
// 无工程模式
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('无工程模式需要提供源文件');
|
||||
}
|
||||
tcl += `# 读取源文件\n`;
|
||||
files.forEach(file => {
|
||||
tcl += `read_verilog {${tclPath(file)}}\n`;
|
||||
});
|
||||
tcl += `\n`;
|
||||
|
||||
if (constraints) {
|
||||
tcl += `read_xdc {${tclPath(constraints)}}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
tcl += `# 执行综合\n`;
|
||||
tcl += `synth_design -top ${topModule} -part ${part}\n\n`;
|
||||
|
||||
if (outputDir) {
|
||||
const dcpFile = tclPath(path.join(outputDir, `${topModule}_synth.dcp`));
|
||||
tcl += `# 保存检查点\n`;
|
||||
tcl += `write_checkpoint -force {${dcpFile}}\n\n`;
|
||||
tcl += `# 生成报告\n`;
|
||||
tcl += `report_utilization -file {${tclPath(path.join(outputDir, `${topModule}_utilization.rpt`))}}\n`;
|
||||
tcl += `report_timing_summary -file {${tclPath(path.join(outputDir, `${topModule}_timing.rpt`))}}\n`;
|
||||
}
|
||||
|
||||
return tcl;
|
||||
}
|
||||
87
src/utils/vivadoConfig.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Vivado 配置管理
|
||||
* 功能:读取和验证 Vivado 配置
|
||||
* 依赖:vscode
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface VivadoConfig {
|
||||
enabled: boolean;
|
||||
executablePath: string;
|
||||
workingDir: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Vivado 配置
|
||||
*/
|
||||
export function getVivadoConfig(): VivadoConfig | null {
|
||||
// 优先读取项目配置
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (workspaceFolder) {
|
||||
const projectConfigPath = path.join(
|
||||
workspaceFolder.uri.fsPath,
|
||||
'.vscode',
|
||||
'ic-coder-vivado.json'
|
||||
);
|
||||
if (fs.existsSync(projectConfigPath)) {
|
||||
const content = fs.readFileSync(projectConfigPath, 'utf-8');
|
||||
return JSON.parse(content).vivado;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取全局配置
|
||||
const config = vscode.workspace.getConfiguration('ic-coder');
|
||||
const vivadoConfig = config.get<VivadoConfig>('vivado');
|
||||
|
||||
if (vivadoConfig) {
|
||||
return vivadoConfig;
|
||||
}
|
||||
|
||||
// 自动检测 Vivado
|
||||
return autoDetectVivado();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
*/
|
||||
export function validateConfig(config: VivadoConfig): string | null {
|
||||
if (!config.enabled) {
|
||||
return 'Vivado 未启用';
|
||||
}
|
||||
// 如果是完整路径,检查文件是否存在
|
||||
if (path.isAbsolute(config.executablePath) && !fs.existsSync(config.executablePath)) {
|
||||
return `Vivado 可执行文件不存在: ${config.executablePath}`;
|
||||
}
|
||||
// 环境变量命令不检查,运行时会自动报错
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工作目录
|
||||
*/
|
||||
export function resolveWorkingDir(workingDir: string): string {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (workspaceFolder) {
|
||||
return workingDir.replace('${workspaceFolder}', workspaceFolder.uri.fsPath);
|
||||
}
|
||||
return workingDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动检测 Vivado
|
||||
*/
|
||||
function autoDetectVivado(): VivadoConfig | null {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
|
||||
// 默认使用环境变量中的 vivado 命令
|
||||
return {
|
||||
enabled: true,
|
||||
executablePath: 'vivado',
|
||||
workingDir: workspaceFolder
|
||||
? path.join(workspaceFolder.uri.fsPath, 'vivado_projects')
|
||||
: path.join(process.env.USERPROFILE || 'C:\\Users\\Default', 'vivado_projects')
|
||||
};
|
||||
}
|
||||
246
src/utils/vivadoRunner.ts
Normal file
@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Vivado 执行器
|
||||
* 功能:执行 Vivado 命令
|
||||
* 依赖:vivadoConfig, tclGenerator
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { getVivadoConfig, validateConfig, resolveWorkingDir } from './vivadoConfig';
|
||||
import { generateCreateProjectTcl, generateSynthesisTcl } from './tclGenerator';
|
||||
|
||||
export interface VivadoToolResponse {
|
||||
success: boolean;
|
||||
command: string;
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string;
|
||||
outputFiles?: string[];
|
||||
reports?: {
|
||||
resources?: string;
|
||||
timing?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Vivado 工程
|
||||
*/
|
||||
export async function createVivadoProject(params: {
|
||||
projectName: string;
|
||||
part: string;
|
||||
topModule: string;
|
||||
files: string[];
|
||||
constraints?: string;
|
||||
mode: 'gui' | 'batch';
|
||||
runSynthesis?: boolean;
|
||||
}): Promise<VivadoToolResponse> {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 读取配置
|
||||
const config = getVivadoConfig();
|
||||
if (!config) {
|
||||
return {
|
||||
success: false,
|
||||
command: 'create_project',
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: 'Vivado 未配置'
|
||||
};
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
const configError = validateConfig(config);
|
||||
if (configError) {
|
||||
return {
|
||||
success: false,
|
||||
command: 'create_project',
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: configError
|
||||
};
|
||||
}
|
||||
|
||||
// 准备工作目录
|
||||
const workingDir = resolveWorkingDir(config.workingDir);
|
||||
if (!fs.existsSync(workingDir)) {
|
||||
fs.mkdirSync(workingDir, { recursive: true });
|
||||
}
|
||||
|
||||
const projectDir = path.join(workingDir, params.projectName);
|
||||
|
||||
// 生成 TCL 脚本
|
||||
const tclScript = generateCreateProjectTcl(
|
||||
params.projectName,
|
||||
projectDir,
|
||||
params.part,
|
||||
params.topModule,
|
||||
params.files,
|
||||
params.constraints,
|
||||
params.runSynthesis
|
||||
);
|
||||
|
||||
const tclPath = path.join(workingDir, 'create_project.tcl');
|
||||
fs.writeFileSync(tclPath, tclScript);
|
||||
|
||||
// 执行 Vivado
|
||||
const result = await executeVivado(
|
||||
config.executablePath,
|
||||
tclPath,
|
||||
workingDir,
|
||||
params.mode
|
||||
);
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
// 查找产出文件
|
||||
const xprFile = path.join(projectDir, `${params.projectName}.xpr`);
|
||||
const outputFiles = fs.existsSync(xprFile) ? [xprFile] : [];
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
command: 'create_project',
|
||||
executionTime,
|
||||
output: result.output,
|
||||
error: result.error,
|
||||
outputFiles
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Vivado 综合
|
||||
*/
|
||||
export async function runVivadoSynthesis(params: {
|
||||
projectPath?: string;
|
||||
part: string;
|
||||
topModule: string;
|
||||
files?: string[];
|
||||
constraints?: string;
|
||||
mode: 'gui' | 'batch';
|
||||
}): Promise<VivadoToolResponse> {
|
||||
const startTime = Date.now();
|
||||
|
||||
const config = getVivadoConfig();
|
||||
if (!config) {
|
||||
return {
|
||||
success: false,
|
||||
command: 'synthesis',
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: 'Vivado 未配置'
|
||||
};
|
||||
}
|
||||
|
||||
const configError = validateConfig(config);
|
||||
if (configError) {
|
||||
return {
|
||||
success: false,
|
||||
command: 'synthesis',
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: configError
|
||||
};
|
||||
}
|
||||
|
||||
const workingDir = resolveWorkingDir(config.workingDir);
|
||||
if (!fs.existsSync(workingDir)) {
|
||||
fs.mkdirSync(workingDir, { recursive: true });
|
||||
}
|
||||
|
||||
const outputDir = path.join(workingDir, 'synth_output');
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const tclScript = generateSynthesisTcl(
|
||||
params.projectPath,
|
||||
params.part,
|
||||
params.topModule,
|
||||
params.files,
|
||||
params.constraints,
|
||||
outputDir
|
||||
);
|
||||
|
||||
const tclPath = path.join(workingDir, 'synthesis.tcl');
|
||||
fs.writeFileSync(tclPath, tclScript);
|
||||
|
||||
const result = await executeVivado(
|
||||
config.executablePath,
|
||||
tclPath,
|
||||
workingDir,
|
||||
params.mode
|
||||
);
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
const dcpFile = path.join(outputDir, `${params.topModule}_synth.dcp`);
|
||||
const utilizationRpt = path.join(outputDir, `${params.topModule}_utilization.rpt`);
|
||||
const timingRpt = path.join(outputDir, `${params.topModule}_timing.rpt`);
|
||||
|
||||
const outputFiles = [];
|
||||
if (fs.existsSync(dcpFile)) outputFiles.push(dcpFile);
|
||||
if (fs.existsSync(utilizationRpt)) outputFiles.push(utilizationRpt);
|
||||
if (fs.existsSync(timingRpt)) outputFiles.push(timingRpt);
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
command: 'synthesis',
|
||||
executionTime,
|
||||
output: result.output,
|
||||
error: result.error,
|
||||
outputFiles
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Vivado 命令
|
||||
*/
|
||||
async function executeVivado(
|
||||
executablePath: string,
|
||||
tclPath: string,
|
||||
workingDir: string,
|
||||
mode: 'gui' | 'batch'
|
||||
): Promise<{ success: boolean; output: string; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
const args = mode === 'gui'
|
||||
? ['-source', tclPath]
|
||||
: ['-mode', 'batch', '-source', tclPath];
|
||||
|
||||
const process = spawn(executablePath, args, {
|
||||
cwd: workingDir,
|
||||
shell: true
|
||||
});
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ success: true, output });
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
output,
|
||||
error: errorOutput || `执行失败,退出码: ${code}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
process.on('error', (err) => {
|
||||
resolve({
|
||||
success: false,
|
||||
output,
|
||||
error: `启动 Vivado 失败: ${err.message}`
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
@ -98,24 +99,24 @@ export function getAgentCardStyles(): string {
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
.agent-step.low-profile {
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
opacity: 0.85;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.agent-step.low-profile .step-icon {
|
||||
opacity: 0.4;
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.agent-step.low-profile .step-name {
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.7;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.agent-step.low-profile .step-result {
|
||||
opacity: 0.6;
|
||||
font-size: 9px;
|
||||
opacity: 0.85;
|
||||
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);
|
||||
});
|
||||
|
||||
@ -51,7 +51,11 @@ export function getContextDisplayStyles(): string {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.context-item:hover {
|
||||
.context-item.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.context-item.clickable:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
@ -126,6 +130,11 @@ export function getContextDisplayScript(): string {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取代码图标 SVG
|
||||
function getCodeIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z m-484.7-122.1l39.6-39.5 113.1 113.1-39.6 39.5-113.1-113.1z m226.4-290.2l113.1 113.1-39.6 39.5-113.1-113.1 39.6-39.5z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取删除图标 SVG
|
||||
function getRemoveIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
|
||||
@ -137,9 +146,12 @@ export function getContextDisplayScript(): string {
|
||||
}
|
||||
|
||||
// 添加上下文项
|
||||
function addContextItem(type, path) {
|
||||
function addContextItem(type, path, displayPath) {
|
||||
const exists = contextItems.some(item => item.type === type && item.path === path);
|
||||
if (exists) return;
|
||||
|
||||
const id = Date.now() + Math.random();
|
||||
contextItems.push({ id, type, path });
|
||||
contextItems.push({ id, type, path, displayPath: displayPath || '' });
|
||||
renderContextItems();
|
||||
}
|
||||
|
||||
@ -169,13 +181,17 @@ export function getContextDisplayScript(): string {
|
||||
case 'folder': icon = getFolderIcon(); break;
|
||||
case 'image': icon = getImageIcon(); break;
|
||||
case 'document': icon = getDocumentIcon(); break;
|
||||
case 'code': icon = getCodeIcon(); break;
|
||||
}
|
||||
|
||||
const clickable = item.type !== 'folder' ? 'clickable' : '';
|
||||
const onclick = item.type !== 'folder' ? \`onclick="window.handleContextItemClick(\${item.id})"\` : '';
|
||||
|
||||
return \`
|
||||
<div class="context-item" title="\${item.path}">
|
||||
<div class="context-item \${clickable}" title="\${item.path || item.displayPath}" \${onclick}>
|
||||
\${icon}
|
||||
<span class="context-item-name">\${getFileName(item.path)}</span>
|
||||
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
||||
<span class="context-item-name">\${item.displayPath || getFileName(item.path)}</span>
|
||||
<span class="context-item-remove" onclick="event.stopPropagation(); removeContextItem(\${item.id})">
|
||||
\${getRemoveIcon()}
|
||||
</span>
|
||||
</div>
|
||||
@ -183,6 +199,27 @@ export function getContextDisplayScript(): string {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 全局访问函数
|
||||
window.handleContextItemClick = function(id) {
|
||||
const item = contextItems.find(i => i.id === id);
|
||||
if (!item || item.type === 'folder') return;
|
||||
|
||||
if (item.type === 'code') {
|
||||
const codeData = JSON.parse(item.path);
|
||||
vscode.postMessage({
|
||||
command: 'openFileWithSelection',
|
||||
filePath: codeData.fileName,
|
||||
startLine: codeData.startLine,
|
||||
endLine: codeData.endLine
|
||||
});
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
command: 'openFile',
|
||||
filePath: item.path
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理后端返回的文件选择结果
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
@ -208,6 +245,18 @@ export function getContextDisplayScript(): string {
|
||||
message.documents.forEach(doc => addContextItem('document', doc));
|
||||
}
|
||||
break;
|
||||
case 'addCodeContext':
|
||||
// 添加代码上下文
|
||||
const displayName = \`\${message.fileName.split(/[\\\\/]/).pop()}:\${message.startLine}-\${message.endLine}\`;
|
||||
const codeData = {
|
||||
fileName: message.fileName,
|
||||
startLine: message.startLine,
|
||||
endLine: message.endLine,
|
||||
code: message.code,
|
||||
languageId: message.languageId
|
||||
};
|
||||
addContextItem('code', JSON.stringify(codeData), displayName);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -1,3 +1,24 @@
|
||||
import {
|
||||
getUserInfoComponentContent,
|
||||
getUserInfoComponentStyles,
|
||||
getUserInfoComponentScript,
|
||||
} from "./userInfoComponent";
|
||||
import {
|
||||
getMoreOptionsComponentContent,
|
||||
getMoreOptionsComponentStyles,
|
||||
getMoreOptionsComponentScript,
|
||||
} from "./moreOptionsComponent";
|
||||
import {
|
||||
getSettingsComponentContent,
|
||||
getSettingsComponentStyles,
|
||||
getSettingsComponentScript,
|
||||
} from "./settingsComponent";
|
||||
import {
|
||||
userAvatarIconSvg,
|
||||
moreIconSvg,
|
||||
setting,
|
||||
} from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取会话历史栏的 HTML 内容
|
||||
*/
|
||||
@ -6,7 +27,7 @@ export function getConversationHistoryBarContent(): string {
|
||||
<div class="conversation-history-bar">
|
||||
<div class="history-dropdown-container">
|
||||
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
|
||||
<span class="dropdown-label">Past Conversations</span>
|
||||
<span class="dropdown-label">历史对话</span>
|
||||
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
|
||||
</svg>
|
||||
@ -19,12 +40,36 @@ export function getConversationHistoryBarContent(): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="right-actions">
|
||||
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="user-info-container">
|
||||
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
|
||||
${userAvatarIconSvg}
|
||||
</button>
|
||||
${getUserInfoComponentContent()}
|
||||
</div>
|
||||
|
||||
<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()}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -49,13 +94,125 @@ export function getConversationHistoryBarStyles(): string {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-info-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-avatar-icon-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;
|
||||
}
|
||||
|
||||
.user-avatar-icon-button:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button.active {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
${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;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-input-foreground);
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@ -64,7 +221,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.history-dropdown-button:hover {
|
||||
opacity: 0.8;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
@ -157,13 +314,13 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -173,11 +330,12 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button:hover {
|
||||
opacity: 0.7;
|
||||
background: #007ACC;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.new-conversation-button:active {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.new-conversation-button svg {
|
||||
@ -210,6 +368,33 @@ export function getConversationHistoryBarStyles(): string {
|
||||
*/
|
||||
export function getConversationHistoryBarScript(): string {
|
||||
return `
|
||||
${getUserInfoComponentScript()}
|
||||
|
||||
${getMoreOptionsComponentScript()}
|
||||
|
||||
${getSettingsComponentScript()}
|
||||
|
||||
// 更新用户头像图标按钮显示
|
||||
function updateUserAvatarIconButton(userInfo) {
|
||||
const userAvatarIconButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (userInfo && userInfo.nickname) {
|
||||
// 显示用户头像图标按钮
|
||||
if (userAvatarIconButton) {
|
||||
userAvatarIconButton.style.display = 'flex';
|
||||
}
|
||||
// 同时更新用户详情弹窗的数据
|
||||
if (typeof updateUserInfoDisplay === 'function') {
|
||||
updateUserInfoDisplay(userInfo);
|
||||
}
|
||||
} else {
|
||||
// 隐藏用户头像图标按钮
|
||||
if (userAvatarIconButton) {
|
||||
userAvatarIconButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 会话历史相关变量
|
||||
let conversationHistory = [];
|
||||
let currentConversationId = null;
|
||||
@ -217,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;
|
||||
|
||||
@ -260,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
|
||||
});
|
||||
}
|
||||
|
||||
@ -276,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) {
|
||||
@ -368,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;
|
||||
|
||||
382
src/views/exampleShowcase.ts
Normal file
@ -0,0 +1,382 @@
|
||||
/**
|
||||
* 获取展示区域的 HTML 内容
|
||||
*/
|
||||
export function getExampleShowcaseContent(): string {
|
||||
return `
|
||||
<div class="example-showcase" id="exampleShowcase">
|
||||
<div class="showcase-header">
|
||||
<div class="showcase-title">示例</div>
|
||||
<button class="refresh-button" onclick="refreshExamples()" title="换一批">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5 2V8M21.5 8H15.5M21.5 8L18 4.5C16.7429 3.24286 15.1767 2.35596 13.4606 1.93597C11.7446 1.51598 9.94736 1.57986 8.26381 2.12059C6.58027 2.66131 5.07831 3.65985 3.91872 4.99987C2.75913 6.33989 1.98648 7.96902 1.68 9.71M2.5 22V16M2.5 16H8.5M2.5 16L6 19.5C7.25714 20.7571 8.82331 21.644 10.5394 22.064C12.2554 22.484 14.0526 22.4201 15.7362 21.8794C17.4197 21.3387 18.9217 20.3401 20.0813 19.0001C21.2409 17.6601 22.0135 16.031 22.32 14.29" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</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-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.showcase-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.refresh-button:hover {
|
||||
opacity: 1;
|
||||
background: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
.refresh-button svg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-button:active svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.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 allExamples = [
|
||||
'设计一个算术逻辑单元,完成常见运算',
|
||||
'实现一个优先编码器,多个输入同时有效时,只输出优先级最高的那个编号',
|
||||
'实现一个译码器,把二进制编号转换成 one-hot 输出',
|
||||
'实现一个移位寄存器,完成串行/并行数据移位与装载',
|
||||
'实现一个按键消抖模块,解决机械按键抖动问题',
|
||||
'实现一个跑马灯控制器,控制 LED 形成不同流动效果',
|
||||
'实现一个序列检测器,检测串行输入中是否出现指定比特序列',
|
||||
'实现一个LFSR 伪随机数发生器',
|
||||
'实现一个自动售货机,模拟一个简单售货逻辑',
|
||||
'实现一个交通灯控制器,控制两方向交通灯的切换',
|
||||
'实现一个先进先出的数据缓冲区',
|
||||
'单端口 RAM 读写控制器',
|
||||
'实现一个移位加法乘法器,不用 * 运算符'
|
||||
];
|
||||
|
||||
// 当前显示的示例文本
|
||||
let exampleTexts = ['生成一个SPI控制器', '生成一个GMII接口的以太网UDP通信模块'];
|
||||
|
||||
// 存储待发送的示例索引
|
||||
let pendingExampleIndex = -1;
|
||||
|
||||
// 节流控制
|
||||
let refreshing = false;
|
||||
|
||||
// 刷新示例
|
||||
function refreshExamples() {
|
||||
if (refreshing) return;
|
||||
refreshing = true;
|
||||
|
||||
const used = new Set();
|
||||
const newExamples = [];
|
||||
while (newExamples.length < 2) {
|
||||
const idx = Math.floor(Math.random() * allExamples.length);
|
||||
if (!used.has(idx)) {
|
||||
used.add(idx);
|
||||
newExamples.push(allExamples[idx]);
|
||||
}
|
||||
}
|
||||
exampleTexts = newExamples;
|
||||
updateExampleCards();
|
||||
|
||||
setTimeout(() => { refreshing = false; }, 500);
|
||||
}
|
||||
|
||||
// 更新示例卡片显示
|
||||
function updateExampleCards() {
|
||||
const container = document.querySelector('.example-cards');
|
||||
if (!container) return;
|
||||
container.innerHTML = exampleTexts.map((text, i) => \`
|
||||
<div class="example-card" onclick="sendExample(\${i})">
|
||||
<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">\${text}</div>
|
||||
</div>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 直接发送示例消息
|
||||
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();
|
||||
}
|
||||
});
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
73
src/views/filePathTag.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 文件路径标签组件
|
||||
* 功能:显示可点击的文件路径标签
|
||||
* 使用场景:在用户消息中显示上下文文件
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取文件路径标签的样式
|
||||
*/
|
||||
export function getFilePathTagStyles(): string {
|
||||
return `
|
||||
/* 文件路径标签 */
|
||||
.file-path-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-right: 6px;
|
||||
background: rgba(0, 122, 204, 0.15);
|
||||
border: 1px solid rgba(0, 122, 204, 0.3);
|
||||
border-radius: 4px;
|
||||
color: #4fc3f7;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.file-path-tag:hover {
|
||||
background: rgba(0, 122, 204, 0.25);
|
||||
border-color: rgba(0, 122, 204, 0.5);
|
||||
}
|
||||
|
||||
.file-path-tag svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件路径标签的脚本
|
||||
*/
|
||||
export function getFilePathTagScript(): string {
|
||||
return `
|
||||
// 处理文件路径标签点击
|
||||
function handleFilePathClick(filePath) {
|
||||
// 解析文件路径,支持 file.v:5-8 格式
|
||||
const match = filePath.match(/^(.+?):(\\d+)-(\\d+)$/);
|
||||
if (match) {
|
||||
vscode.postMessage({
|
||||
command: 'openFilePathTag',
|
||||
filePath: match[1],
|
||||
startLine: parseInt(match[2]),
|
||||
endLine: parseInt(match[3])
|
||||
});
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
command: 'openFilePathTag',
|
||||
filePath: filePath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建文件路径标签
|
||||
window.createFilePathTag = function(filePath) {
|
||||
const fileIcon = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
|
||||
const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
|
||||
return '<span class="file-path-tag" onclick="handleFilePathClick(\\'' + escapedPath + '\\')">' + fileIcon + filePath + '</span>';
|
||||
};
|
||||
`;
|
||||
}
|
||||
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,21 @@ export function getInputAreaScript(): string {
|
||||
// 获取上下文项
|
||||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||
|
||||
addMessage(text, 'user');
|
||||
// 构建显示消息:如果有上下文项,添加路径前缀
|
||||
let displayText = text;
|
||||
if (contextItems.length > 0) {
|
||||
const contextPaths = contextItems
|
||||
.map(item => item.displayPath || item.path)
|
||||
.join(' ');
|
||||
if (contextPaths) {
|
||||
displayText = contextPaths + ' ' + text;
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(displayText, 'user');
|
||||
|
||||
// 重置分段消息容器,强制下次创建新容器
|
||||
currentSegmentedMessage = null;
|
||||
|
||||
// 标记已有消息,切换布局到底部
|
||||
hasMessages = true;
|
||||
@ -424,6 +467,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 || '验证失败,请重试');
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
`;
|
||||
}
|
||||
220
src/views/messageRenderer.ts
Normal file
@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 消息渲染脚本模块
|
||||
* 功能:消息渲染、滚动控制、工具状态显示
|
||||
* 依赖:toolHelpers, textFormatter, waveformPreviewContent, agentCard, planCard, codeHighlight
|
||||
* 使用场景:webview 中的消息显示逻辑
|
||||
*/
|
||||
|
||||
import { collapseIconSvg } from "../constants/toolIcons";
|
||||
import { getWaveformPreviewScript } from "./waveformPreviewContent";
|
||||
import { getAgentCardScript } from "./agentCard";
|
||||
import { getPlanCardScript } from "./planCard";
|
||||
import { getCodeHighlightScript } from "../components/codeHighlight";
|
||||
|
||||
export function getMessageRendererScript(): string {
|
||||
return `
|
||||
${getAgentCardScript()}
|
||||
${getPlanCardScript()}
|
||||
|
||||
const toolCollapseStates = new Map();
|
||||
let shouldAutoScroll = true;
|
||||
let lastScrollHeight = 0;
|
||||
|
||||
function isUserNearBottom() {
|
||||
const threshold = 50;
|
||||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||
}
|
||||
|
||||
messagesEl.addEventListener('scroll', () => {
|
||||
const isAtBottom = isUserNearBottom();
|
||||
if (isAtBottom) {
|
||||
shouldAutoScroll = true;
|
||||
} else {
|
||||
if (messagesEl.scrollHeight === lastScrollHeight) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
}
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
});
|
||||
|
||||
function smartScrollToBottom() {
|
||||
if (shouldAutoScroll) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(text, sender) {
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message \${sender}-message\`;
|
||||
if (sender === 'bot') {
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const messageContent = document.createElement('span');
|
||||
messageContent.textContent = text;
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => copyMessage(text, copyBtn);
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
actionsDiv.appendChild(messageContent);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
div.appendChild(actionsDiv);
|
||||
} else {
|
||||
const parts = text.split(' ');
|
||||
const filePaths = [];
|
||||
const textParts = [];
|
||||
parts.forEach(part => {
|
||||
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) {
|
||||
filePaths.push(part);
|
||||
} else {
|
||||
textParts.push(part);
|
||||
}
|
||||
});
|
||||
if (filePaths.length > 0) {
|
||||
div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
|
||||
} else {
|
||||
div.textContent = text;
|
||||
}
|
||||
hideHeaderIfNeeded();
|
||||
}
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function hideHeaderIfNeeded() {
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function copyMessage(text, button) {
|
||||
// 从按钮的父消息元素中获取实际文本内容
|
||||
const messageDiv = button.closest('.message');
|
||||
const messageContent = messageDiv?.querySelector('.message-content, div:not(.message-actions)');
|
||||
const textToCopy = messageContent ? messageContent.textContent : text;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1 0.4-12.8-6.3-12.8z" fill="currentColor"/></svg>\`;
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLike(button) {
|
||||
const isActive = button.classList.contains('active');
|
||||
const parent = button.parentElement;
|
||||
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDislike(button) {
|
||||
const isActive = button.classList.contains('active');
|
||||
const parent = button.parentElement;
|
||||
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function updateOrCreateStreamingMessage(text) {
|
||||
hideLoadingIndicator();
|
||||
if (!currentStreamingMessage) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message streaming';
|
||||
const messageContent = document.createElement('div');
|
||||
messageContent.className = 'message-content';
|
||||
messageContent.textContent = text;
|
||||
div.appendChild(messageContent);
|
||||
messagesEl.appendChild(div);
|
||||
currentStreamingMessage = div;
|
||||
} else {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = text;
|
||||
}
|
||||
}
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function finalizeStreamingMessage(finalText) {
|
||||
if (currentStreamingMessage) {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = finalText;
|
||||
}
|
||||
currentStreamingMessage.classList.remove('streaming');
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
copyBtn.onclick = () => copyMessage(finalText, copyBtn);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
currentStreamingMessage.appendChild(actionsDiv);
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function showLoadingIndicator(text) {
|
||||
hideLoadingIndicator();
|
||||
loadingIndicator = document.createElement('div');
|
||||
loadingIndicator.className = 'message bot-message loading-message';
|
||||
loadingIndicator.innerHTML = \`
|
||||
<div class="loading-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<span class="loading-text">\${text}</span>
|
||||
\`;
|
||||
messagesEl.appendChild(loadingIndicator);
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function hideLoadingIndicator() {
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
loadingIndicator = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addToolStatus(toolName, status, detail) {
|
||||
const statusIcons = {
|
||||
start: '🔧',
|
||||
complete: '✅',
|
||||
error: '❌'
|
||||
};
|
||||
const statusTexts = {
|
||||
start: '正在执行',
|
||||
complete: '执行完成',
|
||||
error: '执行失败'
|
||||
};
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message tool-status tool-\${status}\`;
|
||||
div.innerHTML = \`
|
||||
<span class="tool-icon">\${statusIcons[status]}</span>
|
||||
<span class="tool-name">\${getToolDisplayName(toolName)}</span>
|
||||
<span class="tool-status-text">\${statusTexts[status]}</span>
|
||||
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
||||
\`;
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
${getCodeHighlightScript()}
|
||||
`;
|
||||
}
|
||||
632
src/views/messageStyles.ts
Normal file
@ -0,0 +1,632 @@
|
||||
/**
|
||||
* 消息样式模块
|
||||
* 功能:提供消息区域的所有 CSS 样式
|
||||
* 依赖:agentCard, planCard, codeHighlight, waveformPreviewContent
|
||||
* 使用场景:webview 样式注入
|
||||
*/
|
||||
|
||||
import { getAgentCardStyles } from "./agentCard";
|
||||
import { getPlanCardStyles } from "./planCard";
|
||||
import { getCodeHighlightStyles } from "../components/codeHighlight";
|
||||
import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||||
|
||||
export function getMessageAreaStyles(): string {
|
||||
return `
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 15px;
|
||||
min-height: 0;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.user-message {
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
margin-left: auto;
|
||||
width: fit-content;
|
||||
max-width: 80%;
|
||||
}
|
||||
.bot-message {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: var(--vscode-foreground);
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.message-actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.2s ease;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
}
|
||||
.message-actions > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.message-actions:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
position: relative;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.action-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.action-btn.active {
|
||||
color: var(--vscode-button-background);
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn .action-tooltip {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(5px);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;white-space: nowrap;pointer-events: none;
|
||||
}
|
||||
.action-btn .action-tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #1e1e1e transparent transparent transparent;
|
||||
}
|
||||
.action-btn .action-tooltip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
.action-btn:hover .action-tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.streaming .message-content {
|
||||
border-right: 2px solid var(--vscode-focusBorder);
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 50% { border-color: var(--vscode-focusBorder); }
|
||||
51%, 100% { border-color: transparent; }
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;}
|
||||
.loading-dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-focusBorder);
|
||||
animation: loadingDot 1.4s infinite ease-in-out;
|
||||
}
|
||||
.loading-dots span:nth-child(1) { animation-delay: 0s; }
|
||||
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes loadingDot {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
}
|
||||
.tool-status.tool-start {
|
||||
border-left: 3px solid var(--vscode-charts-blue);
|
||||
}
|
||||
.tool-status.tool-complete {
|
||||
border-left: 3px solid var(--vscode-charts-green);
|
||||
}
|
||||
.tool-status.tool-error {
|
||||
border-left: 3px solid var(--vscode-charts-red);
|
||||
}
|
||||
.tool-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
.tool-name {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.tool-status-text {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.tool-detail {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: pre-wrap;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.question-message {
|
||||
padding: 16px;
|
||||
}
|
||||
.question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.question-option {
|
||||
padding: 8px 16px;
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border: 1px solid #007ACC;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.question-message.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.custom-submit {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.question-message.answered .custom-input-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.segmented-message {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.segment-text h1,
|
||||
.segment-text h2,
|
||||
.segment-text h3,
|
||||
.question-text h1,
|
||||
.question-text h2,
|
||||
.question-text h3 {
|
||||
margin: 0px 0 -10px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.segment-text h1,
|
||||
.question-text h1 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.segment-text h2,
|
||||
.question-text h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.segment-text h3,
|
||||
.question-text h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.segment-text ul,
|
||||
.segment-text ol,
|
||||
.question-text ul,
|
||||
.question-text ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.segment-text li,
|
||||
.question-text li {
|
||||
line-height: 1;
|
||||
}
|
||||
.segment-text strong,
|
||||
.question-text strong {
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.segment-text em,
|
||||
.question-text em {
|
||||
font-style: italic;
|
||||
}
|
||||
.segment-text a,
|
||||
.question-text a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.segment-text a:hover,
|
||||
.question-text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.segment-text p,
|
||||
.question-text p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.segment-text code,
|
||||
.question-text code {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.segment-tool {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.segment-tool.low-profile {
|
||||
margin: 25px 0px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
.tool-segment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-segment-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
.tool-segment-name {
|
||||
font-weight: normal;
|
||||
}
|
||||
.tool-segment-result {
|
||||
display: inline;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 500px;
|
||||
}
|
||||
.tool-collapse-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-collapse-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.icon-expanded svg path {
|
||||
fill: #007ACC !important;
|
||||
}
|
||||
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
.tool-file-write-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-write-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-read-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-read-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-delete-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-syntax-check-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-syntax-check-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-search-code-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-search-code-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-save-knowledge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-save-knowledge-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-simulation-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-simulation-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-waveform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-waveform-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-knowledge-load-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-knowledge-load-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-state-transition-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-state-transition-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-segment-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
.tool-segment-content.collapsed {
|
||||
max-height: 0;
|
||||
}
|
||||
.tool-segment-description {
|
||||
margin: 25px 0 0 0px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-header {
|
||||
opacity: 0.65;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-icon {
|
||||
opacity: 0.55;
|
||||
font-size: 11px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-name {
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-result {
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.segment-question .question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.segment-question .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.segment-question .question-option {
|
||||
padding: 8px 16px;
|
||||
background: #3d3f41;
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid #474747;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question .question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.segment-question .question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.segment-question.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.segment-question .custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.segment-question .custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.segment-question .custom-submit {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.segment-question .custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.segment-question.answered .custom-input-container {
|
||||
display: none;
|
||||
}
|
||||
.question-segment .question-text {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-segment .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.question-opt {
|
||||
padding: 4px 10px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;}
|
||||
|
||||
${getAgentCardStyles()}
|
||||
|
||||
${getPlanCardStyles()}
|
||||
|
||||
${getCodeHighlightStyles()}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||