pipeline-bidding/wwwroot/api/bid_task_io.dspy

333 lines
18 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_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": "#1e293b"}}},
{"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": "#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": {
"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": "#1e293b"}}}]
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": "#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": {"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": "#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": {"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": "#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": {"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)