pipeline-sdlc/wwwroot/api/task_tree.dspy

224 lines
9.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# task_tree.dspy - 任务树(树状结构:项目 → 迭代 → 阶段 → 任务)
# 懒加载协议:
# 无 id → 返回项目根节点
# __root__ → 返回迭代列表sd_iterations + 未分配迭代)
# iter:{iter_id} → 返回阶段列表(需求/设计/开发/测试/部署,只显示有任务的)
# iter:{iter_id}:phase:{role} → 返回该迭代该阶段的任务列表(叶子)
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': '✅', 'approved': '✅', 'running': '🟡', 'failed': '❌',
'waiting': '⚠️', 'submitted': '⬜', 'review': '👀', 'rejected': '🚫',
'paused': '⏸️', 'cancelled': '🛑', 'qc_review': '🔍',
}
_state_zh = {
'completed': '已完成', 'approved': '已批准', 'running': '运行中',
'failed': '失败', 'waiting': '等待中', 'submitted': '已提交',
'review': '审核中', 'rejected': '已驳回', 'paused': '已暂停', 'cancelled': '已取消',
}
# 阶段顺序从产线角色集SDL_ROLES动态生成用 next_role 链推导线性角色(自动排除 qc 门禁)。
def _get_phase_order():
try:
from pipeline_core import list_roles
roles = list_roles("sdlc_general") or []
role_map = {r.name: r for r in roles}
next_set = {getattr(r, 'next_role', '') for r in roles if getattr(r, 'next_role', '')}
heads = [r for r in roles if r.name not in next_set and getattr(r, 'next_role', '')]
order = []
seen = set()
cur = heads[0] if heads else None
while cur and cur.name not in seen:
seen.add(cur.name)
desc = getattr(cur, 'description', '') or cur.name
order.append((cur.name, desc))
cur = role_map.get(getattr(cur, 'next_role', ''))
return order
except Exception:
return [
('agent.requirement', '需求阶段'),
('agent.design', '设计阶段'),
('agent.develop', '开发阶段'),
('agent.deploy_test', '测试环境部署'),
('agent.test', '测试阶段'),
('agent.deploy_prod', '生产环境部署'),
]
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:
# 当前项目2026-09-11 升级会话级解析——session 优先→全局兜底,
# 含悬空自愈+产线隔离;旧实现只读全局 pipeline_agent_settings多 tab 串项目)
_sid = (params_kw or {}).get('session_id', '') or ''
pid = ''
try:
from pipeline_service.workspace import get_session_project_id
pid = await get_session_project_id(sor, uid, _sid, 'sdlc_general') or ''
except Exception:
pid = ''
if not pid:
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 ''
# 一次查所有任务 + 所有迭代任务数不大Python 分组避免 SQL JSON 函数坑)
all_tasks = await sor.sqlExe(
"SELECT id, title, role, state, params, parent_id 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__':
# 迭代列表
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))
if none_cnt > 0:
nodes.append({
"id": "iter:__none__",
"label": "🗂 未分配迭代 ({})".format(none_cnt),
"is_leaf": 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:
# 子任务(有 parent_id挂在父任务下展示不计入阶段计数
# 否则阶段节点计数与点开后的平铺任务数不一致(计数含子任务、平铺只列顶层任务)。
if (getattr(t, 'parent_id', '') or '').strip():
continue
r = (getattr(t, 'role', '') or '').strip()
role_cnt[r] = role_cnt.get(r, 0) + 1
nodes = []
for rn, rzh in _get_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)
# 任务列表只列顶层任务parent_id 为空的;子任务挂在父任务下展开,避免平铺重复)
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 ''
parent = (getattr(t, 'parent_id', '') or '').strip()
if parent:
continue # 子任务不在阶段平铺,挂在父任务下展示
if not tid or not title:
continue
icon = _state_icons.get(state, '⬜')
zh = _state_zh.get(state, state)
has_child = any((getattr(c, 'parent_id', '') or '').strip() == tid for c in all_tasks)
nodes.append({
"id": tid,
"label": "{} [{}] {}".format(icon, zh, title),
"is_leaf": not has_child,
})
return json.dumps(nodes, ensure_ascii=False)
# 子任务层裸任务ID → 返回该任务的子任务parent_id == 该ID递归支持多级分解
nodes = []
for t in all_tasks:
if (getattr(t, 'parent_id', '') or '').strip() != node_id:
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)
has_child = any((getattr(c, 'parent_id', '') or '').strip() == tid for c in all_tasks)
nodes.append({
"id": tid,
"label": "{} [{}] {}".format(icon, zh, title),
"is_leaf": not has_child,
})
return json.dumps(nodes, ensure_ascii=False)