diff --git a/wwwroot/api/cockpit_chat_v2.dspy b/wwwroot/api/cockpit_chat_v2.dspy index a5cf997..72fae61 100644 --- a/wwwroot/api/cockpit_chat_v2.dspy +++ b/wwwroot/api/cockpit_chat_v2.dspy @@ -130,7 +130,12 @@ if action == 'send_message': result = data.get('result', '') yield json.dumps({"content": result + "\n\n"}, ensure_ascii=False) + '\n' elif t == 'reply': - yield json.dumps({"content": data.get('message', '')}, ensure_ascii=False) + '\n' + msg = data.get('message', '') + if isinstance(msg, dict) and msg.get('widgettype'): + # widget JSON:顶层透传,前端 AgentOut 检测 widgettype 渲染(如 /task 的 PopupWindow) + yield json.dumps(msg, ensure_ascii=False) + '\n' + else: + yield json.dumps({"content": msg}, ensure_ascii=False) + '\n' elif t == 'ask_user': yield json.dumps({"content": "❓ " + data.get('message', '')}, ensure_ascii=False) + '\n' elif t == 'error': diff --git a/wwwroot/api/task_io.dspy b/wwwroot/api/task_io.dspy new file mode 100644 index 0000000..bfd7d92 --- /dev/null +++ b/wwwroot/api/task_io.dspy @@ -0,0 +1,161 @@ +# task_io.dspy - 任务的输入输出(ResourceBrowser 右侧面板,5 秒自动刷新) + +import json + +uid = await get_user() +if not uid: + uid = 'user-01' + +task_id = (params_kw or {}).get('id', '').strip() + +dbname = get_module_dbname('pipeline-sdlc') + +_state_colors = { + 'completed': '#16a34a', + 'approved': '#16a34a', + 'running': '#2563eb', + 'failed': '#dc2626', + 'waiting': '#f0a040', + 'submitted': '#64748b', + 'review': '#2196f3', + 'rejected': '#dc2626', + 'paused': '#f0a040', + 'cancelled': '#64748b', +} + +if not task_id: + return json.dumps({ + "widgettype": "Text", + "options": {"text": "请选择左侧任务查看输入输出", "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"}, + }, ensure_ascii=False) + +async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.sqlExe( + "SELECT id, title, role, state, params, last_error, created_at, updated_at " + "FROM pipeline_tasks WHERE id=${t}$", {"t": task_id}) + if not recs: + return json.dumps({ + "widgettype": "Text", + "options": {"text": "任务不存在", "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"}, + }, ensure_ascii=False) + + task = recs[0] + title = getattr(task, 'title', '') or '' + role = getattr(task, 'role', '') or '' + state = getattr(task, 'state', '') or '' + last_error = getattr(task, 'last_error', '') or '' + created_at = str(getattr(task, 'created_at', '') or '')[:16] + updated_at = str(getattr(task, 'updated_at', '') or '')[:16] + + # 输入:params 里的 description / input_text + params_str = getattr(task, 'params', '') or '' + description = '' + try: + p = json.loads(params_str) if params_str else {} + description = p.get('description', '') or p.get('input_text', '') or p.get('prompt', '') or '' + except Exception: + description = params_str + + # 输出:交付件 + dels = await sor.sqlExe( + "SELECT deliverable_type, title, content, review_status, quality_score, review_comment " + "FROM pipeline_deliverables WHERE task_id=${t}$ ORDER BY created_at DESC", {"t": task_id}) + + sc = _state_colors.get(state, '#64748b') + + subwidgets = [] + + # 标题行:任务名 + 状态 + subwidgets.append({ + "widgettype": "Text", + "options": {"text": title, "halign": "left", "cfontsize": 1.15, "style": {"fontWeight": "bold", "color": "#1e293b"}}, + }) + subwidgets.append({ + "widgettype": "HBox", + "options": {"gap": "12px", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "状态: " + state, "cfontsize": 0.8, "color": sc, "halign": "left"}}, + {"widgettype": "Text", "options": {"text": "角色: " + role, "cfontsize": 0.8, "color": "#64748b", "halign": "left"}}, + {"widgettype": "Text", "options": {"text": "更新: " + updated_at, "cfontsize": 0.75, "color": "#94a3b8", "halign": "left"}}, + {"widgettype": "Filler"}, + ], + }) + + if last_error: + subwidgets.append({ + "widgettype": "Text", + "options": {"text": "错误: " + last_error[:300], "halign": "left", "cfontsize": 0.8, "css": "resp-error", "wrap": True}, + }) + + # 输入 + subwidgets.append({ + "widgettype": "Text", + "options": {"text": "—— 输入 ——", "halign": "left", "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#3b82f6", "marginTop": "8px"}}, + }) + subwidgets.append({ + "widgettype": "Text", + "options": {"text": (description or "(无)")[:4000], "halign": "left", "cfontsize": 0.85, "color": "#334155", "wrap": True, "css": "card"}, + }) + + # 输出 + subwidgets.append({ + "widgettype": "Text", + "options": {"text": "—— 输出(交付件)——", "halign": "left", "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#16a34a", "marginTop": "8px"}}, + }) + if not dels: + subwidgets.append({ + "widgettype": "Text", + "options": {"text": "(暂无交付件)", "halign": "left", "cfontsize": 0.8, "color": "#94a3b8"}, + }) + else: + for d in dels: + d_type = getattr(d, 'deliverable_type', '') or '' + d_title = getattr(d, 'title', '') or '' + d_content = getattr(d, 'content', '') or '' + d_status = getattr(d, 'review_status', '') or '' + d_score = getattr(d, 'quality_score', '') or '' + d_comment = getattr(d, 'review_comment', '') or '' + card = { + "widgettype": "VBox", + "options": {"css": "card", "gap": "4px", "width": "100%", "padding": "8px"}, + "subwidgets": [ + {"widgettype": "HBox", "options": {"gap": "8px", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": d_type, "cfontsize": 0.8, "color": "#3b82f6", "halign": "left"}}, + {"widgettype": "Text", "options": {"text": d_title, "cfontsize": 0.8, "color": "#1e293b", "halign": "left"}}, + {"widgettype": "Filler"}, + {"widgettype": "Text", "options": {"text": d_status, "cfontsize": 0.75, "color": "#94a3b8", "halign": "left"}}, + ]}, + {"widgettype": "Text", "options": {"text": (d_content or "")[:3000], "cfontsize": 0.78, "color": "#475569", "wrap": True, "halign": "left"}}, + ], + } + if d_comment: + card["subwidgets"].append({ + "widgettype": "Text", + "options": {"text": "审核意见: " + d_comment[:300], "cfontsize": 0.75, "color": "#f0a040", "wrap": True, "halign": "left"}, + }) + subwidgets.append(card) + +# 5 秒刷新:on_parent 事件(add_widget 时触发)里 setInterval,重新 render_browser 刷新当前任务内容 +refresh_script = ( + "if(!window._task_io_timer){" + "window._task_io_timer=setInterval(function(){" + "var rb=bricks.getWidgetById('task_rb',bricks.app);" + "if(rb&&rb.current_id){var cid=rb.current_id;rb.current_id=undefined;rb.render_browser(cid);}" + "},5000);}" +) + +resp = { + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "padding": "12px", "gap": "6px", "overflowY": "auto"}, + "subwidgets": subwidgets, + "binds": [{ + "wid": "self", + "event": "on_parent", + "actiontype": "script", + "target": "self", + "script": refresh_script, + }], +} + +return json.dumps(resp, ensure_ascii=False) diff --git a/wwwroot/api/task_tree.dspy b/wwwroot/api/task_tree.dspy new file mode 100644 index 0000000..a7f42d8 --- /dev/null +++ b/wwwroot/api/task_tree.dspy @@ -0,0 +1,71 @@ +# task_tree.dspy - 任务树(懒加载:无 id 返回根节点,id=__root__ 返回任务列表) + +import json + +uid = await get_user() +if not uid: + uid = 'user-01' + +node_id = (params_kw or {}).get('id', '').strip() + +dbname = get_module_dbname('pipeline-sdlc') + +_state_icons = { + 'completed': '\u2705', + 'approved': '\u2705', + 'running': '\U0001f7e1', + 'failed': '\u274c', + 'waiting': '\u26a0\ufe0f', + 'submitted': '\u2b1c', + 'review': '\U0001f440', + 'rejected': '\U0001f6ab', + 'paused': '\u23f8\ufe0f', + 'cancelled': '\U0001f6d1', +} + +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}) + pid = getattr(recs[0], 'current_project_id', '') if recs else '' + + if not pid: + if not node_id: + return json.dumps([{"id": "__root__", "label": "请先选择项目", "is_leaf": True}], ensure_ascii=False) + return json.dumps([], ensure_ascii=False) + + # 项目名 + pname = '' + precs = await sor.sqlExe("SELECT name FROM sd_projects WHERE id=${p}$", {"p": pid}) + if precs: + pname = getattr(precs[0], 'name', '') or '' + + if not node_id: + # 根节点 + return json.dumps([{"id": "__root__", "label": pname or "项目", "is_leaf": False}], ensure_ascii=False) + + if node_id != '__root__': + return json.dumps([], ensure_ascii=False) + + # 任务列表(子节点) + tasks = await sor.sqlExe( + "SELECT id, title, role, state FROM pipeline_tasks WHERE tenant_id=${p}$ ORDER BY created_at ASC", + {"p": pid}) + + nodes = [] + for t in (tasks or []): + tid = getattr(t, 'id', '') + title = getattr(t, 'title', '') or '' + role = getattr(t, 'role', '') or '' + state = getattr(t, 'state', '') or '' + if not tid or not title: + continue + icon = _state_icons.get(state, '\u2b1c') + nodes.append({ + "id": tid, + "label": "{} [{}] {}".format(icon, role, title), + "is_leaf": True, + }) + + return json.dumps(nodes, ensure_ascii=False)