From 7039c4980e1758eea72e4d297c7a7ec42b1eaf0b Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 3 Sep 2026 16:55:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=BB=E5=8A=A1=E6=A0=91=E7=BB=B4?= =?UTF-8?q?=E5=BA=A6=E5=88=86=E7=BB=84=E2=80=94=E2=80=94analyst=E6=8C=894?= =?UTF-8?q?=E7=BB=B4=E5=BA=A6/qc=E6=8C=89=E7=B1=BB=E5=9E=8B=E8=81=9A?= =?UTF-8?q?=E5=90=88=EF=BC=8C=E4=B8=80=E6=AC=A1=E8=BF=87=3D=E5=8F=B6?= =?UTF-8?q?=E5=AD=90=EF=BC=8C=E5=A4=9A=E8=BD=AE=3D=E7=BB=84=E8=8A=82?= =?UTF-8?q?=E7=82=B9+=E8=BD=AE=E6=AC=A1=E5=8D=87=E5=BA=8F=E5=AD=90?= =?UTF-8?q?=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wwwroot/api/bid_task_tree.dspy | 130 +++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 6 deletions(-) diff --git a/wwwroot/api/bid_task_tree.dspy b/wwwroot/api/bid_task_tree.dspy index 1ab277c..65b377d 100644 --- a/wwwroot/api/bid_task_tree.dspy +++ b/wwwroot/api/bid_task_tree.dspy @@ -114,9 +114,27 @@ async with DBPools().sqlorContext(dbname) as sor: return json.dumps(nodes, ensure_ascii=False) if node_id.startswith('role:'): - # 该角色分组下的任务(只列顶层任务) + # 该角色分组下的任务。 + # 维度型角色(analyst 按 analysis_dim、qc 按 qc_type):先按维度/类型聚合, + # 一次过=叶子节点;多轮=组节点,子节点按轮次升序(第1轮→第N轮)。 role = node_id[5:] - nodes = [] + + def _params_of(t): + try: + return json.loads(getattr(t, 'params', '') or '{}') + except Exception: + return {} + + # QC 任务 → (qc_type, round) 映射(review 表的 round 是权威轮次) + qc_map = {} + if role == 'agent.qc': + for r in await sor.sqlExe( + "SELECT task_id, qc_type, round FROM bid_qc_reviews WHERE project_id=${p}$", + {"p": pid}) or []: + qc_map[getattr(r, 'task_id', '')] = (getattr(r, 'qc_type', '') or '', + int(getattr(r, 'round', 0) or 0)) + + role_tasks = [] for t in all_tasks: r = (getattr(t, 'role', '') or '').strip() if role == '__other__': @@ -127,20 +145,120 @@ async with DBPools().sqlorContext(dbname) as sor: if (getattr(t, 'parent_id', '') or '').strip(): continue tid = getattr(t, 'id', '') + if not tid or not (getattr(t, 'title', '') or ''): + continue + role_tasks.append(t) + + # ── 维度/类型分组 ── + if role in ('agent.tender_analyst', 'agent.qc'): + groups = {} + for t in role_tasks: + if role == 'agent.tender_analyst': + key = (_params_of(t).get('analysis_dim') or '').strip() or '__legacy__' + else: + key = qc_map.get(getattr(t, 'id', ''), ('', 0))[0] or '__none__' + groups.setdefault(key, []).append(t) + for key in groups: + groups[key].sort(key=lambda x: str(getattr(x, 'created_at', '') or '')) + + dim_labels = { + 'scoring': '评分项与得分规则', 'quals': '资质清单与要求', + 'reqs_outline': '投标文件要求与章节骨架', 'cost_benefit': '成本收益分析', + '__legacy__': '全量解析(维度拆分前)', + 'scoring_items': '评分项 QC', 'qualifications': '资质 QC', + 'doc_requirements': '投标文件要求 QC', 'chapter_outline': '章节骨架 QC', + '__none__': 'QC 审核', + } + order = ['scoring', 'quals', 'reqs_outline', 'cost_benefit', '__legacy__', + 'scoring_items', 'qualifications', 'doc_requirements', + 'chapter_outline', '__none__'] + nodes = [] + for key in order + [k for k in groups if k not in order]: + if key not in groups: + continue + ts = groups[key] + label = dim_labels.get(key, key) + if len(ts) == 1: + # 一次过:叶子节点 + t = ts[0] + st = getattr(t, 'state', '') or '' + nodes.append({ + "id": getattr(t, 'id', ''), + "label": "{} [{}] {}".format(_state_icons.get(st, '\u2b1c'), + _state_zh.get(st, st), label), + "is_leaf": not (step_cnt.get(getattr(t, 'id', ''), 0) > 0 or + del_cnt.get(getattr(t, 'id', ''), 0) > 0), + }) + else: + # 多轮:组节点 + 轮次升序子节点 + final_state = getattr(ts[-1], 'state', '') or '' + nodes.append({ + "id": "grp:" + role + ":" + key, + "label": "{}({} 轮)最新[{}]".format(label, len(ts), + _state_zh.get(final_state, final_state)), + "is_leaf": False, + }) + return json.dumps(nodes, ensure_ascii=False) + + # ── 非维度型角色:平铺任务列表(原逻辑)── + nodes = [] + for t in role_tasks: + tid = getattr(t, 'id', '') title = getattr(t, 'title', '') or '' state = getattr(t, 'state', '') or '' - if not tid or not title: - continue icon = _state_icons.get(state, '\u2b1c') zh = _state_zh.get(state, state) - upd = str(getattr(t, 'updated_at', '') or '')[5:16] has_child = step_cnt.get(tid, 0) > 0 or del_cnt.get(tid, 0) > 0 or \ any((getattr(c, 'parent_id', '') or '').strip() == tid for c in all_tasks) nodes.append({ "id": tid, "label": "{} [{}] {}".format(icon, zh, title[:60]), "is_leaf": not has_child, - "upd": upd, + }) + return json.dumps(nodes, ensure_ascii=False) + + if node_id.startswith('grp:'): + # 轮次子节点:按轮数升序(第1轮→第N轮) + _, role, key = node_id.split(':', 2) + + def _params_of2(t): + try: + return json.loads(getattr(t, 'params', '') or '{}') + except Exception: + return {} + + qc_map = {} + if role == 'agent.qc': + for r in await sor.sqlExe( + "SELECT task_id, qc_type, round FROM bid_qc_reviews WHERE project_id=${p}$", + {"p": pid}) or []: + qc_map[getattr(r, 'task_id', '')] = (getattr(r, 'qc_type', '') or '', + int(getattr(r, 'round', 0) or 0)) + ts = [] + for t in all_tasks: + if (getattr(t, 'role', '') or '').strip() != role: + continue + if (getattr(t, 'parent_id', '') or '').strip(): + continue + if role == 'agent.tender_analyst': + k = (_params_of2(t).get('analysis_dim') or '').strip() or '__legacy__' + else: + k = qc_map.get(getattr(t, 'id', ''), ('', 0))[0] or '__none__' + if k == key: + ts.append(t) + ts.sort(key=lambda x: str(getattr(x, 'created_at', '') or '')) + nodes = [] + for i, t in enumerate(ts): + tid = getattr(t, 'id', '') + st = getattr(t, 'state', '') or '' + rnd = qc_map.get(tid, ('', 0))[1] if role == 'agent.qc' else 0 + rnd_label = "第%d轮" % (rnd or (i + 1)) + upd = str(getattr(t, 'updated_at', '') or '')[5:16] + nodes.append({ + "id": tid, + "label": "{} {} [{}] {}".format(rnd_label, _state_icons.get(st, '\u2b1c'), + _state_zh.get(st, st), upd), + "is_leaf": not (step_cnt.get(tid, 0) > 0 or del_cnt.get(tid, 0) > 0), }) return json.dumps(nodes, ensure_ascii=False)