333 lines
18 KiB
Plaintext
333 lines
18 KiB
Plaintext
# 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',
|
||
'qc_rejected': '#dc2626',
|
||
'verify_failed': '#f0a040',
|
||
}
|
||
|
||
if not raw_id:
|
||
return json.dumps({
|
||
"widgettype": "Text",
|
||
"options": {"otext": "请选择左侧任务查看输入输出", "text": "请选择左侧任务查看输入输出", "i18n": True, "cfontsize": 0.9,
|
||
"color": "#94a3b8", "padding": "16px"},
|
||
}, ensure_ascii=False)
|
||
|
||
async with DBPools().sqlorContext(dbname) as sor:
|
||
# ── 步骤/交付件/QC审核轮次节点:先按自身查,查到就展示;查不到回落任务视图 ──
|
||
task_id = raw_id
|
||
step_focus = ''
|
||
del_focus = ''
|
||
if raw_id.startswith('rev:'):
|
||
rid = raw_id[4:]
|
||
recs = await sor.sqlExe(
|
||
"SELECT id, task_id, qc_type, round, reviewer, fit_score, pass_score, passed, "
|
||
"improvement, comments, created_at FROM bid_qc_reviews WHERE id=${r}$", {"r": rid})
|
||
if recs:
|
||
rv = recs[0]
|
||
passed = str(getattr(rv, 'passed', '0')) == '1'
|
||
sc = float(getattr(rv, 'fit_score', 0) or 0)
|
||
ps = float(getattr(rv, 'pass_score', 0) or 0)
|
||
task_id = getattr(rv, 'task_id', '') or ''
|
||
subwidgets = [
|
||
{"widgettype": "Text",
|
||
"options": {"otext": "第${p0}轮 ${p1} QC 契合度审核", "text": "第${p0}轮 ${p1} QC 契合度审核", "i18n": True, "i18n_params": {"p0": str(getattr(rv, 'round', '')), "p1": str(getattr(rv, 'qc_type', '') or '')},
|
||
"halign": "left", "cfontsize": 1.1,
|
||
"style": {"fontWeight": "bold", "color": "var(--ink-1)"}}},
|
||
{"widgettype": "HBox", "options": {"gap": "12px", "width": "100%"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text", "options": {
|
||
"otext": ("${p0} / ${p1} → 通过" if passed else "${p0} / ${p1} → 未通过"),
|
||
"text": ("${p0} / ${p1} → 通过" if passed else "${p0} / ${p1} → 未通过"),
|
||
"i18n": True, "i18n_params": {"p0": "%.1f" % sc, "p1": "%.1f" % ps},
|
||
"cfontsize": 0.85, "color": "#ffffff", "padding": "1px 10px",
|
||
"style": {"background": '#16a34a' if passed else '#dc2626',
|
||
"borderRadius": "12px", "fontWeight": "bold"}}},
|
||
{"widgettype": "Text", "options": {
|
||
"text": "审核: " + (getattr(rv, 'reviewer', '') or ''),
|
||
"cfontsize": 0.75, "color": "#94a3b8"}},
|
||
{"widgettype": "Text", "options": {
|
||
"text": str(getattr(rv, 'created_at', '') or '')[:16],
|
||
"cfontsize": 0.75, "color": "#94a3b8"}},
|
||
{"widgettype": "Filler"},
|
||
]},
|
||
{"widgettype": "Text",
|
||
"options": {"otext": "\u2500\u2500 审核说明 \u2500\u2500", "text": "\u2500\u2500 审核说明 \u2500\u2500", "i18n": True, "cfontsize": 0.85,
|
||
"style": {"fontWeight": "bold", "color": "#8b5cf6", "marginTop": "8px"}}},
|
||
{"widgettype": "VScrollPanel",
|
||
"options": {"css": "filler", "width": "100%", "minHeight": "100px",
|
||
"maxHeight": "360px", "padding": "6px 12px", "bgcolor": "#ffffff",
|
||
"border": "1px solid #e2e8f0", "borderRadius": "6px"},
|
||
"subwidgets": [{"widgettype": "MdWidget",
|
||
"options": {"mdtext": getattr(rv, 'comments', '') or '(无)',
|
||
"width": "100%"}}]},
|
||
]
|
||
imp = getattr(rv, 'improvement', '') or ''
|
||
if imp:
|
||
subwidgets.append({"widgettype": "Text",
|
||
"options": {"otext": "\u2500\u2500 改进意见 \u2500\u2500", "text": "\u2500\u2500 改进意见 \u2500\u2500", "i18n": True, "cfontsize": 0.85,
|
||
"style": {"fontWeight": "bold", "color": "#b45309", "marginTop": "8px"}}})
|
||
subwidgets.append({"widgettype": "VScrollPanel",
|
||
"options": {"css": "filler", "width": "100%", "minHeight": "100px",
|
||
"maxHeight": "360px", "padding": "6px 12px", "bgcolor": "#ffffff",
|
||
"border": "1px solid #e2e8f0", "borderRadius": "6px"},
|
||
"subwidgets": [{"widgettype": "MdWidget",
|
||
"options": {"mdtext": imp, "width": "100%"}}]})
|
||
subwidgets.append({"widgettype": "Text",
|
||
"options": {"otext": "(以下为该轮审核所属任务详情)", "text": "(以下为该轮审核所属任务详情)", "i18n": True, "cfontsize": 0.75,
|
||
"color": "#94a3b8", "marginTop": "8px"}})
|
||
return json.dumps({
|
||
"widgettype": "VBox",
|
||
"options": {"width": "100%", "padding": "12px", "gap": "6px", "overflowY": "auto"},
|
||
"subwidgets": subwidgets,
|
||
}, ensure_ascii=False)
|
||
task_id = raw_id
|
||
elif 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": "var(--ink-1)"}}},
|
||
{"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": {
|
||
"otext": "开始: ${p0}", "text": "开始: ${p0}", "i18n": True, "i18n_params": {"p0": str(str(getattr(s, 'started_at', '') or '')[:16])},
|
||
"cfontsize": 0.75, "color": "#94a3b8"}},
|
||
{"widgettype": "Text", "options": {
|
||
"otext": "结束: ${p0}", "text": "结束: ${p0}", "i18n": True, "i18n_params": {"p0": str(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": {"otext": "(以下为所属任务详情)", "text": "(以下为所属任务详情)", "i18n": True, "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, project_id, "
|
||
"review_status, review_comment, quality_score FROM pipeline_deliverables WHERE id=${d}$",
|
||
{"d": did})
|
||
if recs:
|
||
d = recs[0]
|
||
from pipeline_service.task_io_render import deliverable_widgets
|
||
subwidgets = [{"widgettype": "Text",
|
||
"options": {"text": "\U0001f4e4 " + (getattr(d, 'title', '') or getattr(d, 'deliverable_type', '') or ''),
|
||
"halign": "left", "cfontsize": 1.1,
|
||
"style": {"fontWeight": "bold", "color": "var(--ink-1)"}}}]
|
||
subwidgets += deliverable_widgets({
|
||
"deliverable_type": getattr(d, 'deliverable_type', '') or '',
|
||
"title": getattr(d, 'title', '') or '',
|
||
"content": getattr(d, 'content', '') or '',
|
||
"file_path": getattr(d, 'file_path', '') or '',
|
||
"project_id": getattr(d, 'project_id', '') or '',
|
||
"review_status": getattr(d, 'review_status', '') or '',
|
||
"quality_score": getattr(d, 'quality_score', '') or '',
|
||
"review_comment": getattr(d, 'review_comment', '') or '',
|
||
}, entire_url("/pipeline-sdlc/api/project_file.dspy"))
|
||
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": {"otext": "任务不存在", "text": "任务不存在", "i18n": True, "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
|
||
|
||
# 输出:交付件(md→MdWidget渲染+下载;office/pdf→下载链接;全产线统一)
|
||
dels = await sor.sqlExe(
|
||
"SELECT deliverable_type, title, content, file_path, project_id, review_status, "
|
||
"quality_score, review_comment "
|
||
"FROM pipeline_deliverables WHERE task_id=${t}$ ORDER BY created_at DESC", {"t": task_id})
|
||
|
||
proj_file_url = entire_url("/pipeline-sdlc/api/project_file.dspy")
|
||
|
||
# 执行步骤
|
||
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": "var(--ink-1)"}},
|
||
})
|
||
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": {"otext": "角色: ${p0}", "text": "角色: ${p0}", "i18n": True, "i18n_params": {"p0": str(role)},
|
||
"cfontsize": 0.8, "color": "#64748b"}},
|
||
]
|
||
if stage:
|
||
info_row.append({"widgettype": "Text", "options": {"otext": "阶段: ${p0}${p1}", "text": "阶段: ${p0}${p1}", "i18n": True, "i18n_params": {"p0": str(stage), "p1": str((" / " + dim) if dim else "")},
|
||
"cfontsize": 0.75, "color": "#94a3b8"}})
|
||
info_row.append({"widgettype": "Text", "options": {"otext": "创建: ${p0} | 更新: ${p1}", "text": "创建: ${p0} | 更新: ${p1}", "i18n": True, "i18n_params": {"p0": str(created_at), "p1": str(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": {"otext": "\u2500\u2500 输入 \u2500\u2500", "text": "\u2500\u2500 输入 \u2500\u2500", "i18n": True, "cfontsize": 0.85,
|
||
"style": {"fontWeight": "bold", "color": "#3b82f6", "marginTop": "8px"}},
|
||
})
|
||
if description:
|
||
subwidgets.append({
|
||
"widgettype": "Text",
|
||
"options": {"text": description[:4000], "cfontsize": 0.85,
|
||
"color": "var(--ink-2)", "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": {"otext": "(无任务描述)", "text": "(无任务描述)", "i18n": True, "cfontsize": 0.8, "color": "#94a3b8"}})
|
||
|
||
# 执行步骤
|
||
if steps:
|
||
subwidgets.append({
|
||
"widgettype": "Text",
|
||
"options": {"otext": "\u2500\u2500 执行步骤 \u2500\u2500", "text": "\u2500\u2500 执行步骤 \u2500\u2500", "i18n": True, "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": "var(--ink-1)"}},
|
||
{"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": {"otext": "\u2500\u2500 输出(交付件)\u2500\u2500", "text": "\u2500\u2500 输出(交付件)\u2500\u2500", "i18n": True, "cfontsize": 0.85,
|
||
"style": {"fontWeight": "bold", "color": "#16a34a", "marginTop": "8px"}},
|
||
})
|
||
if not dels:
|
||
subwidgets.append({"widgettype": "Text",
|
||
"options": {"otext": "(暂无交付件)", "text": "(暂无交付件)", "i18n": True, "cfontsize": 0.8, "color": "#94a3b8"}})
|
||
else:
|
||
from pipeline_service.task_io_render import deliverable_card
|
||
for d in dels:
|
||
subwidgets.append(deliverable_card({
|
||
"deliverable_type": getattr(d, 'deliverable_type', '') or '',
|
||
"title": getattr(d, 'title', '') or '',
|
||
"content": getattr(d, 'content', '') or '',
|
||
"file_path": getattr(d, 'file_path', '') or '',
|
||
"project_id": getattr(d, 'project_id', '') or '',
|
||
"review_status": getattr(d, 'review_status', '') or '',
|
||
"quality_score": getattr(d, 'quality_score', '') or '',
|
||
"review_comment": getattr(d, 'review_comment', '') or '',
|
||
}, proj_file_url))
|
||
|
||
return json.dumps({
|
||
"widgettype": "VBox",
|
||
"options": {"width": "100%", "height": "100%", "padding": "12px",
|
||
"gap": "6px", "overflowY": "auto"},
|
||
"subwidgets": subwidgets,
|
||
}, ensure_ascii=False)
|