From 44bbcde5cf6ce57e69ee6d859520f6d357397b7e Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:40:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9F=A5=E8=AF=86=E5=9B=BE=E8=B0=B1?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=94=AF=E6=8C=81=20+=20=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dialogService: 添加智能体 SSE 事件处理 - toolExecutor: 添加 knowledge_save/knowledge_load 工具 - messageArea: 添加智能体消息渲染支持 - 添加 CLAUDE.md 项目配置 --- .idea/.gitignore | 8 + .idea/IC-Coder-Plugin.iml | 9 ++ .idea/codeStyles/Project.xml | 16 ++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + CLAUDE.md | 90 ++++++++++++ package.json | 2 +- src/services/dialogService.ts | 121 ++++++++++++++-- src/services/toolExecutor.ts | 13 ++ src/views/messageArea.ts | 145 +++++++++++++++++++ src/views/webviewContent.ts | 4 +- 14 files changed, 423 insertions(+), 16 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/IC-Coder-Plugin.iml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 CLAUDE.md diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/IC-Coder-Plugin.iml b/.idea/IC-Coder-Plugin.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/IC-Coder-Plugin.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..52bb793 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2bfdeda --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..37ca468 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e5ecf2a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +IC Coder Plugin 是一个 VS Code 扩展,为 Verilog/FPGA 开发提供智能辅助功能,包括代码生成、文件操作、iverilog 仿真和 VCD 波形查看。 + +## Build Commands + +```bash +# 安装依赖 +pnpm install + +# 编译 (开发模式) +pnpm run compile + +# 监听模式编译 +pnpm run watch + +# 生产环境打包 +pnpm run package + +# 代码检查 +pnpm run lint + +# 运行测试 +pnpm run test + +# 编译测试文件 +pnpm run compile-tests +``` + +## Development + +- 按 F5 在 VS Code 中启动调试模式 +- 使用 webpack 打包,入口文件为 `src/extension.ts` +- 输出目录为 `dist/` + +## Architecture + +``` +src/ +├── extension.ts # 插件入口,注册命令和视图 +├── panels/ +│ ├── ICHelperPanel.ts # 主聊天面板 (WebviewPanel) +│ └── VCDViewerPanel.ts # VCD 波形查看器面板 +├── views/ +│ ├── ICViewProvider.ts # 侧边栏视图提供者 +│ └── webviewContent.ts # Webview HTML 内容 (大文件,使用搜索) +├── utils/ +│ ├── messageHandler.ts # 消息处理核心逻辑 (大文件,使用搜索) +│ ├── iverilogRunner.ts # iverilog 编译和仿真执行 +│ ├── chatHistoryManager.ts # 会话历史管理 +│ ├── createFiles.ts # 文件创建工具 +│ └── readFiles.ts # 文件读取工具 +├── types/ +│ └── chatHistory.ts # 消息类型定义 (LangChain4j 格式) +└── test/ # 测试文件 +``` + +### Key Components + +**消息流程**: Webview -> `onDidReceiveMessage` -> `messageHandler.ts` -> 后端处理 -> `panel.webview.postMessage` -> Webview + +**消息类型** (`src/types/chatHistory.ts`): +- `MessageType.SYSTEM` / `USER` / `AI` / `TOOL_EXECUTION_RESULT` +- 兼容 LangChain4j 格式 + +**iverilog 集成** (`tools/iverilog/`): +- 内置 Windows x64 版本的 iverilog/vvp +- 通过 `IVERILOG_ROOT` 环境变量配置库路径 +- 支持命令: "生成 VCD"、"运行仿真"、"生成波形" + +## VS Code Extension Points + +- 命令: `ic-coder.openPanel`, `ic-coder.openChat`, `ic-coder.openVCDViewer` +- 侧边栏视图: `ic-coder.mainView` +- 激活事件: `onLanguage:verilog`, `onLanguage:vhdl`, `onStartupFinished` + +## Dependencies + +- `vcdrom`, `vcd-stream`, `waveql` - VCD 波形处理 +- `@wavedrom/doppler`, `onml` - 波形渲染 +- `iconv-lite` - 编码转换 + +## Notes + +- `webviewContent.ts` 和 `messageHandler.ts` 文件较大,建议使用搜索而非完整读取 +- 当前仅支持 Windows 平台的 iverilog,其他平台需用户自行安装 diff --git a/package.json b/package.json index 5a8eb53..aad18aa 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "properties": { "icCoder.backendUrl": { "type": "string", - "default": "http://192.168.1.108:2233", + "default": "http://localhost:2233", "description": "后端服务地址" }, "icCoder.timeout": { diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 4ef9bd8..736730d 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -13,7 +13,7 @@ import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api' * 消息段落类型 */ export interface MessageSegment { - type: 'text' | 'tool' | 'question'; + type: 'text' | 'tool' | 'question' | 'agent'; content?: string; toolName?: string; toolStatus?: 'running' | 'success' | 'error'; @@ -21,6 +21,22 @@ export interface MessageSegment { askId?: string; question?: string; options?: string[]; + // 智能体相关字段 + agentId?: string; + agentName?: string; + agentStatus?: 'running' | 'completed' | 'error'; + agentSteps?: AgentStep[]; +} + +/** + * 智能体执行步骤 + */ +export interface AgentStep { + step: number; + toolName: string; + toolInput?: unknown; + toolResult?: string; + status: 'running' | 'completed' | 'error'; } /** @@ -164,28 +180,42 @@ export class DialogSession { onToolCall: async (data: ToolCallRequest) => { const toolName = data.params.name; console.log('[DialogSession] onToolCall:', toolName); - // 检查是否已经有相同的工具段落(可能由 onToolStart 添加) - const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop(); - if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') { - console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName); + + // 检查是否有活跃的智能体(如果有,工具执行会显示在智能体卡片内,不需要单独显示) + const hasActiveAgent = this.segments.some( + s => s.type === 'agent' && s.agentStatus === 'running' + ); + + if (hasActiveAgent) { + console.log('[DialogSession] onToolCall: 智能体执行中,跳过工具段落:', toolName); } else { - this.addToolSegment(toolName, 'running'); - // 实时发送段落更新 - callbacks.onSegmentUpdate?.(this.segments); + // 检查是否已经有相同的工具段落(可能由 onToolStart 添加) + const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop(); + if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') { + console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName); + } else { + this.addToolSegment(toolName, 'running'); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); + } } + // 注意:不在这里调用 callbacks.onToolStart,避免与 onToolStart 事件重复 try { await executeToolCall(data, this.toolContext); - this.updateToolSegment(toolName, 'success', '执行完成'); - // 实时发送段落更新 - callbacks.onSegmentUpdate?.(this.segments); + if (!hasActiveAgent) { + this.updateToolSegment(toolName, 'success', '执行完成'); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); + } // 也不调用 callbacks.onToolComplete,避免重复 } catch (error) { const errorMsg = error instanceof Error ? error.message : '未知错误'; - this.updateToolSegment(toolName, 'error', errorMsg); + if (!hasActiveAgent) { + this.updateToolSegment(toolName, 'error', errorMsg); + callbacks.onSegmentUpdate?.(this.segments); + } callbacks.onToolError?.(toolName, errorMsg); - // 实时发送段落更新 - callbacks.onSegmentUpdate?.(this.segments); } }, @@ -257,6 +287,69 @@ export class DialogSession { callbacks.onNotification?.(data.message); }, + // 智能体事件处理 + onAgentStart: (data) => { + console.log('[DialogSession] onAgentStart:', data.agentId); + this.finalizeTextSegment(); + this.segments.push({ + type: 'agent', + agentId: data.agentId, + agentName: data.agentName, + content: data.instruction, + agentStatus: 'running', + agentSteps: [] + }); + callbacks.onSegmentUpdate?.(this.segments); + }, + + onAgentProgress: (data) => { + console.log('[DialogSession] onAgentProgress:', data.agentId, data.step, data.status); + const agentSegment = this.segments.find( + s => s.type === 'agent' && s.agentId === data.agentId + ); + if (agentSegment && agentSegment.agentSteps) { + if (data.status === 'running') { + agentSegment.agentSteps.push({ + step: data.step, + toolName: data.toolName, + toolInput: data.toolInput, + status: 'running' + }); + } else { + const step = agentSegment.agentSteps.find(s => s.step === data.step); + if (step) { + step.status = data.status; + step.toolResult = data.toolResult; + } + } + callbacks.onSegmentUpdate?.(this.segments); + } + }, + + onAgentComplete: (data) => { + console.log('[DialogSession] onAgentComplete:', data.agentId); + const agentSegment = this.segments.find( + s => s.type === 'agent' && s.agentId === data.agentId + ); + if (agentSegment) { + agentSegment.agentStatus = 'completed'; + agentSegment.content = data.summary; + callbacks.onSegmentUpdate?.(this.segments); + } + }, + + onAgentError: (data) => { + console.log('[DialogSession] onAgentError:', data.agentId, data.error); + const agentSegment = this.segments.find( + s => s.type === 'agent' && s.agentId === data.agentId + ); + if (agentSegment) { + agentSegment.agentStatus = 'error'; + agentSegment.content = data.error; + callbacks.onSegmentUpdate?.(this.segments); + } + }, + onOpen: () => { console.log('[DialogSession] SSE 连接已建立'); }, diff --git a/src/services/toolExecutor.ts b/src/services/toolExecutor.ts index b1ebfff..51edda2 100644 --- a/src/services/toolExecutor.ts +++ b/src/services/toolExecutor.ts @@ -117,6 +117,13 @@ async function executeFileRead(args: FileReadArgs): Promise { */ async function executeFileWrite(args: FileWriteArgs): Promise { await createOrOverwriteFile(args.path, args.content); + + // Verilog 文件添加知识图谱提示 + const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv'); + if (isVerilogFile) { + return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`; + } + return `文件已写入: ${args.path}`; } @@ -154,6 +161,12 @@ async function executeFileDelete(args: FileDeleteArgs): Promise { // 删除文件 fs.unlinkSync(absolutePath); + // Verilog 文件添加知识图谱提示 + const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv'); + if (isVerilogFile) { + return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`; + } + return `文件已删除: ${filePath}`; } diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index f88f87f..92e8108 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -528,6 +528,89 @@ export function getMessageAreaStyles(): string { border-radius: 4px; font-size: 12px;} + /* 智能体卡片样式 */ + .segment-agent { + margin: 8px 0; + } + .agent-card { + border: 1px solid var(--vscode-input-border); + border-radius: 8px; + overflow: hidden; + background: var(--vscode-editor-background); + } + .agent-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--vscode-sideBar-background); + border-bottom: 1px solid var(--vscode-input-border); + } + .agent-icon { + font-size: 16px; + } + .agent-name { + font-weight: 500; + flex: 1; + } + .agent-status { + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + } + .agent-status.running { + background: var(--vscode-inputValidation-infoBackground); + color: var(--vscode-inputValidation-infoForeground); + } + .agent-status.completed { + background: #28a745; + color: white; + } + .agent-status.error { + background: #dc3545; + color: white; + } + .agent-body { + padding: 8px; + } + .agent-steps-container { + max-height: 150px; + overflow-y: auto; + font-size: 12px; + } + .agent-step { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 4px; + margin-bottom: 4px; + background: var(--vscode-list-hoverBackground); + } + .agent-step:last-child { + margin-bottom: 0; + } + .step-icon { + flex-shrink: 0; + } + .step-name { + font-weight: 500; + color: var(--vscode-foreground); + } + .step-result { + color: var(--vscode-descriptionForeground); + font-size: 11px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .agent-step-placeholder { + color: var(--vscode-descriptionForeground); + font-style: italic; + padding: 8px; + text-align: center; + } + ${getWaveformPreviewContent()} `; } @@ -903,6 +986,41 @@ export function getMessageAreaScript(): string { } }, 0); } + } else if (segment.type === 'agent') { + // 智能体卡片渲染 + segmentDiv.className += ' segment-agent'; + const statusText = segment.agentStatus === 'completed' ? '完成' + : segment.agentStatus === 'error' ? '错误' : '执行中'; + const statusClass = segment.agentStatus || 'running'; + + const stepsHtml = (segment.agentSteps || []).map(step => { + const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄'; + const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : ''; + return \`
\${icon}\${step.toolName}\${result}
\`; + }).join(''); + + segmentDiv.innerHTML = \` +
+
+ 🤖 + \${segment.agentName || '智能体'} + \${statusText} +
+
+
+ \${stepsHtml || '
等待执行...
'} +
+
+
+ \`; + + // 自动滚动到最新步骤 + setTimeout(() => { + const container = segmentDiv.querySelector('.agent-steps-container'); + if (container) { + container.scrollTop = container.scrollHeight; + } + }, 0); } currentSegmentedMessage.appendChild(segmentDiv); @@ -1045,6 +1163,33 @@ export function getMessageAreaScript(): string { \`; + } else if (segment.type === 'agent') { + // 智能体卡片渲染 + segmentDiv.className += ' segment-agent'; + const statusText = segment.agentStatus === 'completed' ? '完成' + : segment.agentStatus === 'error' ? '错误' : '执行中'; + const statusClass = segment.agentStatus || 'running'; + + const stepsHtml = (segment.agentSteps || []).map(step => { + const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄'; + const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : ''; + return \`
\${icon}\${step.toolName}\${result}
\`; + }).join(''); + + segmentDiv.innerHTML = \` +
+
+ 🤖 + \${segment.agentName || '智能体'} + \${statusText} +
+
+
+ \${stepsHtml || '
等待执行...
'} +
+
+
+ \`; } container.appendChild(segmentDiv); diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 740d484..93066e4 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -377,6 +377,7 @@ export function getWebviewContent(iconUri?: string): string { + ${getInputAreaContent()} @@ -399,7 +400,8 @@ export function getWebviewContent(iconUri?: string): string { const questions = { counter: '生成一个4位同步计数器', fsm: '生成一个状态机', - testbench: '生成测试平台' + testbench: '生成测试平台', + explore: '请启动知识探索智能体,分析当前项目结构' }; if (questions[type]) { messageInput.value = questions[type];