Compare commits
11 Commits
feat/front
...
f56ad33366
| Author | SHA1 | Date | |
|---|---|---|---|
| f56ad33366 | |||
| 35c63802b5 | |||
| 3458f6fe23 | |||
| 8f305033f7 | |||
| 4a18f1c418 | |||
| 373edb6d80 | |||
| 1c66e0e599 | |||
| 536e7720cb | |||
| 75eac4b1ce | |||
| 9ed0afee6b | |||
| f700473967 |
17
CHANGELOG.md
17
CHANGELOG.md
@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
所有重要的项目变更都将记录在此文件中。
|
所有重要的项目变更都将记录在此文件中。
|
||||||
|
|
||||||
|
## [1.0.7] - 2026-03-02
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 修复 AI 响应内容重复显示问题
|
||||||
|
|
||||||
|
## [1.0.6] - 2026-03-02
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- Git Diff 功能:支持查看当前文件的 Git 差异对比
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 修复添加上下文搜索选择文件不匹配的问题
|
||||||
|
- 修复过期认证状态未清除导致重新登录失败的问题
|
||||||
|
|
||||||
## [1.0.4] - 2026-01-28
|
## [1.0.4] - 2026-01-28
|
||||||
|
|
||||||
IC Coder插件端正式上线。
|
IC Coder插件端正式上线。
|
||||||
|
|||||||
19
PUBLISH.md
19
PUBLISH.md
@ -88,6 +88,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
|||||||
5. 点击 **Create** 完成创建
|
5. 点击 **Create** 完成创建
|
||||||
|
|
||||||
**注意事项:**
|
**注意事项:**
|
||||||
|
|
||||||
- Publisher ID 一旦创建无法修改
|
- Publisher ID 一旦创建无法修改
|
||||||
- Publisher ID 必须全局唯一
|
- Publisher ID 必须全局唯一
|
||||||
- 建议使用有意义且专业的 ID
|
- 建议使用有意义且专业的 ID
|
||||||
@ -126,6 +127,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
|||||||
## [0.0.2] - 2025-12-29
|
## [0.0.2] - 2025-12-29
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|
||||||
- 添加发送和暂停按钮功能
|
- 添加发送和暂停按钮功能
|
||||||
- 添加一键优化按钮组件
|
- 添加一键优化按钮组件
|
||||||
- 添加 Plan 开关组件
|
- 添加 Plan 开关组件
|
||||||
@ -133,11 +135,13 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
|
|||||||
- 添加上下文压缩功能
|
- 添加上下文压缩功能
|
||||||
|
|
||||||
### 改进
|
### 改进
|
||||||
|
|
||||||
- 优化用户界面交互体验
|
- 优化用户界面交互体验
|
||||||
|
|
||||||
## [0.0.1] - 2025-12-XX
|
## [0.0.1] - 2025-12-XX
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|
||||||
- 初始版本发布
|
- 初始版本发布
|
||||||
- Verilog 代码智能生成
|
- Verilog 代码智能生成
|
||||||
- 集成 iverilog 仿真工具
|
- 集成 iverilog 仿真工具
|
||||||
@ -161,6 +165,7 @@ in the Software without restriction...
|
|||||||
### 4. 优化 README.md
|
### 4. 优化 README.md
|
||||||
|
|
||||||
确保 README 包含:
|
确保 README 包含:
|
||||||
|
|
||||||
- 清晰的功能介绍
|
- 清晰的功能介绍
|
||||||
- 使用截图或 GIF 演示
|
- 使用截图或 GIF 演示
|
||||||
- 详细的使用说明
|
- 详细的使用说明
|
||||||
@ -219,6 +224,7 @@ pnpm vsce publish
|
|||||||
**步骤:**
|
**步骤:**
|
||||||
|
|
||||||
1. 本地打包插件:
|
1. 本地打包插件:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run package
|
pnpm run package
|
||||||
pnpm vsce package[pnpm vsce package --no-dependencies]
|
pnpm vsce package[pnpm vsce package --no-dependencies]
|
||||||
@ -257,7 +263,7 @@ pnpm vsce publish major
|
|||||||
|
|
||||||
```bash
|
```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. 执行发布命令
|
4. 执行发布命令
|
||||||
5. 验证市场上的插件是否正常
|
5. 验证市场上的插件是否正常
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 更新流程
|
## 更新流程
|
||||||
|
|
||||||
1. 修改版本号
|
1. 修改版本号
|
||||||
@ -302,10 +306,7 @@ pnpm vsce publish 0.0.3
|
|||||||
pnpm vsce package --no-dependencies
|
pnpm vsce package --no-dependencies
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
3. 手动上传/命令上传
|
3. 手动上传/命令上传
|
||||||
|
|
||||||
- https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update
|
- https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update
|
||||||
- 命令上传:vsce publish
|
- 命令上传:vsce publish
|
||||||
|
|
||||||
@ -318,6 +319,7 @@ pnpm vsce publish 0.0.3
|
|||||||
**原因:** PAT Token 无效或过期
|
**原因:** PAT Token 无效或过期
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
- 重新生成 PAT Token
|
- 重新生成 PAT Token
|
||||||
- 重新登录:`pnpm vsce login ic-coder-team`
|
- 重新登录:`pnpm vsce login ic-coder-team`
|
||||||
|
|
||||||
@ -326,6 +328,7 @@ pnpm vsce publish 0.0.3
|
|||||||
**原因:** Publisher ID 不存在或不匹配
|
**原因:** Publisher ID 不存在或不匹配
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
- 检查 `package.json` 中的 `publisher` 字段
|
- 检查 `package.json` 中的 `publisher` 字段
|
||||||
- 确认已在市场创建对应的 Publisher
|
- 确认已在市场创建对应的 Publisher
|
||||||
|
|
||||||
@ -334,17 +337,20 @@ pnpm vsce publish 0.0.3
|
|||||||
**原因:** 必需文件缺失
|
**原因:** 必需文件缺失
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
- 确保 `dist/` 目录存在且包含编译后的代码
|
- 确保 `dist/` 目录存在且包含编译后的代码
|
||||||
- 运行 `pnpm run package` 重新构建
|
- 运行 `pnpm run package` 重新构建
|
||||||
|
|
||||||
### 4. 插件审核被拒
|
### 4. 插件审核被拒
|
||||||
|
|
||||||
**常见原因:**
|
**常见原因:**
|
||||||
|
|
||||||
- 插件名称或描述违反市场规则
|
- 插件名称或描述违反市场规则
|
||||||
- 图标不符合要求(建议 128x128 PNG)
|
- 图标不符合要求(建议 128x128 PNG)
|
||||||
- README 内容不完整
|
- README 内容不完整
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
- 查看审核反馈邮件
|
- 查看审核反馈邮件
|
||||||
- 修改相关内容后重新发布
|
- 修改相关内容后重新发布
|
||||||
|
|
||||||
@ -366,6 +372,7 @@ code --install-extension ic-coder-plugin-0.0.2.vsix
|
|||||||
```
|
```
|
||||||
|
|
||||||
或者在 VS Code 中:
|
或者在 VS Code 中:
|
||||||
|
|
||||||
1. 打开扩展面板
|
1. 打开扩展面板
|
||||||
2. 点击 `...` 菜单
|
2. 点击 `...` 菜单
|
||||||
3. 选择 **Install from VSIX...**
|
3. 选择 **Install from VSIX...**
|
||||||
|
|||||||
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. 考虑添加用户配置选项
|
||||||
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`),多端一致。
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "iccoder",
|
"name": "iccoder",
|
||||||
"displayName": "IC Coder: Agentic Verilog Platform",
|
"displayName": "IC Coder: Agentic Verilog Platform",
|
||||||
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
||||||
"version": "1.0.5",
|
"version": "1.0.7",
|
||||||
"publisher": "ICCoderAgenticVerilogPlatform",
|
"publisher": "ICCoderAgenticVerilogPlatform",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.80.0"
|
"vscode": "^1.80.0"
|
||||||
|
|||||||
@ -159,20 +159,32 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
// 注册命令:用户登录
|
// 注册命令:用户登录
|
||||||
const loginCommand = vscode.commands.registerCommand(
|
const loginCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.login",
|
"ic-coder.login",
|
||||||
async () => {
|
async (options?: { forceReauth?: boolean }) => {
|
||||||
try {
|
try {
|
||||||
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
|
const forceReauth = options?.forceReauth === true;
|
||||||
try {
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
await vscode.authentication.getSession("iccoder", [], {
|
createIfNone: false,
|
||||||
clearSessionPreference: true,
|
|
||||||
createIfNone: false
|
|
||||||
});
|
});
|
||||||
} catch {
|
const expired = session?.accessToken
|
||||||
// 忽略错误
|
? isTokenExpired(session.accessToken)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 会话仍有效时,直接打开聊天面板
|
||||||
|
if (session && expired === false && !forceReauth) {
|
||||||
|
vscode.commands.executeCommand("ic-coder.openChat");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新 session
|
// 1) 清空当前登录状态信息
|
||||||
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
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) {
|
} catch (error) {
|
||||||
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,7 +91,9 @@ export async function showICHelperPanel(
|
|||||||
"立即登录",
|
"立即登录",
|
||||||
);
|
);
|
||||||
if (action === "立即登录") {
|
if (action === "立即登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -106,7 +108,9 @@ export async function showICHelperPanel(
|
|||||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
if (selection === "立即登录") {
|
if (selection === "立即登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -116,7 +120,9 @@ export async function showICHelperPanel(
|
|||||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
if (selection === "立即登录") {
|
if (selection === "立即登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -752,6 +758,23 @@ export async function showICHelperPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
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":
|
case "checkWorkspace":
|
||||||
const hasWorkspace = !!(
|
const hasWorkspace = !!(
|
||||||
|
|||||||
@ -449,7 +449,9 @@ export class DialogSession {
|
|||||||
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
if (selection === "重新登录") {
|
if (selection === "重新登录") {
|
||||||
vscode.commands.executeCommand("iccoder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
throw new Error("登录已过期,请重新登录");
|
throw new Error("登录已过期,请重新登录");
|
||||||
@ -894,7 +896,9 @@ export class DialogSession {
|
|||||||
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
||||||
.then((selection) => {
|
.then((selection) => {
|
||||||
if (selection === "重新登录") {
|
if (selection === "重新登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 登录过期错误已处理,不再传递给外部
|
// 登录过期错误已处理,不再传递给外部
|
||||||
|
|||||||
@ -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
|
* 生成会话 ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2,23 +2,31 @@
|
|||||||
* 工具执行器
|
* 工具执行器
|
||||||
* 接收后端的 tool_call 事件,执行本地工具,返回结果
|
* 接收后端的 tool_call 事件,执行本地工具,返回结果
|
||||||
*/
|
*/
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from "vscode";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
import * as os from 'os';
|
import * as os from "os";
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
import { readFileContent, readDirectory } from "../utils/readFiles";
|
||||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
import { createOrOverwriteFile } from "../utils/createFiles";
|
||||||
import { resolveWorkspaceFilePath, showFileDiff } from '../utils/fileDiff';
|
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
|
||||||
import { changeTracker } from './changeTracker';
|
import { changeTracker } from "./changeTracker";
|
||||||
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
import {
|
||||||
import { analyzeVcdFile } from '../utils/vcdParser';
|
generateVCD,
|
||||||
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
checkIverilogAvailable,
|
||||||
|
generateMultiVCD,
|
||||||
|
DumpModule,
|
||||||
|
} from "../utils/iverilogRunner";
|
||||||
|
import { analyzeVcdFile } from "../utils/vcdParser";
|
||||||
|
import {
|
||||||
|
executeWaveformTrace,
|
||||||
|
WaveformTraceArgs,
|
||||||
|
} from "../utils/waveformTracer";
|
||||||
import {
|
import {
|
||||||
submitToolResult,
|
submitToolResult,
|
||||||
createSuccessResult,
|
createSuccessResult,
|
||||||
createBusinessErrorResult,
|
createBusinessErrorResult,
|
||||||
createSystemErrorResult
|
createSystemErrorResult,
|
||||||
} from './apiClient';
|
} from "./apiClient";
|
||||||
import type {
|
import type {
|
||||||
ToolCallRequest,
|
ToolCallRequest,
|
||||||
ToolName,
|
ToolName,
|
||||||
@ -31,8 +39,8 @@ import type {
|
|||||||
SimulationArgs,
|
SimulationArgs,
|
||||||
WaveformSummaryArgs,
|
WaveformSummaryArgs,
|
||||||
KnowledgeSaveArgs,
|
KnowledgeSaveArgs,
|
||||||
KnowledgeLoadArgs
|
KnowledgeLoadArgs,
|
||||||
} from '../types/api';
|
} from "../types/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具执行器上下文
|
* 工具执行器上下文
|
||||||
@ -51,7 +59,7 @@ export interface ToolExecutorContext {
|
|||||||
*/
|
*/
|
||||||
export async function executeToolCall(
|
export async function executeToolCall(
|
||||||
request: ToolCallRequest,
|
request: ToolCallRequest,
|
||||||
context: ToolExecutorContext
|
context: ToolExecutorContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const toolName = request.params.name as ToolName;
|
const toolName = request.params.name as ToolName;
|
||||||
const args = request.params.arguments;
|
const args = request.params.arguments;
|
||||||
@ -63,37 +71,53 @@ export async function executeToolCall(
|
|||||||
let resultText: string;
|
let resultText: string;
|
||||||
|
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case 'file_read':
|
case "file_read":
|
||||||
resultText = await executeFileRead(args as unknown as FileReadArgs);
|
resultText = await executeFileRead(args as unknown as FileReadArgs);
|
||||||
break;
|
break;
|
||||||
case 'file_write':
|
case "file_write":
|
||||||
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
||||||
break;
|
break;
|
||||||
case 'file_delete':
|
case "file_delete":
|
||||||
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
|
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
|
||||||
break;
|
break;
|
||||||
case 'file_list':
|
case "file_list":
|
||||||
resultText = await executeFileList(args as unknown as FileListArgs);
|
resultText = await executeFileList(args as unknown as FileListArgs);
|
||||||
break;
|
break;
|
||||||
case 'syntax_check':
|
case "syntax_check":
|
||||||
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
|
resultText = await executeSyntaxCheck(
|
||||||
|
args as unknown as SyntaxCheckArgs,
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'iverilog':
|
case "iverilog":
|
||||||
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
|
resultText = await executeIverilog(
|
||||||
|
args as unknown as IverilogArgs,
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'simulation':
|
case "simulation":
|
||||||
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
resultText = await executeSimulation(
|
||||||
|
args as unknown as SimulationArgs,
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'waveform_summary':
|
case "waveform_summary":
|
||||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
resultText = await executeWaveformSummary(
|
||||||
|
args as unknown as WaveformSummaryArgs,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'waveform_trace':
|
case "waveform_trace":
|
||||||
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
|
resultText = await executeWaveformTrace(
|
||||||
|
args as unknown as WaveformTraceArgs,
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'knowledge_save':
|
case "knowledge_save":
|
||||||
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
resultText = await executeKnowledgeSave(
|
||||||
|
args as unknown as KnowledgeSaveArgs,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'knowledge_load':
|
case "knowledge_load":
|
||||||
resultText = await executeKnowledgeLoad();
|
resultText = await executeKnowledgeLoad();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -104,10 +128,12 @@ export async function executeToolCall(
|
|||||||
const result = createSuccessResult(callId, resultText);
|
const result = createSuccessResult(callId, resultText);
|
||||||
await submitToolResult(result);
|
await submitToolResult(result);
|
||||||
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
|
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||||
console.error(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error);
|
console.error(
|
||||||
|
`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
// 提交错误结果
|
// 提交错误结果
|
||||||
const result = createBusinessErrorResult(callId, errorMessage);
|
const result = createBusinessErrorResult(callId, errorMessage);
|
||||||
@ -129,7 +155,7 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
|||||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||||
const absolutePath = resolveWorkspaceFilePath(args.path);
|
const absolutePath = resolveWorkspaceFilePath(args.path);
|
||||||
const existedBeforeWrite = fs.existsSync(absolutePath);
|
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);
|
await createOrOverwriteFile(args.path, args.content);
|
||||||
|
|
||||||
@ -137,11 +163,11 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
|||||||
try {
|
try {
|
||||||
changeTracker.trackChange(args.path, oldContent, args.content);
|
changeTracker.trackChange(args.path, oldContent, args.content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[ToolExecutor] 记录文件变更失败:', error);
|
console.warn("[ToolExecutor] 记录文件变更失败:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verilog 文件添加知识图谱提示
|
// Verilog 文件添加知识图谱提示
|
||||||
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
const isVerilogFile = args.path.endsWith(".v") || args.path.endsWith(".sv");
|
||||||
if (isVerilogFile) {
|
if (isVerilogFile) {
|
||||||
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
|
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
|
||||||
}
|
}
|
||||||
@ -151,7 +177,7 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 file_delete 工具
|
* 执行 file_delete 工具
|
||||||
* 删除指定路径的文件
|
* 删除指定路径的文件(带用户确认)
|
||||||
*/
|
*/
|
||||||
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||||
const filePath = args.path;
|
const filePath = args.path;
|
||||||
@ -159,7 +185,7 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
|||||||
// 获取工作区路径
|
// 获取工作区路径
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
@ -180,18 +206,60 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
|||||||
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
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);
|
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 文件添加知识图谱提示
|
// Verilog 文件添加知识图谱提示
|
||||||
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
|
const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv");
|
||||||
if (isVerilogFile) {
|
if (isVerilogFile) {
|
||||||
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
||||||
}
|
}
|
||||||
@ -203,13 +271,13 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
|||||||
* 执行 file_list 工具
|
* 执行 file_list 工具
|
||||||
*/
|
*/
|
||||||
async function executeFileList(args: FileListArgs): Promise<string> {
|
async function executeFileList(args: FileListArgs): Promise<string> {
|
||||||
const dirPath = args.path || '.';
|
const dirPath = args.path || ".";
|
||||||
const extensions = args.extension ? [args.extension] : undefined;
|
const extensions = args.extension ? [args.extension] : undefined;
|
||||||
|
|
||||||
const files = await readDirectory(dirPath, extensions);
|
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(
|
async function executeSyntaxCheck(
|
||||||
args: SyntaxCheckArgs,
|
args: SyntaxCheckArgs,
|
||||||
context: ToolExecutorContext
|
context: ToolExecutorContext,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// 检查 iverilog 是否可用
|
// 检查 iverilog 是否可用
|
||||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||||
@ -232,33 +300,33 @@ async function executeSyntaxCheck(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 写入代码到临时文件
|
// 写入代码到临时文件
|
||||||
fs.writeFileSync(tempFile, args.code, 'utf-8');
|
fs.writeFileSync(tempFile, args.code, "utf-8");
|
||||||
|
|
||||||
// 调用 iverilog 进行语法检查
|
// 调用 iverilog 进行语法检查
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require("child_process");
|
||||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(iverilogPath, ['-t', 'null', tempFile], {
|
const child = spawn(iverilogPath, ["-t", "null", tempFile], {
|
||||||
cwd: tempDir,
|
cwd: tempDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = "";
|
||||||
let stderr = '';
|
let stderr = "";
|
||||||
|
|
||||||
child.stdout.on('data', (data: Buffer) => {
|
child.stdout.on("data", (data: Buffer) => {
|
||||||
stdout += data.toString();
|
stdout += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on('data', (data: Buffer) => {
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code: number) => {
|
child.on("close", (code: number) => {
|
||||||
// 清理临时文件
|
// 清理临时文件
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(tempFile);
|
fs.unlinkSync(tempFile);
|
||||||
@ -267,13 +335,13 @@ async function executeSyntaxCheck(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve('语法检查通过,无错误。');
|
resolve("语法检查通过,无错误。");
|
||||||
} else {
|
} else {
|
||||||
resolve(`语法检查发现错误:\n${stderr || stdout}`);
|
resolve(`语法检查发现错误:\n${stderr || stdout}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (error: Error) => {
|
child.on("error", (error: Error) => {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(tempFile);
|
fs.unlinkSync(tempFile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -282,7 +350,6 @@ async function executeSyntaxCheck(
|
|||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 确保清理临时文件
|
// 确保清理临时文件
|
||||||
try {
|
try {
|
||||||
@ -300,7 +367,7 @@ async function executeSyntaxCheck(
|
|||||||
*/
|
*/
|
||||||
async function executeIverilog(
|
async function executeIverilog(
|
||||||
args: IverilogArgs,
|
args: IverilogArgs,
|
||||||
context: ToolExecutorContext
|
context: ToolExecutorContext,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// 检查 iverilog 是否可用
|
// 检查 iverilog 是否可用
|
||||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||||
@ -311,7 +378,7 @@ async function executeIverilog(
|
|||||||
// 获取工作目录
|
// 获取工作目录
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
throw new Error('没有打开的工作区');
|
throw new Error("没有打开的工作区");
|
||||||
}
|
}
|
||||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||||
const workDir = args.workDir
|
const workDir = args.workDir
|
||||||
@ -320,32 +387,32 @@ async function executeIverilog(
|
|||||||
|
|
||||||
// 解析参数
|
// 解析参数
|
||||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(iverilogPath, cmdArgs, {
|
const child = spawn(iverilogPath, cmdArgs, {
|
||||||
cwd: workDir,
|
cwd: workDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = "";
|
||||||
let stderr = '';
|
let stderr = "";
|
||||||
|
|
||||||
child.stdout.on('data', (data: Buffer) => {
|
child.stdout.on("data", (data: Buffer) => {
|
||||||
stdout += data.toString();
|
stdout += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on('data', (data: Buffer) => {
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code: number) => {
|
child.on("close", (code: number) => {
|
||||||
const output = stderr || stdout || '(无输出)';
|
const output = stderr || stdout || "(无输出)";
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve(`执行成功\n${output}`);
|
resolve(`执行成功\n${output}`);
|
||||||
} else {
|
} else {
|
||||||
@ -353,7 +420,7 @@ async function executeIverilog(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (error: Error) => {
|
child.on("error", (error: Error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -364,12 +431,12 @@ async function executeIverilog(
|
|||||||
*/
|
*/
|
||||||
async function executeSimulation(
|
async function executeSimulation(
|
||||||
args: SimulationArgs,
|
args: SimulationArgs,
|
||||||
context: ToolExecutorContext
|
context: ToolExecutorContext,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// 获取工作区路径
|
// 获取工作区路径
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||||
@ -377,21 +444,24 @@ async function executeSimulation(
|
|||||||
// 检查是否有 dumpModules 参数(多 VCD 模式)
|
// 检查是否有 dumpModules 参数(多 VCD 模式)
|
||||||
if (args.dumpModules) {
|
if (args.dumpModules) {
|
||||||
const modules = parseDumpModules(args.dumpModules);
|
const modules = parseDumpModules(args.dumpModules);
|
||||||
const vcdDir = args.vcdDir || 'vcd';
|
const vcdDir = args.vcdDir || "vcd";
|
||||||
|
|
||||||
const result = await generateMultiVCD(
|
const result = await generateMultiVCD(
|
||||||
projectPath,
|
projectPath,
|
||||||
context.extensionPath,
|
context.extensionPath,
|
||||||
args.tbPath,
|
args.tbPath,
|
||||||
modules,
|
modules,
|
||||||
vcdDir
|
vcdDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const vcdList = result.vcdFiles
|
const vcdList = result.vcdFiles
|
||||||
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`)
|
.map(
|
||||||
.join('\n');
|
(f) =>
|
||||||
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`;
|
`- ${f.moduleName}: ${f.success ? f.vcdPath : "失败 - " + f.error}`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? "\n\n仿真输出:" + result.stdout : ""}`;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message);
|
throw new Error(result.message);
|
||||||
}
|
}
|
||||||
@ -420,8 +490,8 @@ async function executeSimulation(
|
|||||||
* 格式:name:path,name:path
|
* 格式:name:path,name:path
|
||||||
*/
|
*/
|
||||||
function parseDumpModules(dumpModules: string): DumpModule[] {
|
function parseDumpModules(dumpModules: string): DumpModule[] {
|
||||||
return dumpModules.split(',').map(item => {
|
return dumpModules.split(",").map((item) => {
|
||||||
const [name, modulePath] = item.trim().split(':');
|
const [name, modulePath] = item.trim().split(":");
|
||||||
return { name: name.trim(), path: modulePath.trim() };
|
return { name: name.trim(), path: modulePath.trim() };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -430,13 +500,15 @@ function parseDumpModules(dumpModules: string): DumpModule[] {
|
|||||||
* 执行 waveform_summary 工具
|
* 执行 waveform_summary 工具
|
||||||
* 解析 VCD 文件并返回波形摘要
|
* 解析 VCD 文件并返回波形摘要
|
||||||
*/
|
*/
|
||||||
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
|
async function executeWaveformSummary(
|
||||||
|
args: WaveformSummaryArgs,
|
||||||
|
): Promise<string> {
|
||||||
const { vcdPath, signals, checkpoints } = args;
|
const { vcdPath, signals, checkpoints } = args;
|
||||||
|
|
||||||
// 获取工作区路径
|
// 获取工作区路径
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
@ -467,17 +539,20 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
|
|||||||
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||||
const workspaceFolder = getWorkspaceFolder();
|
const workspaceFolder = getWorkspaceFolder();
|
||||||
if (!workspaceFolder) {
|
if (!workspaceFolder) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error("请先打开一个工作区");
|
||||||
}
|
}
|
||||||
|
|
||||||
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
|
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, ".iccoder");
|
||||||
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
|
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, "knowledge.json");
|
||||||
|
|
||||||
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
|
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
|
||||||
await vscode.workspace.fs.createDirectory(iccoderDirUri);
|
await vscode.workspace.fs.createDirectory(iccoderDirUri);
|
||||||
|
|
||||||
// 写入知识图谱(UTF-8)
|
// 写入知识图谱(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`;
|
return `知识图谱已保存: .iccoder/knowledge.json`;
|
||||||
}
|
}
|
||||||
@ -489,20 +564,33 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
|||||||
async function executeKnowledgeLoad(): Promise<string> {
|
async function executeKnowledgeLoad(): Promise<string> {
|
||||||
const workspaceFolder = getWorkspaceFolder();
|
const workspaceFolder = getWorkspaceFolder();
|
||||||
if (!workspaceFolder) {
|
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 {
|
try {
|
||||||
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
|
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;
|
return content;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 文件不存在:返回空图谱
|
// 文件不存在:返回空图谱
|
||||||
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
if (
|
||||||
|
error instanceof vscode.FileSystemError &&
|
||||||
|
error.code === "FileNotFound"
|
||||||
|
) {
|
||||||
// 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段)
|
// 与后端 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;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -515,7 +603,9 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
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];
|
return activeFolder ?? folders[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,22 +614,24 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
|||||||
*/
|
*/
|
||||||
function getIverilogPath(extensionPath: string): string {
|
function getIverilogPath(extensionPath: string): string {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
if (platform === 'win32') {
|
if (platform === "win32") {
|
||||||
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe');
|
return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
|
||||||
} else {
|
} 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 workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || '';
|
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
extensionPath,
|
extensionPath,
|
||||||
workspacePath
|
workspacePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,9 @@ export async function handleUserMessage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (action === '立即登录') {
|
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 === '立即登录') {
|
if (action === '立即登录') {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login", {
|
||||||
|
forceReauth: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复输入状态
|
// 恢复输入状态
|
||||||
@ -365,13 +369,12 @@ async function handleUserMessageWithBackend(
|
|||||||
command: "hideStatus",
|
command: "hideStatus",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 最后一次发送完整的段落
|
// 发送完成标记(不再重复发送 segments,避免内容重复显示)
|
||||||
const result = await panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "updateSegments",
|
command: "updateSegments",
|
||||||
segments: segments,
|
segments: [],
|
||||||
isComplete: true,
|
isComplete: true,
|
||||||
});
|
});
|
||||||
console.log("[MessageHandler] postMessage 返回值:", result);
|
|
||||||
|
|
||||||
// 发送系统通知 - AI 响应完成
|
// 发送系统通知 - AI 响应完成
|
||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
|
|||||||
@ -256,7 +256,7 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login");
|
||||||
} else if (message.command === "logout") {
|
} else if (message.command === "logout") {
|
||||||
// 退出登录(前端已有确认对话框)
|
// 退出登录(前端已有确认对话框)
|
||||||
vscode.commands.executeCommand('iccoder.logout');
|
vscode.commands.executeCommand("ic-coder.logout");
|
||||||
} else if (message.command === "openICCoder") {
|
} else if (message.command === "openICCoder") {
|
||||||
// 打开 IC Coder 官网
|
// 打开 IC Coder 官网
|
||||||
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
|
||||||
|
|||||||
@ -271,6 +271,7 @@ export function getContextButtonStyles(): string {
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-list-item label {
|
.context-menu-list-item label {
|
||||||
@ -335,6 +336,7 @@ export function getContextButtonScript(): string {
|
|||||||
return `
|
return `
|
||||||
// 上下文菜单状态
|
// 上下文菜单状态
|
||||||
let currentListData = [];
|
let currentListData = [];
|
||||||
|
let filteredListData = [];
|
||||||
let currentListType = '';
|
let currentListType = '';
|
||||||
let selectedItems = new Set();
|
let selectedItems = new Set();
|
||||||
|
|
||||||
@ -392,6 +394,15 @@ export function getContextButtonScript(): string {
|
|||||||
|
|
||||||
selectedItems.clear();
|
selectedItems.clear();
|
||||||
currentListData = [];
|
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;
|
titleEl.textContent = title;
|
||||||
|
|
||||||
currentListType = type;
|
currentListType = type;
|
||||||
currentListData = data;
|
currentListData = data || [];
|
||||||
|
filteredListData = currentListData;
|
||||||
selectedItems.clear();
|
selectedItems.clear();
|
||||||
|
|
||||||
renderList(data);
|
clearContextSearchInput();
|
||||||
|
renderList(filteredListData);
|
||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,33 +432,37 @@ export function getContextButtonScript(): string {
|
|||||||
const body = document.getElementById('contextMenuListBody');
|
const body = document.getElementById('contextMenuListBody');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
body.innerHTML = data.map((item, index) => \`
|
filteredListData = data || [];
|
||||||
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
|
|
||||||
<input type="checkbox" id="item-\${index}" />
|
body.innerHTML = filteredListData.map((item, index) => \`
|
||||||
<label for="item-\${index}">\${item.relativePath}</label>
|
<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>
|
</div>
|
||||||
\`).join('');
|
\`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换项选择
|
// 切换项选择
|
||||||
function toggleItemSelection(index) {
|
function toggleItemSelection(index) {
|
||||||
|
const selectedItem = filteredListData[index];
|
||||||
|
if (!selectedItem) return;
|
||||||
|
|
||||||
|
const selectedPath = selectedItem.path;
|
||||||
const checkbox = document.getElementById('item-' + index);
|
const checkbox = document.getElementById('item-' + index);
|
||||||
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
||||||
|
|
||||||
if (checkbox && item) {
|
if (selectedItems.has(selectedPath)) {
|
||||||
checkbox.checked = !checkbox.checked;
|
selectedItems.delete(selectedPath);
|
||||||
|
if (checkbox) checkbox.checked = false;
|
||||||
if (checkbox.checked) {
|
if (item) item.classList.remove('selected');
|
||||||
selectedItems.add(index);
|
|
||||||
item.classList.add('selected');
|
|
||||||
} else {
|
} else {
|
||||||
selectedItems.delete(index);
|
selectedItems.add(selectedPath);
|
||||||
item.classList.remove('selected');
|
if (checkbox) checkbox.checked = true;
|
||||||
|
if (item) item.classList.add('selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 更新选中数量
|
// 更新选中数量
|
||||||
function updateSelectedCount() {
|
function updateSelectedCount() {
|
||||||
@ -457,15 +474,25 @@ export function getContextButtonScript(): string {
|
|||||||
|
|
||||||
// 确认选择
|
// 确认选择
|
||||||
function confirmSelection() {
|
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) {
|
if (selected.length > 0) {
|
||||||
selected.forEach(item => {
|
selected.forEach(item => {
|
||||||
addContextItem(currentListType, item.path);
|
addContextItem(currentListType, item.path, item.relativePath || item.path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
toggleContextMenu();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加图片
|
// 添加图片
|
||||||
@ -484,9 +511,9 @@ export function getContextButtonScript(): string {
|
|||||||
const searchInput = document.getElementById('contextMenuSearch');
|
const searchInput = document.getElementById('contextMenuSearch');
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.addEventListener('input', function(e) {
|
searchInput.addEventListener('input', function(e) {
|
||||||
const keyword = e.target.value.toLowerCase();
|
const keyword = (e.target.value || '').toLowerCase().trim();
|
||||||
const filtered = currentListData.filter(item =>
|
const filtered = currentListData.filter(item =>
|
||||||
item.relativePath.toLowerCase().includes(keyword)
|
(item.relativePath || item.path || '').toLowerCase().includes(keyword)
|
||||||
);
|
);
|
||||||
renderList(filtered);
|
renderList(filtered);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -137,9 +137,12 @@ export function getContextDisplayScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加上下文项
|
// 添加上下文项
|
||||||
function addContextItem(type, path) {
|
function addContextItem(type, path, displayPath) {
|
||||||
|
const exists = contextItems.some(item => item.type === type && item.path === path);
|
||||||
|
if (exists) return;
|
||||||
|
|
||||||
const id = Date.now() + Math.random();
|
const id = Date.now() + Math.random();
|
||||||
contextItems.push({ id, type, path });
|
contextItems.push({ id, type, path, displayPath: displayPath || '' });
|
||||||
renderContextItems();
|
renderContextItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +177,7 @@ export function getContextDisplayScript(): string {
|
|||||||
return \`
|
return \`
|
||||||
<div class="context-item" title="\${item.path}">
|
<div class="context-item" title="\${item.path}">
|
||||||
\${icon}
|
\${icon}
|
||||||
<span class="context-item-name">\${getFileName(item.path)}</span>
|
<span class="context-item-name">\${item.displayPath || getFileName(item.path)}</span>
|
||||||
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
||||||
\${getRemoveIcon()}
|
\${getRemoveIcon()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
62
src/views/filePathTag.ts
Normal file
62
src/views/filePathTag.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 文件路径标签组件
|
||||||
|
* 功能:显示可点击的文件路径标签
|
||||||
|
* 使用场景:在用户消息中显示上下文文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件路径标签的样式
|
||||||
|
*/
|
||||||
|
export function getFilePathTagStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 文件路径标签 */
|
||||||
|
.file-path-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-right: 6px;
|
||||||
|
background: rgba(0, 122, 204, 0.15);
|
||||||
|
border: 1px solid rgba(0, 122, 204, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path-tag:hover {
|
||||||
|
background: rgba(0, 122, 204, 0.25);
|
||||||
|
border-color: rgba(0, 122, 204, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path-tag svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件路径标签的脚本
|
||||||
|
*/
|
||||||
|
export function getFilePathTagScript(): string {
|
||||||
|
return `
|
||||||
|
// 处理文件路径标签点击
|
||||||
|
function handleFilePathClick(filePath) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openFile',
|
||||||
|
filePath: filePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文件路径标签
|
||||||
|
window.createFilePathTag = function(filePath) {
|
||||||
|
const fileIcon = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
|
||||||
|
const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
|
||||||
|
return '<span class="file-path-tag" onclick="handleFilePathClick(\\'' + escapedPath + '\\')">' + fileIcon + filePath + '</span>';
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -24,6 +24,10 @@ import {
|
|||||||
getContextCompressStyles,
|
getContextCompressStyles,
|
||||||
getContextCompressScript,
|
getContextCompressScript,
|
||||||
} from "./contextCompress";
|
} from "./contextCompress";
|
||||||
|
import {
|
||||||
|
getFilePathTagStyles,
|
||||||
|
getFilePathTagScript,
|
||||||
|
} from "./filePathTag";
|
||||||
import {
|
import {
|
||||||
getOptimizeButtonContent,
|
getOptimizeButtonContent,
|
||||||
getOptimizeButtonStyles,
|
getOptimizeButtonStyles,
|
||||||
@ -98,6 +102,7 @@ export function getInputAreaStyles(): string {
|
|||||||
${getModelSelectorStyles()}
|
${getModelSelectorStyles()}
|
||||||
${getContextButtonStyles()}
|
${getContextButtonStyles()}
|
||||||
${getContextDisplayStyles()}
|
${getContextDisplayStyles()}
|
||||||
|
${getFilePathTagStyles()}
|
||||||
${getContextCompressStyles()}
|
${getContextCompressStyles()}
|
||||||
${getOptimizeButtonStyles()}
|
${getOptimizeButtonStyles()}
|
||||||
${getExampleShowcaseStyles()}
|
${getExampleShowcaseStyles()}
|
||||||
@ -309,6 +314,7 @@ export function getInputAreaScript(): string {
|
|||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
${getOptimizeButtonScript()}
|
${getOptimizeButtonScript()}
|
||||||
${getChangePanelScript()}
|
${getChangePanelScript()}
|
||||||
|
${getFilePathTagScript()}
|
||||||
|
|
||||||
// 对话状态管理
|
// 对话状态管理
|
||||||
let isConversationActive = false;
|
let isConversationActive = false;
|
||||||
@ -426,7 +432,19 @@ export function getInputAreaScript(): string {
|
|||||||
// 获取上下文项
|
// 获取上下文项
|
||||||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||||
|
|
||||||
addMessage(text, 'user');
|
// 构建显示消息:如果有上下文文件,添加文件路径前缀
|
||||||
|
let displayText = text;
|
||||||
|
if (contextItems.length > 0) {
|
||||||
|
const filePaths = contextItems
|
||||||
|
.filter(item => item.type === 'file')
|
||||||
|
.map(item => item.displayPath || item.path)
|
||||||
|
.join(' ');
|
||||||
|
if (filePaths) {
|
||||||
|
displayText = filePaths + ' ' + text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(displayText, 'user');
|
||||||
|
|
||||||
// 标记已有消息,切换布局到底部
|
// 标记已有消息,切换布局到底部
|
||||||
hasMessages = true;
|
hasMessages = true;
|
||||||
|
|||||||
@ -849,8 +849,27 @@ export function getMessageAreaScript(): string {
|
|||||||
actionsDiv.appendChild(dislikeBtn);
|
actionsDiv.appendChild(dislikeBtn);
|
||||||
|
|
||||||
div.appendChild(actionsDiv);
|
div.appendChild(actionsDiv);
|
||||||
|
} else {
|
||||||
|
// 用户消息:解析文件路径并转换为标签
|
||||||
|
const parts = text.split(' ');
|
||||||
|
const filePaths = [];
|
||||||
|
const textParts = [];
|
||||||
|
|
||||||
|
parts.forEach(part => {
|
||||||
|
// 判断是否为文件路径:包含路径分隔符或文件扩展名
|
||||||
|
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part)) {
|
||||||
|
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 {
|
} else {
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
// 当添加用户消息时,隐藏 header
|
// 当添加用户消息时,隐藏 header
|
||||||
hideHeaderIfNeeded();
|
hideHeaderIfNeeded();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user