feat: 投标产线任务树——/task slash命令+菜单+弹窗(ResourceBrowser左树右详情,与开发产线同构):任务状态/输入/输出/执行步骤可见

This commit is contained in:
yumoqing 2026-09-03 15:59:22 +08:00
parent d66207032c
commit a1411fdd48
6 changed files with 573 additions and 0 deletions

View File

@ -461,6 +461,8 @@ def register_bid_ability():
"type": "popup", "width": "88%", "height": "82%"},
{"label": "📦 标书", "icon": "", "url": "/pipeline-bidding/bid_documents/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "📋 任务树", "icon": "", "url": "/pipeline-bidding/bid_task_tree_popup.ui",
"type": "popup", "width": "92%", "height": "86%", "require_project": True},
],
)
register_ability(ability)
@ -497,12 +499,49 @@ async def _slash_score(args, ctx):
return await ex._execute_ability_tool("bid_score_report", {})
async def _slash_task(args, ctx):
"""任务树 + 输入输出ResourceBrowser 弹窗,与开发产线 /task 同构)。"""
base = (ctx.get("base_url") or "").rstrip("/")
tree_url = f"{base}/pipeline-bidding/api/bid_task_tree.dspy"
io_url = f"{base}/pipeline-bidding/api/bid_task_io.dspy"
return {
"widgettype": "PopupWindow",
"options": {
"title": "任务树 - 输入输出",
"width": "85%",
"height": "80%",
"auto_open": True,
},
"subwidgets": [{
"widgettype": "ResourceBrowser",
"id": "bid_task_rb",
"options": {
"tree_options": {
"widgettype": "Tree",
"options": {
"dataurl": tree_url,
"textField": "label",
"idField": "id",
"cfontsize": 1.0,
},
},
"browser_options": {
"widgettype": "urlwidget",
"options": {"url": io_url},
},
"tree_width": "42%",
},
}],
}
def register_bid_slash_commands():
for cmd in [
SlashCommand("files", "列出招标文件及解析状态", _slash_files, "pipeline", PIPELINE_ID),
SlashCommand("bid", "诊断当前投标项目卡点", _slash_bid, "pipeline", PIPELINE_ID),
SlashCommand("chapters", "列出标书章节及状态", _slash_chapters, "pipeline", PIPELINE_ID),
SlashCommand("score", "查看最近一轮整书评分报告", _slash_score, "pipeline", PIPELINE_ID),
SlashCommand("task", "任务树(任务状态+输入输出)", _slash_task, "pipeline", PIPELINE_ID),
]:
register_slash_command(cmd)

View File

@ -33,6 +33,7 @@ PATHS_LOGINED = [
# 多 session 会话入口agent 目录 + 页面,菜单 open_tab 指向 /pipeline-bidding/agent
"/%s/agent" % MOD,
"/%s/agent/index.ui" % MOD,
"/%s/bid_task_tree_popup.ui" % MOD,
]
for t in TABLES:
PATHS_LOGINED += [

View File

@ -20,6 +20,19 @@
"target": "self",
"script": "var tp=bricks.getWidgetById('session_tabs',bricks.app);if(!tp)return;var sid='s'+Date.now()+'_'+Math.floor(Math.random()*100000);var n=tp.opts.items.length+1;tp.open_tab({name:'session_'+sid,label:'会话 '+n,removable:true,content:{widgettype:'VBox',options:{css:'filler',width:'100%',height:'100%'},subwidgets:[{widgettype:'urlwidget',options:{url:'/pipeline_core/api/agent_menus.dspy?session_id='+sid+'&pipeline_id=bidding_general',method:'GET'}},{widgettype:'AgentIO',options:{css:'filler',margin:'0 24px 24px 24px',url:'/pipeline_core/api/agent_chat.dspy?session_id='+sid+'&pipeline_id=bidding_general',model_dataurl:'/pipeline_core/api/agent_model_options.dspy?session_id='+sid+'&pipeline_id=bidding_general',model_cwidth:14,placeholder:'输入你的需求...'}}]}});"
}]
},
{
"widgettype": "Button",
"id": "task_tree_btn",
"options": {"label": "📋 任务树", "css": "small"},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "Popup",
"popup_options": {"title": "投标项目任务树", "width": "92%", "height": "88%", "auto_open": true},
"url": "/pipeline-bidding/bid_task_tree_popup.ui"
}]
}
]
},

View File

@ -0,0 +1,302 @@
# 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',
}
if not raw_id:
return json.dumps({
"widgettype": "Text",
"options": {"text": "请选择左侧任务查看输入输出", "cfontsize": 0.9,
"color": "#94a3b8", "padding": "16px"},
}, ensure_ascii=False)
async with DBPools().sqlorContext(dbname) as sor:
# ── 步骤/交付件节点:先按自身查,查到就展示;查不到回落任务视图 ──
task_id = raw_id
step_focus = ''
del_focus = ''
if 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": {
"text": "开始: " + str(getattr(s, 'started_at', '') or '')[:16],
"cfontsize": 0.75, "color": "#94a3b8"}},
{"widgettype": "Text", "options": {
"text": "结束: " + 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": {"text": "(以下为所属任务详情)", "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, review_status, "
"review_comment, quality_score FROM pipeline_deliverables WHERE id=${d}$", {"d": did})
if recs:
d = recs[0]
task_id = getattr(d, 'task_id', '') or ''
del_focus = did
sc = '#16a34a'
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"}}},
{"widgettype": "HBox", "options": {"gap": "12px", "width": "100%"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": getattr(d, 'deliverable_type', '') or '',
"cfontsize": 0.8, "color": "#3b82f6"}},
{"widgettype": "Text", "options": {"text": getattr(d, 'review_status', '') or '',
"cfontsize": 0.75, "color": "#94a3b8"}},
{"widgettype": "Text", "options": {"text": "评分: " + str(getattr(d, 'quality_score', '') or '-'),
"cfontsize": 0.75, "color": "#94a3b8"}},
{"widgettype": "Filler"},
]},
]
fp = getattr(d, 'file_path', '') or ''
if fp:
subwidgets.append({"widgettype": "Text",
"options": {"text": "文件: " + fp, "cfontsize": 0.78,
"color": "#64748b", "wrap": True, "halign": "left"}})
subwidgets.append({"widgettype": "Text",
"options": {"text": (getattr(d, 'content', '') or '')[:8000],
"cfontsize": 0.82, "color": "#334155", "wrap": True,
"halign": "left", "css": "card"}})
if getattr(d, 'review_comment', ''):
subwidgets.append({"widgettype": "Text",
"options": {"text": "审核意见: " + str(getattr(d, 'review_comment', ''))[:500],
"cfontsize": 0.78, "color": "#f0a040", "wrap": True, "halign": "left"}})
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": {"text": "任务不存在", "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
# 输出:交付件
dels = await sor.sqlExe(
"SELECT deliverable_type, title, content, file_path, review_status, quality_score, review_comment "
"FROM pipeline_deliverables WHERE task_id=${t}$ ORDER BY created_at DESC", {"t": task_id})
# 执行步骤
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": {"text": "角色: " + role,
"cfontsize": 0.8, "color": "#64748b"}},
]
if stage:
info_row.append({"widgettype": "Text", "options": {"text": "阶段: " + stage + ((" / " + dim) if dim else ""),
"cfontsize": 0.75, "color": "#94a3b8"}})
info_row.append({"widgettype": "Text", "options": {"text": "创建: " + created_at + " | 更新: " + 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": {"text": "\u2500\u2500 输入 \u2500\u2500", "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": {"text": "(无任务描述)", "cfontsize": 0.8, "color": "#94a3b8"}})
# 执行步骤
if steps:
subwidgets.append({
"widgettype": "Text",
"options": {"text": "\u2500\u2500 执行步骤 \u2500\u2500", "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": {"text": "\u2500\u2500 输出(交付件)\u2500\u2500", "cfontsize": 0.85,
"style": {"fontWeight": "bold", "color": "#16a34a", "marginTop": "8px"}},
})
if not dels:
subwidgets.append({"widgettype": "Text",
"options": {"text": "(暂无交付件)", "cfontsize": 0.8, "color": "#94a3b8"}})
else:
for d in dels:
d_type = getattr(d, 'deliverable_type', '') or ''
d_title = getattr(d, 'title', '') or ''
d_content = getattr(d, 'content', '') or ''
d_file = getattr(d, 'file_path', '') or ''
d_status = getattr(d, 'review_status', '') or ''
d_score = getattr(d, 'quality_score', '') or ''
d_comment = getattr(d, 'review_comment', '') or ''
card = {
"widgettype": "VBox",
"options": {"css": "card", "gap": "4px", "width": "100%", "padding": "8px"},
"subwidgets": [
{"widgettype": "HBox", "options": {"gap": "8px", "width": "100%"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": d_type,
"cfontsize": 0.8, "color": "#3b82f6"}},
{"widgettype": "Text", "options": {"text": d_title[:60],
"cfontsize": 0.8, "color": "#1e293b"}},
{"widgettype": "Filler"},
{"widgettype": "Text", "options": {"text": (d_status or '') + ((" " + str(d_score)) if d_score else ''),
"cfontsize": 0.75, "color": "#94a3b8"}},
]},
{"widgettype": "Text", "options": {"text": d_content[:5000],
"cfontsize": 0.78, "color": "#475569", "wrap": True, "halign": "left"}},
],
}
if d_file:
card["subwidgets"].append({"widgettype": "Text",
"options": {"text": "\U0001f4ce " + d_file, "cfontsize": 0.75,
"color": "#64748b", "wrap": True, "halign": "left"}})
if d_comment:
card["subwidgets"].append({"widgettype": "Text",
"options": {"text": "审核意见: " + d_comment[:300],
"cfontsize": 0.75, "color": "#f0a040", "wrap": True, "halign": "left"}})
subwidgets.append(card)
return json.dumps({
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "12px",
"gap": "6px", "overflowY": "auto"},
"subwidgets": subwidgets,
}, ensure_ascii=False)

View File

@ -0,0 +1,186 @@
# 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)

View File

@ -0,0 +1,32 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "0"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "左树:项目 → 阶段 → 任务 → 步骤/交付件;点击任务节点查看右侧输入输出。",
"cfontsize": 0.8, "color": "#94a3b8", "padding": "4px 12px"}
},
{
"widgettype": "ResourceBrowser",
"id": "bid_task_rb",
"options": {
"css": "filler",
"tree_options": {
"widgettype": "Tree",
"options": {
"dataurl": "{{entire_url('/pipeline-bidding/api/bid_task_tree.dspy')}}",
"textField": "label",
"idField": "id",
"cfontsize": 0.95
}
},
"browser_options": {
"widgettype": "urlwidget",
"options": {"url": "{{entire_url('/pipeline-bidding/api/bid_task_io.dspy')}}"}
},
"tree_width": "42%"
}
}
]
}