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
This commit is contained in:
ymq 2026-08-08 17:15:50 +08:00
parent 0d2f6da261
commit 883b87c0bb
2 changed files with 38 additions and 36 deletions

View File

@ -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:

View File

@ -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;i<fnames.length;i++){var fn=fnames[i];var fl=new bricks.Text({text:'📎 '+fn,cfontsize:0.75,color:'#3b82f6',css:'file-tag',style:{background:'#eff6ff',padding:'2px 8px',borderRadius:'4px',display:'inline-block',marginTop:'4px'}});um.add_widget(fl)}ub.add_widget(new bricks.VBox({css:'filler'}));ub.add_widget(um);ub.add_widget(new bricks.Svg({rate:2,url:bricks_resource('imgs/chat-user.svg')}));chat.add_widget(ub);var ab=new bricks.HBox({width:'100%'});var am=new bricks.VBox({width:'85%',alignSelf:'flex-start',bgcolor:'#e8f0fe',borderRadius:'12px',padding:'12px 16px',marginBottom:'10px',gap:'4px'});am.add_widget(new bricks.Text({text:'Agent',cfontsize:0.75,color:'#3b82f6',fontWeight:'bold'}));at=new bricks.Text({text:'正在分析需求...',cfontsize:0.85,color:'#64748b'});am.add_widget(at);ab.add_widget(new bricks.Svg({rate:2,url:bricks_resource('imgs/llm.svg')}));ab.add_widget(am);ab.add_widget(new bricks.VBox({css:'filler'}));chat.add_widget(ab);}var fd=new FormData();fd.append('action','send_message');fd.append('message_text',p);fd.append('model_id',mid);fd.append('iteration_id',iid);fd.append('project_id',pid);fd.append('file_paths',JSON.stringify(fnames));fetch('/pipeline-sdlc/api/cockpit_chat.dspy',{method:'POST',body:fd}).then(function(r){return r.json()}).then(function(r){if(r.success){if(at)at.set_text(r.agent_reply||'已处理');if(r.widgets&&chat){for(var i=0;i<r.widgets.length;i++){var w=_makeWidget(r.widgets[i]);if(w)chat.add_widget(w);chat.scrollToBottom()}}}else{if(at)at.set_text('错误: '+(r.error||'未知错误'))}var s=bricks.getWidgetById('stats_row',bricks.app);if(s)s.render({})}).catch(function(e){if(at)at.set_text('网络错误: '+e.message)})"
"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;i<fnames.length;i++){var fn=fnames[i];var fl=new bricks.Text({text:'📎 '+fn,cfontsize:0.75,color:'#3b82f6',css:'file-tag',style:{background:'#eff6ff',padding:'2px 8px',borderRadius:'4px',display:'inline-block',marginTop:'4px'}});um.add_widget(fl)}ub.add_widget(new bricks.VBox({css:'filler'}));ub.add_widget(um);ub.add_widget(new bricks.Svg({rate:2,url:bricks_resource('imgs/chat-user.svg')}));chat.add_widget(ub);var ab=new bricks.HBox({width:'100%'});var am=new bricks.VBox({width:'85%',alignSelf:'flex-start',bgcolor:'#e8f0fe',borderRadius:'12px',padding:'12px 16px',marginBottom:'10px',gap:'4px'});am.add_widget(new bricks.Text({text:'Agent',cfontsize:0.75,color:'#3b82f6',fontWeight:'bold'}));at=new bricks.Text({text:'正在分析需求...',cfontsize:0.85,color:'#64748b'});am.add_widget(at);ab.add_widget(new bricks.Svg({rate:2,url:bricks_resource('imgs/llm.svg')}));ab.add_widget(am);ab.add_widget(new bricks.VBox({css:'filler'}));chat.add_widget(ab);}var fd=new FormData();fd.append('action','send_message');fd.append('message_text',p);fd.append('model_id',mid);fd.append('iteration_id',iid);fd.append('project_id',pid);fd.append('file_paths',JSON.stringify(fnames));var xhr=new XMLHttpRequest();xhr.open('POST','/pipeline-sdlc/api/cockpit_chat.dspy');var lastLen=0;var first=true;xhr.onprogress=function(){var t=xhr.responseText;var d=t.substring(lastLen);lastLen=t.length;var lines=d.split('\n');for(var j=0;j<lines.length;j++){var line=lines[j].trim();if(!line)continue;try{var obj=JSON.parse(line);if(obj.type==='widget'&&obj.widget){var w=_makeWidget(obj.widget);if(w&&chat){if(first){am.remove_widget(at);first=false}chat.add_widget(w);chat.scrollToBottom()}}}catch(e){}}};xhr.onload=function(){if(at)at.set_text('已完成');var s=bricks.getWidgetById('stats_row',bricks.app);if(s)s.render({});};xhr.onerror=function(){if(at)at.set_text('网络错误')};xhr.send(fd)"
}
]
}