Compare commits
47 Commits
feat/front
...
eb345e3e1f
| Author | SHA1 | Date | |
|---|---|---|---|
| eb345e3e1f | |||
| 8751944053 | |||
| 06573e37d7 | |||
| d740f4da44 | |||
| f24bd38ec7 | |||
| 45934baf0a | |||
| 4384ee53c5 | |||
| d89c326be5 | |||
| 2dccb4f871 | |||
| a9ddf3074e | |||
| db087bb184 | |||
| 5e9083041f | |||
| be0555d6bc | |||
| ea19dfcbe6 | |||
| fa55e32153 | |||
| f6b1f5c45a | |||
| 1f9a1822c9 | |||
| 63015c6bbc | |||
| 24b30df992 | |||
| 67b1003831 | |||
| 00d37bdaf0 | |||
| c5fcb1427e | |||
| 9118ebd662 | |||
| 19cdf47bed | |||
| 95bac94479 | |||
| 421a8934a7 | |||
| f7f45668d3 | |||
| c8e9a5b897 | |||
| a1bfa62796 | |||
| 64e11cbc3c | |||
| 15445aa13c | |||
| 52834047f2 | |||
| 76817675f1 | |||
| 2cce8f94c9 | |||
| 9b5f102d9f | |||
| 68de33165e | |||
| f56ad33366 | |||
| 35c63802b5 | |||
| 3458f6fe23 | |||
| 8f305033f7 | |||
| 4a18f1c418 | |||
| 373edb6d80 | |||
| 1c66e0e599 | |||
| 536e7720cb | |||
| 75eac4b1ce | |||
| 9ed0afee6b | |||
| f700473967 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,8 +4,7 @@ node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
|
||||
# waveform_trace 打包产物(exe 太大,通过 Release 发布)
|
||||
tools/waveform_trace/bin/
|
||||
# waveform_trace 打包产物
|
||||
tools/waveform_trace/src/build/
|
||||
tools/waveform_trace/src/dist/
|
||||
tools/waveform_trace/src/*.spec
|
||||
|
||||
28
.vscodeignore
Normal file
28
.vscodeignore
Normal file
@ -0,0 +1,28 @@
|
||||
# 开发文件
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
|
||||
# 测试文件
|
||||
out/test/**
|
||||
|
||||
# 依赖
|
||||
node_modules/**
|
||||
|
||||
# 文档(避免中文文件名打包问题)
|
||||
docs/**
|
||||
PUBLISH.md
|
||||
|
||||
# 只排除 waveform_trace 的 src/dist 目录
|
||||
tools/waveform_trace/src/dist/**
|
||||
|
||||
# Git 相关
|
||||
.git/**
|
||||
.github/**
|
||||
51
CHANGELOG.md
51
CHANGELOG.md
@ -2,6 +2,57 @@
|
||||
|
||||
所有重要的项目变更都将记录在此文件中。
|
||||
|
||||
## [1.0.12] - 2026-03-06
|
||||
|
||||
### 新增
|
||||
|
||||
- 支持 AskUserQuestion 多问题和多选功能
|
||||
|
||||
## [1.0.9] - 2026-03-04
|
||||
|
||||
### 优化
|
||||
|
||||
- 将工具折叠图标颜色从蓝色改为灰色
|
||||
- 统一使用蓝色主题色
|
||||
- 优化打包配置,排除重复的 exe 文件
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复代码变更继续对话查找不到之前的代码变更信息的 bug
|
||||
- 修复对话展示两遍的问题
|
||||
|
||||
## [1.0.8] - 2026-03-03
|
||||
|
||||
### 新增
|
||||
|
||||
- 删除文件确认功能
|
||||
- 文件路径标签显示
|
||||
- 企业试用用户欢迎弹窗优化
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复继续对话时消息覆盖问题
|
||||
- 修复试用用户欢迎弹窗显示逻辑
|
||||
- 修复企业试用用户仍弹出邀请码的问题
|
||||
- 修复登录过期点击重新登录失败的问题
|
||||
|
||||
## [1.0.7] - 2026-03-02
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 AI 响应内容重复显示问题
|
||||
|
||||
## [1.0.6] - 2026-03-02
|
||||
|
||||
### 新增
|
||||
|
||||
- Git Diff 功能:支持查看当前文件的 Git 差异对比
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复添加上下文搜索选择文件不匹配的问题
|
||||
- 修复过期认证状态未清除导致重新登录失败的问题
|
||||
|
||||
## [1.0.4] - 2026-01-28
|
||||
|
||||
IC Coder插件端正式上线。
|
||||
|
||||
27
PUBLISH.md
27
PUBLISH.md
@ -88,6 +88,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
||||
5. 点击 **Create** 完成创建
|
||||
|
||||
**注意事项:**
|
||||
|
||||
- Publisher ID 一旦创建无法修改
|
||||
- Publisher ID 必须全局唯一
|
||||
- 建议使用有意义且专业的 ID
|
||||
@ -126,6 +127,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
||||
## [0.0.2] - 2025-12-29
|
||||
|
||||
### 新增
|
||||
|
||||
- 添加发送和暂停按钮功能
|
||||
- 添加一键优化按钮组件
|
||||
- 添加 Plan 开关组件
|
||||
@ -133,11 +135,13 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
||||
- 添加上下文压缩功能
|
||||
|
||||
### 改进
|
||||
|
||||
- 优化用户界面交互体验
|
||||
|
||||
## [0.0.1] - 2025-12-XX
|
||||
|
||||
### 新增
|
||||
|
||||
- 初始版本发布
|
||||
- Verilog 代码智能生成
|
||||
- 集成 iverilog 仿真工具
|
||||
@ -161,6 +165,7 @@ in the Software without restriction...
|
||||
### 4. 优化 README.md
|
||||
|
||||
确保 README 包含:
|
||||
|
||||
- 清晰的功能介绍
|
||||
- 使用截图或 GIF 演示
|
||||
- 详细的使用说明
|
||||
@ -219,6 +224,7 @@ pnpm vsce publish
|
||||
**步骤:**
|
||||
|
||||
1. 本地打包插件:
|
||||
|
||||
```bash
|
||||
pnpm run package
|
||||
pnpm vsce package[pnpm vsce package --no-dependencies]
|
||||
@ -257,7 +263,7 @@ pnpm vsce publish major
|
||||
|
||||
```bash
|
||||
# 发布指定版本
|
||||
pnpm vsce publish 0.0.3
|
||||
npx vsce publish --packagePath iccoder-1.0.7.vsix
|
||||
```
|
||||
|
||||
### 更新流程建议
|
||||
@ -268,8 +274,6 @@ pnpm vsce publish 0.0.3
|
||||
4. 执行发布命令
|
||||
5. 验证市场上的插件是否正常
|
||||
|
||||
|
||||
|
||||
## 更新流程
|
||||
|
||||
1. 修改版本号
|
||||
@ -281,10 +285,10 @@ pnpm vsce publish 0.0.3
|
||||
```bash
|
||||
#补丁版本 (1.0.0 -> 1.0.1)
|
||||
pnpm version patch
|
||||
|
||||
|
||||
#次要版本 (1.0.0 -> 1.1.0)
|
||||
pnpm version minor
|
||||
|
||||
|
||||
#主要版本 (1.0.0 -> 2.0.0)
|
||||
pnpm version major
|
||||
```
|
||||
@ -294,18 +298,15 @@ pnpm vsce publish 0.0.3
|
||||
```bash
|
||||
#先编译
|
||||
pnpm run compile
|
||||
|
||||
|
||||
#中间build
|
||||
pnpm run build
|
||||
|
||||
|
||||
#后打包成.vsix
|
||||
pnpm vsce package --no-dependencies
|
||||
```
|
||||
|
||||
|
||||
|
||||
3. 手动上传/命令上传
|
||||
|
||||
- https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update
|
||||
- 命令上传:vsce publish
|
||||
|
||||
@ -318,6 +319,7 @@ pnpm vsce publish 0.0.3
|
||||
**原因:** PAT Token 无效或过期
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 重新生成 PAT Token
|
||||
- 重新登录:`pnpm vsce login ic-coder-team`
|
||||
|
||||
@ -326,6 +328,7 @@ pnpm vsce publish 0.0.3
|
||||
**原因:** Publisher ID 不存在或不匹配
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 检查 `package.json` 中的 `publisher` 字段
|
||||
- 确认已在市场创建对应的 Publisher
|
||||
|
||||
@ -334,17 +337,20 @@ pnpm vsce publish 0.0.3
|
||||
**原因:** 必需文件缺失
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 确保 `dist/` 目录存在且包含编译后的代码
|
||||
- 运行 `pnpm run package` 重新构建
|
||||
|
||||
### 4. 插件审核被拒
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- 插件名称或描述违反市场规则
|
||||
- 图标不符合要求(建议 128x128 PNG)
|
||||
- README 内容不完整
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 查看审核反馈邮件
|
||||
- 修改相关内容后重新发布
|
||||
|
||||
@ -366,6 +372,7 @@ code --install-extension ic-coder-plugin-0.0.2.vsix
|
||||
```
|
||||
|
||||
或者在 VS Code 中:
|
||||
|
||||
1. 打开扩展面板
|
||||
2. 点击 `...` 菜单
|
||||
3. 选择 **Install from VSIX...**
|
||||
|
||||
261
docs/AskUserQuestion-API设计.md
Normal file
261
docs/AskUserQuestion-API设计.md
Normal file
@ -0,0 +1,261 @@
|
||||
# AskUserQuestion 多选支持 - API 设计文档
|
||||
|
||||
## 问题描述
|
||||
|
||||
当前 AI 询问用户问题时存在以下问题:
|
||||
1. 后端返回的选项不准确
|
||||
2. 多个问题只给几个选项
|
||||
3. 不支持多选方式
|
||||
|
||||
## 需求
|
||||
|
||||
实现一个问题对应多个选项,支持多选的方式。
|
||||
|
||||
## 数据结构设计
|
||||
|
||||
### 后端返回格式
|
||||
|
||||
后端通过 SSE 的 `ask_user` 事件返回以下格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"askId": "ask_1234567890",
|
||||
"questions": [
|
||||
{
|
||||
"question": "请确认 SPI 控制器的配置需求:工作模式?",
|
||||
"options": [
|
||||
"Master/8位/模式0/固定分频/需要CS",
|
||||
"Master/可配置位宽/可配置模式/需要CS",
|
||||
"Slave模式"
|
||||
],
|
||||
"multiSelect": false
|
||||
},
|
||||
{
|
||||
"question": "数据位宽?",
|
||||
"options": [
|
||||
"8位 还是其他?"
|
||||
],
|
||||
"multiSelect": false
|
||||
},
|
||||
{
|
||||
"question": "时钟极性和相位?",
|
||||
"options": [
|
||||
"CPOL=0/CPHA=0 (模式0) 还是其他模式?"
|
||||
],
|
||||
"multiSelect": false
|
||||
},
|
||||
{
|
||||
"question": "时钟分频?",
|
||||
"options": [
|
||||
"需要可配置的分频比吗?"
|
||||
],
|
||||
"multiSelect": false
|
||||
},
|
||||
{
|
||||
"question": "是否需要芯片选信号 (CS) 控制?",
|
||||
"options": [
|
||||
"是",
|
||||
"否"
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 前端数据结构
|
||||
|
||||
#### 1. API 类型定义 (`src/types/api.ts`)
|
||||
|
||||
```typescript
|
||||
/** ask_user 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
questions: QuestionItem[];
|
||||
}
|
||||
|
||||
/** 单个问题项 */
|
||||
export interface QuestionItem {
|
||||
question: string;
|
||||
options: string[];
|
||||
multiSelect?: boolean; // 是否支持多选,默认 false
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. MessageSegment 类型 (`src/services/dialogService.ts`)
|
||||
|
||||
```typescript
|
||||
export interface MessageSegment {
|
||||
type: "text" | "tool" | "question" | "agent" | "plan" | "progress";
|
||||
// ... 其他字段
|
||||
askId?: string;
|
||||
questions?: QuestionItem[]; // 改为问题数组
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 用户回答格式 (`src/types/api.ts`)
|
||||
|
||||
```typescript
|
||||
export interface AnswerRequest {
|
||||
taskId: string;
|
||||
askId: string;
|
||||
answers: {
|
||||
[questionIndex: number]: string[]; // 每个问题的答案数组(支持多选)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 前端实现要点
|
||||
|
||||
### 1. 显示多个问题
|
||||
|
||||
```typescript
|
||||
// 遍历 questions 数组,为每个问题生成 UI
|
||||
segment.questions?.forEach((q, index) => {
|
||||
// 显示问题标题
|
||||
// 显示选项(单选或多选)
|
||||
// 收集答案
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 多选支持
|
||||
|
||||
```typescript
|
||||
if (q.multiSelect) {
|
||||
// 渲染复选框
|
||||
// 允许选择多个选项
|
||||
} else {
|
||||
// 渲染单选按钮
|
||||
// 只允许选择一个选项
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 提交答案
|
||||
|
||||
```typescript
|
||||
const answers = {
|
||||
0: ["Master/8位/模式0/固定分频/需要CS"], // 第1个问题的答案
|
||||
1: ["8位 还是其他?"], // 第2个问题的答案
|
||||
2: ["CPOL=0/CPHA=0 (模式0) 还是其他模式?"], // 第3个问题的答案
|
||||
// ...
|
||||
};
|
||||
|
||||
vscode.postMessage({
|
||||
command: 'userAnswer',
|
||||
askId: 'ask_1234567890',
|
||||
answers: answers
|
||||
});
|
||||
```
|
||||
|
||||
## 后端需要做的修改
|
||||
|
||||
### 1. 修改 AskUserQuestion 工具的返回格式
|
||||
|
||||
从:
|
||||
```json
|
||||
{
|
||||
"askId": "xxx",
|
||||
"question": "单个问题",
|
||||
"options": ["选项1", "选项2"]
|
||||
}
|
||||
```
|
||||
|
||||
改为:
|
||||
```json
|
||||
{
|
||||
"askId": "xxx",
|
||||
"questions": [
|
||||
{
|
||||
"question": "问题1",
|
||||
"options": ["选项1", "选项2"],
|
||||
"multiSelect": false
|
||||
},
|
||||
{
|
||||
"question": "问题2",
|
||||
"options": ["选项A", "选项B", "选项C"],
|
||||
"multiSelect": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 接收答案的格式
|
||||
|
||||
从:
|
||||
```json
|
||||
{
|
||||
"taskId": "xxx",
|
||||
"askId": "xxx",
|
||||
"selected": ["选项1"],
|
||||
"customInput": "自定义输入"
|
||||
}
|
||||
```
|
||||
|
||||
改为:
|
||||
```json
|
||||
{
|
||||
"taskId": "xxx",
|
||||
"askId": "xxx",
|
||||
"answers": {
|
||||
"0": ["选项1"], // 第1个问题的答案
|
||||
"1": ["选项A", "选项B"] // 第2个问题的答案(多选)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例场景
|
||||
|
||||
### 场景:SPI 控制器配置
|
||||
|
||||
**后端发送:**
|
||||
```json
|
||||
{
|
||||
"askId": "ask_spi_config",
|
||||
"questions": [
|
||||
{
|
||||
"question": "工作模式?",
|
||||
"options": [
|
||||
"Master/8位/模式0/固定分频/需要CS",
|
||||
"Master/可配置位宽/可配置模式/需要CS",
|
||||
"Slave模式"
|
||||
],
|
||||
"multiSelect": false
|
||||
},
|
||||
{
|
||||
"question": "需要哪些功能?",
|
||||
"options": [
|
||||
"可配置时钟分频",
|
||||
"可配置数据位宽",
|
||||
"支持多个CS",
|
||||
"DMA支持"
|
||||
],
|
||||
"multiSelect": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**用户选择:**
|
||||
- 问题1:选择 "Master/8位/模式0/固定分频/需要CS"
|
||||
- 问题2:选择 "可配置时钟分频" 和 "可配置数据位宽"
|
||||
|
||||
**前端提交:**
|
||||
```json
|
||||
{
|
||||
"taskId": "task_xxx",
|
||||
"askId": "ask_spi_config",
|
||||
"answers": {
|
||||
"0": ["Master/8位/模式0/固定分频/需要CS"],
|
||||
"1": ["可配置时钟分频", "可配置数据位宽"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
这个设计方案:
|
||||
1. ✅ 支持多个问题
|
||||
2. ✅ 每个问题有多个选项
|
||||
3. ✅ 支持单选和多选
|
||||
4. ✅ 数据结构清晰,易于扩展
|
||||
5. ✅ 向后兼容(可以只有一个问题)
|
||||
804
docs/VSCode-Extension-API-Guide.md
Normal file
804
docs/VSCode-Extension-API-Guide.md
Normal file
@ -0,0 +1,804 @@
|
||||
# VS Code Extension API 核心知识点
|
||||
|
||||
## 目录
|
||||
- [1. Extension 生命周期](#1-extension-生命周期) ⭐⭐⭐
|
||||
- [2. 激活事件 (Activation Events)](#2-激活事件-activation-events) ⭐⭐
|
||||
- [3. 命令系统 (Commands)](#3-命令系统-commands) ⭐⭐
|
||||
- [4. Webview API](#4-webview-api) ⭐⭐⭐⭐⭐ **面试重点**
|
||||
- [5. TreeView 和自定义视图](#5-treeview-和自定义视图) ⭐⭐
|
||||
- [6. 文件系统操作](#6-文件系统操作) ⭐⭐⭐
|
||||
- [7. 配置和存储](#7-配置和存储) ⭐⭐⭐⭐ **面试重点**
|
||||
- [8. 消息通知](#8-消息通知) ⭐
|
||||
- [9. 语言特性支持](#9-语言特性支持) ⭐
|
||||
- [10. 调试和诊断](#10-调试和诊断) ⭐
|
||||
|
||||
---
|
||||
|
||||
## 1. Extension 生命周期 ⭐⭐⭐
|
||||
|
||||
### 1.1 核心函数 🔥必考
|
||||
|
||||
```typescript
|
||||
// extension.ts
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
// 插件激活时调用(只调用一次)
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Extension is now active!');
|
||||
|
||||
// 注册命令、视图、事件监听等
|
||||
// 使用 context.subscriptions 管理资源
|
||||
}
|
||||
|
||||
// 插件停用时调用(清理资源)
|
||||
export function deactivate() {
|
||||
console.log('Extension is deactivated');
|
||||
// 清理资源、关闭连接等
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 ExtensionContext 重要属性 🔥必考
|
||||
|
||||
```typescript
|
||||
interface ExtensionContext {
|
||||
// 插件订阅管理(自动清理)
|
||||
subscriptions: { dispose(): any }[];
|
||||
|
||||
// 工作区存储路径
|
||||
storageUri: vscode.Uri | undefined;
|
||||
globalStorageUri: vscode.Uri;
|
||||
|
||||
// 插件路径
|
||||
extensionUri: vscode.Uri;
|
||||
extensionPath: string;
|
||||
|
||||
// 状态存储
|
||||
workspaceState: Memento; // 工作区级别
|
||||
globalState: Memento; // 全局级别
|
||||
secrets: SecretStorage; // 敏感信息存储
|
||||
|
||||
// 环境变量
|
||||
environmentVariableCollection: EnvironmentVariableCollection;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 资源管理最佳实践 🔥必考
|
||||
|
||||
```typescript
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
// ✅ 推荐:使用 context.subscriptions 自动管理
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('extension.command', () => {})
|
||||
);
|
||||
|
||||
// ❌ 不推荐:手动管理容易忘记清理
|
||||
const disposable = vscode.commands.registerCommand('extension.command', () => {});
|
||||
// 需要在 deactivate 中手动调用 disposable.dispose()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 激活事件 (Activation Events) ⭐⭐
|
||||
|
||||
### 2.1 常用激活事件 📌重要
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"activationEvents": [
|
||||
// 启动时激活
|
||||
"onStartupFinished",
|
||||
|
||||
// 执行命令时激活
|
||||
"onCommand:extension.helloWorld",
|
||||
|
||||
// 打开特定语言文件时激活
|
||||
"onLanguage:javascript",
|
||||
"onLanguage:verilog",
|
||||
|
||||
// 打开特定文件类型时激活
|
||||
"onFileSystem:sftp",
|
||||
|
||||
// 打开特定视图时激活
|
||||
"onView:myCustomView",
|
||||
|
||||
// 调试时激活
|
||||
"onDebug",
|
||||
|
||||
// 打开特定 URI 时激活
|
||||
"onUri",
|
||||
|
||||
// Webview 恢复时激活
|
||||
"onWebviewPanel:myWebview",
|
||||
|
||||
// 任务执行时激活
|
||||
"onTaskType:npm"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 延迟激活策略 🔥必考
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用 onStartupFinished 延迟激活
|
||||
"activationEvents": ["onStartupFinished"]
|
||||
|
||||
// ❌ 不推荐:使用 * 会拖慢启动速度
|
||||
"activationEvents": ["*"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 命令系统 (Commands)
|
||||
|
||||
### 3.1 注册命令
|
||||
|
||||
```typescript
|
||||
// 注册简单命令
|
||||
const disposable = vscode.commands.registerCommand(
|
||||
'extension.helloWorld',
|
||||
() => {
|
||||
vscode.window.showInformationMessage('Hello World!');
|
||||
}
|
||||
);
|
||||
context.subscriptions.push(disposable);
|
||||
|
||||
// 注册带参数的命令
|
||||
vscode.commands.registerCommand(
|
||||
'extension.openFile',
|
||||
(filePath: string) => {
|
||||
vscode.workspace.openTextDocument(filePath).then(doc => {
|
||||
vscode.window.showTextDocument(doc);
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 执行命令
|
||||
|
||||
```typescript
|
||||
// 执行内置命令
|
||||
await vscode.commands.executeCommand('workbench.action.files.save');
|
||||
|
||||
// 执行自定义命令
|
||||
await vscode.commands.executeCommand('extension.openFile', '/path/to/file');
|
||||
|
||||
// 获取所有可用命令
|
||||
const commands = await vscode.commands.getCommands();
|
||||
```
|
||||
|
||||
### 3.3 常用内置命令
|
||||
|
||||
```typescript
|
||||
// 文件操作
|
||||
'workbench.action.files.save'
|
||||
'workbench.action.files.saveAll'
|
||||
'workbench.action.closeActiveEditor'
|
||||
|
||||
// 编辑器操作
|
||||
'editor.action.formatDocument'
|
||||
'editor.action.commentLine'
|
||||
'editor.action.selectAll'
|
||||
|
||||
// 窗口操作
|
||||
'workbench.action.toggleSidebarVisibility'
|
||||
'workbench.action.terminal.new'
|
||||
'workbench.action.quickOpen'
|
||||
|
||||
// Git 操作
|
||||
'git.commit'
|
||||
'git.push'
|
||||
'git.pull'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Webview API ⭐⭐⭐⭐⭐ **面试重点**
|
||||
|
||||
### 4.1 创建 Webview Panel 🔥必考
|
||||
|
||||
```typescript
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'myWebview', // viewType(唯一标识)
|
||||
'My Webview', // 标题
|
||||
vscode.ViewColumn.One, // 显示位置
|
||||
{
|
||||
enableScripts: true, // 启用 JavaScript
|
||||
retainContextWhenHidden: true, // 隐藏时保留状态
|
||||
localResourceRoots: [ // 允许访问的本地资源路径
|
||||
vscode.Uri.joinPath(context.extensionUri, 'media')
|
||||
]
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 4.2 设置 Webview 内容
|
||||
|
||||
```typescript
|
||||
panel.webview.html = getWebviewContent();
|
||||
|
||||
function getWebviewContent() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My Webview</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from Webview!</h1>
|
||||
<button onclick="sendMessage()">Send Message</button>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
function sendMessage() {
|
||||
vscode.postMessage({
|
||||
command: 'alert',
|
||||
text: 'Hello from Webview!'
|
||||
});
|
||||
}
|
||||
|
||||
// 接收来自 Extension 的消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
console.log('Received:', message);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Webview 消息通信 🔥必考(项目核心)
|
||||
|
||||
```typescript
|
||||
// Extension → Webview
|
||||
panel.webview.postMessage({
|
||||
command: 'update',
|
||||
data: 'some data'
|
||||
});
|
||||
|
||||
// Webview → Extension
|
||||
panel.webview.onDidReceiveMessage(
|
||||
message => {
|
||||
switch (message.command) {
|
||||
case 'alert':
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case 'getData':
|
||||
// 处理数据请求
|
||||
panel.webview.postMessage({
|
||||
command: 'dataResponse',
|
||||
data: fetchData()
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
```
|
||||
|
||||
### 4.4 Webview 生命周期管理 📌重要
|
||||
|
||||
```typescript
|
||||
// 监听 Webview 关闭事件
|
||||
panel.onDidDispose(
|
||||
() => {
|
||||
// 清理资源
|
||||
console.log('Webview disposed');
|
||||
},
|
||||
null,
|
||||
context.subscriptions
|
||||
);
|
||||
|
||||
// 监听 Webview 可见性变化
|
||||
panel.onDidChangeViewState(
|
||||
e => {
|
||||
if (e.webviewPanel.visible) {
|
||||
console.log('Webview is now visible');
|
||||
}
|
||||
},
|
||||
null,
|
||||
context.subscriptions
|
||||
);
|
||||
```
|
||||
|
||||
### 4.5 加载本地资源 📌重要
|
||||
|
||||
```typescript
|
||||
// 获取本地资源 URI
|
||||
const scriptUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, 'media', 'script.js')
|
||||
);
|
||||
|
||||
const styleUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, 'media', 'style.css')
|
||||
);
|
||||
|
||||
// 在 HTML 中使用
|
||||
const html = `
|
||||
<link href="${styleUri}" rel="stylesheet">
|
||||
<script src="${scriptUri}"></script>
|
||||
`;
|
||||
```
|
||||
|
||||
### 4.6 Webview 状态持久化 📌重要
|
||||
|
||||
```typescript
|
||||
// Webview 中保存状态
|
||||
const vscode = acquireVsCodeApi();
|
||||
const state = vscode.getState() || { count: 0 };
|
||||
|
||||
// 更新状态
|
||||
state.count++;
|
||||
vscode.setState(state);
|
||||
|
||||
// Extension 中序列化状态
|
||||
panel.webview.options = {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true
|
||||
};
|
||||
|
||||
// 恢复 Webview
|
||||
vscode.window.registerWebviewPanelSerializer('myWebview', {
|
||||
async deserializeWebviewPanel(webviewPanel, state) {
|
||||
webviewPanel.webview.html = getWebviewContent();
|
||||
// 恢复状态
|
||||
webviewPanel.webview.postMessage({ command: 'restore', state });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. TreeView 和自定义视图
|
||||
|
||||
### 5.1 创建 TreeView Provider
|
||||
|
||||
```typescript
|
||||
class MyTreeDataProvider implements vscode.TreeDataProvider<TreeItem> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
getTreeItem(element: TreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(element?: TreeItem): Thenable<TreeItem[]> {
|
||||
if (!element) {
|
||||
// 返回根节点
|
||||
return Promise.resolve([
|
||||
new TreeItem('Item 1', vscode.TreeItemCollapsibleState.None),
|
||||
new TreeItem('Item 2', vscode.TreeItemCollapsibleState.Collapsed)
|
||||
]);
|
||||
}
|
||||
// 返回子节点
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
class TreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly collapsibleState: vscode.TreeItemCollapsibleState
|
||||
) {
|
||||
super(label, collapsibleState);
|
||||
this.tooltip = `Tooltip for ${label}`;
|
||||
this.command = {
|
||||
command: 'extension.itemClicked',
|
||||
title: 'Click Item',
|
||||
arguments: [this]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 注册 TreeView
|
||||
|
||||
```typescript
|
||||
const treeDataProvider = new MyTreeDataProvider();
|
||||
const treeView = vscode.window.createTreeView('myTreeView', {
|
||||
treeDataProvider,
|
||||
showCollapseAll: true
|
||||
});
|
||||
|
||||
context.subscriptions.push(treeView);
|
||||
|
||||
// 刷新视图
|
||||
treeDataProvider.refresh();
|
||||
```
|
||||
|
||||
### 5.3 WebviewView Provider(侧边栏 Webview)
|
||||
|
||||
```typescript
|
||||
class MyWebviewViewProvider implements vscode.WebviewViewProvider {
|
||||
resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
context: vscode.WebviewViewResolveContext,
|
||||
token: vscode.CancellationToken
|
||||
) {
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true
|
||||
};
|
||||
|
||||
webviewView.webview.html = getWebviewContent();
|
||||
|
||||
webviewView.webview.onDidReceiveMessage(message => {
|
||||
// 处理消息
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 注册
|
||||
vscode.window.registerWebviewViewProvider(
|
||||
'myWebviewView',
|
||||
new MyWebviewViewProvider()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 文件系统操作 ⭐⭐⭐
|
||||
|
||||
### 6.1 读取文件 📌重要
|
||||
|
||||
```typescript
|
||||
// 读取文本文件
|
||||
const uri = vscode.Uri.file('/path/to/file.txt');
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
const text = Buffer.from(content).toString('utf8');
|
||||
|
||||
// 使用 TextDocument API
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
const text = document.getText();
|
||||
```
|
||||
|
||||
### 6.2 写入文件
|
||||
|
||||
```typescript
|
||||
// 写入文件
|
||||
const uri = vscode.Uri.file('/path/to/file.txt');
|
||||
const content = Buffer.from('Hello World', 'utf8');
|
||||
await vscode.workspace.fs.writeFile(uri, content);
|
||||
|
||||
// 使用 WorkspaceEdit
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
edit.createFile(uri, { overwrite: true });
|
||||
edit.insert(uri, new vscode.Position(0, 0), 'Hello World');
|
||||
await vscode.workspace.applyEdit(edit);
|
||||
```
|
||||
|
||||
### 6.3 文件监听
|
||||
|
||||
```typescript
|
||||
// 监听文件变化
|
||||
const watcher = vscode.workspace.createFileSystemWatcher('**/*.js');
|
||||
|
||||
watcher.onDidCreate(uri => {
|
||||
console.log('File created:', uri.fsPath);
|
||||
});
|
||||
|
||||
watcher.onDidChange(uri => {
|
||||
console.log('File changed:', uri.fsPath);
|
||||
});
|
||||
|
||||
watcher.onDidDelete(uri => {
|
||||
console.log('File deleted:', uri.fsPath);
|
||||
});
|
||||
|
||||
context.subscriptions.push(watcher);
|
||||
```
|
||||
|
||||
### 6.4 工作区操作
|
||||
|
||||
```typescript
|
||||
// 获取工作区文件夹
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (workspaceFolders) {
|
||||
const rootPath = workspaceFolders[0].uri.fsPath;
|
||||
}
|
||||
|
||||
// 查找文件
|
||||
const files = await vscode.workspace.findFiles(
|
||||
'**/*.ts', // include pattern
|
||||
'**/node_modules/**' // exclude pattern
|
||||
);
|
||||
|
||||
// 打开文件
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(document);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 配置和存储 ⭐⭐⭐⭐ **面试重点**
|
||||
|
||||
### 7.1 读取配置 📌重要
|
||||
|
||||
```typescript
|
||||
// 读取配置
|
||||
const config = vscode.workspace.getConfiguration('myExtension');
|
||||
const value = config.get<string>('settingName', 'defaultValue');
|
||||
|
||||
// 监听配置变化
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('myExtension.settingName')) {
|
||||
console.log('Configuration changed');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 7.2 更新配置
|
||||
|
||||
```typescript
|
||||
const config = vscode.workspace.getConfiguration('myExtension');
|
||||
|
||||
// 更新用户配置(全局)
|
||||
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Global);
|
||||
|
||||
// 更新工作区配置
|
||||
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Workspace);
|
||||
```
|
||||
|
||||
### 7.3 状态存储 🔥必考
|
||||
|
||||
```typescript
|
||||
// 工作区状态(仅当前工作区)
|
||||
await context.workspaceState.update('key', 'value');
|
||||
const value = context.workspaceState.get('key');
|
||||
|
||||
// 全局状态(跨工作区)
|
||||
await context.globalState.update('key', 'value');
|
||||
const value = context.globalState.get('key');
|
||||
|
||||
// 存储对象
|
||||
await context.globalState.update('userData', { name: 'John', age: 30 });
|
||||
```
|
||||
|
||||
### 7.4 敏感信息存储 🔥必考(Token 管理)
|
||||
|
||||
```typescript
|
||||
// 存储密码、Token 等敏感信息
|
||||
await context.secrets.store('apiToken', 'secret-token-value');
|
||||
|
||||
// 读取
|
||||
const token = await context.secrets.get('apiToken');
|
||||
|
||||
// 删除
|
||||
await context.secrets.delete('apiToken');
|
||||
|
||||
// 监听变化
|
||||
context.secrets.onDidChange(e => {
|
||||
console.log('Secret changed:', e.key);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 消息通知
|
||||
|
||||
### 8.1 信息提示
|
||||
|
||||
```typescript
|
||||
// 普通信息
|
||||
vscode.window.showInformationMessage('Operation completed!');
|
||||
|
||||
// 警告
|
||||
vscode.window.showWarningMessage('This action may cause issues');
|
||||
|
||||
// 错误
|
||||
vscode.window.showErrorMessage('Operation failed!');
|
||||
```
|
||||
|
||||
### 8.2 带按钮的提示
|
||||
|
||||
```typescript
|
||||
const result = await vscode.window.showInformationMessage(
|
||||
'Do you want to continue?',
|
||||
'Yes',
|
||||
'No',
|
||||
'Cancel'
|
||||
);
|
||||
|
||||
if (result === 'Yes') {
|
||||
// 用户点击了 Yes
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 输入框
|
||||
|
||||
```typescript
|
||||
// 简单输入
|
||||
const input = await vscode.window.showInputBox({
|
||||
prompt: 'Enter your name',
|
||||
placeHolder: 'John Doe',
|
||||
validateInput: (value) => {
|
||||
return value.length < 3 ? 'Name too short' : null;
|
||||
}
|
||||
});
|
||||
|
||||
// 快速选择
|
||||
const selected = await vscode.window.showQuickPick(
|
||||
['Option 1', 'Option 2', 'Option 3'],
|
||||
{
|
||||
placeHolder: 'Select an option',
|
||||
canPickMany: false
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 8.4 进度提示
|
||||
|
||||
```typescript
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: 'Processing...',
|
||||
cancellable: true
|
||||
},
|
||||
async (progress, token) => {
|
||||
token.onCancellationRequested(() => {
|
||||
console.log('User canceled');
|
||||
});
|
||||
|
||||
progress.report({ increment: 0, message: 'Starting...' });
|
||||
await doWork1();
|
||||
|
||||
progress.report({ increment: 50, message: 'Half done...' });
|
||||
await doWork2();
|
||||
|
||||
progress.report({ increment: 100, message: 'Complete!' });
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 语言特性支持
|
||||
|
||||
### 9.1 代码补全
|
||||
|
||||
```typescript
|
||||
const provider = vscode.languages.registerCompletionItemProvider(
|
||||
'javascript',
|
||||
{
|
||||
provideCompletionItems(document, position) {
|
||||
const item = new vscode.CompletionItem('myFunction');
|
||||
item.kind = vscode.CompletionItemKind.Function;
|
||||
item.detail = 'My custom function';
|
||||
item.documentation = 'This is a custom function';
|
||||
item.insertText = new vscode.SnippetString('myFunction($1)$0');
|
||||
|
||||
return [item];
|
||||
}
|
||||
},
|
||||
'.' // 触发字符
|
||||
);
|
||||
|
||||
context.subscriptions.push(provider);
|
||||
```
|
||||
|
||||
### 9.2 悬停提示
|
||||
|
||||
```typescript
|
||||
const provider = vscode.languages.registerHoverProvider('javascript', {
|
||||
provideHover(document, position) {
|
||||
const range = document.getWordRangeAtPosition(position);
|
||||
const word = document.getText(range);
|
||||
|
||||
return new vscode.Hover([
|
||||
`**${word}**`,
|
||||
'This is a hover tooltip'
|
||||
]);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 9.3 诊断(错误提示)
|
||||
|
||||
```typescript
|
||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('myExtension');
|
||||
context.subscriptions.push(diagnosticCollection);
|
||||
|
||||
function updateDiagnostics(document: vscode.TextDocument) {
|
||||
const diagnostics: vscode.Diagnostic[] = [];
|
||||
|
||||
const text = document.getText();
|
||||
const regex = /TODO/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(text))) {
|
||||
const range = new vscode.Range(
|
||||
document.positionAt(match.index),
|
||||
document.positionAt(match.index + match[0].length)
|
||||
);
|
||||
|
||||
const diagnostic = new vscode.Diagnostic(
|
||||
range,
|
||||
'TODO found',
|
||||
vscode.DiagnosticSeverity.Warning
|
||||
);
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
diagnosticCollection.set(document.uri, diagnostics);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 调试和诊断
|
||||
|
||||
### 10.1 输出通道
|
||||
|
||||
```typescript
|
||||
const outputChannel = vscode.window.createOutputChannel('My Extension');
|
||||
context.subscriptions.push(outputChannel);
|
||||
|
||||
outputChannel.appendLine('Extension activated');
|
||||
outputChannel.show(); // 显示输出面板
|
||||
```
|
||||
|
||||
### 10.2 日志记录
|
||||
|
||||
```typescript
|
||||
// 使用 LogOutputChannel(带时间戳)
|
||||
const logger = vscode.window.createOutputChannel('My Extension', { log: true });
|
||||
|
||||
logger.trace('Trace message');
|
||||
logger.debug('Debug message');
|
||||
logger.info('Info message');
|
||||
logger.warn('Warning message');
|
||||
logger.error('Error message');
|
||||
```
|
||||
|
||||
### 10.3 错误处理
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
vscode.window.showErrorMessage(`Error: ${error.message}`);
|
||||
logger.error(error.stack || error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践总结
|
||||
|
||||
### ✅ 推荐做法
|
||||
|
||||
1. **资源管理**:所有 disposable 对象都放入 `context.subscriptions`
|
||||
2. **延迟激活**:使用 `onStartupFinished` 而不是 `*`
|
||||
3. **异步操作**:使用 `async/await` 处理异步操作
|
||||
4. **错误处理**:捕获异常并给用户友好提示
|
||||
5. **类型安全**:充分利用 TypeScript 类型系统
|
||||
6. **状态持久化**:使用 `globalState`/`workspaceState` 保存状态
|
||||
7. **敏感信息**:使用 `secrets` API 存储 Token、密码等
|
||||
|
||||
### ❌ 避免做法
|
||||
|
||||
1. 不要在 `activate` 中执行耗时操作
|
||||
2. 不要忘记清理资源(监听器、Webview 等)
|
||||
3. 不要在 Webview 中直接访问文件系统
|
||||
4. 不要在配置中存储敏感信息
|
||||
5. 不要阻塞主线程(使用 Worker 或异步操作)
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [VS Code Extension API 官方文档](https://code.visualstudio.com/api)
|
||||
- [Extension Samples](https://github.com/microsoft/vscode-extension-samples)
|
||||
- [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
|
||||
42
docs/code-to-chat-feature.md
Normal file
42
docs/code-to-chat-feature.md
Normal file
@ -0,0 +1,42 @@
|
||||
# 代码快速添加到对话功能
|
||||
|
||||
## 功能说明
|
||||
|
||||
选中代码后,通过右键菜单/小灯泡/快捷键(Ctrl+Shift+I),将代码作为上下文添加到聊天面板输入框上方。
|
||||
|
||||
## 实现方式
|
||||
|
||||
### 1. Code Action Provider
|
||||
`src/providers/codeActionProvider.ts` - 提供小灯泡菜单选项
|
||||
|
||||
### 2. 命令注册
|
||||
`src/extension.ts` - 注册 `ic-coder.addCodeToChat` 命令,发送消息到 webview
|
||||
|
||||
### 3. 全局引用
|
||||
`src/panels/ICHelperPanel.ts` - 保存 panel 到 `(global as any).currentICHelperPanel`
|
||||
|
||||
### 4. 上下文显示
|
||||
`src/views/contextDisplay.ts` - 添加 `code` 类型支持和 `addCodeContext` 消息处理
|
||||
|
||||
### 5. 配置
|
||||
`package.json` - 配置命令、右键菜单、快捷键
|
||||
|
||||
## 用户体验
|
||||
|
||||
1. 选中代码
|
||||
2. 右键/小灯泡/Ctrl+Shift+I
|
||||
3. 代码显示为上下文项:`文件名.v:10-25` 📄
|
||||
4. 输入问题发送(代码自动作为上下文)
|
||||
|
||||
## 数据结构
|
||||
|
||||
代码上下文存储为 JSON:
|
||||
```json
|
||||
{
|
||||
"fileName": "路径",
|
||||
"startLine": 10,
|
||||
"endLine": 25,
|
||||
"code": "代码内容",
|
||||
"languageId": "verilog"
|
||||
}
|
||||
```
|
||||
294
docs/delete-file-confirmation.md
Normal file
294
docs/delete-file-confirmation.md
Normal file
@ -0,0 +1,294 @@
|
||||
# 删除文件确认功能实现文档
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
在 AI 返回删除文件命令时,前端拦截并弹出确认对话框,用户确认后才执行删除操作。
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 消息流程
|
||||
|
||||
```
|
||||
AI 后端 → 删除文件工具调用 → 前端拦截 → 用户确认对话框
|
||||
↓
|
||||
确定/取消
|
||||
↓
|
||||
执行删除/返回取消结果
|
||||
↓
|
||||
返回 TOOL_EXECUTION_RESULT
|
||||
↓
|
||||
AI 后端
|
||||
```
|
||||
|
||||
### 2.2 关键原则
|
||||
|
||||
**前端必须返回结果**:无论用户选择什么,前端都必须向后端返回 `TOOL_EXECUTION_RESULT`,否则后端会等待超时。
|
||||
|
||||
## 3. 实现方案
|
||||
|
||||
### 3.1 修改位置
|
||||
|
||||
文件:`src/utils/messageHandler.ts`
|
||||
|
||||
在处理工具调用的函数中,找到删除文件的工具处理逻辑。
|
||||
|
||||
### 3.2 核心代码实现
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 处理删除文件工具调用(带用户确认)
|
||||
*/
|
||||
async function handleDeleteFileTool(
|
||||
toolCall: any,
|
||||
panel: vscode.WebviewPanel
|
||||
): Promise<ToolExecutionResult> {
|
||||
const filePath = toolCall.arguments.filePath; // 根据实际参数名调整
|
||||
|
||||
// 弹出确认对话框
|
||||
const confirmed = await vscode.window.showWarningMessage(
|
||||
`确定要删除文件吗?\n\n${filePath}`,
|
||||
{
|
||||
modal: true, // 模态对话框,阻止其他操作
|
||||
detail: '此操作不可撤销'
|
||||
},
|
||||
'确定删除',
|
||||
'取消'
|
||||
);
|
||||
|
||||
// 用户确认删除
|
||||
if (confirmed === '确定删除') {
|
||||
try {
|
||||
// 执行删除操作
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
await vscode.workspace.fs.delete(uri, {
|
||||
recursive: false, // 如果是目录需要设置为 true
|
||||
useTrash: true // 移到回收站而非永久删除(推荐)
|
||||
});
|
||||
|
||||
// 返回成功结果
|
||||
return {
|
||||
type: 'TOOL_EXECUTION_RESULT',
|
||||
toolCallId: toolCall.id,
|
||||
result: JSON.stringify({
|
||||
success: true,
|
||||
message: `文件已删除: ${filePath}`
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
// 删除失败
|
||||
return {
|
||||
type: 'TOOL_EXECUTION_RESULT',
|
||||
toolCallId: toolCall.id,
|
||||
result: JSON.stringify({
|
||||
success: false,
|
||||
error: `删除失败: ${error.message}`
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 用户取消或关闭对话框
|
||||
return {
|
||||
type: 'TOOL_EXECUTION_RESULT',
|
||||
toolCallId: toolCall.id,
|
||||
result: JSON.stringify({
|
||||
success: false,
|
||||
message: '用户取消了删除操作'
|
||||
})
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 集成到消息处理流程
|
||||
|
||||
在 `messageHandler.ts` 的工具调用处理逻辑中:
|
||||
|
||||
```typescript
|
||||
// 示例:在处理工具调用的地方
|
||||
async function handleToolCall(toolCall: any, panel: vscode.WebviewPanel) {
|
||||
switch (toolCall.name) {
|
||||
case 'deleteFile': // 根据实际工具名称调整
|
||||
return await handleDeleteFileTool(toolCall, panel);
|
||||
|
||||
case 'deleteDirectory': // 如果有删除目录的工具
|
||||
return await handleDeleteDirectoryTool(toolCall, panel);
|
||||
|
||||
// ... 其他工具
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 用户体验优化
|
||||
|
||||
### 4.1 对话框样式
|
||||
|
||||
```typescript
|
||||
const confirmed = await vscode.window.showWarningMessage(
|
||||
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
|
||||
{
|
||||
modal: true,
|
||||
detail: '⚠️ 文件将被移到回收站,可以恢复'
|
||||
},
|
||||
'确定删除',
|
||||
'取消'
|
||||
);
|
||||
```
|
||||
|
||||
### 4.2 批量删除优化
|
||||
|
||||
如果 AI 一次返回多个删除操作:
|
||||
|
||||
```typescript
|
||||
// 方案 1:逐个确认
|
||||
for (const file of filesToDelete) {
|
||||
await handleDeleteFileTool(file, panel);
|
||||
}
|
||||
|
||||
// 方案 2:批量确认(推荐)
|
||||
const confirmed = await vscode.window.showWarningMessage(
|
||||
`确定要删除以下 ${filesToDelete.length} 个文件吗?\n\n${filesToDelete.join('\n')}`,
|
||||
{ modal: true },
|
||||
'全部删除',
|
||||
'取消'
|
||||
);
|
||||
```
|
||||
|
||||
## 5. 安全考虑
|
||||
|
||||
### 5.1 使用回收站
|
||||
|
||||
```typescript
|
||||
await vscode.workspace.fs.delete(uri, {
|
||||
useTrash: true // 移到回收站,可恢复
|
||||
});
|
||||
```
|
||||
|
||||
### 5.2 路径验证
|
||||
|
||||
```typescript
|
||||
// 防止删除工作区外的文件
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders) {
|
||||
return { success: false, error: '未打开工作区' };
|
||||
}
|
||||
|
||||
const isInWorkspace = workspaceFolders.some(folder =>
|
||||
filePath.startsWith(folder.uri.fsPath)
|
||||
);
|
||||
|
||||
if (!isInWorkspace) {
|
||||
return { success: false, error: '只能删除工作区内的文件' };
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 敏感文件保护
|
||||
|
||||
```typescript
|
||||
const protectedFiles = [
|
||||
'package.json',
|
||||
'tsconfig.json',
|
||||
'.git',
|
||||
'node_modules'
|
||||
];
|
||||
|
||||
const fileName = path.basename(filePath);
|
||||
if (protectedFiles.includes(fileName)) {
|
||||
vscode.window.showErrorMessage(`不允许删除系统文件: ${fileName}`);
|
||||
return { success: false, error: '受保护的文件' };
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 错误处理
|
||||
|
||||
### 6.1 常见错误
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await vscode.workspace.fs.delete(uri, { useTrash: true });
|
||||
} catch (error) {
|
||||
if (error.code === 'FileNotFound') {
|
||||
return { success: false, error: '文件不存在' };
|
||||
}
|
||||
if (error.code === 'NoPermissions') {
|
||||
return { success: false, error: '没有删除权限' };
|
||||
}
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 测试场景
|
||||
|
||||
### 7.1 基本测试
|
||||
|
||||
- [ ] 用户点击"确定删除" → 文件被删除
|
||||
- [ ] 用户点击"取消" → 文件保留,返回取消消息
|
||||
- [ ] 用户关闭对话框 → 文件保留,返回取消消息
|
||||
- [ ] 文件不存在 → 返回错误消息
|
||||
- [ ] 没有删除权限 → 返回错误消息
|
||||
|
||||
### 7.2 边界测试
|
||||
|
||||
- [ ] 删除工作区外的文件 → 拒绝
|
||||
- [ ] 删除受保护文件 → 拒绝
|
||||
- [ ] 批量删除 → 正确处理
|
||||
- [ ] 后端收到取消消息后继续对话 → 流程正常
|
||||
|
||||
## 8. 配置选项(可选)
|
||||
|
||||
可以添加用户设置来控制行为:
|
||||
|
||||
```json
|
||||
// package.json
|
||||
"configuration": {
|
||||
"properties": {
|
||||
"ic-coder.confirmDelete": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "删除文件前是否需要确认"
|
||||
},
|
||||
"ic-coder.useTrash": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "删除文件时移到回收站而非永久删除"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
读取配置:
|
||||
|
||||
```typescript
|
||||
const config = vscode.workspace.getConfiguration('ic-coder');
|
||||
const needConfirm = config.get<boolean>('confirmDelete', true);
|
||||
const useTrash = config.get<boolean>('useTrash', true);
|
||||
|
||||
if (needConfirm) {
|
||||
// 弹出确认对话框
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 总结
|
||||
|
||||
### 9.1 后端是否需要修改?
|
||||
|
||||
**不需要**。后端继续返回删除工具调用,前端负责:
|
||||
1. 拦截工具调用
|
||||
2. 弹出确认对话框
|
||||
3. 执行或取消删除
|
||||
4. **必须返回结果给后端**
|
||||
|
||||
### 9.2 关键要点
|
||||
|
||||
- ✅ 前端必须返回 `TOOL_EXECUTION_RESULT`
|
||||
- ✅ 使用 `useTrash: true` 提高安全性
|
||||
- ✅ 验证文件路径在工作区内
|
||||
- ✅ 保护敏感文件
|
||||
- ✅ 提供清晰的错误消息
|
||||
|
||||
### 9.3 下一步
|
||||
|
||||
1. 在 `messageHandler.ts` 中找到工具调用处理逻辑
|
||||
2. 实现 `handleDeleteFileTool` 函数
|
||||
3. 集成到现有流程
|
||||
4. 测试各种场景
|
||||
5. 考虑添加用户配置选项
|
||||
247
docs/personal-rules-backend-integration.md
Normal file
247
docs/personal-rules-backend-integration.md
Normal file
@ -0,0 +1,247 @@
|
||||
# 个人规则功能 - 后端对接文档
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
个人规则功能允许用户创建多条自定义规则,这些规则会在每次对话时自动传递给后端,由后端注入到 AI 的系统提示词中,从而影响 AI 的回答风格和行为。
|
||||
|
||||
## 2. 前端实现说明
|
||||
|
||||
### 2.1 用户界面
|
||||
- 用户可以在设置页面创建、修改、删除多条规则
|
||||
- 每条规则包含:规则名称 + 规则内容
|
||||
- 全局开关:启用/禁用所有规则
|
||||
|
||||
### 2.2 规则存储
|
||||
- 存储位置:`C:\Users\{用户名}\.iccoder\rules\`
|
||||
- 文件格式:每条规则一个独立的 `.md` 文件
|
||||
- 文件命名:`rule-{时间戳}.md`
|
||||
- 文件内容格式:
|
||||
```markdown
|
||||
# 规则名称
|
||||
|
||||
规则内容详细描述...
|
||||
```
|
||||
|
||||
### 2.3 规则传输逻辑
|
||||
- **开关开启**:所有规则内容合并后通过 `personalRules` 字段传给后端
|
||||
- **开关关闭**:`personalRules` 字段为 `undefined`,不传给后端
|
||||
|
||||
## 3. 后端接口变更
|
||||
|
||||
### 3.1 DialogRequest 接口新增字段
|
||||
|
||||
在现有的 `DialogRequest` 接口中新增 `personalRules` 字段:
|
||||
|
||||
```typescript
|
||||
export interface DialogRequest {
|
||||
taskId: string;
|
||||
message: string;
|
||||
userId: string;
|
||||
mode: RunMode;
|
||||
serviceTier?: ServiceTier;
|
||||
token?: string;
|
||||
compactedData?: CompactedMemory;
|
||||
newMessages?: CompactedMessage[];
|
||||
knowledgeData?: string;
|
||||
personalRules?: string; // 新增:个人规则内容
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 字段说明
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `personalRules` | `string` | 否 | 用户的个人规则内容,多条规则用 `\n\n` 分隔 |
|
||||
|
||||
### 3.3 字段示例
|
||||
|
||||
**单条规则:**
|
||||
```json
|
||||
{
|
||||
"message": "帮我写一个排序函数",
|
||||
"personalRules": "始终使用中文回复,代码注释要详细"
|
||||
}
|
||||
```
|
||||
|
||||
**多条规则(合并后):**
|
||||
```json
|
||||
{
|
||||
"message": "帮我写一个排序函数",
|
||||
"personalRules": "始终使用中文回复,代码注释要详细\n\n使用 TypeScript 严格模式\n\n遵循项目编码规范"
|
||||
}
|
||||
```
|
||||
|
||||
**规则关闭:**
|
||||
```json
|
||||
{
|
||||
"message": "帮我写一个排序函数",
|
||||
"personalRules": undefined
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 后端处理要求
|
||||
|
||||
### 4.1 接收处理
|
||||
|
||||
```typescript
|
||||
// 伪代码示例
|
||||
function handleDialogRequest(request: DialogRequest) {
|
||||
const { message, personalRules, ...otherFields } = request;
|
||||
|
||||
// 检查是否有个人规则
|
||||
if (personalRules && personalRules.trim()) {
|
||||
// 有规则:注入到系统提示词
|
||||
return processWithRules(message, personalRules, otherFields);
|
||||
} else {
|
||||
// 无规则:正常处理
|
||||
return processNormal(message, otherFields);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 规则注入策略
|
||||
|
||||
**重要:规则必须注入到系统提示词层,而不是用户消息层**
|
||||
|
||||
推荐的注入顺序(优先级从高到低):
|
||||
|
||||
1. **平台安全策略**(最高优先级,不可被覆盖)
|
||||
2. **产品默认系统提示**
|
||||
3. **用户个人规则** ← 在这里注入
|
||||
4. **用户输入消息**
|
||||
|
||||
### 4.3 注入示例
|
||||
|
||||
```typescript
|
||||
// 伪代码示例
|
||||
function buildSystemPrompt(personalRules?: string): string {
|
||||
let systemPrompt = `
|
||||
你是一个专业的 AI 助手。
|
||||
遵循以下基本原则:
|
||||
- 安全第一
|
||||
- 准确回答
|
||||
- 友好交流
|
||||
`;
|
||||
|
||||
// 如果有个人规则,追加到系统提示词
|
||||
if (personalRules && personalRules.trim()) {
|
||||
systemPrompt += `\n\n用户的个人偏好和规则:\n${personalRules}`;
|
||||
}
|
||||
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
function processWithRules(
|
||||
userMessage: string,
|
||||
personalRules: string,
|
||||
otherFields: any
|
||||
) {
|
||||
const systemPrompt = buildSystemPrompt(personalRules);
|
||||
|
||||
// 调用 AI 模型
|
||||
return callAIModel({
|
||||
system: systemPrompt,
|
||||
user: userMessage,
|
||||
...otherFields
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
### 5.1 安全性
|
||||
- ⚠️ **个人规则不能覆盖平台安全策略**
|
||||
- ⚠️ **需要对规则内容进行基本的安全检查**
|
||||
- ⚠️ **防止注入攻击(如提示词注入)**
|
||||
|
||||
### 5.2 长度限制
|
||||
- 前端已限制单条规则内容,但多条规则合并后可能较长
|
||||
- 建议后端设置总长度上限(如 10000 字符)
|
||||
- 超限时可以截断或返回错误提示
|
||||
|
||||
### 5.3 兼容性
|
||||
- `personalRules` 字段为可选字段
|
||||
- 旧版本前端不传此字段时,后端应正常处理(向后兼容)
|
||||
- 字段为 `undefined` 或空字符串时,视为无规则
|
||||
|
||||
### 5.4 日志记录
|
||||
建议在日志中记录:
|
||||
- 本次请求是否包含个人规则
|
||||
- 规则内容的长度(不要记录完整内容,避免隐私泄露)
|
||||
- 规则注入是否成功
|
||||
|
||||
示例日志:
|
||||
```
|
||||
[INFO] Dialog request received
|
||||
- taskId: abc123
|
||||
- userId: user456
|
||||
- hasPersonalRules: true
|
||||
- rulesLength: 156
|
||||
- rulesInjected: success
|
||||
```
|
||||
|
||||
## 6. 测试建议
|
||||
|
||||
### 6.1 功能测试
|
||||
1. **无规则场景**:`personalRules` 为 `undefined`,正常对话
|
||||
2. **单条规则**:传入一条规则,验证 AI 是否遵循
|
||||
3. **多条规则**:传入多条规则,验证 AI 是否同时遵循
|
||||
4. **规则冲突**:传入相互矛盾的规则,观察 AI 行为
|
||||
5. **超长规则**:传入超长内容,验证截断或错误处理
|
||||
|
||||
### 6.2 安全测试
|
||||
1. **提示词注入**:尝试在规则中注入恶意提示词
|
||||
2. **覆盖安全策略**:尝试用规则覆盖平台安全限制
|
||||
3. **特殊字符**:测试规则中包含特殊字符的情况
|
||||
|
||||
### 6.3 性能测试
|
||||
1. **大量规则**:测试 10+ 条规则的性能影响
|
||||
2. **高频请求**:测试规则注入对响应时间的影响
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
### 7.1 可能的错误场景
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| 规则内容为空字符串 | 视为无规则,正常处理 |
|
||||
| 规则内容超长 | 截断或返回错误 |
|
||||
| 规则包含非法内容 | 过滤或拒绝请求 |
|
||||
| 规则注入失败 | 降级为无规则对话 |
|
||||
|
||||
### 7.2 错误响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "RULES_TOO_LONG",
|
||||
"message": "个人规则内容超过长度限制(最大 10000 字符)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
### 8.1 基本功能
|
||||
- [ ] 能正确接收 `personalRules` 字段
|
||||
- [ ] 规则能正确注入到系统提示词
|
||||
- [ ] 规则关闭时不影响正常对话
|
||||
- [ ] 多条规则能同时生效
|
||||
|
||||
### 8.2 安全性
|
||||
- [ ] 规则不能覆盖平台安全策略
|
||||
- [ ] 有基本的内容安全检查
|
||||
- [ ] 日志中不记录完整规则内容
|
||||
|
||||
### 8.3 兼容性
|
||||
- [ ] 旧版本前端(无此字段)能正常工作
|
||||
- [ ] 字段为 `undefined` 时正常处理
|
||||
|
||||
## 9. 联系方式
|
||||
|
||||
如有疑问,请联系前端开发团队。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2026-03-07
|
||||
161
docs/personal-rules-mvp-requirements.md
Normal file
161
docs/personal-rules-mvp-requirements.md
Normal file
@ -0,0 +1,161 @@
|
||||
# 个人规则功能需求文档(方案 A:本地 `.md` 注入)
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
在不改动现有核心对话模式的前提下,实现“个人规则(Personal Rules)”能力:
|
||||
用户可在插件内维护个人规则文本,插件保存到本地 `.md` 文件;每次发起对话时自动读取并随请求传递给后端,由后端注入模型上下文,影响回答风格与行为。
|
||||
|
||||
## 2. 范围定义
|
||||
|
||||
### 2.1 本期范围(MVP)
|
||||
|
||||
1. 支持用户编辑、保存、启用/停用个人规则。
|
||||
2. 本地落盘为 `.md` 文件。
|
||||
3. 发消息时自动加载规则并传给后端。
|
||||
4. 后端接收结构化字段并注入提示词。
|
||||
5. 基础异常处理和可观测提示。
|
||||
|
||||
### 2.2 非本期范围
|
||||
|
||||
1. 云端同步、多设备同步。
|
||||
2. 规则版本历史/回滚。
|
||||
3. 多规则集合管理(仅单份个人规则文本)。
|
||||
4. 团队共享规则。
|
||||
|
||||
## 3. 术语与核心概念
|
||||
|
||||
1. `Personal Rules`:用户个人偏好与约束文本。
|
||||
2. `Rules File`:本地规则文件,Markdown 格式。
|
||||
3. `Rules Enabled`:规则开关;关闭时不注入。
|
||||
4. `Rules Injection`:请求时将规则传后端并参与模型上下文构建。
|
||||
|
||||
## 4. 用户故事
|
||||
|
||||
1. 作为用户,我希望在插件里写下“回答风格、代码习惯、语言偏好”等个人规则。
|
||||
2. 作为用户,我希望规则保存在本地可见文件中。
|
||||
3. 作为用户,我希望发消息时自动生效,无需每次重复输入。
|
||||
4. 作为用户,我希望可以一键关闭规则,临时不生效。
|
||||
|
||||
## 5. 功能需求(前端/Webview + 扩展端)
|
||||
|
||||
### 5.1 规则管理界面
|
||||
|
||||
1. 提供“个人规则”入口。
|
||||
2. 提供多行编辑框(显示当前规则内容)。
|
||||
3. 提供“保存”按钮。
|
||||
4. 提供“启用/停用”开关。
|
||||
5. 显示当前状态:
|
||||
6. 规则是否启用。
|
||||
7. 规则字数/长度。
|
||||
8. 最近保存时间(可选)。
|
||||
|
||||
### 5.2 本地文件存储
|
||||
|
||||
1. 规则内容保存到本地 `.md`。
|
||||
2. 推荐文件名:`personal-rules.md`。
|
||||
3. 推荐路径(优先):插件全局存储目录下固定子路径。
|
||||
4. 文件不存在时可自动创建。
|
||||
5. 用户可通过“打开规则文件”查看(可选)。
|
||||
|
||||
### 5.3 对话发送前处理
|
||||
|
||||
1. 用户点击发送消息。
|
||||
2. 扩展端检查规则开关:
|
||||
3. 关闭:不读取规则,不传后端。
|
||||
4. 开启:读取 `.md` 内容。
|
||||
5. 读取成功且非空时,将规则文本附加到对话请求结构化字段。
|
||||
6. 读取失败时:提示告警,但不阻断正常对话。
|
||||
|
||||
### 5.4 限制与防护
|
||||
|
||||
1. 规则长度上限(例如 4000 字符,可配置)。
|
||||
2. 超限时保存被拒绝,提示用户缩短。
|
||||
3. 空白内容视为“无规则”。
|
||||
4. 不允许二进制或非文本写入。
|
||||
|
||||
## 6. 功能需求(后端)
|
||||
|
||||
### 6.1 请求协议扩展
|
||||
|
||||
在现有对话请求结构中增加字段:
|
||||
|
||||
1. `personalRules`:字符串,可选。
|
||||
2. `rulesEnabled`:布尔,可选(便于追踪)。
|
||||
3. `rulesMeta`:可选元信息(长度、来源)。
|
||||
|
||||
### 6.2 注入策略
|
||||
|
||||
1. 后端收到 `personalRules` 后,将其注入系统提示层(而非用户消息层)。
|
||||
2. 注入顺序建议:
|
||||
3. 系统安全与平台策略。
|
||||
4. 产品默认系统提示。
|
||||
5. 用户个人规则。
|
||||
6. 用户输入。
|
||||
7. 若 `personalRules` 为空或开关关闭,则跳过注入。
|
||||
|
||||
### 6.3 风险控制
|
||||
|
||||
1. 规则文本不允许覆盖平台安全策略。
|
||||
2. 记录本次是否注入规则(日志字段即可)。
|
||||
3. 异常不应导致整次对话失败(可降级为无规则对话)。
|
||||
|
||||
## 7. 前后端对接设计
|
||||
|
||||
### 7.1 消息链路
|
||||
|
||||
1. Webview 触发 `sendMessage`。
|
||||
2. 扩展端 `messageHandler` 统一处理发送。
|
||||
3. `messageHandler` 在调用 `dialogService.sendMessage` 前读取个人规则。
|
||||
4. `dialogService` 组装 `DialogRequest`,带上 `personalRules`。
|
||||
5. `sseHandler` 发起流式请求。
|
||||
6. 后端注入规则后进入模型推理。
|
||||
7. 正常走现有 SSE 回传流程。
|
||||
|
||||
### 7.2 职责边界
|
||||
|
||||
1. Webview:展示与编辑,不直接拼接最终请求。
|
||||
2. 扩展端:规则文件读写、开关状态管理、请求组装。
|
||||
3. 后端:规则注入、优先级控制、审计日志。
|
||||
|
||||
## 8. 数据与状态设计
|
||||
|
||||
### 8.1 本地文件
|
||||
|
||||
1. 文件格式:Markdown 纯文本。
|
||||
2. 内容约定:无强制模板,允许自由文本。
|
||||
3. 编码:UTF-8。
|
||||
|
||||
### 8.2 本地配置状态
|
||||
|
||||
1. `personalRulesEnabled`:是否启用。
|
||||
2. `personalRulesPath`:规则文件路径(可固定也可配置)。
|
||||
3. `lastSavedAt`:最近保存时间(可选)。
|
||||
|
||||
## 9. 异常与降级
|
||||
|
||||
1. 文件不存在:自动创建空文件,视为无规则。
|
||||
2. 文件读取失败:弹出提示,继续无规则发送。
|
||||
3. 文件写入失败:保存失败提示,不更新状态。
|
||||
4. 后端字段不识别:请求兼容,后端忽略新字段。
|
||||
5. 后端注入失败:降级为普通对话,记录日志。
|
||||
|
||||
## 10. 安全与合规要求
|
||||
|
||||
1. 个人规则属于用户本地数据,不主动上传除非发起对话。
|
||||
2. 日志中避免完整打印规则正文(最多打印长度和哈希)。
|
||||
3. 后端注入时必须确保平台安全策略优先级更高。
|
||||
|
||||
## 11. 验收标准(UAT)
|
||||
|
||||
1. 用户保存规则后,本地存在 `personal-rules.md` 且内容一致。
|
||||
2. 开启规则发送消息时,请求中可观测到 `personalRules` 字段。
|
||||
3. 关闭规则发送消息时,请求中不含该字段或为空。
|
||||
4. 规则文件损坏/读取失败时,不影响正常聊天。
|
||||
5. 超过长度上限时,前端保存被拒绝且提示明确。
|
||||
6. 后端日志可确认“本次是否注入个人规则”。
|
||||
|
||||
## 12. 迭代建议(下一阶段)
|
||||
|
||||
1. 规则模板(代码风格、语言风格、测试偏好)。
|
||||
2. 项目规则与个人规则合并策略。
|
||||
3. 云端同步(按 `userId`),多端一致。
|
||||
379
docs/webpack-optimization-guide.md
Normal file
379
docs/webpack-optimization-guide.md
Normal file
@ -0,0 +1,379 @@
|
||||
# Webpack 打包优化完整教程
|
||||
|
||||
## 目录
|
||||
1. [优化前的问题](#优化前的问题)
|
||||
2. [优化方案详解](#优化方案详解)
|
||||
3. [配置对比](#配置对比)
|
||||
4. [使用指南](#使用指南)
|
||||
5. [效果验证](#效果验证)
|
||||
|
||||
---
|
||||
|
||||
## 优化前的问题
|
||||
|
||||
### 原始配置存在的问题
|
||||
|
||||
```javascript
|
||||
// ❌ 问题1:固定使用 none 模式
|
||||
mode: 'none'
|
||||
// 导致:生产环境代码不压缩,体积大
|
||||
|
||||
// ❌ 问题2:没有 Tree Shaking
|
||||
// 导致:未使用的代码也被打包
|
||||
|
||||
// ❌ 问题3:ts-loader 默认配置
|
||||
loader: 'ts-loader'
|
||||
// 导致:每次编译都做类型检查,速度慢
|
||||
|
||||
// ❌ 问题4:没有性能监控
|
||||
// 导致:打包体积过大时不知道
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优化方案详解
|
||||
|
||||
### 1. 自动模式切换
|
||||
|
||||
**原理**:根据环境变量自动选择打包模式
|
||||
|
||||
```javascript
|
||||
// 优化前
|
||||
mode: 'none'
|
||||
|
||||
// 优化后
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none'
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 开发模式:代码可读,方便调试
|
||||
- 生产模式:自动压缩,体积减小 40-60%
|
||||
|
||||
---
|
||||
|
||||
### 2. Tree Shaking(摇树优化)
|
||||
|
||||
**原理**:移除未使用的代码
|
||||
|
||||
```javascript
|
||||
optimization: {
|
||||
minimize: process.env.NODE_ENV === 'production',
|
||||
usedExports: true // 标记未使用的导出
|
||||
}
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```javascript
|
||||
// utils.ts
|
||||
export function usedFunc() { }
|
||||
export function unusedFunc() { } // 不会被打包
|
||||
|
||||
// main.ts
|
||||
import { usedFunc } from './utils';
|
||||
```
|
||||
|
||||
**效果**:减少 10-30% 体积
|
||||
|
||||
---
|
||||
|
||||
### 3. 加快编译速度
|
||||
|
||||
**原理**:跳过类型检查,只做转译
|
||||
|
||||
```javascript
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true, // 跳过类型检查
|
||||
compilerOptions: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 类型检查交给 IDE 和 CI
|
||||
- 编译速度提升 50-70%
|
||||
|
||||
---
|
||||
|
||||
### 4. 自动清理旧文件
|
||||
|
||||
```javascript
|
||||
output: {
|
||||
clean: true // 每次打包前清空 dist 目录
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:避免旧文件残留
|
||||
|
||||
---
|
||||
|
||||
### 5. 性能监控
|
||||
|
||||
```javascript
|
||||
performance: {
|
||||
hints: 'warning',
|
||||
maxAssetSize: 2 * 1024 * 1024, // 2MB
|
||||
maxEntrypointSize: 2 * 1024 * 1024
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:超过 2MB 会警告
|
||||
|
||||
---
|
||||
|
||||
### 6. Source Map 优化
|
||||
|
||||
```javascript
|
||||
devtool: process.env.NODE_ENV === 'production'
|
||||
? 'hidden-source-map' // 生产:隐藏源码
|
||||
: 'nosources-source-map' // 开发:保留调试信息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 模块解析优化
|
||||
|
||||
```javascript
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
mainFields: ['module', 'main'] // 优先使用 ES 模块
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:更好的 Tree Shaking 效果
|
||||
|
||||
---
|
||||
|
||||
## 配置对比
|
||||
|
||||
### 优化前
|
||||
```javascript
|
||||
const extensionConfig = {
|
||||
target: 'node',
|
||||
mode: 'none', // 固定模式
|
||||
entry: './src/extension.ts',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2'
|
||||
// 没有 clean
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
use: [{ loader: 'ts-loader' }] // 默认配置
|
||||
}]
|
||||
},
|
||||
devtool: 'nosources-source-map' // 固定
|
||||
// 没有 optimization
|
||||
// 没有 performance
|
||||
};
|
||||
```
|
||||
|
||||
### 优化后
|
||||
```javascript
|
||||
const extensionConfig = {
|
||||
target: 'node',
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
|
||||
entry: './src/extension.ts',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
clean: true // ✅ 自动清理
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
mainFields: ['module', 'main'] // ✅ 优化解析
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
use: [{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true, // ✅ 加速编译
|
||||
compilerOptions: { sourceMap: true }
|
||||
}
|
||||
}]
|
||||
}]
|
||||
},
|
||||
devtool: process.env.NODE_ENV === 'production'
|
||||
? 'hidden-source-map'
|
||||
: 'nosources-source-map',
|
||||
optimization: {
|
||||
minimize: process.env.NODE_ENV === 'production',
|
||||
usedExports: true // ✅ Tree Shaking
|
||||
},
|
||||
performance: {
|
||||
hints: 'warning',
|
||||
maxAssetSize: 2 * 1024 * 1024,
|
||||
maxEntrypointSize: 2 * 1024 * 1024
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
# 单次编译
|
||||
pnpm run compile
|
||||
|
||||
# 监听模式(推荐)
|
||||
pnpm run watch
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 不压缩代码
|
||||
- 快速编译
|
||||
- 保留调试信息
|
||||
|
||||
---
|
||||
|
||||
### 生产模式
|
||||
|
||||
#### Windows
|
||||
```bash
|
||||
set NODE_ENV=production && pnpm run package
|
||||
```
|
||||
|
||||
#### macOS/Linux
|
||||
```bash
|
||||
NODE_ENV=production pnpm run package
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 代码压缩
|
||||
- Tree Shaking
|
||||
- 隐藏源码
|
||||
|
||||
---
|
||||
|
||||
### 一键打包 VSIX
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
set NODE_ENV=production && pnpm run package && npx vsce package
|
||||
|
||||
# macOS/Linux
|
||||
NODE_ENV=production pnpm run package && npx vsce package
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 效果验证
|
||||
|
||||
### 1. 查看打包体积
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
dir dist\extension.js
|
||||
|
||||
# macOS/Linux
|
||||
ls -lh dist/extension.js
|
||||
```
|
||||
|
||||
### 2. 对比测试
|
||||
|
||||
| 模式 | 体积 | 编译时间 | 可读性 |
|
||||
|------|------|----------|--------|
|
||||
| 开发模式 | ~800KB | 5s | 高 |
|
||||
| 生产模式 | ~400KB | 8s | 低(压缩) |
|
||||
|
||||
### 3. 性能警告
|
||||
|
||||
如果看到这个警告:
|
||||
```
|
||||
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (2 MiB).
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
1. 检查是否引入了不必要的依赖
|
||||
2. 将大型库添加到 `externals`
|
||||
3. 考虑代码分割
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么开发模式不压缩?
|
||||
**A**: 保持代码可读性,方便调试和查看错误堆栈。
|
||||
|
||||
### Q2: transpileOnly 会影响类型安全吗?
|
||||
**A**: 不会。IDE 和 `tsc --noEmit` 仍会做类型检查。
|
||||
|
||||
### Q3: 如何查看 Tree Shaking 效果?
|
||||
**A**: 使用 `webpack-bundle-analyzer`:
|
||||
```bash
|
||||
pnpm add -D webpack-bundle-analyzer
|
||||
```
|
||||
|
||||
### Q4: 生产模式编译失败怎么办?
|
||||
**A**: 先用开发模式确认代码无误,再切换生产模式。
|
||||
|
||||
---
|
||||
|
||||
## 进阶优化(可选)
|
||||
|
||||
### 1. 排除更多依赖
|
||||
|
||||
```javascript
|
||||
externals: {
|
||||
vscode: 'commonjs vscode',
|
||||
'node-notifier': 'commonjs node-notifier',
|
||||
// 如果这些库很大,可以排除
|
||||
'vcdrom': 'commonjs vcdrom',
|
||||
'@wavedrom/doppler': 'commonjs @wavedrom/doppler'
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 代码分割
|
||||
|
||||
```javascript
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
priority: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 缓存优化
|
||||
|
||||
```javascript
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
experimentalWatchApi: true // 监听模式优化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
通过这些优化:
|
||||
- ✅ 生产体积减少 40-60%
|
||||
- ✅ 编译速度提升 50-70%
|
||||
- ✅ 自动清理和监控
|
||||
- ✅ 更好的开发体验
|
||||
|
||||
**推荐工作流**:
|
||||
1. 开发时用 `pnpm run watch`
|
||||
2. 提交前用 `pnpm run compile` 检查
|
||||
3. 发布前用生产模式打包
|
||||
55
docs/webpack-optimization.md
Normal file
55
docs/webpack-optimization.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Webpack 打包优化说明
|
||||
|
||||
## 优化内容
|
||||
|
||||
### 1. 自动模式切换
|
||||
- 开发模式:保持源码可读性
|
||||
- 生产模式:自动压缩代码
|
||||
|
||||
### 2. 性能优化
|
||||
- **Tree Shaking**:移除未使用的代码
|
||||
- **transpileOnly**:跳过类型检查,加快编译速度
|
||||
- **自动清理**:每次打包自动删除旧文件
|
||||
|
||||
### 3. 体积监控
|
||||
- 单文件超过 2MB 会发出警告
|
||||
- 帮助及时发现打包体积问题
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 开发模式
|
||||
```bash
|
||||
# 编译(不压缩)
|
||||
pnpm run compile
|
||||
|
||||
# 监听模式(自动重新编译)
|
||||
pnpm run watch
|
||||
```
|
||||
|
||||
### 生产模式
|
||||
```bash
|
||||
# Windows
|
||||
set NODE_ENV=production && pnpm run package
|
||||
|
||||
# macOS/Linux
|
||||
NODE_ENV=production pnpm run package
|
||||
```
|
||||
|
||||
## 打包结果
|
||||
|
||||
- **输出目录**:`dist/`
|
||||
- **入口文件**:`dist/extension.js`
|
||||
- **静态资源**:`dist/assets/`
|
||||
|
||||
## 性能对比
|
||||
|
||||
| 模式 | 体积 | 编译速度 | Source Map |
|
||||
|------|------|----------|------------|
|
||||
| 开发 | 较大 | 快 | 完整 |
|
||||
| 生产 | 小 | 较慢 | 隐藏 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 开发时使用 `pnpm run watch`,修改代码自动重新编译
|
||||
2. 发布前必须使用生产模式打包
|
||||
3. 如果打包体积超过 2MB,检查是否引入了不必要的依赖
|
||||
38
package.json
38
package.json
@ -2,7 +2,7 @@
|
||||
"name": "iccoder",
|
||||
"displayName": "IC Coder: Agentic Verilog Platform",
|
||||
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.12",
|
||||
"publisher": "ICCoderAgenticVerilogPlatform",
|
||||
"engines": {
|
||||
"vscode": "^1.80.0"
|
||||
@ -54,6 +54,28 @@
|
||||
"command": "ic-coder.testNotification",
|
||||
"title": "测试系统通知",
|
||||
"category": "IC Coder"
|
||||
},
|
||||
{
|
||||
"command": "ic-coder.addCodeToChat",
|
||||
"title": "添加到 IC Coder 对话",
|
||||
"category": "IC Coder"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"editor/context": [
|
||||
{
|
||||
"command": "ic-coder.addCodeToChat",
|
||||
"when": "editorHasSelection",
|
||||
"group": "9_cutcopypaste"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "ic-coder.addCodeToChat",
|
||||
"key": "ctrl+l",
|
||||
"mac": "cmd+l",
|
||||
"when": "editorTextFocus && editorHasSelection"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
@ -95,6 +117,11 @@
|
||||
"configuration": {
|
||||
"title": "IC Coder",
|
||||
"properties": {
|
||||
"ic-coder.personalRulesEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "启用个人规则"
|
||||
},
|
||||
"ic-coder.enableSystemNotification": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@ -135,6 +162,7 @@
|
||||
"@vscode/test-cli": "^0.0.12",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.7.1",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"eslint": "^9.39.1",
|
||||
"ts-loader": "^9.5.4",
|
||||
"typescript": "^5.9.3",
|
||||
@ -142,14 +170,6 @@
|
||||
"webpack": "^5.103.0",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"media",
|
||||
"tools",
|
||||
"src/assets",
|
||||
"LICENSE",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"@wavedrom/doppler": "^1.14.0",
|
||||
"eventsource-parser": "^3.0.6",
|
||||
|
||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@ -57,6 +57,9 @@ importers:
|
||||
'@vscode/vsce':
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
copy-webpack-plugin:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0(webpack@5.103.0)
|
||||
eslint:
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.1
|
||||
@ -823,6 +826,12 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
copy-webpack-plugin@14.0.0:
|
||||
resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==}
|
||||
engines: {node: '>= 20.9.0'}
|
||||
peerDependencies:
|
||||
webpack: ^5.1.0
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
@ -1928,6 +1937,10 @@ packages:
|
||||
serialize-javascript@6.0.2:
|
||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||
|
||||
serialize-javascript@7.0.4:
|
||||
resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
@ -3297,6 +3310,15 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
copy-webpack-plugin@14.0.0(webpack@5.103.0):
|
||||
dependencies:
|
||||
glob-parent: 6.0.2
|
||||
normalize-path: 3.0.0
|
||||
schema-utils: 4.3.3
|
||||
serialize-javascript: 7.0.4
|
||||
tinyglobby: 0.2.15
|
||||
webpack: 5.103.0(webpack-cli@6.0.1)
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
@ -4431,6 +4453,8 @@ snapshots:
|
||||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
|
||||
serialize-javascript@7.0.4: {}
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
shallow-clone@3.0.1:
|
||||
|
||||
@ -200,3 +200,8 @@ export const setting = `<svg t="1768535209135" class="icon" viewBox="0 0 1024 10
|
||||
* 成功的图标svg
|
||||
*/
|
||||
export const successIconSvg = `<svg t="1771828214449" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5360" width="16" height="16"><path d="M748.864 302.528a49.024 49.024 0 0 1 68.288-1.28 46.656 46.656 0 0 1 5.952 61.44l-4.608 5.44-346.56 353.344a49.024 49.024 0 0 1-64.384 4.608l-5.504-4.864-196.8-203.264a46.72 46.72 0 0 1 1.792-66.88A49.024 49.024 0 0 1 270.08 448l5.312 4.736 162.048 167.232L748.8 302.528z" fill="#8a8a8a" p-id="5361"></path></svg>`;
|
||||
|
||||
/**
|
||||
* 个人规则的图标svg
|
||||
*/
|
||||
export const peopleRules = `<svg t="1772851533961" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11188" width="16" height="16"><path d="M652.8 534.4c70.4-44.8 115.2-124.8 115.2-214.4 0-140.8-115.2-256-256-256s-256 115.2-256 256c0 89.6 44.8 169.6 115.2 214.4C192 592 64 761.6 64 960h64c0-211.2 172.8-384 384-384s384 172.8 384 384h64c0-198.4-128-368-307.2-425.6zM512 512c-105.6 0-192-86.4-192-192s86.4-192 192-192 192 86.4 192 192-86.4 192-192 192z" fill="#6caed4" p-id="11189"></path></svg>`;
|
||||
|
||||
152
src/extension.ts
152
src/extension.ts
@ -10,10 +10,42 @@ import { initCreditsService } from "./services/creditsService";
|
||||
import { isTokenExpired } from "./utils/jwtUtils";
|
||||
import { NotificationService } from "./services/notificationService";
|
||||
import { InvitationService } from "./services/invitationService";
|
||||
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 创建装饰类型(代码旁边的提示)
|
||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||
after: {
|
||||
contentText: ' Ctrl+L 添加到 IC Coder 对话',
|
||||
color: '#888',
|
||||
fontStyle: 'italic',
|
||||
margin: '0 0 0 1em'
|
||||
}
|
||||
});
|
||||
|
||||
// 更新装饰
|
||||
const updateDecorations = () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
if (!editor.selection.isEmpty) {
|
||||
const range = new vscode.Range(editor.selection.end, editor.selection.end);
|
||||
const decoration = { range };
|
||||
editor.setDecorations(decorationType, [decoration]);
|
||||
} else {
|
||||
editor.setDecorations(decorationType, []);
|
||||
}
|
||||
};
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeTextEditorSelection(updateDecorations),
|
||||
vscode.window.onDidChangeActiveTextEditor(updateDecorations)
|
||||
);
|
||||
|
||||
updateDecorations();
|
||||
|
||||
// 初始化通知服务
|
||||
const notificationService = NotificationService.getInstance(context);
|
||||
console.log('[Extension] 通知服务已初始化');
|
||||
@ -159,20 +191,32 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// 注册命令:用户登录
|
||||
const loginCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.login",
|
||||
async () => {
|
||||
async (options?: { forceReauth?: boolean }) => {
|
||||
try {
|
||||
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
|
||||
try {
|
||||
await vscode.authentication.getSession("iccoder", [], {
|
||||
clearSessionPreference: true,
|
||||
createIfNone: false
|
||||
});
|
||||
} catch {
|
||||
// 忽略错误
|
||||
const forceReauth = options?.forceReauth === true;
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
const expired = session?.accessToken
|
||||
? isTokenExpired(session.accessToken)
|
||||
: null;
|
||||
|
||||
// 会话仍有效时,直接打开聊天面板
|
||||
if (session && expired === false && !forceReauth) {
|
||||
vscode.commands.executeCommand("ic-coder.openChat");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新 session
|
||||
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
||||
// 1) 清空当前登录状态信息
|
||||
await authProvider.clearSessionsForRelogin();
|
||||
await context.globalState.update("icCoderSessions", []);
|
||||
await context.globalState.update("icCoderUserInfo", undefined);
|
||||
|
||||
// 2) 重新登录(强制新会话)
|
||||
await vscode.authentication.getSession("iccoder", [], {
|
||||
clearSessionPreference: true,
|
||||
forceNewSession: true,
|
||||
});
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
||||
}
|
||||
@ -238,6 +282,81 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:将选中代码添加到对话
|
||||
const addCodeToChat = vscode.commands.registerCommand(
|
||||
"ic-coder.addCodeToChat",
|
||||
async () => {
|
||||
console.log('[addCodeToChat] 命令触发');
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
console.log('[addCodeToChat] 没有活动编辑器');
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = editor.selection;
|
||||
const selectedText = editor.document.getText(selection);
|
||||
|
||||
if (!selectedText) {
|
||||
vscode.window.showWarningMessage("请先选择代码");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = editor.document.fileName;
|
||||
const startLine = selection.start.line + 1;
|
||||
const endLine = selection.end.line + 1;
|
||||
|
||||
// 检查是否已有打开的面板
|
||||
let panel = (global as any).currentICHelperPanel;
|
||||
let needCreatePanel = false;
|
||||
|
||||
if (!panel) {
|
||||
needCreatePanel = true;
|
||||
} else {
|
||||
// 尝试访问 webview,如果抛出异常说明已销毁
|
||||
try {
|
||||
const _ = panel.webview;
|
||||
} catch (e) {
|
||||
needCreatePanel = true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[addCodeToChat] 需要创建面板:', needCreatePanel);
|
||||
|
||||
if (needCreatePanel) {
|
||||
console.log('[addCodeToChat] 正在打开面板...');
|
||||
await showICHelperPanel(context);
|
||||
panel = (global as any).currentICHelperPanel;
|
||||
console.log('[addCodeToChat] 面板打开后状态:', panel ? '成功' : '失败');
|
||||
|
||||
// 如果面板仍未创建(如未登录),直接返回
|
||||
if (!panel) {
|
||||
console.log('[addCodeToChat] 面板创建失败,退出');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送代码上下文
|
||||
console.log('[addCodeToChat] 准备发送代码到面板');
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (panel?.webview) {
|
||||
console.log('[addCodeToChat] 发送 addCodeContext 消息');
|
||||
panel.webview.postMessage({
|
||||
command: 'addCodeContext',
|
||||
fileName,
|
||||
startLine,
|
||||
endLine,
|
||||
code: selectedText,
|
||||
languageId: editor.document.languageId
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[addCodeToChat] 发送消息失败:', e);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:查看会话历史
|
||||
// TODO: 这些命令需要根据新的任务架构重新实现
|
||||
// 暂时注释掉,等待重新实现
|
||||
@ -300,6 +419,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// 注册 VCD 自定义编辑器
|
||||
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
|
||||
|
||||
// 注册 Code Action Provider
|
||||
const codeActionProvider = vscode.languages.registerCodeActionsProvider(
|
||||
{ scheme: 'file' },
|
||||
new ICCoderCodeActionProvider(),
|
||||
{ providedCodeActionKinds: [vscode.CodeActionKind.RefactorRewrite] }
|
||||
);
|
||||
|
||||
// 添加到订阅
|
||||
context.subscriptions.push(
|
||||
openPanelCommand,
|
||||
@ -310,6 +436,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
logoutCommand,
|
||||
changeInvitationCodeCommand,
|
||||
testNotificationCommand,
|
||||
addCodeToChat,
|
||||
// testTrialUserCommand,
|
||||
// testExpiredUserCommand,
|
||||
// TODO: 等待重新实现这些命令
|
||||
@ -320,7 +447,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// clearHistoryCommand,
|
||||
// searchSessionCommand,
|
||||
viewRegistration,
|
||||
vcdEditorProvider
|
||||
vcdEditorProvider,
|
||||
codeActionProvider
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ import { MessageType } from "../types/chatHistory";
|
||||
import { getCachedUserInfo } from "../services/userService";
|
||||
import { isTokenExpired } from "../utils/jwtUtils";
|
||||
import { setBalanceUpdateCallback } from "../services/creditsService";
|
||||
import { savePersonalRule, updatePersonalRule, deletePersonalRule, loadPersonalRules } from "../utils/personalRulesManager";
|
||||
|
||||
/**
|
||||
* 获取会员等级图标 URI
|
||||
@ -53,7 +54,7 @@ function getTierIconUri(
|
||||
const iconUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"titleIcon",
|
||||
iconFile,
|
||||
@ -91,7 +92,9 @@ export async function showICHelperPanel(
|
||||
"立即登录",
|
||||
);
|
||||
if (action === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -106,7 +109,9 @@ export async function showICHelperPanel(
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
@ -116,7 +121,9 @@ export async function showICHelperPanel(
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
@ -132,11 +139,14 @@ export async function showICHelperPanel(
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "dist", "assets"),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
// 保存 panel 引用到全局
|
||||
(global as any).currentICHelperPanel = panel;
|
||||
|
||||
// 为面板生成唯一ID
|
||||
const panelId = `panel_${Date.now()}_${Math.random()
|
||||
.toString(36)
|
||||
@ -160,7 +170,7 @@ export async function showICHelperPanel(
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Auto.png",
|
||||
@ -169,7 +179,7 @@ export async function showICHelperPanel(
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"lite.png",
|
||||
@ -178,7 +188,7 @@ export async function showICHelperPanel(
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Sy.png",
|
||||
@ -187,7 +197,7 @@ export async function showICHelperPanel(
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"model",
|
||||
"Max.png",
|
||||
@ -198,7 +208,7 @@ export async function showICHelperPanel(
|
||||
const qrCodeUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"dist",
|
||||
"assets",
|
||||
"QRCode",
|
||||
"wx.png",
|
||||
@ -430,6 +440,7 @@ export async function showICHelperPanel(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput,
|
||||
message.answers
|
||||
);
|
||||
break;
|
||||
// 新增:中止对话
|
||||
@ -484,6 +495,125 @@ export async function showICHelperPanel(
|
||||
// 退出登录
|
||||
vscode.commands.executeCommand("ic-coder.logout");
|
||||
break;
|
||||
case "savePersonalRule":
|
||||
// 保存个人规则
|
||||
if (message.name && message.content && message.enabled !== undefined) {
|
||||
const success = await savePersonalRule(message.name, message.content, message.enabled);
|
||||
if (success) {
|
||||
const rulesData = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: rulesData
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "updatePersonalRule":
|
||||
// 更新个人规则
|
||||
if (message.filename && message.name && message.content && message.enabled !== undefined) {
|
||||
const success = await updatePersonalRule(message.filename, message.name, message.content, message.enabled);
|
||||
if (success) {
|
||||
const rulesData = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: rulesData
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "deletePersonalRule":
|
||||
// 删除个人规则
|
||||
if (message.filename) {
|
||||
const success = await deletePersonalRule(message.filename);
|
||||
if (success) {
|
||||
const rulesData = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: rulesData
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "loadPersonalRules":
|
||||
// 加载个人规则
|
||||
const rulesData = loadPersonalRules();
|
||||
panel.webview.postMessage({
|
||||
command: "personalRulesLoaded",
|
||||
data: rulesData
|
||||
});
|
||||
break;
|
||||
case "openFile":
|
||||
// 打开文件
|
||||
if (message.filePath) {
|
||||
const path = require('path');
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder
|
||||
? message.filePath
|
||||
: vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
|
||||
vscode.workspace.openTextDocument(fullPath).then(doc => {
|
||||
vscode.window.showTextDocument(doc);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "openFileWithSelection":
|
||||
// 打开文件并选中代码
|
||||
if (message.filePath) {
|
||||
const path = require('path');
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder
|
||||
? message.filePath
|
||||
: vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
|
||||
vscode.workspace.openTextDocument(fullPath).then(doc => {
|
||||
vscode.window.showTextDocument(doc).then(editor => {
|
||||
const start = new vscode.Position(message.startLine - 1, 0);
|
||||
const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length);
|
||||
editor.selection = new vscode.Selection(start, end);
|
||||
editor.revealRange(new vscode.Range(start, end));
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "openFilePathTag":
|
||||
// 打开文件路径标签(智能查找)
|
||||
if (message.filePath) {
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
|
||||
let fullPath = message.filePath;
|
||||
|
||||
// 如果是相对路径且工作区存在
|
||||
if (!path.isAbsolute(message.filePath) && workspaceFolder) {
|
||||
const candidatePath = vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
|
||||
// 检查文件是否存在
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
fullPath = candidatePath;
|
||||
} else {
|
||||
// 尝试在工作区中搜索该文件
|
||||
const fileName = path.basename(message.filePath);
|
||||
const files = await vscode.workspace.findFiles(`**/${fileName}`, '**/node_modules/**', 1);
|
||||
if (files.length > 0) {
|
||||
fullPath = files[0].fsPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.startLine && message.endLine) {
|
||||
vscode.workspace.openTextDocument(fullPath).then(doc => {
|
||||
vscode.window.showTextDocument(doc).then(editor => {
|
||||
const start = new vscode.Position(message.startLine - 1, 0);
|
||||
const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length);
|
||||
editor.selection = new vscode.Selection(start, end);
|
||||
editor.revealRange(new vscode.Range(start, end));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
vscode.workspace.openTextDocument(fullPath).then(doc => {
|
||||
vscode.window.showTextDocument(doc);
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "acceptChange":
|
||||
// 采纳变更
|
||||
if (message.changeId) {
|
||||
@ -533,25 +663,39 @@ export async function showICHelperPanel(
|
||||
// 检查是否需要显示欢迎弹窗
|
||||
{
|
||||
console.log("[ICHelperPanel] 收到 checkWelcomeModal 消息");
|
||||
const showWelcome = context.globalState.get("showWelcomeModal");
|
||||
console.log(
|
||||
"[ICHelperPanel] showWelcomeModal 标记值:",
|
||||
showWelcome,
|
||||
);
|
||||
const userInfo = getCachedUserInfo();
|
||||
|
||||
if (showWelcome) {
|
||||
// 清除标记并显示欢迎弹窗
|
||||
await context.globalState.update("showWelcomeModal", undefined);
|
||||
console.log(
|
||||
"[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端",
|
||||
);
|
||||
console.log("[ICHelperPanel] 用户信息:", userInfo);
|
||||
console.log("[ICHelperPanel] isPluginTrial:", userInfo?.isPluginTrial);
|
||||
console.log("[ICHelperPanel] pluginTrialExpiresAt:", userInfo?.pluginTrialExpiresAt);
|
||||
|
||||
if (userInfo?.isPluginTrial === true) {
|
||||
// undefined 表示无效,不显示
|
||||
if (userInfo.pluginTrialExpiresAt === undefined) {
|
||||
console.log("[ICHelperPanel] pluginTrialExpiresAt 未设置,不显示欢迎弹窗");
|
||||
break;
|
||||
}
|
||||
|
||||
// null 表示长期有效,显示弹窗
|
||||
// 有值则检查是否过期
|
||||
if (userInfo.pluginTrialExpiresAt !== null) {
|
||||
const now = Date.now();
|
||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
console.log("[ICHelperPanel] 是否过期:", isExpired);
|
||||
|
||||
if (isExpired) {
|
||||
console.log("[ICHelperPanel] 试用已过期,不显示欢迎弹窗");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 未过期或长期有效(null),显示欢迎弹窗
|
||||
console.log("[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端");
|
||||
panel.webview.postMessage({
|
||||
command: "showWelcomeModal",
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
"[ICHelperPanel] showWelcomeModal 标记为 false,不显示弹窗",
|
||||
);
|
||||
console.log("[ICHelperPanel] 非试用用户");
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -752,6 +896,23 @@ export async function showICHelperPanel(
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 打开文件
|
||||
case "openFile":
|
||||
{
|
||||
let filePath = message.filePath;
|
||||
if (filePath) {
|
||||
// 如果是相对路径,转换为绝对路径
|
||||
if (!require("path").isAbsolute(filePath)) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (workspaceFolder) {
|
||||
filePath = require("path").join(workspaceFolder.uri.fsPath, filePath);
|
||||
}
|
||||
}
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
vscode.window.showTextDocument(uri);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 新增:检查工作区状态
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
|
||||
26
src/providers/codeActionProvider.ts
Normal file
26
src/providers/codeActionProvider.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Code Action Provider - 为选中代码提供快捷操作
|
||||
* 功能:在小灯泡菜单中显示"添加到 IC Coder 对话"选项
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class ICCoderCodeActionProvider implements vscode.CodeActionProvider {
|
||||
provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range
|
||||
): vscode.CodeAction[] {
|
||||
const selectedText = document.getText(range);
|
||||
if (!selectedText) return [];
|
||||
|
||||
const action = new vscode.CodeAction(
|
||||
'💬 添加到 IC Coder 对话',
|
||||
vscode.CodeActionKind.RefactorRewrite
|
||||
);
|
||||
action.command = {
|
||||
command: 'ic-coder.addCodeToChat',
|
||||
title: '添加到对话'
|
||||
};
|
||||
|
||||
return [action];
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,12 @@ class ChangeTrackerService {
|
||||
* 开始新的变更会话
|
||||
*/
|
||||
startSession(sessionId: string): void {
|
||||
// 如果已有 session(无论状态),重用并重置为 active
|
||||
if (this.currentSession) {
|
||||
this.currentSession.status = 'active';
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentSession = {
|
||||
sessionId,
|
||||
startTime: Date.now(),
|
||||
@ -71,7 +77,6 @@ class ChangeTrackerService {
|
||||
this.notifyListeners();
|
||||
return session;
|
||||
}
|
||||
this.currentSession = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import type {
|
||||
PlanConfirmEvent,
|
||||
} from "../types/api";
|
||||
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
|
||||
import { getActiveRules } from "../utils/personalRulesManager";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
|
||||
import { updateCachedBalance } from "./creditsService";
|
||||
@ -43,8 +44,7 @@ export interface MessageSegment {
|
||||
toolResult?: string;
|
||||
toolDescription?: string;
|
||||
askId?: string;
|
||||
question?: string;
|
||||
options?: string[];
|
||||
questions?: import("../types/api").QuestionItem[];
|
||||
// 智能体相关字段
|
||||
agentId?: string;
|
||||
agentName?: string;
|
||||
@ -97,7 +97,7 @@ export interface DialogCallbacks {
|
||||
summary: string
|
||||
) => void;
|
||||
/** 显示问题(ask_user) */
|
||||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
||||
onQuestion?: (askId: string, questions: import("../types/api").QuestionItem[]) => void;
|
||||
/** 实时更新段落(流式过程中) */
|
||||
onSegmentUpdate?: (segments: MessageSegment[]) => void;
|
||||
/** 对话完成,返回所有段落 */
|
||||
@ -449,7 +449,9 @@ export class DialogSession {
|
||||
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
||||
.then((selection) => {
|
||||
if (selection === "重新登录") {
|
||||
vscode.commands.executeCommand("iccoder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
throw new Error("登录已过期,请重新登录");
|
||||
@ -501,6 +503,7 @@ export class DialogSession {
|
||||
compactedData: compactedData || undefined,
|
||||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||||
knowledgeData: knowledgeData || undefined,
|
||||
personalRules: getActiveRules() || undefined,
|
||||
};
|
||||
|
||||
// 追踪用户消息
|
||||
@ -645,8 +648,11 @@ export class DialogSession {
|
||||
this.segments.push({
|
||||
type: "question",
|
||||
askId: askId,
|
||||
question: question,
|
||||
options: ["确认执行", "取消"],
|
||||
questions: [{
|
||||
question: question,
|
||||
options: ["确认执行", "取消"],
|
||||
multiSelect: false
|
||||
}],
|
||||
});
|
||||
|
||||
// 实时发送段落更新
|
||||
@ -664,8 +670,11 @@ export class DialogSession {
|
||||
await userInteractionManager.handleAskUser(
|
||||
{
|
||||
askId: askId,
|
||||
question: question,
|
||||
options: ["确认执行", "取消"],
|
||||
questions: [{
|
||||
question: question,
|
||||
options: ["确认执行", "取消"],
|
||||
multiSelect: false
|
||||
}]
|
||||
} as AskUserEvent,
|
||||
this.taskId
|
||||
);
|
||||
@ -712,8 +721,11 @@ export class DialogSession {
|
||||
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||||
const planEvent = {
|
||||
askId: askId,
|
||||
question: `请确认执行计划:${data.title}`,
|
||||
options: ["确认执行", "修改计划", "取消"],
|
||||
questions: [{
|
||||
question: `请确认执行计划:${data.title}`,
|
||||
options: ["确认执行", "修改计划", "取消"],
|
||||
multiSelect: false
|
||||
}]
|
||||
};
|
||||
try {
|
||||
await userInteractionManager.handleAskUser(
|
||||
@ -854,13 +866,12 @@ export class DialogSession {
|
||||
this.segments.push({
|
||||
type: "question",
|
||||
askId: data.askId,
|
||||
question: data.question,
|
||||
options: data.options,
|
||||
questions: data.questions,
|
||||
});
|
||||
// 实时发送段落更新(包含问题)
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
// 同时调用 onQuestion 用于更新状态栏等
|
||||
callbacks.onQuestion?.(data.askId, data.question, data.options);
|
||||
callbacks.onQuestion?.(data.askId, data.questions);
|
||||
try {
|
||||
await userInteractionManager.handleAskUser(data, this.taskId);
|
||||
} catch (error) {
|
||||
@ -894,7 +905,9 @@ export class DialogSession {
|
||||
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
||||
.then((selection) => {
|
||||
if (selection === "重新登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
// 登录过期错误已处理,不再传递给外部
|
||||
@ -1106,7 +1119,8 @@ export class DialogSession {
|
||||
async submitAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
customInput?: string,
|
||||
answers?: { [questionIndex: string]: string[] }
|
||||
): Promise<void> {
|
||||
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
||||
// 如果 pendingQuestions 中有问题,走正常流程
|
||||
@ -1115,6 +1129,7 @@ export class DialogSession {
|
||||
askId,
|
||||
selected,
|
||||
customInput,
|
||||
answers,
|
||||
this.taskId
|
||||
);
|
||||
}
|
||||
|
||||
@ -176,6 +176,28 @@ export class ICCoderAuthenticationProvider
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear local authentication state without window reload.
|
||||
* Used by re-login flow when session is expired.
|
||||
*/
|
||||
async clearSessionsForRelogin(): Promise<void> {
|
||||
if (this._sessions.length === 0) {
|
||||
await clearUserInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
const removed = [...this._sessions];
|
||||
this._sessions = [];
|
||||
await this.saveSessions();
|
||||
await clearUserInfo();
|
||||
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
removed,
|
||||
changed: [],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话 ID
|
||||
*/
|
||||
|
||||
@ -2,23 +2,31 @@
|
||||
* 工具执行器
|
||||
* 接收后端的 tool_call 事件,执行本地工具,返回结果
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
||||
import { resolveWorkspaceFilePath, showFileDiff } from '../utils/fileDiff';
|
||||
import { changeTracker } from './changeTracker';
|
||||
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
||||
import { analyzeVcdFile } from '../utils/vcdParser';
|
||||
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import * as fs from "fs";
|
||||
import { readFileContent, readDirectory } from "../utils/readFiles";
|
||||
import { createOrOverwriteFile } from "../utils/createFiles";
|
||||
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
|
||||
import { changeTracker } from "./changeTracker";
|
||||
import {
|
||||
generateVCD,
|
||||
checkIverilogAvailable,
|
||||
generateMultiVCD,
|
||||
DumpModule,
|
||||
} from "../utils/iverilogRunner";
|
||||
import { analyzeVcdFile } from "../utils/vcdParser";
|
||||
import {
|
||||
executeWaveformTrace,
|
||||
WaveformTraceArgs,
|
||||
} from "../utils/waveformTracer";
|
||||
import {
|
||||
submitToolResult,
|
||||
createSuccessResult,
|
||||
createBusinessErrorResult,
|
||||
createSystemErrorResult
|
||||
} from './apiClient';
|
||||
createSystemErrorResult,
|
||||
} from "./apiClient";
|
||||
import type {
|
||||
ToolCallRequest,
|
||||
ToolName,
|
||||
@ -31,8 +39,8 @@ import type {
|
||||
SimulationArgs,
|
||||
WaveformSummaryArgs,
|
||||
KnowledgeSaveArgs,
|
||||
KnowledgeLoadArgs
|
||||
} from '../types/api';
|
||||
KnowledgeLoadArgs,
|
||||
} from "../types/api";
|
||||
|
||||
/**
|
||||
* 工具执行器上下文
|
||||
@ -51,7 +59,7 @@ export interface ToolExecutorContext {
|
||||
*/
|
||||
export async function executeToolCall(
|
||||
request: ToolCallRequest,
|
||||
context: ToolExecutorContext
|
||||
context: ToolExecutorContext,
|
||||
): Promise<void> {
|
||||
const toolName = request.params.name as ToolName;
|
||||
const args = request.params.arguments;
|
||||
@ -63,37 +71,53 @@ export async function executeToolCall(
|
||||
let resultText: string;
|
||||
|
||||
switch (toolName) {
|
||||
case 'file_read':
|
||||
case "file_read":
|
||||
resultText = await executeFileRead(args as unknown as FileReadArgs);
|
||||
break;
|
||||
case 'file_write':
|
||||
case "file_write":
|
||||
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
||||
break;
|
||||
case 'file_delete':
|
||||
case "file_delete":
|
||||
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
|
||||
break;
|
||||
case 'file_list':
|
||||
case "file_list":
|
||||
resultText = await executeFileList(args as unknown as FileListArgs);
|
||||
break;
|
||||
case 'syntax_check':
|
||||
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
|
||||
case "syntax_check":
|
||||
resultText = await executeSyntaxCheck(
|
||||
args as unknown as SyntaxCheckArgs,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case 'iverilog':
|
||||
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
|
||||
case "iverilog":
|
||||
resultText = await executeIverilog(
|
||||
args as unknown as IverilogArgs,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case 'simulation':
|
||||
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
||||
case "simulation":
|
||||
resultText = await executeSimulation(
|
||||
args as unknown as SimulationArgs,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case 'waveform_summary':
|
||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
||||
case "waveform_summary":
|
||||
resultText = await executeWaveformSummary(
|
||||
args as unknown as WaveformSummaryArgs,
|
||||
);
|
||||
break;
|
||||
case 'waveform_trace':
|
||||
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
|
||||
case "waveform_trace":
|
||||
resultText = await executeWaveformTrace(
|
||||
args as unknown as WaveformTraceArgs,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case 'knowledge_save':
|
||||
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
||||
case "knowledge_save":
|
||||
resultText = await executeKnowledgeSave(
|
||||
args as unknown as KnowledgeSaveArgs,
|
||||
);
|
||||
break;
|
||||
case 'knowledge_load':
|
||||
case "knowledge_load":
|
||||
resultText = await executeKnowledgeLoad();
|
||||
break;
|
||||
default:
|
||||
@ -104,10 +128,12 @@ export async function executeToolCall(
|
||||
const result = createSuccessResult(callId, resultText);
|
||||
await submitToolResult(result);
|
||||
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
console.error(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error);
|
||||
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||
console.error(
|
||||
`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`,
|
||||
error,
|
||||
);
|
||||
|
||||
// 提交错误结果
|
||||
const result = createBusinessErrorResult(callId, errorMessage);
|
||||
@ -129,7 +155,7 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||
const absolutePath = resolveWorkspaceFilePath(args.path);
|
||||
const existedBeforeWrite = fs.existsSync(absolutePath);
|
||||
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : '';
|
||||
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : "";
|
||||
|
||||
await createOrOverwriteFile(args.path, args.content);
|
||||
|
||||
@ -137,11 +163,11 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||
try {
|
||||
changeTracker.trackChange(args.path, oldContent, args.content);
|
||||
} catch (error) {
|
||||
console.warn('[ToolExecutor] 记录文件变更失败:', error);
|
||||
console.warn("[ToolExecutor] 记录文件变更失败:", error);
|
||||
}
|
||||
|
||||
// Verilog 文件添加知识图谱提示
|
||||
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
||||
const isVerilogFile = args.path.endsWith(".v") || args.path.endsWith(".sv");
|
||||
if (isVerilogFile) {
|
||||
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
|
||||
}
|
||||
@ -151,7 +177,7 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||
|
||||
/**
|
||||
* 执行 file_delete 工具
|
||||
* 删除指定路径的文件
|
||||
* 删除指定路径的文件(带用户确认)
|
||||
*/
|
||||
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||
const filePath = args.path;
|
||||
@ -159,7 +185,7 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
throw new Error("请先打开一个工作区");
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
@ -180,18 +206,60 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
||||
}
|
||||
|
||||
// 验证文件路径在工作区内
|
||||
const isInWorkspace = workspaceFolders.some((folder) =>
|
||||
absolutePath.startsWith(folder.uri.fsPath),
|
||||
);
|
||||
|
||||
if (!isInWorkspace) {
|
||||
throw new Error("只能删除工作区内的文件");
|
||||
}
|
||||
|
||||
// 保护敏感文件
|
||||
const protectedFiles = [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
".git",
|
||||
"node_modules",
|
||||
];
|
||||
|
||||
const fileName = path.basename(absolutePath);
|
||||
if (protectedFiles.includes(fileName)) {
|
||||
throw new Error(`不允许删除系统文件: ${fileName}`);
|
||||
}
|
||||
|
||||
// 弹出确认对话框
|
||||
const confirmed = await vscode.window.showWarningMessage(
|
||||
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
|
||||
{
|
||||
modal: true, // 模态对话框,阻止其他操作
|
||||
detail: "⚠️ 文件将被移到回收站,可以恢复",
|
||||
},
|
||||
"确定删除",
|
||||
"取消",
|
||||
);
|
||||
|
||||
// 用户取消或关闭对话框
|
||||
if (confirmed !== "确定删除") {
|
||||
throw new Error("用户取消了删除操作");
|
||||
}
|
||||
|
||||
// 读取文件内容用于变更追踪
|
||||
const oldContent = fs.readFileSync(absolutePath, 'utf-8');
|
||||
const oldContent = fs.readFileSync(absolutePath, "utf-8");
|
||||
|
||||
// 记录删除变更
|
||||
const relativePath = path.relative(workspacePath, absolutePath);
|
||||
changeTracker.trackChange(relativePath, oldContent, '');
|
||||
changeTracker.trackChange(relativePath, oldContent, "");
|
||||
|
||||
// 删除文件
|
||||
fs.unlinkSync(absolutePath);
|
||||
// 删除文件(移到回收站)
|
||||
const uri = vscode.Uri.file(absolutePath);
|
||||
await vscode.workspace.fs.delete(uri, {
|
||||
recursive: false, // 不是目录,设为 false
|
||||
useTrash: true, // 移到回收站而非永久删除
|
||||
});
|
||||
|
||||
// Verilog 文件添加知识图谱提示
|
||||
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
|
||||
const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv");
|
||||
if (isVerilogFile) {
|
||||
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
||||
}
|
||||
@ -203,13 +271,13 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||
* 执行 file_list 工具
|
||||
*/
|
||||
async function executeFileList(args: FileListArgs): Promise<string> {
|
||||
const dirPath = args.path || '.';
|
||||
const dirPath = args.path || ".";
|
||||
const extensions = args.extension ? [args.extension] : undefined;
|
||||
|
||||
const files = await readDirectory(dirPath, extensions);
|
||||
const fileList = files.map(f => f.path).join('\n');
|
||||
const fileList = files.map((f) => f.path).join("\n");
|
||||
|
||||
return fileList || '(目录为空)';
|
||||
return fileList || "(目录为空)";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,7 +286,7 @@ async function executeFileList(args: FileListArgs): Promise<string> {
|
||||
*/
|
||||
async function executeSyntaxCheck(
|
||||
args: SyntaxCheckArgs,
|
||||
context: ToolExecutorContext
|
||||
context: ToolExecutorContext,
|
||||
): Promise<string> {
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
@ -232,33 +300,33 @@ async function executeSyntaxCheck(
|
||||
|
||||
try {
|
||||
// 写入代码到临时文件
|
||||
fs.writeFileSync(tempFile, args.code, 'utf-8');
|
||||
fs.writeFileSync(tempFile, args.code, "utf-8");
|
||||
|
||||
// 调用 iverilog 进行语法检查
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn } = require("child_process");
|
||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(iverilogPath, ['-t', 'null', tempFile], {
|
||||
const child = spawn(iverilogPath, ["-t", "null", tempFile], {
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
||||
}
|
||||
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
|
||||
},
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
child.stdout.on("data", (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
child.stderr.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number) => {
|
||||
child.on("close", (code: number) => {
|
||||
// 清理临时文件
|
||||
try {
|
||||
fs.unlinkSync(tempFile);
|
||||
@ -267,13 +335,13 @@ async function executeSyntaxCheck(
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
resolve('语法检查通过,无错误。');
|
||||
resolve("语法检查通过,无错误。");
|
||||
} else {
|
||||
resolve(`语法检查发现错误:\n${stderr || stdout}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
child.on("error", (error: Error) => {
|
||||
try {
|
||||
fs.unlinkSync(tempFile);
|
||||
} catch (e) {
|
||||
@ -282,7 +350,6 @@ async function executeSyntaxCheck(
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// 确保清理临时文件
|
||||
try {
|
||||
@ -300,7 +367,7 @@ async function executeSyntaxCheck(
|
||||
*/
|
||||
async function executeIverilog(
|
||||
args: IverilogArgs,
|
||||
context: ToolExecutorContext
|
||||
context: ToolExecutorContext,
|
||||
): Promise<string> {
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
@ -311,7 +378,7 @@ async function executeIverilog(
|
||||
// 获取工作目录
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('没有打开的工作区');
|
||||
throw new Error("没有打开的工作区");
|
||||
}
|
||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||
const workDir = args.workDir
|
||||
@ -320,32 +387,32 @@ async function executeIverilog(
|
||||
|
||||
// 解析参数
|
||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||
const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0);
|
||||
const cmdArgs = args.args.split(/\s+/).filter((a) => a.length > 0);
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(iverilogPath, cmdArgs, {
|
||||
cwd: workDir,
|
||||
env: {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
||||
}
|
||||
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
|
||||
},
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
child.stdout.on("data", (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
child.stderr.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number) => {
|
||||
const output = stderr || stdout || '(无输出)';
|
||||
child.on("close", (code: number) => {
|
||||
const output = stderr || stdout || "(无输出)";
|
||||
if (code === 0) {
|
||||
resolve(`执行成功\n${output}`);
|
||||
} else {
|
||||
@ -353,7 +420,7 @@ async function executeIverilog(
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
child.on("error", (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
@ -364,12 +431,12 @@ async function executeIverilog(
|
||||
*/
|
||||
async function executeSimulation(
|
||||
args: SimulationArgs,
|
||||
context: ToolExecutorContext
|
||||
context: ToolExecutorContext,
|
||||
): Promise<string> {
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
throw new Error("请先打开一个工作区");
|
||||
}
|
||||
|
||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||
@ -377,21 +444,24 @@ async function executeSimulation(
|
||||
// 检查是否有 dumpModules 参数(多 VCD 模式)
|
||||
if (args.dumpModules) {
|
||||
const modules = parseDumpModules(args.dumpModules);
|
||||
const vcdDir = args.vcdDir || 'vcd';
|
||||
const vcdDir = args.vcdDir || "vcd";
|
||||
|
||||
const result = await generateMultiVCD(
|
||||
projectPath,
|
||||
context.extensionPath,
|
||||
args.tbPath,
|
||||
modules,
|
||||
vcdDir
|
||||
vcdDir,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const vcdList = result.vcdFiles
|
||||
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`)
|
||||
.join('\n');
|
||||
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`;
|
||||
.map(
|
||||
(f) =>
|
||||
`- ${f.moduleName}: ${f.success ? f.vcdPath : "失败 - " + f.error}`,
|
||||
)
|
||||
.join("\n");
|
||||
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? "\n\n仿真输出:" + result.stdout : ""}`;
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
@ -420,8 +490,8 @@ async function executeSimulation(
|
||||
* 格式:name:path,name:path
|
||||
*/
|
||||
function parseDumpModules(dumpModules: string): DumpModule[] {
|
||||
return dumpModules.split(',').map(item => {
|
||||
const [name, modulePath] = item.trim().split(':');
|
||||
return dumpModules.split(",").map((item) => {
|
||||
const [name, modulePath] = item.trim().split(":");
|
||||
return { name: name.trim(), path: modulePath.trim() };
|
||||
});
|
||||
}
|
||||
@ -430,13 +500,15 @@ function parseDumpModules(dumpModules: string): DumpModule[] {
|
||||
* 执行 waveform_summary 工具
|
||||
* 解析 VCD 文件并返回波形摘要
|
||||
*/
|
||||
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
|
||||
async function executeWaveformSummary(
|
||||
args: WaveformSummaryArgs,
|
||||
): Promise<string> {
|
||||
const { vcdPath, signals, checkpoints } = args;
|
||||
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
throw new Error("请先打开一个工作区");
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
@ -467,17 +539,20 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
|
||||
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||
const workspaceFolder = getWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
throw new Error("请先打开一个工作区");
|
||||
}
|
||||
|
||||
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
|
||||
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
|
||||
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, ".iccoder");
|
||||
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, "knowledge.json");
|
||||
|
||||
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
|
||||
await vscode.workspace.fs.createDirectory(iccoderDirUri);
|
||||
|
||||
// 写入知识图谱(UTF-8)
|
||||
await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8'));
|
||||
await vscode.workspace.fs.writeFile(
|
||||
knowledgeUri,
|
||||
Buffer.from(args.data || "", "utf-8"),
|
||||
);
|
||||
|
||||
return `知识图谱已保存: .iccoder/knowledge.json`;
|
||||
}
|
||||
@ -489,20 +564,33 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||
async function executeKnowledgeLoad(): Promise<string> {
|
||||
const workspaceFolder = getWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
throw new Error("请先打开一个工作区");
|
||||
}
|
||||
|
||||
const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json');
|
||||
const knowledgeUri = vscode.Uri.joinPath(
|
||||
workspaceFolder.uri,
|
||||
".iccoder",
|
||||
"knowledge.json",
|
||||
);
|
||||
|
||||
try {
|
||||
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
|
||||
const content = Buffer.from(bytes).toString('utf-8');
|
||||
const content = Buffer.from(bytes).toString("utf-8");
|
||||
return content;
|
||||
} catch (error) {
|
||||
// 文件不存在:返回空图谱
|
||||
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
||||
if (
|
||||
error instanceof vscode.FileSystemError &&
|
||||
error.code === "FileNotFound"
|
||||
) {
|
||||
// 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段)
|
||||
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] });
|
||||
return JSON.stringify({
|
||||
taskId: "",
|
||||
version: 1,
|
||||
module: null,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@ -515,7 +603,9 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
||||
}
|
||||
|
||||
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||
const activeFolder = activeUri
|
||||
? vscode.workspace.getWorkspaceFolder(activeUri)
|
||||
: undefined;
|
||||
return activeFolder ?? folders[0];
|
||||
}
|
||||
|
||||
@ -524,22 +614,24 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
||||
*/
|
||||
function getIverilogPath(extensionPath: string): string {
|
||||
const platform = process.platform;
|
||||
if (platform === 'win32') {
|
||||
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe');
|
||||
if (platform === "win32") {
|
||||
return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
|
||||
} else {
|
||||
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog');
|
||||
return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工具执行器上下文
|
||||
*/
|
||||
export function createToolExecutorContext(extensionPath: string): ToolExecutorContext {
|
||||
export function createToolExecutorContext(
|
||||
extensionPath: string,
|
||||
): ToolExecutorContext {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || '';
|
||||
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || "";
|
||||
|
||||
return {
|
||||
extensionPath,
|
||||
workspacePath
|
||||
workspacePath,
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import { submitAnswer, submitToolConfirm } from './apiClient';
|
||||
import type { AskUserEvent, AnswerRequest } from '../types/api';
|
||||
import type { AskUserEvent, AnswerRequest, QuestionItem } from '../types/api';
|
||||
|
||||
/**
|
||||
* 待处理的用户问题
|
||||
@ -12,8 +12,7 @@ import type { AskUserEvent, AnswerRequest } from '../types/api';
|
||||
interface PendingQuestion {
|
||||
askId: string;
|
||||
taskId: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
questions: QuestionItem[];
|
||||
resolve: (answer: string) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
@ -45,9 +44,9 @@ export class UserInteractionManager {
|
||||
* @param taskId 当前任务ID
|
||||
*/
|
||||
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
|
||||
const { askId, question, options } = event;
|
||||
const { askId, questions } = event;
|
||||
|
||||
console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`);
|
||||
console.log(`[UserInteraction] 收到问题: askId=${askId}, count=${questions.length}`);
|
||||
|
||||
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
|
||||
// 这里不再单独发送 showQuestion 命令,避免重复显示
|
||||
@ -57,8 +56,7 @@ export class UserInteractionManager {
|
||||
this.pendingQuestions.set(askId, {
|
||||
askId,
|
||||
taskId,
|
||||
question,
|
||||
options,
|
||||
questions,
|
||||
resolve: (answer: string) => {
|
||||
this.submitUserAnswer(askId, taskId, answer)
|
||||
.then(() => resolve())
|
||||
@ -80,24 +78,38 @@ export class UserInteractionManager {
|
||||
/**
|
||||
* 处理用户提交的回答(从 WebView 调用)
|
||||
* @param askId 问题ID
|
||||
* @param selected 选中的选项
|
||||
* @param customInput 自定义输入
|
||||
* @param selected 选中的选项(旧格式)
|
||||
* @param customInput 自定义输入(旧格式)
|
||||
* @param answers 新格式:按问题索引的答案
|
||||
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
||||
*/
|
||||
async receiveAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string,
|
||||
answers?: { [questionIndex: string]: string[] },
|
||||
fallbackTaskId?: string
|
||||
): Promise<void> {
|
||||
const pending = this.pendingQuestions.get(askId);
|
||||
const answer = customInput || selected?.join(', ') || '';
|
||||
|
||||
// 构建答案字符串
|
||||
let answer = '';
|
||||
if (answers && Object.keys(answers).length > 0) {
|
||||
// 新格式:多问题答案
|
||||
answer = Object.entries(answers)
|
||||
.sort(([a], [b]) => parseInt(a) - parseInt(b))
|
||||
.map(([_, vals]) => vals.join('; '))
|
||||
.join(' | ');
|
||||
} else {
|
||||
// 旧格式:单问题答案
|
||||
answer = customInput || selected?.join(', ') || '';
|
||||
}
|
||||
|
||||
if (!pending) {
|
||||
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
||||
if (fallbackTaskId) {
|
||||
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
||||
await this.submitUserAnswer(askId, fallbackTaskId, answer);
|
||||
await this.submitUserAnswer(askId, fallbackTaskId, answer, answers);
|
||||
} else {
|
||||
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||||
}
|
||||
@ -119,7 +131,8 @@ export class UserInteractionManager {
|
||||
private async submitUserAnswer(
|
||||
askId: string,
|
||||
taskId: string,
|
||||
answer: string
|
||||
answer: string,
|
||||
answers?: { [questionIndex: string]: string[] }
|
||||
): Promise<void> {
|
||||
// 检查是否是工具确认类型的问题
|
||||
if (askId.startsWith('tool_confirm_')) {
|
||||
@ -148,7 +161,8 @@ export class UserInteractionManager {
|
||||
const request: AnswerRequest = {
|
||||
askId,
|
||||
taskId,
|
||||
customInput: answer
|
||||
answers: answers,
|
||||
customInput: answers ? undefined : answer
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@ -162,10 +162,14 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||||
console.log('[UserService] 从 getInfo 接口获取到 isPluginTrial: true');
|
||||
}
|
||||
|
||||
// 获取试用到期时间
|
||||
if (response.enterpriseTrialExpires) {
|
||||
// 获取试用到期时间(null 表示长期有效)
|
||||
if (response.enterpriseTrialExpires !== undefined) {
|
||||
userInfo.pluginTrialExpiresAt = response.enterpriseTrialExpires;
|
||||
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
|
||||
if (response.enterpriseTrialExpires === null) {
|
||||
console.log('[UserService] 试用长期有效');
|
||||
} else {
|
||||
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
@ -335,28 +339,38 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
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 (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
|
||||
// 检查是否过期
|
||||
const now = Date.now();
|
||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
console.log('[UserService] 试用到期时间:', new Date(userInfo.pluginTrialExpiresAt).toLocaleString());
|
||||
console.log('[UserService] 当前时间:', new Date(now).toLocaleString());
|
||||
console.log('[UserService] 是否过期:', isExpired);
|
||||
|
||||
if (!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,无法设置标记');
|
||||
if (isExpired) {
|
||||
// 已过期:显示邀请码弹窗
|
||||
console.log('[UserService] 试用已过期,将显示邀请码弹窗');
|
||||
} else {
|
||||
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
|
||||
// 未过期:显示欢迎弹窗
|
||||
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] 正式用户登录,将在面板中检查邀请码');
|
||||
// isPluginTrial=false 或 enterpriseTrialExpires 为 null:显示邀请码弹窗
|
||||
console.log('[UserService] 非试用用户或无过期时间,将显示邀请码弹窗');
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
|
||||
@ -48,6 +48,8 @@ export interface DialogRequest {
|
||||
newMessages?: CompactedMessage[];
|
||||
/** 知识图谱数据(JSON 字符串,用于恢复知识图谱) */
|
||||
knowledgeData?: string;
|
||||
/** 个人规则 */
|
||||
personalRules?: string;
|
||||
}
|
||||
|
||||
// ============== SSE 事件类型 ==============
|
||||
@ -194,11 +196,17 @@ export interface PlanSummaryUpdateEvent {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** 单个问题项 */
|
||||
export interface QuestionItem {
|
||||
question: string;
|
||||
options: string[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
/** ask_user 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
questions: QuestionItem[];
|
||||
}
|
||||
|
||||
/** complete 事件数据 */
|
||||
@ -351,10 +359,12 @@ export interface AnswerRequest {
|
||||
askId: string;
|
||||
/** 任务ID */
|
||||
taskId: string;
|
||||
/** 选中的选项列表 */
|
||||
/** 选中的选项列表(旧格式,兼容) */
|
||||
selected?: string[];
|
||||
/** 自定义输入内容 */
|
||||
/** 自定义输入内容(旧格式,兼容) */
|
||||
customInput?: string;
|
||||
/** 新格式:按问题索引的答案 */
|
||||
answers?: { [questionIndex: string]: string[] };
|
||||
}
|
||||
|
||||
/** 用户回答响应 */
|
||||
|
||||
@ -92,7 +92,9 @@ export async function handleUserMessage(
|
||||
);
|
||||
|
||||
if (action === '立即登录') {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 恢复输入状态
|
||||
@ -126,7 +128,9 @@ export async function handleUserMessage(
|
||||
);
|
||||
|
||||
if (action === '立即登录') {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 恢复输入状态
|
||||
@ -320,7 +324,7 @@ async function handleUserMessageWithBackend(
|
||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onQuestion: (askId, question, options) => {
|
||||
onQuestion: (askId: string, questions: import("../types/api").QuestionItem[]) => {
|
||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
@ -365,13 +369,12 @@ async function handleUserMessageWithBackend(
|
||||
command: "hideStatus",
|
||||
});
|
||||
|
||||
// 最后一次发送完整的段落
|
||||
const result = await panel.webview.postMessage({
|
||||
// 发送完成标记(不再重复发送 segments,避免内容重复显示)
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
console.log("[MessageHandler] postMessage 返回值:", result);
|
||||
|
||||
// 发送系统通知 - AI 响应完成
|
||||
const notificationService = NotificationService.getInstance();
|
||||
@ -466,10 +469,11 @@ async function handleUserMessageWithBackend(
|
||||
export async function handleUserAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
customInput?: string,
|
||||
answers?: { [questionIndex: string]: string[] }
|
||||
): Promise<void> {
|
||||
if (currentSession) {
|
||||
await currentSession.submitAnswer(askId, selected, customInput);
|
||||
await currentSession.submitAnswer(askId, selected, customInput, answers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
147
src/utils/personalRulesManager.ts
Normal file
147
src/utils/personalRulesManager.ts
Normal file
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 个人规则管理工具
|
||||
* 功能:读写个人规则文件
|
||||
* 依赖:vscode, fs, path
|
||||
* 使用场景:保存和加载用户的个人规则
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
/**
|
||||
* 获取规则目录路径
|
||||
*/
|
||||
function getRulesDir(): string {
|
||||
return path.join(os.homedir(), '.iccoder', 'rules');
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保规则目录存在
|
||||
*/
|
||||
function ensureRulesDir(): void {
|
||||
const dir = getRulesDir();
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件内容中提取规则名称
|
||||
*/
|
||||
function extractRuleName(content: string): string {
|
||||
const lines = content.split('\n');
|
||||
const firstLine = lines[0]?.trim();
|
||||
if (firstLine && firstLine.startsWith('# ')) {
|
||||
return firstLine.substring(2).trim();
|
||||
}
|
||||
return content.substring(0, 30) + (content.length > 30 ? '...' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新规则
|
||||
*/
|
||||
export async function savePersonalRule(name: string, content: string, enabled: boolean): Promise<boolean> {
|
||||
try {
|
||||
ensureRulesDir();
|
||||
|
||||
const timestamp = Date.now();
|
||||
const filename = `rule-${timestamp}.md`;
|
||||
const filePath = path.join(getRulesDir(), filename);
|
||||
|
||||
const fileContent = `# ${name}\n\n${content}`;
|
||||
fs.writeFileSync(filePath, fileContent, 'utf-8');
|
||||
|
||||
await vscode.workspace.getConfiguration('ic-coder').update('personalRulesEnabled', enabled, vscode.ConfigurationTarget.Global);
|
||||
|
||||
vscode.window.showInformationMessage('规则已保存');
|
||||
return true;
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`保存规则失败: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新规则
|
||||
*/
|
||||
export async function updatePersonalRule(filename: string, name: string, content: string, enabled: boolean): Promise<boolean> {
|
||||
try {
|
||||
const filePath = path.join(getRulesDir(), filename);
|
||||
const fileContent = `# ${name}\n\n${content}`;
|
||||
fs.writeFileSync(filePath, fileContent, 'utf-8');
|
||||
|
||||
await vscode.workspace.getConfiguration('ic-coder').update('personalRulesEnabled', enabled, vscode.ConfigurationTarget.Global);
|
||||
|
||||
vscode.window.showInformationMessage('规则已更新');
|
||||
return true;
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`更新规则失败: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除规则
|
||||
*/
|
||||
export async function deletePersonalRule(filename: string): Promise<boolean> {
|
||||
try {
|
||||
const filePath = path.join(getRulesDir(), filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
vscode.window.showInformationMessage('规则已删除');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`删除规则失败: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载所有规则
|
||||
*/
|
||||
export function loadPersonalRules(): { rules: Array<{ filename: string; name: string; content: string }>; enabled: boolean } {
|
||||
const enabled = vscode.workspace.getConfiguration('ic-coder').get<boolean>('personalRulesEnabled', true);
|
||||
const dir = getRulesDir();
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
return { rules: [], enabled };
|
||||
}
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
||||
const rules = files.map(filename => {
|
||||
const content = fs.readFileSync(path.join(dir, filename), 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
let name = '';
|
||||
let actualContent = content;
|
||||
|
||||
if (lines[0]?.trim().startsWith('# ')) {
|
||||
name = lines[0].substring(2).trim();
|
||||
actualContent = lines.slice(2).join('\n').trim();
|
||||
} else {
|
||||
name = extractRuleName(content);
|
||||
}
|
||||
|
||||
return { filename, name, content: actualContent };
|
||||
});
|
||||
return { rules, enabled };
|
||||
} catch (error) {
|
||||
console.error('读取规则失败:', error);
|
||||
return { rules: [], enabled };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前生效的所有规则内容
|
||||
*/
|
||||
export function getActiveRules(): string | null {
|
||||
const { rules, enabled } = loadPersonalRules();
|
||||
if (!enabled || rules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return rules.map(r => r.content).join('\n\n');
|
||||
}
|
||||
@ -92,7 +92,8 @@ export async function executeWaveformTrace(
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
// 成功时返回 stdout,忽略 stderr 中的进度信息
|
||||
resolve(stdout || stderr);
|
||||
} else {
|
||||
reject(new Error(
|
||||
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`
|
||||
|
||||
@ -28,7 +28,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||
vscode.Uri.joinPath(context.extensionUri, "dist", "assets")
|
||||
],
|
||||
}
|
||||
);
|
||||
@ -47,21 +47,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Auto.png")
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "lite.png")
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Sy.png")
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Max.png")
|
||||
);
|
||||
|
||||
// 获取二维码图片URI
|
||||
const qrCodeUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "QRCode", "wx.png")
|
||||
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "QRCode", "wx.png")
|
||||
);
|
||||
|
||||
// 获取Logo URI
|
||||
@ -129,7 +129,8 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput
|
||||
message.customInput,
|
||||
message.answers
|
||||
);
|
||||
break;
|
||||
// 新增:中止对话
|
||||
@ -256,7 +257,7 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
} else if (message.command === "logout") {
|
||||
// 退出登录(前端已有确认对话框)
|
||||
vscode.commands.executeCommand('iccoder.logout');
|
||||
vscode.commands.executeCommand("ic-coder.logout");
|
||||
} else if (message.command === "openICCoder") {
|
||||
// 打开 IC Coder 官网
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
||||
@ -320,15 +321,15 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
width: 200px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
.btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
background: #005a9e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -16,7 +16,7 @@ export function getContextButtonContent(): string {
|
||||
<span class="add-context-label">添加上下文</span>
|
||||
|
||||
</button>
|
||||
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
||||
<span class="tooltiptext">添加文件、文件夹作为上下文</span>
|
||||
</div>
|
||||
|
||||
<!-- 上拉菜单 -->
|
||||
@ -271,6 +271,7 @@ export function getContextButtonStyles(): string {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.context-menu-list-item label {
|
||||
@ -335,6 +336,7 @@ export function getContextButtonScript(): string {
|
||||
return `
|
||||
// 上下文菜单状态
|
||||
let currentListData = [];
|
||||
let filteredListData = [];
|
||||
let currentListType = '';
|
||||
let selectedItems = new Set();
|
||||
|
||||
@ -392,6 +394,15 @@ export function getContextButtonScript(): string {
|
||||
|
||||
selectedItems.clear();
|
||||
currentListData = [];
|
||||
filteredListData = [];
|
||||
clearContextSearchInput();
|
||||
}
|
||||
|
||||
function clearContextSearchInput() {
|
||||
const searchInput = document.getElementById('contextMenuSearch');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到列表视图
|
||||
@ -406,10 +417,12 @@ export function getContextButtonScript(): string {
|
||||
titleEl.textContent = title;
|
||||
|
||||
currentListType = type;
|
||||
currentListData = data;
|
||||
currentListData = data || [];
|
||||
filteredListData = currentListData;
|
||||
selectedItems.clear();
|
||||
|
||||
renderList(data);
|
||||
clearContextSearchInput();
|
||||
renderList(filteredListData);
|
||||
updateSelectedCount();
|
||||
}
|
||||
}
|
||||
@ -419,32 +432,36 @@ export function getContextButtonScript(): string {
|
||||
const body = document.getElementById('contextMenuListBody');
|
||||
if (!body) return;
|
||||
|
||||
body.innerHTML = data.map((item, index) => \`
|
||||
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
|
||||
<input type="checkbox" id="item-\${index}" />
|
||||
<label for="item-\${index}">\${item.relativePath}</label>
|
||||
filteredListData = data || [];
|
||||
|
||||
body.innerHTML = filteredListData.map((item, index) => \`
|
||||
<div class="context-menu-list-item \${selectedItems.has(item.path) ? 'selected' : ''}" onclick="toggleItemSelection(\${index})">
|
||||
<input type="checkbox" id="item-\${index}" \${selectedItems.has(item.path) ? 'checked' : ''} />
|
||||
<label>\${item.relativePath || item.path}</label>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 切换项选择
|
||||
function toggleItemSelection(index) {
|
||||
const selectedItem = filteredListData[index];
|
||||
if (!selectedItem) return;
|
||||
|
||||
const selectedPath = selectedItem.path;
|
||||
const checkbox = document.getElementById('item-' + index);
|
||||
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
||||
|
||||
if (checkbox && item) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
|
||||
if (checkbox.checked) {
|
||||
selectedItems.add(index);
|
||||
item.classList.add('selected');
|
||||
} else {
|
||||
selectedItems.delete(index);
|
||||
item.classList.remove('selected');
|
||||
}
|
||||
|
||||
updateSelectedCount();
|
||||
if (selectedItems.has(selectedPath)) {
|
||||
selectedItems.delete(selectedPath);
|
||||
if (checkbox) checkbox.checked = false;
|
||||
if (item) item.classList.remove('selected');
|
||||
} else {
|
||||
selectedItems.add(selectedPath);
|
||||
if (checkbox) checkbox.checked = true;
|
||||
if (item) item.classList.add('selected');
|
||||
}
|
||||
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
// 更新选中数量
|
||||
@ -457,15 +474,25 @@ export function getContextButtonScript(): string {
|
||||
|
||||
// 确认选择
|
||||
function confirmSelection() {
|
||||
const selected = Array.from(selectedItems).map(index => currentListData[index]);
|
||||
try {
|
||||
const selected = currentListData.filter(item => selectedItems.has(item.path));
|
||||
|
||||
if (selected.length > 0) {
|
||||
selected.forEach(item => {
|
||||
addContextItem(currentListType, item.path);
|
||||
});
|
||||
if (selected.length > 0) {
|
||||
selected.forEach(item => {
|
||||
addContextItem(currentListType, item.path, item.relativePath || item.path);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const button = document.querySelector('.add-context-button');
|
||||
if (menu) {
|
||||
menu.classList.remove('show');
|
||||
}
|
||||
if (button) {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
backToMainMenu();
|
||||
}
|
||||
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 添加图片
|
||||
@ -484,9 +511,9 @@ export function getContextButtonScript(): string {
|
||||
const searchInput = document.getElementById('contextMenuSearch');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const keyword = e.target.value.toLowerCase();
|
||||
const keyword = (e.target.value || '').toLowerCase().trim();
|
||||
const filtered = currentListData.filter(item =>
|
||||
item.relativePath.toLowerCase().includes(keyword)
|
||||
(item.relativePath || item.path || '').toLowerCase().includes(keyword)
|
||||
);
|
||||
renderList(filtered);
|
||||
});
|
||||
|
||||
@ -51,7 +51,11 @@ export function getContextDisplayStyles(): string {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.context-item:hover {
|
||||
.context-item.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.context-item.clickable:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
@ -126,6 +130,11 @@ export function getContextDisplayScript(): string {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取代码图标 SVG
|
||||
function getCodeIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z m-484.7-122.1l39.6-39.5 113.1 113.1-39.6 39.5-113.1-113.1z m226.4-290.2l113.1 113.1-39.6 39.5-113.1-113.1 39.6-39.5z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取删除图标 SVG
|
||||
function getRemoveIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
|
||||
@ -137,9 +146,12 @@ export function getContextDisplayScript(): string {
|
||||
}
|
||||
|
||||
// 添加上下文项
|
||||
function addContextItem(type, path) {
|
||||
function addContextItem(type, path, displayPath) {
|
||||
const exists = contextItems.some(item => item.type === type && item.path === path);
|
||||
if (exists) return;
|
||||
|
||||
const id = Date.now() + Math.random();
|
||||
contextItems.push({ id, type, path });
|
||||
contextItems.push({ id, type, path, displayPath: displayPath || '' });
|
||||
renderContextItems();
|
||||
}
|
||||
|
||||
@ -169,13 +181,17 @@ export function getContextDisplayScript(): string {
|
||||
case 'folder': icon = getFolderIcon(); break;
|
||||
case 'image': icon = getImageIcon(); break;
|
||||
case 'document': icon = getDocumentIcon(); break;
|
||||
case 'code': icon = getCodeIcon(); break;
|
||||
}
|
||||
|
||||
const clickable = item.type !== 'folder' ? 'clickable' : '';
|
||||
const onclick = item.type !== 'folder' ? \`onclick="window.handleContextItemClick(\${item.id})"\` : '';
|
||||
|
||||
return \`
|
||||
<div class="context-item" title="\${item.path}">
|
||||
<div class="context-item \${clickable}" title="\${item.path || item.displayPath}" \${onclick}>
|
||||
\${icon}
|
||||
<span class="context-item-name">\${getFileName(item.path)}</span>
|
||||
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
||||
<span class="context-item-name">\${item.displayPath || getFileName(item.path)}</span>
|
||||
<span class="context-item-remove" onclick="event.stopPropagation(); removeContextItem(\${item.id})">
|
||||
\${getRemoveIcon()}
|
||||
</span>
|
||||
</div>
|
||||
@ -183,6 +199,27 @@ export function getContextDisplayScript(): string {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 全局访问函数
|
||||
window.handleContextItemClick = function(id) {
|
||||
const item = contextItems.find(i => i.id === id);
|
||||
if (!item || item.type === 'folder') return;
|
||||
|
||||
if (item.type === 'code') {
|
||||
const codeData = JSON.parse(item.path);
|
||||
vscode.postMessage({
|
||||
command: 'openFileWithSelection',
|
||||
filePath: codeData.fileName,
|
||||
startLine: codeData.startLine,
|
||||
endLine: codeData.endLine
|
||||
});
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
command: 'openFile',
|
||||
filePath: item.path
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理后端返回的文件选择结果
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
@ -208,6 +245,18 @@ export function getContextDisplayScript(): string {
|
||||
message.documents.forEach(doc => addContextItem('document', doc));
|
||||
}
|
||||
break;
|
||||
case 'addCodeContext':
|
||||
// 添加代码上下文
|
||||
const displayName = \`\${message.fileName.split(/[\\\\/]/).pop()}:\${message.startLine}-\${message.endLine}\`;
|
||||
const codeData = {
|
||||
fileName: message.fileName,
|
||||
startLine: message.startLine,
|
||||
endLine: message.endLine,
|
||||
code: message.code,
|
||||
languageId: message.languageId
|
||||
};
|
||||
addContextItem('code', JSON.stringify(codeData), displayName);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -330,7 +330,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
background: #007ACC;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
73
src/views/filePathTag.ts
Normal file
73
src/views/filePathTag.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 文件路径标签组件
|
||||
* 功能:显示可点击的文件路径标签
|
||||
* 使用场景:在用户消息中显示上下文文件
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取文件路径标签的样式
|
||||
*/
|
||||
export function getFilePathTagStyles(): string {
|
||||
return `
|
||||
/* 文件路径标签 */
|
||||
.file-path-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-right: 6px;
|
||||
background: rgba(0, 122, 204, 0.15);
|
||||
border: 1px solid rgba(0, 122, 204, 0.3);
|
||||
border-radius: 4px;
|
||||
color: #4fc3f7;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.file-path-tag:hover {
|
||||
background: rgba(0, 122, 204, 0.25);
|
||||
border-color: rgba(0, 122, 204, 0.5);
|
||||
}
|
||||
|
||||
.file-path-tag svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件路径标签的脚本
|
||||
*/
|
||||
export function getFilePathTagScript(): string {
|
||||
return `
|
||||
// 处理文件路径标签点击
|
||||
function handleFilePathClick(filePath) {
|
||||
// 解析文件路径,支持 file.v:5-8 格式
|
||||
const match = filePath.match(/^(.+?):(\\d+)-(\\d+)$/);
|
||||
if (match) {
|
||||
vscode.postMessage({
|
||||
command: 'openFilePathTag',
|
||||
filePath: match[1],
|
||||
startLine: parseInt(match[2]),
|
||||
endLine: parseInt(match[3])
|
||||
});
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
command: 'openFilePathTag',
|
||||
filePath: filePath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建文件路径标签
|
||||
window.createFilePathTag = function(filePath) {
|
||||
const fileIcon = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
|
||||
const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
|
||||
return '<span class="file-path-tag" onclick="handleFilePathClick(\\'' + escapedPath + '\\')">' + fileIcon + filePath + '</span>';
|
||||
};
|
||||
`;
|
||||
}
|
||||
@ -24,6 +24,10 @@ import {
|
||||
getContextCompressStyles,
|
||||
getContextCompressScript,
|
||||
} from "./contextCompress";
|
||||
import {
|
||||
getFilePathTagStyles,
|
||||
getFilePathTagScript,
|
||||
} from "./filePathTag";
|
||||
import {
|
||||
getOptimizeButtonContent,
|
||||
getOptimizeButtonStyles,
|
||||
@ -98,6 +102,7 @@ export function getInputAreaStyles(): string {
|
||||
${getModelSelectorStyles()}
|
||||
${getContextButtonStyles()}
|
||||
${getContextDisplayStyles()}
|
||||
${getFilePathTagStyles()}
|
||||
${getContextCompressStyles()}
|
||||
${getOptimizeButtonStyles()}
|
||||
${getExampleShowcaseStyles()}
|
||||
@ -309,6 +314,7 @@ export function getInputAreaScript(): string {
|
||||
${getContextCompressScript()}
|
||||
${getOptimizeButtonScript()}
|
||||
${getChangePanelScript()}
|
||||
${getFilePathTagScript()}
|
||||
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
@ -426,7 +432,21 @@ export function getInputAreaScript(): string {
|
||||
// 获取上下文项
|
||||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||
|
||||
addMessage(text, 'user');
|
||||
// 构建显示消息:如果有上下文项,添加路径前缀
|
||||
let displayText = text;
|
||||
if (contextItems.length > 0) {
|
||||
const contextPaths = contextItems
|
||||
.map(item => item.displayPath || item.path)
|
||||
.join(' ');
|
||||
if (contextPaths) {
|
||||
displayText = contextPaths + ' ' + text;
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(displayText, 'user');
|
||||
|
||||
// 重置分段消息容器,强制下次创建新容器
|
||||
currentSegmentedMessage = null;
|
||||
|
||||
// 标记已有消息,切换布局到底部
|
||||
hasMessages = true;
|
||||
|
||||
@ -248,19 +248,21 @@ export function getMessageAreaStyles(): string {
|
||||
}
|
||||
.question-option {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border: 1px solid #007ACC;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.question-option:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.question-option.selected {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.question-message.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
@ -420,6 +422,9 @@ export function getMessageAreaStyles(): string {
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.icon-expanded svg path {
|
||||
fill: #007ACC !important;
|
||||
}
|
||||
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
@ -546,7 +551,7 @@ export function getMessageAreaStyles(): string {
|
||||
.tool-segment-description {
|
||||
margin: 6px 0 0 0px;
|
||||
font-size: 0.9rem;
|
||||
color: #ccc;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.4;
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
@ -585,20 +590,22 @@ export function getMessageAreaStyles(): string {
|
||||
}
|
||||
.segment-question .question-option {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border: 1px solid #007ACC;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
}
|
||||
.segment-question .question-option:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.segment-question .question-option.selected {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.segment-question.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
@ -850,7 +857,26 @@ export function getMessageAreaScript(): string {
|
||||
|
||||
div.appendChild(actionsDiv);
|
||||
} else {
|
||||
div.textContent = text;
|
||||
// 用户消息:解析文件路径并转换为标签
|
||||
const parts = text.split(' ');
|
||||
const filePaths = [];
|
||||
const textParts = [];
|
||||
|
||||
parts.forEach(part => {
|
||||
// 判断是否为文件路径或代码片段:包含路径分隔符、文件扩展名或代码片段格式(文件名:行号-行号)
|
||||
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) {
|
||||
filePaths.push(part);
|
||||
} else {
|
||||
textParts.push(part);
|
||||
}
|
||||
});
|
||||
|
||||
if (filePaths.length > 0) {
|
||||
div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
|
||||
} else {
|
||||
div.textContent = text;
|
||||
}
|
||||
|
||||
// 当添加用户消息时,隐藏 header
|
||||
hideHeaderIfNeeded();
|
||||
}
|
||||
@ -988,34 +1014,42 @@ export function getMessageAreaScript(): string {
|
||||
|
||||
// 实时更新分段消息(按后端返回顺序)
|
||||
function updateSegmentsRealtime(segments, isComplete) {
|
||||
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
||||
// 如果对话完成且没有新段落,只重置容器
|
||||
if (isComplete && (!segments || segments.length === 0)) {
|
||||
currentSegmentedMessage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!segments || segments.length === 0) {
|
||||
console.log('[WebView] segments 为空,跳过渲染');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有当前分段消息容器,创建一个
|
||||
if (!currentSegmentedMessage) {
|
||||
console.log('[WebView] 创建新的分段消息容器');
|
||||
// 移除流式消息(如果有)
|
||||
if (currentStreamingMessage) {
|
||||
console.log('[WebView] 移除流式消息');
|
||||
currentStreamingMessage.remove();
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
|
||||
// 移除所有工具状态消息(因为会在分段中显示)
|
||||
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||||
console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
|
||||
toolStatuses.forEach(el => {
|
||||
console.log('[WebView] 移除工具状态消息:', el.className);
|
||||
el.remove();
|
||||
});
|
||||
|
||||
currentSegmentedMessage = document.createElement('div');
|
||||
currentSegmentedMessage.className = 'message bot-message segmented-message';
|
||||
messagesEl.appendChild(currentSegmentedMessage);
|
||||
// 检查最后一个容器是否是未完成的对话(没有操作按钮)
|
||||
const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
|
||||
if (lastSegmented && !lastSegmented.querySelector('.message-actions')) {
|
||||
// 复用未完成的容器
|
||||
currentSegmentedMessage = lastSegmented;
|
||||
} else {
|
||||
// 创建新容器
|
||||
currentSegmentedMessage = document.createElement('div');
|
||||
currentSegmentedMessage.className = 'message bot-message segmented-message';
|
||||
messagesEl.appendChild(currentSegmentedMessage);
|
||||
}
|
||||
renderedSegmentCount = 0;
|
||||
}
|
||||
|
||||
// 保存当前所有工具的展开/折叠状态
|
||||
@ -1154,64 +1188,60 @@ export function getMessageAreaScript(): string {
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.className += ' segment-question';
|
||||
|
||||
// 兼容旧格式:如果有 segment.question,转换为 questions 数组
|
||||
const questions = segment.questions || (segment.question ? [{
|
||||
question: segment.question,
|
||||
options: segment.options || [],
|
||||
multiSelect: false
|
||||
}] : []);
|
||||
|
||||
// 检查是否已回答
|
||||
const isAnswered = answeredQuestions.has(segment.askId);
|
||||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||||
const savedAnswers = answeredQuestions.get(segment.askId) || {};
|
||||
|
||||
if (isAnswered) {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
// 检查是否有选项
|
||||
const hasOptions = segment.options && segment.options.length > 0;
|
||||
// 渲染多个问题
|
||||
const questionsHtml = questions.map((q, qIndex) => {
|
||||
const inputType = q.multiSelect ? 'checkbox' : 'radio';
|
||||
const inputName = \`q\${qIndex}\`;
|
||||
const selectedAnswers = savedAnswers[qIndex] || [];
|
||||
|
||||
const optionsHtml = hasOptions
|
||||
? (segment.options || []).map(opt => {
|
||||
const isSelected = isAnswered && opt === selectedAnswer;
|
||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||
}).join('')
|
||||
: '';
|
||||
const optionsHtml = q.options.map(opt => {
|
||||
const isSelected = selectedAnswers.includes(opt);
|
||||
return \`<label class="question-option\${isSelected ? ' selected' : ''}" style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 0;">
|
||||
<input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
|
||||
<span>\${opt}</span>
|
||||
</label>\`;
|
||||
}).join('');
|
||||
|
||||
return \`
|
||||
<div class="question-item" data-question-index="\${qIndex}" style="margin-bottom:12px;">
|
||||
<div class="question-text" style="margin-bottom:8px;">\${formatText(q.question)}</div>
|
||||
<div class="question-options">\${optionsHtml}</div>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||||
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
|
||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
|
||||
<button class="custom-submit">提交</button>
|
||||
</div>
|
||||
\${questionsHtml}
|
||||
<button class="custom-submit" style="display:\${isAnswered ? 'none' : 'block'};margin-top:8px;padding:8px 16px;background:var(--vscode-button-background);color:var(--vscode-button-foreground);border:none;border-radius:6px;cursor:pointer;">提交答案</button>
|
||||
\`;
|
||||
|
||||
// 只在未回答时添加事件监听
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
if (hasOptions) {
|
||||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||||
optionButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const option = this.getAttribute('data-option');
|
||||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
const customInput = segmentDiv.querySelector('.custom-input');
|
||||
if (submitBtn && customInput) {
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
}
|
||||
});
|
||||
|
||||
// 支持回车提交
|
||||
customInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
}
|
||||
}
|
||||
const answers = {};
|
||||
questions.forEach((q, qIndex) => {
|
||||
const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
|
||||
answers[qIndex] = Array.from(inputs).map(input => input.value);
|
||||
});
|
||||
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
@ -1227,7 +1257,7 @@ export function getMessageAreaScript(): string {
|
||||
currentSegmentedMessage.appendChild(segmentDiv);
|
||||
});
|
||||
|
||||
// 如果对话完成,添加操作按钮
|
||||
// 如果对话完成,添加操作按钮并重置容器
|
||||
if (isComplete) {
|
||||
console.log('[WebView] 对话完成,添加操作按钮');
|
||||
const actionsDiv = document.createElement('div');
|
||||
@ -1262,7 +1292,7 @@ export function getMessageAreaScript(): string {
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
currentSegmentedMessage.appendChild(actionsDiv);
|
||||
|
||||
// 重置当前分段消息容器
|
||||
// 重置当前分段消息容器(继续对话时创建新容器)
|
||||
currentSegmentedMessage = null;
|
||||
}
|
||||
|
||||
@ -1689,6 +1719,43 @@ export function getMessageAreaScript(): string {
|
||||
});
|
||||
}
|
||||
|
||||
// 处理多问题答案提交
|
||||
function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
|
||||
console.log('[WebView] 多问题答案提交:', askId, answers);
|
||||
|
||||
// 保存答案到 Map 中
|
||||
answeredQuestions.set(askId, answers);
|
||||
|
||||
// 标记问题已回答
|
||||
segmentDiv.classList.add('answered');
|
||||
|
||||
// 禁用所有输入并保持选中状态的高亮
|
||||
const inputs = segmentDiv.querySelectorAll('input');
|
||||
inputs.forEach(input => {
|
||||
input.disabled = true;
|
||||
// 确保选中的选项保持高亮
|
||||
if (input.checked) {
|
||||
const label = input.closest('.question-option');
|
||||
if (label) {
|
||||
label.classList.add('selected');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 隐藏提交按钮
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
if (submitBtn) {
|
||||
submitBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// 发送答案到后端
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
answers: answers
|
||||
});
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
|
||||
${getCodeHighlightScript()}
|
||||
|
||||
@ -18,7 +18,7 @@ export function getNdtWelcomeModalContent(logoUri?: string): string {
|
||||
|
||||
<div class="ndt-welcome-modal-header">
|
||||
<div class="ndt-welcome-icon">🎉</div>
|
||||
<h2>欢迎宁德时代新能源科技股份有限公司<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder!</h2>
|
||||
<h2>欢迎企业<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder!</h2>
|
||||
</div>
|
||||
|
||||
<div class="ndt-welcome-modal-body">
|
||||
|
||||
@ -195,11 +195,11 @@ export function getPlanCardStyles(): string {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.plan-btn-confirm {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
}
|
||||
.plan-btn-confirm:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
background: #005a9e;
|
||||
}
|
||||
.plan-btn-cancel {
|
||||
background: transparent;
|
||||
|
||||
@ -186,8 +186,8 @@ export function getProgressBarStyles(): string {
|
||||
|
||||
/* 已完成状态 */
|
||||
.progress-step.completed .step-circle {
|
||||
background: var(--vscode-button-background);
|
||||
border-color: var(--vscode-button-background);
|
||||
background: #007ACC;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-number {
|
||||
@ -204,14 +204,14 @@ export function getProgressBarStyles(): string {
|
||||
}
|
||||
|
||||
.progress-step.completed + .progress-line {
|
||||
background: var(--vscode-button-background);
|
||||
background: #007ACC;
|
||||
}
|
||||
|
||||
/* 进行中状态 */
|
||||
.progress-step.active .step-circle {
|
||||
background: var(--vscode-button-background);
|
||||
border-color: var(--vscode-button-background);
|
||||
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
||||
background: #007ACC;
|
||||
border-color: #007ACC;
|
||||
box-shadow: 0 0 0 2px #007ACC33;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@ -226,10 +226,10 @@ export function getProgressBarStyles(): string {
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
||||
box-shadow: 0 0 0 2px #007ACC33;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 4px var(--vscode-button-background)1a;
|
||||
box-shadow: 0 0 0 4px #007ACC1a;
|
||||
}
|
||||
}
|
||||
|
||||
@ -351,7 +351,7 @@ export function getProgressBarScript(): string {
|
||||
// 更新连接线
|
||||
document.querySelectorAll('.progress-line').forEach((line, index) => {
|
||||
if (index < currentIndex) {
|
||||
line.style.background = 'var(--vscode-button-background)';
|
||||
line.style.background = '#007ACC';
|
||||
} else {
|
||||
line.style.background = 'var(--vscode-input-border)';
|
||||
}
|
||||
|
||||
@ -1,77 +1,67 @@
|
||||
import { peopleRules } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取规则设置组件的 HTML 内容
|
||||
*/
|
||||
export function getRulesSettingsComponentContent(): string {
|
||||
return `
|
||||
<div class="rules-settings">
|
||||
<h3 class="settings-section-title">规则设置</h3>
|
||||
<div class="rules-header">
|
||||
<h3 class="settings-section-title">个人规则</h3>
|
||||
<button class="add-rule-button" onclick="showAddRuleModal()">+ 创建</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-header">
|
||||
<label class="settings-item-label">启用自定义规则</label>
|
||||
<span class="settings-item-description">使用自定义规则来控制 AI 行为</span>
|
||||
<label class="settings-item-label">启用个人规则</label>
|
||||
<span class="settings-item-description">规则将在每次对话时自动应用</span>
|
||||
</div>
|
||||
<label class="settings-switch">
|
||||
<input type="checkbox" id="enableCustomRulesCheckbox" checked>
|
||||
<input type="checkbox" id="enablePersonalRulesCheckbox" checked>
|
||||
<span class="settings-switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">系统规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<div class="rules-list" id="rulesList">
|
||||
<!-- 规则列表将动态插入这里 -->
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑规则弹窗 -->
|
||||
<div class="rule-modal" id="ruleModal" style="display: none;">
|
||||
<div class="rule-modal-content">
|
||||
<h4 id="modalTitle">创建个人规则</h4>
|
||||
<input
|
||||
type="text"
|
||||
class="rule-name-input"
|
||||
id="ruleNameInput"
|
||||
placeholder="规则名称"
|
||||
/>
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="systemRulesTextarea"
|
||||
placeholder="在此输入系统规则,例如: - 始终使用中文回复 - 代码注释要详细 - 遵循项目编码规范"
|
||||
rows="8"
|
||||
class="rule-textarea"
|
||||
id="ruleTextarea"
|
||||
placeholder="输入规则内容..."
|
||||
rows="10"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
系统规则会在每次对话开始时应用
|
||||
<div class="rule-modal-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="saveRule()">保存</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="closeRuleModal()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">代码生成规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="codeRulesTextarea"
|
||||
placeholder="在此输入代码生成规则,例如: - 使用 TypeScript 严格模式 - 函数命名使用驼峰命名法 - 添加必要的错误处理"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
这些规则会在生成代码时应用
|
||||
<!-- 删除确认弹窗 -->
|
||||
<div class="rule-modal" id="deleteConfirmModal" style="display: none;">
|
||||
<div class="rule-modal-content" style="width: 400px;">
|
||||
<h4>确认删除</h4>
|
||||
<p id="deleteConfirmText" style="color: var(--vscode-foreground); margin: 16px 0;"></p>
|
||||
<div class="rule-modal-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="confirmDelete()">确定</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="closeDeleteConfirmModal()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-subsection-title">Verilog 规则</h4>
|
||||
<div class="rules-textarea-container">
|
||||
<textarea
|
||||
class="rules-textarea"
|
||||
id="verilogRulesTextarea"
|
||||
placeholder="在此输入 Verilog 代码规则,例如: - 使用非阻塞赋值 (<=) 在时序逻辑中 - 模块命名使用小写加下划线 - 添加详细的端口注释"
|
||||
rows="8"
|
||||
></textarea>
|
||||
<div class="rules-textarea-hint">
|
||||
这些规则会在生成 Verilog 代码时应用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="settings-button settings-button-primary" onclick="saveRulesSettings()">
|
||||
保存规则
|
||||
</button>
|
||||
<button class="settings-button settings-button-secondary" onclick="resetRulesSettings()">
|
||||
重置为默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -85,11 +75,144 @@ export function getRulesSettingsComponentStyles(): string {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.rules-textarea-container {
|
||||
margin-top: 8px;
|
||||
.rules-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rules-textarea {
|
||||
.add-rule-button {
|
||||
padding: 6px 12px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.add-rule-button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.rules-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rule-item-name {
|
||||
color: var(--vscode-foreground);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rule-item > div:first-child svg {
|
||||
background-color: rgba(148, 204, 241, 0.3);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.rule-item-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rule-menu-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.rule-menu-icon:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.rule-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 28px;
|
||||
background: var(--vscode-menu-background);
|
||||
border: 1px solid var(--vscode-menu-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.rule-dropdown button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-menu-foreground);
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rule-dropdown button:hover {
|
||||
background: var(--vscode-menu-selectionBackground);
|
||||
color: var(--vscode-menu-selectionForeground);
|
||||
}
|
||||
|
||||
.rule-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.rule-modal-content {
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.rule-modal-content h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.rule-name-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rule-name-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.rule-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--vscode-input-background);
|
||||
@ -98,26 +221,20 @@ export function getRulesSettingsComponentStyles(): string {
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rules-textarea:focus {
|
||||
.rule-textarea:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.rules-textarea::placeholder {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.rules-textarea-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
.rule-modal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -127,51 +244,163 @@ export function getRulesSettingsComponentStyles(): string {
|
||||
*/
|
||||
export function getRulesSettingsComponentScript(): string {
|
||||
return `
|
||||
// 保存规则设置
|
||||
function saveRulesSettings() {
|
||||
const settings = {
|
||||
enableCustomRules: document.getElementById('enableCustomRulesCheckbox').checked,
|
||||
systemRules: document.getElementById('systemRulesTextarea').value,
|
||||
codeRules: document.getElementById('codeRulesTextarea').value,
|
||||
verilogRules: document.getElementById('verilogRulesTextarea').value,
|
||||
};
|
||||
let currentRules = [];
|
||||
let editingRule = null;
|
||||
let deletingFilename = null;
|
||||
|
||||
// 发送消息到扩展
|
||||
vscode.postMessage({
|
||||
command: 'saveRulesSettings',
|
||||
settings: settings
|
||||
});
|
||||
|
||||
// 显示保存成功提示
|
||||
console.log('规则设置已保存', settings);
|
||||
// 显示添加规则弹窗
|
||||
function showAddRuleModal() {
|
||||
editingRule = null;
|
||||
document.getElementById('modalTitle').textContent = '创建个人规则';
|
||||
document.getElementById('ruleNameInput').value = '';
|
||||
document.getElementById('ruleTextarea').value = '';
|
||||
document.getElementById('ruleModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 重置规则设置
|
||||
function resetRulesSettings() {
|
||||
document.getElementById('enableCustomRulesCheckbox').checked = true;
|
||||
document.getElementById('systemRulesTextarea').value = '';
|
||||
document.getElementById('codeRulesTextarea').value = '';
|
||||
document.getElementById('verilogRulesTextarea').value = '';
|
||||
|
||||
console.log('规则设置已重置为默认值');
|
||||
// 关闭弹窗
|
||||
function closeRuleModal() {
|
||||
document.getElementById('ruleModal').style.display = 'none';
|
||||
closeAllDropdowns();
|
||||
}
|
||||
|
||||
// 加载规则设置
|
||||
function loadRulesSettings(settings) {
|
||||
if (!settings) return;
|
||||
|
||||
if (settings.enableCustomRules !== undefined) {
|
||||
document.getElementById('enableCustomRulesCheckbox').checked = settings.enableCustomRules;
|
||||
}
|
||||
if (settings.systemRules) {
|
||||
document.getElementById('systemRulesTextarea').value = settings.systemRules;
|
||||
}
|
||||
if (settings.codeRules) {
|
||||
document.getElementById('codeRulesTextarea').value = settings.codeRules;
|
||||
}
|
||||
if (settings.verilogRules) {
|
||||
document.getElementById('verilogRulesTextarea').value = settings.verilogRules;
|
||||
// 切换下拉菜单
|
||||
function toggleDropdown(filename, event) {
|
||||
event.stopPropagation();
|
||||
closeAllDropdowns();
|
||||
const dropdown = document.getElementById('dropdown-' + filename);
|
||||
if (dropdown) {
|
||||
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭所有下拉菜单
|
||||
function closeAllDropdowns() {
|
||||
document.querySelectorAll('.rule-dropdown').forEach(d => d.style.display = 'none');
|
||||
}
|
||||
|
||||
// 点击页面其他地方关闭下拉菜单
|
||||
document.addEventListener('click', closeAllDropdowns);
|
||||
|
||||
// 编辑规则
|
||||
function editRule(filename) {
|
||||
const rule = currentRules.find(r => r.filename === filename);
|
||||
if (rule) {
|
||||
editingRule = rule;
|
||||
document.getElementById('modalTitle').textContent = '修改个人规则';
|
||||
document.getElementById('ruleNameInput').value = rule.name;
|
||||
document.getElementById('ruleTextarea').value = rule.content;
|
||||
document.getElementById('ruleModal').style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存规则
|
||||
function saveRule() {
|
||||
const name = document.getElementById('ruleNameInput').value.trim();
|
||||
const content = document.getElementById('ruleTextarea').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('规则名称不能为空');
|
||||
return;
|
||||
}
|
||||
if (!content) {
|
||||
alert('规则内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = document.getElementById('enablePersonalRulesCheckbox').checked;
|
||||
|
||||
if (editingRule) {
|
||||
vscode.postMessage({
|
||||
command: 'updatePersonalRule',
|
||||
filename: editingRule.filename,
|
||||
name: name,
|
||||
content: content,
|
||||
enabled: enabled
|
||||
});
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
command: 'savePersonalRule',
|
||||
name: name,
|
||||
content: content,
|
||||
enabled: enabled
|
||||
});
|
||||
}
|
||||
|
||||
closeRuleModal();
|
||||
}
|
||||
|
||||
// 删除规则
|
||||
function deleteRule(filename) {
|
||||
closeAllDropdowns();
|
||||
const rule = currentRules.find(r => r.filename === filename);
|
||||
const ruleName = rule ? rule.name : filename;
|
||||
|
||||
deletingFilename = filename;
|
||||
document.getElementById('deleteConfirmText').textContent = '确定要删除规则"' + ruleName + '"吗?此操作无法撤销。';
|
||||
document.getElementById('deleteConfirmModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 关闭删除确认弹窗
|
||||
function closeDeleteConfirmModal() {
|
||||
document.getElementById('deleteConfirmModal').style.display = 'none';
|
||||
deletingFilename = null;
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
function confirmDelete() {
|
||||
if (deletingFilename) {
|
||||
vscode.postMessage({
|
||||
command: 'deletePersonalRule',
|
||||
filename: deletingFilename
|
||||
});
|
||||
}
|
||||
closeDeleteConfirmModal();
|
||||
}
|
||||
|
||||
// 渲染规则列表
|
||||
function renderRulesList(rules) {
|
||||
currentRules = rules || [];
|
||||
const listEl = document.getElementById('rulesList');
|
||||
|
||||
if (currentRules.length === 0) {
|
||||
listEl.innerHTML = '<div style="color: var(--vscode-descriptionForeground); padding: 16px; text-align: center;">暂无规则,点击"+ 创建"添加</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const peopleRulesIcon = '${peopleRules}';
|
||||
|
||||
listEl.innerHTML = currentRules.map(rule => \`
|
||||
<div class="rule-item">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
\${peopleRulesIcon}
|
||||
<div class="rule-item-name">\${rule.filename}</div>
|
||||
</div>
|
||||
<div class="rule-item-menu">
|
||||
<svg class="rule-menu-icon" onclick="toggleDropdown('\${rule.filename}', event)" viewBox="0 0 16 16" fill="currentColor">
|
||||
<circle cx="8" cy="3" r="1.5"/>
|
||||
<circle cx="8" cy="8" r="1.5"/>
|
||||
<circle cx="8" cy="13" r="1.5"/>
|
||||
</svg>
|
||||
<div class="rule-dropdown" id="dropdown-\${rule.filename}" style="display: none;">
|
||||
<button onclick="editRule('\${rule.filename}')">编辑</button>
|
||||
<button onclick="deleteRule('\${rule.filename}')">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 加载规则列表
|
||||
function loadPersonalRules(data) {
|
||||
if (data && data.enabled !== undefined) {
|
||||
document.getElementById('enablePersonalRulesCheckbox').checked = data.enabled;
|
||||
}
|
||||
if (data && data.rules) {
|
||||
renderRulesList(data.rules);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时请求规则数据
|
||||
vscode.postMessage({ command: 'loadPersonalRules' });
|
||||
`;
|
||||
}
|
||||
|
||||
@ -715,6 +715,13 @@ export function getWebviewContent(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'personalRulesLoaded':
|
||||
// 加载个人规则数据
|
||||
if (typeof loadPersonalRules === 'function') {
|
||||
loadPersonalRules(message.data);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'autoSendMessage':
|
||||
// 自动发送待发送的消息(登录后)
|
||||
console.log('[WebView] 自动发送待发送消息:', message.text);
|
||||
|
||||
@ -19,40 +19,41 @@ export function getWelcomeModalContent(logoUri?: string): string {
|
||||
<div class="welcome-modal-header">
|
||||
<div class="welcome-icon">🎉</div>
|
||||
<h2>欢迎使用 IC Coder!</h2>
|
||||
<p class="welcome-modal-subtitle">您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!</p>
|
||||
</div>
|
||||
|
||||
<div class="welcome-modal-body">
|
||||
<div class="welcome-step">
|
||||
<div class="welcome-step-icon">📝</div>
|
||||
<div class="welcome-step-content">
|
||||
<h3>步骤 1:打开聊天面板</h3>
|
||||
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
|
||||
<!-- 试用期提示 -->
|
||||
<div class="trial-banner">
|
||||
<span>您已获得 <strong>5 天企业版试用期</strong>,企业版试用期内Credits用量无限,并可无限制使用所有功能</span>
|
||||
</div>
|
||||
|
||||
<!-- IC Coder 简介 -->
|
||||
<div class="intro-section">
|
||||
<h3 class="section-title">关于 IC Coder</h3>
|
||||
<p class="intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform(自主式人工智能 Verilog 编码平台)。我们采用全球顶尖的IC Coder自研芯片设计微调模型,为代码生成提供强大的AI能力支撑。</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-item">
|
||||
<span class="feature-text">多智能体架构(Multi-Agent System):多个专业化AI智能体协同工作,分别负责架构设计、代码生成、验证测试等不同环节</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-text">AI自主仿真:IC Coder提供完全自动化的仿真验证流程,无需手动编写测试代码</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-step">
|
||||
<div class="welcome-step-icon">💬</div>
|
||||
<div class="welcome-step-content">
|
||||
<h3>步骤 2:输入您的需求</h3>
|
||||
<p>描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案</p>
|
||||
</div>
|
||||
<!-- 按钮组 -->
|
||||
<div class="button-group">
|
||||
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
|
||||
<span>开始使用</span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="welcome-step">
|
||||
<div class="welcome-step-icon">🔬</div>
|
||||
<div class="welcome-step-content">
|
||||
<h3>步骤 3:运行仿真</h3>
|
||||
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
|
||||
<span>开始使用</span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -96,7 +97,7 @@ export function getWelcomeModalStyles(): string {
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-width: 480px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
overflow: hidden;
|
||||
@ -124,9 +125,10 @@ export function getWelcomeModalStyles(): string {
|
||||
|
||||
.welcome-modal-header h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.welcome-modal-subtitle {
|
||||
@ -140,37 +142,70 @@ export function getWelcomeModalStyles(): string {
|
||||
padding: 0 32px 32px;
|
||||
}
|
||||
|
||||
.welcome-step {
|
||||
/* 试用期横幅 */
|
||||
.trial-banner {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 20px 0;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 16px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.welcome-step-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.welcome-step-content h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.welcome-step-content p {
|
||||
margin: 0;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.5;
|
||||
border-left: 3px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.trial-banner strong {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* IC Coder 简介区域 */
|
||||
.intro-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
border-left: 2px solid var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.button-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.welcome-btn {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@ -181,18 +216,32 @@ export function getWelcomeModalStyles(): string {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
transition: all 0.2s;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.welcome-btn:hover {
|
||||
.welcome-btn-primary {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.welcome-btn-primary:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.welcome-btn-secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
}
|
||||
|
||||
.welcome-btn-secondary:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.welcome-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
BIN
tools/waveform_trace/bin/waveform_trace.exe
Normal file
BIN
tools/waveform_trace/bin/waveform_trace.exe
Normal file
Binary file not shown.
@ -3,30 +3,30 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
//@ts-check
|
||||
/** @typedef {import('webpack').Configuration} WebpackConfig **/
|
||||
|
||||
/** @type WebpackConfig */
|
||||
const extensionConfig = {
|
||||
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
|
||||
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
|
||||
target: 'node',
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
|
||||
|
||||
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
|
||||
entry: './src/extension.ts',
|
||||
output: {
|
||||
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2'
|
||||
libraryTarget: 'commonjs2',
|
||||
clean: true // 自动清理旧文件
|
||||
},
|
||||
externals: {
|
||||
vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
|
||||
'node-notifier': 'commonjs node-notifier' // node-notifier 依赖原生模块,必须排除
|
||||
// modules added here also need to be added in the .vscodeignore file
|
||||
vscode: 'commonjs vscode',
|
||||
'node-notifier': 'commonjs node-notifier'
|
||||
},
|
||||
resolve: {
|
||||
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
|
||||
extensions: ['.ts', '.js']
|
||||
extensions: ['.ts', '.js'],
|
||||
mainFields: ['module', 'main']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@ -35,15 +35,37 @@ const extensionConfig = {
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader'
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true, // 加快编译速度
|
||||
compilerOptions: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
devtool: 'nosources-source-map',
|
||||
devtool: process.env.NODE_ENV === 'production' ? 'hidden-source-map' : 'nosources-source-map',
|
||||
infrastructureLogging: {
|
||||
level: "log", // enables logging required for problem matchers
|
||||
level: "log",
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: 'src/assets', to: 'assets' }
|
||||
]
|
||||
})
|
||||
],
|
||||
optimization: {
|
||||
minimize: process.env.NODE_ENV === 'production',
|
||||
usedExports: true // Tree Shaking
|
||||
},
|
||||
performance: {
|
||||
hints: 'warning',
|
||||
maxAssetSize: 2 * 1024 * 1024, // 2MB
|
||||
maxEntrypointSize: 2 * 1024 * 1024
|
||||
}
|
||||
};
|
||||
module.exports = [ extensionConfig ];
|
||||
Reference in New Issue
Block a user