上传时做结构化类型路由(后缀 + Tika MIME,无 LLM):
| 路径 | 条件 | 处理 | 进大模型的方式 |
|---|---|---|---|
| 拒绝 | 图片/音视频/压缩包/加密 | 明确提示不支持(无 OCR/ASR) | 不发消息 |
| A 内联 | 小表格(≤200 行)/ 小文档(全文 ≤30000 字) | 一次提取:表格→markdown 全文;文档→Tika 全文 | 全文拼进 USER 消息(Claude document block 同款) |
| B Python 直读 | 大表格(含 100MB CSV) | 一次流式扫描:行数+表头+前 20 样本(不物化);xlsx 顺手转 sidecar csv(免 openpyxl 依赖)。不落 DuckDB、不建表 | 消息注入「文件路径+行数+列+样本+指引」→ Agent 写 execute_python:pd.read_csv(path) pandas/numpy 全量计算(沙箱已验证可读任意服务器路径) |
| C RAG | 大文档(100MB txt/word、全文 >30000 字) | 全文落 sidecar → 分块(1000/重叠100, 上限500块) → embedding → pgvector | doStream 按用户问题 cosine 检索 top-8 片段注入 + 指引 |
计算意图 = 已有沙箱,零新开发:PythonAnalysisTool.execute_python(ProcessBuilder 子进程隔离,pandas/numpy/duckdb/matplotlib,只读 DuckDB 快照,超时控制)+ execute_sql。不加前置意图分类器——意图路由由会话绑定 Agent 内化(IntentMiddleware + 工具选择),注入块写死指引:
表数据文件:
{path}(N 行,列:…,前 20 行样本见上)。计算/统计必须用 execute_python 读该文件全量计算,不要只依据样本;文档问题基于上方全文/检索片段回答。
执行过程经现有 tool_call/tool_input/tool_result SSE 在 ProcessTimeline 可视化(写了什么代码、跑了什么、返回什么)——f.txt 的"生成代码→沙箱执行→返回"完整呈现。
业界对应:A=Claude document block / ChatGPT 小文件直注入;B=Code Interpreter 直读文件;C=ChatGPT file_search RAG。
{storage.root}/chat-attachments/{userId}/{caseId}/{sessionId}/{雪花}_{safeName},sanitize + normalize 防穿越(沿用 DmService#saveUploadedFile 手法)chat_attachment 记 user_id/case_id/session_id,loadForSession 校验不匹配即 400——跨用户/跨案件/跨会话读不到;Python 沙箱只拿到归属校验通过后的路径chat_att_vec_{caseId}_d{dims} 按案件分表(沿 rag_ws{id}_d{dims} 惯例);chunk payload 强制带 sessionId+attachmentId,检索 SQL WHERE 过滤当前会话——同案件其他会话搜不到别人的附件片段(不写 DuckDB,故无需 DuckDB 层隔离;沙箱可读服务器路径是既有属性,非本次新增面。)
新增 module/agent/attachment/:
ChatAttachmentService
upload(file, session):校验(表格 ≤200MB / 文档 ≤50MB / 白名单 / sanitize)→ 存盘三层路径 → 路由执行(A 提取 / B 流式扫描+(xlsx)sidecar csv / C Tika 全文→sidecar→同步 RAG 索引)→ 落表 → 返回 {id, name, size, kind, rows/chars, truncated, indexedChunks?, status}loadForSession(ids, session, question):三重归属 → 内联块 / 表指引块(路径+schema+样本)/ 大文档用 question 检索 top-8 片段(复用 RagVectorMapper.searchVector 同款 cosine SQL + session 过滤)purgeOrphans():@Scheduled 24h 清理(文件 + sidecar + pgvector 行)AttachmentSheetScanner:csv 流式一行行读(行数+表头+样本,常数内存);xls/xlsx Fesod AiRowReader.forEachRow 流式(同一回调顺写 sidecar csv)AttachmentDocIndexer:分块 → embedding(EmbeddingModelFactory.resolveDefault(),与 RagSchemaService 同一模型维度,并发 8 路)→ RagVectorMapper 建表/写入。未配置 embedding 模型 → 上传报错「未配置嵌入模型,无法索引大文档」(与现有 RAG 同一前提)AttachmentContentRouter:DOCUMENT | TABLE | UNSUPPORTED(纯函数单测)ChatAttachment entity + Mapper + sql/chat_attachment.sql:id/user_id/case_id/session_id/file_name/file_path/sidecar_path/mime/kind/size_bytes/char_count/row_count/truncated/index_blocks/index_status/status/link_message_id/create_atChatAttachmentProperties:zsjz.chat-attachment(enabled/max-doc-mb=50/max-sheet-mb=200/inline-max-chars=30000/inline-max-rows=200/chunk-size=1000/chunk-overlap=100/max-chunks=500/top-k=8/白名单)AgentChatController 增 POST /chat/attachments(multipart)修改:ChatRequestDTO + attachmentIds;AgentChatServiceImpl#doStream 的 msgs 组装(附件块+问题原文单条 USER Msg);persistUserMessage 落原文 + metadata.attachments 徽标。
ChatComposer.vue:附件按钮 + hidden input(aiPlugin 三件套)→ 选择即上传(defHttp.uploadFile 显式拼 ctxPath+adminPath,进度抄 governApi)→ chips(文件名+大小+「N 行/已索引 N 片/截断」+转圈/失败×移除);emit('send'):string → { text, attachmentIds }ChatPanel / useAiChatPage / chatStream.ts:载荷透传;乐观 userMessage 带附件;onRegenerate 复用原 attachmentIdschatApi.ts+types.ts:ChatStreamParams.attachmentIds(协议前后端同步,AI_AGENT.md §6)ChatMessageItem.vue:用户气泡徽标;hydrateMessage 从 metadata.attachments 还原历史mvn compile + 新单测:Router 三分类、SheetScanner(csv 行数/样本/xlsx sidecar)、Extractor 截断、Indexer 分块与 payload session 字段、loadForSession 跨会话 400、doStream 注入块(mock agent 捕获 Msg)execute_python(pd.read_csv)→ 结果对行数核对;② 100MB txt/word 问细节 → RAG 片段注入 → 原文可核对;③ 小 txt/小表直接回答;④ 图片拒绝;⑤ 刷新徽标仍在;⑥ 旧 attachmentId 换会话 400read_csv 参数迭代),这是 Code Interpreter 模式的固有能力预计改动:后端新增 7 文件 + DDL,修改 3;前端新增 1,修改 7;测试新增 3-4。相比上版砍掉了 DuckDB 导入整条流水线。