From 883b87c0bb11e0662b69027281044ef07f19b587 Mon Sep 17 00:00:00 2001 From: ymq Date: Sat, 8 Aug 2026 17:15:50 +0800 Subject: [PATCH] streaming v3.1: XHR+onprogress frontend + stream_response backend Backend: stream_response with NDJSON (restored) Frontend: XMLHttpRequest.onprogress replaces fetch.getReader() - more compatible, no ReadableStream API dependency - incrementally parses NDJSON lines as they arrive --- wwwroot/api/cockpit_chat.dspy | 72 ++++++++++++++++++----------------- wwwroot/index_cockpit.ui | 2 +- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/wwwroot/api/cockpit_chat.dspy b/wwwroot/api/cockpit_chat.dspy index c8dd2e6..272a98d 100644 --- a/wwwroot/api/cockpit_chat.dspy +++ b/wwwroot/api/cockpit_chat.dspy @@ -193,49 +193,51 @@ if action == 'send_message': uid = await get_user() org_id = await get_userorgid() or '0' - widgets = [] - async with DBPools().sqlorContext(dbname) as sor: - ctx = await _load_ctx(sor, uid) - model = await _sel_model(sor, user_model_id) - if not model: return json.dumps({"success":True,"agent_reply":"❌ 没有可用的LLM模型配置"},ensure_ascii=False) + async def agent_stream(): + async with DBPools().sqlorContext(dbname) as sor: + ctx = await _load_ctx(sor, uid) + model = await _sel_model(sor, user_model_id) + if not model: + yield json.dumps({"type":"widget","widget":_w_card("❌ 错误",_w_text("无可用LLM"),"error")},ensure_ascii=False)+'\n' + return - repos_str = ', '.join([r['n'] for r in ctx['repos']]) or '无' - env_text = f"项目: {ctx['pname'] or '未选择'}\n仓库: {repos_str}" - system = AGENT_PROMPT.replace('__TOOLS__',TOOLS_TEXT).replace('__ENV__',env_text) - msgs = [{"role":"system","content":system}] + repos_str = ', '.join([r['n'] for r in ctx['repos']]) or '无' + env_text = f"项目: {ctx['pname'] or '未选择'}\n仓库: {repos_str}" + system = AGENT_PROMPT.replace('__TOOLS__',TOOLS_TEXT).replace('__ENV__',env_text) + msgs = [{"role":"system","content":system}] - history = await sor.sqlExe("SELECT role,content FROM pipeline_conversations ORDER BY created_at ASC LIMIT 20",{}) - for h in (history or []): - msgs.append({"role":'user' if getattr(h,'role','')=='user' else 'assistant',"content":getattr(h,'content','')}) - msgs.append({"role":"user","content":message_text}) + history = await sor.sqlExe("SELECT role,content FROM pipeline_conversations ORDER BY created_at ASC LIMIT 20",{}) + for h in (history or []): + msgs.append({"role":'user' if getattr(h,'role','')=='user' else 'assistant',"content":getattr(h,'content','')}) + msgs.append({"role":"user","content":message_text}) - await sor.C('pipeline_conversations',{'id':getID(),'role':'user','content':message_text,'msg_type':'text','org_id':org_id,'created_by':uid}) + await sor.C('pipeline_conversations',{'id':getID(),'role':'user','content':message_text,'msg_type':'text','org_id':org_id,'created_by':uid}) - agent_reply = '' - for turn in range(10): - raw = await _call_llm(model, msgs, 0.4) - act = _parse(raw) - if act.get('action') == 'reply': - agent_reply = act.get('message', raw); break - if act.get('action') == 'tool_call': - tool = act.get('tool','') - label = _tool_labels.get(tool,tool) - widgets.append(_w_progress(f"🔄 {label}")) - result = await _exec_tool(sor, tool, act.get('params',{}), ctx, uid, org_id) - ok = result.startswith("OK:") - widgets.append(_w_card(f"{'✅' if ok else '❌'} {label}", _w_text(result), "success" if ok else "error")) - msgs.append({"role":"assistant","content":raw}) - msgs.append({"role":"user","content":f"工具 {tool} 结果:\n{result}"}) - continue - agent_reply = raw; break + agent_reply = '' + for turn in range(10): + raw = await _call_llm(model, msgs, 0.4) + act = _parse(raw) + if act.get('action') == 'reply': + agent_reply = act.get('message', raw); break + if act.get('action') == 'tool_call': + tool = act.get('tool','') + label = _tool_labels.get(tool,tool) + yield json.dumps({"type":"widget","widget":_w_progress(f"🔄 {label}")},ensure_ascii=False)+'\n' + result = await _exec_tool(sor, tool, act.get('params',{}), ctx, uid, org_id) + ok = result.startswith("OK:") + yield json.dumps({"type":"widget","widget":_w_card(f"{'✅' if ok else '❌'} {label}",_w_text(result),"success" if ok else "error")},ensure_ascii=False)+'\n' + 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 = "处理超时,请简化需求。" - widgets.append(_w_card("🤖 驾驶舱 Agent", _w_text(agent_reply), "reply")) + if not agent_reply: agent_reply = "处理超时,请简化需求。" + yield json.dumps({"type":"widget","widget":_w_card("🤖 驾驶舱 Agent",_w_text(agent_reply),"reply")},ensure_ascii=False)+'\n' - await sor.C('pipeline_conversations',{'id':getID(),'role':'agent','content':agent_reply,'msg_type':'text','org_id':org_id,'created_by':'system'}) + await sor.C('pipeline_conversations',{'id':getID(),'role':'agent','content':agent_reply,'msg_type':'text','org_id':org_id,'created_by':'system'}) - return json.dumps({"success":True,"agent_reply":agent_reply,"widgets":widgets},ensure_ascii=False) + return await stream_response(request, agent_stream, 'application/x-ndjson') elif action == 'list_messages': async with DBPools().sqlorContext(dbname) as sor: diff --git a/wwwroot/index_cockpit.ui b/wwwroot/index_cockpit.ui index f7bc736..1741023 100644 --- a/wwwroot/index_cockpit.ui +++ b/wwwroot/index_cockpit.ui @@ -191,7 +191,7 @@ "event": "inputed", "actiontype": "script", "target": "self", - "script": "var p=params.prompt;var mid=params.model_id||'';if(typeof mid==='object'&&mid!==null)mid=mid.model_id||'';var ctx=bricks.getWidgetById('context_data',bricks.app);var cd=ctx?ctx.getValue():{};var iid=cd.iteration_id||'';var pid=cd.project_id||'';var fnames=params.file_names||[];var chat=bricks.getWidgetById('chat_scroll',bricks.app);var at=null;var _makeWidget=function(desc){if(!desc||!desc.widgettype)return null;var Klass=bricks.Factory.get(desc.widgettype);if(!Klass)return null;var opts=JSON.parse(JSON.stringify(desc.options||{}));if(desc.subwidgets&&desc.subwidgets.length>0){opts.subwidgets=desc.subwidgets.map(function(s){return _makeWidget(s)}).filter(function(s){return s!==null})}return new Klass(opts)};if(chat){var ub=new bricks.HBox({width:'100%'});var um=new bricks.VBox({width:'85%',alignSelf:'flex-end',bgcolor:'#dbeafe',borderRadius:'12px',padding:'12px 16px',marginBottom:'10px',gap:'4px'});um.add_widget(new bricks.Text({text:'你',cfontsize:0.75,color:'#2563eb',fontWeight:'bold'}));um.add_widget(new bricks.Text({text:p,cfontsize:0.95,color:'#1e293b',whiteSpace:'pre-wrap'}));for(var i=0;i0){opts.subwidgets=desc.subwidgets.map(function(s){return _makeWidget(s)}).filter(function(s){return s!==null})}return new Klass(opts)};if(chat){var ub=new bricks.HBox({width:'100%'});var um=new bricks.VBox({width:'85%',alignSelf:'flex-end',bgcolor:'#dbeafe',borderRadius:'12px',padding:'12px 16px',marginBottom:'10px',gap:'4px'});um.add_widget(new bricks.Text({text:'你',cfontsize:0.75,color:'#2563eb',fontWeight:'bold'}));um.add_widget(new bricks.Text({text:p,cfontsize:0.95,color:'#1e293b',whiteSpace:'pre-wrap'}));for(var i=0;i