pipeline-bidding/wwwroot/api/bid_task_tree.dspy

187 lines
8.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.

# bid_task_tree.dspy - 投标产线任务树(懒加载,与开发产线 /task 同构)
# 树状结构:项目 → 阶段(角色分组) → 任务 → 步骤/交付件
# 协议:无 id → 根节点__root__ → 阶段role:xxx → 任务裸任务ID → 步骤+交付件
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-bidding')
_state_icons = {
'completed': '\u2705', 'approved': '\u2705', 'running': '\U0001f7e1',
'failed': '\u274c', 'waiting': '\u26a0\ufe0f', 'submitted': '\u2b1c',
'review': '\U0001f440', 'rejected': '\U0001f6ab', 'paused': '\u23f8\ufe0f',
'cancelled': '\U0001f6d1', 'qc_review': '\U0001f50d',
}
_state_zh = {
'completed': '已完成', 'approved': '已批准', 'running': '运行中',
'failed': '失败', 'waiting': '等待中', 'submitted': '已提交',
'review': '审核中', 'rejected': '已驳回', 'paused': '已暂停',
'cancelled': '已取消', 'qc_review': '审核中',
}
# 投标产线角色分组(按业务流转顺序)
_ROLE_GROUPS = [
('agent.tender_analyst', '\U0001f50d 招标解析'),
('agent.qc', '\U0001f4cb QC契合度审核'),
('agent.bid_prep', '\U0001f4c4 资料准备'),
('agent.bid_writer', '\u270d\ufe0f 技术章编写'),
('agent.bid_biz_writer', '\u270d\ufe0f 商务章编写'),
('agent.bid_reviewer', '\U0001f50e 章节评审'),
('agent.bid_compositor', '\U0001f4e6 标书合成'),
('agent.bid_scorer', '\U0001f3af 整书评分'),
('agent.pm', '\U0001f91d 项目管理'),
]
_ROLE_SET = set(r for r, _ in _ROLE_GROUPS)
async with DBPools().sqlorContext(dbname) as sor:
# ── 解析当前投标项目(三级兜底) ──
pid = ''
# 1) 投标产线默认会话的项目绑定
recs = await sor.sqlExe(
"SELECT current_project_id FROM pipeline_session_settings "
"WHERE user_id=${u}$ AND session_id='default_bidding_general' LIMIT 1", {"u": uid})
if recs:
pid = getattr(recs[0], 'current_project_id', '') or ''
# 2) agent 全局设置
if not pid:
recs = await sor.sqlExe(
"SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$", {"u": uid})
if recs:
pid = getattr(recs[0], 'current_project_id', '') or ''
# 3) 最近一个进行中的投标项目
if not pid:
recs = await sor.sqlExe(
"SELECT id FROM sd_projects WHERE pipeline_id='bidding_general' "
"AND status='in_progress' ORDER BY updated_at DESC LIMIT 1")
if recs:
pid = getattr(recs[0], 'id', '') or ''
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)
# 一次查全部任务 + 步骤数 + 交付件数项目内任务量不大Python 分组)
all_tasks = await sor.sqlExe(
"SELECT id, title, role, state, params, parent_id, created_at, updated_at "
"FROM pipeline_tasks WHERE tenant_id=${p}$ ORDER BY created_at ASC", {"p": pid}) or []
step_cnt = {}
for r in await sor.sqlExe(
"SELECT task_id, COUNT(*) AS c FROM pipeline_task_steps "
"WHERE task_id IN (SELECT id FROM pipeline_tasks WHERE tenant_id=${p}$) GROUP BY task_id",
{"p": pid}) or []:
step_cnt[getattr(r, 'task_id', '')] = int(getattr(r, 'c', 0) or 0)
del_cnt = {}
for r in await sor.sqlExe(
"SELECT task_id, COUNT(*) AS c FROM pipeline_deliverables WHERE project_id=${p}$ GROUP BY task_id",
{"p": pid}) or []:
del_cnt[getattr(r, 'task_id', '')] = int(getattr(r, 'c', 0) or 0)
if node_id == '__root__':
# 阶段列表(按角色分组,只列有任务的)
role_cnt = {}
for t in all_tasks:
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, label in _ROLE_GROUPS:
if rn in role_cnt:
nodes.append({"id": "role:" + rn,
"label": "{} ({})".format(label, role_cnt[rn]), "is_leaf": False})
other_cnt = sum(v for k, v in role_cnt.items() if k not in _ROLE_SET)
if other_cnt > 0:
nodes.append({"id": "role:__other__",
"label": "\U0001f4ad 其他 ({})".format(other_cnt), "is_leaf": False})
if not nodes:
nodes = [{"id": "__empty__", "label": "暂无任务", "is_leaf": True}]
return json.dumps(nodes, ensure_ascii=False)
if node_id.startswith('role:'):
# 该角色分组下的任务(只列顶层任务)
role = node_id[5:]
nodes = []
for t in all_tasks:
r = (getattr(t, 'role', '') or '').strip()
if role == '__other__':
if r in _ROLE_SET:
continue
elif r != role:
continue
if (getattr(t, 'parent_id', '') or '').strip():
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, '\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)
# ── 裸任务 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, '\u2b1c')
zh = _state_zh.get(state, state)
nodes.append({"id": tid, "label": "{} [{}] {}".format(icon, zh, title[:60]),
"is_leaf": not (step_cnt.get(tid, 0) > 0 or del_cnt.get(tid, 0) > 0)})
steps = await sor.sqlExe(
"SELECT id, step_name, display_name, state FROM pipeline_task_steps "
"WHERE task_id=${t}$ ORDER BY step_order ASC", {"t": node_id}) or []
for s in steps:
sid = getattr(s, 'id', '')
label = getattr(s, 'display_name', '') or getattr(s, 'step_name', '') or ''
st = getattr(s, 'state', '') or ''
if label:
nodes.append({"id": "step:" + sid,
"label": " {} [{}] \u2699\ufe0f {}".format(_state_icons.get(st, '\u2b1c'),
_state_zh.get(st, st), label[:50]),
"is_leaf": True})
dels = await sor.sqlExe(
"SELECT id, deliverable_type, title FROM pipeline_deliverables "
"WHERE task_id=${t}$ ORDER BY created_at DESC", {"t": node_id}) or []
for d in dels:
did = getattr(d, 'id', '')
dt = getattr(d, 'deliverable_type', '') or ''
dtitle = getattr(d, 'title', '') or ''
nodes.append({"id": "del:" + did,
"label": " \U0001f4e4 {}: {}".format(dt, (dtitle or '')[:40]),
"is_leaf": True})
return json.dumps(nodes, ensure_ascii=False)