# bid_task_io.dspy - 投标任务详情(任务树右侧面板,与开发产线 task_io 同构) # 入参 id 可能为:任务ID / step:步骤ID / del:交付件ID import json uid = await get_user() if not uid: uid = 'user-01' raw_id = (params_kw or {}).get('id', '').strip() dbname = get_module_dbname('pipeline-bidding') _state_colors = { 'completed': '#16a34a', 'approved': '#16a34a', 'running': '#2563eb', 'failed': '#dc2626', 'waiting': '#f0a040', 'submitted': '#64748b', 'review': '#2196f3', 'rejected': '#dc2626', 'paused': '#f0a040', 'cancelled': '#64748b', 'qc_review': '#8b5cf6', 'qc_rejected': '#dc2626', 'verify_failed': '#f0a040', } if not raw_id: return json.dumps({ "widgettype": "Text", "options": {"otext": "请选择左侧任务查看输入输出", "text": "请选择左侧任务查看输入输出", "i18n": True, "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"}, }, ensure_ascii=False) async with DBPools().sqlorContext(dbname) as sor: # ── 步骤/交付件/QC审核轮次节点:先按自身查,查到就展示;查不到回落任务视图 ── task_id = raw_id step_focus = '' del_focus = '' if raw_id.startswith('rev:'): rid = raw_id[4:] recs = await sor.sqlExe( "SELECT id, task_id, qc_type, round, reviewer, fit_score, pass_score, passed, " "improvement, comments, created_at FROM bid_qc_reviews WHERE id=${r}$", {"r": rid}) if recs: rv = recs[0] passed = str(getattr(rv, 'passed', '0')) == '1' sc = float(getattr(rv, 'fit_score', 0) or 0) ps = float(getattr(rv, 'pass_score', 0) or 0) task_id = getattr(rv, 'task_id', '') or '' subwidgets = [ {"widgettype": "Text", "options": {"otext": "第${p0}轮 ${p1} QC 契合度审核", "text": "第${p0}轮 ${p1} QC 契合度审核", "i18n": True, "i18n_params": {"p0": str(getattr(rv, 'round', '')), "p1": str(getattr(rv, 'qc_type', '') or '')}, "halign": "left", "cfontsize": 1.1, "style": {"fontWeight": "bold", "color": "#1e293b"}}}, {"widgettype": "HBox", "options": {"gap": "12px", "width": "100%"}, "subwidgets": [ {"widgettype": "Text", "options": { "otext": ("${p0} / ${p1} → 通过" if passed else "${p0} / ${p1} → 未通过"), "text": ("${p0} / ${p1} → 通过" if passed else "${p0} / ${p1} → 未通过"), "i18n": True, "i18n_params": {"p0": "%.1f" % sc, "p1": "%.1f" % ps}, "cfontsize": 0.85, "color": "#ffffff", "padding": "1px 10px", "style": {"background": '#16a34a' if passed else '#dc2626', "borderRadius": "12px", "fontWeight": "bold"}}}, {"widgettype": "Text", "options": { "text": "审核: " + (getattr(rv, 'reviewer', '') or ''), "cfontsize": 0.75, "color": "#94a3b8"}}, {"widgettype": "Text", "options": { "text": str(getattr(rv, 'created_at', '') or '')[:16], "cfontsize": 0.75, "color": "#94a3b8"}}, {"widgettype": "Filler"}, ]}, {"widgettype": "Text", "options": {"otext": "\u2500\u2500 审核说明 \u2500\u2500", "text": "\u2500\u2500 审核说明 \u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#8b5cf6", "marginTop": "8px"}}}, {"widgettype": "VScrollPanel", "options": {"css": "filler", "width": "100%", "minHeight": "100px", "maxHeight": "360px", "padding": "6px 12px", "bgcolor": "#ffffff", "border": "1px solid #e2e8f0", "borderRadius": "6px"}, "subwidgets": [{"widgettype": "MdWidget", "options": {"mdtext": getattr(rv, 'comments', '') or '(无)', "width": "100%"}}]}, ] imp = getattr(rv, 'improvement', '') or '' if imp: subwidgets.append({"widgettype": "Text", "options": {"otext": "\u2500\u2500 改进意见 \u2500\u2500", "text": "\u2500\u2500 改进意见 \u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#b45309", "marginTop": "8px"}}}) subwidgets.append({"widgettype": "VScrollPanel", "options": {"css": "filler", "width": "100%", "minHeight": "100px", "maxHeight": "360px", "padding": "6px 12px", "bgcolor": "#ffffff", "border": "1px solid #e2e8f0", "borderRadius": "6px"}, "subwidgets": [{"widgettype": "MdWidget", "options": {"mdtext": imp, "width": "100%"}}]}) subwidgets.append({"widgettype": "Text", "options": {"otext": "(以下为该轮审核所属任务详情)", "text": "(以下为该轮审核所属任务详情)", "i18n": True, "cfontsize": 0.75, "color": "#94a3b8", "marginTop": "8px"}}) return json.dumps({ "widgettype": "VBox", "options": {"width": "100%", "padding": "12px", "gap": "6px", "overflowY": "auto"}, "subwidgets": subwidgets, }, ensure_ascii=False) task_id = raw_id elif raw_id.startswith('step:'): sid = raw_id[5:] recs = await sor.sqlExe( "SELECT id, task_id, step_name, display_name, step_type, state, error_msg, " "started_at, completed_at FROM pipeline_task_steps WHERE id=${s}$", {"s": sid}) if recs: s = recs[0] task_id = getattr(s, 'task_id', '') or '' step_focus = sid st = getattr(s, 'state', '') or '' subwidgets = [ {"widgettype": "Text", "options": {"text": "\u2699\ufe0f " + (getattr(s, 'display_name', '') or getattr(s, 'step_name', '') or ''), "halign": "left", "cfontsize": 1.1, "style": {"fontWeight": "bold", "color": "#1e293b"}}}, {"widgettype": "HBox", "options": {"gap": "12px", "width": "100%"}, "subwidgets": [ {"widgettype": "Text", "options": {"text": st, "cfontsize": 0.75, "color": "#ffffff", "padding": "1px 10px", "style": {"background": _state_colors.get(st, '#64748b'), "borderRadius": "12px", "fontWeight": "bold"}}}, {"widgettype": "Text", "options": { "otext": "开始: ${p0}", "text": "开始: ${p0}", "i18n": True, "i18n_params": {"p0": str(str(getattr(s, 'started_at', '') or '')[:16])}, "cfontsize": 0.75, "color": "#94a3b8"}}, {"widgettype": "Text", "options": { "otext": "结束: ${p0}", "text": "结束: ${p0}", "i18n": True, "i18n_params": {"p0": str(str(getattr(s, 'completed_at', '') or '')[:16])}, "cfontsize": 0.75, "color": "#94a3b8"}}, {"widgettype": "Filler"}, ]}, ] if getattr(s, 'error_msg', ''): subwidgets.append({"widgettype": "Text", "options": {"text": "错误: " + str(getattr(s, 'error_msg', ''))[:500], "cfontsize": 0.8, "color": "#dc2626", "wrap": True, "halign": "left"}}) subwidgets.append({"widgettype": "Text", "options": {"otext": "(以下为所属任务详情)", "text": "(以下为所属任务详情)", "i18n": True, "cfontsize": 0.75, "color": "#94a3b8", "marginTop": "8px"}}) return json.dumps({ "widgettype": "VBox", "options": {"width": "100%", "padding": "12px", "gap": "6px"}, "subwidgets": subwidgets, }, ensure_ascii=False) task_id = raw_id elif raw_id.startswith('del:'): did = raw_id[4:] recs = await sor.sqlExe( "SELECT id, task_id, deliverable_type, title, content, file_path, project_id, " "review_status, review_comment, quality_score FROM pipeline_deliverables WHERE id=${d}$", {"d": did}) if recs: d = recs[0] from pipeline_service.task_io_render import deliverable_widgets subwidgets = [{"widgettype": "Text", "options": {"text": "\U0001f4e4 " + (getattr(d, 'title', '') or getattr(d, 'deliverable_type', '') or ''), "halign": "left", "cfontsize": 1.1, "style": {"fontWeight": "bold", "color": "#1e293b"}}}] subwidgets += deliverable_widgets({ "deliverable_type": getattr(d, 'deliverable_type', '') or '', "title": getattr(d, 'title', '') or '', "content": getattr(d, 'content', '') or '', "file_path": getattr(d, 'file_path', '') or '', "project_id": getattr(d, 'project_id', '') or '', "review_status": getattr(d, 'review_status', '') or '', "quality_score": getattr(d, 'quality_score', '') or '', "review_comment": getattr(d, 'review_comment', '') or '', }, entire_url("/pipeline-sdlc/api/project_file.dspy")) return json.dumps({ "widgettype": "VBox", "options": {"width": "100%", "padding": "12px", "gap": "6px", "overflowY": "auto"}, "subwidgets": subwidgets, }, ensure_ascii=False) task_id = raw_id # ── 任务视图 ── 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": {"otext": "任务不存在", "text": "任务不存在", "i18n": True, "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 '' updated_at = str(getattr(task, 'updated_at', '') or '')[:16] created_at = str(getattr(task, 'created_at', '') or '')[:16] # 输入:params 里的任务描述与 QC 改进意见 params_str = getattr(task, 'params', '') or '' description = '' qc_imp = '' stage = '' dim = '' try: p = json.loads(params_str) if params_str else {} description = p.get('description', '') or p.get('input_text', '') or p.get('prompt', '') or '' qc_imp = p.get('qc_improvements', '') or '' if isinstance(qc_imp, list): qc_imp = '\n'.join(str(x) for x in qc_imp) stage = p.get('stage', '') or '' dim = p.get('analysis_dim', '') or '' except Exception: description = params_str # 输出:交付件(md→MdWidget渲染+下载;office/pdf→下载链接;全产线统一) dels = await sor.sqlExe( "SELECT deliverable_type, title, content, file_path, project_id, review_status, " "quality_score, review_comment " "FROM pipeline_deliverables WHERE task_id=${t}$ ORDER BY created_at DESC", {"t": task_id}) proj_file_url = entire_url("/pipeline-sdlc/api/project_file.dspy") # 执行步骤 steps = await sor.sqlExe( "SELECT step_name, display_name, state, error_msg, started_at, completed_at " "FROM pipeline_task_steps WHERE task_id=${t}$ ORDER BY step_order ASC", {"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"}}, }) info_row = [ {"widgettype": "Text", "options": {"text": state, "cfontsize": 0.75, "color": "#ffffff", "padding": "1px 10px", "style": {"background": sc, "borderRadius": "12px", "fontWeight": "bold"}}}, {"widgettype": "Text", "options": {"otext": "角色: ${p0}", "text": "角色: ${p0}", "i18n": True, "i18n_params": {"p0": str(role)}, "cfontsize": 0.8, "color": "#64748b"}}, ] if stage: info_row.append({"widgettype": "Text", "options": {"otext": "阶段: ${p0}${p1}", "text": "阶段: ${p0}${p1}", "i18n": True, "i18n_params": {"p0": str(stage), "p1": str((" / " + dim) if dim else "")}, "cfontsize": 0.75, "color": "#94a3b8"}}) info_row.append({"widgettype": "Text", "options": {"otext": "创建: ${p0} | 更新: ${p1}", "text": "创建: ${p0} | 更新: ${p1}", "i18n": True, "i18n_params": {"p0": str(created_at), "p1": str(updated_at)}, "cfontsize": 0.72, "color": "#94a3b8"}}) info_row.append({"widgettype": "Filler"}) subwidgets.append({"widgettype": "HBox", "options": {"gap": "12px", "width": "100%"}, "subwidgets": info_row}) if last_error: subwidgets.append({ "widgettype": "Text", "options": {"text": "\u274c 错误: " + last_error[:500], "cfontsize": 0.8, "color": "#dc2626", "wrap": True, "halign": "left", "css": "card"}, }) # 输入 subwidgets.append({ "widgettype": "Text", "options": {"otext": "\u2500\u2500 输入 \u2500\u2500", "text": "\u2500\u2500 输入 \u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#3b82f6", "marginTop": "8px"}}, }) if description: subwidgets.append({ "widgettype": "Text", "options": {"text": description[:4000], "cfontsize": 0.85, "color": "#334155", "wrap": True, "halign": "left", "css": "card"}, }) if qc_imp: subwidgets.append({ "widgettype": "Text", "options": {"text": "QC改进意见(重做输入):\n" + str(qc_imp)[:3000], "cfontsize": 0.82, "color": "#b45309", "wrap": True, "halign": "left", "css": "card"}, }) if not description and not qc_imp: subwidgets.append({"widgettype": "Text", "options": {"otext": "(无任务描述)", "text": "(无任务描述)", "i18n": True, "cfontsize": 0.8, "color": "#94a3b8"}}) # 执行步骤 if steps: subwidgets.append({ "widgettype": "Text", "options": {"otext": "\u2500\u2500 执行步骤 \u2500\u2500", "text": "\u2500\u2500 执行步骤 \u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#8b5cf6", "marginTop": "8px"}}, }) for s in steps: sn = getattr(s, 'step_name', '') or '' dn = getattr(s, 'display_name', '') or '' st = getattr(s, 'state', '') or '' err = getattr(s, 'error_msg', '') or '' label = dn or sn s_color = _state_colors.get(st, '#64748b') subwidgets.append({ "widgettype": "HBox", "options": {"gap": "8px", "width": "100%", "padding": "4px 8px", "css": "card"}, "subwidgets": [ {"widgettype": "Text", "options": {"text": label[:80], "cfontsize": 0.82, "color": "#1e293b"}}, {"widgettype": "Filler"}, {"widgettype": "Text", "options": {"text": st, "cfontsize": 0.75, "color": s_color}}, ], }) if err: subwidgets.append({"widgettype": "Text", "options": {"text": " " + err[:300], "cfontsize": 0.72, "color": "#dc2626", "wrap": True, "halign": "left"}}) # 输出(交付件) subwidgets.append({ "widgettype": "Text", "options": {"otext": "\u2500\u2500 输出(交付件)\u2500\u2500", "text": "\u2500\u2500 输出(交付件)\u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#16a34a", "marginTop": "8px"}}, }) if not dels: subwidgets.append({"widgettype": "Text", "options": {"otext": "(暂无交付件)", "text": "(暂无交付件)", "i18n": True, "cfontsize": 0.8, "color": "#94a3b8"}}) else: from pipeline_service.task_io_render import deliverable_card for d in dels: subwidgets.append(deliverable_card({ "deliverable_type": getattr(d, 'deliverable_type', '') or '', "title": getattr(d, 'title', '') or '', "content": getattr(d, 'content', '') or '', "file_path": getattr(d, 'file_path', '') or '', "project_id": getattr(d, 'project_id', '') or '', "review_status": getattr(d, 'review_status', '') or '', "quality_score": getattr(d, 'quality_score', '') or '', "review_comment": getattr(d, 'review_comment', '') or '', }, proj_file_url)) return json.dumps({ "widgettype": "VBox", "options": {"width": "100%", "height": "100%", "padding": "12px", "gap": "6px", "overflowY": "auto"}, "subwidgets": subwidgets, }, ensure_ascii=False)