Compare commits
50 Commits
feat/Plugi
...
c22081c5e9
| Author | SHA1 | Date | |
|---|---|---|---|
| c22081c5e9 | |||
| e4ff49bade | |||
| ada4806493 | |||
| e48e822d07 | |||
| acf3f9ff37 | |||
| c27b08cccf | |||
| 9fc3c9f056 | |||
| 60d8eaf0eb | |||
| df6f983e83 | |||
| acf60f2a17 | |||
| f933d84cd1 | |||
| b794d1ceb0 | |||
| 259310a29d | |||
| 715eac5949 | |||
| c2936395d9 | |||
| 8762eacb3e | |||
| 3d535fd3e1 | |||
| ecdbe0bdc0 | |||
| c49aaf753c | |||
| 0f8674e1c7 | |||
| ef2159f1bd | |||
| b662d25c9c | |||
| 1ce1ed715c | |||
| 2587018405 | |||
| 28b75e8475 | |||
| 5287d483d8 | |||
| 16e91bd2c0 | |||
| e6b6bc3698 | |||
| d43cd610a0 | |||
| 842e5fb49b | |||
| 430581598b | |||
| 2d5b297171 | |||
| 023fdb66c3 | |||
| 42481cd314 | |||
| e77194628a | |||
| 2aff54de74 | |||
| 91fadf591f | |||
| 02b56a7031 | |||
| c42ebdfe59 | |||
| 3f0cc8ae29 | |||
| 0f458f6299 | |||
| d415d8ee4e | |||
| bd7a85b705 | |||
| 44bbcde5cf | |||
| d4c726ea9c | |||
| 082ef923b2 | |||
| 5cb68652f9 | |||
| 9bfa774336 | |||
| 009da59d38 | |||
| 5ea5ddba6e |
11
.gitignore
vendored
11
.gitignore
vendored
@ -3,3 +3,14 @@ dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
|
||||
# waveform_trace 打包产物(exe 太大,通过 Release 发布)
|
||||
tools/waveform_trace/bin/
|
||||
tools/waveform_trace/src/build/
|
||||
tools/waveform_trace/src/dist/
|
||||
tools/waveform_trace/src/*.spec
|
||||
|
||||
# Python 缓存
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -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
|
||||
9
.idea/IC-Coder-Plugin.iml
generated
Normal file
9
.idea/IC-Coder-Plugin.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
16
.idea/codeStyles/Project.xml
generated
Normal file
16
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,16 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
|
||||
</JavaCodeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="IF_BRACE_FORCE" value="3" />
|
||||
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||
<option name="FOR_BRACE_FORCE" value="3" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/IC-Coder-Plugin.iml" filepath="$PROJECT_DIR$/.idea/IC-Coder-Plugin.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
29
.vscodeignore
Normal file
29
.vscodeignore
Normal file
@ -0,0 +1,29 @@
|
||||
# 排除开发文件
|
||||
.vscode/**
|
||||
.git/**
|
||||
.gitignore
|
||||
node_modules/**
|
||||
src/**
|
||||
**/*.ts
|
||||
**/*.map
|
||||
|
||||
# 排除测试文件
|
||||
test/**
|
||||
**/*.test.js
|
||||
|
||||
# 排除文档
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# 排除 waveform_trace Python 源码(只保留 exe)
|
||||
tools/waveform_trace/src/**
|
||||
tools/waveform_trace/build/**
|
||||
tools/waveform_trace/dist/**
|
||||
tools/waveform_trace/build.bat
|
||||
tools/waveform_trace/build.sh
|
||||
|
||||
# 排除打包临时文件
|
||||
**/__pycache__/**
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
**/*.spec
|
||||
90
CLAUDE.md
Normal file
90
CLAUDE.md
Normal file
@ -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,其他平台需用户自行安装
|
||||
25
package.json
25
package.json
@ -9,7 +9,8 @@
|
||||
},
|
||||
"icon": "media/icon.png",
|
||||
"categories": [
|
||||
"Other"
|
||||
"Chat",
|
||||
"Programming Languages"
|
||||
],
|
||||
"keywords": [
|
||||
"IC",
|
||||
@ -69,27 +70,7 @@
|
||||
"id": "iccoder",
|
||||
"label": "IC Coder"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "IC Coder",
|
||||
"properties": {
|
||||
"icCoder.backendUrl": {
|
||||
"type": "string",
|
||||
"default": "http://192.168.1.108:2233",
|
||||
"description": "后端服务地址"
|
||||
},
|
||||
"icCoder.timeout": {
|
||||
"type": "number",
|
||||
"default": 60000,
|
||||
"description": "请求超时时间(毫秒)"
|
||||
},
|
||||
"icCoder.userId": {
|
||||
"type": "string",
|
||||
"default": "default-user",
|
||||
"description": "用户ID(临时配置)"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "pnpm run package",
|
||||
|
||||
BIN
src/assets/model/Auto.png
Normal file
BIN
src/assets/model/Auto.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/model/Max.png
Normal file
BIN
src/assets/model/Max.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/model/Sy.png
Normal file
BIN
src/assets/model/Sy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/model/lite.png
Normal file
BIN
src/assets/model/lite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@ -1,9 +1,15 @@
|
||||
/**
|
||||
* 配置管理
|
||||
* 从 VSCode 设置读取配置项
|
||||
* 支持 dev(本地开发)和 test(测试服务器)两种环境
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
/** 环境类型 */
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "dev";
|
||||
|
||||
/** 配置项接口 */
|
||||
export interface IccoderConfig {
|
||||
/** 后端服务地址 */
|
||||
@ -14,24 +20,40 @@ export interface IccoderConfig {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/** 默认配置 */
|
||||
const DEFAULT_CONFIG: IccoderConfig = {
|
||||
backendUrl: "http://localhost:8080",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
/** 环境配置 */
|
||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 */
|
||||
dev: {
|
||||
backendUrl: "http://localhost:2233",
|
||||
timeout: 300000, // 5分钟,与子智能体超时一致
|
||||
userId: "default-user",
|
||||
},
|
||||
/** 测试服务器环境 */
|
||||
test: {
|
||||
backendUrl: "http://192.168.1.108:2233",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
},
|
||||
/** 生产环境 */
|
||||
prod: {
|
||||
backendUrl: "https://api.iccoder.com", // TODO: 替换为实际生产地址
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前环境
|
||||
*/
|
||||
export function getCurrentEnv(): Environment {
|
||||
return CURRENT_ENV;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项
|
||||
*/
|
||||
export function getConfig(): IccoderConfig {
|
||||
const config = vscode.workspace.getConfiguration("icCoder");
|
||||
|
||||
return {
|
||||
backendUrl: config.get<string>("backendUrl", DEFAULT_CONFIG.backendUrl),
|
||||
timeout: config.get<number>("timeout", DEFAULT_CONFIG.timeout),
|
||||
userId: config.get<string>("userId", DEFAULT_CONFIG.userId),
|
||||
};
|
||||
return { ...ENV_CONFIG[CURRENT_ENV] };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,7 +61,6 @@ export function getConfig(): IccoderConfig {
|
||||
*/
|
||||
export function getApiUrl(path: string): string {
|
||||
const { backendUrl } = getConfig();
|
||||
// 确保 URL 格式正确
|
||||
const baseUrl = backendUrl.endsWith("/")
|
||||
? backendUrl.slice(0, -1)
|
||||
: backendUrl;
|
||||
|
||||
@ -70,3 +70,98 @@ export const stopIconSvg = `
|
||||
<path d="M349.75 349.75m57.15 0l210.2 0q57.15 0 57.15 57.15l0 210.2q0 57.15-57.15 57.15l-210.2 0q-57.15 0-57.15-57.15l0-210.2q0-57.15 57.15-57.15Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 探索智能体图标 SVG
|
||||
*/
|
||||
export const agentIconSvg = `
|
||||
<svg t="1767101071638" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7779" width="16" height="16" style="display: inline-block; vertical-align: middle;"><path d="M173.474909 410.414545c-20.293818 0-33.838545-13.498182-33.838545-33.792v-135.377454C139.636364 187.066182 187.019636 139.636364 241.198545 139.636364h135.447273c20.270545 0 33.815273 13.591273 33.815273 33.885091s-13.591273 33.838545-33.885091 33.838545h-135.447273c-20.317091 0-33.815273 13.591273-33.815272 33.885091v135.377454c0.046545 20.293818-13.498182 33.792-33.838546 33.792z m203.101091 473.902546h-135.447273C187.042909 884.317091 139.636364 836.933818 139.636364 782.754909V647.447273c0-20.386909 13.544727-33.838545 33.838545-33.838546s33.885091 13.451636 33.885091 33.838546v135.330909c0 20.293818 13.544727 33.931636 33.838545 33.931636h135.447273c20.270545 0 33.815273 13.451636 33.815273 33.745455-0.046545 20.340364-13.591273 33.885091-33.885091 33.885091z m406.178909 0H647.447273c-20.386909 0-33.931636-13.544727-33.931637-33.931636 0-20.293818 13.544727-33.745455 33.931637-33.745455h135.330909c20.386909 0 33.838545-13.637818 33.838545-33.931636V647.447273c0-20.386909 13.544727-33.838545 33.931637-33.838546 20.293818 0 33.838545 13.451636 33.838545 33.838546v135.330909c-0.046545 54.178909-47.522909 101.562182-101.608727 101.562182z m67.723636-473.902546c-20.386909 0-33.931636-13.498182-33.931636-33.792v-135.377454c0-20.340364-13.451636-33.885091-33.838545-33.885091H647.447273c-20.386909 0-33.931636-13.498182-33.931637-33.838545S627.083636 139.636364 647.424 139.636364h135.330909c54.085818 0 101.562182 47.429818 101.562182 101.608727v135.377454c0 20.293818-13.544727 33.792-33.838546 33.792z m0 135.493819H173.474909c-20.293818 0-33.838545-13.591273-33.838545-33.931637s13.544727-33.885091 33.838545-33.885091h677.003636c20.293818 0 33.838545 13.591273 33.838546 33.885091s-13.544727 33.931636-33.838546 33.931637z" fill="#8a8a8a" p-id="7780"></path></svg>`;
|
||||
|
||||
/**
|
||||
* planner 图标 SVG
|
||||
*/
|
||||
export const plannerIconSvg = `<svg t="1767143425474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10954" width="16" height="16"><path d="M860.544 633.856c-82.368 0-152.128 69.632-158.464 152h-354.88c-31.616 0-63.296-31.68-63.296-63.296V437.376c12.608 0 25.344 6.4 44.288 6.4h380.16c12.672 69.696 76.032 126.656 152.128 126.656 88.704 0 158.336-69.696 158.336-158.4s-69.632-158.4-158.336-158.4c-76.096 0-139.456 57.024-152.128 126.656h-361.216c-31.616 0-63.296-31.68-63.296-63.296v-133.12h164.736c31.68 0 63.296-22.848 63.296-54.528a55.04 55.04 0 0 0-56-56h-380.16c-31.68 0-70.72 17.984-70.72 56s31.68 54.528 63.36 54.528h133.056v538.624c0 69.696 57.088 126.656 126.72 126.656h386.56c25.344 57.088 82.368 101.376 145.728 101.376a156.8 156.8 0 0 0 158.336-158.4 156.608 156.608 0 0 0-158.208-158.272z m0-316.8c50.624 0 94.912 44.288 94.912 94.976s-44.288 94.976-94.912 94.976c-50.752 0-95.104-44.288-95.104-94.976s44.352-94.976 95.104-94.976z m0 570.24c-50.752 0-95.104-44.352-95.104-95.04s44.352-95.04 95.104-95.04c50.624 0 94.912 44.352 94.912 95.04s-44.288 95.04-94.912 95.04z" p-id="10955" fill="#8a8a8a"></path></svg>`;
|
||||
|
||||
/**
|
||||
* 保存知识库图标 SVG
|
||||
*/
|
||||
export const saveKnowledgeIconSvg = `
|
||||
<span class="tool-save-knowledge-icon">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M827.733333 315.733333l-123.733333-123.733333c-12.8-12.8-34.133333-21.333333-59.733333-21.333334H256c-46.96 0-85.44 40.96-85.44 85.44v512c0 46.96 40.96 85.44 85.44 85.44h512c46.96 0 85.44-40.96 85.44-85.44V375.466667c0-25.6-8.533333-46.933333-21.333333-59.733334z m-140.8 469.333334H337.066667v-85.333334h349.866666v85.333334z m0-170.666667H337.066667v-85.333333h349.866666v85.333333z m0-170.666667H337.066667v-85.333333h349.866666v85.333333z" fill="#8a8a8a"/>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件读取图标 SVG
|
||||
*/
|
||||
export const fileReadIconSvg = `
|
||||
<span class="tool-file-read-icon">
|
||||
<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.7zM790.2 326H602V137.8L790.2 326z m1.8 562H232V136h302v216c0 23.2 18.8 42 42 42h216v494z" fill="#8a8a8a"/>
|
||||
<path d="M342 472h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM342 616h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM342 760h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z" fill="#8a8a8a"/>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件删除图标 SVG
|
||||
*/
|
||||
export const fileDeleteIconSvg = `
|
||||
<span class="tool-file-delete-icon">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72z" fill="#8a8a8a"/>
|
||||
<path d="M832 256H192c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z" fill="#8a8a8a"/>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 仿真图标 SVG
|
||||
*/
|
||||
export const simulationIconSvg = `
|
||||
<span class="tool-simulation-icon">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z" fill="#8a8a8a"/>
|
||||
<path d="M210 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H210c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM210 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H210c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8zM462 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H462c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM462 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H462c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8zM714 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H714c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM714 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H714c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8z" fill="#8a8a8a"/>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 波形分析图标 SVG
|
||||
*/
|
||||
export const waveformIconSvg = `
|
||||
<span class="tool-waveform-icon">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M952 474H829.8C812.5 327.6 696.4 211.5 550 194.2V72c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v122.2C327.6 211.5 211.5 327.6 194.2 474H72c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h122.2C211.5 696.4 327.6 812.5 474 829.8V952c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V829.8C696.4 812.5 812.5 696.4 829.8 550H952c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zM512 756c-134.8 0-244-109.2-244-244s109.2-244 244-244 244 109.2 244 244-109.2 244-244 244z" fill="#8a8a8a"/>
|
||||
<path d="M512 392c-32.1 0-62.1 12.4-84.8 35.2-22.7 22.7-35.2 52.7-35.2 84.8s12.5 62.1 35.2 84.8c22.7 22.7 52.7 35.2 84.8 35.2s62.1-12.5 84.8-35.2c22.7-22.7 35.2-52.7 35.2-84.8s-12.5-62.1-35.2-84.8C574.1 404.4 544.1 392 512 392z" fill="#8a8a8a"/>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 知识库加载图标 SVG
|
||||
*/
|
||||
export const knowledgeLoadIconSvg = `
|
||||
<span class="tool-knowledge-load-icon">
|
||||
<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" fill="#8a8a8a"/>
|
||||
<path d="M492 400h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM492 544h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM492 688h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM340 368a40 40 0 1 0 80 0 40 40 0 1 0-80 0zM340 512a40 40 0 1 0 80 0 40 40 0 1 0-80 0zM340 656a40 40 0 1 0 80 0 40 40 0 1 0-80 0z" fill="#8a8a8a"/>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 状态转换图标 SVG
|
||||
*/
|
||||
export const stateTransitionIconSvg = `
|
||||
<span class="tool-state-transition-icon">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#8a8a8a"/>
|
||||
<path d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.9-11.2z" fill="#8a8a8a"/>
|
||||
<path d="M512 320c-4.4 0-8 3.6-8 8v184c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H560V328c0-4.4-3.6-8-8-8h-40z" fill="#8a8a8a"/>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@ -9,7 +9,12 @@ import {
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handlePlanAction,
|
||||
setPendingPlanExecution,
|
||||
getCurrentTaskId,
|
||||
setLastTaskId,
|
||||
} from "../utils/messageHandler";
|
||||
import { compactDialog } from "../services/apiClient";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { MessageType } from "../types/chatHistory";
|
||||
@ -23,21 +28,27 @@ export async function showICHelperPanel(
|
||||
) {
|
||||
// 检查用户是否已登录
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (!session) {
|
||||
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -49,7 +60,10 @@ export async function showICHelperPanel(
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
@ -71,8 +85,28 @@ export async function showICHelperPanel(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||
);
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
||||
panel.webview.html = getWebviewContent(
|
||||
iconUri.toString(),
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString()
|
||||
);
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
@ -107,7 +141,12 @@ export async function showICHelperPanel(
|
||||
// 切换到当前面板的任务上下文
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
|
||||
handleUserMessage(panel, message.text, context.extensionPath);
|
||||
handleUserMessage(
|
||||
panel,
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode
|
||||
);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
@ -179,7 +218,90 @@ export async function showICHelperPanel(
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
// 新增:压缩会话
|
||||
case "compressConversation":
|
||||
{
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
compactDialog(taskId)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "✅ 会话压缩完成",
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${result.error || "未知错误"}`,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "❌ 没有活跃的会话",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
// 确认执行:切换到 Agent 模式
|
||||
panel.webview.postMessage({
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 获取当前会话的 taskId,用于复用知识图谱数据
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
||||
setPendingPlanExecution(
|
||||
panel,
|
||||
message.planTitle || "计划",
|
||||
context.extensionPath,
|
||||
taskId
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 新增:检查工作区状态
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
vscode.workspace.workspaceFolders &&
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
);
|
||||
if (!hasWorkspace) {
|
||||
// 弹窗提示用户需要打开工作区
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||
"打开文件夹"
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "打开文件夹") {
|
||||
vscode.commands.executeCommand("vscode.openFolder");
|
||||
}
|
||||
});
|
||||
}
|
||||
// 返回工作区状态给前端
|
||||
panel.webview.postMessage({
|
||||
command: "workspaceStatus",
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
@ -464,6 +586,9 @@ async function selectConversation(
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 lastTaskId,用于压缩等操作
|
||||
setLastTaskId(taskId);
|
||||
|
||||
// 更新面板的任务映射,确保后续对话保存到正确的任务中
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||
|
||||
@ -6,7 +6,7 @@ import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl, getConfig } from '../config/settings';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse } from '../types/api';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
@ -103,6 +103,18 @@ export async function submitAnswer(answer: AnswerRequest): Promise<AnswerRespons
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工具确认响应(Ask 模式)
|
||||
* POST /api/tool/confirm
|
||||
*/
|
||||
export async function submitToolConfirm(response: ToolConfirmResponse): Promise<ToolResultResponse> {
|
||||
console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`);
|
||||
return request<ToolResultResponse>('/api/tool/confirm', {
|
||||
method: 'POST',
|
||||
body: response
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* GET /api/dialog/health
|
||||
@ -114,6 +126,55 @@ export async function healthCheck(): Promise<{ status: string }> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话请求
|
||||
*/
|
||||
export interface StopDialogRequest {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话响应
|
||||
*/
|
||||
export interface StopDialogResponse {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话
|
||||
* POST /api/dialog/stop
|
||||
*/
|
||||
export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
|
||||
console.log(`[API] 停止对话: taskId=${taskId}`);
|
||||
return request<StopDialogResponse>('/api/dialog/stop', {
|
||||
method: 'POST',
|
||||
body: { taskId }
|
||||
});
|
||||
}
|
||||
|
||||
/** 压缩对话响应 */
|
||||
export interface CompactDialogResponse {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动压缩对话历史
|
||||
* POST /api/dialog/compact
|
||||
*/
|
||||
export async function compactDialog(taskId: string): Promise<CompactDialogResponse> {
|
||||
console.log(`[API] 压缩对话: taskId=${taskId}`);
|
||||
return request<CompactDialogResponse>('/api/dialog/compact', {
|
||||
method: 'POST',
|
||||
body: { taskId }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功的工具结果
|
||||
*/
|
||||
|
||||
@ -3,17 +3,21 @@
|
||||
* 整合 SSE 通信、工具执行、用户交互
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler';
|
||||
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
|
||||
import { userInteractionManager } from './userInteraction';
|
||||
import { getConfig } from '../config/settings';
|
||||
import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api';
|
||||
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
|
||||
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
||||
|
||||
/**
|
||||
* 消息段落类型
|
||||
*/
|
||||
export interface MessageSegment {
|
||||
type: 'text' | 'tool' | 'question';
|
||||
type: 'text' | 'tool' | 'question' | 'agent' | 'plan';
|
||||
content?: string;
|
||||
toolName?: string;
|
||||
toolStatus?: 'running' | 'success' | 'error';
|
||||
@ -21,6 +25,26 @@ export interface MessageSegment {
|
||||
askId?: string;
|
||||
question?: string;
|
||||
options?: string[];
|
||||
// 智能体相关字段
|
||||
agentId?: string;
|
||||
agentName?: string;
|
||||
agentStatus?: 'running' | 'completed' | 'error';
|
||||
agentSteps?: AgentStep[];
|
||||
// 计划相关字段
|
||||
planTitle?: string;
|
||||
planSteps?: string[];
|
||||
planSummary?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能体执行步骤
|
||||
*/
|
||||
export interface AgentStep {
|
||||
step: number;
|
||||
toolName: string;
|
||||
toolInput?: unknown;
|
||||
toolResult?: string;
|
||||
status: 'running' | 'completed' | 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,6 +59,10 @@ export interface DialogCallbacks {
|
||||
onToolComplete?: (toolName: string, result: string) => void;
|
||||
/** 工具执行错误 */
|
||||
onToolError?: (toolName: string, error: string) => void;
|
||||
/** 工具确认请求(Ask 模式) */
|
||||
onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void;
|
||||
/** 计划确认请求(Plan 模式) */
|
||||
onPlanConfirm?: (confirmId: number, title: string, steps: string[], summary: string) => void;
|
||||
/** 显示问题(ask_user) */
|
||||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
||||
/** 实时更新段落(流式过程中) */
|
||||
@ -45,6 +73,8 @@ export interface DialogCallbacks {
|
||||
onError?: (message: string) => void;
|
||||
/** 通知消息 */
|
||||
onNotification?: (message: string) => void;
|
||||
/** 上下文使用量更新 */
|
||||
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,8 +89,9 @@ export class DialogSession {
|
||||
private segments: MessageSegment[] = [];
|
||||
private currentTextSegment: MessageSegment | null = null;
|
||||
|
||||
constructor(extensionPath: string) {
|
||||
this.taskId = generateTaskId();
|
||||
constructor(extensionPath: string, existingTaskId?: string) {
|
||||
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
this.taskId = existingTaskId || generateTaskId();
|
||||
this.toolContext = createToolExecutorContext(extensionPath);
|
||||
}
|
||||
|
||||
@ -126,12 +157,166 @@ export class DialogSession {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载知识图谱数据
|
||||
* 从 .iccoder/knowledge.json 读取
|
||||
*/
|
||||
private async loadKnowledgeData(): Promise<string | null> {
|
||||
console.log('[DialogSession] loadKnowledgeData 开始执行');
|
||||
|
||||
// 等待 workspaceFolders 就绪(首次打开窗口/首次触发命令时可能为空)
|
||||
const workspaceFolders = await this.waitForWorkspaceFolders();
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
console.log('[DialogSession] 没有工作区文件夹');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 多根工作区场景:优先读取实际存在 knowledge.json 的根目录
|
||||
for (const folder of this.getWorkspaceFolderCandidates(workspaceFolders)) {
|
||||
const knowledgeUri = vscode.Uri.joinPath(folder.uri, '.iccoder', 'knowledge.json');
|
||||
console.log('[DialogSession] 知识图谱 URI:', knowledgeUri.toString());
|
||||
|
||||
try {
|
||||
const content = await this.readTextFileWithRetry(knowledgeUri, 5);
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 基础校验 + 清洗:避免偶发读取到半截内容导致后端反序列化失败
|
||||
try {
|
||||
const parsed = JSON.parse(content) as any;
|
||||
|
||||
// 兼容:后端 KnowledgeGraph.isEmpty() 可能被序列化为 "empty",老后端反序列化会失败
|
||||
if (parsed && typeof parsed === 'object' && 'empty' in parsed) {
|
||||
delete parsed.empty;
|
||||
}
|
||||
|
||||
const sanitized = JSON.stringify(parsed);
|
||||
console.log('[DialogSession] 知识图谱已清洗, sanitizedLen:', sanitized.length);
|
||||
return sanitized;
|
||||
} catch (e) {
|
||||
console.warn('[DialogSession] 知识图谱 JSON 解析失败,跳过本次读取:', e);
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[DialogSession] 加载知识图谱失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async waitForWorkspaceFolders(): Promise<readonly vscode.WorkspaceFolder[] | undefined> {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (folders && folders.length > 0) {
|
||||
return folders;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
return vscode.workspace.workspaceFolders;
|
||||
}
|
||||
|
||||
private getWorkspaceFolderCandidates(
|
||||
workspaceFolders: readonly vscode.WorkspaceFolder[]
|
||||
): vscode.WorkspaceFolder[] {
|
||||
const result: vscode.WorkspaceFolder[] = [];
|
||||
|
||||
// 1) 当前激活文件所在的 workspace folder(如果有)
|
||||
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||
if (activeFolder) {
|
||||
result.push(activeFolder);
|
||||
}
|
||||
|
||||
// 2) 其它 workspace folders(去重)
|
||||
for (const folder of workspaceFolders) {
|
||||
if (!result.some(f => f.uri.toString() === folder.uri.toString())) {
|
||||
result.push(folder);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async readTextFileWithRetry(uri: vscode.Uri, maxAttempts: number): Promise<string | null> {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const bytes = await vscode.workspace.fs.readFile(uri);
|
||||
const text = Buffer.from(bytes).toString('utf-8');
|
||||
if (!text || !text.trim()) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
} catch (error) {
|
||||
// 文件不存在:不是错误,直接返回 null
|
||||
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const retryable =
|
||||
(error instanceof vscode.FileSystemError && error.code === 'Unavailable') ||
|
||||
(typeof (error as any)?.code === 'string' && ['EBUSY', 'EPERM', 'EACCES'].includes((error as any).code));
|
||||
|
||||
if (!retryable || attempt >= maxAttempts) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const delayMs = 50 * attempt;
|
||||
console.log(`[DialogSession] 读取知识图谱失败(可重试): attempt=${attempt}/${maxAttempts}, delay=${delayMs}ms`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具操作描述(用于确认对话框)
|
||||
*/
|
||||
private getToolDescription(toolName: string, toolInput: Record<string, unknown>): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
switch (toolName) {
|
||||
case 'file_write':
|
||||
lines.push(`文件路径: ${toolInput.path || '未知'}`);
|
||||
if (toolInput.content) {
|
||||
const content = String(toolInput.content);
|
||||
lines.push(`内容长度: ${content.length} 字符`);
|
||||
lines.push(`内容预览: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
break;
|
||||
case 'file_delete':
|
||||
lines.push(`删除文件: ${toolInput.path || '未知'}`);
|
||||
break;
|
||||
case 'syntax_check':
|
||||
lines.push('执行语法检查');
|
||||
if (toolInput.code) {
|
||||
const code = String(toolInput.code);
|
||||
lines.push(`代码长度: ${code.length} 字符`);
|
||||
}
|
||||
break;
|
||||
case 'simulation':
|
||||
lines.push(`RTL文件: ${toolInput.rtlPath || '未知'}`);
|
||||
lines.push(`TB文件: ${toolInput.tbPath || '未知'}`);
|
||||
if (toolInput.duration) {
|
||||
lines.push(`仿真时长: ${toolInput.duration}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
lines.push(`参数: ${JSON.stringify(toolInput, null, 2)}`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息并开始流式对话
|
||||
*/
|
||||
async sendMessage(
|
||||
message: string,
|
||||
callbacks: DialogCallbacks
|
||||
callbacks: DialogCallbacks,
|
||||
mode?: RunMode
|
||||
): Promise<void> {
|
||||
if (this.isActive) {
|
||||
callbacks.onError?.('当前有对话正在进行中');
|
||||
@ -144,13 +329,29 @@ export class DialogSession {
|
||||
this.currentTextSegment = null;
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
// 获取压缩数据和新消息(用于后端重启后恢复)
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const compactedData = await historyManager.loadCompactedData(this.taskId);
|
||||
const newMessages = historyManager.getNewMessagesSinceCompaction();
|
||||
|
||||
// 加载知识图谱数据
|
||||
const knowledgeData = await this.loadKnowledgeData();
|
||||
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
|
||||
|
||||
const request: DialogRequest = {
|
||||
taskId: this.taskId,
|
||||
message,
|
||||
userId: config.userId,
|
||||
toolMode: 'AGENT'
|
||||
mode: mode || 'agent',
|
||||
compactedData: compactedData || undefined,
|
||||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||||
knowledgeData: knowledgeData || undefined
|
||||
};
|
||||
|
||||
// 追踪用户消息
|
||||
historyManager.trackUserMessage(message);
|
||||
|
||||
const sseCallbacks: SSECallbacks = {
|
||||
onTextDelta: (data) => {
|
||||
this.accumulatedText += data.text;
|
||||
@ -164,28 +365,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);
|
||||
}
|
||||
},
|
||||
|
||||
@ -218,6 +433,106 @@ export class DialogSession {
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
},
|
||||
|
||||
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||||
console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId);
|
||||
|
||||
// 结束当前文本段落
|
||||
this.finalizeTextSegment();
|
||||
|
||||
// 生成工具描述
|
||||
const toolDescription = this.getToolDescription(data.toolName, data.toolInput);
|
||||
|
||||
// 构建问题文本
|
||||
const toolNameMap: Record<string, string> = {
|
||||
'file_write': '写入文件',
|
||||
'file_delete': '删除文件',
|
||||
'syntax_check': '语法检查',
|
||||
'simulation': '运行仿真'
|
||||
};
|
||||
const toolDisplayName = toolNameMap[data.toolName] || data.toolName;
|
||||
const question = `确认执行操作:${toolDisplayName}\n\n${toolDescription}`;
|
||||
|
||||
// 生成唯一的 askId
|
||||
const askId = `tool_confirm_${data.confirmId}`;
|
||||
|
||||
// 添加问题段落到聊天界面
|
||||
this.segments.push({
|
||||
type: 'question',
|
||||
askId: askId,
|
||||
question: question,
|
||||
options: ['确认执行', '取消']
|
||||
});
|
||||
|
||||
// 实时发送段落更新
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
|
||||
// 调用回调通知 UI
|
||||
callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput);
|
||||
|
||||
// 使用 userInteractionManager 等待用户回答
|
||||
try {
|
||||
await userInteractionManager.handleAskUser(
|
||||
{
|
||||
askId: askId,
|
||||
question: question,
|
||||
options: ['确认执行', '取消']
|
||||
} as AskUserEvent,
|
||||
this.taskId
|
||||
);
|
||||
|
||||
// 注意:用户回答后,需要在 receiveAnswer 中处理 tool_confirm 类型的 askId
|
||||
// 这里不直接调用 submitToolConfirm,而是在 userInteractionManager 中统一处理
|
||||
} catch (error) {
|
||||
console.error('[DialogSession] 处理工具确认失败:', error);
|
||||
// 如果出错,默认取消执行
|
||||
try {
|
||||
await submitToolConfirm({
|
||||
confirmId: data.confirmId,
|
||||
taskId: this.taskId,
|
||||
approved: false
|
||||
});
|
||||
} catch (submitError) {
|
||||
console.error('[DialogSession] 发送取消响应失败:', submitError);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onPlanConfirm: async (data: PlanConfirmEvent) => {
|
||||
console.log('[DialogSession] onPlanConfirm:', data.title);
|
||||
|
||||
// 结束当前文本段落
|
||||
this.finalizeTextSegment();
|
||||
|
||||
const askId = `ask_${data.confirmId}`;
|
||||
|
||||
// 添加计划段落到聊天界面(包含 askId 用于响应)
|
||||
this.segments.push({
|
||||
type: 'plan',
|
||||
askId: askId,
|
||||
planTitle: data.title,
|
||||
planSteps: data.steps,
|
||||
planSummary: data.summary
|
||||
});
|
||||
|
||||
// 实时发送段落更新
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
|
||||
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||||
const planEvent = {
|
||||
askId: askId,
|
||||
question: `请确认执行计划:${data.title}`,
|
||||
options: ['确认执行', '修改计划', '取消']
|
||||
};
|
||||
try {
|
||||
await userInteractionManager.handleAskUser(planEvent as AskUserEvent, this.taskId);
|
||||
} catch (error) {
|
||||
console.error('[DialogSession] 处理计划确认失败:', error);
|
||||
}
|
||||
|
||||
// 调用回调通知 UI
|
||||
callbacks.onPlanConfirm?.(data.confirmId, data.title, data.steps, data.summary);
|
||||
},
|
||||
|
||||
onAskUser: async (data: AskUserEvent) => {
|
||||
this.finalizeTextSegment();
|
||||
this.segments.push({
|
||||
@ -240,6 +555,12 @@ export class DialogSession {
|
||||
onComplete: (data) => {
|
||||
this.isActive = false;
|
||||
this.finalizeTextSegment();
|
||||
|
||||
// 追踪 AI 消息(用于后端重启后恢复)
|
||||
if (this.accumulatedText) {
|
||||
historyManager.trackAiMessage(this.accumulatedText);
|
||||
}
|
||||
|
||||
// 发送所有段落
|
||||
callbacks.onComplete?.(this.segments);
|
||||
},
|
||||
@ -257,6 +578,80 @@ 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);
|
||||
}
|
||||
},
|
||||
|
||||
onMemoryCompacted: async (data) => {
|
||||
console.log('[DialogSession] onMemoryCompacted:', data.taskId);
|
||||
// 保存压缩数据到本地
|
||||
await historyManager.saveCompactedData(data.compactedData);
|
||||
},
|
||||
|
||||
onContextUsage: (data) => {
|
||||
console.log('[DialogSession] onContextUsage:', data.currentTokens, '/', data.maxTokens);
|
||||
callbacks.onContextUsage?.(data);
|
||||
},
|
||||
|
||||
onOpen: () => {
|
||||
console.log('[DialogSession] SSE 连接已建立');
|
||||
},
|
||||
@ -287,6 +682,25 @@ export class DialogSession {
|
||||
}
|
||||
this.isActive = false;
|
||||
userInteractionManager.cancelAll();
|
||||
|
||||
// 通知后端停止处理
|
||||
stopDialog(this.taskId).catch(err => {
|
||||
console.warn('[DialogSession] 停止对话请求失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前的消息段落(用于中止时保存)
|
||||
*/
|
||||
getSegments(): MessageSegment[] {
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取累积的文本内容
|
||||
*/
|
||||
getAccumulatedText(): string {
|
||||
return this.accumulatedText;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -309,13 +723,15 @@ class DialogManager {
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
* @param extensionPath 扩展路径
|
||||
* @param existingTaskId 可选,复用现有的 taskId(用于 Plan 模式确认后继续执行)
|
||||
*/
|
||||
createSession(extensionPath: string): DialogSession {
|
||||
createSession(extensionPath: string, existingTaskId?: string): DialogSession {
|
||||
// 如果有活跃会话,先中止
|
||||
if (this.currentSession?.active) {
|
||||
this.currentSession.abort();
|
||||
}
|
||||
this.currentSession = new DialogSession(extensionPath);
|
||||
this.currentSession = new DialogSession(extensionPath, existingTaskId);
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@ import type {
|
||||
SSEEventType,
|
||||
TextDeltaEvent,
|
||||
ToolCallRequest,
|
||||
ToolConfirmEvent,
|
||||
PlanConfirmEvent,
|
||||
AskUserEvent,
|
||||
CompleteEvent,
|
||||
ErrorEvent,
|
||||
@ -21,8 +23,14 @@ import type {
|
||||
ToolErrorEvent,
|
||||
WarningEvent,
|
||||
NotificationEvent,
|
||||
DepthUpdateEvent
|
||||
DepthUpdateEvent,
|
||||
AgentStartEvent,
|
||||
AgentProgressEvent,
|
||||
AgentCompleteEvent,
|
||||
AgentErrorEvent,
|
||||
ContextUsageEvent
|
||||
} from '../types/api';
|
||||
import type { MemoryCompactedEvent } from '../types/memory';
|
||||
|
||||
/**
|
||||
* SSE 事件回调接口
|
||||
@ -32,6 +40,10 @@ export interface SSECallbacks {
|
||||
onTextDelta?: (data: TextDeltaEvent) => void;
|
||||
/** 收到工具调用请求 */
|
||||
onToolCall?: (data: ToolCallRequest) => void;
|
||||
/** 收到工具确认请求(Ask 模式) */
|
||||
onToolConfirm?: (data: ToolConfirmEvent) => void;
|
||||
/** 收到计划确认请求(Plan 模式) */
|
||||
onPlanConfirm?: (data: PlanConfirmEvent) => void;
|
||||
/** 工具开始执行 */
|
||||
onToolStart?: (data: ToolStartEvent) => void;
|
||||
/** 工具执行完成 */
|
||||
@ -50,6 +62,18 @@ export interface SSECallbacks {
|
||||
onNotification?: (data: NotificationEvent) => void;
|
||||
/** 深度更新 */
|
||||
onDepthUpdate?: (data: DepthUpdateEvent) => void;
|
||||
/** 子智能体启动 */
|
||||
onAgentStart?: (data: AgentStartEvent) => void;
|
||||
/** 子智能体进度 */
|
||||
onAgentProgress?: (data: AgentProgressEvent) => void;
|
||||
/** 子智能体完成 */
|
||||
onAgentComplete?: (data: AgentCompleteEvent) => void;
|
||||
/** 子智能体错误 */
|
||||
onAgentError?: (data: AgentErrorEvent) => void;
|
||||
/** 记忆压缩完成 */
|
||||
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
||||
/** 上下文使用量更新 */
|
||||
onContextUsage?: (data: ContextUsageEvent) => void;
|
||||
/** 连接打开 */
|
||||
onOpen?: () => void;
|
||||
/** 连接关闭 */
|
||||
@ -124,7 +148,7 @@ export async function startStreamDialog(
|
||||
|
||||
const body = JSON.stringify(request);
|
||||
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, url=${urlString}`);
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options: http.RequestOptions = {
|
||||
@ -256,6 +280,12 @@ function dispatchEvent(
|
||||
case 'tool_call':
|
||||
callbacks.onToolCall?.(data as ToolCallRequest);
|
||||
break;
|
||||
case 'tool_confirm':
|
||||
callbacks.onToolConfirm?.(data as ToolConfirmEvent);
|
||||
break;
|
||||
case 'plan_confirm':
|
||||
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
|
||||
break;
|
||||
case 'tool_start':
|
||||
callbacks.onToolStart?.(data as ToolStartEvent);
|
||||
break;
|
||||
@ -283,6 +313,29 @@ function dispatchEvent(
|
||||
case 'depth_update':
|
||||
callbacks.onDepthUpdate?.(data as DepthUpdateEvent);
|
||||
break;
|
||||
case 'agent_start':
|
||||
callbacks.onAgentStart?.(data as AgentStartEvent);
|
||||
break;
|
||||
case 'agent_progress':
|
||||
callbacks.onAgentProgress?.(data as AgentProgressEvent);
|
||||
break;
|
||||
case 'agent_complete':
|
||||
callbacks.onAgentComplete?.(data as AgentCompleteEvent);
|
||||
break;
|
||||
case 'agent_error':
|
||||
callbacks.onAgentError?.(data as AgentErrorEvent);
|
||||
break;
|
||||
case 'memory_compacted':
|
||||
callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent);
|
||||
break;
|
||||
case 'context_usage':
|
||||
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
||||
break;
|
||||
case 'heartbeat':
|
||||
// 心跳事件:仅用于保持连接,不需要特殊处理
|
||||
// Node.js req.setTimeout 会在收到数据时自动重置计时器
|
||||
console.log('[SSE] 收到心跳');
|
||||
break;
|
||||
default:
|
||||
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import * as fs from 'fs';
|
||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
||||
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
|
||||
import { analyzeVcdFile } from '../utils/vcdParser';
|
||||
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
||||
import {
|
||||
submitToolResult,
|
||||
createSuccessResult,
|
||||
@ -20,10 +22,13 @@ import type {
|
||||
ToolName,
|
||||
FileReadArgs,
|
||||
FileWriteArgs,
|
||||
FileDeleteArgs,
|
||||
FileListArgs,
|
||||
SyntaxCheckArgs,
|
||||
SimulationArgs,
|
||||
WaveformSummaryArgs
|
||||
WaveformSummaryArgs,
|
||||
KnowledgeSaveArgs,
|
||||
KnowledgeLoadArgs
|
||||
} from '../types/api';
|
||||
|
||||
/**
|
||||
@ -61,6 +66,9 @@ export async function executeToolCall(
|
||||
case 'file_write':
|
||||
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
||||
break;
|
||||
case 'file_delete':
|
||||
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
|
||||
break;
|
||||
case 'file_list':
|
||||
resultText = await executeFileList(args as unknown as FileListArgs);
|
||||
break;
|
||||
@ -73,6 +81,15 @@ export async function executeToolCall(
|
||||
case 'waveform_summary':
|
||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
||||
break;
|
||||
case 'waveform_trace':
|
||||
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
|
||||
break;
|
||||
case 'knowledge_save':
|
||||
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
||||
break;
|
||||
case 'knowledge_load':
|
||||
resultText = await executeKnowledgeLoad();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`未知工具: ${toolName}`);
|
||||
}
|
||||
@ -105,9 +122,59 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
||||
*/
|
||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 file_delete 工具
|
||||
* 删除指定路径的文件
|
||||
*/
|
||||
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||
const filePath = args.path;
|
||||
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 解析文件路径(支持相对路径和绝对路径)
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(workspacePath, filePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
throw new Error(`文件不存在: ${filePath}`);
|
||||
}
|
||||
|
||||
// 检查是否为文件(不允许删除目录)
|
||||
const stat = fs.statSync(absolutePath);
|
||||
if (stat.isDirectory()) {
|
||||
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
fs.unlinkSync(absolutePath);
|
||||
|
||||
// Verilog 文件添加知识图谱提示
|
||||
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
|
||||
if (isVerilogFile) {
|
||||
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
||||
}
|
||||
|
||||
return `文件已删除: ${filePath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 file_list 工具
|
||||
*/
|
||||
@ -238,12 +305,95 @@ async function executeSimulation(
|
||||
|
||||
/**
|
||||
* 执行 waveform_summary 工具
|
||||
* TODO: 实现 VCD 波形分析
|
||||
* 解析 VCD 文件并返回波形摘要
|
||||
*/
|
||||
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
|
||||
// TODO: 使用 vcdrom/vcd-stream 解析 VCD 文件
|
||||
// 目前返回一个占位响应
|
||||
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`;
|
||||
const { vcdPath, signals, checkpoints } = args;
|
||||
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 解析 VCD 文件路径(支持相对路径)
|
||||
const absolutePath = path.isAbsolute(vcdPath)
|
||||
? vcdPath
|
||||
: path.join(workspacePath, vcdPath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
throw new Error(`VCD 文件不存在: ${vcdPath}`);
|
||||
}
|
||||
|
||||
// 解析检查点时间
|
||||
const checkpoint = checkpoints ? parseInt(checkpoints, 10) : undefined;
|
||||
|
||||
// 调用 VCD 解析器
|
||||
const result = analyzeVcdFile(absolutePath, signals, checkpoint);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 knowledge_save 工具
|
||||
* 保存知识图谱到 .iccoder/knowledge.json
|
||||
*/
|
||||
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||
const workspaceFolder = getWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
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'));
|
||||
|
||||
return `知识图谱已保存: .iccoder/knowledge.json`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 knowledge_load 工具
|
||||
* 从 .iccoder/knowledge.json 加载知识图谱
|
||||
*/
|
||||
async function executeKnowledgeLoad(): Promise<string> {
|
||||
const workspaceFolder = getWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
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');
|
||||
return content;
|
||||
} catch (error) {
|
||||
// 文件不存在:返回空图谱
|
||||
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
||||
// 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段)
|
||||
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||
return activeFolder ?? folders[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import { submitAnswer } from './apiClient';
|
||||
import { submitAnswer, submitToolConfirm } from './apiClient';
|
||||
import type { AskUserEvent, AnswerRequest } from '../types/api';
|
||||
|
||||
/**
|
||||
@ -32,6 +32,13 @@ export class UserInteractionManager {
|
||||
this.webviewPanel = panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebView 面板
|
||||
*/
|
||||
getWebviewPanel(): vscode.WebviewPanel | null {
|
||||
return this.webviewPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ask_user 事件
|
||||
* @param event ask_user 事件数据
|
||||
@ -60,13 +67,13 @@ export class UserInteractionManager {
|
||||
reject
|
||||
});
|
||||
|
||||
// 设置超时(5分钟)
|
||||
// 设置超时(2小时)
|
||||
setTimeout(() => {
|
||||
if (this.pendingQuestions.has(askId)) {
|
||||
this.pendingQuestions.delete(askId);
|
||||
reject(new Error('用户回答超时'));
|
||||
}
|
||||
}, 300000);
|
||||
}, 7200000);
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,21 +114,46 @@ export class UserInteractionManager {
|
||||
taskId: string,
|
||||
answer: string
|
||||
): Promise<void> {
|
||||
const request: AnswerRequest = {
|
||||
askId,
|
||||
taskId,
|
||||
customInput: answer
|
||||
};
|
||||
// 检查是否是工具确认类型的问题
|
||||
if (askId.startsWith('tool_confirm_')) {
|
||||
// 提取 confirmId
|
||||
const confirmId = parseInt(askId.replace('tool_confirm_', ''));
|
||||
const approved = answer === '确认执行';
|
||||
|
||||
try {
|
||||
const response = await submitAnswer(request);
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || '提交回答失败');
|
||||
console.log(`[UserInteraction] 提交工具确认: confirmId=${confirmId}, approved=${approved}`);
|
||||
|
||||
try {
|
||||
const response = await submitToolConfirm({
|
||||
confirmId,
|
||||
taskId,
|
||||
approved
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || '提交工具确认失败');
|
||||
}
|
||||
console.log(`[UserInteraction] 工具确认已提交: confirmId=${confirmId}`);
|
||||
} catch (error) {
|
||||
console.error(`[UserInteraction] 提交工具确认失败: confirmId=${confirmId}`, error);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// 普通问题回答
|
||||
const request: AnswerRequest = {
|
||||
askId,
|
||||
taskId,
|
||||
customInput: answer
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await submitAnswer(request);
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || '提交回答失败');
|
||||
}
|
||||
console.log(`[UserInteraction] 回答已提交: askId=${askId}`);
|
||||
} catch (error) {
|
||||
console.error(`[UserInteraction] 提交回答失败: askId=${askId}`, error);
|
||||
throw error;
|
||||
}
|
||||
console.log(`[UserInteraction] 回答已提交: askId=${askId}`);
|
||||
} catch (error) {
|
||||
console.error(`[UserInteraction] 提交回答失败: askId=${askId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
157
src/types/api.ts
157
src/types/api.ts
@ -3,8 +3,19 @@
|
||||
* 对应后端 IC Coder Backend 的接口格式
|
||||
*/
|
||||
|
||||
import { CompactedMemory, CompactedMessage } from './memory';
|
||||
|
||||
// ============== 对话请求/响应 ==============
|
||||
|
||||
/**
|
||||
* 运行模式类型
|
||||
* - plan: 只读模式,只能查询分析
|
||||
* - ask: 逐个确认,每个写操作需确认
|
||||
* - agent: 智能体自主(默认)
|
||||
* - auto: 完全自动
|
||||
*/
|
||||
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
|
||||
|
||||
/**
|
||||
* 对话请求
|
||||
* POST /api/dialog/stream
|
||||
@ -16,8 +27,14 @@ export interface DialogRequest {
|
||||
message: string;
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 工具模式 */
|
||||
toolMode: 'ASK' | 'AGENT';
|
||||
/** 运行模式 */
|
||||
mode: RunMode;
|
||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||
compactedData?: CompactedMemory;
|
||||
/** 压缩后产生的新消息 */
|
||||
newMessages?: CompactedMessage[];
|
||||
/** 知识图谱数据(JSON 字符串,用于恢复知识图谱) */
|
||||
knowledgeData?: string;
|
||||
}
|
||||
|
||||
// ============== SSE 事件类型 ==============
|
||||
@ -26,10 +43,18 @@ export interface DialogRequest {
|
||||
export type SSEEventType =
|
||||
| 'text_delta' // 文本增量
|
||||
| 'tool_call' // 客户端工具调用请求
|
||||
| 'tool_confirm' // 工具确认请求(Ask 模式)
|
||||
| 'plan_confirm' // 计划确认请求(Plan 模式)
|
||||
| 'tool_start' // 工具开始执行
|
||||
| 'tool_complete' // 工具执行完成
|
||||
| 'tool_error' // 工具执行错误
|
||||
| 'ask_user' // 向用户提问
|
||||
| 'agent_start' // 子智能体启动
|
||||
| 'agent_progress' // 子智能体进度
|
||||
| 'agent_complete' // 子智能体完成
|
||||
| 'agent_error' // 子智能体错误
|
||||
| 'memory_compacted' // 记忆压缩完成
|
||||
| 'context_usage' // 上下文使用量
|
||||
| 'complete' // 对话完成
|
||||
| 'error' // 错误
|
||||
| 'warning' // 警告
|
||||
@ -59,6 +84,32 @@ export interface ToolErrorEvent {
|
||||
error: string;
|
||||
}
|
||||
|
||||
/** tool_confirm 事件数据(Ask 模式确认请求) */
|
||||
export interface ToolConfirmEvent {
|
||||
/** 确认ID,用于匹配响应 */
|
||||
confirmId: number;
|
||||
/** 工具名称 */
|
||||
toolName: string;
|
||||
/** 工具输入参数 */
|
||||
toolInput: Record<string, unknown>;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_confirm 事件数据(Plan 模式计划确认) */
|
||||
export interface PlanConfirmEvent {
|
||||
/** 确认ID */
|
||||
confirmId: number;
|
||||
/** 计划标题 */
|
||||
title: string;
|
||||
/** 执行步骤列表 */
|
||||
steps: string[];
|
||||
/** 计划摘要 */
|
||||
summary: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** ask_user 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
@ -92,6 +143,52 @@ export interface DepthUpdateEvent {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
// ============== 智能体事件类型 ==============
|
||||
|
||||
/** agent_start 事件数据 */
|
||||
export interface AgentStartEvent {
|
||||
agentId: string;
|
||||
agentType: string;
|
||||
agentName: string;
|
||||
instruction: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** agent_progress 事件数据 */
|
||||
export interface AgentProgressEvent {
|
||||
agentId: string;
|
||||
step: number;
|
||||
toolName: string;
|
||||
toolInput?: unknown;
|
||||
toolResult?: string;
|
||||
status: 'running' | 'completed' | 'error';
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** agent_complete 事件数据 */
|
||||
export interface AgentCompleteEvent {
|
||||
agentId: string;
|
||||
agentType: string;
|
||||
summary: string;
|
||||
stats: Record<string, unknown>;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** agent_error 事件数据 */
|
||||
export interface AgentErrorEvent {
|
||||
agentId: string;
|
||||
agentType: string;
|
||||
error: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** context_usage 事件数据 */
|
||||
export interface ContextUsageEvent {
|
||||
currentTokens: number;
|
||||
maxTokens: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
// ============== 工具调用协议 (MCP 格式) ==============
|
||||
|
||||
/**
|
||||
@ -186,16 +283,35 @@ export interface ToolResultResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ============== 工具确认响应 ==============
|
||||
|
||||
/**
|
||||
* 工具确认响应请求
|
||||
* POST /api/tool/confirm
|
||||
*/
|
||||
export interface ToolConfirmResponse {
|
||||
/** 确认ID,与 ToolConfirmEvent.confirmId 对应 */
|
||||
confirmId: number;
|
||||
/** 任务ID */
|
||||
taskId: string;
|
||||
/** 是否批准执行 */
|
||||
approved: boolean;
|
||||
}
|
||||
|
||||
// ============== 辅助类型 ==============
|
||||
|
||||
/** 后端工具名称 */
|
||||
export type ToolName =
|
||||
| 'file_read'
|
||||
| 'file_write'
|
||||
| 'file_delete'
|
||||
| 'file_list'
|
||||
| 'syntax_check'
|
||||
| 'simulation'
|
||||
| 'waveform_summary';
|
||||
| 'waveform_summary'
|
||||
| 'waveform_trace'
|
||||
| 'knowledge_save'
|
||||
| 'knowledge_load';
|
||||
|
||||
/** file_read 工具参数 */
|
||||
export interface FileReadArgs {
|
||||
@ -208,6 +324,12 @@ export interface FileWriteArgs {
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** file_delete 工具参数 */
|
||||
export interface FileDeleteArgs {
|
||||
/** 要删除的文件路径 */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/** file_list 工具参数 */
|
||||
export interface FileListArgs {
|
||||
path?: string;
|
||||
@ -233,11 +355,38 @@ export interface WaveformSummaryArgs {
|
||||
checkpoints?: string;
|
||||
}
|
||||
|
||||
/** waveform_trace 工具参数 */
|
||||
export interface WaveformTraceArgs {
|
||||
/** Verilog 源文件路径(相对于项目根目录) */
|
||||
verilogPath: string;
|
||||
/** VCD 波形文件路径(相对于项目根目录) */
|
||||
vcdPath: string;
|
||||
/** 仿真工具的输出字符串(包含 mismatch 信息) */
|
||||
simOutput: string;
|
||||
/** BFS 回溯层数,默认 2 */
|
||||
traceLevel?: number;
|
||||
}
|
||||
|
||||
/** knowledge_save 工具参数 */
|
||||
export interface KnowledgeSaveArgs {
|
||||
/** 知识图谱 JSON 数据 */
|
||||
data: string;
|
||||
}
|
||||
|
||||
/** knowledge_load 工具参数 */
|
||||
export interface KnowledgeLoadArgs {
|
||||
// 无参数,直接读取 .iccoder/knowledge.json
|
||||
}
|
||||
|
||||
/** 工具参数联合类型 */
|
||||
export type ToolArgs =
|
||||
| FileReadArgs
|
||||
| FileWriteArgs
|
||||
| FileDeleteArgs
|
||||
| FileListArgs
|
||||
| SyntaxCheckArgs
|
||||
| SimulationArgs
|
||||
| WaveformSummaryArgs;
|
||||
| WaveformSummaryArgs
|
||||
| WaveformTraceArgs
|
||||
| KnowledgeSaveArgs
|
||||
| KnowledgeLoadArgs;
|
||||
|
||||
@ -5,7 +5,8 @@ export enum MessageType {
|
||||
SYSTEM = "SYSTEM",
|
||||
USER = "USER",
|
||||
AI = "AI",
|
||||
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT"
|
||||
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT",
|
||||
COMPACTION_SUMMARY = "COMPACTION_SUMMARY" // 压缩摘要
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,10 +70,22 @@ export interface ToolExecutionResultMessage extends BaseMessage {
|
||||
text: string; // JSON字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩摘要消息
|
||||
*/
|
||||
export interface CompactionSummaryMessage extends BaseMessage {
|
||||
type: MessageType.COMPACTION_SUMMARY;
|
||||
summary: string;
|
||||
version: number;
|
||||
compactedAt: string;
|
||||
originalMessageCount: number;
|
||||
compactedMessageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 联合消息类型
|
||||
*/
|
||||
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage;
|
||||
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage | CompactionSummaryMessage;
|
||||
|
||||
/**
|
||||
* 对话轮次元数据
|
||||
|
||||
42
src/types/memory.ts
Normal file
42
src/types/memory.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 压缩记忆相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 压缩后的记忆数据
|
||||
*/
|
||||
export interface CompactedMemory {
|
||||
taskId: string;
|
||||
version: number;
|
||||
compactedAt: string;
|
||||
summary: string;
|
||||
recentMessages: CompactedMessage[];
|
||||
originalMessageCount: number;
|
||||
compactedMessageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩消息格式
|
||||
*/
|
||||
export interface CompactedMessage {
|
||||
type: 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT';
|
||||
content: string;
|
||||
toolCall?: ToolCallInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用信息
|
||||
*/
|
||||
export interface ToolCallInfo {
|
||||
toolName: string;
|
||||
toolInput: string;
|
||||
toolOutput?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记忆压缩 SSE 事件
|
||||
*/
|
||||
export interface MemoryCompactedEvent {
|
||||
taskId: string;
|
||||
compactedData: CompactedMemory;
|
||||
}
|
||||
@ -9,8 +9,10 @@ import {
|
||||
UserMessage,
|
||||
AiMessage,
|
||||
SystemMessage,
|
||||
ToolExecutionResultMessage
|
||||
ToolExecutionResultMessage,
|
||||
CompactionSummaryMessage
|
||||
} from '../types/chatHistory';
|
||||
import { CompactedMemory, CompactedMessage } from '../types/memory';
|
||||
|
||||
/**
|
||||
* 会话历史管理器
|
||||
@ -23,6 +25,8 @@ export class ChatHistoryManager {
|
||||
private currentProjectPath: string | null = null;
|
||||
// 存储每个面板的任务信息(taskId 和 projectPath)
|
||||
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
|
||||
// 追踪压缩后产生的新消息
|
||||
private newMessagesSinceCompaction: CompactedMessage[] = [];
|
||||
|
||||
private constructor() {
|
||||
// 设置存储路径: ~/.iccoder
|
||||
@ -690,4 +694,203 @@ export class ChatHistoryManager {
|
||||
hasMore: end < total
|
||||
};
|
||||
}
|
||||
|
||||
// ========== 压缩数据相关方法 ==========
|
||||
|
||||
/**
|
||||
* 保存压缩数据(存入 conversation.json 作为压缩摘要消息)
|
||||
*/
|
||||
public async saveCompactedData(compacted: CompactedMemory): Promise<void> {
|
||||
// 尝试从多个来源获取 projectPath
|
||||
let projectPath = this.currentProjectPath;
|
||||
|
||||
if (!projectPath) {
|
||||
for (const [, taskInfo] of this.panelTaskMap) {
|
||||
if (taskInfo.taskId === compacted.taskId) {
|
||||
projectPath = taskInfo.projectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取现有对话历史
|
||||
const taskDir = this.getTaskDir(projectPath, compacted.taskId);
|
||||
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||
let messages: ChatMessage[] = [];
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
messages = JSON.parse(Buffer.from(content).toString('utf-8'));
|
||||
} catch {
|
||||
// 文件不存在,使用空数组
|
||||
}
|
||||
|
||||
// 创建压缩摘要消息
|
||||
const summaryMessage: CompactionSummaryMessage = {
|
||||
type: MessageType.COMPACTION_SUMMARY,
|
||||
summary: compacted.summary,
|
||||
version: compacted.version,
|
||||
compactedAt: compacted.compactedAt,
|
||||
originalMessageCount: compacted.originalMessageCount,
|
||||
compactedMessageCount: compacted.compactedMessageCount
|
||||
};
|
||||
|
||||
// 添加到对话历史
|
||||
messages.push(summaryMessage);
|
||||
|
||||
// 保存
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = Buffer.from(JSON.stringify(messages, null, 2), 'utf-8');
|
||||
await vscode.workspace.fs.writeFile(uri, content);
|
||||
|
||||
// 重置新消息追踪
|
||||
this.newMessagesSinceCompaction = [];
|
||||
|
||||
console.log(`[ChatHistoryManager] 压缩摘要已保存到 conversation.json: taskId=${compacted.taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载压缩数据(从 conversation.json 构建)
|
||||
*/
|
||||
public async loadCompactedData(taskId: string): Promise<CompactedMemory | null> {
|
||||
// 尝试从多个来源获取 projectPath
|
||||
let projectPath = this.currentProjectPath;
|
||||
|
||||
if (!projectPath) {
|
||||
for (const [, taskInfo] of this.panelTaskMap) {
|
||||
if (taskInfo.taskId === taskId) {
|
||||
projectPath = taskInfo.projectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.log('[ChatHistoryManager] loadCompactedData: projectPath 为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取 conversation.json
|
||||
const taskDir = this.getTaskDir(projectPath, taskId);
|
||||
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
const messages: ChatMessage[] = JSON.parse(Buffer.from(content).toString('utf-8'));
|
||||
|
||||
if (messages.length === 0) {
|
||||
console.log('[ChatHistoryManager] conversation.json 为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从 conversation.json 构建 CompactedMemory
|
||||
return this.buildCompactedMemoryFromConversation(taskId, messages);
|
||||
} catch {
|
||||
console.log('[ChatHistoryManager] conversation.json 不存在:', conversationPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 conversation.json 构建 CompactedMemory
|
||||
*/
|
||||
private buildCompactedMemoryFromConversation(taskId: string, messages: ChatMessage[]): CompactedMemory {
|
||||
// 查找最后一个压缩摘要消息
|
||||
let lastSummary: CompactionSummaryMessage | null = null;
|
||||
let summaryIndex = -1;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||
lastSummary = messages[i] as CompactionSummaryMessage;
|
||||
summaryIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取摘要后的消息(或全部消息)
|
||||
const recentMessages = summaryIndex >= 0
|
||||
? messages.slice(summaryIndex + 1)
|
||||
: messages;
|
||||
|
||||
// 转换为 CompactedMessage 格式
|
||||
const compactedMessages: CompactedMessage[] = recentMessages.map(msg => ({
|
||||
type: this.mapMessageType(msg.type),
|
||||
content: this.extractMessageContent(msg)
|
||||
}));
|
||||
|
||||
return {
|
||||
taskId,
|
||||
version: lastSummary?.version || Date.now(),
|
||||
compactedAt: lastSummary?.compactedAt || new Date().toISOString(),
|
||||
summary: lastSummary?.summary || '',
|
||||
recentMessages: compactedMessages,
|
||||
originalMessageCount: messages.length,
|
||||
compactedMessageCount: compactedMessages.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射消息类型
|
||||
*/
|
||||
private mapMessageType(type: MessageType): 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT' {
|
||||
switch (type) {
|
||||
case MessageType.USER: return 'USER';
|
||||
case MessageType.AI: return 'AI';
|
||||
case MessageType.SYSTEM: return 'SYSTEM';
|
||||
case MessageType.TOOL_EXECUTION_RESULT: return 'TOOL_RESULT';
|
||||
default: return 'USER';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取消息内容
|
||||
*/
|
||||
private extractMessageContent(msg: ChatMessage): string {
|
||||
switch (msg.type) {
|
||||
case MessageType.USER:
|
||||
return (msg as UserMessage).contents?.[0]?.text || '';
|
||||
case MessageType.AI:
|
||||
return (msg as AiMessage).text || '';
|
||||
case MessageType.SYSTEM:
|
||||
return (msg as SystemMessage).text || '';
|
||||
case MessageType.TOOL_EXECUTION_RESULT:
|
||||
return (msg as ToolExecutionResultMessage).text || '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取压缩后产生的新消息
|
||||
*/
|
||||
public getNewMessagesSinceCompaction(): CompactedMessage[] {
|
||||
return this.newMessagesSinceCompaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(用户消息)
|
||||
*/
|
||||
public trackUserMessage(text: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'USER',
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(AI消息)
|
||||
*/
|
||||
public trackAiMessage(text: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'AI',
|
||||
content: text
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,9 @@ export async function createFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +30,7 @@ export async function createFile(
|
||||
throw new Error(`文件已存在: ${absolutePath}`);
|
||||
} catch (error: any) {
|
||||
// 如果文件不存在,继续创建
|
||||
if (error.code !== 'FileNotFound') {
|
||||
if (error.code !== "FileNotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -65,7 +67,9 @@ export async function createOrOverwriteFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +103,9 @@ export async function createDirectory(dirPath: string): Promise<void> {
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的目录");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建目录了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +121,7 @@ export async function createDirectory(dirPath: string): Promise<void> {
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 如果目录不存在,继续创建
|
||||
if (error.code !== 'FileNotFound') {
|
||||
if (error.code !== "FileNotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -161,7 +167,9 @@ export async function deleteFile(filePath: string): Promise<void> {
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法删除相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您删除文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +205,9 @@ export async function updateFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,7 +246,9 @@ export async function appendToFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法追加相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您追加文件内容了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,7 +286,9 @@ export async function replaceFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,14 +305,17 @@ export async function replaceFile(
|
||||
|
||||
// 转义特殊字符,将字符串作为字面量处理
|
||||
const escapeRegExp = (str: string) => {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
// 替换内容 - 如果是字符串,先转义特殊字符
|
||||
let newContent: string;
|
||||
if (typeof searchValue === 'string') {
|
||||
if (typeof searchValue === "string") {
|
||||
const escapedSearch = escapeRegExp(searchValue);
|
||||
newContent = fileContent.replace(new RegExp(escapedSearch, "g"), replaceValue);
|
||||
newContent = fileContent.replace(
|
||||
new RegExp(escapedSearch, "g"),
|
||||
replaceValue
|
||||
);
|
||||
} else {
|
||||
newContent = fileContent.replace(searchValue, replaceValue);
|
||||
}
|
||||
@ -330,7 +347,9 @@ export async function insertAtLine(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -382,7 +401,9 @@ export async function renameFile(
|
||||
absoluteNewPath = path.join(workspaceRoot, newPath);
|
||||
}
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法重命名相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您重命名文件了"
|
||||
);
|
||||
}
|
||||
|
||||
const oldUri = vscode.Uri.file(absoluteOldPath);
|
||||
@ -401,10 +422,13 @@ export async function renameFile(
|
||||
throw new Error(`目标文件已存在: ${absoluteNewPath}`);
|
||||
} catch (error: any) {
|
||||
// 如果文件不存在,继续重命名
|
||||
if (error.code !== 'FileNotFound' && !error.message.includes('目标文件已存在')) {
|
||||
if (
|
||||
error.code !== "FileNotFound" &&
|
||||
!error.message.includes("目标文件已存在")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (error.message.includes('目标文件已存在')) {
|
||||
if (error.message.includes("目标文件已存在")) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,19 +19,46 @@ import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
import { healthCheck } from "../services/apiClient";
|
||||
|
||||
import type { RunMode } from "../types/api";
|
||||
|
||||
/** 是否使用后端服务(可通过配置控制) */
|
||||
let useBackendService = true;
|
||||
|
||||
/** 当前对话会话 */
|
||||
let currentSession: DialogSession | null = null;
|
||||
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||
let pendingPlanExecution: {
|
||||
panel: vscode.WebviewPanel;
|
||||
planTitle: string;
|
||||
extensionPath: string;
|
||||
taskId: string; // 保存 taskId 以便复用
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* 设置待执行的计划(由 ICHelperPanel 调用)
|
||||
*/
|
||||
export function setPendingPlanExecution(
|
||||
panel: vscode.WebviewPanel,
|
||||
planTitle: string,
|
||||
extensionPath: string,
|
||||
taskId: string
|
||||
): void {
|
||||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
||||
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户消息
|
||||
*/
|
||||
export async function handleUserMessage(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string,
|
||||
extensionPath?: string
|
||||
extensionPath?: string,
|
||||
mode?: RunMode
|
||||
) {
|
||||
console.log("收到用户消息:", text);
|
||||
|
||||
@ -63,32 +90,31 @@ export async function handleUserMessage(
|
||||
// 尝试使用后端服务
|
||||
if (useBackendService && extensionPath) {
|
||||
try {
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath);
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("后端服务不可用,回退到本地模式:", error);
|
||||
// 后端不可用时,使用本地模拟回复
|
||||
console.error("后端服务不可用:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "后端服务不可用",
|
||||
type: "error",
|
||||
});
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 本地模拟回复(后端不可用时的 fallback)
|
||||
console.log("使用本地模拟回复");
|
||||
const reply = getMockReply(text);
|
||||
|
||||
// 记录AI回复到历史(允许失败)
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
await historyManager.addAiMessage(reply);
|
||||
} catch (error) {
|
||||
console.warn("记录AI回复历史失败:", error);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: reply,
|
||||
});
|
||||
}, 500);
|
||||
// 如果没有 extensionPath,显示错误
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "无法处理消息:缺少必要参数",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,15 +123,24 @@ export async function handleUserMessage(
|
||||
async function handleUserMessageWithBackend(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
mode?: RunMode,
|
||||
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
): Promise<void> {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
|
||||
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
||||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(extensionPath);
|
||||
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
|
||||
}
|
||||
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
|
||||
// 显示状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
@ -114,94 +149,146 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
currentSession!.sendMessage(text, {
|
||||
onText: (fullText, isStreaming) => {
|
||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||
currentSession!.sendMessage(
|
||||
text,
|
||||
{
|
||||
onText: (fullText, isStreaming) => {
|
||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||
},
|
||||
|
||||
onSegmentUpdate: (segments) => {
|
||||
// 实时发送段落更新,按后端返回顺序展示
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
});
|
||||
},
|
||||
|
||||
onToolStart: (toolName) => {
|
||||
// 更新状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: `正在执行 ${toolName}...`,
|
||||
type: "working",
|
||||
});
|
||||
},
|
||||
|
||||
onToolComplete: (toolName, result) => {
|
||||
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onToolError: (toolName, error) => {
|
||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onQuestion: (askId, question, options) => {
|
||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "等待用户回答...",
|
||||
type: "working",
|
||||
});
|
||||
},
|
||||
|
||||
onComplete: async (segments) => {
|
||||
// 隐藏状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "hideStatus",
|
||||
});
|
||||
|
||||
// 最后一次发送完整的段落
|
||||
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
||||
|
||||
const result = await panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
isComplete: true,
|
||||
});
|
||||
console.log("[MessageHandler] postMessage 返回值:", result);
|
||||
|
||||
// 保存完整的 segments 到历史记录
|
||||
try {
|
||||
// 将完整的 segments 保存到一条 AI 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
const textContent = segments
|
||||
.filter((s) => s.type === "text" && s.content)
|
||||
.map((s) => s.content)
|
||||
.join("\n");
|
||||
|
||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||
} catch (error) {
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||||
if (pendingPlanExecution) {
|
||||
const {
|
||||
panel: execPanel,
|
||||
planTitle,
|
||||
extensionPath: execPath,
|
||||
taskId: reuseTaskId,
|
||||
} = pendingPlanExecution;
|
||||
pendingPlanExecution = null;
|
||||
console.log(
|
||||
"[MessageHandler] 自动执行计划:",
|
||||
planTitle,
|
||||
"复用 taskId:",
|
||||
reuseTaskId
|
||||
);
|
||||
|
||||
// 延迟一小段时间确保当前对话完全结束
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||||
await handleUserMessageWithBackend(
|
||||
execPanel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
execPath,
|
||||
"agent",
|
||||
reuseTaskId // 复用 Plan 模式的 taskId
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[MessageHandler] 自动执行计划失败:", err);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
|
||||
onError: (message) => {
|
||||
panel.webview.postMessage({
|
||||
command: "hideLoading",
|
||||
});
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 错误: ${message}`,
|
||||
});
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
reject(new Error(message));
|
||||
},
|
||||
|
||||
onNotification: (message) => {
|
||||
vscode.window.showInformationMessage(message);
|
||||
},
|
||||
|
||||
onContextUsage: (data) => {
|
||||
// 发送上下文使用量到 WebView
|
||||
panel.webview.postMessage({
|
||||
command: "contextUsage",
|
||||
currentTokens: data.currentTokens,
|
||||
maxTokens: data.maxTokens,
|
||||
percentage: data.percentage,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
onSegmentUpdate: (segments) => {
|
||||
// 实时发送段落更新,按后端返回顺序展示
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
});
|
||||
},
|
||||
|
||||
onToolStart: (toolName) => {
|
||||
// 更新状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: `正在执行 ${toolName}...`,
|
||||
type: "working",
|
||||
});
|
||||
},
|
||||
|
||||
onToolComplete: (toolName, result) => {
|
||||
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onToolError: (toolName, error) => {
|
||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onQuestion: (askId, question, options) => {
|
||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "等待用户回答...",
|
||||
type: "working",
|
||||
});
|
||||
},
|
||||
|
||||
onComplete: async (segments) => {
|
||||
// 隐藏状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "hideStatus",
|
||||
});
|
||||
|
||||
// 最后一次发送完整的段落
|
||||
console.log('[MessageHandler] 对话完成, 段落数:', segments.length);
|
||||
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments));
|
||||
|
||||
const result = await panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
isComplete: true,
|
||||
});
|
||||
console.log('[MessageHandler] postMessage 返回值:', result);
|
||||
|
||||
// 保存完整的 segments 到历史记录
|
||||
try {
|
||||
// 将完整的 segments 保存到一条 AI 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
.map(s => s.content)
|
||||
.join('\n');
|
||||
|
||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||
} catch (error) {
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
|
||||
onError: (message) => {
|
||||
panel.webview.postMessage({
|
||||
command: "hideLoading",
|
||||
});
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 错误: ${message}`,
|
||||
});
|
||||
reject(new Error(message));
|
||||
},
|
||||
|
||||
onNotification: (message) => {
|
||||
vscode.window.showInformationMessage(message);
|
||||
},
|
||||
});
|
||||
mode
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -221,11 +308,116 @@ export async function handleUserAnswer(
|
||||
/**
|
||||
* 中止当前对话
|
||||
*/
|
||||
export function abortCurrentDialog(): void {
|
||||
export async function abortCurrentDialog(): Promise<void> {
|
||||
if (currentSession) {
|
||||
// 保存当前已有的对话内容
|
||||
const segments = currentSession.getSegments();
|
||||
if (segments && segments.length > 0) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const textContent = segments
|
||||
.filter((s) => s.type === "text" && s.content)
|
||||
.map((s) => s.content)
|
||||
.join("\n");
|
||||
|
||||
// 添加中止标记
|
||||
const abortedContent = textContent + "\n\n[对话已被用户中止]";
|
||||
await historyManager.addAiMessage(abortedContent, undefined, segments);
|
||||
console.log("[MessageHandler] 已保存中止前的对话内容");
|
||||
} catch (error) {
|
||||
console.warn("[MessageHandler] 保存中止对话失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知 WebView 重置分段消息容器
|
||||
const panel = userInteractionManager.getWebviewPanel();
|
||||
if (panel) {
|
||||
panel.webview.postMessage({ command: "resetSegmentedMessage" });
|
||||
console.log("[MessageHandler] 已发送重置分段消息命令");
|
||||
}
|
||||
|
||||
dialogManager.abortCurrentSession();
|
||||
currentSession = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话的 taskId
|
||||
*/
|
||||
export function getCurrentTaskId(): string | null {
|
||||
return currentSession?.getTaskId() || lastTaskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最后的 taskId(加载历史会话时调用)
|
||||
*/
|
||||
export function setLastTaskId(taskId: string): void {
|
||||
lastTaskId = taskId;
|
||||
console.log("[MessageHandler] 设置 lastTaskId:", taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理计划操作(Plan 模式)
|
||||
* @param panel WebView 面板
|
||||
* @param action 操作类型:confirm/modify/cancel
|
||||
* @param planTitle 计划标题
|
||||
* @param extensionPath 扩展路径
|
||||
*/
|
||||
export async function handlePlanAction(
|
||||
panel: vscode.WebviewPanel,
|
||||
action: string,
|
||||
planTitle: string,
|
||||
extensionPath: string
|
||||
): Promise<void> {
|
||||
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle);
|
||||
|
||||
switch (action) {
|
||||
case "confirm":
|
||||
// 确认执行:切换到 Agent 模式并发送执行消息
|
||||
panel.webview.postMessage({
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 发送执行消息
|
||||
await handleUserMessage(
|
||||
panel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
extensionPath,
|
||||
"agent"
|
||||
);
|
||||
break;
|
||||
|
||||
case "modify":
|
||||
// 修改计划:提示用户输入修改建议
|
||||
const modification = await vscode.window.showInputBox({
|
||||
prompt: "请输入您对计划的修改建议",
|
||||
placeHolder: "例如:第2步需要先检查文件是否存在...",
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (modification) {
|
||||
await handleUserMessage(
|
||||
panel,
|
||||
`请根据以下建议修改计划:${modification}`,
|
||||
extensionPath,
|
||||
"plan"
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "cancel":
|
||||
// 取消计划:通知用户
|
||||
panel.webview.postMessage({
|
||||
command: "addMessage",
|
||||
text: "计划已取消。",
|
||||
sender: "bot",
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("[handlePlanAction] 未知操作:", action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件操作命令
|
||||
*/
|
||||
@ -261,7 +453,9 @@ function parseFileOperation(text: string): {
|
||||
}
|
||||
|
||||
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
||||
const renameMatch = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/);
|
||||
const renameMatch = lowerText.match(
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
|
||||
);
|
||||
if (renameMatch) {
|
||||
const oldPath = renameMatch[1].trim();
|
||||
const newPath = renameMatch[2].trim();
|
||||
@ -276,7 +470,9 @@ function parseFileOperation(text: string): {
|
||||
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
|
||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
||||
const replaceMatch1 = lowerText.match(/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
|
||||
const replaceMatch1 = lowerText.match(
|
||||
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||
);
|
||||
if (replaceMatch1) {
|
||||
const filePath = replaceMatch1[1].trim();
|
||||
const searchText = replaceMatch1[2].trim();
|
||||
@ -290,7 +486,9 @@ function parseFileOperation(text: string): {
|
||||
}
|
||||
|
||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||
const replaceMatch2 = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
|
||||
const replaceMatch2 = lowerText.match(
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||
);
|
||||
if (replaceMatch2) {
|
||||
const filePath = replaceMatch2[1].trim();
|
||||
const searchText = replaceMatch2[2].trim();
|
||||
@ -618,41 +816,6 @@ export async function handleReplaceInFile(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模拟回复
|
||||
*/
|
||||
function getMockReply(question: string): string {
|
||||
const replies = [
|
||||
`已收到您的问题:"${question}"
|
||||
|
||||
这是一个演示版本,实际需要连接AI服务。
|
||||
|
||||
示例回复:这是一个计数器模板:
|
||||
\`\`\`verilog
|
||||
module counter (
|
||||
input clk,
|
||||
input rst_n,
|
||||
output reg [3:0] count
|
||||
);
|
||||
always @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) count <= 0;
|
||||
else count <= count + 1;
|
||||
end
|
||||
endmodule
|
||||
\`\`\``,
|
||||
|
||||
`感谢提问!关于"${question}",在真实版本中我会:
|
||||
1. 分析您的代码上下文
|
||||
2. 提供优化建议
|
||||
3. 生成完整代码
|
||||
4. 解释设计原理
|
||||
|
||||
当前是演示版,请点击侧边栏按钮快速生成代码。`,
|
||||
];
|
||||
|
||||
return replies[Math.floor(Math.random() * replies.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将代码插入到编辑器
|
||||
*/
|
||||
@ -745,7 +908,8 @@ async function handleVCDGeneration(
|
||||
|
||||
if (!projectCheck.hasTestbench) {
|
||||
errorMsg += "• ❌ 缺少 testbench 文件\n";
|
||||
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||||
errorMsg +=
|
||||
"\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||||
} else {
|
||||
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
|
||||
}
|
||||
@ -789,9 +953,7 @@ async function handleVCDGeneration(
|
||||
fileName: fileName,
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`VCD 文件生成成功: ${fileName}`
|
||||
);
|
||||
vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
|
||||
@ -94,6 +94,13 @@ export async function readDirectory(
|
||||
const results = [];
|
||||
|
||||
for (const [fileName, fileType] of entries) {
|
||||
// 处理目录
|
||||
if (fileType === vscode.FileType.Directory) {
|
||||
results.push({ path: fileName + '/', content: '[目录]', isDirectory: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理文件
|
||||
if (fileType === vscode.FileType.File) {
|
||||
// 如果指定了扩展名过滤
|
||||
if (extensions && extensions.length > 0) {
|
||||
@ -108,7 +115,7 @@ export async function readDirectory(
|
||||
const fileUri = vscode.Uri.file(filePath);
|
||||
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
|
||||
const content = Buffer.from(contentBytes).toString("utf-8");
|
||||
results.push({ path: fileName, content });
|
||||
results.push({ path: fileName, content, isDirectory: false });
|
||||
} catch (error) {
|
||||
// 跳过无法读取的文件
|
||||
continue;
|
||||
|
||||
467
src/utils/vcdParser.ts
Normal file
467
src/utils/vcdParser.ts
Normal file
@ -0,0 +1,467 @@
|
||||
/**
|
||||
* VCD (Value Change Dump) 解析器
|
||||
* 纯 TypeScript 实现,参照 VerilogCoder 项目格式
|
||||
*
|
||||
* @deprecated 当前未使用,保留备用
|
||||
* 目前使用 waveformTracer.ts 调用 Python 打包的 waveform_trace.exe
|
||||
* 未来可能用此文件替换 Python 实现
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/** 信号定义 */
|
||||
export interface VcdSignal {
|
||||
name: string; // 完整路径名,如 "tb.top_module.data"
|
||||
shortName: string; // 短名,如 "data"
|
||||
symbolId: string; // VCD 符号 ID,如 "!", "#"
|
||||
width: number; // 位宽
|
||||
varType: string; // 变量类型:wire, reg
|
||||
module: string; // 所属模块
|
||||
}
|
||||
|
||||
/** 时间-值对 */
|
||||
export interface TimeValue {
|
||||
time: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** 信号波形数据 */
|
||||
export interface SignalWaveform {
|
||||
signal: VcdSignal;
|
||||
changes: TimeValue[];
|
||||
}
|
||||
|
||||
/** VCD 解析结果 */
|
||||
export interface VcdData {
|
||||
date?: string;
|
||||
version?: string;
|
||||
timescale: string;
|
||||
endTime: number;
|
||||
signals: Map<string, VcdSignal>; // symbolId -> signal
|
||||
waveforms: Map<string, TimeValue[]>; // symbolId -> changes
|
||||
}
|
||||
|
||||
/** Mismatch 信息 */
|
||||
export interface MismatchInfo {
|
||||
time: number;
|
||||
signal: string;
|
||||
dutValue: string;
|
||||
refValue: string;
|
||||
}
|
||||
|
||||
// ==================== VCD 解析器 ====================
|
||||
|
||||
export class VcdParser {
|
||||
private signals: Map<string, VcdSignal> = new Map();
|
||||
private waveforms: Map<string, TimeValue[]> = new Map();
|
||||
private scopeStack: string[] = [];
|
||||
private timescale: string = '1ns';
|
||||
private currentTime: number = 0;
|
||||
private endTime: number = 0;
|
||||
private date?: string;
|
||||
private version?: string;
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件
|
||||
*/
|
||||
parse(filePath: string): VcdData {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return this.parseContent(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 内容
|
||||
*/
|
||||
parseContent(content: string): VcdData {
|
||||
// 预处理:将多行指令合并成单行
|
||||
const normalizedContent = this.normalizeVcdContent(content);
|
||||
const lines = normalizedContent.split('\n');
|
||||
let inDefinitions = true;
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
|
||||
if (inDefinitions) {
|
||||
// 解析定义区
|
||||
if (line.startsWith('$enddefinitions')) {
|
||||
inDefinitions = false;
|
||||
continue;
|
||||
}
|
||||
this.parseDefinition(line);
|
||||
} else {
|
||||
// 解析数据区
|
||||
this.parseValueChange(line);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
date: this.date,
|
||||
version: this.version,
|
||||
timescale: this.timescale,
|
||||
endTime: this.endTime,
|
||||
signals: this.signals,
|
||||
waveforms: this.waveforms
|
||||
};
|
||||
}
|
||||
|
||||
private parseDefinition(line: string): void {
|
||||
if (line.startsWith('$date')) {
|
||||
this.date = this.extractValue(line);
|
||||
} else if (line.startsWith('$version')) {
|
||||
this.version = this.extractValue(line);
|
||||
} else if (line.startsWith('$timescale')) {
|
||||
this.timescale = this.extractValue(line) || '1ns';
|
||||
} else if (line.startsWith('$scope')) {
|
||||
const match = line.match(/\$scope\s+\w+\s+(\S+)/);
|
||||
if (match) {
|
||||
this.scopeStack.push(match[1]);
|
||||
}
|
||||
} else if (line.startsWith('$upscope')) {
|
||||
this.scopeStack.pop();
|
||||
} else if (line.startsWith('$var')) {
|
||||
this.parseVariable(line);
|
||||
}
|
||||
}
|
||||
|
||||
private parseVariable(line: string): void {
|
||||
// $var wire 8 # data [7:0] $end
|
||||
// $var reg 1 ! clk $end
|
||||
const match = line.match(/\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+(\S+)/);
|
||||
if (!match) return;
|
||||
|
||||
const [, varType, widthStr, symbolId, name] = match;
|
||||
const width = parseInt(widthStr, 10);
|
||||
const module = this.scopeStack.join('.');
|
||||
const fullName = module ? `${module}.${name}` : name;
|
||||
|
||||
const signal: VcdSignal = {
|
||||
name: fullName,
|
||||
shortName: name.replace(/\[\d+:\d+\]/, ''), // 移除位宽标注
|
||||
symbolId,
|
||||
width,
|
||||
varType,
|
||||
module
|
||||
};
|
||||
|
||||
this.signals.set(symbolId, signal);
|
||||
this.waveforms.set(symbolId, []);
|
||||
}
|
||||
|
||||
private parseValueChange(line: string): void {
|
||||
if (line.startsWith('#')) {
|
||||
// 时间戳: #100
|
||||
this.currentTime = parseInt(line.substring(1), 10);
|
||||
this.endTime = Math.max(this.endTime, this.currentTime);
|
||||
} else if (line.startsWith('b') || line.startsWith('B')) {
|
||||
// 多位值: b10101010 #
|
||||
const spaceIdx = line.indexOf(' ');
|
||||
if (spaceIdx > 0) {
|
||||
const value = line.substring(1, spaceIdx);
|
||||
const symbolId = line.substring(spaceIdx + 1).trim();
|
||||
this.addChange(symbolId, value);
|
||||
}
|
||||
} else if (line.length >= 2 && !line.startsWith('$')) {
|
||||
// 单位值: 0! 或 1# 或 x$
|
||||
const value = line[0];
|
||||
const symbolId = line.substring(1).trim();
|
||||
if (symbolId && this.signals.has(symbolId)) {
|
||||
this.addChange(symbolId, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addChange(symbolId: string, value: string): void {
|
||||
const changes = this.waveforms.get(symbolId);
|
||||
if (changes) {
|
||||
changes.push({ time: this.currentTime, value });
|
||||
}
|
||||
}
|
||||
|
||||
private extractValue(line: string): string {
|
||||
// 提取 $xxx value $end 中的 value
|
||||
const match = line.match(/\$\w+\s+(.+?)\s*\$end/);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 预处理 VCD 内容,将多行指令合并成单行
|
||||
*/
|
||||
private normalizeVcdContent(content: string): string {
|
||||
// 将多行 $xxx ... $end 合并成单行
|
||||
return content.replace(/(\$\w+)\s*\n\s*([^\$]+?)\s*\n\s*(\$end)/g, '$1 $2 $3');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 波形分析工具 ====================
|
||||
|
||||
/**
|
||||
* 二进制字符串转十六进制
|
||||
*/
|
||||
export function binaryToHex(binary: string): string {
|
||||
if (binary === 'x' || binary === 'X' || binary.includes('x')) {
|
||||
return 'xx';
|
||||
}
|
||||
if (binary === 'z' || binary === 'Z' || binary.includes('z')) {
|
||||
return 'zz';
|
||||
}
|
||||
if (binary.length <= 1) {
|
||||
return binary;
|
||||
}
|
||||
// 补齐到 4 的倍数
|
||||
const padded = binary.padStart(Math.ceil(binary.length / 4) * 4, '0');
|
||||
let hex = '';
|
||||
for (let i = 0; i < padded.length; i += 4) {
|
||||
hex += parseInt(padded.substring(i, i + 4), 2).toString(16);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取信号在指定时间的值
|
||||
*/
|
||||
export function getValueAtTime(
|
||||
changes: TimeValue[],
|
||||
time: number
|
||||
): string {
|
||||
let value = 'x';
|
||||
for (const change of changes) {
|
||||
if (change.time <= time) {
|
||||
value = change.value;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找 DUT 和 REF 信号的第一个 mismatch
|
||||
*/
|
||||
export function findFirstMismatch(
|
||||
vcdData: VcdData,
|
||||
dutSignals: string[],
|
||||
refSignals: string[]
|
||||
): MismatchInfo | null {
|
||||
// 收集所有时间点
|
||||
const allTimes = new Set<number>();
|
||||
for (const changes of vcdData.waveforms.values()) {
|
||||
for (const c of changes) {
|
||||
allTimes.add(c.time);
|
||||
}
|
||||
}
|
||||
const sortedTimes = Array.from(allTimes).sort((a, b) => a - b);
|
||||
|
||||
// 按信号名匹配 DUT 和 REF
|
||||
for (const time of sortedTimes) {
|
||||
for (let i = 0; i < dutSignals.length; i++) {
|
||||
const dutSig = findSignalByName(vcdData, dutSignals[i]);
|
||||
const refSig = findSignalByName(vcdData, refSignals[i]);
|
||||
|
||||
if (!dutSig || !refSig) continue;
|
||||
|
||||
const dutChanges = vcdData.waveforms.get(dutSig.symbolId) || [];
|
||||
const refChanges = vcdData.waveforms.get(refSig.symbolId) || [];
|
||||
|
||||
const dutVal = getValueAtTime(dutChanges, time);
|
||||
const refVal = getValueAtTime(refChanges, time);
|
||||
|
||||
// 跳过未知值
|
||||
if (dutVal.includes('x') || refVal.includes('x')) continue;
|
||||
|
||||
if (dutVal !== refVal) {
|
||||
return {
|
||||
time,
|
||||
signal: dutSig.shortName,
|
||||
dutValue: binaryToHex(dutVal),
|
||||
refValue: binaryToHex(refVal)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称查找信号
|
||||
*/
|
||||
function findSignalByName(vcdData: VcdData, name: string): VcdSignal | null {
|
||||
for (const signal of vcdData.signals.values()) {
|
||||
if (signal.name.endsWith(name) || signal.shortName === name) {
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成波形表格(参照 VerilogCoder 格式)
|
||||
*/
|
||||
export function generateWaveformTable(
|
||||
vcdData: VcdData,
|
||||
signalNames: string[],
|
||||
startTime: number = 0,
|
||||
endTime?: number,
|
||||
windowSize: number = 20
|
||||
): string {
|
||||
const actualEndTime = endTime ?? vcdData.endTime;
|
||||
|
||||
// 查找信号
|
||||
const signals: VcdSignal[] = [];
|
||||
for (const name of signalNames) {
|
||||
const sig = findSignalByName(vcdData, name);
|
||||
if (sig) signals.push(sig);
|
||||
}
|
||||
|
||||
if (signals.length === 0) {
|
||||
return '未找到指定信号';
|
||||
}
|
||||
|
||||
// 收集时间点
|
||||
const times = new Set<number>();
|
||||
for (const sig of signals) {
|
||||
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||
for (const c of changes) {
|
||||
if (c.time >= startTime && c.time <= actualEndTime) {
|
||||
times.add(c.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sortedTimes = Array.from(times).sort((a, b) => a - b);
|
||||
if (sortedTimes.length > windowSize) {
|
||||
sortedTimes = sortedTimes.slice(0, windowSize);
|
||||
}
|
||||
|
||||
// 生成表头
|
||||
const headers = ['time(ns)', ...signals.map(s => s.shortName)];
|
||||
const colWidths = headers.map(h => Math.max(h.length, 8));
|
||||
|
||||
let table = '### Waveform Trace ###\n';
|
||||
table += headers.map((h, i) => h.padEnd(colWidths[i])).join(' ') + '\n';
|
||||
table += colWidths.map(w => '─'.repeat(w)).join('──') + '\n';
|
||||
|
||||
// 生成数据行
|
||||
for (const time of sortedTimes) {
|
||||
const row = [time.toString()];
|
||||
for (const sig of signals) {
|
||||
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||
const val = getValueAtTime(changes, time);
|
||||
row.push(binaryToHex(val));
|
||||
}
|
||||
table += row.map((v, i) => v.padEnd(colWidths[i])).join(' ') + '\n';
|
||||
}
|
||||
|
||||
table += '### Waveform Trace End ###\n';
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取信号显示名称(模块.信号名[位宽])
|
||||
*/
|
||||
function getSignalDisplayName(sig: VcdSignal): string {
|
||||
const moduleParts = sig.module.split('.');
|
||||
const moduleShort = moduleParts[moduleParts.length - 1] || '';
|
||||
const bitInfo = sig.width > 1 ? `[${sig.width - 1}:0]` : '';
|
||||
return moduleShort ? `${moduleShort}.${sig.shortName}${bitInfo}` : `${sig.shortName}${bitInfo}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成变化日志格式(只记录信号变化)
|
||||
*/
|
||||
export function generateChangeLog(vcdData: VcdData): string {
|
||||
// 筛选信号(排除 parameter)
|
||||
const signals: VcdSignal[] = [];
|
||||
for (const sig of vcdData.signals.values()) {
|
||||
if (sig.varType !== 'parameter') {
|
||||
signals.push(sig);
|
||||
}
|
||||
}
|
||||
|
||||
// 收集所有时间点
|
||||
const times = new Set<number>();
|
||||
for (const sig of signals) {
|
||||
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||
for (const c of changes) {
|
||||
times.add(c.time);
|
||||
}
|
||||
}
|
||||
const sortedTimes = Array.from(times).sort((a, b) => a - b);
|
||||
|
||||
// 记录每个信号的上一个值
|
||||
const lastValues = new Map<string, string | null>();
|
||||
for (const sig of signals) {
|
||||
lastValues.set(sig.symbolId, null);
|
||||
}
|
||||
|
||||
let log = '';
|
||||
|
||||
for (const time of sortedTimes) {
|
||||
const changes: string[] = [];
|
||||
|
||||
for (const sig of signals) {
|
||||
const waveform = vcdData.waveforms.get(sig.symbolId) || [];
|
||||
const currentVal = binaryToHex(getValueAtTime(waveform, time));
|
||||
const lastVal = lastValues.get(sig.symbolId);
|
||||
|
||||
if (lastVal === null) {
|
||||
changes.push(`${getSignalDisplayName(sig)}=${currentVal}`);
|
||||
} else if (currentVal !== lastVal) {
|
||||
changes.push(`${getSignalDisplayName(sig)} ${lastVal}→${currentVal}`);
|
||||
}
|
||||
|
||||
lastValues.set(sig.symbolId, currentVal);
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
log += `#${time}: ${changes.join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析 VCD 文件(主入口)
|
||||
*/
|
||||
export function analyzeVcdFile(
|
||||
filePath: string,
|
||||
signalFilter?: string,
|
||||
checkpoint?: number
|
||||
): string {
|
||||
// 解析 VCD
|
||||
const parser = new VcdParser();
|
||||
const vcdData = parser.parse(filePath);
|
||||
|
||||
// 解析信号过滤器
|
||||
const signalNames = signalFilter
|
||||
? signalFilter.split(',').map(s => s.trim())
|
||||
: Array.from(vcdData.signals.values()).map(s => s.shortName);
|
||||
|
||||
// 生成摘要
|
||||
let result = `=== VCD 波形分析 ===\n`;
|
||||
result += `文件: ${path.basename(filePath)}\n`;
|
||||
result += `时间单位: ${vcdData.timescale}\n`;
|
||||
result += `仿真时长: 0 - ${vcdData.endTime}${vcdData.timescale}\n\n`;
|
||||
|
||||
// 信号列表
|
||||
result += `--- 信号列表 (${vcdData.signals.size} 个) ---\n`;
|
||||
let idx = 1;
|
||||
for (const sig of vcdData.signals.values()) {
|
||||
if (idx <= 10) {
|
||||
result += `${idx}. ${sig.shortName} (${sig.width}-bit, ${sig.varType})\n`;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
if (vcdData.signals.size > 10) {
|
||||
result += `... 还有 ${vcdData.signals.size - 10} 个信号\n`;
|
||||
}
|
||||
result += '\n';
|
||||
|
||||
// 变化日志
|
||||
result += generateChangeLog(vcdData);
|
||||
|
||||
return result;
|
||||
}
|
||||
145
src/utils/waveformTracer.ts
Normal file
145
src/utils/waveformTracer.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 波形追踪工具
|
||||
* 调用 PyInstaller 打包的 waveform_trace 可执行文件
|
||||
*/
|
||||
import { spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* 波形追踪参数
|
||||
*/
|
||||
export interface WaveformTraceArgs {
|
||||
/** Verilog 源文件路径(相对于项目根目录) */
|
||||
verilogPath: string;
|
||||
/** VCD 波形文件路径(相对于项目根目录) */
|
||||
vcdPath: string;
|
||||
/** 仿真工具的输出字符串(包含 mismatch 信息) */
|
||||
simOutput: string;
|
||||
/** BFS 回溯层数,默认 2 */
|
||||
traceLevel?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行波形追踪
|
||||
* @param args 追踪参数
|
||||
* @param context 执行上下文
|
||||
* @returns 追踪结果字符串
|
||||
*/
|
||||
export async function executeWaveformTrace(
|
||||
args: WaveformTraceArgs,
|
||||
context: { extensionPath: string }
|
||||
): Promise<string> {
|
||||
// 获取可执行文件路径
|
||||
const tracerPath = getWaveformTracerPath(context.extensionPath);
|
||||
|
||||
// 检查可执行文件是否存在
|
||||
if (!fs.existsSync(tracerPath)) {
|
||||
throw new Error(
|
||||
`waveform_trace 工具未安装: ${tracerPath}\n` +
|
||||
'请确保插件包含 tools/waveform_trace/bin/ 目录'
|
||||
);
|
||||
}
|
||||
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 解析路径(支持相对路径)
|
||||
const verilogAbsPath = path.isAbsolute(args.verilogPath)
|
||||
? args.verilogPath
|
||||
: path.join(workspacePath, args.verilogPath);
|
||||
const vcdAbsPath = path.isAbsolute(args.vcdPath)
|
||||
? args.vcdPath
|
||||
: path.join(workspacePath, args.vcdPath);
|
||||
|
||||
// 验证文件存在
|
||||
if (!fs.existsSync(verilogAbsPath)) {
|
||||
throw new Error(`Verilog 文件不存在: ${args.verilogPath}`);
|
||||
}
|
||||
if (!fs.existsSync(vcdAbsPath)) {
|
||||
throw new Error(`VCD 文件不存在: ${args.vcdPath}`);
|
||||
}
|
||||
|
||||
// 调用可执行文件
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(tracerPath, [
|
||||
'--verilog', verilogAbsPath,
|
||||
'--vcd', vcdAbsPath,
|
||||
'--sim-output', args.simOutput,
|
||||
'--trace-level', String(args.traceLevel || 2),
|
||||
'--output-format', 'text'
|
||||
], {
|
||||
windowsHide: true,
|
||||
cwd: workspacePath,
|
||||
shell: false
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
reject(new Error(
|
||||
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
reject(new Error(`waveform_trace 启动失败: ${error.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 waveform_trace 可执行文件路径
|
||||
*/
|
||||
function getWaveformTracerPath(extensionPath: string): string {
|
||||
const platform = process.platform;
|
||||
let binName = 'waveform_trace';
|
||||
|
||||
if (platform === 'win32') {
|
||||
binName = 'waveform_trace.exe';
|
||||
}
|
||||
|
||||
return path.join(extensionPath, 'tools', 'waveform_trace', 'bin', binName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 waveform_trace 工具是否可用
|
||||
*/
|
||||
export function checkWaveformTraceAvailable(extensionPath: string): {
|
||||
available: boolean;
|
||||
message: string;
|
||||
path?: string;
|
||||
} {
|
||||
const tracerPath = getWaveformTracerPath(extensionPath);
|
||||
|
||||
if (fs.existsSync(tracerPath)) {
|
||||
return {
|
||||
available: true,
|
||||
message: 'waveform_trace 工具可用',
|
||||
path: tracerPath
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
available: false,
|
||||
message: `waveform_trace 工具未找到: ${tracerPath}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
@ -39,15 +42,36 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
const iconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||
);
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
||||
panel.webview.html = getWebviewContent(
|
||||
iconUri.toString(),
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString()
|
||||
);
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
handleUserMessage(panel, message.text, context.extensionPath);
|
||||
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
@ -90,7 +114,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
181
src/views/agentCard.ts
Normal file
181
src/views/agentCard.ts
Normal file
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 智能体卡片组件
|
||||
*
|
||||
* 功能说明:
|
||||
* - 提供智能体执行状态的可视化展示
|
||||
* - 显示智能体名称、状态和执行步骤
|
||||
* - 支持实时更新步骤信息
|
||||
*/
|
||||
|
||||
import { agentIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取智能体卡片的样式
|
||||
*/
|
||||
export function getAgentCardStyles(): string {
|
||||
return `
|
||||
/* 智能体卡片样式 */
|
||||
.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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体卡片的脚本
|
||||
*/
|
||||
export function getAgentCardScript(): string {
|
||||
return `
|
||||
// 工具名称中文映射
|
||||
function getAgentToolDisplayName(toolName) {
|
||||
const toolNameMap = {
|
||||
'file_read': '文件读取',
|
||||
'file_write': '文件写入',
|
||||
'file_delete': '文件删除',
|
||||
'file_list': '检索文件',
|
||||
'syntax_check': '语法检查',
|
||||
'simulation': '仿真',
|
||||
'waveform_summary': '波形分析',
|
||||
'knowledge_save': '保存知识库',
|
||||
'knowledge_load': '加载知识库',
|
||||
'queryKnowledgeSummary': '查询知识摘要',
|
||||
'queryRules': '查询规则',
|
||||
'setModule': '设置模块',
|
||||
'addSignal': '添加信号',
|
||||
'addSignalExample': '添加信号示例',
|
||||
'validateKnowledgeGraph': '验证知识图谱',
|
||||
'querySignals': '查询信号',
|
||||
'addPlan': '添加计划',
|
||||
'addEdge': '添加边',
|
||||
'showPlan': '显示计划',
|
||||
'spawnExplorer': '代码探索',
|
||||
'spawnDebugger': '波形调试',
|
||||
'queryByBFS': 'BFS查询',
|
||||
'queryStateTransitions': '查询状态转移',
|
||||
'addStateTransition': '添加状态转移'
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染智能体卡片
|
||||
* @param {Object} segment - 智能体段落数据
|
||||
* @param {HTMLElement} segmentDiv - 段落容器元素
|
||||
*/
|
||||
function renderAgentCard(segment, segmentDiv) {
|
||||
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 displayName = getAgentToolDisplayName(step.toolName);
|
||||
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
|
||||
return \`<div class="agent-step"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
|
||||
}).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="agent-card">
|
||||
<div class="agent-header">
|
||||
<span class="agent-icon">${agentIconSvg}</span>
|
||||
<span class="agent-name">\${segment.agentName || '智能体'}</span>
|
||||
<span class="agent-status \${statusClass}">\${statusText}</span>
|
||||
</div>
|
||||
<div class="agent-body">
|
||||
<div class="agent-steps-container">
|
||||
\${stepsHtml || '<div class="agent-step-placeholder">等待执行...</div>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 自动滚动到最新步骤
|
||||
setTimeout(() => {
|
||||
const container = segmentDiv.querySelector('.agent-steps-container');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -1,6 +1,11 @@
|
||||
/**
|
||||
* 模式选择器组件
|
||||
* 提供 Agent/Ask/Auto 三种模式的选择功能
|
||||
* 提供 Plan/Ask/Agent 四种模式的选择功能
|
||||
*
|
||||
* 模式说明:
|
||||
* - Plan: 只读模式,只能查询分析,不能写文件
|
||||
* - Ask: 逐个确认,每个写操作需用户确认
|
||||
* - Agent: 智能体自主,自动执行大部分操作
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -17,12 +22,21 @@ export function getModeSelectorContent(): string {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mode-dropdown" id="modeDropdown">
|
||||
<div class="mode-option" data-value="agent" onclick="selectMode('agent', 'Agent')">Agent</div>
|
||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">Ask</div>
|
||||
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">Auto</div>
|
||||
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
|
||||
<span class="mode-option-label">Plan</span>
|
||||
<span class="mode-option-desc">只读模式</span>
|
||||
</div>
|
||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
|
||||
<span class="mode-option-label">Ask</span>
|
||||
<span class="mode-option-desc">逐个确认</span>
|
||||
</div>
|
||||
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
|
||||
<span class="mode-option-label">Agent</span>
|
||||
<span class="mode-option-desc">智能体自主</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="tooltiptext">切换模式</span>
|
||||
<span class="tooltiptext" id="modeTooltip">智能体自主模式</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -69,7 +83,7 @@ export function getModeSelectorStyles(): string {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 2px);
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
min-width: 140px;
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 4px;
|
||||
@ -83,7 +97,10 @@ export function getModeSelectorStyles(): string {
|
||||
}
|
||||
/* 模式选择器的选项样式 */
|
||||
.mode-option {
|
||||
padding: 6px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
@ -93,8 +110,15 @@ export function getModeSelectorStyles(): string {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
.mode-option.selected {
|
||||
background: rgba(128, 128, 128, 0.5);
|
||||
color: var(--vscode-foreground);
|
||||
background: rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
.mode-option-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.mode-option-desc {
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-left: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -124,10 +148,22 @@ export function getModeSelectorScript(): string {
|
||||
function selectMode(value, label) {
|
||||
currentMode = value;
|
||||
const modeValue = document.getElementById('modeValue');
|
||||
const modeTooltip = document.getElementById('modeTooltip');
|
||||
|
||||
if (modeValue) {
|
||||
modeValue.textContent = label;
|
||||
}
|
||||
|
||||
// 更新 tooltip
|
||||
if (modeTooltip) {
|
||||
const tooltipMap = {
|
||||
'plan': '只读模式 - 只能查询分析',
|
||||
'ask': '逐个确认 - 每个写操作需确认',
|
||||
'agent': '智能体自主模式','
|
||||
};
|
||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||
}
|
||||
|
||||
// 更新选中状态
|
||||
const options = document.querySelectorAll('.mode-option');
|
||||
options.forEach(option => {
|
||||
|
||||
@ -2,42 +2,39 @@ import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||||
import {
|
||||
getModelSelectorContent,
|
||||
getModelSelectorStyles,
|
||||
getModelSelectorScript
|
||||
getModelSelectorScript,
|
||||
} from "./modelSelector";
|
||||
import {
|
||||
getModeSelectorContent,
|
||||
getModeSelectorStyles,
|
||||
getModeSelectorScript
|
||||
getModeSelectorScript,
|
||||
} from "./agentModeSelector";
|
||||
import {
|
||||
getContextButtonContent,
|
||||
getContextButtonStyles,
|
||||
getContextButtonScript
|
||||
getContextButtonScript,
|
||||
} from "./contextButton";
|
||||
import {
|
||||
getContextCompressContent,
|
||||
getContextCompressStyles,
|
||||
getContextCompressScript
|
||||
getContextCompressScript,
|
||||
} from "./contextCompress";
|
||||
import {
|
||||
getPlanToggleContent,
|
||||
getPlanToggleStyles,
|
||||
getPlanToggleScript
|
||||
} from "./planToggle";
|
||||
import {
|
||||
getOptimizeButtonContent,
|
||||
getOptimizeButtonStyles,
|
||||
getOptimizeButtonScript
|
||||
getOptimizeButtonScript,
|
||||
} from "./optimizeButton";
|
||||
import {
|
||||
sendIconSvg,
|
||||
stopIconSvg
|
||||
} from "../constants/toolIcons";
|
||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取输入区域的 HTML 内容
|
||||
*/
|
||||
export function getInputAreaContent(): string {
|
||||
export function getInputAreaContent(
|
||||
autoIcon: string = '',
|
||||
liteIcon: string = '',
|
||||
syIcon: string = '',
|
||||
maxIcon: string = ''
|
||||
): string {
|
||||
return `
|
||||
<div class="input-area">
|
||||
<div class="input-group">
|
||||
@ -45,17 +42,16 @@ export function getInputAreaContent(): string {
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="input-top-toolbar">
|
||||
${getContextButtonContent()}
|
||||
${getPlanToggleContent()}
|
||||
</div>
|
||||
<textarea
|
||||
id="messageInput"
|
||||
placeholder="输入您的问题..."
|
||||
placeholder="输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
||||
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
|
||||
></textarea>
|
||||
<div class="input-bottom-row">
|
||||
<div class="mode-selector">
|
||||
${getModeSelectorContent()}
|
||||
${getModelSelectorContent()}
|
||||
${getModelSelectorContent(autoIcon, liteIcon, syIcon, maxIcon)}
|
||||
</div>
|
||||
<div class="input-actions">
|
||||
${getContextCompressContent()}
|
||||
@ -81,7 +77,6 @@ export function getInputAreaStyles(): string {
|
||||
${getModelSelectorStyles()}
|
||||
${getContextButtonStyles()}
|
||||
${getContextCompressStyles()}
|
||||
${getPlanToggleStyles()}
|
||||
${getOptimizeButtonStyles()}
|
||||
.input-area {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
@ -207,6 +202,11 @@ export function getInputAreaStyles(): string {
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
textarea:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
/* 简洁的滚动条样式 */
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@ -261,16 +261,19 @@ export function getInputAreaStyles(): string {
|
||||
*/
|
||||
export function getInputAreaScript(): string {
|
||||
return `
|
||||
${getModeSelectorScript()}
|
||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||
${getModelSelectorScript()}
|
||||
${getContextButtonScript()}
|
||||
${getContextCompressScript()}
|
||||
${getPlanToggleScript()}
|
||||
${getOptimizeButtonScript()}
|
||||
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
|
||||
// 工作区检测状态
|
||||
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
||||
let hasWorkspace = true; // 工作区状态
|
||||
|
||||
// 自动调整 textarea 高度
|
||||
function autoResizeTextarea() {
|
||||
if (messageInput) {
|
||||
@ -283,11 +286,16 @@ export function getInputAreaScript(): string {
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('input', autoResizeTextarea);
|
||||
|
||||
// 监听点击事件,检测工作区状态
|
||||
messageInput.addEventListener('focus', () => {
|
||||
if (!hasCheckedWorkspace) {
|
||||
hasCheckedWorkspace = true;
|
||||
vscode.postMessage({ command: 'checkWorkspace' });
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化时调整一次高度
|
||||
autoResizeTextarea();
|
||||
|
||||
// 聚焦到输入框
|
||||
messageInput.focus();
|
||||
}
|
||||
|
||||
// 切换发送按钮状态
|
||||
@ -302,11 +310,17 @@ export function getInputAreaScript(): string {
|
||||
sendIconContainer.style.display = 'none';
|
||||
stopIconContainer.style.display = 'block';
|
||||
isConversationActive = true;
|
||||
// 禁用输入框
|
||||
messageInput.disabled = true;
|
||||
messageInput.placeholder = '正在处理中,请稍候...';
|
||||
} else {
|
||||
sendButton.classList.remove('sending');
|
||||
sendIconContainer.style.display = 'block';
|
||||
stopIconContainer.style.display = 'none';
|
||||
isConversationActive = false;
|
||||
// 启用输入框
|
||||
messageInput.disabled = false;
|
||||
messageInput.placeholder = '输入您的问题,按 Enter 发送,Shift + Enter 换行...';
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,15 +340,29 @@ export function getInputAreaScript(): string {
|
||||
const text = messageInput.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
// 如果正在对话中,阻止发送新消息
|
||||
if (isConversationActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查工作区状态
|
||||
if (!hasWorkspace) {
|
||||
// 如果没有工作区,阻止发送并清空输入框
|
||||
messageInput.value = '';
|
||||
autoResizeTextarea();
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
|
||||
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
||||
const planMode = document.getElementById('planToggle')?.checked || false;
|
||||
|
||||
addMessage(text, 'user');
|
||||
|
||||
// 切换按钮为暂停状态
|
||||
setSendButtonState(true);
|
||||
|
||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model });
|
||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model, planMode: planMode });
|
||||
messageInput.value = '';
|
||||
autoResizeTextarea(); // 重置输入框高度
|
||||
messageInput.focus();
|
||||
|
||||
@ -13,13 +13,23 @@
|
||||
import {
|
||||
collapseIconSvg,
|
||||
fileWriteIconSvg,
|
||||
fileReadIconSvg,
|
||||
fileDeleteIconSvg,
|
||||
syntaxCheckIconSvg,
|
||||
SearchCode,
|
||||
agentIconSvg,
|
||||
saveKnowledgeIconSvg,
|
||||
simulationIconSvg,
|
||||
waveformIconSvg,
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
import {
|
||||
getWaveformPreviewContent,
|
||||
getWaveformPreviewScript,
|
||||
} from "./waveformPreviewContent";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
|
||||
|
||||
/**
|
||||
* 获取消息区域的 HTML 内容
|
||||
@ -400,7 +410,7 @@ export function getMessageAreaStyles(): string {
|
||||
display: block;
|
||||
}
|
||||
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
@ -416,6 +426,28 @@ export function getMessageAreaStyles(): string {
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-read-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-read-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-delete-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-syntax-check-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@ -438,6 +470,61 @@ export function getMessageAreaStyles(): string {
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-save-knowledge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-save-knowledge-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-simulation-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-simulation-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-waveform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-waveform-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-knowledge-load-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-knowledge-load-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-state-transition-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-state-transition-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-segment-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
@ -528,6 +615,10 @@ export function getMessageAreaStyles(): string {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;}
|
||||
|
||||
${getAgentCardStyles()}
|
||||
|
||||
${getPlanCardStyles()}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||
@ -540,18 +631,75 @@ export function getMessageAreaScript(): string {
|
||||
// 工具图标定义
|
||||
const collapseIconSvg = \`${collapseIconSvg}\`;
|
||||
const fileWriteIconSvg = \`${fileWriteIconSvg}\`;
|
||||
const fileReadIconSvg = \`${fileReadIconSvg}\`;
|
||||
const fileDeleteIconSvg = \`${fileDeleteIconSvg}\`;
|
||||
const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`;
|
||||
const searchCodeIconSvg = \`${SearchCode}\`;
|
||||
const saveKnowledgeIconSvg = \`${saveKnowledgeIconSvg}\`;
|
||||
const simulationIconSvg = \`${simulationIconSvg}\`;
|
||||
const waveformIconSvg = \`${waveformIconSvg}\`;
|
||||
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||
|
||||
${getAgentCardScript()}
|
||||
|
||||
${getPlanCardScript()}
|
||||
|
||||
// 获取工具图标
|
||||
function getToolIcon(toolName) {
|
||||
const iconMap = {
|
||||
'file_read': fileReadIconSvg,
|
||||
'file_write': fileWriteIconSvg,
|
||||
'file_delete': fileDeleteIconSvg,
|
||||
'file_list': searchCodeIconSvg,
|
||||
'syntax_check': syntaxCheckIconSvg,
|
||||
'simulation': simulationIconSvg,
|
||||
'waveform_summary': waveformIconSvg,
|
||||
'knowledge_save': saveKnowledgeIconSvg,
|
||||
'knowledge_load': knowledgeLoadIconSvg,
|
||||
'queryKnowledgeSummary': knowledgeLoadIconSvg,
|
||||
'queryRules': knowledgeLoadIconSvg,
|
||||
'setModule': fileWriteIconSvg,
|
||||
'addSignal': fileWriteIconSvg,
|
||||
'addSignalExample': fileWriteIconSvg,
|
||||
'validateKnowledgeGraph': syntaxCheckIconSvg,
|
||||
'querySignals': searchCodeIconSvg,
|
||||
'addPlan': fileWriteIconSvg,
|
||||
'addEdge': fileWriteIconSvg,
|
||||
'showPlan': searchCodeIconSvg,
|
||||
'addRule': fileWriteIconSvg,
|
||||
'updateNode': fileWriteIconSvg,
|
||||
'addStateTransition': stateTransitionIconSvg
|
||||
};
|
||||
return iconMap[toolName] || '';
|
||||
}
|
||||
|
||||
// 工具名称映射
|
||||
function getToolDisplayName(toolName) {
|
||||
const toolNameMap = {
|
||||
'file_read': '已完成文件读取',
|
||||
'file_write': '已完成文件写入',
|
||||
'file_delete': '已完成文件删除',
|
||||
'file_list': '已检索代码文件',
|
||||
'syntax_check': '已完成语法检查',
|
||||
'simulation': '已完成仿真',
|
||||
'waveform_summary': '已完成波形分析'
|
||||
'waveform_summary': '已完成波形分析',
|
||||
'knowledge_save': '已保存知识库',
|
||||
'knowledge_load': '已加载知识库',
|
||||
'queryKnowledgeSummary': '已查询知识摘要',
|
||||
'queryRules': '已查询规则',
|
||||
'setModule': '已设置模块',
|
||||
'addSignal': '已添加信号',
|
||||
'addSignalExample': '已添加信号示例',
|
||||
'validateKnowledgeGraph': '已验证知识图谱',
|
||||
'querySignals': '已查询信号',
|
||||
'addPlan': '已添加计划',
|
||||
'addEdge': '已添加边',
|
||||
'showPlan': '已显示计划',
|
||||
'addRule': '已添加规则',
|
||||
'updateNode': '已更新节点',
|
||||
'addStateTransition': '已添加状态转换',
|
||||
'spawnExplorer': '代码探索'
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
@ -742,6 +890,9 @@ export function getMessageAreaScript(): string {
|
||||
// 存储已回答问题的状态
|
||||
const answeredQuestions = new Map(); // askId -> answer
|
||||
|
||||
// 存储工具展开/折叠状态
|
||||
const toolCollapseStates = new Map(); // index -> isCollapsed
|
||||
|
||||
// 实时更新分段消息(按后端返回顺序)
|
||||
function updateSegmentsRealtime(segments, isComplete) {
|
||||
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
||||
@ -773,9 +924,19 @@ export function getMessageAreaScript(): string {
|
||||
messagesEl.appendChild(currentSegmentedMessage);
|
||||
}
|
||||
|
||||
// 保存当前所有工具的展开/折叠状态
|
||||
if (currentSegmentedMessage) {
|
||||
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
|
||||
toolHeaders.forEach((header, idx) => {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
toolCollapseStates.set(idx, isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
// 清空容器并重新渲染所有段落
|
||||
currentSegmentedMessage.innerHTML = '';
|
||||
|
||||
let toolIndex = 0; // 用于跟踪工具段落的索引
|
||||
segments.forEach((segment, index) => {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||
@ -784,22 +945,29 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.className += ' segment-text';
|
||||
segmentDiv.innerHTML = formatText(segment.content);
|
||||
} else if (segment.type === 'tool') {
|
||||
// 过滤掉不需要显示的工具
|
||||
if (segment.toolName === 'spawnExplorer') {
|
||||
return;
|
||||
}
|
||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||
const toolResult = segment.toolResult || '';
|
||||
|
||||
// 检查工具结果是否过长(超过一行显示不下)
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
|
||||
// 恢复之前保存的展开/折叠状态
|
||||
const savedState = toolCollapseStates.get(toolIndex);
|
||||
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
|
||||
const currentToolIndex = toolIndex;
|
||||
toolIndex++; // 递增工具索引
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
||||
\${shouldCollapse ? collapseIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''}
|
||||
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
||||
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\`;
|
||||
|
||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||
@ -825,27 +993,24 @@ export function getMessageAreaScript(): string {
|
||||
setTimeout(() => {
|
||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
|
||||
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
|
||||
|
||||
if (header && content) {
|
||||
header.addEventListener('click', function() {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||
|
||||
if (isCollapsed) {
|
||||
// 展开
|
||||
header.classList.remove('collapsed');
|
||||
content.classList.remove('collapsed');
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
if (iconCollapsed) iconCollapsed.style.display = 'none';
|
||||
if (iconExpanded) iconExpanded.style.display = 'block';
|
||||
toolCollapseStates.set(toolIdx, false);
|
||||
} else {
|
||||
// 折叠
|
||||
header.classList.add('collapsed');
|
||||
content.classList.add('collapsed');
|
||||
content.style.maxHeight = '0';
|
||||
if (iconCollapsed) iconCollapsed.style.display = 'block';
|
||||
if (iconExpanded) iconExpanded.style.display = 'none';
|
||||
toolCollapseStates.set(toolIdx, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -916,6 +1081,12 @@ export function getMessageAreaScript(): string {
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'agent') {
|
||||
// 智能体卡片渲染
|
||||
renderAgentCard(segment, segmentDiv);
|
||||
} else if (segment.type === 'plan') {
|
||||
// 计划卡片渲染(使用独立组件)
|
||||
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||||
}
|
||||
|
||||
currentSegmentedMessage.appendChild(segmentDiv);
|
||||
@ -926,9 +1097,11 @@ export function getMessageAreaScript(): string {
|
||||
console.log('[WebView] 对话完成,添加操作按钮');
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
|
||||
// 复制按钮
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
@ -936,7 +1109,22 @@ export function getMessageAreaScript(): string {
|
||||
.join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
|
||||
// 点赞按钮
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
|
||||
// 点踩按钮
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
currentSegmentedMessage.appendChild(actionsDiv);
|
||||
|
||||
// 重置当前分段消息容器
|
||||
@ -982,6 +1170,10 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.className += ' segment-text';
|
||||
segmentDiv.innerHTML = formatText(segment.content);
|
||||
} else if (segment.type === 'tool') {
|
||||
// 过滤掉不需要显示的工具
|
||||
if (segment.toolName === 'spawnExplorer') {
|
||||
return;
|
||||
}
|
||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||
const toolResult = segment.toolResult || '';
|
||||
|
||||
@ -990,10 +1182,7 @@ export function getMessageAreaScript(): string {
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
||||
\${shouldCollapse ? collapseIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''}
|
||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''}
|
||||
\${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
@ -1023,27 +1212,24 @@ export function getMessageAreaScript(): string {
|
||||
setTimeout(() => {
|
||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
|
||||
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
|
||||
|
||||
if (header && content) {
|
||||
header.addEventListener('click', function() {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||
|
||||
if (isCollapsed) {
|
||||
// 展开
|
||||
header.classList.remove('collapsed');
|
||||
content.classList.remove('collapsed');
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
if (iconCollapsed) iconCollapsed.style.display = 'none';
|
||||
if (iconExpanded) iconExpanded.style.display = 'block';
|
||||
toolCollapseStates.set(toolIdx, false);
|
||||
} else {
|
||||
// 折叠
|
||||
header.classList.add('collapsed');
|
||||
content.classList.add('collapsed');
|
||||
content.style.maxHeight = '0';
|
||||
if (iconCollapsed) iconCollapsed.style.display = 'block';
|
||||
if (iconExpanded) iconExpanded.style.display = 'none';
|
||||
toolCollapseStates.set(toolIdx, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1058,6 +1244,12 @@ export function getMessageAreaScript(): string {
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
} else if (segment.type === 'agent') {
|
||||
// 智能体卡片渲染
|
||||
renderAgentCard(segment, segmentDiv);
|
||||
} else if (segment.type === 'plan') {
|
||||
// 计划卡片渲染(使用独立组件)
|
||||
renderPlanCardStatic(segment, segmentDiv);
|
||||
}
|
||||
|
||||
container.appendChild(segmentDiv);
|
||||
@ -1066,9 +1258,11 @@ export function getMessageAreaScript(): string {
|
||||
// 添加操作按钮
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
|
||||
// 复制按钮
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
@ -1076,7 +1270,22 @@ export function getMessageAreaScript(): string {
|
||||
.join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
|
||||
// 点赞按钮
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
|
||||
// 点踩按钮
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
container.appendChild(actionsDiv);
|
||||
|
||||
messagesEl.appendChild(container);
|
||||
|
||||
@ -5,7 +5,12 @@
|
||||
/**
|
||||
* 获取模型选择器的 HTML 内容
|
||||
*/
|
||||
export function getModelSelectorContent(): string {
|
||||
export function getModelSelectorContent(
|
||||
autoIcon: string = '',
|
||||
liteIcon: string = '',
|
||||
syIcon: string = '',
|
||||
maxIcon: string = ''
|
||||
): string {
|
||||
return `
|
||||
<!-- 模型选择 -->
|
||||
<div class="tooltip">
|
||||
@ -17,10 +22,22 @@ export function getModelSelectorContent(): string {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="select-dropdown" id="modelDropdown">
|
||||
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">Lite</div>
|
||||
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">Auto</div>
|
||||
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">Syntaxic</div>
|
||||
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">Max</div>
|
||||
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">
|
||||
${autoIcon ? `<img src="${autoIcon}" class="model-icon" alt="Auto">` : ''}
|
||||
<span class="option-label">Auto</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">
|
||||
${liteIcon ? `<img src="${liteIcon}" class="model-icon" alt="Lite">` : ''}
|
||||
<span class="option-label">Lite</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">
|
||||
${syIcon ? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">` : ''}
|
||||
<span class="option-label">Syntaxic</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">
|
||||
${maxIcon ? `<img src="${maxIcon}" class="model-icon" alt="Max">` : ''}
|
||||
<span class="option-label">Max</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型选择器的 tooltip 容器 -->
|
||||
<div id="modelTooltip" class="model-tooltip"></div>
|
||||
@ -88,10 +105,12 @@ export function getModelSelectorStyles(): string {
|
||||
#modelDropdown .select-option {
|
||||
position: relative;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
#modelDropdown .select-option:hover {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
@ -100,6 +119,17 @@ export function getModelSelectorStyles(): string {
|
||||
background: rgba(128, 128, 128, 0.5);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.model-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
.option-label {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 模型选择器的 tooltip 样式 */
|
||||
.model-tooltip {
|
||||
position: fixed;
|
||||
|
||||
273
src/views/planCard.ts
Normal file
273
src/views/planCard.ts
Normal file
@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 计划卡片组件
|
||||
*
|
||||
* 功能说明:
|
||||
* - 显示执行计划的卡片界面
|
||||
* - 包含计划标题、摘要和步骤列表
|
||||
* - 提供确认执行、修改计划、取消等操作按钮
|
||||
*/
|
||||
|
||||
import { plannerIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取计划卡片的样式
|
||||
*/
|
||||
export function getPlanCardStyles(): string {
|
||||
return `
|
||||
/* 计划卡片样式 */
|
||||
.segment-plan {
|
||||
margin: 12px 0;
|
||||
}
|
||||
.plan-card {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.plan-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-bottom: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
.plan-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.plan-body {
|
||||
padding: 16px;
|
||||
}
|
||||
.plan-summary {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.plan-steps {
|
||||
font-size: 13px;
|
||||
}
|
||||
.plan-step {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.plan-step:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.step-num {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 500;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-sideBar-background);
|
||||
}
|
||||
.plan-actions .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.plan-btn {
|
||||
padding: 8px 18px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.plan-btn-confirm {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
.plan-btn-confirm:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.plan-btn-modify {
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.plan-actions .custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.plan-actions .custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.plan-actions .custom-submit {
|
||||
padding: 8px 18px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.plan-actions .custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计划卡片的脚本
|
||||
*/
|
||||
export function getPlanCardScript(): string {
|
||||
return `
|
||||
// 渲染计划卡片(在 updateSegmentsRealtime 中使用)
|
||||
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
|
||||
// 检查是否已回答
|
||||
const isAnswered = answeredQuestions.has(segment.askId);
|
||||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||||
|
||||
if (isAnswered) {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
||||
).join('');
|
||||
|
||||
// 选项按钮
|
||||
const options = ['确认执行', '修改计划', '取消'];
|
||||
const optionsHtml = options.map(opt => {
|
||||
const isSelected = isAnswered && opt === selectedAnswer;
|
||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||
}).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
<div class="plan-header">
|
||||
<span class="plan-icon">${plannerIconSvg}</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||
<div class="plan-steps">\${stepsHtml}</div>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<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="输入修改建议..." />
|
||||
<button class="custom-submit">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 只在未回答时添加事件监听
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||||
optionButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const option = this.getAttribute('data-option');
|
||||
// 发送答案到后端
|
||||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||||
// 同时发送 planAction 用于模式切换
|
||||
const actionMap = {
|
||||
'确认执行': 'confirm',
|
||||
'修改计划': 'modify',
|
||||
'取消': 'cancel'
|
||||
};
|
||||
vscode.postMessage({
|
||||
command: 'planAction',
|
||||
action: actionMap[option] || option,
|
||||
planTitle: segment.planTitle
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
const customInput = segmentDiv.querySelector('.custom-input');
|
||||
if (submitBtn && customInput) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染计划卡片(在 renderSegments 中使用)
|
||||
function renderPlanCardStatic(segment, segmentDiv) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
||||
).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
<div class="plan-header">
|
||||
<span class="plan-icon">📋</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||
<div class="plan-steps">\${stepsHtml}</div>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
|
||||
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 绑定按钮事件
|
||||
setTimeout(() => {
|
||||
const planCard = segmentDiv.querySelector('.plan-card');
|
||||
if (planCard) {
|
||||
planCard.querySelectorAll('.plan-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const action = e.currentTarget?.dataset?.action;
|
||||
vscode.postMessage({
|
||||
command: 'planAction',
|
||||
action: action,
|
||||
planTitle: segment.planTitle
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -1,19 +1,21 @@
|
||||
/**
|
||||
* Plan 开关组件
|
||||
* 注意:功能已移至模式选择器,此组件仅保留样式(已禁用)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取 Plan 开关的 HTML 内容
|
||||
* 已禁用,仅保留样式展示
|
||||
*/
|
||||
export function getPlanToggleContent(): string {
|
||||
return `
|
||||
<div class="tooltip">
|
||||
<label class="plan-toggle">
|
||||
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
|
||||
<label class="plan-toggle plan-toggle-disabled">
|
||||
<input type="checkbox" id="planToggle" disabled>
|
||||
<span class="plan-toggle-slider"></span>
|
||||
<span class="plan-toggle-label">Plan</span>
|
||||
</label>
|
||||
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span>
|
||||
<span class="tooltiptext" id="planTooltip">请使用模式选择器切换 Plan 模式</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -73,6 +75,17 @@ export function getPlanToggleStyles(): string {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
/* 禁用状态样式 */
|
||||
.plan-toggle-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.plan-toggle-disabled .plan-toggle-slider {
|
||||
background: var(--vscode-input-background);
|
||||
border-color: var(--vscode-input-border);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@ -263,61 +263,63 @@ export function getWaveformPreviewScript(): string {
|
||||
const timeRange = maxTime - minTime || 1;
|
||||
|
||||
// 绘制波形
|
||||
let pathData = '';
|
||||
let lastX = leftMargin;
|
||||
let lastValue = signal.values[0].value;
|
||||
if (signal.width === 1) {
|
||||
// 单比特信号 - 绘制数字波形
|
||||
let pathData = '';
|
||||
const yHigh = y;
|
||||
const yLow = y + signalHeight;
|
||||
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
const value = point.value;
|
||||
|
||||
if (signal.width === 1) {
|
||||
// 单比特信号 - 绘制数字波形
|
||||
const yHigh = y;
|
||||
const yLow = y + signalHeight;
|
||||
const currentY = (value === '1') ? yHigh : yLow;
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
const currentY = (point.value === '1') ? yHigh : yLow;
|
||||
|
||||
if (i === 0) {
|
||||
pathData = \`M \${x} \${currentY}\`;
|
||||
} else {
|
||||
// 绘制垂直跳变
|
||||
const prevY = (lastValue === '1') ? yHigh : yLow;
|
||||
const prevValue = signal.values[i - 1].value;
|
||||
const prevY = (prevValue === '1') ? yHigh : yLow;
|
||||
if (prevY !== currentY) {
|
||||
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
|
||||
} else {
|
||||
pathData += \` L \${x} \${currentY}\`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastValue = value;
|
||||
lastX = x;
|
||||
} else {
|
||||
// 多比特信号 - 绘制总线波形(梯形)
|
||||
const yTop = y + 5;
|
||||
const yBottom = y + signalHeight - 5;
|
||||
const transitionWidth = 5;
|
||||
|
||||
if (i === 0) {
|
||||
pathData = \`M \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
||||
} else {
|
||||
// 绘制梯形过渡
|
||||
pathData += \` L \${x - transitionWidth} \${yTop} L \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
}
|
||||
});
|
||||
|
||||
// 延伸到右边界
|
||||
if (signal.width === 1) {
|
||||
const lastY = (lastValue === '1') ? y : (y + signalHeight);
|
||||
// 延伸到右边界
|
||||
const lastValue = signal.values[signal.values.length - 1].value;
|
||||
const lastY = (lastValue === '1') ? yHigh : yLow;
|
||||
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`;
|
||||
} else {
|
||||
const yMid = y + signalHeight / 2;
|
||||
pathData += \` L \${leftMargin + waveformWidth} \${yMid}\`;
|
||||
}
|
||||
|
||||
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
} else {
|
||||
// 多比特信号 - 绘制总线波形(上下双线)
|
||||
const yTop = y + 5;
|
||||
const yBottom = y + signalHeight - 5;
|
||||
const transitionWidth = 4;
|
||||
|
||||
let topPath = \`M \${leftMargin} \${yTop}\`;
|
||||
let bottomPath = \`M \${leftMargin} \${yBottom}\`;
|
||||
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
|
||||
// 上线和下线都延伸到变化点
|
||||
topPath += \` L \${x} \${yTop}\`;
|
||||
bottomPath += \` L \${x} \${yBottom}\`;
|
||||
|
||||
// 绘制梯形过渡
|
||||
topPath += \` L \${x + transitionWidth} \${yBottom} L \${x + transitionWidth} \${yTop}\`;
|
||||
bottomPath += \` L \${x + transitionWidth} \${yTop} L \${x + transitionWidth} \${yBottom}\`;
|
||||
});
|
||||
|
||||
// 延伸到右边界
|
||||
topPath += \` L \${leftMargin + waveformWidth} \${yTop}\`;
|
||||
bottomPath += \` L \${leftMargin + waveformWidth} \${yBottom}\`;
|
||||
|
||||
svgContent += \`<path d="\${topPath}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
svgContent += \`<path d="\${bottomPath}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
}
|
||||
});
|
||||
|
||||
// 绘制时间轴
|
||||
|
||||
@ -17,10 +17,17 @@ import {
|
||||
getMessageAreaStyles,
|
||||
getMessageAreaScript,
|
||||
} from "./messageArea";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
/**
|
||||
* 获取 WebView 面板的 HTML 内容
|
||||
*/
|
||||
export function getWebviewContent(iconUri?: string): string {
|
||||
export function getWebviewContent(
|
||||
iconUri?: string,
|
||||
autoIconUri?: string,
|
||||
liteIconUri?: string,
|
||||
syIconUri?: string,
|
||||
maxIconUri?: string
|
||||
): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@ -65,6 +72,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
${getMessageAreaStyles()}
|
||||
${getAgentCardStyles()}
|
||||
${getWaveformPreviewContent()}
|
||||
${getConversationHistoryBarStyles()}
|
||||
${getInputAreaStyles()}
|
||||
@ -250,7 +258,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 22 px;
|
||||
padding: 10px 22px;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
@ -389,13 +397,14 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
<span id="statusText">思考中...</span>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<!-- <div class="quick-actions">
|
||||
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
|
||||
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
|
||||
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
|
||||
</div>
|
||||
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
|
||||
</div> -->
|
||||
|
||||
${getInputAreaContent()}
|
||||
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@ -411,11 +420,68 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
let loadingIndicator = null;
|
||||
let currentSegmentedMessage = null; // 当前分段消息容器
|
||||
|
||||
// ========== 模式选择器脚本(直接内联,避免模板字符串嵌套问题)==========
|
||||
let currentMode = 'agent';
|
||||
|
||||
function toggleModeDropdown() {
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
const modelSelectEl = document.getElementById('modelSelect');
|
||||
if (modeSelectEl) {
|
||||
modeSelectEl.classList.toggle('active');
|
||||
if (modelSelectEl) {
|
||||
modelSelectEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectMode(value, label) {
|
||||
currentMode = value;
|
||||
const modeValue = document.getElementById('modeValue');
|
||||
const modeTooltip = document.getElementById('modeTooltip');
|
||||
if (modeValue) {
|
||||
modeValue.textContent = label;
|
||||
}
|
||||
if (modeTooltip) {
|
||||
const tooltipMap = {
|
||||
'plan': '只读模式 - 只能查询分析',
|
||||
'ask': '逐个确认 - 每个写操作需确认',
|
||||
'agent': '智能体自主模式',
|
||||
'auto': '完全自动 - 所有操作自动执行'
|
||||
};
|
||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||
}
|
||||
const options = document.querySelectorAll('.mode-option');
|
||||
options.forEach(option => {
|
||||
if (option.getAttribute('data-value') === value) {
|
||||
option.classList.add('selected');
|
||||
} else {
|
||||
option.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
if (modeSelectEl) {
|
||||
modeSelectEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentMode() {
|
||||
return currentMode;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
if (modeSelectEl && !modeSelectEl.contains(event.target)) {
|
||||
modeSelectEl.classList.remove('active');
|
||||
}
|
||||
});
|
||||
// ========== 模式选择器脚本结束 ==========
|
||||
|
||||
function quickAction(type) {
|
||||
const questions = {
|
||||
counter: '生成一个4位同步计数器',
|
||||
fsm: '生成一个状态机',
|
||||
testbench: '生成测试平台'
|
||||
testbench: '生成测试平台',
|
||||
explore: '请启动知识探索智能体,分析当前项目结构'
|
||||
};
|
||||
if (questions[type]) {
|
||||
messageInput.value = questions[type];
|
||||
@ -436,8 +502,6 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
|
||||
// 监听来自插件的消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
@ -504,11 +568,32 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'resetSegmentedMessage':
|
||||
// 重置分段消息容器(停止对话时调用)
|
||||
console.log('[WebView] 重置分段消息容器');
|
||||
currentSegmentedMessage = null;
|
||||
break;
|
||||
|
||||
case 'contextUsage':
|
||||
// 更新上下文使用量显示
|
||||
if (typeof updateContextDisplay === 'function') {
|
||||
updateContextDisplay(message.currentTokens, message.maxTokens);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'hideLoading':
|
||||
// 隐藏加载指示器
|
||||
hideLoadingIndicator();
|
||||
break;
|
||||
|
||||
case 'workspaceStatus':
|
||||
// 更新工作区状态
|
||||
if (typeof hasWorkspace !== 'undefined') {
|
||||
hasWorkspace = message.hasWorkspace;
|
||||
console.log('[WebView] 工作区状态:', hasWorkspace);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'vcdInfo':
|
||||
// 渲染迷你波形预览信息
|
||||
try {
|
||||
@ -581,12 +666,34 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'switchMode':
|
||||
// 切换运行模式(Plan 确认后自动切换到 Agent)
|
||||
if (message.mode && typeof selectMode === 'function') {
|
||||
const labelMap = {
|
||||
'plan': 'Plan',
|
||||
'ask': 'Ask',
|
||||
'agent': 'Agent',
|
||||
'auto': 'Auto'
|
||||
};
|
||||
selectMode(message.mode, labelMap[message.mode] || message.mode);
|
||||
console.log('[WebView] 模式已切换到:', message.mode);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addMessage':
|
||||
// 添加消息(通用)
|
||||
if (message.text && message.sender) {
|
||||
addMessage(message.text, message.sender);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||
}
|
||||
});
|
||||
|
||||
${getMessageAreaScript()}
|
||||
${getAgentCardScript()}
|
||||
${getWaveformPreviewScript()}
|
||||
${getConversationHistoryBarScript()}
|
||||
${getInputAreaScript()}
|
||||
|
||||
42
tools/waveform_trace/build.bat
Normal file
42
tools/waveform_trace/build.bat
Normal file
@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
REM waveform_trace 打包脚本 (Windows)
|
||||
REM 用法: build.bat
|
||||
|
||||
echo ========================================
|
||||
echo waveform_trace 打包脚本
|
||||
echo ========================================
|
||||
|
||||
cd /d "%~dp0src"
|
||||
|
||||
echo.
|
||||
echo [1/3] 安装依赖...
|
||||
pip install -r requirements.txt
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 依赖安装失败
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [2/3] 清理旧文件...
|
||||
if exist build rmdir /s /q build
|
||||
if exist dist rmdir /s /q dist
|
||||
if exist waveform_trace.spec del waveform_trace.spec
|
||||
|
||||
echo.
|
||||
echo [3/3] PyInstaller 打包...
|
||||
pyinstaller --onefile --name waveform_trace --collect-all pyverilog waveform_trace_cli.py
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 打包失败
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [4/4] 复制到 bin 目录...
|
||||
if not exist "..\bin" mkdir "..\bin"
|
||||
copy /y "dist\waveform_trace.exe" "..\bin\"
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 打包完成!
|
||||
echo 输出: tools/waveform_trace/bin/waveform_trace.exe
|
||||
echo ========================================
|
||||
35
tools/waveform_trace/build.sh
Normal file
35
tools/waveform_trace/build.sh
Normal file
@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# waveform_trace 打包脚本 (Linux/macOS)
|
||||
# 用法: ./build.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================"
|
||||
echo " waveform_trace 打包脚本"
|
||||
echo "========================================"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR/src"
|
||||
|
||||
echo ""
|
||||
echo "[1/4] 安装依赖..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo ""
|
||||
echo "[2/4] 清理旧文件..."
|
||||
rm -rf build dist *.spec
|
||||
|
||||
echo ""
|
||||
echo "[3/4] PyInstaller 打包..."
|
||||
pyinstaller --onefile --name waveform_trace --collect-all pyverilog waveform_trace_cli.py
|
||||
|
||||
echo ""
|
||||
echo "[4/4] 复制到 bin 目录..."
|
||||
mkdir -p ../bin
|
||||
cp dist/waveform_trace ../bin/
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " 打包完成!"
|
||||
echo " 输出: tools/waveform_trace/bin/waveform_trace"
|
||||
echo "========================================"
|
||||
115
tools/waveform_trace/src/README.md
Normal file
115
tools/waveform_trace/src/README.md
Normal file
@ -0,0 +1,115 @@
|
||||
# AST 波形调试核心代码
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 作用 | 核心函数 | TS重写需要 |
|
||||
|------|------|----------|------------|
|
||||
| `ast_node.py` | AST节点定义,遍历建图 | `toplogic_tree_traverse()` | ✅ 已完成 |
|
||||
| `graph_builder.py` | 入口函数,调用解析器 | `generate_top_logic_graph()` | ✅ 已完成 |
|
||||
| `debug_graph_analyzer.py` | BFS回溯控制信号 | `get_k_control_signals()` | ⚠️ 需重写 |
|
||||
| `vcd_waveform_analyzer.py` | VCD波形文件解析 | `parse_mismatch()`, `get_tabular()` | ⚠️ 需重写 |
|
||||
| `waveform_trace_tool.py` | 完整追踪工具封装 | `waveform_trace_tool()` | ⚠️ 需重写 |
|
||||
|
||||
---
|
||||
|
||||
## 调用流程
|
||||
|
||||
```
|
||||
Verilog代码文件
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ graph_builder.py │
|
||||
│ generate_top_logic_graph(filelist) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ PyVerilog.parse() → AST │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ast.toplogic_tree_traverse() │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ NetworkX 有向图(信号依赖图) │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ debug_graph_analyzer.py │
|
||||
│ DebugGraph.get_k_control_signals() │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ BFS回溯K层,找到控制信号链 │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ vcd_waveform_analyzer.py │
|
||||
│ parse_mismatch() + get_tabular() │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 提取相关信号的波形表 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心代码位置
|
||||
|
||||
### 1. AST遍历建图 (ast_node.py:32-137)
|
||||
|
||||
```python
|
||||
def toplogic_tree_traverse(self, network_G, rvalue=False, lvalue=False, offset=0):
|
||||
"""
|
||||
递归遍历AST,提取信号依赖关系,填充到NetworkX图中
|
||||
|
||||
关键逻辑:
|
||||
1. 识别 Rvalue(右值)和 Lvalue(左值)
|
||||
2. 递归收集子节点的信号
|
||||
3. 建立边:右值信号 → 左值信号(控制关系)
|
||||
"""
|
||||
```
|
||||
|
||||
### 2. 图构建入口 (graph_builder.py:89-99)
|
||||
|
||||
```python
|
||||
def generate_top_logic_graph(filelist: list[str]):
|
||||
# 1. PyVerilog解析Verilog代码
|
||||
ast, directives = parse(filelist, preprocess_include=[], preprocess_define=[])
|
||||
# 2. 遍历AST,构建信号依赖图
|
||||
return create_graph_from_ast(ast, display=False, display_signal_only=False)
|
||||
```
|
||||
|
||||
### 3. BFS回溯 (debug_graph_analyzer.py:20-66)
|
||||
|
||||
```python
|
||||
def get_k_control_signals(self, target_signals: list[str], k: int, signal_only: bool = False):
|
||||
"""
|
||||
从出错信号出发,BFS回溯K层,找到所有控制信号
|
||||
|
||||
输入:target_signals = ['out'] # 出错的信号
|
||||
输出:control_signals = {'out': (10,10), 'state': (5,8), 'clk': (1,1)}
|
||||
signal_level_tracer = [['clk->state', 'reset->state'], ['state->out']]
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖库
|
||||
|
||||
```
|
||||
pyverilog # Verilog解析,生成AST
|
||||
networkx # 图数据结构
|
||||
pandas # 波形数据处理(可选)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 如果要用JavaScript重写
|
||||
|
||||
需要重写的核心逻辑:
|
||||
|
||||
1. **Verilog解析器** → 用 ANTLR4 + Verilog.g4 或 tree-sitter-verilog
|
||||
2. **AST遍历建图** → 约100行,参考 ast_node.py:32-137
|
||||
3. **BFS回溯** → 约70行,参考 debug_graph_analyzer.py
|
||||
|
||||
总计约 **200行核心逻辑**(不含解析器)
|
||||
455
tools/waveform_trace/src/TS_REWRITE_SPEC.md
Normal file
455
tools/waveform_trace/src/TS_REWRITE_SPEC.md
Normal file
@ -0,0 +1,455 @@
|
||||
# AST波形调试工具 - TypeScript重写规范
|
||||
|
||||
## 一、项目背景
|
||||
|
||||
将Python实现的Verilog AST波形调试工具重写为TypeScript,用于VSCode插件。
|
||||
|
||||
**已完成部分**:
|
||||
- ✅ Verilog AST解析(生成JSON格式的信号依赖图)
|
||||
- ✅ 图结构定义
|
||||
|
||||
**待重写部分**:
|
||||
- ⚠️ BFS信号回溯
|
||||
- ⚠️ VCD波形解析
|
||||
- ⚠️ 仿真输出解析
|
||||
- ⚠️ 工具整合封装
|
||||
|
||||
---
|
||||
|
||||
## 二、数据结构定义
|
||||
|
||||
### 2.1 AST图结构(已完成)
|
||||
|
||||
```typescript
|
||||
interface ASTNode {
|
||||
id: string;
|
||||
attributes: {
|
||||
lines: [number, number]; // [起始行, 结束行]
|
||||
type: string; // Input/Output/Reg/Wire/Always/Assign等
|
||||
};
|
||||
}
|
||||
|
||||
interface ASTEdge {
|
||||
from: string; // 控制信号
|
||||
to: string; // 被控制信号
|
||||
attributes: {
|
||||
lines: [number, number];
|
||||
type: string; // Always/Assign/IfStatement等
|
||||
};
|
||||
}
|
||||
|
||||
interface ASTGraph {
|
||||
metadata: {
|
||||
moduleName: string;
|
||||
nodeCount: number;
|
||||
edgeCount: number;
|
||||
generatedAt: string;
|
||||
};
|
||||
nodes: ASTNode[];
|
||||
edges: ASTEdge[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 追踪结果结构
|
||||
|
||||
```typescript
|
||||
interface TraceResult {
|
||||
controlSignals: Map<string, [number, number]>; // 信号名 -> 代码行号
|
||||
signalLevelTracer: string[][]; // 每层的控制关系链
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 波形数据结构
|
||||
|
||||
```typescript
|
||||
interface WaveformData {
|
||||
time: number; // 时间点(ns)
|
||||
signals: {
|
||||
[signalName: string]: string; // 信号名 -> 值(十六进制)
|
||||
};
|
||||
}
|
||||
|
||||
interface MismatchInfo {
|
||||
signals: string[]; // 出错的信号列表
|
||||
firstMismatchTime: number; // 第一次出错的时间
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、需要重写的模块
|
||||
|
||||
### 3.1 BFS信号回溯模块
|
||||
|
||||
**源文件**: `debug_graph_analyzer.py`
|
||||
**代码行数**: ~70行
|
||||
**第三方依赖**: 无
|
||||
|
||||
#### 功能描述
|
||||
从出错信号出发,BFS反向遍历图,找到所有控制该信号的上游信号。
|
||||
|
||||
#### 输入输出
|
||||
```typescript
|
||||
// 输入
|
||||
graph: ASTGraph // AST图(JSON格式)
|
||||
targetSignals: string[] // 出错的信号列表,如 ['count', 'overflow']
|
||||
k: number // 回溯层数
|
||||
signalOnly: boolean // 是否只返回信号节点(过滤Always/Assign等)
|
||||
|
||||
// 输出
|
||||
TraceResult {
|
||||
controlSignals: Map<string, [number, number]>,
|
||||
signalLevelTracer: string[][]
|
||||
}
|
||||
```
|
||||
|
||||
#### 核心算法(伪代码)
|
||||
```
|
||||
1. 构建前驱映射(反向边)
|
||||
for each edge in graph.edges:
|
||||
predecessorMap[edge.to].push(edge.from)
|
||||
|
||||
2. 初始化BFS队列
|
||||
for each signal in targetSignals:
|
||||
queue.push([signal, signal])
|
||||
controlSignals.set(signal, node.lines)
|
||||
|
||||
3. BFS遍历K层
|
||||
for level = 0 to k:
|
||||
while queue not empty:
|
||||
[curSignal, controlledSignal] = queue.pop()
|
||||
记录关系: curSignal -> controlledSignal
|
||||
|
||||
for each predecessor of curSignal:
|
||||
if not visited and not filtered:
|
||||
queue.push([predecessor, curSignal])
|
||||
|
||||
记录本层关系到 signalLevelTracer
|
||||
|
||||
4. 返回结果
|
||||
```
|
||||
|
||||
#### 过滤规则
|
||||
```typescript
|
||||
// 需要过滤的节点类型
|
||||
const FILTERED_TYPES = ['Parameter', 'Localparam'];
|
||||
|
||||
// signalOnly=true时,还需要过滤以下前缀
|
||||
const FILTERED_PREFIXES = ['Always', 'Assign', 'Module', 'IntConst'];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 仿真输出解析模块
|
||||
|
||||
**源文件**: `vcd_waveform_analyzer.py` 中的 `parse_mismatch()`
|
||||
**代码行数**: ~20行
|
||||
**第三方依赖**: 无
|
||||
|
||||
#### 功能描述
|
||||
解析仿真工具的输出文本,提取出错信号名和出错时间。
|
||||
|
||||
#### 输入输出
|
||||
```typescript
|
||||
// 输入
|
||||
testOutput: string // 仿真工具的输出文本
|
||||
|
||||
// 输出
|
||||
MismatchInfo {
|
||||
signals: string[], // 出错信号列表
|
||||
firstMismatchTime: number // 第一次出错时间(ns)
|
||||
}
|
||||
```
|
||||
|
||||
#### 解析规则
|
||||
```typescript
|
||||
// 需要匹配的格式
|
||||
// "First mismatch occurred at time 100. Output 'count' ..."
|
||||
|
||||
const pattern = /First mismatch occurred at time (\d+).*Output '(\w+)'/g;
|
||||
|
||||
// 提取所有匹配
|
||||
// 返回信号列表和最小时间戳
|
||||
```
|
||||
|
||||
#### 示例
|
||||
```
|
||||
输入:
|
||||
"First mismatch occurred at time 100. Output 'count' expected 0001, got 0000
|
||||
First mismatch occurred at time 150. Output 'overflow' expected 1, got 0"
|
||||
|
||||
输出:
|
||||
{
|
||||
signals: ['count', 'overflow'],
|
||||
firstMismatchTime: 100
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 VCD波形解析模块
|
||||
|
||||
**源文件**: `vcd_waveform_analyzer.py` 中的 `get_tabular()` 和 `tabular_via_dataframe()`
|
||||
**代码行数**: ~150行
|
||||
**第三方依赖**: Python版用了 `vcdvcd`, `pandas`, `numpy`
|
||||
|
||||
#### 功能描述
|
||||
读取VCD(Value Change Dump)波形文件,提取指定信号的波形值,生成表格。
|
||||
|
||||
#### VCD文件格式简介
|
||||
```vcd
|
||||
$timescale 1ns $end
|
||||
$scope module tb $end
|
||||
$var wire 1 ! clk $end
|
||||
$var wire 8 " count [7:0] $end
|
||||
$upscope $end
|
||||
$enddefinitions $end
|
||||
#0
|
||||
b0 "
|
||||
1!
|
||||
#5
|
||||
0!
|
||||
#10
|
||||
1!
|
||||
b00000001 "
|
||||
...
|
||||
```
|
||||
|
||||
#### 输入输出
|
||||
```typescript
|
||||
// 输入
|
||||
vcdPath: string // VCD文件路径
|
||||
signalsToTrace: string[] // 需要提取的信号列表
|
||||
offset: number // 时间偏移(从哪个时间点开始)
|
||||
windowSize: number // 窗口大小(提取多少个时间点)
|
||||
|
||||
// 输出
|
||||
string // 格式化的波形表格字符串
|
||||
```
|
||||
|
||||
#### 输出格式示例
|
||||
```
|
||||
### First mismatched signals time(ns) Trace ###
|
||||
time(ns) clk reset count_ref count_dut
|
||||
0 1 1 00 00
|
||||
5 0 1 00 00
|
||||
10 1 0 00 00
|
||||
15 0 0 00 00
|
||||
20 1 0 01 00 <- mismatch
|
||||
### First mismatched signals time(ns) End ###
|
||||
```
|
||||
|
||||
#### TS实现建议
|
||||
1. **方案A**: 找现有的JS VCD解析库
|
||||
- 搜索: `npm vcd parser`, `vcd-stream`, `wavedrom`
|
||||
|
||||
2. **方案B**: 自己实现简单的VCD解析器
|
||||
- VCD格式相对简单,核心是解析变量定义和时间变化
|
||||
- 约100-150行代码
|
||||
|
||||
#### VCD解析核心逻辑
|
||||
```typescript
|
||||
class VCDParser {
|
||||
signals: Map<string, Signal>; // 信号定义
|
||||
timeValues: Map<number, Map<string, string>>; // 时间 -> 信号值
|
||||
|
||||
parse(vcdContent: string): void {
|
||||
// 1. 解析头部($var定义)
|
||||
// 2. 解析数据部分(#时间 和 值变化)
|
||||
}
|
||||
|
||||
getSignalValues(signalName: string, startTime: number, endTime: number): WaveformData[] {
|
||||
// 提取指定信号在时间范围内的值
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 工具整合封装模块
|
||||
|
||||
**源文件**: `waveform_trace_tool.py`
|
||||
**代码行数**: ~150行
|
||||
**第三方依赖**: 依赖上面所有模块
|
||||
|
||||
#### 功能描述
|
||||
整合所有模块,提供统一的调试接口。
|
||||
|
||||
#### 输入输出
|
||||
```typescript
|
||||
// 输入
|
||||
verilogFilePath: string // Verilog文件路径
|
||||
vcdFilePath: string // VCD波形文件路径
|
||||
simulationOutput: string // 仿真输出文本
|
||||
traceLevel: number // 回溯层数
|
||||
|
||||
// 输出
|
||||
string // 完整的调试报告
|
||||
```
|
||||
|
||||
#### 调试报告格式
|
||||
```
|
||||
[Signal Traces] Backtrace control signal relations.
|
||||
clk->count
|
||||
reset->count
|
||||
-count->state
|
||||
--state->out (*last output port level)
|
||||
|
||||
[Signal Waveform]:
|
||||
<signal>_ref 是期望值(golden)
|
||||
<signal>_dut 是实际输出
|
||||
[Traced Signals]: out, state, count, clk, reset
|
||||
|
||||
[Table Waveform in hexadecimal format]
|
||||
time(ns) clk reset count_ref count_dut
|
||||
...
|
||||
|
||||
[Verilog of DUT]:
|
||||
```verilog
|
||||
module counter(...);
|
||||
...
|
||||
endmodule
|
||||
```
|
||||
|
||||
[Hint] ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、调用流程图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ waveform_trace_tool() │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. 检查文件是否存在 │
|
||||
│ ├── verilogFilePath │
|
||||
│ └── vcdFilePath │
|
||||
│ │
|
||||
│ 2. 加载AST图(已有JSON) │
|
||||
│ └── graph = loadASTGraph(verilogFilePath) │
|
||||
│ │
|
||||
│ 3. 解析仿真输出,获取出错信号 │
|
||||
│ └── mismatchInfo = parseMismatch(simulationOutput) │
|
||||
│ ├── signals: ['count', 'overflow'] │
|
||||
│ └── firstMismatchTime: 100 │
|
||||
│ │
|
||||
│ 4. BFS回溯,找到控制信号链 │
|
||||
│ └── traceResult = getKControlSignals(graph, signals, k) │
|
||||
│ ├── controlSignals: Map<信号名, 行号> │
|
||||
│ └── signalLevelTracer: [['clk->count'], ...] │
|
||||
│ │
|
||||
│ 5. 读取VCD波形,提取相关信号的值 │
|
||||
│ └── waveformTable = getTabular(vcdPath, signals, offset) │
|
||||
│ │
|
||||
│ 6. 读取Verilog源码 │
|
||||
│ └── verilogCode = readFile(verilogFilePath) │
|
||||
│ │
|
||||
│ 7. 组装调试报告 │
|
||||
│ └── return formatReport(traceResult, waveformTable, code) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、参考实现
|
||||
|
||||
### 5.1 Python源文件位置
|
||||
|
||||
```
|
||||
ast_debug_core/
|
||||
├── ast_node.py # AST节点定义(参考32-137行)
|
||||
├── graph_builder.py # 图构建入口
|
||||
├── debug_graph_analyzer.py # BFS回溯(完整文件,约70行)
|
||||
├── vcd_waveform_analyzer.py # VCD解析(参考89-285行)
|
||||
└── waveform_trace_tool.py # 工具封装(完整文件,约180行)
|
||||
```
|
||||
|
||||
### 5.2 关键函数对照表
|
||||
|
||||
| Python函数 | 位置 | TS函数名建议 |
|
||||
|------------|------|--------------|
|
||||
| `get_k_control_signals()` | debug_graph_analyzer.py:20 | `getKControlSignals()` |
|
||||
| `parse_mismatch()` | vcd_waveform_analyzer.py:244 | `parseMismatch()` |
|
||||
| `get_tabular()` | vcd_waveform_analyzer.py:264 | `getTabular()` |
|
||||
| `tabular_via_dataframe()` | vcd_waveform_analyzer.py:95 | `generateWaveformTable()` |
|
||||
| `waveform_trace_tool()` | waveform_trace_tool.py:63 | `waveformTraceTool()` |
|
||||
|
||||
---
|
||||
|
||||
## 六、测试用例
|
||||
|
||||
### 6.1 BFS回溯测试
|
||||
|
||||
```typescript
|
||||
// 输入
|
||||
const graph: ASTGraph = /* 加载 counter_ast_graph.json */;
|
||||
const targetSignals = ['count'];
|
||||
const k = 2;
|
||||
|
||||
// 期望输出
|
||||
const expected = {
|
||||
controlSignals: new Map([
|
||||
['count', [6, 6]],
|
||||
['next_count', [10, 10]],
|
||||
['reset', [4, 4]],
|
||||
['clk', [3, 3]],
|
||||
['enable', [5, 5]]
|
||||
]),
|
||||
signalLevelTracer: [
|
||||
['count->count'],
|
||||
['next_count->count', 'reset->count', 'clk->count'],
|
||||
['enable->next_count', 'count->next_count']
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 仿真输出解析测试
|
||||
|
||||
```typescript
|
||||
// 输入
|
||||
const testOutput = `
|
||||
Mismatches: 2
|
||||
First mismatch occurred at time 100. Output 'count' expected 0001, got 0000
|
||||
First mismatch occurred at time 150. Output 'overflow' expected 1, got 0
|
||||
`;
|
||||
|
||||
// 期望输出
|
||||
const expected = {
|
||||
signals: ['count', 'overflow'],
|
||||
firstMismatchTime: 100
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
1. **无第三方依赖要求**
|
||||
- BFS回溯和仿真解析完全可以用原生TS实现
|
||||
- VCD解析可以自己实现或找现有库
|
||||
|
||||
2. **性能考虑**
|
||||
- 图遍历使用Map而非Object,提高查找效率
|
||||
- VCD文件可能很大,考虑流式解析
|
||||
|
||||
3. **错误处理**
|
||||
- 文件不存在时返回友好错误信息
|
||||
- 信号不在图中时跳过而非报错
|
||||
|
||||
4. **兼容性**
|
||||
- 信号名可能包含方括号,如 `count[7:0]`
|
||||
- 时间单位统一为ns
|
||||
|
||||
---
|
||||
|
||||
## 八、交付物
|
||||
|
||||
1. `debugGraphAnalyzer.ts` - BFS回溯模块
|
||||
2. `simulationParser.ts` - 仿真输出解析模块
|
||||
3. `vcdParser.ts` - VCD波形解析模块
|
||||
4. `waveformTraceTool.ts` - 工具整合封装
|
||||
5. `types.ts` - 类型定义
|
||||
6. 单元测试文件
|
||||
1403
tools/waveform_trace/src/ast_node.py
Normal file
1403
tools/waveform_trace/src/ast_node.py
Normal file
File diff suppressed because it is too large
Load Diff
70
tools/waveform_trace/src/debug_graph_analyzer.py
Normal file
70
tools/waveform_trace/src/debug_graph_analyzer.py
Normal file
@ -0,0 +1,70 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Author : Chia-Tung (Mark) Ho, NVIDIA
|
||||
#
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections import deque
|
||||
from graph_builder import generate_top_logic_graph
|
||||
|
||||
# use class
|
||||
class DebugGraph:
|
||||
|
||||
def __init__(self, verilog_filelist: list[str]):
|
||||
self.filelist = verilog_filelist
|
||||
self.graph = generate_top_logic_graph(verilog_filelist)
|
||||
# print(list(self.graph.nodes(data=True)))
|
||||
|
||||
def get_k_control_signals(self, target_signals: list[str], k:int, signal_only: bool=False) -> list[str]:
|
||||
|
||||
control_signals = {}
|
||||
signal_level_tracer = []
|
||||
# queue
|
||||
q = deque()
|
||||
tmp_q = deque()
|
||||
|
||||
for signal in target_signals:
|
||||
# store (predecessors, controlled signal)
|
||||
q.append((signal, signal))
|
||||
control_signals[signal] = self.graph.nodes[signal]['lines']
|
||||
|
||||
# BFS
|
||||
for l in range (k + 1):
|
||||
# traverse l layers
|
||||
tmp_q.clear()
|
||||
level_signal_control_rels = []
|
||||
while len(q) > 0:
|
||||
cur_signal = q.popleft()
|
||||
level_signal_control_rels.append(cur_signal[0] + "->" + cur_signal[1])
|
||||
if cur_signal[0] not in control_signals:
|
||||
if self.graph.has_edge(cur_signal[0], cur_signal[1]):
|
||||
# must be the control signals through the edge
|
||||
control_signals[cur_signal[0]] = self.graph[cur_signal[0]][cur_signal[1]]['lines']
|
||||
else:
|
||||
print("[Error] Edge not found! - ", cur_signal)
|
||||
# find the predecessors
|
||||
controls = self.graph.predecessors(cur_signal[0])
|
||||
for c in controls:
|
||||
if c in control_signals:
|
||||
continue
|
||||
# exclude the parameter
|
||||
if 'type' in self.graph.nodes[c] and self.graph.nodes[c]['type'] in ["Parameter", "Localparam"]:
|
||||
continue
|
||||
if signal_only and (re.match('^Always', c) or re.match('^Assign', c) or re.match('^Module', c) or re.match('^IntConst', c)):
|
||||
continue
|
||||
# store (predecessors, controlled signal)
|
||||
tmp_q.append((c, cur_signal[0]))
|
||||
# swap the q
|
||||
assert(len(q) == 0)
|
||||
print(tmp_q)
|
||||
q = copy.deepcopy(tmp_q)
|
||||
# record the signal relations
|
||||
signal_level_tracer.append(level_signal_control_rels)
|
||||
|
||||
return control_signals, signal_level_tracer
|
||||
|
||||
if __name__ == '__main__':
|
||||
debug_graph_tracer = DebugGraph(["/home/scratch.chiatungh_nvresearch/hardware-agent-marco/hardware_agent/examples/verilog_testcases/fsm_serialdata.v"])
|
||||
print(debug_graph_tracer.get_k_control_signals(['out_byte', 'done'], k=3, signal_only=True))
|
||||
144
tools/waveform_trace/src/graph_builder.py
Normal file
144
tools/waveform_trace/src/graph_builder.py
Normal file
@ -0,0 +1,144 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Author : Chia-Tung (Mark) Ho, NVIDIA
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import os
|
||||
from optparse import OptionParser
|
||||
|
||||
# 优先使用本地修改过的 pyverilog(包含 toplogic_tree_traverse 方法)
|
||||
_local_path = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, _local_path)
|
||||
|
||||
from pyverilog.vparser.parser import parse
|
||||
from io import StringIO
|
||||
import networkx as nx
|
||||
# importing matplotlib.pyplot
|
||||
import matplotlib.pyplot as plt
|
||||
import re
|
||||
|
||||
# create graph from ast str
|
||||
# directed graph from networkX
|
||||
def create_graph_from_ast(ast, display=False, display_signal_only=False):
|
||||
graph = nx.DiGraph()
|
||||
ast.toplogic_tree_traverse(network_G=graph, rvalue=False, lvalue=False)
|
||||
if not display and not display_signal_only:
|
||||
return graph
|
||||
# Print out nodes with attributes
|
||||
nodes_to_display = []
|
||||
edges_to_display = []
|
||||
print("Nodes:")
|
||||
for node, attrs in graph.nodes(data=True):
|
||||
if display_signal_only and (not re.match("^Assign", node) and not re.match("^Always", node) and not re.match("^Module", node)):
|
||||
nodes_to_display.append(node)
|
||||
print(f"Node {node}: {attrs}")
|
||||
|
||||
# Print out edges with attributes
|
||||
print("\nEdges:")
|
||||
for src, dst, attrs in graph.edges(data=True):
|
||||
if display_signal_only and src in nodes_to_display and dst in nodes_to_display:
|
||||
edges_to_display.append((src, dst))
|
||||
print(f"Edge {src} to {dst}: {attrs}")
|
||||
|
||||
# displaying graphs
|
||||
plt.figure(figsize=(18, 16)) # Set the figure size
|
||||
pos = nx.spring_layout(graph, k=1.0)
|
||||
if display_signal_only:
|
||||
subgraph = graph.subgraph(nodes_to_display)
|
||||
# subgraph.add_edges_from(edges_to_display)
|
||||
else:
|
||||
subgraph = graph
|
||||
|
||||
nx.draw_networkx(subgraph, pos, with_labels=True) # Draw the graph without labels
|
||||
|
||||
# Add node labels
|
||||
# node_labels = nx.get_node_attributes(graph, 'label')
|
||||
# nx.draw_networkx_labels(graph, pos, labels=node_labels)
|
||||
|
||||
# edge labels
|
||||
edge_labels = nx.get_edge_attributes(subgraph, 'lines')
|
||||
nx.draw_networkx_edge_labels(
|
||||
subgraph, pos,
|
||||
edge_labels=edge_labels,
|
||||
font_color='blue'
|
||||
)
|
||||
# plt.axis('off')
|
||||
plt.show()
|
||||
return graph
|
||||
|
||||
def get_ast_structure_str(ast):
|
||||
normal_stdout = sys.stdout
|
||||
# put the string output to a string buffer
|
||||
result = StringIO()
|
||||
sys.stdout = result
|
||||
|
||||
# traverse the ast
|
||||
ast.show(buf=sys.stdout)
|
||||
|
||||
# Redirect std output to the normal mode
|
||||
sys.stdout = normal_stdout
|
||||
|
||||
# Get the result out
|
||||
ast_str = result.getvalue()
|
||||
# print('ast str = ', ast_str, '\n ast end')
|
||||
return ast_str
|
||||
|
||||
def generate_top_logic_graph(filelist: list[str]):
|
||||
for f in filelist:
|
||||
if not os.path.exists(f):
|
||||
raise IOError("file not found: " + f)
|
||||
|
||||
ast, directives = parse(filelist,
|
||||
preprocess_include=[],
|
||||
preprocess_define=[])
|
||||
|
||||
# ast_str = get_ast_structure_str(ast)
|
||||
return create_graph_from_ast(ast, display=False, display_signal_only=False)
|
||||
|
||||
def main():
|
||||
INFO = "Verilog code parser"
|
||||
VERSION = pyverilog.__version__
|
||||
USAGE = "Usage: python example_parser.py file ..."
|
||||
|
||||
def showVersion():
|
||||
print(INFO)
|
||||
print(VERSION)
|
||||
print(USAGE)
|
||||
sys.exit()
|
||||
|
||||
optparser = OptionParser()
|
||||
optparser.add_option("-v", "--version", action="store_true", dest="showversion",
|
||||
default=False, help="Show the version")
|
||||
optparser.add_option("-I", "--include", dest="include", action="append",
|
||||
default=[], help="Include path")
|
||||
optparser.add_option("-D", dest="define", action="append",
|
||||
default=[], help="Macro Definition")
|
||||
(options, args) = optparser.parse_args()
|
||||
|
||||
filelist = args
|
||||
# print(filelist)
|
||||
if options.showversion:
|
||||
showVersion()
|
||||
|
||||
for f in filelist:
|
||||
if not os.path.exists(f):
|
||||
raise IOError("file not found: " + f)
|
||||
|
||||
if len(filelist) == 0:
|
||||
showVersion()
|
||||
|
||||
ast, directives = parse(filelist,
|
||||
preprocess_include=options.include,
|
||||
preprocess_define=options.define)
|
||||
|
||||
# ast_str = get_ast_structure_str(ast)
|
||||
create_graph_from_ast(ast, display_signal_only=True, display=True)
|
||||
ast.show(attrnames=True)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
39241
tools/waveform_trace/src/parser.out
Normal file
39241
tools/waveform_trace/src/parser.out
Normal file
File diff suppressed because it is too large
Load Diff
443
tools/waveform_trace/src/parsetab.py
Normal file
443
tools/waveform_trace/src/parsetab.py
Normal file
File diff suppressed because one or more lines are too long
8
tools/waveform_trace/src/pyverilog/Makefile
Normal file
8
tools/waveform_trace/src/pyverilog/Makefile
Normal file
@ -0,0 +1,8 @@
|
||||
.PHONY: clean
|
||||
clean:
|
||||
make clean -C ./utils
|
||||
make clean -C ./vparser
|
||||
make clean -C ./dataflow
|
||||
make clean -C ./controlflow
|
||||
make clean -C ./ast_code_generator
|
||||
rm -rf *.pyc __pycache__ *.out parsetab.py *.html
|
||||
1
tools/waveform_trace/src/pyverilog/VERSION
Normal file
1
tools/waveform_trace/src/pyverilog/VERSION
Normal file
@ -0,0 +1 @@
|
||||
1.3.0
|
||||
7
tools/waveform_trace/src/pyverilog/__init__.py
Normal file
7
tools/waveform_trace/src/pyverilog/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "VERSION")) as f:
|
||||
__version__ = f.read().splitlines()[0]
|
||||
@ -0,0 +1,3 @@
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf *.pyc __pycache__ parsetab.py *.out
|
||||
1030
tools/waveform_trace/src/pyverilog/ast_code_generator/codegen.py
Normal file
1030
tools/waveform_trace/src/pyverilog/ast_code_generator/codegen.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,104 @@
|
||||
Source
|
||||
Description
|
||||
ModuleDef
|
||||
Paramlist
|
||||
Portlist
|
||||
Port
|
||||
Width
|
||||
Length
|
||||
Dimensions
|
||||
Identifier
|
||||
Value
|
||||
Constant
|
||||
IntConst
|
||||
FloatConst
|
||||
StringConst
|
||||
Variable
|
||||
Input
|
||||
Output
|
||||
Inout
|
||||
Tri
|
||||
Wire
|
||||
Reg
|
||||
Integer
|
||||
Real
|
||||
Genvar
|
||||
Ioport
|
||||
Parameter
|
||||
Localparam
|
||||
Decl
|
||||
Concat
|
||||
LConcat
|
||||
Repeat
|
||||
Partselect
|
||||
Pointer
|
||||
Lvalue
|
||||
Rvalue
|
||||
Operator
|
||||
UnaryOperator
|
||||
Uminus
|
||||
Ulnot
|
||||
Unot
|
||||
Uand
|
||||
Unand
|
||||
Uor
|
||||
Unor
|
||||
Uxor
|
||||
Uxnor
|
||||
Power
|
||||
Times
|
||||
Divide
|
||||
Mod
|
||||
Plus
|
||||
Minus
|
||||
Sll
|
||||
Srl
|
||||
Sra
|
||||
LessThan
|
||||
GreaterThan
|
||||
LessEq
|
||||
GreaterEq
|
||||
Eq
|
||||
NotEq
|
||||
Eql
|
||||
NotEql
|
||||
And
|
||||
Xor
|
||||
Xnor
|
||||
Or
|
||||
Land
|
||||
Lor
|
||||
Cond
|
||||
Assign
|
||||
Always
|
||||
SensList
|
||||
Sens
|
||||
Substitution
|
||||
BlockingSubstitution
|
||||
NonblockingSubstitution
|
||||
IfStatement
|
||||
ForStatement
|
||||
WhileStatement
|
||||
CaseStatement
|
||||
Case
|
||||
Block
|
||||
Initial
|
||||
WaitStatement
|
||||
ForeverStatement
|
||||
DelayStatement
|
||||
InstanceList
|
||||
Instance
|
||||
ParamArg
|
||||
PortArg
|
||||
Function
|
||||
FunctionCall
|
||||
Task
|
||||
GenerateStatement
|
||||
SystemCall
|
||||
IdentifierScopeLabel
|
||||
IdentifierScope
|
||||
Pragma
|
||||
PragmaEntry
|
||||
Disable
|
||||
ParallelBlock
|
||||
SingleStatement
|
||||
@ -0,0 +1,3 @@
|
||||
|
||||
always @({{ sens_list }}) {{ statement }}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
({{ left }} {{ op }} {{ right }})
|
||||
@ -0,0 +1 @@
|
||||
assign {{ left }} = {{ right }};
|
||||
@ -0,0 +1,5 @@
|
||||
begin{% if scope != '' %} : {{ scope }}{% endif %}
|
||||
{%- for statement in statements %}
|
||||
{{ statement }}
|
||||
{%- endfor %}
|
||||
end
|
||||
@ -0,0 +1 @@
|
||||
{% if ldelay != '' %}{{ ldelay }} {% endif %}{{ left }} = {% if rdelay != '' %}{{ rdelay }} {% endif %}{{ right }};
|
||||
@ -0,0 +1 @@
|
||||
{{ cond }}: {{ statement }}
|
||||
@ -0,0 +1,5 @@
|
||||
case({{ comp }})
|
||||
{%- for case in caselist %}
|
||||
{{ case }}
|
||||
{%- endfor %}
|
||||
endcase
|
||||
@ -0,0 +1,5 @@
|
||||
casex({{ comp }})
|
||||
{%- for case in caselist %}
|
||||
{{ case }}
|
||||
{%- endfor %}
|
||||
endcase
|
||||
@ -0,0 +1 @@
|
||||
{ {% for item in items %}{{ item }}{% if loop.index < len_items %}, {% endif %}{% endfor %} }
|
||||
@ -0,0 +1 @@
|
||||
(({{ cond }})? {{ true_value }} : {{ false_value }})
|
||||
@ -0,0 +1 @@
|
||||
{{ value }}
|
||||
@ -0,0 +1,2 @@
|
||||
{%- for item in items %}{{ item }}
|
||||
{%- endfor %}
|
||||
@ -0,0 +1 @@
|
||||
#{{ delay }}
|
||||
@ -0,0 +1,3 @@
|
||||
{% for definition in definitions %}
|
||||
{{ definition }}
|
||||
{% endfor %}
|
||||
@ -0,0 +1 @@
|
||||
diable {{ name }}
|
||||
@ -0,0 +1 @@
|
||||
({{ left }} {{ op }} {{ right }})
|
||||
@ -0,0 +1 @@
|
||||
({{ left }} {{ op }} {{ right }})
|
||||
@ -0,0 +1 @@
|
||||
({{ left }} {{ op }} {{ right }})
|
||||
@ -0,0 +1 @@
|
||||
@({{ senslist }});
|
||||
@ -0,0 +1 @@
|
||||
{{ value }}
|
||||
@ -0,0 +1 @@
|
||||
forever {{ statement }}
|
||||
@ -0,0 +1 @@
|
||||
for({{ pre }} {{ cond }}; {{ post }}) {{ statement }}
|
||||
@ -0,0 +1,7 @@
|
||||
|
||||
function {{ retwidth }} {{ name }};
|
||||
{%- for s in statement %}
|
||||
{{ s }}
|
||||
{%- endfor %}
|
||||
endfunction
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{{ name }}({% for arg in args %}{{ arg }}{% if loop.index < len_args %}, {% endif %}{% endfor %})
|
||||
@ -0,0 +1,4 @@
|
||||
|
||||
generate {% for item in items %}{{ item }}{% endfor %}
|
||||
endgenerate
|
||||
|
||||
@ -0,0 +1 @@
|
||||
genvar {{ name }};
|
||||
@ -0,0 +1 @@
|
||||
({{ left }} {{ op }} {{ right }})
|
||||
@ -0,0 +1 @@
|
||||
({{ left }} {{ op }} {{ right }})
|
||||
@ -0,0 +1 @@
|
||||
{{ scope }}{{ name }}
|
||||
@ -0,0 +1 @@
|
||||
{% for scope in scopes %}{{ scope }}{% endfor %}
|
||||
@ -0,0 +1 @@
|
||||
{{ name }}{%- if loop != '' %}[{{ loop }}]{%- endif %}.
|
||||
@ -0,0 +1,5 @@
|
||||
if({{ cond }}) {{ true_statement }}
|
||||
{%- if true_statement[-1] != ' ' and true_statement[-1] != '\n' %} {% endif -%}
|
||||
{%- if true_statement.count('\n') == 0 and false_statement != '' %}
|
||||
{% endif -%}
|
||||
{%- if false_statement != '' %}else {{ false_statement }}{% endif -%}
|
||||
@ -0,0 +1,3 @@
|
||||
|
||||
initial {{ statement }}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
inout {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};
|
||||
@ -0,0 +1 @@
|
||||
input {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};
|
||||
@ -0,0 +1,5 @@
|
||||
{{ name }}{{ array }}
|
||||
({% for port in portlist %}
|
||||
{{ port }}{%- if loop.index < len_portlist -%}, {%- endif -%}
|
||||
{% endfor %}
|
||||
)
|
||||
@ -0,0 +1,12 @@
|
||||
|
||||
{{ module }}
|
||||
{%- if len_parameterlist > 0 %}
|
||||
#({% for param in parameterlist %}
|
||||
{{ param }}{%- if loop.index < len_parameterlist -%},
|
||||
{%- endif -%}{% endfor %}
|
||||
)
|
||||
{%- endif %}
|
||||
{%- for instance in instances %}
|
||||
{{ instance }}{%- if loop.index < len_instances -%},
|
||||
{%- endif -%}{%- endfor -%};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{{ value }}
|
||||
@ -0,0 +1 @@
|
||||
integer {{ name }};
|
||||
@ -0,0 +1 @@
|
||||
{{ first }} {% if second != '' %}{{ second }} {% endif %}{% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user