- 新增完整的数据流程文档,详细说明从用户输入到响应显示的全流程
- 修复 messageArea.ts 中的消息渲染逻辑:
- 移除用户消息时重置分段容器的逻辑
- 移除对话完成时跳过 segments 处理的逻辑
- 确保对话完成时正确渲染最终的 segments
1028 lines
28 KiB
Markdown
1028 lines
28 KiB
Markdown
# IC Coder Plugin 数据流程详解
|
||
|
||
## 概述
|
||
|
||
本文档详细说明从用户输入文本到最终显示响应的完整数据流程。
|
||
|
||
---
|
||
|
||
## 📊 完整数据流程图
|
||
|
||
```
|
||
用户输入文本
|
||
↓
|
||
前端 WebView (inputArea.ts)
|
||
↓ vscode.postMessage
|
||
VS Code Extension (ICHelperPanel.ts)
|
||
↓ onDidReceiveMessage
|
||
消息处理器 (messageHandler.ts)
|
||
↓ handleUserMessage
|
||
后端服务 (dialogService.ts)
|
||
↓ 回调函数
|
||
前端 WebView (webviewContent.ts)
|
||
↓ window.addEventListener
|
||
消息区域渲染 (messageArea.ts)
|
||
↓
|
||
显示给用户
|
||
```
|
||
|
||
---
|
||
|
||
## 第一阶段:用户输入 → 前端处理
|
||
|
||
### 1. 用户在输入框输入文本并点击发送
|
||
|
||
**文件位置**: `src/views/inputArea.ts:415-422`
|
||
|
||
```javascript
|
||
// 用户点击发送按钮或按下 Enter 键
|
||
function sendMessage() {
|
||
const text = messageInput.value.trim();
|
||
if (!text) return;
|
||
|
||
// 获取当前配置
|
||
const mode = getCurrentMode(); // 运行模式 (agent/plan等)
|
||
const model = getCurrentModel(); // 选择的模型
|
||
const planMode = document.getElementById('planToggle')?.checked || false;
|
||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||
|
||
// 1. 立即显示用户消息
|
||
addMessage(text, 'user');
|
||
|
||
// 2. 更新 UI 状态
|
||
hasMessages = true;
|
||
updateInputAreaLayout();
|
||
setSendButtonState(true); // 切换为暂停按钮
|
||
|
||
// 3. 发送消息到 VS Code 扩展
|
||
vscode.postMessage({
|
||
command: 'sendMessage',
|
||
text: text,
|
||
mode: mode,
|
||
model: model,
|
||
planMode: planMode,
|
||
contextItems: contextItems
|
||
});
|
||
|
||
// 4. 清空输入框
|
||
messageInput.value = '';
|
||
autoResizeTextarea();
|
||
messageInput.focus();
|
||
|
||
// 5. 重置优化状态
|
||
resetOptimizeButton();
|
||
}
|
||
```
|
||
|
||
**前端同时做的事情:**
|
||
- ✅ 显示用户消息到聊天区域
|
||
- ✅ 切换发送按钮为暂停状态
|
||
- ✅ 清空输入框并重置高度
|
||
- ✅ 更新布局(如果是第一条消息)
|
||
|
||
---
|
||
|
||
## 第二阶段:VS Code 扩展接收消息
|
||
|
||
### 2. ICHelperPanel 接收并处理消息
|
||
|
||
**文件位置**: `src/panels/ICHelperPanel.ts:112-152`
|
||
|
||
```javascript
|
||
// 监听来自 WebView 的消息
|
||
panel.webview.onDidReceiveMessage(async (message) => {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
const panelId = (panel as any).__uniqueId;
|
||
|
||
switch (message.command) {
|
||
case "sendMessage":
|
||
// 步骤 1: 确保面板有任务上下文 (taskId)
|
||
if (!historyManager.getPanelTask(panelId)) {
|
||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||
if (workspacePath) {
|
||
try {
|
||
// 创建新任务
|
||
const taskMeta = await historyManager.createTask(
|
||
workspacePath,
|
||
"新对话"
|
||
);
|
||
// 关联面板和任务
|
||
historyManager.setPanelTask(
|
||
panelId,
|
||
taskMeta.taskId,
|
||
workspacePath
|
||
);
|
||
} catch (error) {
|
||
console.error("创建任务失败:", error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 步骤 2: 切换到当前面板的任务上下文
|
||
historyManager.switchToPanelTask(panelId);
|
||
|
||
// 步骤 3: 显示进度条
|
||
panel.webview.postMessage({ type: 'showProgress' });
|
||
|
||
// 步骤 4: 调用消息处理器
|
||
handleUserMessage(
|
||
panel,
|
||
message.text,
|
||
context.extensionPath,
|
||
message.mode
|
||
);
|
||
break;
|
||
}
|
||
});
|
||
```
|
||
|
||
**关键概念:**
|
||
- **panelId**: 每个 WebView 面板的唯一标识
|
||
- **taskId**: 每个对话任务的唯一标识,用于历史记录管理
|
||
- **workspacePath**: 当前工作区路径
|
||
|
||
---
|
||
|
||
## 第三阶段:消息处理器处理
|
||
|
||
### 3. handleUserMessage 处理用户消息
|
||
|
||
**文件位置**: `src/utils/messageHandler.ts:56-117`
|
||
|
||
```javascript
|
||
export async function handleUserMessage(
|
||
panel: vscode.WebviewPanel,
|
||
text: string,
|
||
extensionPath?: string,
|
||
mode?: RunMode
|
||
) {
|
||
console.log("收到用户消息:", text);
|
||
|
||
// 步骤 1: 记录用户消息到历史(允许失败,不阻塞主流程)
|
||
try {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
await historyManager.addUserMessage(text);
|
||
} catch (error) {
|
||
console.warn("记录消息历史失败(可能没有打开工作区):", error);
|
||
}
|
||
|
||
// 步骤 2: 设置 WebView 面板用于用户交互
|
||
userInteractionManager.setWebviewPanel(panel);
|
||
|
||
// 步骤 3: 检查是否是 VCD 生成命令(本地处理)
|
||
if (isVCDGenerationCommand(text)) {
|
||
await handleVCDGeneration(panel, extensionPath || "");
|
||
return;
|
||
}
|
||
|
||
// 步骤 4: 检查是否是文件操作命令(本地处理)
|
||
const fileOperation = parseFileOperation(text);
|
||
if (fileOperation) {
|
||
console.log("执行文件操作:", fileOperation.type, fileOperation.filePath);
|
||
await handleFileOperation(panel, fileOperation);
|
||
return;
|
||
}
|
||
|
||
// 步骤 5: 使用后端服务处理
|
||
if (useBackendService && extensionPath) {
|
||
try {
|
||
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
|
||
return;
|
||
} catch (error) {
|
||
console.error("后端服务不可用:", error);
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: "后端服务不可用",
|
||
type: "error",
|
||
});
|
||
// 恢复输入状态
|
||
panel.webview.postMessage({
|
||
command: "updateSegments",
|
||
segments: [],
|
||
isComplete: true,
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**处理流程:**
|
||
1. 记录用户消息到历史数据库
|
||
2. 设置用户交互管理器
|
||
3. 检查特殊命令(VCD生成、文件操作)
|
||
4. 调用后端服务处理
|
||
|
||
---
|
||
|
||
## 第四阶段:后端服务处理
|
||
|
||
### 4. handleUserMessageWithBackend 调用后端
|
||
|
||
**文件位置**: `src/utils/messageHandler.ts:122-149`
|
||
|
||
```javascript
|
||
async function handleUserMessageWithBackend(
|
||
panel: vscode.WebviewPanel,
|
||
text: string,
|
||
extensionPath: string,
|
||
mode?: RunMode,
|
||
reuseTaskId?: string // 可选,复用现有 taskId
|
||
): Promise<void> {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
|
||
// 步骤 1: 获取或创建会话
|
||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||
|
||
if (!currentSession || !currentSession.active) {
|
||
currentSession = dialogManager.createSession(
|
||
extensionPath,
|
||
taskIdToUse || undefined
|
||
);
|
||
// 保存 taskId 用于后续操作(如压缩)
|
||
lastTaskId = currentSession.getTaskId();
|
||
console.log(
|
||
"[MessageHandler] 创建会话: taskId=",
|
||
lastTaskId,
|
||
"来源=",
|
||
taskIdToUse ? "historyManager" : "新生成"
|
||
);
|
||
}
|
||
|
||
// 步骤 2: 显示状态栏
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: "思考中...",
|
||
type: "thinking",
|
||
});
|
||
|
||
// 步骤 3: 发送消息到后端,并注册回调函数
|
||
return new Promise((resolve, reject) => {
|
||
currentSession!.sendMessage(
|
||
text,
|
||
{
|
||
// 回调 1: 文本更新(已废弃,统一通过 onSegmentUpdate 处理)
|
||
onText: (fullText, isStreaming) => {
|
||
// 不再单独处理文本
|
||
},
|
||
|
||
// 回调 2: 段落更新(核心回调)
|
||
onSegmentUpdate: (segments) => {
|
||
// 实时发送段落更新到前端
|
||
panel.webview.postMessage({
|
||
command: "updateSegments",
|
||
segments: segments,
|
||
});
|
||
},
|
||
|
||
// 回调 3: 工具开始执行
|
||
onToolStart: (toolName) => {
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: `正在执行 ${toolName}...`,
|
||
type: "working",
|
||
});
|
||
},
|
||
|
||
// 回调 4: 工具执行完成
|
||
onToolComplete: (toolName, result) => {
|
||
// 通过 onSegmentUpdate 统一更新
|
||
},
|
||
|
||
// 回调 5: 工具执行错误
|
||
onToolError: (toolName, error) => {
|
||
// 通过 onSegmentUpdate 统一更新
|
||
},
|
||
|
||
// 回调 6: 用户问题
|
||
onQuestion: (askId, question, options) => {
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: "等待用户回答...",
|
||
type: "working",
|
||
});
|
||
},
|
||
|
||
// 回调 7: 对话完成
|
||
onComplete: async (segments) => {
|
||
// 隐藏状态栏
|
||
panel.webview.postMessage({
|
||
command: "hideStatus",
|
||
});
|
||
|
||
// 最后一次发送完整的段落
|
||
panel.webview.postMessage({
|
||
command: "updateSegments",
|
||
segments: segments,
|
||
isComplete: true, // 标记对话完成
|
||
});
|
||
|
||
// 保存完整的 segments 到历史记录
|
||
const textContent = segments
|
||
.filter((s) => s.type === "text" && s.content)
|
||
.map((s) => s.content)
|
||
.join("\n");
|
||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||
|
||
resolve();
|
||
},
|
||
|
||
// 回调 8: 错误处理
|
||
onError: (error) => {
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: `错误: ${error.message}`,
|
||
type: "error",
|
||
});
|
||
reject(error);
|
||
},
|
||
},
|
||
mode
|
||
);
|
||
});
|
||
}
|
||
```
|
||
|
||
**关键回调函数:**
|
||
- `onSegmentUpdate`: 实时更新段落(核心)
|
||
- `onToolStart/Complete/Error`: 工具执行状态
|
||
- `onQuestion`: 用户问题
|
||
- `onComplete`: 对话完成
|
||
- `onError`: 错误处理
|
||
|
||
---
|
||
|
||
## 第五阶段:前端接收并处理响应
|
||
|
||
### 5. WebView 监听消息
|
||
|
||
**文件位置**: `src/views/webviewContent.ts:523-546`
|
||
|
||
```javascript
|
||
// 监听来自插件的消息
|
||
window.addEventListener('message', event => {
|
||
const message = event.data;
|
||
console.log('[WebView] 收到消息:', message.command, message);
|
||
|
||
switch (message.command) {
|
||
case 'updateSegments':
|
||
// 实时更新分段消息(核心处理)
|
||
console.log('[WebView] 实时更新段落, segments:', message.segments);
|
||
updateSegmentsRealtime(message.segments, message.isComplete);
|
||
|
||
// 如果对话完成,恢复发送按钮状态
|
||
if (message.isComplete && typeof setSendButtonState === 'function') {
|
||
setSendButtonState(false);
|
||
}
|
||
break;
|
||
|
||
case 'updateStatus':
|
||
// 更新状态栏
|
||
const statusBar = document.getElementById('statusBar');
|
||
const statusText = document.getElementById('statusText');
|
||
if (statusBar && statusText) {
|
||
statusBar.style.display = 'flex';
|
||
statusText.textContent = message.text;
|
||
statusBar.className = 'status-bar ' + (message.type || '');
|
||
}
|
||
break;
|
||
|
||
case 'hideStatus':
|
||
// 隐藏状态栏
|
||
const statusBarHide = document.getElementById('statusBar');
|
||
if (statusBarHide) {
|
||
statusBarHide.style.display = 'none';
|
||
}
|
||
break;
|
||
}
|
||
});
|
||
```
|
||
|
||
**消息类型:**
|
||
- `updateSegments`: 更新段落(核心)
|
||
- `updateStatus`: 更新状态栏
|
||
- `hideStatus`: 隐藏状态栏
|
||
|
||
---
|
||
|
||
## 第六阶段:消息区域渲染
|
||
|
||
### 6. updateSegmentsRealtime 实时渲染段落
|
||
|
||
**文件位置**: `src/views/messageArea.ts:903-1171`
|
||
|
||
这是整个数据流程中最复杂的部分,负责将后端返回的 segments 数据渲染成用户可见的 UI。
|
||
|
||
```javascript
|
||
function updateSegmentsRealtime(segments, isComplete) {
|
||
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
||
|
||
if (!segments || segments.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 步骤 1: 创建或复用分段消息容器
|
||
if (!currentSegmentedMessage) {
|
||
// 移除流式消息(如果有)
|
||
if (currentStreamingMessage) {
|
||
currentStreamingMessage.remove();
|
||
currentStreamingMessage = null;
|
||
}
|
||
|
||
// 移除所有工具状态消息
|
||
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||
toolStatuses.forEach(el => el.remove());
|
||
|
||
// 创建新容器
|
||
currentSegmentedMessage = document.createElement('div');
|
||
currentSegmentedMessage.className = 'message bot-message segmented-message';
|
||
messagesEl.appendChild(currentSegmentedMessage);
|
||
}
|
||
|
||
// 步骤 2: 保存工具的展开/折叠状态
|
||
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
|
||
toolHeaders.forEach((header, idx) => {
|
||
const isCollapsed = header.classList.contains('collapsed');
|
||
toolCollapseStates.set(idx, isCollapsed);
|
||
});
|
||
|
||
// 步骤 3: 清空容器并重新渲染所有段落
|
||
currentSegmentedMessage.innerHTML = '';
|
||
|
||
// 步骤 4: 合并连续相同的工具调用
|
||
const mergedSegments = mergeConsecutiveTools(segments);
|
||
|
||
// 步骤 5: 遍历每个 segment 并渲染
|
||
let toolIndex = 0;
|
||
mergedSegments.forEach((segment, index) => {
|
||
const segmentDiv = document.createElement('div');
|
||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||
|
||
// 根据 segment 类型渲染
|
||
if (segment.type === 'text') {
|
||
renderTextSegment(segmentDiv, segment);
|
||
} else if (segment.type === 'tool') {
|
||
renderToolSegment(segmentDiv, segment, toolIndex);
|
||
toolIndex++;
|
||
} else if (segment.type === 'question') {
|
||
renderQuestionSegment(segmentDiv, segment);
|
||
} else if (segment.type === 'agent') {
|
||
renderAgentCard(segment, segmentDiv);
|
||
} else if (segment.type === 'plan') {
|
||
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||
}
|
||
|
||
currentSegmentedMessage.appendChild(segmentDiv);
|
||
});
|
||
|
||
// 步骤 6: 如果对话完成,添加操作按钮
|
||
if (isComplete) {
|
||
addMessageActions(currentSegmentedMessage, segments);
|
||
currentSegmentedMessage = null;
|
||
}
|
||
|
||
// 步骤 7: 智能滚动到底部
|
||
smartScrollToBottom();
|
||
}
|
||
```
|
||
|
||
**渲染流程说明:**
|
||
|
||
1. **容器管理**: 创建或复用 `currentSegmentedMessage` 容器
|
||
2. **状态保持**: 保存工具的展开/折叠状态,避免重新渲染时丢失
|
||
3. **清空重绘**: 每次更新都清空容器并重新渲染所有段落
|
||
4. **工具合并**: 连续相同的工具调用会合并显示(如 "已完成文件读取 x3")
|
||
5. **类型渲染**: 根据 segment.type 调用不同的渲染函数
|
||
6. **完成处理**: 对话完成时添加操作按钮(复制、点赞、点踩)
|
||
7. **智能滚动**: 只在用户位于底部时自动滚动
|
||
|
||
---
|
||
|
||
### 7. Segment 类型详解
|
||
|
||
#### 7.1 Text Segment (文本段落)
|
||
|
||
**数据结构:**
|
||
```javascript
|
||
{
|
||
type: 'text',
|
||
content: '这是 AI 的回复内容...'
|
||
}
|
||
```
|
||
|
||
**渲染函数**: `formatText()` (第 1359-1432 行)
|
||
|
||
**处理流程:**
|
||
1. 提取代码块 (\`\`\`language\ncode\`\`\`)
|
||
2. 提取行内代码 (\`code\`)
|
||
3. 转义 HTML 特殊字符
|
||
4. 处理 Markdown 语法:
|
||
- 标题: `#`, `##`, `###`
|
||
- 粗体: `**text**`
|
||
- 斜体: `*text*`
|
||
- 列表: `-`, `*`, `1.`
|
||
- 链接: `[text](url)`
|
||
5. 处理换行: `\n` → `<br>`
|
||
6. 恢复代码块和行内代码
|
||
|
||
**示例:**
|
||
```javascript
|
||
// 输入
|
||
"## 标题\n这是**粗体**和*斜体*\n```js\nconst a = 1;\n```"
|
||
|
||
// 输出
|
||
"<h2>标题</h2><br>这是<strong>粗体</strong>和<em>斜体</em><br><pre><code class='language-js'>const a = 1;</code></pre>"
|
||
```
|
||
|
||
---
|
||
|
||
#### 7.2 Tool Segment (工具调用段落)
|
||
|
||
**数据结构:**
|
||
```javascript
|
||
{
|
||
type: 'tool',
|
||
toolName: 'file_read', // 工具名称
|
||
toolStatus: 'success', // 状态: success/error
|
||
toolResult: '文件内容...', // 工具执行结果
|
||
toolCount: 3, // 合并后的计数(可选)
|
||
vcdFilePath: '/path/to/file.vcd' // 特殊字段(仿真工具)
|
||
}
|
||
```
|
||
|
||
**渲染逻辑** (第 976-1053 行):
|
||
|
||
1. **过滤工具**: 跳过不需要显示的工具(如 `spawnExplorer`)
|
||
2. **低调样式**: 所有工具调用使用低调样式(小字体、低透明度)
|
||
3. **工具图标**: 根据 `toolName` 显示对应图标
|
||
4. **工具名称**: 映射为中文显示名称(如 `file_read` → "已完成文件读取")
|
||
5. **结果显示**:
|
||
- 短结果(≤60字符): 直接显示在同一行
|
||
- 长结果(>60字符): 默认折叠,点击展开
|
||
6. **特殊处理**: 仿真工具成功后添加波形预览组件
|
||
|
||
**工具名称映射:**
|
||
```javascript
|
||
const toolNameMap = {
|
||
'file_read': '已完成文件读取',
|
||
'file_write': '已完成文件写入',
|
||
'file_delete': '已完成文件删除',
|
||
'file_list': '已检索代码文件',
|
||
'syntax_check': '已完成语法检查',
|
||
'simulation': '已完成仿真',
|
||
'waveform_summary': '已完成波形分析',
|
||
'knowledge_save': '已保存知识库',
|
||
'addSignal': '信号分析完成',
|
||
// ... 更多映射
|
||
};
|
||
```
|
||
|
||
**HTML 结构:**
|
||
```html
|
||
<div class="message-segment segment-tool low-profile">
|
||
<div class="tool-segment-header" data-collapsible="true">
|
||
<span class="tool-collapse-icon">▼</span>
|
||
<span class="tool-segment-name">已完成文件读取 x3</span>
|
||
</div>
|
||
<div class="tool-segment-content collapsed">
|
||
<span class="tool-segment-result">文件内容...</span>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
#### 7.3 Question Segment (用户问题段落)
|
||
|
||
**数据结构:**
|
||
```javascript
|
||
{
|
||
type: 'question',
|
||
askId: 'ask_123456', // 问题唯一标识
|
||
question: '请选择一个选项:', // 问题文本
|
||
options: ['选项1', '选项2', '选项3'] // 选项列表
|
||
}
|
||
```
|
||
|
||
**渲染逻辑** (第 1054-1118 行):
|
||
|
||
1. **检查状态**: 从 `answeredQuestions` Map 中检查是否已回答
|
||
2. **显示问题**: 显示问题文本
|
||
3. **显示选项**: 渲染选项按钮
|
||
4. **自定义输入**: 提供自定义输入框("其他"选项)
|
||
5. **事件监听**:
|
||
- 点击选项按钮 → 提交答案
|
||
- 输入自定义答案 → 提交答案
|
||
- 回车键 → 提交答案
|
||
6. **状态更新**: 提交后标记为已回答,高亮选中选项
|
||
|
||
**交互流程:**
|
||
```
|
||
用户点击选项/输入自定义答案
|
||
↓
|
||
handleQuestionAnswerInSegment()
|
||
↓
|
||
保存答案到 answeredQuestions Map
|
||
↓
|
||
标记问题为已回答 (添加 .answered 类)
|
||
↓
|
||
高亮选中选项 (添加 .selected 类)
|
||
↓
|
||
发送答案到后端 (vscode.postMessage)
|
||
```
|
||
|
||
**HTML 结构:**
|
||
```html
|
||
<div class="message-segment segment-question">
|
||
<div class="question-text">请选择一个选项:</div>
|
||
<div class="question-options" data-ask-id="ask_123456">
|
||
<button class="question-option" data-option="选项1">选项1</button>
|
||
<button class="question-option selected" data-option="选项2">选项2</button>
|
||
<button class="question-option" data-option="选项3">选项3</button>
|
||
</div>
|
||
<div class="custom-input-container">
|
||
<input type="text" class="custom-input" placeholder="输入其他答案..." />
|
||
<button class="custom-submit">提交</button>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
#### 7.4 Agent Segment (智能体卡片)
|
||
|
||
**数据结构:**
|
||
```javascript
|
||
{
|
||
type: 'agent',
|
||
agentName: 'CodeExplorer',
|
||
agentStatus: 'running', // running/completed/error
|
||
agentMessage: '正在探索代码...'
|
||
}
|
||
```
|
||
|
||
**渲染**: 调用 `renderAgentCard()` 函数(定义在 `agentCard.ts`)
|
||
|
||
---
|
||
|
||
#### 7.5 Plan Segment (计划卡片)
|
||
|
||
**数据结构:**
|
||
```javascript
|
||
{
|
||
type: 'plan',
|
||
planTitle: '实现用户认证功能',
|
||
planSteps: [
|
||
{ step: 1, description: '创建用户模型', status: 'pending' },
|
||
{ step: 2, description: '实现登录接口', status: 'pending' }
|
||
]
|
||
}
|
||
```
|
||
|
||
**渲染**: 调用 `renderPlanCardInSegment()` 函数(定义在 `planCard.ts`)
|
||
|
||
---
|
||
|
||
## 第七阶段:关键特性说明
|
||
|
||
### 8.1 实时更新机制
|
||
|
||
**核心原理**: 每次后端推送新数据时,前端都会**清空容器并重新渲染所有段落**
|
||
|
||
**为什么这样设计?**
|
||
- ✅ 简化状态管理,避免复杂的 diff 算法
|
||
- ✅ 确保显示内容与后端数据完全一致
|
||
- ✅ 支持段落顺序变化和内容更新
|
||
|
||
**性能优化:**
|
||
- 保存工具的展开/折叠状态(`toolCollapseStates` Map)
|
||
- 保存问题的回答状态(`answeredQuestions` Map)
|
||
- 智能滚动(只在用户位于底部时自动滚动)
|
||
|
||
---
|
||
|
||
### 8.2 工具合并机制
|
||
|
||
**目的**: 减少视觉噪音,提升用户体验
|
||
|
||
**实现** (第 946-966 行):
|
||
```javascript
|
||
// 合并连续相同的工具调用
|
||
const mergedSegments = [];
|
||
let i = 0;
|
||
while (i < segments.length) {
|
||
const segment = segments[i];
|
||
if (segment.type === 'tool') {
|
||
// 统计连续相同的工具调用
|
||
let count = 1;
|
||
while (i + count < segments.length &&
|
||
segments[i + count].type === 'tool' &&
|
||
segments[i + count].toolName === segment.toolName) {
|
||
count++;
|
||
}
|
||
// 添加合并后的段落(带计数)
|
||
mergedSegments.push({ ...segment, toolCount: count });
|
||
i += count;
|
||
} else {
|
||
mergedSegments.push(segment);
|
||
i++;
|
||
}
|
||
}
|
||
```
|
||
|
||
**效果:**
|
||
```
|
||
原始: [file_read, file_read, file_read, text, file_write, file_write]
|
||
合并: [file_read x3, text, file_write x2]
|
||
```
|
||
|
||
---
|
||
|
||
### 8.3 智能滚动机制
|
||
|
||
**目的**: 只在用户位于底部时自动滚动,避免打断用户阅读
|
||
|
||
**实现** (第 714-724 行):
|
||
```javascript
|
||
// 检查用户是否在底部附近(允许50px的误差)
|
||
function isUserNearBottom() {
|
||
const threshold = 50;
|
||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||
}
|
||
|
||
// 智能滚动:只有用户在底部附近时才自动滚动
|
||
function smartScrollToBottom() {
|
||
if (isUserNearBottom()) {
|
||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||
}
|
||
}
|
||
```
|
||
|
||
**用户体验:**
|
||
- ✅ 用户在底部 → 自动滚动到最新消息
|
||
- ✅ 用户在阅读历史消息 → 不自动滚动,避免打断
|
||
|
||
---
|
||
|
||
### 8.4 状态保持机制
|
||
|
||
**问题**: 每次重新渲染都会清空容器,如何保持用户的交互状态?
|
||
|
||
**解决方案**: 使用 Map 存储状态
|
||
|
||
**1. 工具折叠状态** (`toolCollapseStates` Map):
|
||
```javascript
|
||
// 保存状态(重新渲染前)
|
||
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
|
||
toolHeaders.forEach((header, idx) => {
|
||
const isCollapsed = header.classList.contains('collapsed');
|
||
toolCollapseStates.set(idx, isCollapsed);
|
||
});
|
||
|
||
// 恢复状态(渲染时)
|
||
const savedState = toolCollapseStates.get(toolIndex);
|
||
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
|
||
```
|
||
|
||
**2. 问题回答状态** (`answeredQuestions` Map):
|
||
```javascript
|
||
// 保存答案
|
||
answeredQuestions.set(askId, answer);
|
||
|
||
// 检查是否已回答
|
||
const isAnswered = answeredQuestions.has(segment.askId);
|
||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||
```
|
||
|
||
---
|
||
|
||
## 完整数据流程总结
|
||
|
||
### 时序图
|
||
|
||
```
|
||
用户输入 "帮我读取 main.v 文件"
|
||
↓ (0ms)
|
||
前端显示用户消息
|
||
↓ (1ms)
|
||
vscode.postMessage({ command: 'sendMessage', text: '...' })
|
||
↓ (2ms)
|
||
ICHelperPanel.onDidReceiveMessage
|
||
↓ (3ms)
|
||
创建/获取 taskId
|
||
↓ (5ms)
|
||
handleUserMessage
|
||
↓ (10ms)
|
||
handleUserMessageWithBackend
|
||
↓ (15ms)
|
||
dialogManager.createSession
|
||
↓ (20ms)
|
||
currentSession.sendMessage
|
||
↓ (100ms - 后端处理)
|
||
onSegmentUpdate 回调 (多次)
|
||
↓ (101ms, 150ms, 200ms...)
|
||
panel.webview.postMessage({ command: 'updateSegments', segments: [...] })
|
||
↓ (102ms, 151ms, 201ms...)
|
||
window.addEventListener('message')
|
||
↓ (103ms, 152ms, 202ms...)
|
||
updateSegmentsRealtime(segments, false)
|
||
↓ (105ms, 155ms, 205ms...)
|
||
渲染 segments 到 DOM
|
||
↓ (3000ms - 对话完成)
|
||
onComplete 回调
|
||
↓ (3001ms)
|
||
panel.webview.postMessage({ command: 'updateSegments', segments: [...], isComplete: true })
|
||
↓ (3002ms)
|
||
updateSegmentsRealtime(segments, true)
|
||
↓ (3005ms)
|
||
添加操作按钮 (复制、点赞、点踩)
|
||
↓ (3010ms)
|
||
恢复发送按钮状态
|
||
```
|
||
|
||
---
|
||
|
||
### 关键文件索引
|
||
|
||
| 文件 | 职责 | 关键函数 |
|
||
|------|------|----------|
|
||
| `inputArea.ts` | 输入框组件 | `sendMessage()` |
|
||
| `ICHelperPanel.ts` | 面板管理 | `onDidReceiveMessage()` |
|
||
| `messageHandler.ts` | 消息处理核心 | `handleUserMessage()`, `handleUserMessageWithBackend()` |
|
||
| `webviewContent.ts` | WebView 主入口 | `window.addEventListener('message')` |
|
||
| `messageArea.ts` | 消息渲染核心 | `updateSegmentsRealtime()`, `formatText()` |
|
||
| `agentCard.ts` | 智能体卡片 | `renderAgentCard()` |
|
||
| `planCard.ts` | 计划卡片 | `renderPlanCardInSegment()` |
|
||
| `waveformPreviewContent.ts` | 波形预览 | `createWaveformPreview()` |
|
||
|
||
---
|
||
|
||
### 数据结构总览
|
||
|
||
**Segment 类型定义:**
|
||
```typescript
|
||
type Segment =
|
||
| TextSegment
|
||
| ToolSegment
|
||
| QuestionSegment
|
||
| AgentSegment
|
||
| PlanSegment;
|
||
|
||
interface TextSegment {
|
||
type: 'text';
|
||
content: string;
|
||
}
|
||
|
||
interface ToolSegment {
|
||
type: 'tool';
|
||
toolName: string;
|
||
toolStatus: 'success' | 'error';
|
||
toolResult?: string;
|
||
toolCount?: number;
|
||
vcdFilePath?: string;
|
||
fileName?: string;
|
||
}
|
||
|
||
interface QuestionSegment {
|
||
type: 'question';
|
||
askId: string;
|
||
question: string;
|
||
options: string[];
|
||
}
|
||
|
||
interface AgentSegment {
|
||
type: 'agent';
|
||
agentName: string;
|
||
agentStatus: 'running' | 'completed' | 'error';
|
||
agentMessage: string;
|
||
}
|
||
|
||
interface PlanSegment {
|
||
type: 'plan';
|
||
planTitle: string;
|
||
planSteps: Array<{
|
||
step: number;
|
||
description: string;
|
||
status: 'pending' | 'running' | 'completed' | 'error';
|
||
}>;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 消息通信协议
|
||
|
||
**前端 → 后端 (vscode.postMessage):**
|
||
|
||
| Command | 参数 | 说明 |
|
||
|---------|------|------|
|
||
| `sendMessage` | `text`, `mode`, `model`, `planMode`, `contextItems` | 发送用户消息 |
|
||
| `submitAnswer` | `askId`, `selected`, `customInput` | 提交问题答案 |
|
||
| `abortDialog` | - | 中止当前对话 |
|
||
| `openWaveformViewer` | `vcdFilePath` | 打开波形查看器 |
|
||
|
||
**后端 → 前端 (panel.webview.postMessage):**
|
||
|
||
| Command | 参数 | 说明 |
|
||
|---------|------|------|
|
||
| `updateSegments` | `segments`, `isComplete` | 更新段落(核心) |
|
||
| `updateStatus` | `text`, `type` | 更新状态栏 |
|
||
| `hideStatus` | - | 隐藏状态栏 |
|
||
| `showProgress` | - | 显示进度条 |
|
||
| `resetSegmentedMessage` | - | 重置分段消息容器 |
|
||
|
||
---
|
||
|
||
## 常见问题 FAQ
|
||
|
||
### Q1: 为什么每次更新都要清空容器重新渲染?
|
||
|
||
**A**: 这是一种简化的状态管理策略:
|
||
- ✅ 避免复杂的 diff 算法
|
||
- ✅ 确保显示内容与后端数据完全一致
|
||
- ✅ 支持段落顺序变化
|
||
- ✅ 代码简单易维护
|
||
|
||
性能影响很小,因为:
|
||
- 渲染速度很快(通常 < 10ms)
|
||
- 用户感知不到闪烁
|
||
- 通过状态保持机制避免丢失用户交互
|
||
|
||
---
|
||
|
||
### Q2: 工具折叠状态如何保持?
|
||
|
||
**A**: 使用 `toolCollapseStates` Map 存储:
|
||
```javascript
|
||
// 重新渲染前保存
|
||
toolCollapseStates.set(toolIndex, isCollapsed);
|
||
|
||
// 渲染时恢复
|
||
const savedState = toolCollapseStates.get(toolIndex);
|
||
```
|
||
|
||
---
|
||
|
||
### Q3: 如何添加新的 Segment 类型?
|
||
|
||
**A**: 按以下步骤操作:
|
||
|
||
1. 在 `updateSegmentsRealtime()` 中添加类型判断
|
||
2. 创建对应的渲染函数
|
||
3. 在 `messageArea.ts` 中添加样式
|
||
4. 更新工具图标映射(如果需要)
|
||
|
||
示例:
|
||
```javascript
|
||
// 1. 添加类型判断
|
||
else if (segment.type === 'myNewType') {
|
||
renderMyNewType(segmentDiv, segment);
|
||
}
|
||
|
||
// 2. 创建渲染函数
|
||
function renderMyNewType(container, segment) {
|
||
container.innerHTML = `<div>${segment.content}</div>`;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Q4: 如何调试数据流程?
|
||
|
||
**A**: 在浏览器开发者工具中查看日志:
|
||
|
||
1. 打开 VS Code 开发者工具: `Help` → `Toggle Developer Tools`
|
||
2. 切换到 `Console` 标签
|
||
3. 查看关键日志:
|
||
- `[WebView] 收到消息:` - 前端接收消息
|
||
- `[WebView] updateSegmentsRealtime 被调用` - 渲染触发
|
||
- `[MessageHandler] 创建会话:` - 后端会话创建
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
本文档详细说明了 IC Coder Plugin 从用户输入到显示响应的完整数据流程:
|
||
|
||
1. **用户输入** → 前端显示并发送消息
|
||
2. **VS Code 扩展** → 接收消息并创建任务上下文
|
||
3. **消息处理器** → 调用后端服务
|
||
4. **后端服务** → 处理消息并通过回调推送数据
|
||
5. **前端接收** → 监听消息并触发渲染
|
||
6. **消息渲染** → 根据 segment 类型渲染 UI
|
||
7. **用户交互** → 保持状态并响应用户操作
|
||
|
||
核心设计理念:
|
||
- ✅ **实时更新**: 支持流式数据推送
|
||
- ✅ **状态保持**: 避免丢失用户交互
|
||
- ✅ **智能滚动**: 不打断用户阅读
|
||
- ✅ **工具合并**: 减少视觉噪音
|
||
- ✅ **简化管理**: 清空重绘而非复杂 diff
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0
|
||
**最后更新**: 2026-01-08
|
||
**作者**: IC Coder Team
|
||
|