Compare commits
26 Commits
032dd1b215
...
feat/front
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
2
.npmrc
2
.npmrc
@ -1 +1,3 @@
|
|||||||
enable-pre-post-scripts = true
|
enable-pre-post-scripts = true
|
||||||
|
shamefully-hoist = true
|
||||||
|
public-hoist-pattern[] = *
|
||||||
24
CHANGELOG.md
24
CHANGELOG.md
@ -2,22 +2,16 @@
|
|||||||
|
|
||||||
所有重要的项目变更都将记录在此文件中。
|
所有重要的项目变更都将记录在此文件中。
|
||||||
|
|
||||||
## [1.0.2] - 2026-01-13
|
## [1.0.4] - 2026-01-28
|
||||||
|
|
||||||
IC Coder插件端正式发布。
|
IC Coder插件端正式上线。
|
||||||
|
|
||||||
IC Coder 插件端是一个专为 FPGA 开发设计的 VS Code 扩展,提供 AI 驱动的智能辅助功能。
|
IC Coder 插件端是一个是一个自主式人工智能 Verilog 编码平台,可以将芯片设计与验证的效率提升至少20倍!
|
||||||
|
|
||||||
主要功能:
|
主要功能:
|
||||||
- VCD波形解析
|
|
||||||
- 自动生成完整工程
|
- 自动搭建电路架构:够根据自然语言描述的设计需求,自动生成完整的电路架构
|
||||||
- 自动仿真
|
- AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码
|
||||||
- 自主代码迭代
|
- AI自主代码迭代:实现了真正的自主式开发循环,能够持续优化代码直到满足设计要求
|
||||||
- 智能匹配最优模型
|
- 随时可掌控:提供透明化的开发过程,让用户始终掌握AI的工作状态
|
||||||
- 多线程任务处理
|
- 多层次安全保障:将数据安全和隐私保护作为核心设计原则,提供企业级的安全保障
|
||||||
- 实时跟随
|
|
||||||
- 丰富的上下文工具
|
|
||||||
- 全双工交互
|
|
||||||
- 多层次安全保障
|
|
||||||
- 自动搭建电路架构
|
|
||||||
- 多平台支持
|
|
||||||
|
|||||||
41
PUBLISH.md
41
PUBLISH.md
@ -268,6 +268,47 @@ pnpm vsce publish 0.0.3
|
|||||||
4. 执行发布命令
|
4. 执行发布命令
|
||||||
5. 验证市场上的插件是否正常
|
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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|||||||
74
README.md
74
README.md
@ -1,40 +1,82 @@
|
|||||||
## 什么是IC Coder
|
## 什么是IC Coder
|
||||||
|
|
||||||
**IC Coder** 是一款 **专注于真实 FPGA 研发的 Verilog 智能体编程平台**。我们立志于用 AI 重塑 FPGA 研发效率,让 FPGA 开发者们,都能享受到 AI 发展所带来的科技福利!目标成为全球最好用的 **LLM 生成 Verilog**的平台!
|
IC Coder是一款**The Agentic AI Verilog Coding Platform(自主式人工智能 Verilog 编码平台)**。我们立志于用AI重塑芯片开发者的效率,将芯片设计与验证的效率提升至少20倍!让芯片开发者们,都能享受到AI发展所带来的科技福利!目标成为全球最好用的"LLM生成Verilog"的平台!
|
||||||
|
|
||||||
从 WEB 端到插件端,IC Coder 智能体架构完成了**全新升级**,采用当前主流的**层级架构**设计,这种高内聚、低耦合的架构特性,不仅支持更多功能扩展,更预留了充足的迭代空间。当前,插件端拥有了调用本地工具的能力,不再是单纯代码生成的智能体,升级为拥有**语法校验、波形逻辑检查**等工具的**全流程 Verilog 编程智能体平台**,给用户带来更沉浸的**Vibe Verilog Coding**体验。
|

|
||||||
|
|
||||||
## 输入需求 对话补充需求
|
### 核心技术架构
|
||||||
|
|
||||||
**无需**输入完整需求,放心交给智能体补充完善。
|
**我们采用全球顶尖的大语言模型**,加上自研的针对芯片设计领域深度优化的微调模型,为代码生成提供强大的AI能力支撑。
|
||||||
|
|
||||||
## Plan 模型下确认设计文档
|
**核心技术栈**包括:
|
||||||
|
|
||||||
**确定**好用户需求以及相关参数后,整理并输出一份 FPGA 开发**设计文档**。Plan 模式下用户可以**进一步**与 IC Coder 沟通需求,或**直接修改**设计文档。
|
- **多智能体架构(Multi-Agent System)**:多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节
|
||||||
|
- **增强上下文引擎**:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性
|
||||||
|
|
||||||
|
这些技术共同支撑着从需求分析、架构设计、代码生成到验证调试的全流程智能化开发体验。
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
## 实时跟随
|
## AI自主仿真
|
||||||
|
|
||||||
实时展示全流程执行细节,与智能体协同随时反馈,让 AI 开发更清晰、高效
|
IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码:
|
||||||
|
|
||||||
## VCD 波形解析
|
- **智能Testbench生成**:根据设计模块自动生成完整的测试平台,包括激励生成、时钟复位、接口驱动等
|
||||||
|
- **测试用例自动化**:根据设计规格自动生成覆盖各种场景的测试用例,包括正常功能、边界条件、异常情况等
|
||||||
|
- **一键运行仿真**:自动调用集成仿真器执行仿真
|
||||||
|
- **波形自动生成**:仿真完成后自动生成VCD、波形文件,便于后续分析
|
||||||
|
- **实时进度反馈**:仿真过程中实时显示执行状态和日志信息
|
||||||
|
|
||||||
自动解析 VCD 波形文件,自动根据需求,检查是否存在逻辑错误
|

|
||||||
|
|
||||||
## 自主代码迭代
|
## AI自主代码迭代
|
||||||
|
|
||||||
根据波形解析结果,自动对代码进行优化,然后重新仿真并解析波形,如此迭代,直到仿真无误
|
IC Coder实现了真正的自主式开发循环,能够持续优化代码直到满足设计要求:
|
||||||
|
|
||||||
|
- **智能问题诊断**:根据波形分析结果,自动定位代码中的问题根源
|
||||||
|
- **自动代码修复**:针对发现的问题自动生成修复方案并更新代码
|
||||||
|
- **迭代验证循环**:修复后自动重新运行仿真和波形分析,验证问题是否解决
|
||||||
|
- **持续优化**:如果仍存在问题,继续分析和修复,形成闭环迭代
|
||||||
|
- **收敛保证**:智能判断迭代进展,避免无效循环,确保最终收敛到正确设计
|
||||||
|
- **全程可追溯**:记录每次迭代的修改内容和验证结果,便于回溯和审查
|
||||||
|
|
||||||
|
这种自主迭代能力大幅减少了人工调试时间,让设计验证过程更加高效可靠。
|
||||||
|
|
||||||
|
## 随时可掌控
|
||||||
|
|
||||||
|
IC Coder提供透明化的开发过程,让用户始终掌握AI的工作状态:
|
||||||
|
|
||||||
|
- **实时流程展示**:可视化展示当前执行到哪个阶段(需求分析、架构设计、代码生成、仿真验证等)
|
||||||
|
- **详细执行日志**:记录每一步操作的详细信息,包括AI的思考过程、决策依据、执行结果
|
||||||
|
- **人机协同交互**:在关键决策点支持用户介入,可随时提供反馈、调整方向或修改参数
|
||||||
|
- **进度实时追踪**:显示任务完成进度、预计剩余步骤,让开发过程更加可预期
|
||||||
|
- **智能建议系统**:AI主动提供优化建议和替代方案,用户可选择采纳或自定义
|
||||||
|
- **即时响应机制**:支持随时暂停、恢复或调整AI的工作流程
|
||||||
|
|
||||||
|
这种透明可控的设计理念,让AI开发不再是"黑盒",而是真正的智能协作伙伴。
|
||||||
|
|
||||||
## 多层次安全保障
|
## 多层次安全保障
|
||||||
|
|
||||||
默认本地存储与云端即时加密保障隐私,真正做到了代码全链路加密传输、云端零存储
|
IC Coder将数据安全和隐私保护作为核心设计原则,提供企业级的安全保障:
|
||||||
|
|
||||||
|
- **本地优先存储**:所有设计文件默认存储在本地,用户完全掌控自己的代码资产
|
||||||
|
- **全链路加密传输**:与云端通信采用TLS/SSL加密,确保数据传输过程中不被窃取或篡改
|
||||||
|
- **云端零存储策略**:云端服务器不保存用户的源代码,仅处理加密后的临时数据,处理完成后立即销毁
|
||||||
|
- **定制化部署选项**:支持企业私有云或本地部署,满足高安全等级需求
|
||||||
|
|
||||||
|
真正做到了代码全链路加密传输、云端零存储,让芯片设计企业可以放心使用AI工具。
|
||||||
|
|
||||||
## 反馈
|
## 反馈
|
||||||
|
|
||||||
|
|||||||
45
docs/code-changes-feature.md
Normal file
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. 处理前端的采纳/拒绝响应
|
||||||
50
docs/integration-guide.md
Normal file
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. 点击"采纳"或"拒绝"按钮测试功能
|
||||||
783
docs/插件试用用户功能实现方案.md
Normal file
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. 更友好的过期提示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档完成!** 基于你现有的代码架构,这个方案改动最小,实现最简单。
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 252 KiB |
@ -2,7 +2,7 @@
|
|||||||
"name": "iccoder",
|
"name": "iccoder",
|
||||||
"displayName": "IC Coder: Agentic Verilog Platform",
|
"displayName": "IC Coder: Agentic Verilog Platform",
|
||||||
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"publisher": "ICCoderAgenticVerilogPlatform",
|
"publisher": "ICCoderAgenticVerilogPlatform",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.80.0"
|
"vscode": "^1.80.0"
|
||||||
@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onCommand:ic-coder.openPanel",
|
"onCommand:ic-coder.openPanel",
|
||||||
"onView:ic-coder-sidebar",
|
"onView:ic-coder.mainView",
|
||||||
"onLanguage:verilog",
|
"onLanguage:verilog",
|
||||||
"onLanguage:vhdl",
|
"onLanguage:vhdl",
|
||||||
"onStartupFinished"
|
"onStartupFinished"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -289,7 +289,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
const viewProvider = new ICViewProvider(context.extensionUri, context);
|
const viewProvider = new ICViewProvider(context.extensionUri, context);
|
||||||
const viewRegistration = vscode.window.registerWebviewViewProvider(
|
const viewRegistration = vscode.window.registerWebviewViewProvider(
|
||||||
"ic-coder.mainView",
|
"ic-coder.mainView",
|
||||||
viewProvider
|
viewProvider,
|
||||||
|
{
|
||||||
|
webviewOptions: {
|
||||||
|
retainContextWhenHidden: true
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 注册 VCD 自定义编辑器
|
// 注册 VCD 自定义编辑器
|
||||||
@ -305,6 +310,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
logoutCommand,
|
logoutCommand,
|
||||||
changeInvitationCodeCommand,
|
changeInvitationCodeCommand,
|
||||||
testNotificationCommand,
|
testNotificationCommand,
|
||||||
|
// testTrialUserCommand,
|
||||||
|
// testExpiredUserCommand,
|
||||||
// TODO: 等待重新实现这些命令
|
// TODO: 等待重新实现这些命令
|
||||||
// viewHistoryCommand,
|
// viewHistoryCommand,
|
||||||
// newSessionCommand,
|
// newSessionCommand,
|
||||||
|
|||||||
78
src/panels/ExpiredPanel.ts
Normal file
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,10 @@ import {
|
|||||||
handlePlanAction,
|
handlePlanAction,
|
||||||
getCurrentTaskId,
|
getCurrentTaskId,
|
||||||
setLastTaskId,
|
setLastTaskId,
|
||||||
|
handleAcceptChange,
|
||||||
|
handleRejectChange,
|
||||||
|
startChangeSession,
|
||||||
|
handleOpenFileDiff,
|
||||||
} from "../utils/messageHandler";
|
} from "../utils/messageHandler";
|
||||||
import { compactDialog } from "../services/apiClient";
|
import { compactDialog } from "../services/apiClient";
|
||||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||||
@ -20,6 +24,7 @@ import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
|||||||
import { MessageType } from "../types/chatHistory";
|
import { MessageType } from "../types/chatHistory";
|
||||||
import { getCachedUserInfo } from "../services/userService";
|
import { getCachedUserInfo } from "../services/userService";
|
||||||
import { isTokenExpired } from "../utils/jwtUtils";
|
import { isTokenExpired } from "../utils/jwtUtils";
|
||||||
|
import { setBalanceUpdateCallback } from "../services/creditsService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会员等级图标 URI
|
* 获取会员等级图标 URI
|
||||||
@ -27,17 +32,17 @@ import { isTokenExpired } from "../utils/jwtUtils";
|
|||||||
function getTierIconUri(
|
function getTierIconUri(
|
||||||
webview: vscode.Webview,
|
webview: vscode.Webview,
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
tierCode?: string
|
tierCode?: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (!tierCode) {
|
if (!tierCode) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tierIconMap: Record<string, string> = {
|
const tierIconMap: Record<string, string> = {
|
||||||
'BASIC': 'free.png',
|
BASIC: "free.png",
|
||||||
'TRIAL': 'PRO-Try.png',
|
TRIAL: "PRO-Try.png",
|
||||||
'ADVANCED': 'PRO.png',
|
ADVANCED: "PRO.png",
|
||||||
'PROFESSIONAL': 'PRO+.png'
|
PROFESSIONAL: "PRO+.png",
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconFile = tierIconMap[tierCode];
|
const iconFile = tierIconMap[tierCode];
|
||||||
@ -46,7 +51,13 @@ function getTierIconUri(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const iconUri = webview.asWebviewUri(
|
const iconUri = webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, 'src', 'assets', 'titleIcon', iconFile)
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"titleIcon",
|
||||||
|
iconFile,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return iconUri.toString();
|
return iconUri.toString();
|
||||||
@ -57,27 +68,29 @@ function getTierIconUri(
|
|||||||
*/
|
*/
|
||||||
export async function showICHelperPanel(
|
export async function showICHelperPanel(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
viewColumn?: vscode.ViewColumn
|
viewColumn?: vscode.ViewColumn,
|
||||||
) {
|
) {
|
||||||
// 检查 token 是否过期
|
// 检查 token 是否过期
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
try {
|
try {
|
||||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
createIfNone: false,
|
||||||
|
});
|
||||||
token = session?.accessToken;
|
token = session?.accessToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[ICHelperPanel] 获取 session 失败:', error);
|
console.warn("[ICHelperPanel] 获取 session 失败:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token && isTokenExpired(token)) {
|
if (token && isTokenExpired(token)) {
|
||||||
// 清除过期的 session
|
// 清除过期的 session
|
||||||
await context.globalState.update('icCoderSessions', []);
|
await context.globalState.update("icCoderSessions", []);
|
||||||
await context.globalState.update('icCoderUserInfo', undefined);
|
await context.globalState.update("icCoderUserInfo", undefined);
|
||||||
|
|
||||||
const action = await vscode.window.showWarningMessage(
|
const action = await vscode.window.showWarningMessage(
|
||||||
'登录已过期,请重新登录',
|
"登录已过期,请重新登录",
|
||||||
'立即登录'
|
"立即登录",
|
||||||
);
|
);
|
||||||
if (action === '立即登录') {
|
if (action === "立即登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -119,9 +132,9 @@ export async function showICHelperPanel(
|
|||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [
|
localResourceRoots: [
|
||||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets"),
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 为面板生成唯一ID
|
// 为面板生成唯一ID
|
||||||
@ -135,31 +148,66 @@ export async function showICHelperPanel(
|
|||||||
panel.iconPath = vscode.Uri.joinPath(
|
panel.iconPath = vscode.Uri.joinPath(
|
||||||
context.extensionUri,
|
context.extensionUri,
|
||||||
"media",
|
"media",
|
||||||
"icon.png"
|
"icon.png",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取页面内图标URI
|
// 获取页面内图标URI
|
||||||
const iconUri = panel.webview.asWebviewUri(
|
const iconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取模型图标URI
|
// 获取模型图标URI
|
||||||
const autoIconUri = panel.webview.asWebviewUri(
|
const autoIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"model",
|
||||||
|
"Auto.png",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const liteIconUri = panel.webview.asWebviewUri(
|
const liteIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"model",
|
||||||
|
"lite.png",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const syIconUri = panel.webview.asWebviewUri(
|
const syIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"model",
|
||||||
|
"Sy.png",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const maxIconUri = panel.webview.asWebviewUri(
|
const maxIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"model",
|
||||||
|
"Max.png",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取二维码图片URI
|
// 获取二维码图片URI
|
||||||
const qrCodeUri = panel.webview.asWebviewUri(
|
const qrCodeUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "QRCode", "wx.png")
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"QRCode",
|
||||||
|
"wx.png",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取Logo URI
|
||||||
|
const logoUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 设置HTML内容
|
// 设置HTML内容
|
||||||
@ -169,7 +217,8 @@ export async function showICHelperPanel(
|
|||||||
liteIconUri.toString(),
|
liteIconUri.toString(),
|
||||||
syIconUri.toString(),
|
syIconUri.toString(),
|
||||||
maxIconUri.toString(),
|
maxIconUri.toString(),
|
||||||
qrCodeUri.toString()
|
qrCodeUri.toString(),
|
||||||
|
logoUri.toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取并发送用户信息到 webview
|
// 获取并发送用户信息到 webview
|
||||||
@ -179,21 +228,25 @@ export async function showICHelperPanel(
|
|||||||
|
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
// 使用缓存的用户信息
|
// 使用缓存的用户信息
|
||||||
console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo);
|
console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo);
|
||||||
console.log('[ICHelperPanel] Credits 余额:', userInfo.credits);
|
console.log("[ICHelperPanel] Credits 余额:", userInfo.credits);
|
||||||
const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode);
|
const tierIconUrl = getTierIconUri(
|
||||||
|
panel.webview,
|
||||||
|
context,
|
||||||
|
userInfo.membership?.tierCode,
|
||||||
|
);
|
||||||
const messageData = {
|
const messageData = {
|
||||||
command: 'updateUserInfo',
|
command: "updateUserInfo",
|
||||||
userInfo: {
|
userInfo: {
|
||||||
userId: userInfo.userId,
|
userId: userInfo.userId,
|
||||||
nickname: userInfo.nickname,
|
nickname: userInfo.nickname,
|
||||||
username: userInfo.username,
|
username: userInfo.username,
|
||||||
credits: userInfo.credits,
|
credits: userInfo.credits,
|
||||||
membership: userInfo.membership
|
membership: userInfo.membership,
|
||||||
},
|
},
|
||||||
tierIconUrl: tierIconUrl
|
tierIconUrl: tierIconUrl,
|
||||||
};
|
};
|
||||||
console.log('[ICHelperPanel] 发送用户信息到前端:', messageData);
|
console.log("[ICHelperPanel] 发送用户信息到前端:", messageData);
|
||||||
panel.webview.postMessage(messageData);
|
panel.webview.postMessage(messageData);
|
||||||
} else {
|
} else {
|
||||||
// 如果没有缓存,从 session 中获取
|
// 如果没有缓存,从 session 中获取
|
||||||
@ -201,36 +254,63 @@ export async function showICHelperPanel(
|
|||||||
createIfNone: false,
|
createIfNone: false,
|
||||||
});
|
});
|
||||||
if (session) {
|
if (session) {
|
||||||
console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account);
|
console.log(
|
||||||
|
"[ICHelperPanel] 从 session 获取用户信息, account:",
|
||||||
|
session.account,
|
||||||
|
);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: 'updateUserInfo',
|
command: "updateUserInfo",
|
||||||
userInfo: {
|
userInfo: {
|
||||||
userId: session.account.id,
|
userId: session.account.id,
|
||||||
nickname: session.account.label,
|
nickname: session.account.label,
|
||||||
username: session.account.label
|
username: session.account.label,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ICHelperPanel] 获取用户信息失败:', error);
|
console.error("[ICHelperPanel] 获取用户信息失败:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置余额更新回调
|
||||||
|
setBalanceUpdateCallback((balance: number) => {
|
||||||
|
const userInfo = getCachedUserInfo();
|
||||||
|
if (userInfo) {
|
||||||
|
userInfo.credits = balance;
|
||||||
|
const tierIconUrl = getTierIconUri(
|
||||||
|
panel.webview,
|
||||||
|
context,
|
||||||
|
userInfo.membership?.tierCode,
|
||||||
|
);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateUserInfo",
|
||||||
|
userInfo: {
|
||||||
|
userId: userInfo.userId,
|
||||||
|
nickname: userInfo.nickname,
|
||||||
|
username: userInfo.username,
|
||||||
|
credits: balance,
|
||||||
|
membership: userInfo.membership,
|
||||||
|
},
|
||||||
|
tierIconUrl: tierIconUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 检查是否有待发送的消息
|
// 检查是否有待发送的消息
|
||||||
const pendingMessage = context.globalState.get('pendingMessage') as any;
|
const pendingMessage = context.globalState.get("pendingMessage") as any;
|
||||||
if (pendingMessage) {
|
if (pendingMessage) {
|
||||||
console.log('[ICHelperPanel] 检测到待发送消息,准备自动发送');
|
console.log("[ICHelperPanel] 检测到待发送消息,准备自动发送");
|
||||||
|
|
||||||
// 清除待发送消息
|
// 清除待发送消息
|
||||||
await context.globalState.update('pendingMessage', undefined);
|
await context.globalState.update("pendingMessage", undefined);
|
||||||
|
|
||||||
// 延迟发送,确保面板已完全初始化
|
// 延迟发送,确保面板已完全初始化
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: 'autoSendMessage',
|
command: "autoSendMessage",
|
||||||
text: pendingMessage.text,
|
text: pendingMessage.text,
|
||||||
mode: pendingMessage.mode,
|
mode: pendingMessage.mode,
|
||||||
serviceTier: pendingMessage.serviceTier
|
serviceTier: pendingMessage.serviceTier,
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
@ -252,12 +332,12 @@ export async function showICHelperPanel(
|
|||||||
try {
|
try {
|
||||||
const taskMeta = await historyManager.createTask(
|
const taskMeta = await historyManager.createTask(
|
||||||
workspacePath,
|
workspacePath,
|
||||||
"新对话"
|
"新对话",
|
||||||
);
|
);
|
||||||
historyManager.setPanelTask(
|
historyManager.setPanelTask(
|
||||||
panelId,
|
panelId,
|
||||||
taskMeta.taskId,
|
taskMeta.taskId,
|
||||||
workspacePath
|
workspacePath,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("创建任务失败:", error);
|
console.error("创建任务失败:", error);
|
||||||
@ -268,15 +348,20 @@ export async function showICHelperPanel(
|
|||||||
// 切换到当前面板的任务上下文
|
// 切换到当前面板的任务上下文
|
||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
|
// 启动变更追踪会话
|
||||||
|
const sessionId = `session_${panelId}_${Date.now()}`;
|
||||||
|
startChangeSession(sessionId);
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
panel.webview.postMessage({ type: 'showProgress' });
|
panel.webview.postMessage({ type: "showProgress" });
|
||||||
|
|
||||||
handleUserMessage(
|
handleUserMessage(
|
||||||
panel,
|
panel,
|
||||||
message.text,
|
message.text,
|
||||||
context.extensionPath,
|
context.extensionPath,
|
||||||
message.mode,
|
message.mode,
|
||||||
message.model // 传递服务等级
|
message.model, // 传递服务等级
|
||||||
|
message.contextItems, // 传递上下文项
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "readFile":
|
case "readFile":
|
||||||
@ -293,7 +378,7 @@ export async function showICHelperPanel(
|
|||||||
panel,
|
panel,
|
||||||
message.filePath,
|
message.filePath,
|
||||||
message.searchText,
|
message.searchText,
|
||||||
message.replaceText
|
message.replaceText,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "insertCode":
|
case "insertCode":
|
||||||
@ -305,7 +390,10 @@ export async function showICHelperPanel(
|
|||||||
case "openWaveformViewer":
|
case "openWaveformViewer":
|
||||||
// 在新列中打开波形查看器
|
// 在新列中打开波形查看器
|
||||||
if (message.vcdFilePath) {
|
if (message.vcdFilePath) {
|
||||||
vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath);
|
vscode.commands.executeCommand(
|
||||||
|
"ic-coder.openVCDViewer",
|
||||||
|
message.vcdFilePath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "getVCDInfo":
|
case "getVCDInfo":
|
||||||
@ -323,7 +411,7 @@ export async function showICHelperPanel(
|
|||||||
loadConversationHistory(
|
loadConversationHistory(
|
||||||
panel,
|
panel,
|
||||||
message.offset || 0,
|
message.offset || 0,
|
||||||
message.limit || 10
|
message.limit || 10,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "selectConversation":
|
case "selectConversation":
|
||||||
@ -332,7 +420,7 @@ export async function showICHelperPanel(
|
|||||||
selectConversation(
|
selectConversation(
|
||||||
panel,
|
panel,
|
||||||
message.conversationId,
|
message.conversationId,
|
||||||
context.extensionPath
|
context.extensionPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -341,7 +429,7 @@ export async function showICHelperPanel(
|
|||||||
void handleUserAnswer(
|
void handleUserAnswer(
|
||||||
message.askId,
|
message.askId,
|
||||||
message.selected,
|
message.selected,
|
||||||
message.customInput
|
message.customInput,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
// 新增:中止对话
|
// 新增:中止对话
|
||||||
@ -396,36 +484,119 @@ export async function showICHelperPanel(
|
|||||||
// 退出登录
|
// 退出登录
|
||||||
vscode.commands.executeCommand("ic-coder.logout");
|
vscode.commands.executeCommand("ic-coder.logout");
|
||||||
break;
|
break;
|
||||||
|
case "acceptChange":
|
||||||
|
// 采纳变更
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleAcceptChange(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "rejectChange":
|
||||||
|
// 拒绝变更
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleRejectChange(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "openFileDiff":
|
||||||
|
// 打开文件 diff
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleOpenFileDiff(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "checkInvitationCode":
|
case "checkInvitationCode":
|
||||||
// 检查邀请码验证状态
|
// 检查邀请码验证状态
|
||||||
{
|
{
|
||||||
const { InvitationService } = require("../services/invitationService");
|
// 先检查是否是试用用户
|
||||||
|
const { getCachedUserInfo } = require("../services/userService");
|
||||||
|
const userInfo = getCachedUserInfo();
|
||||||
|
|
||||||
|
if (userInfo?.isPluginTrial === true) {
|
||||||
|
// 试用用户,跳过邀请码验证,直接返回已验证
|
||||||
|
console.log("[ICHelperPanel] 试用用户,跳过邀请码验证");
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "invitationCodeStatus",
|
||||||
|
verified: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 正式用户,检查邀请码
|
||||||
|
const {
|
||||||
|
InvitationService,
|
||||||
|
} = require("../services/invitationService");
|
||||||
const isVerified = await InvitationService.isVerified(context);
|
const isVerified = await InvitationService.isVerified(context);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "invitationCodeStatus",
|
command: "invitationCodeStatus",
|
||||||
verified: isVerified
|
verified: isVerified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "checkWelcomeModal":
|
||||||
|
// 检查是否需要显示欢迎弹窗
|
||||||
|
{
|
||||||
|
console.log("[ICHelperPanel] 收到 checkWelcomeModal 消息");
|
||||||
|
const showWelcome = context.globalState.get("showWelcomeModal");
|
||||||
|
console.log(
|
||||||
|
"[ICHelperPanel] showWelcomeModal 标记值:",
|
||||||
|
showWelcome,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showWelcome) {
|
||||||
|
// 清除标记并显示欢迎弹窗
|
||||||
|
await context.globalState.update("showWelcomeModal", undefined);
|
||||||
|
console.log(
|
||||||
|
"[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端",
|
||||||
|
);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "showWelcomeModal",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"[ICHelperPanel] showWelcomeModal 标记为 false,不显示弹窗",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "checkTrialExpiration":
|
||||||
|
// 检查试用期是否过期
|
||||||
|
{
|
||||||
|
console.log("[ICHelperPanel] 收到 checkTrialExpiration 消息");
|
||||||
|
const {
|
||||||
|
TrialExpirationService,
|
||||||
|
} = require("../services/trialExpirationService");
|
||||||
|
const trialService = new TrialExpirationService(context, panel);
|
||||||
|
const isExpired = await trialService.checkExpiration();
|
||||||
|
console.log("[ICHelperPanel] 试用期过期状态:", isExpired);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "verifyInvitationCode":
|
case "verifyInvitationCode":
|
||||||
// 验证邀请码
|
// 验证邀请码
|
||||||
{
|
{
|
||||||
const { InvitationService } = require("../services/invitationService");
|
const {
|
||||||
|
InvitationService,
|
||||||
|
} = require("../services/invitationService");
|
||||||
const result = await InvitationService.verifyCode(message.code);
|
const result = await InvitationService.verifyCode(message.code);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 验证成功,保存状态
|
// 验证成功,保存状态
|
||||||
await InvitationService.saveVerificationStatus(context, message.code);
|
await InvitationService.saveVerificationStatus(
|
||||||
|
context,
|
||||||
|
message.code,
|
||||||
|
);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "invitationCodeVerified",
|
command: "invitationCodeVerified",
|
||||||
success: true
|
success: true,
|
||||||
});
|
});
|
||||||
|
// 延迟显示欢迎弹窗,确保邀请码弹窗已关闭
|
||||||
|
setTimeout(() => {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "showNdtWelcomeModal",
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
// 验证失败,返回错误信息
|
// 验证失败,返回错误信息
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "invitationCodeVerified",
|
command: "invitationCodeVerified",
|
||||||
success: false,
|
success: false,
|
||||||
message: result.message
|
message: result.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -434,6 +605,14 @@ export async function showICHelperPanel(
|
|||||||
// 跳转到 IC Coder 官网
|
// 跳转到 IC Coder 官网
|
||||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||||
break;
|
break;
|
||||||
|
case "openTutorial":
|
||||||
|
// 打开使用教程
|
||||||
|
vscode.env.openExternal(
|
||||||
|
vscode.Uri.parse(
|
||||||
|
"https://www.iccoder.com/guides/quick-start/first-task-plugin",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
case "openUserManual":
|
case "openUserManual":
|
||||||
// 打开用户手册
|
// 打开用户手册
|
||||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||||
@ -441,7 +620,7 @@ export async function showICHelperPanel(
|
|||||||
case "openUserFeedback":
|
case "openUserFeedback":
|
||||||
// 打开用户反馈二维码弹窗
|
// 打开用户反馈二维码弹窗
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "showFeedbackQRCode"
|
command: "showFeedbackQRCode",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||||
@ -453,13 +632,16 @@ export async function showICHelperPanel(
|
|||||||
mode: "agent",
|
mode: "agent",
|
||||||
});
|
});
|
||||||
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
|
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
|
||||||
} else if (message.action === "modify" || message.action === "cancel") {
|
} else if (
|
||||||
|
message.action === "modify" ||
|
||||||
|
message.action === "cancel"
|
||||||
|
) {
|
||||||
void handlePlanAction(
|
void handlePlanAction(
|
||||||
panel,
|
panel,
|
||||||
message.action,
|
message.action,
|
||||||
message.planTitle || "",
|
message.planTitle || "",
|
||||||
context.extensionPath,
|
context.extensionPath,
|
||||||
message.model
|
message.model,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -475,7 +657,7 @@ export async function showICHelperPanel(
|
|||||||
// 获取工作区所有文件
|
// 获取工作区所有文件
|
||||||
const files = await vscode.workspace.findFiles(
|
const files = await vscode.workspace.findFiles(
|
||||||
"**/*",
|
"**/*",
|
||||||
"**/node_modules/**"
|
"**/node_modules/**",
|
||||||
);
|
);
|
||||||
|
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -505,7 +687,11 @@ export async function showICHelperPanel(
|
|||||||
try {
|
try {
|
||||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
|
if (
|
||||||
|
item.isDirectory() &&
|
||||||
|
item.name !== "node_modules" &&
|
||||||
|
!item.name.startsWith(".")
|
||||||
|
) {
|
||||||
const fullPath = path.join(dir, item.name);
|
const fullPath = path.join(dir, item.name);
|
||||||
const relativePath = path.relative(baseDir, fullPath);
|
const relativePath = path.relative(baseDir, fullPath);
|
||||||
folders.push({ path: fullPath, relativePath });
|
folders.push({ path: fullPath, relativePath });
|
||||||
@ -534,7 +720,7 @@ export async function showICHelperPanel(
|
|||||||
canSelectMany: true,
|
canSelectMany: true,
|
||||||
openLabel: "选择图片",
|
openLabel: "选择图片",
|
||||||
filters: {
|
filters: {
|
||||||
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
图片文件: ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (imageUris && imageUris.length > 0) {
|
if (imageUris && imageUris.length > 0) {
|
||||||
@ -554,8 +740,8 @@ export async function showICHelperPanel(
|
|||||||
canSelectMany: true,
|
canSelectMany: true,
|
||||||
openLabel: "选择文档",
|
openLabel: "选择文档",
|
||||||
filters: {
|
filters: {
|
||||||
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
|
文档文件: ["pdf", "doc", "docx", "txt", "md"],
|
||||||
"所有文件": ["*"],
|
所有文件: ["*"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (docUris && docUris.length > 0) {
|
if (docUris && docUris.length > 0) {
|
||||||
@ -577,7 +763,7 @@ export async function showICHelperPanel(
|
|||||||
vscode.window
|
vscode.window
|
||||||
.showWarningMessage(
|
.showWarningMessage(
|
||||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||||
"打开文件夹"
|
"打开文件夹",
|
||||||
)
|
)
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
if (selection === "打开文件夹") {
|
if (selection === "打开文件夹") {
|
||||||
@ -599,16 +785,16 @@ export async function showICHelperPanel(
|
|||||||
break;
|
break;
|
||||||
case "openICCoder":
|
case "openICCoder":
|
||||||
// 打开 IC Coder 官网
|
// 打开 IC Coder 官网
|
||||||
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||||
break;
|
break;
|
||||||
case "logout":
|
case "logout":
|
||||||
// 退出登录(前端已有确认对话框)
|
// 退出登录(前端已有确认对话框)
|
||||||
vscode.commands.executeCommand('ic-coder.logout');
|
vscode.commands.executeCommand("ic-coder.logout");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
context.subscriptions
|
context.subscriptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 面板关闭时清理任务映射
|
// 面板关闭时清理任务映射
|
||||||
@ -619,7 +805,7 @@ export async function showICHelperPanel(
|
|||||||
historyManager.removePanelTask(panelId);
|
historyManager.removePanelTask(panelId);
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
context.subscriptions
|
context.subscriptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,7 +815,7 @@ export async function showICHelperPanel(
|
|||||||
async function getVCDFileInfo(
|
async function getVCDFileInfo(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
vcdFilePath: string,
|
vcdFilePath: string,
|
||||||
containerId: string
|
containerId: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
@ -767,7 +953,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
if (signalDef.width === 1) {
|
if (signalDef.width === 1) {
|
||||||
// 单比特信号
|
// 单比特信号
|
||||||
const singleBitMatch = trimmedLine.match(
|
const singleBitMatch = trimmedLine.match(
|
||||||
new RegExp(`^([01xz])${signalDef.identifier}$`)
|
new RegExp(`^([01xz])${signalDef.identifier}$`),
|
||||||
);
|
);
|
||||||
if (singleBitMatch) {
|
if (singleBitMatch) {
|
||||||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||||
@ -775,7 +961,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
} else {
|
} else {
|
||||||
// 多比特信号
|
// 多比特信号
|
||||||
const multiBitMatch = trimmedLine.match(
|
const multiBitMatch = trimmedLine.match(
|
||||||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`)
|
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`),
|
||||||
);
|
);
|
||||||
if (multiBitMatch) {
|
if (multiBitMatch) {
|
||||||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||||
@ -808,7 +994,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
async function loadConversationHistory(
|
async function loadConversationHistory(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
limit: number = 10
|
limit: number = 10,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
@ -829,7 +1015,7 @@ async function loadConversationHistory(
|
|||||||
const result = await historyManager.getConversationHistoryList(
|
const result = await historyManager.getConversationHistoryList(
|
||||||
workspacePath,
|
workspacePath,
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 发送会话历史到前端
|
// 发送会话历史到前端
|
||||||
@ -857,7 +1043,7 @@ async function loadConversationHistory(
|
|||||||
async function selectConversation(
|
async function selectConversation(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
taskId: string,
|
taskId: string,
|
||||||
extensionPath: string
|
extensionPath: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
@ -871,12 +1057,12 @@ async function selectConversation(
|
|||||||
// 加载任务会话
|
// 加载任务会话
|
||||||
const taskSession = await historyManager.loadTaskSession(
|
const taskSession = await historyManager.loadTaskSession(
|
||||||
workspacePath,
|
workspacePath,
|
||||||
taskId
|
taskId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!taskSession) {
|
if (!taskSession) {
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`
|
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1037,7 +1223,7 @@ async function selectConversation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
`已加载会话: ${taskSession.meta.taskName}`
|
`已加载会话: ${taskSession.meta.taskName}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("选择会话失败:", error);
|
console.error("选择会话失败:", error);
|
||||||
|
|||||||
@ -6,8 +6,13 @@ import { VCDFileServer } from "../services/vcdFileServer";
|
|||||||
/**
|
/**
|
||||||
* VCD 波形查看器自定义编辑器提供者
|
* VCD 波形查看器自定义编辑器提供者
|
||||||
*/
|
*/
|
||||||
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider {
|
export class VCDViewerEditorProvider
|
||||||
public static register(context: vscode.ExtensionContext, vcdFileServer: VCDFileServer): vscode.Disposable {
|
implements vscode.CustomReadonlyEditorProvider
|
||||||
|
{
|
||||||
|
public static register(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
vcdFileServer: VCDFileServer,
|
||||||
|
): vscode.Disposable {
|
||||||
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
|
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
|
||||||
const providerRegistration = vscode.window.registerCustomEditorProvider(
|
const providerRegistration = vscode.window.registerCustomEditorProvider(
|
||||||
"ic-coder.vcdViewer",
|
"ic-coder.vcdViewer",
|
||||||
@ -16,20 +21,20 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
|||||||
webviewOptions: {
|
webviewOptions: {
|
||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return providerRegistration;
|
return providerRegistration;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly context: vscode.ExtensionContext,
|
private readonly context: vscode.ExtensionContext,
|
||||||
private readonly vcdFileServer: VCDFileServer
|
private readonly vcdFileServer: VCDFileServer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async openCustomDocument(
|
async openCustomDocument(
|
||||||
uri: vscode.Uri,
|
uri: vscode.Uri,
|
||||||
openContext: vscode.CustomDocumentOpenContext,
|
openContext: vscode.CustomDocumentOpenContext,
|
||||||
token: vscode.CancellationToken
|
token: vscode.CancellationToken,
|
||||||
): Promise<vscode.CustomDocument> {
|
): Promise<vscode.CustomDocument> {
|
||||||
return {
|
return {
|
||||||
uri,
|
uri,
|
||||||
@ -40,7 +45,7 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
|||||||
async resolveCustomEditor(
|
async resolveCustomEditor(
|
||||||
document: vscode.CustomDocument,
|
document: vscode.CustomDocument,
|
||||||
webviewPanel: vscode.WebviewPanel,
|
webviewPanel: vscode.WebviewPanel,
|
||||||
token: vscode.CancellationToken
|
token: vscode.CancellationToken,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
webviewPanel.webview.options = {
|
webviewPanel.webview.options = {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
@ -52,7 +57,7 @@ export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvi
|
|||||||
webviewPanel,
|
webviewPanel,
|
||||||
this.context.extensionUri,
|
this.context.extensionUri,
|
||||||
document.uri.fsPath,
|
document.uri.fsPath,
|
||||||
this.vcdFileServer
|
this.vcdFileServer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,7 +73,11 @@ export class VCDViewerPanel {
|
|||||||
private _currentVcdPath: string | undefined;
|
private _currentVcdPath: string | undefined;
|
||||||
private _vcdFileServer: VCDFileServer | undefined;
|
private _vcdFileServer: VCDFileServer | undefined;
|
||||||
|
|
||||||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) {
|
private constructor(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
extensionUri: vscode.Uri,
|
||||||
|
vcdFileServer?: VCDFileServer,
|
||||||
|
) {
|
||||||
this._panel = panel;
|
this._panel = panel;
|
||||||
this._extensionUri = extensionUri;
|
this._extensionUri = extensionUri;
|
||||||
this._vcdFileServer = vcdFileServer;
|
this._vcdFileServer = vcdFileServer;
|
||||||
@ -91,7 +100,10 @@ export class VCDViewerPanel {
|
|||||||
break;
|
break;
|
||||||
case "loaded":
|
case "loaded":
|
||||||
// Surfer iframe 加载完成,发送 VCD 文件
|
// Surfer iframe 加载完成,发送 VCD 文件
|
||||||
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
|
console.log(
|
||||||
|
"[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:",
|
||||||
|
this._currentVcdPath,
|
||||||
|
);
|
||||||
if (this._currentVcdPath) {
|
if (this._currentVcdPath) {
|
||||||
this.sendVcdToSurfer(this._currentVcdPath);
|
this.sendVcdToSurfer(this._currentVcdPath);
|
||||||
}
|
}
|
||||||
@ -99,14 +111,18 @@ export class VCDViewerPanel {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
this._disposables
|
this._disposables,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建或显示 VCD 查看器面板
|
* 创建或显示 VCD 查看器面板
|
||||||
*/
|
*/
|
||||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
public static createOrShow(
|
||||||
|
extensionUri: vscode.Uri,
|
||||||
|
vcdFilePath?: string,
|
||||||
|
vcdFileServer?: VCDFileServer,
|
||||||
|
) {
|
||||||
// 在当前活动编辑器旁边打开新列
|
// 在当前活动编辑器旁边打开新列
|
||||||
const column = vscode.ViewColumn.Beside;
|
const column = vscode.ViewColumn.Beside;
|
||||||
|
|
||||||
@ -128,10 +144,14 @@ export class VCDViewerPanel {
|
|||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [extensionUri],
|
localResourceRoots: [extensionUri],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
VCDViewerPanel.currentPanel = new VCDViewerPanel(
|
||||||
|
panel,
|
||||||
|
extensionUri,
|
||||||
|
vcdFileServer,
|
||||||
|
);
|
||||||
|
|
||||||
// 如果提供了 VCD 文件路径,加载它
|
// 如果提供了 VCD 文件路径,加载它
|
||||||
if (vcdFilePath) {
|
if (vcdFilePath) {
|
||||||
@ -146,7 +166,7 @@ export class VCDViewerPanel {
|
|||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
extensionUri: vscode.Uri,
|
extensionUri: vscode.Uri,
|
||||||
vcdFilePath: string,
|
vcdFilePath: string,
|
||||||
vcdFileServer?: VCDFileServer
|
vcdFileServer?: VCDFileServer,
|
||||||
) {
|
) {
|
||||||
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||||
viewer.loadVCDFile(vcdFilePath);
|
viewer.loadVCDFile(vcdFilePath);
|
||||||
@ -172,14 +192,14 @@ export class VCDViewerPanel {
|
|||||||
|
|
||||||
// 更新面板标题
|
// 更新面板标题
|
||||||
const fileName = path.basename(vcdFilePath);
|
const fileName = path.basename(vcdFilePath);
|
||||||
this._panel.title = `Surfer 波形查看器 - ${fileName}`;
|
this._panel.title = `波形查看器 - ${fileName}`;
|
||||||
|
|
||||||
// 设置 HTML 内容
|
// 设置 HTML 内容
|
||||||
this._panel.webview.html = this._getWebviewContent();
|
this._panel.webview.html = this._getWebviewContent();
|
||||||
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,8 +210,8 @@ export class VCDViewerPanel {
|
|||||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||||
try {
|
try {
|
||||||
// 读取 VCD 文件
|
// 读取 VCD 文件
|
||||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
|
||||||
const lines = buffer.split('\n');
|
const lines = buffer.split("\n");
|
||||||
|
|
||||||
const scopeNames: string[] = [];
|
const scopeNames: string[] = [];
|
||||||
let scopeDepth = 0;
|
let scopeDepth = 0;
|
||||||
@ -201,7 +221,7 @@ export class VCDViewerPanel {
|
|||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
// 遇到 $enddefinitions 就停止解析
|
// 遇到 $enddefinitions 就停止解析
|
||||||
if (trimmed.startsWith('$enddefinitions')) {
|
if (trimmed.startsWith("$enddefinitions")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,22 +232,22 @@ export class VCDViewerPanel {
|
|||||||
const scopeName = scopeMatch[2];
|
const scopeName = scopeMatch[2];
|
||||||
|
|
||||||
// 记录顶层 module (depth = 0)
|
// 记录顶层 module (depth = 0)
|
||||||
if (scopeDepth === 0 && scopeType === 'module') {
|
if (scopeDepth === 0 && scopeType === "module") {
|
||||||
scopeStack.push(scopeName);
|
scopeStack.push(scopeName);
|
||||||
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
|
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
|
||||||
}
|
}
|
||||||
// 记录顶层下的直接子模块 (depth = 1)
|
// 记录顶层下的直接子模块 (depth = 1)
|
||||||
else if (scopeDepth === 1 && scopeType === 'module') {
|
else if (scopeDepth === 1 && scopeType === "module") {
|
||||||
const fullPath = [...scopeStack, scopeName];
|
const fullPath = [...scopeStack, scopeName];
|
||||||
scopeNames.push(fullPath.join('.'));
|
scopeNames.push(fullPath.join("."));
|
||||||
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.'));
|
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join("."));
|
||||||
}
|
}
|
||||||
|
|
||||||
scopeDepth++;
|
scopeDepth++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 遇到 $upscope 减少深度
|
// 遇到 $upscope 减少深度
|
||||||
if (trimmed.startsWith('$upscope')) {
|
if (trimmed.startsWith("$upscope")) {
|
||||||
scopeDepth--;
|
scopeDepth--;
|
||||||
if (scopeDepth === 0) {
|
if (scopeDepth === 0) {
|
||||||
scopeStack.pop();
|
scopeStack.pop();
|
||||||
@ -277,7 +297,7 @@ export class VCDViewerPanel {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
|
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`
|
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,13 +372,23 @@ export class VCDViewerPanel {
|
|||||||
private _getWebviewContent(): string {
|
private _getWebviewContent(): string {
|
||||||
// 获取 surfer 资源 URI
|
// 获取 surfer 资源 URI
|
||||||
const surferJsUri = this._panel.webview.asWebviewUri(
|
const surferJsUri = this._panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
|
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js"),
|
||||||
);
|
);
|
||||||
const surferWasmUri = this._panel.webview.asWebviewUri(
|
const surferWasmUri = this._panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
|
vscode.Uri.joinPath(
|
||||||
|
this._extensionUri,
|
||||||
|
"media",
|
||||||
|
"surfer",
|
||||||
|
"surfer_bg.wasm",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const integrationJsUri = this._panel.webview.asWebviewUri(
|
const integrationJsUri = this._panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js")
|
vscode.Uri.joinPath(
|
||||||
|
this._extensionUri,
|
||||||
|
"media",
|
||||||
|
"surfer",
|
||||||
|
"integration.js",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
@ -367,7 +397,7 @@ export class VCDViewerPanel {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
|
||||||
<title>Surfer 波形查看器</title>
|
<title>波形查看器</title>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 获取 VS Code API(只能调用一次)
|
// 获取 VS Code API(只能调用一次)
|
||||||
|
|||||||
153
src/panels/WelcomePanel.ts
Normal file
153
src/panels/WelcomePanel.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* 欢迎引导面板
|
||||||
|
* 功能:插件试用用户首次登录显示使用教程
|
||||||
|
* 依赖:vscode
|
||||||
|
* 使用场景:试用用户首次登录时显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class WelcomePanel {
|
||||||
|
public static currentPanel: WelcomePanel | undefined;
|
||||||
|
private readonly _panel: vscode.WebviewPanel;
|
||||||
|
private _disposables: vscode.Disposable[] = [];
|
||||||
|
|
||||||
|
private constructor(panel: vscode.WebviewPanel) {
|
||||||
|
this._panel = panel;
|
||||||
|
this._panel.webview.html = this.getHtmlContent();
|
||||||
|
|
||||||
|
// 监听来自 webview 的消息
|
||||||
|
this._panel.webview.onDidReceiveMessage(
|
||||||
|
(message) => {
|
||||||
|
if (message.command === 'close') {
|
||||||
|
this._panel.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
this._disposables
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听关闭事件
|
||||||
|
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static render(context: vscode.ExtensionContext) {
|
||||||
|
// 避免重复显示
|
||||||
|
if (WelcomePanel.currentPanel) {
|
||||||
|
WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'icCoderWelcome',
|
||||||
|
'欢迎使用 IC Coder',
|
||||||
|
vscode.ViewColumn.One,
|
||||||
|
{
|
||||||
|
enableScripts: true,
|
||||||
|
retainContextWhenHidden: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
WelcomePanel.currentPanel = new WelcomePanel(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlContent(): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>欢迎使用 IC Coder</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 40px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
background-color: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.welcome-message {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
.step h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
.step p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🎉 欢迎使用 IC Coder!</h1>
|
||||||
|
<p class="welcome-message">
|
||||||
|
您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>📝 步骤 1:打开聊天面板</h3>
|
||||||
|
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>💬 步骤 2:输入您的需求</h3>
|
||||||
|
<p>描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h3>🔬 步骤 3:运行仿真</h3>
|
||||||
|
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button" onclick="closePanel()">开始使用</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
|
||||||
|
function closePanel() {
|
||||||
|
vscode.postMessage({ command: 'close' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
WelcomePanel.currentPanel = undefined;
|
||||||
|
this._panel.dispose();
|
||||||
|
while (this._disposables.length) {
|
||||||
|
const disposable = this._disposables.pop();
|
||||||
|
if (disposable) {
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,18 +2,28 @@
|
|||||||
* API 客户端
|
* API 客户端
|
||||||
* 封装与后端的 HTTP 通信
|
* 封装与后端的 HTTP 通信
|
||||||
*/
|
*/
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from "vscode";
|
||||||
import * as https from 'https';
|
import * as https from "https";
|
||||||
import * as http from 'http';
|
import * as http from "http";
|
||||||
import { URL } from 'url';
|
import { URL } from "url";
|
||||||
import { getApiUrl, getConfig } from '../config/settings';
|
import { getApiUrl, getConfig } from "../config/settings";
|
||||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse, UserInfoResponse, InvitationVerifyRequest, InvitationVerifyResponse, InvitationStatusResponse } from '../types/api';
|
import type {
|
||||||
|
ToolCallResult,
|
||||||
|
AnswerRequest,
|
||||||
|
ToolResultResponse,
|
||||||
|
AnswerResponse,
|
||||||
|
ToolConfirmResponse,
|
||||||
|
UserInfoResponse,
|
||||||
|
InvitationVerifyRequest,
|
||||||
|
InvitationVerifyResponse,
|
||||||
|
InvitationStatusResponse,
|
||||||
|
} from "../types/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP 请求选项
|
* HTTP 请求选项
|
||||||
*/
|
*/
|
||||||
interface RequestOptions {
|
interface RequestOptions {
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
@ -24,7 +34,9 @@ interface RequestOptions {
|
|||||||
*/
|
*/
|
||||||
async function getAuthToken(): Promise<string | undefined> {
|
async function getAuthToken(): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
return session?.accessToken;
|
return session?.accessToken;
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -41,7 +53,7 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
|||||||
// 自动获取 Token
|
// 自动获取 Token
|
||||||
const token = await getAuthToken();
|
const token = await getAuthToken();
|
||||||
|
|
||||||
const isHttps = url.protocol === 'https:';
|
const isHttps = url.protocol === "https:";
|
||||||
const httpModule = isHttps ? https : http;
|
const httpModule = isHttps ? https : http;
|
||||||
|
|
||||||
const requestOptions: http.RequestOptions = {
|
const requestOptions: http.RequestOptions = {
|
||||||
@ -50,49 +62,84 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
|||||||
path: url.pathname + url.search,
|
path: url.pathname + url.search,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
...options.headers
|
...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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const req = httpModule.request(requestOptions, (res) => {
|
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;
|
data += chunk;
|
||||||
});
|
});
|
||||||
|
|
||||||
res.on('end', () => {
|
res.on("end", () => {
|
||||||
|
console.log("[HTTP] 响应体:", data);
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(data);
|
const json = JSON.parse(data);
|
||||||
|
// console.log('[HTTP] 解析后的响应:', JSON.stringify(json, null, 2));
|
||||||
|
|
||||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
console.log("[HTTP] 请求成功");
|
||||||
resolve(json as T);
|
resolve(json as T);
|
||||||
} else {
|
} 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) {
|
} catch (e) {
|
||||||
|
// console.error('[HTTP] 解析响应失败:', e);
|
||||||
|
// console.error('[HTTP] 原始响应:', data);
|
||||||
reject(new Error(`解析响应失败: ${data}`));
|
reject(new Error(`解析响应失败: ${data}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', (error) => {
|
req.on("error", (error) => {
|
||||||
|
// console.error('[HTTP] 请求错误:', error);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('timeout', () => {
|
req.on("timeout", () => {
|
||||||
|
// console.error('[HTTP] 请求超时');
|
||||||
req.destroy();
|
req.destroy();
|
||||||
reject(new Error('请求超时'));
|
reject(new Error("请求超时"));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.body) {
|
if (options.body) {
|
||||||
req.write(JSON.stringify(options.body));
|
const bodyStr = JSON.stringify(options.body);
|
||||||
|
// console.log('[HTTP] 发送请求体:', bodyStr);
|
||||||
|
req.write(bodyStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.end();
|
req.end();
|
||||||
|
// console.log('[HTTP] 请求已发送');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,11 +147,13 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
|||||||
* 提交工具执行结果
|
* 提交工具执行结果
|
||||||
* POST /api/tool/result
|
* 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}`);
|
console.log(`[API] 提交工具结果: callId=${result.id}`);
|
||||||
return request<ToolResultResponse>('/api/tool/result', {
|
return request<ToolResultResponse>("/api/tool/result", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: result
|
body: result,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,11 +161,13 @@ export async function submitToolResult(result: ToolCallResult): Promise<ToolResu
|
|||||||
* 提交用户回答
|
* 提交用户回答
|
||||||
* POST /api/task/answer
|
* 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}`);
|
console.log(`[API] 提交用户回答: askId=${answer.askId}`);
|
||||||
return request<AnswerResponse>('/api/task/answer', {
|
return request<AnswerResponse>("/api/task/answer", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: answer
|
body: answer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,11 +175,15 @@ export async function submitAnswer(answer: AnswerRequest): Promise<AnswerRespons
|
|||||||
* 提交工具确认响应(Ask 模式)
|
* 提交工具确认响应(Ask 模式)
|
||||||
* POST /api/tool/confirm
|
* POST /api/tool/confirm
|
||||||
*/
|
*/
|
||||||
export async function submitToolConfirm(response: ToolConfirmResponse): Promise<ToolResultResponse> {
|
export async function submitToolConfirm(
|
||||||
console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`);
|
response: ToolConfirmResponse,
|
||||||
return request<ToolResultResponse>('/api/tool/confirm', {
|
): Promise<ToolResultResponse> {
|
||||||
method: 'POST',
|
console.log(
|
||||||
body: response
|
`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`,
|
||||||
|
);
|
||||||
|
return request<ToolResultResponse>("/api/tool/confirm", {
|
||||||
|
method: "POST",
|
||||||
|
body: response,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,9 +192,9 @@ export async function submitToolConfirm(response: ToolConfirmResponse): Promise<
|
|||||||
* GET /api/dialog/health
|
* GET /api/dialog/health
|
||||||
*/
|
*/
|
||||||
export async function healthCheck(): Promise<{ status: string }> {
|
export async function healthCheck(): Promise<{ status: string }> {
|
||||||
return request<{ status: string }>('/api/dialog/health', {
|
return request<{ status: string }>("/api/dialog/health", {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
timeout: 5000
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,9 +221,9 @@ export interface StopDialogResponse {
|
|||||||
*/
|
*/
|
||||||
export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
|
export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
|
||||||
console.log(`[API] 停止对话: taskId=${taskId}`);
|
console.log(`[API] 停止对话: taskId=${taskId}`);
|
||||||
return request<StopDialogResponse>('/api/dialog/stop', {
|
return request<StopDialogResponse>("/api/dialog/stop", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: { taskId }
|
body: { taskId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,11 +239,13 @@ export interface CompactDialogResponse {
|
|||||||
* 手动压缩对话历史
|
* 手动压缩对话历史
|
||||||
* POST /api/dialog/compact
|
* 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}`);
|
console.log(`[API] 压缩对话: taskId=${taskId}`);
|
||||||
return request<CompactDialogResponse>('/api/dialog/compact', {
|
return request<CompactDialogResponse>("/api/dialog/compact", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: { taskId }
|
body: { taskId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,37 +254,44 @@ export async function compactDialog(taskId: string): Promise<CompactDialogRespon
|
|||||||
*/
|
*/
|
||||||
export function createSuccessResult(id: number, text: string): ToolCallResult {
|
export function createSuccessResult(id: number, text: string): ToolCallResult {
|
||||||
return {
|
return {
|
||||||
jsonrpc: '2.0',
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
content: [{ type: 'text', text }],
|
content: [{ type: "text", text }],
|
||||||
isError: false
|
isError: false,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建业务错误的工具结果(如编译失败)
|
* 创建业务错误的工具结果(如编译失败)
|
||||||
*/
|
*/
|
||||||
export function createBusinessErrorResult(id: number, errorMessage: string): ToolCallResult {
|
export function createBusinessErrorResult(
|
||||||
|
id: number,
|
||||||
|
errorMessage: string,
|
||||||
|
): ToolCallResult {
|
||||||
return {
|
return {
|
||||||
jsonrpc: '2.0',
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
content: [{ type: 'text', text: errorMessage }],
|
content: [{ type: "text", text: errorMessage }],
|
||||||
isError: true
|
isError: true,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建系统错误的工具结果
|
* 创建系统错误的工具结果
|
||||||
*/
|
*/
|
||||||
export function createSystemErrorResult(id: number, code: number, message: string): ToolCallResult {
|
export function createSystemErrorResult(
|
||||||
|
id: number,
|
||||||
|
code: number,
|
||||||
|
message: string,
|
||||||
|
): ToolCallResult {
|
||||||
return {
|
return {
|
||||||
jsonrpc: '2.0',
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
error: { code, message }
|
error: { code, message },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,9 +300,9 @@ export function createSystemErrorResult(id: number, code: number, message: strin
|
|||||||
* GET /system/user/getInfo
|
* GET /system/user/getInfo
|
||||||
*/
|
*/
|
||||||
export async function getUserInfo(): Promise<UserInfoResponse> {
|
export async function getUserInfo(): Promise<UserInfoResponse> {
|
||||||
console.log('[API] 获取用户信息');
|
console.log("[API] 获取用户信息");
|
||||||
return request<UserInfoResponse>('/system/user/getInfo', {
|
return request<UserInfoResponse>("/system/user/getInfo", {
|
||||||
method: 'GET'
|
method: "GET",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,25 +317,45 @@ export interface CreditBalanceResponse {
|
|||||||
* 查询用户资源点余额
|
* 查询用户资源点余额
|
||||||
* GET /api/dialog/balance?userId=xxx
|
* GET /api/dialog/balance?userId=xxx
|
||||||
*/
|
*/
|
||||||
export async function getCreditBalance(userId: string): Promise<CreditBalanceResponse> {
|
export async function getCreditBalance(
|
||||||
console.log('[API] 查询余额: userId=', userId);
|
userId: string,
|
||||||
return request<CreditBalanceResponse>(`/api/dialog/balance?userId=${userId}`, {
|
): Promise<CreditBalanceResponse> {
|
||||||
method: 'GET',
|
console.log("[API] 查询余额: userId=", userId);
|
||||||
timeout: 5000
|
return request<CreditBalanceResponse>(
|
||||||
});
|
`/api/dialog/balance?userId=${userId}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证邀请码
|
* 验证邀请码
|
||||||
* POST /api/invitation/verify
|
* POST /api/invitation/verify
|
||||||
*/
|
*/
|
||||||
export async function verifyInvitationCode(code: string): Promise<InvitationVerifyResponse> {
|
export async function verifyInvitationCode(
|
||||||
console.log('[API] 验证邀请码');
|
code: string,
|
||||||
|
): Promise<InvitationVerifyResponse> {
|
||||||
|
// console.log('[API] 验证邀请码 - 开始');
|
||||||
|
console.log("[API] 邀请码:", code);
|
||||||
const body: InvitationVerifyRequest = { code };
|
const body: InvitationVerifyRequest = { code };
|
||||||
return request<InvitationVerifyResponse>('/api/invitation/verify', {
|
console.log("[API] 请求体:", JSON.stringify(body));
|
||||||
method: 'POST',
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -279,8 +363,33 @@ export async function verifyInvitationCode(code: string): Promise<InvitationVeri
|
|||||||
* GET /api/invitation/status
|
* GET /api/invitation/status
|
||||||
*/
|
*/
|
||||||
export async function checkInvitationStatus(): Promise<InvitationStatusResponse> {
|
export async function checkInvitationStatus(): Promise<InvitationStatusResponse> {
|
||||||
console.log('[API] 查询邀请码验证状态');
|
console.log("[API] 查询邀请码验证状态");
|
||||||
return request<InvitationStatusResponse>('/api/invitation/status', {
|
return request<InvitationStatusResponse>("/api/invitation/status", {
|
||||||
method: 'GET'
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
205
src/services/changeTracker.ts
Normal file
205
src/services/changeTracker.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* 文件变更追踪服务
|
||||||
|
* 功能:收集和管理 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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
this.currentSession = null;
|
||||||
|
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();
|
||||||
@ -25,6 +25,9 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
|
|||||||
/** ExtensionContext 用于持久化存储 */
|
/** ExtensionContext 用于持久化存储 */
|
||||||
let extensionContext: vscode.ExtensionContext | null = null;
|
let extensionContext: vscode.ExtensionContext | null = null;
|
||||||
|
|
||||||
|
/** 余额更新回调函数 */
|
||||||
|
let onBalanceUpdateCallback: ((balance: number) => void) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化 Credits 服务(设置 context)
|
* 初始化 Credits 服务(设置 context)
|
||||||
*/
|
*/
|
||||||
@ -39,6 +42,13 @@ export function initCreditsService(context: vscode.ExtensionContext): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置余额更新回调
|
||||||
|
*/
|
||||||
|
export function setBalanceUpdateCallback(callback: (balance: number) => void): void {
|
||||||
|
onBalanceUpdateCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存余额到持久化存储
|
* 保存余额到持久化存储
|
||||||
*/
|
*/
|
||||||
@ -60,6 +70,10 @@ export function updateCachedBalance(balance: number): void {
|
|||||||
saveBalance(balance).catch(err => {
|
saveBalance(balance).catch(err => {
|
||||||
console.error('[CreditsService] 保存余额失败:', err);
|
console.error('[CreditsService] 保存余额失败:', err);
|
||||||
});
|
});
|
||||||
|
// 通知前端更新余额显示
|
||||||
|
if (onBalanceUpdateCallback) {
|
||||||
|
onBalanceUpdateCallback(balance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export interface MessageSegment {
|
|||||||
toolName?: string;
|
toolName?: string;
|
||||||
toolStatus?: "running" | "success" | "error";
|
toolStatus?: "running" | "success" | "error";
|
||||||
toolResult?: string;
|
toolResult?: string;
|
||||||
|
toolDescription?: string;
|
||||||
askId?: string;
|
askId?: string;
|
||||||
question?: string;
|
question?: string;
|
||||||
options?: string[];
|
options?: string[];
|
||||||
@ -180,7 +181,8 @@ export class DialogSession {
|
|||||||
private updateToolSegment(
|
private updateToolSegment(
|
||||||
toolName: string,
|
toolName: string,
|
||||||
status: "success" | "error",
|
status: "success" | "error",
|
||||||
result?: string
|
result?: string,
|
||||||
|
description?: string
|
||||||
): void {
|
): void {
|
||||||
// 找到最后一个匹配的工具段落
|
// 找到最后一个匹配的工具段落
|
||||||
for (let i = this.segments.length - 1; i >= 0; i--) {
|
for (let i = this.segments.length - 1; i >= 0; i--) {
|
||||||
@ -192,6 +194,9 @@ export class DialogSession {
|
|||||||
) {
|
) {
|
||||||
seg.toolStatus = status;
|
seg.toolStatus = status;
|
||||||
seg.toolResult = result;
|
seg.toolResult = result;
|
||||||
|
if (description !== undefined) {
|
||||||
|
seg.toolDescription = description;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -590,7 +595,7 @@ export class DialogSession {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onToolComplete: (data) => {
|
onToolComplete: (data) => {
|
||||||
this.updateToolSegment(data.tool_name, "success", data.result);
|
this.updateToolSegment(data.tool_name, "success", data.result, data.description);
|
||||||
callbacks.onToolComplete?.(data.tool_name, data.result);
|
callbacks.onToolComplete?.(data.tool_name, data.result);
|
||||||
// 实时发送段落更新
|
// 实时发送段落更新
|
||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import * as path from "path";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||||
import { getConfig } from "../config/settings";
|
import { getConfig } from "../config/settings";
|
||||||
|
import { resetInvitationVerification } from "./apiClient";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IC Coder Authentication Provider
|
* IC Coder Authentication Provider
|
||||||
@ -142,13 +143,24 @@ export class ICCoderAuthenticationProvider
|
|||||||
const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId);
|
const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId);
|
||||||
if (sessionIndex > -1) {
|
if (sessionIndex > -1) {
|
||||||
const session = this._sessions[sessionIndex];
|
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);
|
this._sessions.splice(sessionIndex, 1);
|
||||||
await this.saveSessions();
|
await this.saveSessions();
|
||||||
|
|
||||||
// 清除用户信息缓存
|
// 3. 清除用户信息缓存
|
||||||
await clearUserInfo();
|
await clearUserInfo();
|
||||||
|
|
||||||
// 触发会话变化事件
|
// 4. 触发会话变化事件
|
||||||
this._onDidChangeSessions.fire({
|
this._onDidChangeSessions.fire({
|
||||||
added: [],
|
added: [],
|
||||||
removed: [session],
|
removed: [session],
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* 邀请码验证服务
|
* 邀请码验证服务
|
||||||
*/
|
*/
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from "vscode";
|
||||||
import { verifyInvitationCode, checkInvitationStatus } from './apiClient';
|
import { verifyInvitationCode, checkInvitationStatus } from "./apiClient";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 邀请码验证服务类
|
* 邀请码验证服务类
|
||||||
@ -13,34 +13,51 @@ export class InvitationService {
|
|||||||
*/
|
*/
|
||||||
static async isVerified(context: vscode.ExtensionContext): Promise<boolean> {
|
static async isVerified(context: vscode.ExtensionContext): Promise<boolean> {
|
||||||
// 【临时】使用本地验证,不调用后端
|
// 【临时】使用本地验证,不调用后端
|
||||||
const localVerified = context.globalState.get<boolean>('invitationCodeVerified');
|
const localVerified = context.globalState.get<boolean>(
|
||||||
|
"invitationCodeVerified",
|
||||||
|
);
|
||||||
return localVerified || false;
|
return localVerified || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证邀请码
|
* 验证邀请码
|
||||||
*/
|
*/
|
||||||
static async verifyCode(code: string): Promise<{ success: boolean; message: string }> {
|
static async verifyCode(
|
||||||
|
code: string,
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
console.log('[InvitationService] 验证邀请码:', code);
|
// console.log('[InvitationService] ========== 开始验证邀请码 ==========');
|
||||||
|
// console.log('[InvitationService] 邀请码:', code);
|
||||||
|
// console.log('[InvitationService] 邀请码长度:', code.length);
|
||||||
|
|
||||||
const response = await verifyInvitationCode(code);
|
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) {
|
if (response.code === 200 && response.data?.verified) {
|
||||||
|
console.log("[InvitationService] ✓ 验证成功");
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: response.msg || '验证成功'
|
message: response.msg || "验证成功",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
console.log("[InvitationService] ✗ 验证失败");
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: response.msg || '验证失败'
|
message: response.msg || "验证失败",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[InvitationService] 验证邀请码失败:', error);
|
// console.error('[InvitationService] ========== 验证邀请码异常 ==========');
|
||||||
|
// console.error('[InvitationService] 错误类型:', error.constructor.name);
|
||||||
|
// console.error('[InvitationService] 错误消息:', error.message);
|
||||||
|
// console.error('[InvitationService] 错误堆栈:', error.stack);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || '网络连接失败,请检查网络后重试'
|
message: error.message || "网络连接失败,请检查网络后重试",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,20 +68,25 @@ export class InvitationService {
|
|||||||
static async saveVerificationStatus(
|
static async saveVerificationStatus(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
code: string,
|
code: string,
|
||||||
verifiedTime?: string
|
verifiedTime?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await context.globalState.update('invitationCodeVerified', true);
|
await context.globalState.update("invitationCodeVerified", true);
|
||||||
await context.globalState.update('invitationCode', code);
|
await context.globalState.update("invitationCode", code);
|
||||||
await context.globalState.update('invitationVerifiedTime', verifiedTime || new Date().toISOString());
|
await context.globalState.update(
|
||||||
|
"invitationVerifiedTime",
|
||||||
|
verifiedTime || new Date().toISOString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除验证状态(用于退出登录或更换邀请码)
|
* 清除验证状态(用于退出登录或更换邀请码)
|
||||||
*/
|
*/
|
||||||
static async clearVerificationStatus(context: vscode.ExtensionContext): Promise<void> {
|
static async clearVerificationStatus(
|
||||||
await context.globalState.update('invitationCodeVerified', undefined);
|
context: vscode.ExtensionContext,
|
||||||
await context.globalState.update('invitationCode', undefined);
|
): Promise<void> {
|
||||||
await context.globalState.update('invitationVerifiedTime', undefined);
|
await context.globalState.update("invitationCodeVerified", undefined);
|
||||||
|
await context.globalState.update("invitationCode", undefined);
|
||||||
|
await context.globalState.update("invitationVerifiedTime", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,18 +94,18 @@ export class InvitationService {
|
|||||||
*/
|
*/
|
||||||
static async showInputDialog(): Promise<string | undefined> {
|
static async showInputDialog(): Promise<string | undefined> {
|
||||||
const code = await vscode.window.showInputBox({
|
const code = await vscode.window.showInputBox({
|
||||||
prompt: '请输入邀请码以继续使用 IC Coder',
|
prompt: "请输入邀请码以继续使用 IC Coder",
|
||||||
placeHolder: '例如:INVITE2024ABC',
|
placeHolder: "例如:INVITE2024ABC",
|
||||||
ignoreFocusOut: true,
|
ignoreFocusOut: true,
|
||||||
validateInput: (value) => {
|
validateInput: (value) => {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return '邀请码不能为空';
|
return "邀请码不能为空";
|
||||||
}
|
}
|
||||||
if (value.trim().length < 6) {
|
if (value.trim().length < 6) {
|
||||||
return '邀请码格式不正确';
|
return "邀请码格式不正确";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return code?.trim();
|
return code?.trim();
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
// 使用 require 导入 node-notifier
|
// 尝试加载 node-notifier,如果失败则使用 null
|
||||||
const notifier = require('node-notifier');
|
let notifier: any = null;
|
||||||
|
try {
|
||||||
|
notifier = require('node-notifier');
|
||||||
|
console.log('[NotificationService] node-notifier 加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[NotificationService] node-notifier 加载失败,将只使用 VS Code 内置通知');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知类型枚举
|
* 通知类型枚举
|
||||||
@ -114,6 +120,13 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
console.log('[NotificationService] 通过防抖检查');
|
console.log('[NotificationService] 通过防抖检查');
|
||||||
|
|
||||||
|
// 如果 node-notifier 不可用,直接使用 VS Code 内置通知
|
||||||
|
if (!notifier) {
|
||||||
|
console.log('[NotificationService] node-notifier 不可用,使用 VS Code 内置通知');
|
||||||
|
this.showVSCodeNotification(title, message, type, onClick);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 node-notifier 发送系统通知
|
// 使用 node-notifier 发送系统通知
|
||||||
console.log('[NotificationService] 使用 node-notifier 发送系统通知');
|
console.log('[NotificationService] 使用 node-notifier 发送系统通知');
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import * as os from 'os';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
import { createOrOverwriteFile } from '../utils/createFiles';
|
||||||
|
import { resolveWorkspaceFilePath, showFileDiff } from '../utils/fileDiff';
|
||||||
|
import { changeTracker } from './changeTracker';
|
||||||
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
||||||
import { analyzeVcdFile } from '../utils/vcdParser';
|
import { analyzeVcdFile } from '../utils/vcdParser';
|
||||||
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
||||||
@ -125,8 +127,19 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
|||||||
* 执行 file_write 工具
|
* 执行 file_write 工具
|
||||||
*/
|
*/
|
||||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||||
|
const absolutePath = resolveWorkspaceFilePath(args.path);
|
||||||
|
const existedBeforeWrite = fs.existsSync(absolutePath);
|
||||||
|
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : '';
|
||||||
|
|
||||||
await createOrOverwriteFile(args.path, args.content);
|
await createOrOverwriteFile(args.path, args.content);
|
||||||
|
|
||||||
|
// 记录文件变更
|
||||||
|
try {
|
||||||
|
changeTracker.trackChange(args.path, oldContent, args.content);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[ToolExecutor] 记录文件变更失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Verilog 文件添加知识图谱提示
|
// Verilog 文件添加知识图谱提示
|
||||||
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
||||||
if (isVerilogFile) {
|
if (isVerilogFile) {
|
||||||
@ -167,6 +180,13 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
|||||||
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 读取文件内容用于变更追踪
|
||||||
|
const oldContent = fs.readFileSync(absolutePath, 'utf-8');
|
||||||
|
|
||||||
|
// 记录删除变更
|
||||||
|
const relativePath = path.relative(workspacePath, absolutePath);
|
||||||
|
changeTracker.trackChange(relativePath, oldContent, '');
|
||||||
|
|
||||||
// 删除文件
|
// 删除文件
|
||||||
fs.unlinkSync(absolutePath);
|
fs.unlinkSync(absolutePath);
|
||||||
|
|
||||||
|
|||||||
62
src/services/trialExpirationService.ts
Normal file
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 未提供,无法显示过期弹窗');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -117,6 +117,10 @@ export interface UserInfo {
|
|||||||
};
|
};
|
||||||
// Credits 余额
|
// Credits 余额
|
||||||
credits?: number;
|
credits?: number;
|
||||||
|
// 插件试用用户标识(从 JWT token 中提取)
|
||||||
|
isPluginTrial?: boolean;
|
||||||
|
// 试用到期时间(毫秒时间戳)
|
||||||
|
pluginTrialExpiresAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,7 +143,7 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
|||||||
// 处理响应数据 - 检查 code 是否为 200
|
// 处理响应数据 - 检查 code 是否为 200
|
||||||
if (response.code === 200 && response.user) {
|
if (response.code === 200 && response.user) {
|
||||||
const user = response.user;
|
const user = response.user;
|
||||||
return {
|
const userInfo: UserInfo = {
|
||||||
userId: String(user.userId),
|
userId: String(user.userId),
|
||||||
username: user.userName,
|
username: user.userName,
|
||||||
nickname: user.nickName,
|
nickname: user.nickName,
|
||||||
@ -151,6 +155,20 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
|||||||
createTime: user.createTime,
|
createTime: user.createTime,
|
||||||
loginDate: user.loginDate
|
loginDate: user.loginDate
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 从接口响应中获取企业试用标识
|
||||||
|
if (response.isPluginTrial === true) {
|
||||||
|
userInfo.isPluginTrial = true;
|
||||||
|
console.log('[UserService] 从 getInfo 接口获取到 isPluginTrial: true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取试用到期时间
|
||||||
|
if (response.enterpriseTrialExpires) {
|
||||||
|
userInfo.pluginTrialExpiresAt = response.enterpriseTrialExpires;
|
||||||
|
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('[UserService] 获取用户信息失败:', response);
|
console.error('[UserService] 获取用户信息失败:', response);
|
||||||
@ -313,6 +331,34 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
|||||||
// 保存到持久化存储
|
// 保存到持久化存储
|
||||||
await saveUserInfo(userInfo);
|
await saveUserInfo(userInfo);
|
||||||
|
|
||||||
|
// 判断是否是插件试用用户
|
||||||
|
console.log('[UserService] 检查用户类型,isPluginTrial:', userInfo.isPluginTrial);
|
||||||
|
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
|
||||||
|
|
||||||
|
if (userInfo.isPluginTrial === true) {
|
||||||
|
// 插件试用用户:标记需要显示欢迎弹窗
|
||||||
|
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
|
||||||
|
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
|
||||||
|
|
||||||
|
if (!hasWelcomed && extensionContext) {
|
||||||
|
// 设置标记,让聊天面板显示欢迎弹窗
|
||||||
|
await extensionContext.globalState.update('showWelcomeModal', true);
|
||||||
|
await extensionContext.globalState.update('pluginTrialWelcomed', true);
|
||||||
|
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
|
||||||
|
|
||||||
|
// 验证标记是否设置成功
|
||||||
|
const checkMark = extensionContext.globalState.get('showWelcomeModal');
|
||||||
|
console.log('[UserService] 验证标记:', checkMark);
|
||||||
|
} else if (!extensionContext) {
|
||||||
|
console.error('[UserService] ❌ extensionContext 为 null,无法设置标记');
|
||||||
|
} else {
|
||||||
|
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 正式用户:显示邀请码弹窗(现有逻辑)
|
||||||
|
console.log('[UserService] 正式用户登录,将在面板中检查邀请码');
|
||||||
|
}
|
||||||
|
|
||||||
return userInfo;
|
return userInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[UserService] 获取用户信息失败:', error);
|
console.error('[UserService] 获取用户信息失败:', error);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import * as vscode from "vscode";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* VCD 文件 HTTP 服务器
|
* VCD 文件 HTTP 服务器
|
||||||
* 用于为 Surfer 波形查看器提供 VCD 文件访问
|
* 用于为 波形查看器提供 VCD 文件访问
|
||||||
*/
|
*/
|
||||||
export class VCDFileServer {
|
export class VCDFileServer {
|
||||||
private server: http.Server | null = null;
|
private server: http.Server | null = null;
|
||||||
@ -98,7 +98,10 @@ export class VCDFileServer {
|
|||||||
/**
|
/**
|
||||||
* 处理 HTTP 请求
|
* 处理 HTTP 请求
|
||||||
*/
|
*/
|
||||||
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
private handleRequest(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
): void {
|
||||||
const url = req.url || "";
|
const url = req.url || "";
|
||||||
console.log(`[VCDFileServer] 收到请求: ${url}`);
|
console.log(`[VCDFileServer] 收到请求: ${url}`);
|
||||||
|
|
||||||
@ -214,7 +217,12 @@ export class VCDFileServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileName = match[1];
|
const fileName = match[1];
|
||||||
const filePath = path.join(this.extensionUri.fsPath, "media", "surfer", fileName);
|
const filePath = path.join(
|
||||||
|
this.extensionUri.fsPath,
|
||||||
|
"media",
|
||||||
|
"surfer",
|
||||||
|
fileName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||||||
@ -257,8 +265,8 @@ export class VCDFileServer {
|
|||||||
*/
|
*/
|
||||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||||
try {
|
try {
|
||||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
|
||||||
const lines = buffer.split('\n');
|
const lines = buffer.split("\n");
|
||||||
|
|
||||||
const scopeNames: string[] = [];
|
const scopeNames: string[] = [];
|
||||||
let scopeDepth = 0;
|
let scopeDepth = 0;
|
||||||
@ -267,7 +275,7 @@ export class VCDFileServer {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
if (trimmed.startsWith('$enddefinitions')) {
|
if (trimmed.startsWith("$enddefinitions")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,17 +284,17 @@ export class VCDFileServer {
|
|||||||
const scopeType = scopeMatch[1];
|
const scopeType = scopeMatch[1];
|
||||||
const scopeName = scopeMatch[2];
|
const scopeName = scopeMatch[2];
|
||||||
|
|
||||||
if (scopeDepth === 0 && scopeType === 'module') {
|
if (scopeDepth === 0 && scopeType === "module") {
|
||||||
scopeStack.push(scopeName);
|
scopeStack.push(scopeName);
|
||||||
} else if (scopeDepth === 1 && scopeType === 'module') {
|
} else if (scopeDepth === 1 && scopeType === "module") {
|
||||||
const fullPath = [...scopeStack, scopeName];
|
const fullPath = [...scopeStack, scopeName];
|
||||||
scopeNames.push(fullPath.join('.'));
|
scopeNames.push(fullPath.join("."));
|
||||||
}
|
}
|
||||||
|
|
||||||
scopeDepth++;
|
scopeDepth++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimmed.startsWith('$upscope')) {
|
if (trimmed.startsWith("$upscope")) {
|
||||||
scopeDepth--;
|
scopeDepth--;
|
||||||
if (scopeDepth === 0) {
|
if (scopeDepth === 0) {
|
||||||
scopeStack.pop();
|
scopeStack.pop();
|
||||||
@ -323,7 +331,7 @@ export class VCDFileServer {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>Surfer 波形查看器 - ${fileName}</title>
|
<title>波形查看器 - ${fileName}</title>
|
||||||
<script>
|
<script>
|
||||||
window.surferReady = false;
|
window.surferReady = false;
|
||||||
window.pendingVcdData = null;
|
window.pendingVcdData = null;
|
||||||
|
|||||||
@ -96,6 +96,7 @@ export interface ToolStartEvent {
|
|||||||
export interface ToolCompleteEvent {
|
export interface ToolCompleteEvent {
|
||||||
tool_name: string;
|
tool_name: string;
|
||||||
result: string;
|
result: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** tool_error 事件数据 */
|
/** tool_error 事件数据 */
|
||||||
@ -406,6 +407,10 @@ export interface UserInfoResponse {
|
|||||||
isDefaultModifyPwd: boolean;
|
isDefaultModifyPwd: boolean;
|
||||||
/** 密码是否过期 */
|
/** 密码是否过期 */
|
||||||
isPasswordExpired: boolean;
|
isPasswordExpired: boolean;
|
||||||
|
/** 是否为插件试用用户 */
|
||||||
|
isPluginTrial?: boolean;
|
||||||
|
/** 企业试用到期时间(毫秒时间戳) */
|
||||||
|
enterpriseTrialExpires?: number;
|
||||||
/** 用户信息 */
|
/** 用户信息 */
|
||||||
user: {
|
user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -418,6 +423,7 @@ export interface UserInfoResponse {
|
|||||||
status?: string;
|
status?: string;
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
loginDate?: string;
|
loginDate?: string;
|
||||||
|
remark?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/types/fileChanges.ts
Normal file
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;
|
||||||
|
}
|
||||||
203
src/utils/diffRenderer.ts
Normal file
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
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ export interface JwtPayload {
|
|||||||
user_id?: number; // 用户ID (下划线命名)
|
user_id?: number; // 用户ID (下划线命名)
|
||||||
exp?: number; // 过期时间
|
exp?: number; // 过期时间
|
||||||
iat?: number; // 签发时间
|
iat?: number; // 签发时间
|
||||||
|
ispluginTrial?: boolean; // 是否是插件试用用户
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,3 +103,24 @@ export function isTokenExpired(
|
|||||||
|
|
||||||
return isExpired;
|
return isExpired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 JWT token 中获取 ispluginTrial 标识
|
||||||
|
* @param token JWT token
|
||||||
|
* @returns true=插件试用用户,false=正式用户,null=无法判断
|
||||||
|
*/
|
||||||
|
export function getIsPluginTrialFromToken(token: string): boolean | null {
|
||||||
|
const payload = parseJwtPayload(token);
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 ispluginTrial 字段
|
||||||
|
if (payload.ispluginTrial !== undefined) {
|
||||||
|
console.log("[JWT] 从 token 中获取到 ispluginTrial:", payload.ispluginTrial);
|
||||||
|
return payload.ispluginTrial === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[JWT] token 中没有 ispluginTrial 字段,判定为正式用户");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,10 @@ import {
|
|||||||
} from "../services/creditsService";
|
} from "../services/creditsService";
|
||||||
import { optimizePrompt } from "../services/promptOptimizeService";
|
import { optimizePrompt } from "../services/promptOptimizeService";
|
||||||
import { NotificationService } from "../services/notificationService";
|
import { NotificationService } from "../services/notificationService";
|
||||||
|
import { TrialExpirationService } from "../services/trialExpirationService";
|
||||||
|
import { showFileDiff } from "./fileDiff";
|
||||||
|
import { changeTracker } from "../services/changeTracker";
|
||||||
|
import { generateDiff, renderDiffHtml } from "./diffRenderer";
|
||||||
|
|
||||||
import type { RunMode, ServiceTier } from "../types/api";
|
import type { RunMode, ServiceTier } from "../types/api";
|
||||||
|
|
||||||
@ -37,6 +41,14 @@ let currentSession: DialogSession | null = null;
|
|||||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||||
let lastTaskId: string | null = null;
|
let lastTaskId: string | null = null;
|
||||||
|
|
||||||
|
async function trackFileChange(filePath: string, oldContent: string, newContent: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
changeTracker.trackChange(filePath, oldContent, newContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[MessageHandler] 记录文件变更失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理用户消息
|
* 处理用户消息
|
||||||
*/
|
*/
|
||||||
@ -45,7 +57,8 @@ export async function handleUserMessage(
|
|||||||
text: string,
|
text: string,
|
||||||
extensionPath?: string,
|
extensionPath?: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
serviceTier?: ServiceTier, // 服务等级参数
|
||||||
|
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
|
||||||
) {
|
) {
|
||||||
console.log("收到用户消息:", text);
|
console.log("收到用户消息:", text);
|
||||||
|
|
||||||
@ -124,6 +137,21 @@ export async function handleUserMessage(
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查试用期是否过期
|
||||||
|
const trialService = new TrialExpirationService(context, panel);
|
||||||
|
const isExpired = await trialService.checkExpiration();
|
||||||
|
if (isExpired) {
|
||||||
|
console.warn("[MessageHandler] 试用期已过期,阻止发送");
|
||||||
|
|
||||||
|
// 恢复输入状态
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateSegments",
|
||||||
|
segments: [],
|
||||||
|
isComplete: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录用户消息到历史(允许失败,不阻塞主流程)
|
// 记录用户消息到历史(允许失败,不阻塞主流程)
|
||||||
@ -183,7 +211,8 @@ export async function handleUserMessage(
|
|||||||
extensionPath,
|
extensionPath,
|
||||||
mode,
|
mode,
|
||||||
undefined,
|
undefined,
|
||||||
serviceTier
|
serviceTier,
|
||||||
|
contextItems
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -220,10 +249,19 @@ async function handleUserMessageWithBackend(
|
|||||||
extensionPath: string,
|
extensionPath: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
serviceTier?: ServiceTier, // 服务等级参数
|
||||||
|
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
|
// 处理上下文项:在消息前附加文件/文件夹路径
|
||||||
|
let enhancedText = text;
|
||||||
|
if (contextItems && contextItems.length > 0) {
|
||||||
|
console.log("[MessageHandler] 处理上下文项:", contextItems.length);
|
||||||
|
const paths = contextItems.map(item => item.path).join('\n');
|
||||||
|
enhancedText = `${paths}\n\n${text}`;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
||||||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||||
@ -251,7 +289,7 @@ async function handleUserMessageWithBackend(
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
currentSession!.sendMessage(
|
currentSession!.sendMessage(
|
||||||
text,
|
enhancedText,
|
||||||
{
|
{
|
||||||
onText: (fullText, isStreaming) => {
|
onText: (fullText, isStreaming) => {
|
||||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||||
@ -345,6 +383,9 @@ async function handleUserMessageWithBackend(
|
|||||||
panel.reveal();
|
panel.reveal();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 发送代码变更到前端
|
||||||
|
sendChangesToWebview(panel);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
|
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
|
||||||
}
|
}
|
||||||
@ -747,11 +788,14 @@ async function handleFileOperation(
|
|||||||
if (!operation.searchText || !operation.replaceText) {
|
if (!operation.searchText || !operation.replaceText) {
|
||||||
throw new Error("缺少替换内容");
|
throw new Error("缺少替换内容");
|
||||||
}
|
}
|
||||||
|
const oldContentBeforeReplace = await readFileContent(operation.filePath);
|
||||||
await replaceFile(
|
await replaceFile(
|
||||||
operation.filePath,
|
operation.filePath,
|
||||||
operation.searchText,
|
operation.searchText,
|
||||||
operation.replaceText
|
operation.replaceText
|
||||||
);
|
);
|
||||||
|
const newContentAfterReplace = await readFileContent(operation.filePath);
|
||||||
|
await trackFileChange(operation.filePath, oldContentBeforeReplace, newContentAfterReplace);
|
||||||
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "receiveMessage",
|
command: "receiveMessage",
|
||||||
@ -887,7 +931,9 @@ export async function handleUpdateFile(
|
|||||||
content: string
|
content: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const oldContent = await readFileContent(filePath);
|
||||||
await updateFile(filePath, content);
|
await updateFile(filePath, content);
|
||||||
|
await trackFileChange(filePath, oldContent, content);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "fileUpdated",
|
command: "fileUpdated",
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
@ -952,7 +998,10 @@ export async function handleReplaceInFile(
|
|||||||
replaceText: string
|
replaceText: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const oldContent = await readFileContent(filePath);
|
||||||
await replaceFile(filePath, searchText, replaceText);
|
await replaceFile(filePath, searchText, replaceText);
|
||||||
|
const newContent = await readFileContent(filePath);
|
||||||
|
await trackFileChange(filePath, oldContent, newContent);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "fileReplaced",
|
command: "fileReplaced",
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
@ -1209,3 +1258,165 @@ export async function handleOptimizePrompt(
|
|||||||
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理采纳变更
|
||||||
|
*/
|
||||||
|
export async function handleAcceptChange(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const success = await changeTracker.acceptChange(changeId);
|
||||||
|
if (success) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: "采纳变更失败"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[MessageHandler] 采纳变更失败:", error);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理拒绝变更
|
||||||
|
*/
|
||||||
|
export async function handleRejectChange(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const success = await changeTracker.rejectChange(changeId);
|
||||||
|
if (success) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: "拒绝变更失败"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[MessageHandler] 拒绝变更失败:", error);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在对话结束时发送变更列表到前端
|
||||||
|
*/
|
||||||
|
export function sendChangesToWebview(panel: vscode.WebviewPanel) {
|
||||||
|
const session = changeTracker.endSession();
|
||||||
|
if (session && session.changes.length > 0) {
|
||||||
|
const changesWithDiff = session.changes.map(change => {
|
||||||
|
const diffLines = generateDiff(change.oldContent, change.newContent);
|
||||||
|
const diffHtml = renderDiffHtml(diffLines);
|
||||||
|
return {
|
||||||
|
...change,
|
||||||
|
diffHtml
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "showChanges",
|
||||||
|
changes: changesWithDiff
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始新的变更会话
|
||||||
|
*/
|
||||||
|
export function startChangeSession(sessionId: string) {
|
||||||
|
changeTracker.startSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开文件 diff 编辑器
|
||||||
|
*/
|
||||||
|
export async function handleOpenFileDiff(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = changeTracker.getCurrentSession();
|
||||||
|
if (!session) {
|
||||||
|
vscode.window.showErrorMessage('没有找到变更会话');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = session.changes.find(c => c.changeId === changeId);
|
||||||
|
if (!change) {
|
||||||
|
vscode.window.showErrorMessage('没有找到该变更');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
vscode.window.showErrorMessage('没有打开的工作区');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时文件用于对比
|
||||||
|
const filePath = change.filePath;
|
||||||
|
const absolutePath = vscode.Uri.file(
|
||||||
|
path.join(workspaceFolder.uri.fsPath, filePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建虚拟文档显示旧内容
|
||||||
|
const oldUri = vscode.Uri.parse(
|
||||||
|
`ic-coder-diff:${filePath}.old?${changeId}`
|
||||||
|
).with({ scheme: 'ic-coder-diff' });
|
||||||
|
|
||||||
|
// 注册文档内容提供者(如果还没注册)
|
||||||
|
if (!(global as any).__diffProviderRegistered) {
|
||||||
|
const provider = new (class implements vscode.TextDocumentContentProvider {
|
||||||
|
provideTextDocumentContent(uri: vscode.Uri): string {
|
||||||
|
const changeId = uri.query;
|
||||||
|
const session = changeTracker.getCurrentSession();
|
||||||
|
const change = session?.changes.find(c => c.changeId === changeId);
|
||||||
|
return change?.oldContent || '';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
vscode.workspace.registerTextDocumentContentProvider('ic-coder-diff', provider);
|
||||||
|
(global as any).__diffProviderRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开 diff 编辑器
|
||||||
|
await vscode.commands.executeCommand(
|
||||||
|
'vscode.diff',
|
||||||
|
oldUri,
|
||||||
|
absolutePath,
|
||||||
|
`${filePath} (变更对比)`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MessageHandler] 打开 diff 失败:', error);
|
||||||
|
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -59,13 +59,25 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 获取二维码图片URI
|
||||||
|
const qrCodeUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "QRCode", "wx.png")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取Logo URI
|
||||||
|
const logoUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png")
|
||||||
|
);
|
||||||
|
|
||||||
// 设置HTML内容
|
// 设置HTML内容
|
||||||
panel.webview.html = getWebviewContent(
|
panel.webview.html = getWebviewContent(
|
||||||
iconUri.toString(),
|
iconUri.toString(),
|
||||||
autoIconUri.toString(),
|
autoIconUri.toString(),
|
||||||
liteIconUri.toString(),
|
liteIconUri.toString(),
|
||||||
syIconUri.toString(),
|
syIconUri.toString(),
|
||||||
maxIconUri.toString()
|
maxIconUri.toString(),
|
||||||
|
qrCodeUri.toString(),
|
||||||
|
logoUri.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
@ -194,34 +206,45 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolveWebviewView(webviewView: vscode.WebviewView) {
|
resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||||
|
console.log('[ICViewProvider] ========== resolveWebviewView 被调用 ==========');
|
||||||
|
|
||||||
// 保存引用以便后续刷新
|
// 保存引用以便后续刷新
|
||||||
this._view = webviewView;
|
this._view = webviewView;
|
||||||
|
|
||||||
webviewView.webview.options = {
|
webviewView.webview.options = {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
|
localResourceRoots: [
|
||||||
|
vscode.Uri.joinPath(this.extensionUri, "media"),
|
||||||
|
vscode.Uri.joinPath(this.extensionUri, "src", "assets")
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 异步检查 token 是否过期并清除
|
console.log('[ICViewProvider] Webview options 已设置');
|
||||||
vscode.authentication.getSession("iccoder", [], { createIfNone: false })
|
console.log('[ICViewProvider] extensionUri:', this.extensionUri.toString());
|
||||||
.then((session) => {
|
|
||||||
const token = session?.accessToken;
|
|
||||||
if (token && isTokenExpired(token)) {
|
|
||||||
// 静默清除过期的 session
|
|
||||||
this.context.globalState.update('icCoderSessions', []);
|
|
||||||
this.context.globalState.update('icCoderUserInfo', undefined);
|
|
||||||
console.log('[ICViewProvider] Token 已过期,已清除所有登录状态');
|
|
||||||
}
|
|
||||||
}, () => {
|
|
||||||
// 忽略错误
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查是否已登录(使用 Authentication API)
|
// 【关键修复】先设置默认 HTML,避免一直加载
|
||||||
this.checkLoginStatus().then((isLoggedIn) => {
|
try {
|
||||||
|
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.html = this.getWebviewContent(
|
||||||
webviewView.webview,
|
webviewView.webview,
|
||||||
isLoggedIn
|
isLoggedIn
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('[ICViewProvider] 检查登录状态失败:', error);
|
||||||
|
// 即使失败也显示未登录状态
|
||||||
|
webviewView.webview.html = this.getWebviewContent(webviewView.webview, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理侧边栏的消息
|
// 处理侧边栏的消息
|
||||||
@ -253,14 +276,17 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
webview: vscode.Webview,
|
webview: vscode.Webview,
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
): string {
|
): string {
|
||||||
|
console.log('[ICViewProvider] 开始生成 HTML 内容, isLoggedIn:', isLoggedIn);
|
||||||
|
|
||||||
const logoUri = webview.asWebviewUri(
|
const logoUri = webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this.extensionUri, "media", "icon.png")
|
vscode.Uri.joinPath(this.extensionUri, "media", "icon.png")
|
||||||
);
|
);
|
||||||
|
|
||||||
return `
|
return `<!DOCTYPE html>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -304,136 +330,34 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
.btn:hover {
|
.btn:hover {
|
||||||
background: var(--vscode-button-hoverBackground);
|
background: var(--vscode-button-hoverBackground);
|
||||||
}
|
}
|
||||||
h3 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img src="${logoUri}" alt="IC Coder" width="120" />
|
<img src="${logoUri}" alt="IC Coder" width="120" />
|
||||||
<h2>欢迎使用 IC Coder</h2>
|
<h2>欢迎使用 IC Coder</h2>
|
||||||
${
|
${isLoggedIn
|
||||||
isLoggedIn
|
|
||||||
? '<button class="btn" onclick="openChat()">开始创作</button>'
|
? '<button class="btn" onclick="openChat()">开始创作</button>'
|
||||||
: '<button class="btn" onclick="login()">登录账户</button>'
|
: '<button class="btn" onclick="login()">登录账户</button>'
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
console.log('[Webview] 脚本已加载');
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
|
|
||||||
function openChat() {
|
function openChat() {
|
||||||
|
console.log('[Webview] 点击开始创作');
|
||||||
vscode.postMessage({ command: 'openChat' });
|
vscode.postMessage({ command: 'openChat' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录功能
|
|
||||||
function login() {
|
function login() {
|
||||||
|
console.log('[Webview] 点击登录');
|
||||||
vscode.postMessage({ command: 'login' });
|
vscode.postMessage({ command: 'login' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateCode(type) {
|
console.log('[Webview] 初始化完成');
|
||||||
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>`;
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export function getAgentCardStyles(): string {
|
|||||||
.agent-name {
|
.agent-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
font-size:14px
|
||||||
}
|
}
|
||||||
.agent-status {
|
.agent-status {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -99,14 +100,14 @@ export function getAgentCardStyles(): string {
|
|||||||
/* 低调显示的工具调用样式 */
|
/* 低调显示的工具调用样式 */
|
||||||
.agent-step.low-profile {
|
.agent-step.low-profile {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.agent-step.low-profile .step-icon {
|
.agent-step.low-profile .step-icon {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.agent-step.low-profile .step-name {
|
.agent-step.low-profile .step-name {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -115,7 +116,7 @@ export function getAgentCardStyles(): string {
|
|||||||
}
|
}
|
||||||
.agent-step.low-profile .step-result {
|
.agent-step.low-profile .step-result {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
489
src/views/changePanel.ts
Normal file
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -41,18 +41,18 @@ export function getContextButtonContent(): string {
|
|||||||
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" onclick="handleAddImage()">
|
<!-- <div class="context-menu-item" onclick="handleAddImage()">
|
||||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
|
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>图片</span>
|
<span>图片</span>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="context-menu-item" onclick="handleAddDocument()">
|
<!-- <div class="context-menu-item" onclick="handleAddDocument()">
|
||||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
|
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>文档库</span>
|
<span>文档库</span>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件/文件夹列表视图 -->
|
<!-- 文件/文件夹列表视图 -->
|
||||||
|
|||||||
218
src/views/expiredModal.ts
Normal file
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
@ -34,6 +34,11 @@ import {
|
|||||||
getExampleShowcaseStyles,
|
getExampleShowcaseStyles,
|
||||||
getExampleShowcaseScript,
|
getExampleShowcaseScript,
|
||||||
} from "./exampleShowcase";
|
} from "./exampleShowcase";
|
||||||
|
import {
|
||||||
|
getChangePanelContent,
|
||||||
|
getChangePanelStyles,
|
||||||
|
getChangePanelScript,
|
||||||
|
} from "./changePanel";
|
||||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,6 +54,8 @@ export function getInputAreaContent(
|
|||||||
<div class="input-area centered" id="inputArea">
|
<div class="input-area centered" id="inputArea">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
|
<!-- 代码变更面板 -->
|
||||||
|
${getChangePanelContent()}
|
||||||
<!-- 顶部工具栏 -->
|
<!-- 顶部工具栏 -->
|
||||||
<div class="input-top-toolbar">
|
<div class="input-top-toolbar">
|
||||||
${getContextButtonContent()}
|
${getContextButtonContent()}
|
||||||
@ -94,6 +101,7 @@ export function getInputAreaStyles(): string {
|
|||||||
${getContextCompressStyles()}
|
${getContextCompressStyles()}
|
||||||
${getOptimizeButtonStyles()}
|
${getOptimizeButtonStyles()}
|
||||||
${getExampleShowcaseStyles()}
|
${getExampleShowcaseStyles()}
|
||||||
|
${getChangePanelStyles()}
|
||||||
.input-area {
|
.input-area {
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
@ -296,10 +304,11 @@ export function getInputAreaScript(): string {
|
|||||||
return `
|
return `
|
||||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||||
${getModelSelectorScript()}
|
${getModelSelectorScript()}
|
||||||
${getContextButtonScript()}
|
|
||||||
${getContextDisplayScript()}
|
${getContextDisplayScript()}
|
||||||
|
${getContextButtonScript()}
|
||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
${getOptimizeButtonScript()}
|
${getOptimizeButtonScript()}
|
||||||
|
${getChangePanelScript()}
|
||||||
|
|
||||||
// 对话状态管理
|
// 对话状态管理
|
||||||
let isConversationActive = false;
|
let isConversationActive = false;
|
||||||
@ -339,12 +348,14 @@ export function getInputAreaScript(): string {
|
|||||||
if (messageInput) {
|
if (messageInput) {
|
||||||
messageInput.addEventListener('input', autoResizeTextarea);
|
messageInput.addEventListener('input', autoResizeTextarea);
|
||||||
|
|
||||||
// 监听点击事件,检测工作区状态和邀请码验证状态
|
// 监听点击事件,检测工作区状态、试用期过期和邀请码验证状态
|
||||||
messageInput.addEventListener('focus', () => {
|
messageInput.addEventListener('focus', () => {
|
||||||
if (!hasCheckedWorkspace) {
|
if (!hasCheckedWorkspace) {
|
||||||
hasCheckedWorkspace = true;
|
hasCheckedWorkspace = true;
|
||||||
vscode.postMessage({ command: 'checkWorkspace' });
|
vscode.postMessage({ command: 'checkWorkspace' });
|
||||||
}
|
}
|
||||||
|
// 检查试用期是否过期
|
||||||
|
vscode.postMessage({ command: 'checkTrialExpiration' });
|
||||||
// 检查邀请码验证状态
|
// 检查邀请码验证状态
|
||||||
vscode.postMessage({ command: 'checkInvitationCode' });
|
vscode.postMessage({ command: 'checkInvitationCode' });
|
||||||
});
|
});
|
||||||
@ -436,6 +447,11 @@ export function getInputAreaScript(): string {
|
|||||||
autoResizeTextarea(); // 重置输入框高度
|
autoResizeTextarea(); // 重置输入框高度
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
|
|
||||||
|
// 清空上下文项
|
||||||
|
if (window.clearContextItems) {
|
||||||
|
window.clearContextItems();
|
||||||
|
}
|
||||||
|
|
||||||
// 重置优化状态
|
// 重置优化状态
|
||||||
resetOptimizeButton();
|
resetOptimizeButton();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,39 +5,59 @@
|
|||||||
/**
|
/**
|
||||||
* 获取邀请码弹窗的 HTML 内容
|
* 获取邀请码弹窗的 HTML 内容
|
||||||
*/
|
*/
|
||||||
export function getInvitationModalContent(qrCodeUri?: string): string {
|
export function getInvitationModalContent(
|
||||||
|
qrCodeUri?: string,
|
||||||
|
logoUri?: string,
|
||||||
|
): string {
|
||||||
return `
|
return `
|
||||||
<!-- 邀请码验证弹窗 -->
|
<!-- 邀请码验证弹窗 -->
|
||||||
<div id="invitationModal" class="invitation-modal" style="display: none;">
|
<div id="invitationModal" class="invitation-modal" style="display: none;">
|
||||||
<div class="invitation-modal-overlay"></div>
|
<div class="invitation-modal-overlay"></div>
|
||||||
<div class="invitation-modal-content">
|
<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-modal-header">
|
||||||
<h2>验证邀请码</h2>
|
<!-- <div class="invitation-icon">🔐</div> -->
|
||||||
<p class="invitation-modal-subtitle">仅供企业端用户和内部人员使用</p>
|
<h2>欢迎使用 IC Coder</h2>
|
||||||
|
<p class="invitation-modal-subtitle">目前IC Coder插件端仅供企业端付费用户使用,2026年3月起会逐步开放给所有用户使用~</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="invitation-modal-body">
|
<div class="invitation-modal-body">
|
||||||
<div class="invitation-qrcode-section">
|
<div class="invitation-qrcode-section">
|
||||||
<p class="invitation-qrcode-text">欢迎企业端用户扫码添加微信获取邀请码</p>
|
<div class="invitation-qrcode-wrapper">
|
||||||
<img src="${qrCodeUri}" alt="微信二维码" class="invitation-qrcode-image" />
|
<img src="${qrCodeUri}" alt="微信二维码" class="invitation-qrcode-image" />
|
||||||
</div>
|
</div>
|
||||||
|
<p class="invitation-qrcode-text">欢迎扫码添加微信,填写《企业试用申请表》获取邀请码,与我们一起加速芯片设计与验证吧!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="invitation-divider">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="invitation-input-section">
|
<div class="invitation-input-section">
|
||||||
|
<label class="invitation-input-label">邀请码</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="invitationCodeInput"
|
id="invitationCodeInput"
|
||||||
class="invitation-code-input"
|
class="invitation-code-input"
|
||||||
placeholder="请输入邀请码"
|
placeholder="请输入您的邀请码"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
/>
|
/>
|
||||||
<div id="invitationError" class="invitation-error" style="display: none;"></div>
|
<div id="invitationError" class="invitation-error" style="display: none;"></div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="invitation-modal-footer">
|
|
||||||
<button id="invitationSubmitBtn" class="invitation-btn invitation-btn-primary">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +77,8 @@ export function getInvitationModalStyles(): string {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-modal-overlay {
|
.invitation-modal-overlay {
|
||||||
@ -65,77 +87,164 @@ export function getInvitationModalStyles(): string {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(5px);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-modal-content {
|
.invitation-modal-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--vscode-editor-background);
|
background: var(--vscode-editor-background);
|
||||||
border: 1px solid var(--vscode-panel-border);
|
border: 1px solid var(--vscode-widget-border);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
width: 90%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 420px;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||||
animation: modalSlideIn 0.3s ease-out;
|
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 {
|
@keyframes modalSlideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-20px);
|
transform: translateY(20px) scale(0.98);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-modal-header {
|
.invitation-modal-header {
|
||||||
padding: 24px 24px 16px;
|
padding: 60px 32px 20px;
|
||||||
border-bottom: 1px solid var(--vscode-panel-border);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-modal-header h2 {
|
.invitation-modal-header h2 {
|
||||||
margin: 0;
|
margin: 0 0 12px;
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-modal-subtitle {
|
.invitation-modal-subtitle {
|
||||||
margin: 8px 0 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-modal-body {
|
.invitation-modal-body {
|
||||||
padding: 24px;
|
padding: 0 32px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-qrcode-section {
|
.invitation-qrcode-section {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-qrcode-text {
|
.invitation-qrcode-wrapper {
|
||||||
margin: 0 0 16px;
|
display: inline-block;
|
||||||
font-size: 13px;
|
padding: 8px;
|
||||||
color: var(--vscode-foreground);
|
background: #fff;
|
||||||
line-height: 1.5;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-qrcode-image {
|
.invitation-qrcode-image {
|
||||||
width: 200px;
|
width: 150px;
|
||||||
height: 200px;
|
height: 150px;
|
||||||
border: 1px solid var(--vscode-panel-border);
|
display: block;
|
||||||
border-radius: 8px;
|
}
|
||||||
background: #fff;
|
|
||||||
|
.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 {
|
.invitation-input-section {
|
||||||
margin-top: 24px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invitation-input-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-code-input {
|
.invitation-code-input {
|
||||||
@ -145,14 +254,15 @@ export function getInvitationModalStyles(): string {
|
|||||||
border: 1px solid var(--vscode-input-border);
|
border: 1px solid var(--vscode-input-border);
|
||||||
background: var(--vscode-input-background);
|
background: var(--vscode-input-background);
|
||||||
color: var(--vscode-input-foreground);
|
color: var(--vscode-input-foreground);
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-code-input:focus {
|
.invitation-code-input:focus {
|
||||||
border-color: var(--vscode-focusBorder);
|
border-color: var(--vscode-focusBorder);
|
||||||
|
/* box-shadow: 0 0 0 2px var(--vscode-focusBorder); */
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-code-input::placeholder {
|
.invitation-code-input::placeholder {
|
||||||
@ -160,48 +270,58 @@ export function getInvitationModalStyles(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.invitation-error {
|
.invitation-error {
|
||||||
margin-top: 12px;
|
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: var(--vscode-errorForeground);
|
color: var(--vscode-errorForeground);
|
||||||
background: var(--vscode-inputValidation-errorBackground);
|
background: var(--vscode-inputValidation-errorBackground);
|
||||||
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
|
animation: shakeError 0.4s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-modal-footer {
|
@keyframes shakeError {
|
||||||
padding: 16px 24px;
|
0%, 100% { transform: translateX(0); }
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
25% { transform: translateX(-6px); }
|
||||||
display: flex;
|
75% { transform: translateX(6px); }
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-btn {
|
.invitation-btn {
|
||||||
padding: 8px 20px;
|
width: 100%;
|
||||||
font-size: 13px;
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
display: flex;
|
||||||
outline: none;
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
.invitation-btn-primary {
|
|
||||||
background: var(--vscode-button-background);
|
background: var(--vscode-button-background);
|
||||||
color: var(--vscode-button-foreground);
|
color: var(--vscode-button-foreground);
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-btn-primary:hover {
|
.invitation-btn:hover {
|
||||||
background: var(--vscode-button-hoverBackground);
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-btn-primary:active {
|
.invitation-btn:active {
|
||||||
transform: scale(0.98);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitation-btn-primary:disabled {
|
.invitation-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invitation-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -216,6 +336,7 @@ export function getInvitationModalScript(): string {
|
|||||||
const modal = document.getElementById('invitationModal');
|
const modal = document.getElementById('invitationModal');
|
||||||
const input = document.getElementById('invitationCodeInput');
|
const input = document.getElementById('invitationCodeInput');
|
||||||
const submitBtn = document.getElementById('invitationSubmitBtn');
|
const submitBtn = document.getElementById('invitationSubmitBtn');
|
||||||
|
const closeBtn = document.getElementById('invitationCloseBtn');
|
||||||
const errorDiv = document.getElementById('invitationError');
|
const errorDiv = document.getElementById('invitationError');
|
||||||
|
|
||||||
// 显示邀请码弹窗
|
// 显示邀请码弹窗
|
||||||
@ -270,6 +391,16 @@ export function getInvitationModalScript(): string {
|
|||||||
// 点击提交按钮
|
// 点击提交按钮
|
||||||
submitBtn.addEventListener('click', submitInvitationCode);
|
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) {
|
input.addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
@ -277,6 +408,11 @@ export function getInvitationModalScript(): string {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 点击遮罩层关闭弹窗
|
||||||
|
document.querySelector('.invitation-modal-overlay').addEventListener('click', function() {
|
||||||
|
hideInvitationModal();
|
||||||
|
});
|
||||||
|
|
||||||
// 阻止点击弹窗内容时关闭
|
// 阻止点击弹窗内容时关闭
|
||||||
document.querySelector('.invitation-modal-content').addEventListener('click', function(e) {
|
document.querySelector('.invitation-modal-content').addEventListener('click', function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import {
|
|||||||
stateTransitionIconSvg,
|
stateTransitionIconSvg,
|
||||||
userQuestionIconSvg,
|
userQuestionIconSvg,
|
||||||
updateStageIconSvg,
|
updateStageIconSvg,
|
||||||
|
successIconSvg,
|
||||||
} from "../constants/toolIcons";
|
} from "../constants/toolIcons";
|
||||||
import {
|
import {
|
||||||
getWaveformPreviewContent,
|
getWaveformPreviewContent,
|
||||||
@ -379,7 +380,7 @@ export function getMessageAreaStyles(): string {
|
|||||||
}
|
}
|
||||||
/* 低调显示的工具调用 - 移除边距和背景 */
|
/* 低调显示的工具调用 - 移除边距和背景 */
|
||||||
.segment-tool.low-profile {
|
.segment-tool.low-profile {
|
||||||
margin: 2px 0px;
|
margin: 5px 0px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
@ -542,6 +543,12 @@ export function getMessageAreaStyles(): string {
|
|||||||
.tool-segment-content.collapsed {
|
.tool-segment-content.collapsed {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
}
|
}
|
||||||
|
.tool-segment-description {
|
||||||
|
margin: 6px 0 0 0px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
/* 低调显示的工具调用样式 */
|
/* 低调显示的工具调用样式 */
|
||||||
.segment-tool.low-profile .tool-segment-header {
|
.segment-tool.low-profile .tool-segment-header {
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
@ -557,7 +564,7 @@ export function getMessageAreaStyles(): string {
|
|||||||
}
|
}
|
||||||
.segment-tool.low-profile .tool-segment-result {
|
.segment-tool.low-profile .tool-segment-result {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.segment-question {
|
.segment-question {
|
||||||
background: var(--vscode-textBlockQuote-background);
|
background: var(--vscode-textBlockQuote-background);
|
||||||
@ -672,6 +679,7 @@ export function getMessageAreaScript(): string {
|
|||||||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||||
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
||||||
const updateStageIconSvg = \`${updateStageIconSvg}\`;
|
const updateStageIconSvg = \`${updateStageIconSvg}\`;
|
||||||
|
const successIconSvg = \`${successIconSvg}\`;
|
||||||
|
|
||||||
${getAgentCardScript()}
|
${getAgentCardScript()}
|
||||||
|
|
||||||
@ -727,6 +735,7 @@ export function getMessageAreaScript(): string {
|
|||||||
'addStateTransition': stateTransitionIconSvg,
|
'addStateTransition': stateTransitionIconSvg,
|
||||||
'askUser': userQuestionIconSvg,
|
'askUser': userQuestionIconSvg,
|
||||||
'updatePhase': updateStageIconSvg,
|
'updatePhase': updateStageIconSvg,
|
||||||
|
'iverilog': successIconSvg,
|
||||||
};
|
};
|
||||||
return iconMap[toolName] || '';
|
return iconMap[toolName] || '';
|
||||||
}
|
}
|
||||||
@ -1064,6 +1073,7 @@ export function getMessageAreaScript(): string {
|
|||||||
const toolResult = segment.toolResult || '';
|
const toolResult = segment.toolResult || '';
|
||||||
const toolCount = segment.toolCount || 1;
|
const toolCount = segment.toolCount || 1;
|
||||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||||
|
const toolDescription = segment.toolDescription || '';
|
||||||
|
|
||||||
// 检查工具结果是否过长(超过一行显示不下)
|
// 检查工具结果是否过长(超过一行显示不下)
|
||||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||||
@ -1081,6 +1091,7 @@ export function getMessageAreaScript(): string {
|
|||||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||||
</div>
|
</div>
|
||||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||||
|
\${toolDescription ? \`<p class="tool-segment-description">\${toolDescription}</p>\` : ''}
|
||||||
\`;
|
\`;
|
||||||
|
|
||||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||||
@ -1328,6 +1339,7 @@ export function getMessageAreaScript(): string {
|
|||||||
const toolResult = segment.toolResult || '';
|
const toolResult = segment.toolResult || '';
|
||||||
const toolCount = segment.toolCount || 1;
|
const toolCount = segment.toolCount || 1;
|
||||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||||
|
const toolDescription = segment.toolDescription || '';
|
||||||
|
|
||||||
// 检查工具结果是否过长(超过一行显示不下)
|
// 检查工具结果是否过长(超过一行显示不下)
|
||||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||||
@ -1339,6 +1351,7 @@ export function getMessageAreaScript(): string {
|
|||||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||||
</div>
|
</div>
|
||||||
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||||
|
\${toolDescription ? \`<p class="tool-segment-description">\${toolDescription}</p>\` : ''}
|
||||||
\`;
|
\`;
|
||||||
|
|
||||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||||
|
|||||||
329
src/views/ndtWelcomeModal.ts
Normal file
329
src/views/ndtWelcomeModal.ts
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
/**
|
||||||
|
* 宁德时代欢迎弹窗
|
||||||
|
* 功能:邀请码验证成功后显示欢迎信息
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:宁德时代用户首次验证邀请码成功后显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取宁德时代欢迎弹窗的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getNdtWelcomeModalContent(logoUri?: string): string {
|
||||||
|
return `
|
||||||
|
<!-- 宁德时代欢迎弹窗 -->
|
||||||
|
<div id="ndtWelcomeModal" class="ndt-welcome-modal" style="display: none;">
|
||||||
|
<div class="ndt-welcome-modal-overlay"></div>
|
||||||
|
<div class="ndt-welcome-modal-content">
|
||||||
|
${logoUri ? `<img src="${logoUri}" class="ndt-welcome-logo-corner" alt="IC Coder" />` : ""}
|
||||||
|
|
||||||
|
<div class="ndt-welcome-modal-header">
|
||||||
|
<div class="ndt-welcome-icon">🎉</div>
|
||||||
|
<h2>欢迎宁德时代新能源科技股份有限公司<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder!</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ndt-welcome-modal-body">
|
||||||
|
<!-- 试用期提示 -->
|
||||||
|
<div class="ndt-trial-banner">
|
||||||
|
<span>您已获得 <strong>5 天企业版试用期</strong>,企业版试用期内Credits用量无限,并可无限制使用所有功能</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IC Coder 简介 -->
|
||||||
|
<div class="ndt-intro-section">
|
||||||
|
<h3 class="ndt-section-title">关于 IC Coder</h3>
|
||||||
|
<p class="ndt-intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform(自主式人工智能 Verilog 编码平台)。我们采用全球顶尖的IC Coder自研芯片设计微调模型,为代码生成提供强大的AI能力支撑。</p>
|
||||||
|
|
||||||
|
<div class="ndt-features">
|
||||||
|
<div class="ndt-feature-item">
|
||||||
|
<span class="ndt-feature-text">多智能体架构(Multi-Agent System):多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节</span>
|
||||||
|
</div>
|
||||||
|
<div class="ndt-feature-item">
|
||||||
|
<span class="ndt-feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
|
||||||
|
</div>
|
||||||
|
<div class="ndt-feature-item">
|
||||||
|
<span class="ndt-feature-text">AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 按钮组 -->
|
||||||
|
<div class="ndt-button-group">
|
||||||
|
<button id="ndtTutorialBtn" class="ndt-welcome-btn ndt-welcome-btn-secondary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M8 2C4.7 2 2 4.7 2 8s2.7 6 6 6 6-2.7 6-6-2.7-6-6-6zm0 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z" fill="currentColor"/>
|
||||||
|
<path d="M8 5v3l2 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>查看使用教程</span>
|
||||||
|
</button>
|
||||||
|
<button id="ndtWelcomeStartBtn" class="ndt-welcome-btn ndt-welcome-btn-primary">
|
||||||
|
<span>开始使用</span>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取宁德时代欢迎弹窗的 CSS 样式
|
||||||
|
*/
|
||||||
|
export function getNdtWelcomeModalStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 宁德时代欢迎弹窗样式 */
|
||||||
|
.ndt-welcome-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-widget-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-logo-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 24px;
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-header {
|
||||||
|
padding: 60px 32px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-header h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-modal-body {
|
||||||
|
padding: 0 32px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 试用期横幅 */
|
||||||
|
.ndt-trial-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
border-left: 3px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-trial-banner strong {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IC Coder 简介区域 */
|
||||||
|
.ndt-intro-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-section-title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-intro-text {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-features {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-feature-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border-left: 2px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-feature-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮组 */
|
||||||
|
.ndt-button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn-primary {
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn-primary:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn-secondary {
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
color: var(--vscode-button-secondaryForeground);
|
||||||
|
border: 1px solid var(--vscode-button-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn-secondary:hover {
|
||||||
|
background: var(--vscode-button-secondaryHoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ndt-welcome-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取宁德时代欢迎弹窗的 JavaScript 逻辑
|
||||||
|
*/
|
||||||
|
export function getNdtWelcomeModalScript(): string {
|
||||||
|
return `
|
||||||
|
// 宁德时代欢迎弹窗逻辑
|
||||||
|
(function() {
|
||||||
|
const modal = document.getElementById('ndtWelcomeModal');
|
||||||
|
const startBtn = document.getElementById('ndtWelcomeStartBtn');
|
||||||
|
const tutorialBtn = document.getElementById('ndtTutorialBtn');
|
||||||
|
const overlay = modal?.querySelector('.ndt-welcome-modal-overlay');
|
||||||
|
|
||||||
|
// 显示宁德时代欢迎弹窗
|
||||||
|
window.showNdtWelcomeModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏宁德时代欢迎弹窗
|
||||||
|
window.hideNdtWelcomeModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击"查看使用教程"按钮
|
||||||
|
if (tutorialBtn) {
|
||||||
|
tutorialBtn.addEventListener('click', function() {
|
||||||
|
// 打开使用教程链接
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openTutorial'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击"开始使用"按钮
|
||||||
|
if (startBtn) {
|
||||||
|
startBtn.addEventListener('click', function() {
|
||||||
|
hideNdtWelcomeModal();
|
||||||
|
// 通知后端用户已查看欢迎弹窗
|
||||||
|
vscode.postMessage({ command: 'ndtWelcomeModalViewed' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击遮罩层关闭弹窗
|
||||||
|
if (overlay) {
|
||||||
|
overlay.addEventListener('click', function() {
|
||||||
|
hideNdtWelcomeModal();
|
||||||
|
vscode.postMessage({ command: 'ndtWelcomeModalViewed' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止点击弹窗内容时关闭
|
||||||
|
const content = modal?.querySelector('.ndt-welcome-modal-content');
|
||||||
|
if (content) {
|
||||||
|
content.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自后端的消息
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.command === 'showNdtWelcomeModal') {
|
||||||
|
showNdtWelcomeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -720,7 +720,7 @@ export function getPlanCardScript(): string {
|
|||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="plan-card">
|
<div class="plan-card">
|
||||||
<div class="plan-header">
|
<div class="plan-header">
|
||||||
<span class="plan-icon">📋</span>
|
<!-- <span class="plan-icon">📋</span> -->
|
||||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||||
</div>
|
</div>
|
||||||
\${progressHtml}
|
\${progressHtml}
|
||||||
|
|||||||
@ -26,16 +26,16 @@ export function getUserInfoComponentContent(): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 升级到Pro按钮 (仅BASIC会员显示) -->
|
<!-- 升级到Pro按钮 (仅BASIC会员显示) -->
|
||||||
<div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
|
<!-- <div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
|
||||||
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
|
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-detail-body">
|
<div class="user-detail-body">
|
||||||
<div class="user-detail-item">
|
<!-- <div class="user-detail-item">
|
||||||
<span class="detail-label">剩余 Credits</span>
|
<span class="detail-label">剩余 Credits</span>
|
||||||
<span class="detail-value" id="creditsDetail">-</span>
|
<span class="detail-value" id="creditsDetail">-</span>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="user-detail-item logout-item" id="logoutItem">
|
<div class="user-detail-item logout-item" id="logoutItem">
|
||||||
<span class="detail-label">账户管理</span>
|
<span class="detail-label">账户管理</span>
|
||||||
<span class="detail-value logout-link">退出登录</span>
|
<span class="detail-value logout-link">退出登录</span>
|
||||||
|
|||||||
@ -30,6 +30,21 @@ import {
|
|||||||
getInvitationModalStyles,
|
getInvitationModalStyles,
|
||||||
getInvitationModalScript,
|
getInvitationModalScript,
|
||||||
} from "./invitationModal";
|
} from "./invitationModal";
|
||||||
|
import {
|
||||||
|
getWelcomeModalContent,
|
||||||
|
getWelcomeModalStyles,
|
||||||
|
getWelcomeModalScript,
|
||||||
|
} from "./welcomeModal";
|
||||||
|
import {
|
||||||
|
getNdtWelcomeModalContent,
|
||||||
|
getNdtWelcomeModalStyles,
|
||||||
|
getNdtWelcomeModalScript,
|
||||||
|
} from "./ndtWelcomeModal";
|
||||||
|
import {
|
||||||
|
getExpiredModalContent,
|
||||||
|
getExpiredModalStyles,
|
||||||
|
getExpiredModalScript,
|
||||||
|
} from "./expiredModal";
|
||||||
/**
|
/**
|
||||||
* 获取 WebView 面板的 HTML 内容
|
* 获取 WebView 面板的 HTML 内容
|
||||||
*/
|
*/
|
||||||
@ -39,7 +54,8 @@ export function getWebviewContent(
|
|||||||
liteIconUri?: string,
|
liteIconUri?: string,
|
||||||
syIconUri?: string,
|
syIconUri?: string,
|
||||||
maxIconUri?: string,
|
maxIconUri?: string,
|
||||||
qrCodeUri?: string
|
qrCodeUri?: string,
|
||||||
|
logoUri?: string,
|
||||||
): string {
|
): string {
|
||||||
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
|
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
|
||||||
const currentEnv = getCurrentEnv();
|
const currentEnv = getCurrentEnv();
|
||||||
@ -99,6 +115,9 @@ export function getWebviewContent(
|
|||||||
${getProgressBarStyles()}
|
${getProgressBarStyles()}
|
||||||
${getInputAreaStyles()}
|
${getInputAreaStyles()}
|
||||||
${getInvitationModalStyles()}
|
${getInvitationModalStyles()}
|
||||||
|
${getWelcomeModalStyles()}
|
||||||
|
${getNdtWelcomeModalStyles()}
|
||||||
|
${getExpiredModalStyles()}
|
||||||
|
|
||||||
.file-editor-section {
|
.file-editor-section {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
@ -285,6 +304,7 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
.segment-text {
|
.segment-text {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
font-size:0.9rem
|
||||||
}
|
}
|
||||||
.segment-tool {
|
.segment-tool {
|
||||||
background: var(--vscode-textBlockQuote-background);
|
background: var(--vscode-textBlockQuote-background);
|
||||||
@ -306,7 +326,6 @@ export function getWebviewContent(
|
|||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
}
|
}
|
||||||
.tool-segment-result {
|
.tool-segment-result {
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
padding-left: 22px;
|
padding-left: 22px;
|
||||||
@ -399,18 +418,83 @@ export function getWebviewContent(
|
|||||||
.quick-btn:hover {
|
.quick-btn:hover {
|
||||||
background: var(--vscode-button-secondaryHoverBackground);
|
background: var(--vscode-button-secondaryHoverBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-height: 600px) {
|
||||||
|
.header {
|
||||||
|
/* 使用 clamp 动态调整内边距: 最小值 5px, 理想值 2vh, 最大值 20px */
|
||||||
|
padding: clamp(5px, 2vh, 20px) 20px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
.header img {
|
||||||
|
/* 使用 clamp 动态调整图片高度: 最小值 40px, 理想值 10vh, 最大值 60px */
|
||||||
|
max-height: clamp(40px, 10vh, 60px) !important;
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
/* 使用 clamp 动态调整字体大小 */
|
||||||
|
font-size: clamp(12px, 2.5vh, 14px) !important;
|
||||||
|
margin-top: clamp(4px, 1.5vh, 8px) !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
margin-bottom: clamp(4px, 1.5vh, 8px) !important;
|
||||||
|
}
|
||||||
|
.quick-actions {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.quick-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.chat-container {
|
||||||
|
padding: 0 10px 10px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高度极小时隐藏描述文本 */
|
||||||
|
@media (max-height: 450px) {
|
||||||
|
.header p {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.quick-actions {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.quick-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.chat-container {
|
||||||
|
padding: 0 10px 10px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
${getConversationHistoryBarContent()}
|
${getConversationHistoryBarContent()}
|
||||||
${getProgressBarContent()}
|
${getProgressBarContent()}
|
||||||
${getInvitationModalContent(qrCodeUri)}
|
${getInvitationModalContent(qrCodeUri, logoUri)}
|
||||||
|
${getWelcomeModalContent(logoUri)}
|
||||||
|
${getNdtWelcomeModalContent(logoUri)}
|
||||||
|
${getExpiredModalContent(logoUri)}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
<div style="display: flex; align-items: center; justify-content: center;">
|
||||||
<img src="${iconUri}" alt="IC Coder" style="width: 48px; height: 48px;" />
|
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
|
||||||
<h1 style="margin: 0; font-size: 36px;">IC Coder</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size: 16px; margin-top: 12px;">专注于真实FPGA研发的Verilog智能体编程平台</p>
|
<p style="font-size: 16px; margin-top: 12px; line-height: 1.5;">
|
||||||
|
The <span style="background: linear-gradient(to right, #42bcff, #4A9EFF); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold;">Agentic AI</span> Verilog Coding Platform,
|
||||||
|
<span style="display: block; margin-top: 8px;">将芯片设计与验证的效率提升至少20倍!</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
@ -798,6 +882,27 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'showChanges':
|
||||||
|
// 显示代码变更
|
||||||
|
if (typeof showChangesPanel === 'function') {
|
||||||
|
showChangesPanel(message.changes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'changeAccepted':
|
||||||
|
// 变更已采纳
|
||||||
|
if (typeof handleChangeAccepted === 'function') {
|
||||||
|
handleChangeAccepted(message.changeId, message.success, message.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'changeRejected':
|
||||||
|
// 变更已拒绝
|
||||||
|
if (typeof handleChangeRejected === 'function') {
|
||||||
|
handleChangeRejected(message.changeId, message.success, message.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||||
}
|
}
|
||||||
@ -810,6 +915,9 @@ export function getWebviewContent(
|
|||||||
${getProgressBarScript()}
|
${getProgressBarScript()}
|
||||||
${getInputAreaScript()}
|
${getInputAreaScript()}
|
||||||
${getInvitationModalScript()}
|
${getInvitationModalScript()}
|
||||||
|
${getWelcomeModalScript()}
|
||||||
|
${getNdtWelcomeModalScript()}
|
||||||
|
${getExpiredModalScript()}
|
||||||
</script></body>
|
</script></body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|||||||
261
src/views/welcomeModal.ts
Normal file
261
src/views/welcomeModal.ts
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* 欢迎弹窗(试用用户)
|
||||||
|
* 功能:在聊天面板内显示欢迎模态窗口
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:试用用户首次登录时在聊天面板内显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取欢迎弹窗的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getWelcomeModalContent(logoUri?: string): string {
|
||||||
|
return `
|
||||||
|
<!-- 欢迎弹窗 -->
|
||||||
|
<div id="welcomeModal" class="welcome-modal" style="display: none;">
|
||||||
|
<div class="welcome-modal-overlay"></div>
|
||||||
|
<div class="welcome-modal-content">
|
||||||
|
${logoUri ? `<img src="${logoUri}" class="welcome-logo-corner" alt="IC Coder" />` : ""}
|
||||||
|
|
||||||
|
<div class="welcome-modal-header">
|
||||||
|
<div class="welcome-icon">🎉</div>
|
||||||
|
<h2>欢迎使用 IC Coder!</h2>
|
||||||
|
<p class="welcome-modal-subtitle">您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-modal-body">
|
||||||
|
<div class="welcome-step">
|
||||||
|
<div class="welcome-step-icon">📝</div>
|
||||||
|
<div class="welcome-step-content">
|
||||||
|
<h3>步骤 1:打开聊天面板</h3>
|
||||||
|
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-step">
|
||||||
|
<div class="welcome-step-icon">💬</div>
|
||||||
|
<div class="welcome-step-content">
|
||||||
|
<h3>步骤 2:输入您的需求</h3>
|
||||||
|
<p>描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-step">
|
||||||
|
<div class="welcome-step-icon">🔬</div>
|
||||||
|
<div class="welcome-step-content">
|
||||||
|
<h3>步骤 3:运行仿真</h3>
|
||||||
|
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
|
||||||
|
<span>开始使用</span>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取欢迎弹窗的 CSS 样式
|
||||||
|
*/
|
||||||
|
export function getWelcomeModalStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 欢迎弹窗样式 */
|
||||||
|
.welcome-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-widget-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-logo-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 24px;
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-header {
|
||||||
|
padding: 60px 32px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-header h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-modal-body {
|
||||||
|
padding: 0 32px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-step {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-step-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-step-content h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-step-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取欢迎弹窗的 JavaScript 逻辑
|
||||||
|
*/
|
||||||
|
export function getWelcomeModalScript(): string {
|
||||||
|
return `
|
||||||
|
// 欢迎弹窗逻辑
|
||||||
|
(function() {
|
||||||
|
const modal = document.getElementById('welcomeModal');
|
||||||
|
const startBtn = document.getElementById('welcomeStartBtn');
|
||||||
|
const overlay = modal?.querySelector('.welcome-modal-overlay');
|
||||||
|
|
||||||
|
// 显示欢迎弹窗
|
||||||
|
window.showWelcomeModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏欢迎弹窗
|
||||||
|
window.hideWelcomeModal = function() {
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击"开始使用"按钮
|
||||||
|
if (startBtn) {
|
||||||
|
startBtn.addEventListener('click', function() {
|
||||||
|
hideWelcomeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击遮罩层关闭弹窗
|
||||||
|
if (overlay) {
|
||||||
|
overlay.addEventListener('click', function() {
|
||||||
|
hideWelcomeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止点击弹窗内容时关闭
|
||||||
|
const content = modal?.querySelector('.welcome-modal-content');
|
||||||
|
if (content) {
|
||||||
|
content.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自后端的消息
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.command === 'showWelcomeModal') {
|
||||||
|
showWelcomeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面加载时检查是否需要显示欢迎弹窗
|
||||||
|
vscode.postMessage({ command: 'checkWelcomeModal' });
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user