From 270adbce47a6d8eb0548510c4014d3afd730ad6d Mon Sep 17 00:00:00 2001 From: ymq Date: Tue, 18 Aug 2026 13:53:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20task=5Ftree=20=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=91=E7=8A=B6=E7=BB=93=E6=9E=84=EF=BC=88=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E2=86=92=E8=BF=AD=E4=BB=A3=E2=86=92=E9=98=B6=E6=AE=B5=E2=86=92?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=EF=BC=8C=E6=87=92=E5=8A=A0=E8=BD=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wwwroot/api/task_tree.dspy | 162 +++++++++++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 32 deletions(-) diff --git a/wwwroot/api/task_tree.dspy b/wwwroot/api/task_tree.dspy index a7f42d8..cfedce4 100644 --- a/wwwroot/api/task_tree.dspy +++ b/wwwroot/api/task_tree.dspy @@ -1,4 +1,9 @@ -# task_tree.dspy - 任务树(懒加载:无 id 返回根节点,id=__root__ 返回任务列表) +# task_tree.dspy - 任务树(树状结构:项目 → 迭代 → 阶段 → 任务) +# 懒加载协议: +# 无 id → 返回项目根节点 +# __root__ → 返回迭代列表(sd_iterations + 未分配迭代) +# iter:{iter_id} → 返回阶段列表(需求/设计/开发/测试/部署,只显示有任务的) +# iter:{iter_id}:phase:{role} → 返回该迭代该阶段的任务列表(叶子) import json @@ -11,17 +16,34 @@ 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', + 'completed': '✅', 'approved': '✅', 'running': '🟡', 'failed': '❌', + 'waiting': '⚠️', 'submitted': '⬜', 'review': '👀', 'rejected': '🚫', + 'paused': '⏸️', 'cancelled': '🛑', } +_state_zh = { + 'completed': '已完成', 'approved': '已批准', 'running': '运行中', + 'failed': '失败', 'waiting': '等待中', 'submitted': '已提交', + 'review': '审核中', 'rejected': '已驳回', 'paused': '已暂停', 'cancelled': '已取消', +} +# 阶段顺序(role 规范名 → 中文名) +_phase_order = [ + ('agent.requirement', '需求阶段'), + ('agent.design', '设计阶段'), + ('agent.develop', '开发阶段'), + ('agent.test', '测试阶段'), + ('agent.deploy', '部署阶段'), +] + + +def _iter_of(task): + """从任务 params 里提取 iteration_id(存的是迭代名称,非 id)。""" + params_str = getattr(task, 'params', '') or '' + try: + p = json.loads(params_str) if params_str else {} + return (p.get('iteration_id') or '').strip() + except Exception: + return '' + async with DBPools().sqlorContext(dbname) as sor: # 当前项目 @@ -41,31 +63,107 @@ async with DBPools().sqlorContext(dbname) as sor: if precs: pname = getattr(precs[0], 'name', '') or '' + # 一次查所有任务 + 所有迭代(任务数不大,Python 分组避免 SQL JSON 函数坑) + all_tasks = await sor.sqlExe( + "SELECT id, title, role, state, params FROM pipeline_tasks WHERE tenant_id=${p}$ ORDER BY created_at ASC", + {"p": pid}) or [] + iters = await sor.sqlExe( + "SELECT id, iteration_name, status FROM sd_iterations WHERE project_id=${p}$ ORDER BY created_at ASC", + {"p": pid}) 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') + if node_id == '__root__': + # 迭代列表 + nodes = [] + for it in iters: + iid = getattr(it, 'id', '') + iname = getattr(it, 'iteration_name', '') or '' + istatus = getattr(it, 'status', '') or '' + if not iid: + continue + cnt = sum(1 for t in all_tasks if _iter_of(t) == iname) + nodes.append({ + "id": "iter:" + iid, + "label": "🗂 {} [{}] ({})".format(iname, istatus, cnt), + "is_leaf": False, + }) + # 未分配迭代(无 iteration_id 的任务) + none_cnt = sum(1 for t in all_tasks if not _iter_of(t)) nodes.append({ - "id": tid, - "label": "{} [{}] {}".format(icon, role, title), - "is_leaf": True, + "id": "iter:__none__", + "label": "🗂 未分配迭代 ({})".format(none_cnt), + "is_leaf": False, }) + return json.dumps(nodes, ensure_ascii=False) - return json.dumps(nodes, ensure_ascii=False) + if node_id.startswith('iter:'): + rest = node_id[5:] + parts = rest.split(':phase:') + iter_id = parts[0] + phase_role = parts[1] if len(parts) > 1 else '' + + # 该迭代的迭代名 + iter_name = '' + if iter_id != '__none__': + for it in iters: + if getattr(it, 'id', '') == iter_id: + iter_name = getattr(it, 'iteration_name', '') or '' + break + + # 该迭代的任务 + def _in_iter(t): + if iter_id == '__none__': + return not _iter_of(t) + return _iter_of(t) == iter_name + + iter_tasks = [t for t in all_tasks if _in_iter(t)] + + if not phase_role: + # 阶段列表 + role_cnt = {} + for t in iter_tasks: + r = (getattr(t, 'role', '') or '').strip() + role_cnt[r] = role_cnt.get(r, 0) + 1 + nodes = [] + for rn, rzh in _phase_order: + if rn in role_cnt: + nodes.append({ + "id": "iter:{}:phase:{}".format(iter_id, rn), + "label": "📌 {} ({})".format(rzh, role_cnt[rn]), + "is_leaf": False, + }) + if '' in role_cnt: + nodes.append({ + "id": "iter:{}:phase:__none__".format(iter_id), + "label": "📌 其他 ({})".format(role_cnt['']), + "is_leaf": False, + }) + return json.dumps(nodes, ensure_ascii=False) + + # 任务列表 + nodes = [] + for t in iter_tasks: + r = (getattr(t, 'role', '') or '').strip() + if phase_role == '__none__': + if r: + continue + elif r != phase_role: + continue + 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, '⬜') + zh = _state_zh.get(state, state) + nodes.append({ + "id": tid, + "label": "{} [{}] {}".format(icon, zh, title), + "is_leaf": True, + }) + return json.dumps(nodes, ensure_ascii=False) + + return json.dumps([], ensure_ascii=False)