cockpit: file upload support — read files, inject to LLM, emit file widgets

- Parse file_paths from request, read file contents (UTF-8, max 8000 chars)
- File content appended to LLM message as 【附件文件内容】
- File widget streamed to frontend: 📎 filename (size) + hidden content preview
- Frontend script: click .file-name → toggle .file-content visibility
- File paths saved in pipeline_conversations.attachments
This commit is contained in:
ymq 2026-08-08 15:58:59 +08:00
parent 39deb68ba6
commit 2b0bc7519a

View File

@ -310,12 +310,39 @@ _tool_names = {
def _tool_label(tool):
return _tool_names.get(tool, tool)
def _widget_file(filename, content_preview, file_size=0):
"""Bricks widget: file attachment with expandable content preview."""
size_str = f" ({file_size} bytes)" if file_size else ""
return {
"widgettype": "VBox",
"options": {
"css": "agent-file",
"style": {"border": "1px solid #e2e8f0", "borderRadius": "8px", "padding": "8px 12px", "margin": "4px 0"}
},
"subwidgets": [
{"widgettype": "HBox", "subwidgets": [
{"widgettype": "Text", "options": {"text": f"📎 {filename}{size_str}", "css": "file-name",
"style": {"color": "#2563eb", "cursor": "pointer", "fontSize": "13px", "fontWeight": "bold"}}}
]},
{"widgettype": "Text", "options": {"text": content_preview or "(空文件)", "css": "file-content",
"style": {"display": "none", "whiteSpace": "pre-wrap", "fontSize": "12px", "color": "#475569",
"background": "#f8fafc", "borderRadius": "4px", "padding": "8px", "marginTop": "6px",
"maxHeight": "300px", "overflowY": "auto"}}}
]
}
# ── Streaming Agent Loop ──
if action == 'send_message':
message_text = (params_kw or {}).get('message_text', '').strip()
user_model_id = (params_kw or {}).get('model_id', '')
file_paths_raw = (params_kw or {}).get('file_paths', '[]')
file_paths = []
try:
file_paths = json.loads(file_paths_raw) if isinstance(file_paths_raw, str) else (file_paths_raw or [])
except (json.JSONDecodeError, TypeError):
file_paths = []
if not message_text:
return json.dumps({"error": "message_text is required"}, ensure_ascii=False)
@ -346,8 +373,29 @@ if action == 'send_message':
for h in (history or []):
msgs.append({"role":'user' if getattr(h,'role','')=='user' else 'assistant',"content":getattr(h,'content','')})
await sor.C('pipeline_conversations',{'id':getID(),'iteration_id':'','task_id':'','role':'user','content':message_text,'attachments':'[]','msg_type':'text','org_id':org_id,'created_by':uid})
msgs.append({"role":"user","content":message_text})
await sor.C('pipeline_conversations',{'id':getID(),'iteration_id':'','task_id':'','role':'user','content':message_text,'attachments':json.dumps(file_paths),'msg_type':'text','org_id':org_id,'created_by':uid})
# 处理上传文件读取内容、发送文件widget、注入LLM上下文
file_contents = []
for fp in file_paths:
try:
if os.path.isfile(fp):
with open(fp, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()[:8000]
fsize = os.path.getsize(fp)
fname = os.path.basename(fp)
file_contents.append(f"=== 文件: {fname} ===\n{content}")
# 发送文件widget
preview = content[:1000] + ("\n...(截断)" if len(content) > 1000 else "")
yield json.dumps({"type":"widget","widget":_widget_file(fname, preview, fsize)}, ensure_ascii=False) + '\n'
except Exception:
pass
# 把用户消息+文件内容合并为一条LLM消息
full_message = message_text
if file_contents:
full_message = message_text + "\n\n【附件文件内容】\n" + "\n\n".join(file_contents)
msgs.append({"role":"user","content":full_message})
agent_reply = ''
for turn in range(10):