我们在第一期内测的时候,群里面在讨论上传文件的功能问题,确实首期是不支持的。
毕竟上传文件、解析文件、再注入AI的上下文等等,这些流程还需要理顺;另外还有文件的格式多种多样,所以很难做到一步到位。就连DeepSeek专家模式也把上传文件的功能关掉了。
当然,加上这个功能非常方便,可以把基本的需求列出来,或者直接把程序性文件直接传上来,不用手打或者打开复制,毕竟这种确实麻烦。
那我们这期介绍我们RealPLC对上传文件的支持,当然,我们还是会一步一步进行升级。这个功能会在第二期上线,敬请关注!
聊天文件上传功能允许用户在对话中附加 PLC 工程相关的文本/代码文件,系统将文件提取文本作为 AI 上下文注入本轮生成。
每次对话最多 10 个文件
┌─────────────────────────────────────────────────────────────────┐
│ 前端 (apps/web) │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ ChatInput.tsx │──▶│ uploadedFiles.ts │ FileReader → base64 │
│ │ (UI 组件) │ │ (上传服务) │ POST /api/v1/files │
│ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway (apps/api-gateway) │
│ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ files/routes.ts │──▶│ uploadedFile.ts │ │
│ │ POST /api/v1/files│ │ UploadedFileService │ │
│ │ GET /api/v1/files│ │ .createFromBase64() │ │
│ └──────────────────┘ │ .listForSession() │ │
│ │ .getReadyFilesByIds() │ │
│ └───────────────────────┘ │
│ │ │
│ ┌──────────────────┐ │ │
│ │ generate/routes │◀─────────────┘ │
│ │ POST /chat │ attachmentIds → getReadyFilesByIds() │
│ │ │ → buildAttachmentContext() │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 数据库 (packages/database) │
│ uploaded_files 表 │
│ (id, org_id, user_id, session_id, file_name, extracted_text...) │
└─────────────────────────────────────────────────────────────────┘
POST /api/v1/files
认证要求: 需要登录 + 组织上下文
请求体(JSON):
{
"fileName":"IO_Table.csv",
"mimeType":"text/csv",
"sizeBytes":2048,
"sessionId":"session-uuid",
"contentBase64":"data:text/csv;base64,SW5wdXQxLEkxLjAsQm9vbA..."
}
参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
fileName | string | 是 | 原始文件名 |
mimeType | string | 否 | MIME 类型,默认 text/plain |
sizeBytes | number | 否 | 前端传来的原始大小(后端以解码后 buffer 长度为准) |
sessionId | string | 否 | 关联的会话 ID |
contentBase64 | string | 是 | Base64 编码的文件内容(支持 data: URL 前缀) |
成功响应(201):
{
"file":{
"id":"uuid",
"fileName":"IO_Table.csv",
"mimeType":"text/csv",
"sizeBytes":2048,
"status":"ready",
"extractedTextLength":1800,
"createdAt":1716100000000
}
}
错误响应(400):
{
"error":"UPLOAD_FAILED",
"message":"Unsupported file type: .exe"
}
GET /api/v1/files?sessionId=<session-uuid>
响应(200):
{
"files":[{ ...UploadedFileSummary }]
}
asyncfunctionuploadChatFile(file: File, sessionId?:string|null):Promise<UploadedFileSummary>
**流程:**
1.`FileReader.readAsDataURL()` 将文件读为 base64 Data URL
2.`POST /api/v1/files`JSON 请求,携带 `fileName`、`mimeType`、`sizeBytes`、`sessionId`、`contentBase64`
3. 解析响应 JSON 返回 `UploadedFileSummary`
三种上传入口:
入口 | 触发方式 | 实现位置 |
|---|---|---|
文件选择器 | 点击"+"菜单 → "上传文件" → 系统文件对话框 | 隐藏 <input type="file" multiple> |
拖拽上传 | 拖拽文件到聊天输入框区域 | handleDrop + 拖拽覆盖层 |
粘贴上传 | Ctrl+V 粘贴剪贴板中的文件 | handlePaste |
核心状态:
const[uploadedFiles, setUploadedFiles]=useState<UploadedFileSummary[]>([]);
const[uploadError, setUploadError]=useState('');
const[uploadingCount, setUploadingCount]=useState(0);
const[isDragOver, setIsDragOver]=useState(false);
文件处理逻辑(handleFilesSelected):
1. 计算剩余槽位 = MAX_CHAT_FILES(10) - 已上传数量
│
2. 截取待上传文件列表(超出槽位的忽略)
│
3. Promise.allSettled() 并行上传所有文件
│
4. 成功 → 合并到 uploadedFiles 数组(最多 10 个)
失败 → 收集错误信息到 uploadError
│
5. finally: 递减上传计数,清空 file input value
拖拽处理:
•
使用 dragCounterRef 计数器防止子元素触发 dragLeave 导致的闪烁
•
拖拽时显示蓝色边框 + 覆盖层提示 "拖拽文件到此处上传"

文件卡片展示:
•
文件类型图标(PLC 文件用 FileCode 青色,CSV 用 TableProperties 绿色,文本用 FileText 蓝色)
•
文件名、格式化大小(KB/MB)
•
上传中微调器
•
删除按钮(×)

发送消息时,uploadedFiles 的 ID 通过 attachmentIds 传递到 GenerationConfig,随请求发送到 POST /chat。
我们后续更新方向包括:
1.
项目资料库 — 文件从"本轮附件"升级为"项目持久资料"
2.
结构化解析 — I/O 点表、报警表、PLC 代码语义提取
3.
大文件 RAG — 切块、向量检索、引用溯源
4.
PDF/Excel/Word/OCR — 非文本格式支持
5.
工程包导入 — ZIP/文件夹批量上传
6.
安全审查联动 — 上传代码自动接入验证与修复链路
参考链接:
【1】https://www.realplc.com