pipeline-sdlc/wwwroot/api/todo_detail.dspy

379 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.

# todo_detail.dspy - 待办详情:把要人类看的正文用 MdWidget 渲染(VScrollPanel 可滚动),
# 操作按钮与正文一起给出(先看内容再确认,不再只给两个按钮)。
# 入参:kind=human_task|question, id=<待办id>
import json as _json
user_id = await get_user()
if not user_id:
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录后查看待办"}}
kind = ((params_kw or {}).get('kind') or 'human_task').strip()
oid = ((params_kw or {}).get('id') or '').strip()
if not oid:
return {"widgettype": "Message", "options": {"title": "打开失败", "message": "缺少待办 id"}}
dbname = get_module_dbname('pipeline-sdlc')
TYPE_LABEL = {
'requirement_confirmation': '需求确认',
'design_confirmation': '设计确认',
'bug_acceptance': 'Bug 验收',
}
TYPE_COLOR = {
'requirement_confirmation': '#2563eb',
'design_confirmation': '#2563eb',
'bug_acceptance': '#f0a040',
}
def _s(v):
if v is None:
return ''
return str(v)
confirm_url = entire_url("/pipeline-sdlc/api/stage_confirm.dspy")
bug_url = entire_url("/pipeline-sdlc/api/bug_accept.dspy")
complete_url = entire_url("/pipeline-sdlc/api/human_task_complete.dspy")
answer_url = entire_url("/pipeline-sdlc/api/question_answer.dspy")
# 处理成功:关详情弹窗 + 关列表弹窗 + 刷新角标
_tail = ("if(d.success){"
"var pw=bricks.getWidgetById('todo_detail_pw',bricks.app);if(pw){pw.destroy();}"
"var lw=bricks.getWidgetById('my_todos_pw',bricks.app);if(lw){lw.destroy();}"
"if(window.refreshTodo){window.refreshTodo();}"
"var mo=new bricks.Message({title:'已处理',message:d.message||'处理成功'});mo.open();"
"}else{var mf=new bricks.Message({title:'处理失败',message:d.error||'处理失败'});mf.open();}")
def _read_input_js(wid):
return ("var cw=bricks.getWidgetById(" + _json.dumps(wid) + ",bricks.app);var cv='';"
"if(cw){cv=(typeof cw.resultValue==='function')?cw.resultValue():"
"((cw.dom_element&&cw.dom_element.value)||'');}"
"cv=(cv===null||cv===undefined)?'':String(cv).trim();")
def _post_js(url, fields):
js = "var body=new URLSearchParams();"
for k, v in fields:
js += "body.append(" + _json.dumps(k) + "," + v + ");"
js += ("var rp=await fetch(" + _json.dumps(url) + ",{method:'POST',"
"headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body});"
"var d=await rp.json();")
return js
def _input_box(wid, label, tip):
return {
"widgettype": "VBox",
"options": {"width": "100%", "gap": "4px", "padding": "8px 14px 0 14px"},
"subwidgets": [
{"widgettype": "Text",
"options": {"text": label, "cfontsize": 0.75, "color": "#64748b"}},
{"widgettype": "UiText", "id": wid,
"options": {"name": wid, "width": "100%", "cfontsize": 0.85,
"placeholder": tip, "height": "70px"}}
]
}
def _btn(label, css, script, conform=None):
bind = {"wid": "self", "event": "click", "actiontype": "script",
"target": "self", "script": script}
if conform:
bind["conform"] = conform
return {"widgettype": "Button", "options": {"label": label, "css": css}, "binds": [bind]}
title = ''
badge = '待办'
badge_color = '#64748b'
sub_lines = []
md = []
action_widgets = []
async with DBPools().sqlorContext(dbname) as sor:
project_id = ''
if kind == 'question':
qrecs = await sor.sqlExe(
"SELECT id, tenant_id, task_id, from_role, question, problem_type, "
"current_handler_role, created_at FROM pipeline_agent_questions "
"WHERE id=${i}$", {"i": oid})
await sor.sqlExe("COMMIT", {})
if not qrecs:
return {"widgettype": "Message", "options": {"title": "打开失败", "message": "问题不存在或已处理"}}
q = qrecs[0]
project_id = _s(getattr(q, 'tenant_id', ''))
badge = '待答问题'
badge_color = '#8b5cf6'
qtext = _s(getattr(q, 'question', ''))
title = qtext[:60] if qtext else '冒泡问题'
sub_lines.append('提问角色:' + (_s(getattr(q, 'from_role', '')) or '-'))
sub_lines.append('问题类型:' + (_s(getattr(q, 'problem_type', '')) or '-'))
sub_lines.append('提问时间:' + _s(getattr(q, 'created_at', '')))
md.append('## 问题内容')
md.append('')
md.append(qtext or '(无正文)')
md.append('')
rel_task = _s(getattr(q, 'task_id', ''))
if rel_task:
trecs = await sor.sqlExe("SELECT title, state FROM pipeline_tasks WHERE id=${t}$",
{"t": rel_task})
await sor.sqlExe("COMMIT", {})
if trecs:
md.append('## 关联任务')
md.append('')
md.append('- 任务:' + _s(getattr(trecs[0], 'title', '')))
md.append('- 状态:' + _s(getattr(trecs[0], 'state', '')))
md.append('')
action_widgets.append(_input_box('todo_answer', '你的回答(回答后该问题关闭,任务自动恢复执行)',
'请填写回答内容'))
answer_script = (_read_input_js('todo_answer') +
"if(!cv){var mn=new bricks.Message({title:'请填写回答',"
"message:'回答内容不能为空'});mn.open();return;}" +
_post_js(answer_url, [('question_id', _json.dumps(oid)), ('answer', 'cv')]) +
_tail)
action_widgets.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "10px 14px", "halign": "right"},
"subwidgets": [_btn('提交回答', 'primary', answer_script)]
})
else:
hrecs = await sor.sqlExe(
"SELECT id, project_id, iteration_id, bug_id, task_id, task_type, title, "
"description, status, created_at, submitted_by FROM pipeline_human_tasks "
"WHERE id=${i}$", {"i": oid})
await sor.sqlExe("COMMIT", {})
if not hrecs:
return {"widgettype": "Message", "options": {"title": "打开失败", "message": "待办不存在或已处理"}}
h = hrecs[0]
task_type = _s(getattr(h, 'task_type', ''))
project_id = _s(getattr(h, 'project_id', ''))
title = _s(getattr(h, 'title', '')) or '待办'
badge = TYPE_LABEL.get(task_type, '人类任务')
badge_color = TYPE_COLOR.get(task_type, '#2563eb')
sub_lines.append('创建时间:' + _s(getattr(h, 'created_at', '')))
if _s(getattr(h, 'status', '')) != 'pending':
sub_lines.append('状态:' + _s(getattr(h, 'status', '')))
desc = _s(getattr(h, 'description', ''))
if desc:
md.append('## 待办说明')
md.append('')
md.append(desc)
md.append('')
src_task_id = _s(getattr(h, 'task_id', ''))
bug_id = _s(getattr(h, 'bug_id', ''))
if task_type in ('requirement_confirmation', 'design_confirmation'):
drecs = []
pm_comment = ''
if src_task_id:
trecs = await sor.sqlExe(
"SELECT title, state FROM pipeline_tasks WHERE id=${t}$", {"t": src_task_id})
await sor.sqlExe("COMMIT", {})
if trecs:
sub_lines.append('原任务:' + _s(getattr(trecs[0], 'title', '')))
# 角色 agent 产出的交付件(排除 pm_review —— 那是审核记录不是待确认内容)
drecs = await sor.sqlExe(
"SELECT title, deliverable_type, content, file_path, review_status, "
"review_comment, created_by, created_at FROM pipeline_deliverables "
"WHERE task_id=${t}$ AND deliverable_type<>'pm_review' "
"ORDER BY created_at DESC LIMIT 3", {"t": src_task_id})
await sor.sqlExe("COMMIT", {})
# PM 审核意见单独取(content 是 JSON,取其中 comment)
prrecs = await sor.sqlExe(
"SELECT content, review_comment, created_at FROM pipeline_deliverables "
"WHERE task_id=${t}$ AND deliverable_type='pm_review' "
"ORDER BY created_at DESC LIMIT 1", {"t": src_task_id})
await sor.sqlExe("COMMIT", {})
if prrecs:
pm_comment = _s(getattr(prrecs[0], 'review_comment', ''))
raw = _s(getattr(prrecs[0], 'content', ''))
if raw:
try:
_pj = _json.loads(raw)
pm_comment = _s(_pj.get('comment')) or pm_comment
except Exception:
pm_comment = pm_comment or raw
if pm_comment:
md.append('## PM 审核意见')
md.append('')
md.append(pm_comment)
md.append('')
if drecs:
for _i in range(len(drecs)):
dx = drecs[_i]
_head = '## 待确认交付件' if _i == 0 else '## 交付件(更早版本)'
md.append(_head + ':' + (_s(getattr(dx, 'title', '')) or '(未命名)'))
md.append('')
md.append('- 类型:' + (_s(getattr(dx, 'deliverable_type', '')) or '-')
+ ' | 产出:' + (_s(getattr(dx, 'created_by', '')) or '-')
+ ' | 时间:' + _s(getattr(dx, 'created_at', ''))
+ ' | 评审:' + (_s(getattr(dx, 'review_status', '')) or '-'))
_fp = _s(getattr(dx, 'file_path', ''))
if _fp:
md.append('- 文件:`' + _fp + '`')
md.append('')
md.append('---')
md.append('')
_content = _s(getattr(dx, 'content', ''))
if len(_content) > 20000:
_content = _content[:20000] + '\n\n…(内容过长已截断,完整版见上面的文件路径)'
md.append(_content or '(交付件正文为空,请到项目工作空间 deliverables 目录查看文件)')
md.append('')
if _i == 0 and len(drecs) > 1:
md.append('---')
md.append('')
else:
md.append('## 待确认内容')
md.append('')
md.append('未找到该任务的交付件记录,请到项目工作空间 deliverables 目录查看产出文件。')
md.append('')
action_widgets.append(_input_box(
'todo_comment', '修改意见(确认通过可留空;退回重做必须填写)', '请填写修改意见'))
ok_script = (_read_input_js('todo_comment') +
_post_js(confirm_url, [('human_task_id', _json.dumps(oid)),
('confirmed', "'1'"), ('comment', 'cv')]) +
_tail)
no_script = (_read_input_js('todo_comment') +
"if(!cv){var mn=new bricks.Message({title:'请填写修改意见',"
"message:'退回重做前请说明需要修改什么'});mn.open();return;}" +
_post_js(confirm_url, [('human_task_id', _json.dumps(oid)),
('confirmed', "'0'"), ('comment', 'cv')]) +
_tail)
action_widgets.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "10px 14px", "halign": "right"},
"subwidgets": [
_btn('确认通过', 'primary', ok_script,
conform={"title": "确认通过", "message": "确认该阶段成果满足要求,继续后续任务?",
"conform": {"label": "确认通过"}, "discard": {"label": "再看看"}}),
_btn('不确定,退回重做', 'small', no_script)
]
})
elif task_type == 'bug_acceptance':
if bug_id:
brecs = await sor.sqlExe(
"SELECT title, description, severity, priority, status, fix_description, "
"fix_commit, assignee_id, created_at FROM sd_bugs WHERE id=${b}$", {"b": bug_id})
await sor.sqlExe("COMMIT", {})
else:
brecs = []
if brecs:
b = brecs[0]
md.append('## Bug:' + (_s(getattr(b, 'title', '')) or '(无标题)'))
md.append('')
md.append('- 严重程度:' + (_s(getattr(b, 'severity', '')) or '-')
+ ' | 优先级:' + (_s(getattr(b, 'priority', '')) or '-')
+ ' | 状态:' + (_s(getattr(b, 'status', '')) or '-'))
md.append('- 处理人:' + (_s(getattr(b, 'assignee_id', '')) or '-')
+ ' | 提交时间:' + _s(getattr(b, 'created_at', '')))
md.append('')
md.append('### 问题描述')
md.append('')
md.append(_s(getattr(b, 'description', '')) or '(无)')
md.append('')
md.append('### 修复说明')
md.append('')
md.append(_s(getattr(b, 'fix_description', '')) or '(开发未填写修复说明)')
fc = _s(getattr(b, 'fix_commit', ''))
if fc:
md.append('')
md.append('- 修复提交:`' + fc + '`')
md.append('')
else:
md.append('## Bug 信息')
md.append('')
md.append('未找到关联 Bug 记录(bug_id=' + (bug_id or '空') + ')。')
md.append('')
ok_script = (_post_js(bug_url, [('bug_id', _json.dumps(bug_id)), ('accept', "'1'")]) + _tail)
no_script = (_post_js(bug_url, [('bug_id', _json.dumps(bug_id)), ('accept', "'0'")]) + _tail)
action_widgets.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "12px 14px", "halign": "right"},
"subwidgets": [
_btn('验收通过(关闭 Bug)', 'primary', ok_script,
conform={"title": "验收通过", "message": "确认该 Bug 已修复并关闭?",
"conform": {"label": "验收通过"}, "discard": {"label": "取消"}}),
_btn('不通过(重新打开)', 'small', no_script)
]
})
else:
if not desc:
md.append('## 待办说明')
md.append('')
md.append('(无说明)')
md.append('')
action_widgets.append(_input_box('todo_result', '处理结果(提交后进入 QC 检查)', '请填写处理结果'))
do_script = (_read_input_js('todo_result') +
"if(!cv){var mn=new bricks.Message({title:'请填写处理结果',"
"message:'处理结果不能为空'});mn.open();return;}"
"var rd=JSON.stringify({content:cv});" +
_post_js(complete_url, [('human_task_id', _json.dumps(oid)),
('result_data', 'rd')]) +
_tail)
action_widgets.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "10px 14px", "halign": "right"},
"subwidgets": [_btn('提交处理结果', 'primary', do_script)]
})
# 项目名
if project_id:
precs = await sor.sqlExe("SELECT name FROM sd_projects WHERE id=${p}$", {"p": project_id})
await sor.sqlExe("COMMIT", {})
if precs:
sub_lines.insert(0, '项目:' + (_s(getattr(precs[0], 'name', '')) or project_id))
head_widgets = [{
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "alignItems": "center", "padding": "12px 14px 4px 14px"},
"subwidgets": [
{"widgettype": "Text",
"options": {"text": badge, "cfontsize": 0.75, "color": "#ffffff", "padding": "2px 10px",
"bgcolor": badge_color, "borderRadius": "10px", "whiteSpace": "nowrap"}},
{"widgettype": "Text",
"options": {"text": title, "cfontsize": 1.05, "color": "#0f172a"}}
]
}]
if sub_lines:
head_widgets.append({
"widgettype": "Text",
"options": {"text": ' | '.join(sub_lines), "cfontsize": 0.75, "color": "#94a3b8",
"padding": "0 14px 6px 14px", "width": "100%"}
})
return {
"widgettype": "PopupWindow",
"id": "todo_detail_pw",
"options": {"title": badge + " - " + title[:40], "width": "86%", "height": "86%",
"auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%", "gap": "0px"},
"subwidgets": head_widgets + [
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "62vh", "minHeight": "220px",
"bgcolor": "#ffffff", "border": "1px solid #e2e8f0",
"borderRadius": "6px", "overflow": "hidden",
"margin": "0 14px"},
"subwidgets": [{
"widgettype": "VScrollPanel",
"options": {"css": "filler", "width": "100%", "padding": "6px 12px"},
"subwidgets": [{
"widgettype": "MdWidget",
"options": {"mdtext": '\n'.join(md), "width": "100%"}
}]
}]
}
] + action_widgets
}]
}