diff --git a/wwwroot/api/cockpit_chat_v2.dspy b/wwwroot/api/cockpit_chat_v2.dspy new file mode 100644 index 0000000..09d92a1 --- /dev/null +++ b/wwwroot/api/cockpit_chat_v2.dspy @@ -0,0 +1,112 @@ +# cockpit_chat_v2.dspy - SDLC Agent v2 (AgentExecutor 驱动) +# 替代 cockpit_chat.dspy,使用 pipeline-core/pipeline-service v2 架构 + +import aiohttp + +action = (params_kw or {}).get('action', 'send_message') +if action != 'send_message': + p = params_kw or {} + msg = (p.get('message_text') or '').strip() + if not msg: + inner = p.get('params', {}) + if isinstance(inner, dict): + msg = (inner.get('prompt') or inner.get('message_text') or '').strip() + if msg: + action = 'send_message' + +dbname = get_module_dbname('pipeline-sdlc') + +# ── Widget helpers ── +def _w_text(t, css='agent-text'): + return {"widgettype":"Text","options":{"text":t,"halign":"left","css":css}} +def _w_progress(t): + return {"widgettype":"Text","options":{"text":t,"halign":"left","style":{"color":"#f0a040","fontStyle":"italic"}}} +def _w_md(t): + return {"widgettype":"MdWidget","options":{"text":t}} +def _w_card(title, body, kind="info"): + colors = {"success":"#4caf50","error":"#f44336","info":"#2196f3","warn":"#ff9800"} + color = colors.get(kind, colors["info"]) + title_w = {"widgettype":"Text","options":{"text":title,"halign":"left","style":{"fontWeight":"bold","color":color,"marginBottom":"4px"}}} + body_w = body if isinstance(body, dict) else _w_text(str(body)) + return {"widgettype":"VBox","subwidgets":[title_w, body_w], + "options":{"css":"agent-card","style":{"borderLeft":"3px solid "+color,"paddingLeft":"8px","marginBottom":"8px"}}} + +if action == 'send_message': + prompt = msg + uid = await get_user() + if not uid: + async def _err_stream(): + yield json.dumps(_w_text("请先登录"), ensure_ascii=False) + '\n' + return await stream_response(request, _err_stream, 'text/plain; charset=utf-8') + + # ── 加载上下文 ── + ctx = {'pid': '', 'name': ''} + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.sqlExe( + "SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$", + {"u": uid}) + if recs: + ctx['pid'] = getattr(recs[0], 'current_project_id', '') or '' + if ctx['pid']: + proj_recs = await sor.sqlExe( + "SELECT name FROM sd_projects WHERE id=${p}$", {"p": ctx['pid']}) + if proj_recs: + ctx['name'] = getattr(proj_recs[0], 'name', '') + + # ── 加载 v2 引擎 ── + from pipeline_core.agent_config import load_agent_config + from pipeline_service.agent_loop_v2 import AgentExecutor + + config = await load_agent_config(project_id=ctx['pid']) + executor = AgentExecutor( + config=config, + project_id=ctx['pid'], + user_id=uid, + ) + + async def agent_stream(): + # 项目上下文 + if ctx['name']: + d = json.dumps(_w_progress("项目: " + ctx['name']), ensure_ascii=False) + '\n' + yield d + + # 运行 AgentExecutor + async for chunk in executor.run(prompt): + data = json.loads(chunk) + t = data.get('type', '') + + if t == 'progress': + d = json.dumps(_w_progress(data.get('message', '')), ensure_ascii=False) + '\n' + elif t == 'auto_tool': + d = json.dumps(_w_progress(data.get('message', '')), ensure_ascii=False) + '\n' + elif t == 'tool_call': + tool = data.get('tool', '') + d = json.dumps(_w_card("调用: " + tool, "参数: " + json.dumps(data.get('params', {}), ensure_ascii=False), "info"), ensure_ascii=False) + '\n' + elif t == 'tool_result': + tool = data.get('tool', '') + result = data.get('result', '') + good = not result.startswith('ERROR') and not result.startswith('未知工具') + d = json.dumps(_w_card(tool + " 结果", result[:600], "success" if good else "error"), ensure_ascii=False) + '\n' + elif t == 'reply': + d = json.dumps(_w_md(data.get('message', '')), ensure_ascii=False) + '\n' + elif t == 'error': + d = json.dumps(_w_card("错误", data.get('message', ''), "error"), ensure_ascii=False) + '\n' + else: + d = chunk # 透传 + + yield d + + return await stream_response(request, agent_stream, 'text/plain; charset=utf-8') + +elif action == 'list_messages': + uid = await get_user() + if not uid: + return json.dumps({"error":"请先登录"}, ensure_ascii=False) + async with DBPools().sqlorContext(dbname) as sor: + ms = await sor.sqlExe( + "SELECT role,content,created_at FROM pipeline_conversations ORDER BY created_at ASC LIMIT 100", {}) + result = [{"role":getattr(m,'role',''),"content":getattr(m,'content','')} for m in (ms or [])] + return json.dumps({"success":True,"messages":result}, ensure_ascii=False, default=str) + +else: + return json.dumps({"error":"Unknown: " + str(action)}, ensure_ascii=False)