Files
IC-Coder-Plugin/docs/会话存储技术文档.md
Roe-xin 9bdaf34471 feat:实现任务历史加载功能 - 完整还原对话样式
主要改进:
1. 实现selectConversation功能,支持点击任务历史列表加载会话
2. 优化会话存储格式,保存完整的segments信息(包括工具调用)
3. 添加旧格式到新格式的自动转换,兼容历史数据
4. 改进错误处理,自动清理无效的空任务目录
5. 优化路径编码逻辑,确保跨平台一致性
6. 前端支持clearChat、addUserMessage、addAiMessage命令

技术细节:
- 扩展AiMessage数据结构,添加segments字段
- 修改messageHandler保存逻辑,将完整segments保存到一条消息
- 实现loadTaskSession方法,加载指定任务的完整会话
- 添加自动清理机制,删除无效的空任务目录
2025-12-28 10:38:54 +08:00

18 KiB
Raw Blame History

IC Coder 会话存储技术文档

1. 概述

IC Coder 的会话存储系统负责持久化保存用户与 AI 的对话历史,支持多项目、多任务的会话管理。系统采用文件系统存储方案,将会话数据按项目和任务组织,便于管理和检索。

1.1 核心特性

  • 多项目支持:不同项目的会话数据独立存储
  • 任务级管理:每个会话作为独立任务进行管理
  • 分页加载:支持历史会话的分页查询,提升性能
  • 实时更新:会话数据实时保存,防止数据丢失
  • 统计信息:记录 Token 使用量、对话轮次等统计数据

1.2 技术栈

  • 存储方式文件系统JSON/JSONL 格式)
  • 存储位置~/.iccoder/projects/{项目路径编码}/{taskId}/
  • 数据格式
    • meta.json:任务元数据
    • conversation.json:完整对话历史
    • conversation_meta.jsonl对话轮次元数据JSONL 格式)

2. 架构设计

2.1 目录结构

~/.iccoder/
└── projects/
    └── {项目路径编码}/
        └── {taskId}/
            ├── meta.json              # 任务元数据
            ├── conversation.json      # 对话历史
            └── conversation_meta.jsonl # 对话元数据

项目路径编码规则

  • 移除冒号 :
  • 将斜杠 / 和反斜杠 \ 替换为 --
  • 示例:C:\Users\admin\Documents\ProjectC--Users--admin--Documents--Project

任务 ID 格式

  • 格式:task_{date}_{sequence}
  • 示例:task_20231226_a3f9k2
  • date8 位日期YYYYMMDD
  • sequence6 位随机字符串

2.2 核心类ChatHistoryManager

ChatHistoryManager 是会话存储的核心管理类,采用单例模式设计。

主要职责

  1. 管理会话存储目录
  2. 创建和切换任务
  3. 保存和加载对话历史
  4. 记录统计信息
  5. 提供会话历史查询接口

关键属性

private static instance: ChatHistoryManager;
private baseDir: string;                    // ~/.iccoder
private currentTaskId: string | null;       // 当前任务 ID
private currentProjectPath: string | null;  // 当前项目路径

3. 数据模型

3.1 TaskMeta任务元数据

存储在 meta.json 文件中,记录任务的基本信息和统计数据。

interface TaskMeta {
  taskId: string;           // 任务 ID
  taskName: string;         // 任务名称
  projectPath: string;      // 项目路径
  createdAt: string;        // 创建时间ISO 8601
  updatedAt: string;        // 更新时间ISO 8601
  stats: {
    credits: number;        // 消耗的积分
    totalTokens: number;    // 总 Token 数
    inputTokens: number;    // 输入 Token 数
    outputTokens: number;   // 输出 Token 数
  };
}

示例

{
  "taskId": "task_20231226_a3f9k2",
  "taskName": "实现计数器功能",
  "projectPath": "C:\\Users\\admin\\Documents\\Project",
  "createdAt": "2023-12-26T10:30:00.000Z",
  "updatedAt": "2023-12-26T11:45:00.000Z",
  "stats": {
    "credits": 0,
    "totalTokens": 15420,
    "inputTokens": 8200,
    "outputTokens": 7220
  }
}

3.2 ChatMessage对话消息

存储在 conversation.json 文件中,记录完整的对话历史。

消息类型枚举

enum MessageType {
  USER = "USER",                           // 用户消息
  AI = "AI",                               // AI 消息
  SYSTEM = "SYSTEM",                       // 系统消息
  TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT"  // 工具执行结果
}

用户消息

interface UserMessage {
  type: MessageType.USER;
  contents: Array<{
    type: "TEXT";
    text: string;
  }>;
}

AI 消息

interface AiMessage {
  type: MessageType.AI;
  text: string;
  toolExecutionRequests?: Array<{
    id: string;
    toolName: string;
    parameters: any;
  }>;
}

系统消息

interface SystemMessage {
  type: MessageType.SYSTEM;
  text: string;
}

工具执行结果消息

interface ToolExecutionResultMessage {
  type: MessageType.TOOL_EXECUTION_RESULT;
  id: string;
  toolName: string;
  text: string;
}

3.3 ConversationMeta对话轮次元数据

存储在 conversation_meta.jsonl 文件中每行一条记录JSONL 格式)。

interface ConversationMeta {
  turnId: number;           // 对话轮次 ID
  timestamp: string;        // 时间戳ISO 8601
  usage?: {
    inputTokens?: number;
    outputTokens?: number;
    totalTokens?: number;
  };
  model?: string;           // 使用的模型
  duration?: number;        // 耗时(毫秒)
}

示例

{"turnId":1,"timestamp":"2023-12-26T10:30:15.000Z","usage":{"inputTokens":120,"outputTokens":350,"totalTokens":470},"model":"gpt-4","duration":2500}
{"turnId":2,"timestamp":"2023-12-26T10:32:30.000Z","usage":{"inputTokens":200,"outputTokens":450,"totalTokens":650},"model":"gpt-4","duration":3200}

4. 核心功能实现

4.1 任务创建

方法createTask(projectPath: string, taskName: string): Promise<TaskMeta>

流程

  1. 生成唯一的任务 ID
  2. 创建任务元数据对象
  3. 创建任务目录
  4. 保存 meta.json
  5. 初始化空的 conversation.json
  6. 设置为当前任务

代码位置chatHistoryManager.ts:114-146

public async createTask(projectPath: string, taskName: string): Promise<TaskMeta> {
  const taskId = this.generateTaskId();
  const now = new Date().toISOString();

  const meta: TaskMeta = {
    taskId,
    taskName,
    projectPath,
    createdAt: now,
    updatedAt: now,
    stats: {
      credits: 0,
      totalTokens: 0,
      inputTokens: 0,
      outputTokens: 0
    }
  };

  this.currentTaskId = taskId;
  this.currentProjectPath = projectPath;

  // 创建任务目录
  const taskDir = this.getTaskDir(projectPath, taskId);
  await this.ensureTaskDir(taskDir);

  // 保存 meta.json
  await this.saveTaskMeta(meta);

  // 初始化空的 conversation.json
  await this.saveConversation([]);

  return meta;
}

4.2 消息保存

系统提供了四种消息保存方法:

4.2.1 添加用户消息

方法addUserMessage(text: string): Promise<void>

代码位置chatHistoryManager.ts:285-299

public async addUserMessage(text: string): Promise<void> {
  await this.ensureCurrentTask();
  const messages = await this.loadConversation();

  const userMessage: UserMessage = {
    type: MessageType.USER,
    contents: [{ type: "TEXT", text }]
  };

  messages.push(userMessage);
  await this.saveConversation(messages);

  // 更新任务元数据
  await this.updateTaskTimestamp();
}

4.2.2 添加 AI 消息

方法addAiMessage(text: string, toolRequests?: any[]): Promise<void>

代码位置chatHistoryManager.ts:304-319

4.2.3 添加系统消息

方法addSystemMessage(text: string): Promise<void>

代码位置chatHistoryManager.ts:324-335

4.2.4 添加工具执行结果

方法addToolExecutionResult(id: string, toolName: string, result: string): Promise<void>

代码位置chatHistoryManager.ts:340-353

4.3 对话元数据记录

方法recordTurnMeta(turnId, usage?, model?, duration?): Promise<void>

功能:记录每轮对话的元数据,包括 Token 使用量、模型信息、耗时等。

代码位置chatHistoryManager.ts:358-378

public async recordTurnMeta(
  turnId: number,
  usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number },
  model?: string,
  duration?: number
): Promise<void> {
  const meta: ConversationMeta = {
    turnId,
    timestamp: new Date().toISOString(),
    usage,
    model,
    duration
  };

  await this.appendConversationMeta(meta);

  // 更新任务统计
  if (usage) {
    await this.updateTaskStats(usage);
  }
}

4.4 会话历史查询

方法getConversationHistoryList(projectPath, offset, limit): Promise<{items, total, hasMore}>

功能:分页查询项目的会话历史列表。

参数

  • projectPath:项目路径
  • offset:偏移量(从第几条开始,默认 0
  • limit:每页数量(默认 10

返回值

{
  items: Array<{
    id: string;        // 任务 ID
    title: string;     // 会话标题(第一句用户消息)
    timestamp: string; // 创建时间
  }>;
  total: number;       // 总数
  hasMore: boolean;    // 是否还有更多
}

代码位置chatHistoryManager.ts:525-590

实现逻辑

  1. 获取项目的所有任务列表(按更新时间倒序)
  2. 根据 offset 和 limit 进行分页
  3. 读取每个任务的 conversation.json
  4. 提取第一条用户消息作为标题(截取前 50 个字符)
  5. 返回分页结果

5. 前端集成

5.1 会话历史栏组件

文件conversationHistoryBar.ts

组件结构

  • 下拉按钮:显示 "Past Conversations"
  • 下拉菜单:显示会话历史列表
  • 新建按钮:创建新会话

关键功能

5.1.1 加载会话历史

function loadMoreHistory() {
  if (isLoadingHistory || (currentOffset > 0 && !hasMoreHistory)) {
    return;
  }

  // 检查是否已达到最大数量100 条)
  if (currentOffset >= MAX_HISTORY_ITEMS) {
    return;
  }

  isLoadingHistory = true;
  vscode.postMessage({
    command: 'loadConversationHistory',
    offset: currentOffset,
    limit: HISTORY_PAGE_SIZE
  });
}

5.1.2 渲染会话列表

function renderConversationHistory(data) {
  isLoadingHistory = false;

  // 追加新数据
  conversationHistory = conversationHistory.concat(data.items);
  totalHistory = data.total;
  hasMoreHistory = data.hasMore;
  currentOffset += data.items.length;

  // 渲染所有历史记录
  historyList.innerHTML = conversationHistory.map(item => `
    <div class="history-item" onclick="selectConversation('${item.id}')">
      <div class="history-item-title">${item.title || '未命名会话'}</div>
      <div class="history-item-time">${formatTime(item.timestamp)}</div>
    </div>
  `).join('');

  // 如果还有更多数据,添加"加载更多"提示
  if (hasMoreHistory && currentOffset < MAX_HISTORY_ITEMS) {
    historyList.innerHTML += `
      <div class="history-load-more">
        <span>滚动加载更多...</span>
      </div>
    `;
  }
}

5.1.3 滚动加载

historyDropdownMenu.addEventListener('scroll', () => {
  const menu = historyDropdownMenu;
  const scrollTop = menu.scrollTop;
  const scrollHeight = menu.scrollHeight;
  const clientHeight = menu.clientHeight;

  // 当滚动到距离底部 50px 时,加载更多
  if (scrollHeight - scrollTop - clientHeight < 50) {
    loadMoreHistory();
  }
});

5.1.4 时间格式化

function formatTime(timestamp) {
  const date = new Date(timestamp);
  const now = new Date();
  const diff = now - date;

  if (diff < 60000) return '刚刚';
  if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
  if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
  if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';

  // 超过7天显示具体日期
  return date.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit'
  });
}

5.2 后端消息处理

文件ICHelperPanel.ts

消息处理流程

case "loadConversationHistory":
  // 加载会话历史(支持分页)
  loadConversationHistory(panel, message.offset || 0, message.limit || 10);
  break;

case "selectConversation":
  // 选择会话(暂未实现)
  break;

case "createNewConversation":
  // 创建新会话 - 在当前编辑器组中打开新标签页
  showICHelperPanel(context, panel.viewColumn);
  break;

加载会话历史实现

async function loadConversationHistory(
  panel: vscode.WebviewPanel,
  offset: number = 0,
  limit: number = 10
) {
  try {
    const historyManager = ChatHistoryManager.getInstance();
    const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;

    if (!workspacePath) {
      // 没有打开的工作区,返回空历史
      panel.webview.postMessage({
        command: "conversationHistory",
        items: [],
        total: 0,
        hasMore: false,
      });
      return;
    }

    // 获取会话历史列表(支持分页)
    const result = await historyManager.getConversationHistoryList(
      workspacePath,
      offset,
      limit
    );

    // 发送会话历史到前端
    panel.webview.postMessage({
      command: "conversationHistory",
      items: result.items,
      total: result.total,
      hasMore: result.hasMore,
    });
  } catch (error) {
    console.error("加载会话历史失败:", error);
    // 发生错误时返回空历史
    panel.webview.postMessage({
      command: "conversationHistory",
      items: [],
      total: 0,
      hasMore: false,
    });
  }
}

6. 使用示例

6.1 创建新任务并保存对话

const historyManager = ChatHistoryManager.getInstance();

// 创建新任务
const task = await historyManager.createTask(
  'C:\\Users\\admin\\Documents\\Project',
  '实现计数器功能'
);

// 添加用户消息
await historyManager.addUserMessage('请帮我生成一个4位计数器');

// 添加 AI 消息
await historyManager.addAiMessage(
  '好的我来帮你生成一个4位计数器...',
  [{ id: '1', toolName: 'generateCode', parameters: {} }]
);

// 添加工具执行结果
await historyManager.addToolExecutionResult(
  '1',
  'generateCode',
  '代码生成成功'
);

// 记录对话元数据
await historyManager.recordTurnMeta(
  1,
  { inputTokens: 120, outputTokens: 350, totalTokens: 470 },
  'gpt-4',
  2500
);

6.2 查询会话历史

const historyManager = ChatHistoryManager.getInstance();

// 获取第一页前10条
const page1 = await historyManager.getConversationHistoryList(
  'C:\\Users\\admin\\Documents\\Project',
  0,
  10
);

console.log('总数:', page1.total);
console.log('是否还有更多:', page1.hasMore);
console.log('会话列表:', page1.items);

// 获取第二页第11-20条
const page2 = await historyManager.getConversationHistoryList(
  'C:\\Users\\admin\\Documents\\Project',
  10,
  10
);

6.3 切换任务

const historyManager = ChatHistoryManager.getInstance();

// 切换到指定任务
const success = await historyManager.switchTask(
  'C:\\Users\\admin\\Documents\\Project',
  'task_20231226_a3f9k2'
);

if (success) {
  // 获取当前任务会话
  const session = await historyManager.getCurrentTaskSession();
  console.log('任务元数据:', session.meta);
  console.log('对话历史:', session.messages);
  console.log('对话元数据:', session.conversationMeta);
}

7. 性能优化

7.1 分页加载

  • 前端默认每页加载 10 条记录
  • 最多显示 100 条历史记录
  • 滚动到底部时自动加载下一页

7.2 懒加载

  • 只在打开下拉菜单时才加载会话历史
  • 避免不必要的文件读取操作

7.3 缓存机制

  • 前端缓存已加载的会话列表
  • 避免重复请求相同数据

7.4 文件格式优化

  • 使用 JSONL 格式存储对话元数据,支持追加写入
  • 避免频繁读写整个文件

8. 错误处理

8.1 目录不存在

系统会自动创建不存在的目录:

private async ensureTaskDir(taskDir: string): Promise<void> {
  try {
    const uri = vscode.Uri.file(taskDir);
    try {
      await vscode.workspace.fs.stat(uri);
    } catch {
      // 目录不存在,创建它
      await vscode.workspace.fs.createDirectory(uri);
      console.log(`创建任务目录: ${taskDir}`);
    }
  } catch (error) {
    console.error("创建任务目录失败:", error);
    throw error;
  }
}

8.2 文件读取失败

读取失败时返回默认值:

private async loadConversation(): Promise<ChatMessage[]> {
  try {
    const uri = vscode.Uri.file(conversationPath);
    const content = await vscode.workspace.fs.readFile(uri);
    const data = Buffer.from(content).toString('utf-8');
    return JSON.parse(data);
  } catch (error) {
    // 文件不存在或读取失败,返回空数组
    return [];
  }
}

8.3 无工作区处理

没有打开工作区时,自动创建默认任务:

private async ensureCurrentTask(): Promise<void> {
  if (!this.currentTaskId || !this.currentProjectPath) {
    const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
    if (workspacePath) {
      await this.createTask(workspacePath, "默认任务");
    } else {
      throw new Error("没有打开的工作区,无法创建任务");
    }
  }
}

9. 未来扩展

9.1 会话切换功能

目前 selectConversation 功能暂未实现,未来可以支持:

  • 点击历史会话,加载该会话的完整对话历史
  • 在新标签页中打开历史会话
  • 继续历史会话的对话

9.2 会话搜索

  • 支持按关键词搜索会话
  • 支持按时间范围筛选
  • 支持按 Token 使用量排序

9.3 会话导出

  • 导出为 Markdown 格式
  • 导出为 JSON 格式
  • 导出为 PDF 格式

9.4 会话统计

  • 显示总对话轮次
  • 显示总 Token 使用量
  • 显示平均响应时间

9.5 云端同步

  • 支持将会话数据同步到云端
  • 支持多设备访问
  • 支持团队协作

10. 总结

IC Coder 的会话存储系统采用文件系统存储方案,具有以下优势:

  1. 简单可靠:无需额外的数据库依赖
  2. 易于备份:直接复制文件即可备份
  3. 跨平台:支持 Windows、macOS、Linux
  4. 可扩展:易于添加新的数据字段
  5. 高性能:分页加载,避免一次性加载大量数据

系统已经实现了核心的会话管理功能,包括任务创建、消息保存、历史查询等,为用户提供了完整的会话历史管理体验。