docs/plan/2026-06-18-file-reference-design.md
Rama main · réplica de lectura del repositorio original.
# 文件引用功能设计文档
## 1. 需求概述
在桌面端聊天输入框中,用户输入 `@` 符号时弹出当前工作区的文件列表面板,支持按文件名过滤,选中后以标签形式插入编辑器。发送消息时,Agent 收到被引用文件的完整绝对路径。
**核心价值**:让用户在对话中快速引用工作区文件,无需手动复制粘贴路径,Agent 可以直接读取被引用的文件内容。
## 2. 现状分析
### 2.1 输入框架构
聊天输入基于 **TipTap 富文本编辑器**(`desktop/src/components/chat/ChatInput.vue`),已支持:
- 自定义 `QuickCommandNode` 内联节点(技能/指令标签)
- 触发机制:输入 `/` 或 `、` 打开 `QuickCommandPanel`
- 键盘导航:方向键选择、Enter 确认、Escape 关闭
### 2.2 工作区
| 执行模式 | 工作区来源 | 文件访问方式 |
|---------|-----------|------------|
| LOCAL | 用户通过目录选择器指定本地路径 | Electron IPC 访问本地文件系统 |
| CLOUD | 后端自动生成:`{workspace-root}/{userId}/{sessionId}` | 后端直接访问服务器文件系统 |
- 云端工作区根目录:`/opt/mao/data/workspace`(配置项 `app.path-sandbox.workspace-root`)
- 工作区路径存储在 `Session.workspace` 字段
### 2.3 消息发送
通过 WebSocket `send_message` 事件,payload 结构:
```json
{
"type": "send_message",
"sessionId": 123,
"data": {
"content": "用户文本(含 ${skill}$ 和 #{command}# 标记)",
"eventId": "uuid",
"images": ["https://oss-url"]
}
}
```
后端 `StreamingWsHandler.handleSendMessage()` 解析 content 后直接传入 Agent 执行。
## 3. 功能设计
### 3.1 触发方式
在 TipTap 编辑器中输入 `@` 字符触发文件选择面板,规则:
- `@` 前必须是文本开头或空白字符(避免邮箱地址等误触发)
- `@` 与光标之间不能有空格(空格表示放弃选择)
- 触发后面板显示工作区文件列表,随用户继续输入实时过滤
检测逻辑与现有 `detectSlashTrigger()` 平行,在 `editor.onUpdate` 回调中新增 `detectAtTrigger()` 方法。
### 3.2 文件选择面板
新增组件 `FileReferencePanel.vue`,位于 `desktop/src/components/chat/`。
**交互行为**:
- 面板以浮层(popover/dropdown)形式出现在输入框上方
- 展示当前工作区的文件树(仅文件,不展示空目录)
- 支持输入文本按文件名模糊过滤(路径匹配),输入防抖 300ms
- 键盘操作:`↑`/`↓` 移动选中、`Enter` 确认选择、`Escape` 关闭面板
- 鼠标点击亦可选中
- 文件数量上限:单次最多展示 20 条结果
- 默认展示工作区根目录下的文件(递归,但排除隐藏文件和常见忽略目录)
- 排序规则:按文件修改时间倒序(最近修改的优先展示)
**排除规则**:
- 隐藏文件/目录(以 `.` 开头)
- `node_modules`、`__pycache__`、`.git`、`target`、`dist`、`build` 等常见构建产物目录
### 3.3 文件列表获取
需要根据执行模式从不同来源获取文件列表:
**LOCAL 模式**:
- 通过 Electron IPC 调用本地文件系统扫描
- 新增 IPC 通道:`listWorkspaceFiles(workspace, filter)`
- Electron main 进程执行目录遍历,返回文件路径列表(相对于工作区的相对路径)
**CLOUD 模式**:
- 调用后端 REST API 获取文件列表
- 新增接口:`GET /api/v1/files/workspace-list?sessionId={id}&filter={keyword}`
- 工作区路径由后端根据 sessionId 自动推导,不接受客户端传入 workspace 参数
- 后端扫描指定工作区目录,返回文件列表
前端统一封装为 `listWorkspaceFiles(workspace, filter, executionMode)` 工具函数,根据模式分发到对应数据源。
### 3.4 TipTap 节点 — FileReferenceNode
新增 TipTap 扩展 `FileReferenceNode.ts`,位于 `desktop/src/components/chat/tiptap/`。
**设计原则**:与现有 `QuickCommandNode` 保持一致的架构模式。
```
节点类型:inline, atomic, non-draggable, selectable
属性:
- filePath: string // 文件相对路径(相对于工作区根目录)
渲染:
- 编辑器内:蓝色标签,显示文件名(仅最后一段),hover 显示完整相对路径
- 如:📄 config/app.yml
序列化文本格式:@{config/app.yml}@
```
**序列化格式说明**:
采用 `@{...}@` 标记,与现有 `${...}$`(技能)和 `#{...}#`(指令)风格一致。路径使用相对于工作区的相对路径,避免绝对路径过长。
### 3.5 消息序列化
TipTap 编辑器中 FileReferenceNode 存储的是相对路径,序列化为 `@{相对路径}@` 格式。
发送前,前端在 `useChat.sendMessage()` 中将相对路径替换为绝对路径:
- 拼接规则:`workspace + "/" + 相对路径`
- LOCAL 模式:workspace 为用户选择的本地目录
- CLOUD 模式:workspace 为后端自动生成的隔离目录
采用绝对路径的原因:同一个任务会话内不存在切换工作区的场景,路径在会话生命周期内始终有效。
编辑器内序列化示例:
```
请帮我检查 @{src/config/app.yml}@ 的配置是否正确
```
发送给后端的 content 示例:
```
请帮我检查 @{/opt/mao/data/workspace/1/100/src/config/app.yml}@ 的配置是否正确
```
### 3.6 后端处理
后端 `StreamingWsHandler` 收到消息后,将 content 中的 `@{...}@` 标记**原地替换为文件绝对路径的纯文本**(去除 `@{` 和 `}@` 标记符号),与 `${skill_name}$` 的处理方式类似。
替换前:
```
请帮我检查 @{/opt/mao/data/workspace/1/100/src/config/app.yml}@ 的配置是否正确
```
替换后(传给 LLM 的实际内容):
```
请帮我检查 /opt/mao/data/workspace/1/100/src/config/app.yml 的配置是否正确
```
Agent 收到的是普通文件路径文本,可直接通过 ReadFile 等工具读取,无需理解特殊标记语法。
### 3.7 消息展示
消息气泡中渲染文件引用标签,复用现有 `QuickCommandTag.vue` 的模式,新增 `FileReferenceTag.vue`:
- 显示文件名图标 + 文件名
- hover 提示完整路径
- 可点击:LOCAL 模式下用系统文件管理器打开所在目录
解析逻辑更新 `quick-command-parser.ts`,在 `parseQuickCommandSegments()` 中新增 `@{...}@` 模式识别,segment type 为 `'file'`。
## 4. 前端改动清单
### 新增文件
| 文件 | 说明 |
|------|------|
| `desktop/src/components/chat/FileReferencePanel.vue` | 文件选择浮层面板 |
| `desktop/src/components/chat/tiptap/FileReferenceNode.ts` | TipTap 内联节点扩展 |
| `desktop/src/components/chat/FileReferenceTag.vue` | 消息气泡中的文件引用标签 |
### 修改文件
| 文件 | 改动内容 |
|------|---------|
| `desktop/src/components/chat/ChatInput.vue` | 新增 `detectAtTrigger()` 检测逻辑;注册 FileReferenceNode 扩展;集成 FileReferencePanel;处理文件选中事件 |
| `desktop/src/composables/useChat.ts` | 发送消息前将文件引用的相对路径解析为绝对路径 |
| `desktop/src/utils/quick-command-parser.ts` | 新增 `@{...}@` 模式解析,支持 `file` segment type |
| `desktop/src/utils/chatMessage.ts` | 消息 segment 构建逻辑中处理 `file` 类型 |
| `desktop/electron/preload.cjs` | 新增 `listWorkspaceFiles` IPC 桥接 |
| `desktop/electron/main.cjs` | 实现 `listWorkspaceFiles` 的本地文件扫描逻辑 |
| `desktop/src/types/electron.d.ts` | ElectronAPI 类型声明中新增 `listWorkspaceFiles` |
### 后端新增
| 文件 | 改动内容 |
|------|---------|
| `backend/.../file/controller/FileController.java` | 新增 `GET /v1/files/workspace-list` 接口 |
| `backend/.../file/service/FileService.java` | 实现工作区文件扫描逻辑 |
## 5. API 设计
### 5.1 获取工作区文件列表(云端模式)
```
GET /api/v1/files/workspace-list?sessionId={id}&filter={keyword}&limit=20
```
**Query 参数**:
- `sessionId`(必填):会话 ID,后端根据此 ID 自动推导工作区路径
- `filter`(可选):文件名过滤关键词
- `limit`(可选,默认 20):最大返回数量
**响应**:
```json
{
"code": 0,
"data": {
"files": [
{ "path": "src/config/app.yml", "name": "app.yml", "size": 1024 },
{ "path": "src/main/java/App.java", "name": "App.java", "size": 2048 }
]
}
}
```
- `path` 为相对于工作区根目录的路径
- `name` 为文件名
- `size` 为文件字节数
**安全约束**:
- 工作区路径由后端根据 sessionId 自动推导,不接受客户端传入(防止路径穿越)
- 仅返回文件,不返回目录条目
- 排除隐藏文件和常见忽略目录
### 5.2 Electron IPC 通道
```typescript
// preload.cjs 新增
listWorkspaceFiles: (workspace: string, filter?: string, limit?: number) => Promise<WorkspaceFile[]>
interface WorkspaceFile {
path: string // 相对路径
name: string // 文件名
size: number // 字节数
}
```
## 6. 面板交互细节
### 6.1 触发与关闭
| 操作 | 行为 |
|------|------|
| 输入 `@`(满足触发条件) | 打开面板,展示工作区文件列表 |
| 继续输入字符 | 实时过滤文件列表 |
| `Enter` | 选中当前高亮文件,插入标签 |
| `Escape` | 关闭面板,保留已输入的 `@` 及过滤文本 |
| `Backspace` 删除 `@` | 关闭面板 |
| 点击面板外区域 | 关闭面板,保留已输入文本 |
| 选中文件后 | 插入 FileReferenceNode 标签 + 尾部空格,光标移至空格后 |
### 6.2 面板定位
复用 `QuickCommandPanel` 的定位策略:基于 TipTap 编辑器光标位置计算浮层坐标。
### 6.3 空状态
- 工作区为空时:提示"工作区内暂无文件"
- 过滤无结果时:提示"未找到匹配的文件"
## 7. 与快捷指令的共存
文件引用(`@`)与快捷指令(`/`、`、`)互不冲突:
- 触发字符不同,检测逻辑独立
- 同一条消息中可以同时包含文件引用和快捷指令
- TipTap 编辑器中两种节点各自独立,互不干扰
## 8. 不在本期范围内
以下能力不在本次需求范围内:
- 多文件同时引用(一次 `@` 只选一个文件)
- 文件夹引用(仅引用具体文件)
- 文件内容预览(面板中不展示文件内容)
- 引用文件时自动读取并注入上下文(由 Agent 自行决定是否读取)
- 管理后台(admin)的文件引用支持(admin 为只读会话查看,无输入框)