From bac65a61aa6f87046aeb4adc3c1ae4958b6574df Mon Sep 17 00:00:00 2001 From: ymq Date: Sat, 8 Aug 2026 21:22:34 +0800 Subject: [PATCH] SSE mode: text/event-stream + data: prefix + [DONE] end Backend: _sse() wraps each widget as 'data: \n\n', ends with [DONE] Frontend: XHR.onprogress splits on 'data: ', extracts json before \n\n --- wwwroot/api/.cockpit_chat.dspy.swp | Bin 0 -> 16384 bytes wwwroot/api/cockpit_chat.dspy | 36 +++++++++++++++++------------ 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 wwwroot/api/.cockpit_chat.dspy.swp diff --git a/wwwroot/api/.cockpit_chat.dspy.swp b/wwwroot/api/.cockpit_chat.dspy.swp new file mode 100644 index 0000000000000000000000000000000000000000..ac76b6ceb9dfea017ec82f029767b608e6847ecc GIT binary patch literal 16384 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\n\n" +def _sse(data): + return f"data: {data}\n\n" + def _w_text(t): return {"widgettype":"Text","options":{"text":t,"css":"agent-text"}} def _w_card(title, body, kind="success"): colors = {"success":"#10b981","error":"#ef4444","reply":"#6366f1","progress":"#f59e0b"} @@ -194,14 +198,15 @@ if action == 'send_message': uid = await get_user() org_id = await get_userorgid() or '0' - async def agent_stream(): + async def agent_sse(): async with DBPools().sqlorContext(dbname) as sor: ctx = await _load_ctx(sor, uid) model = await _sel_model(sor, user_model_id) if not model: - d = json.dumps({"type":"widget","widget":_w_card("❌ 错误",_w_text("无可用LLM"),"error")},ensure_ascii=False)+chr(10) - debug(f"AGENT_STREAM yield error: {d}") - yield d + d = json.dumps({"type":"widget","widget":_w_card("❌ 错误",_w_text("无可用LLM"),"error")},ensure_ascii=False) + debug(f"SSE yield error: {d[:80]}") + yield _sse(d) + yield _sse("[DONE]") return repos_str = ', '.join([r['n'] for r in ctx['repos']]) or '无' @@ -225,27 +230,28 @@ if action == 'send_message': if act.get('action') == 'tool_call': tool = act.get('tool','') label = _tool_labels.get(tool,tool) - d = json.dumps({"type":"widget","widget":_w_progress(f"🔄 {label}")},ensure_ascii=False)+chr(10) - debug(f"AGENT_STREAM yield: {d}") - yield d + d = json.dumps({"type":"widget","widget":_w_progress(f"🔄 {label}")},ensure_ascii=False) + debug(f"SSE yield progress: {d[:80]}") + yield _sse(d) result = await _exec_tool(sor, tool, act.get('params',{}), ctx, uid, org_id) ok = result.startswith("OK:") - d = json.dumps({"type":"widget","widget":_w_card(f"{'✅' if ok else '❌'} {label}",_w_text(result),"success" if ok else "error")},ensure_ascii=False)+chr(10) - debug(f"AGENT_STREAM yield: {d}") - yield d + d = json.dumps({"type":"widget","widget":_w_card(f"{'✅' if ok else '❌'} {label}",_w_text(result),"success" if ok else "error")},ensure_ascii=False) + debug(f"SSE yield result: {d[:80]}") + yield _sse(d) msgs.append({"role":"assistant","content":raw}) msgs.append({"role":"user","content":f"工具 {tool} 结果:\n{result}"}) continue agent_reply = raw; break if not agent_reply: agent_reply = "处理超时,请简化需求。" - d = json.dumps({"type":"widget","widget":_w_card("🤖 驾驶舱 Agent",_w_text(agent_reply),"reply")},ensure_ascii=False)+chr(10) - debug(f"AGENT_STREAM yield final: {d}") - yield d + d = json.dumps({"type":"widget","widget":_w_card("🤖 驾驶舱 Agent",_w_text(agent_reply),"reply")},ensure_ascii=False) + debug(f"SSE yield final: {d[:80]}") + yield _sse(d) + yield _sse("[DONE]") await sor.C('pipeline_conversations',{'id':getID(),'role':'agent','content':agent_reply,'msg_type':'text','org_id':org_id,'created_by':'system'}) - return await stream_response(request, agent_stream, 'application/x-ndjson') + return await stream_response(request, agent_sse, 'text/event-stream') elif action == 'list_messages': async with DBPools().sqlorContext(dbname) as sor: