pipeline-sdlc/wwwroot/api/todo_detail.dspy

685 lines
35 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
import os
from urllib.parse import quote as _quote
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 验收',
'kb_ingest_confirm': '知识库入库建议',
}
TYPE_COLOR = {
'requirement_confirmation': '#2563eb',
'design_confirmation': '#2563eb',
'bug_acceptance': '#f0a040',
'kb_ingest_confirm': '#10b981',
}
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]}
def _file_link_row(fname, project_id, fpath):
"""待办中的输出文件行:文件名 + 打开/下载按钮(链接到项目空间内的真实文件)。"""
open_url = (entire_url("/pipeline-sdlc/api/project_file.dspy")
+ "?project_id=" + _quote(project_id) + "&path=" + _quote(fpath))
dl_url = open_url + "&download=1"
return {"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "6px 14px",
"alignItems": "center", "bgcolor": "#f8fafc",
"border": "1px solid #e2e8f0", "borderRadius": "6px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📎 " + fname,
"cfontsize": 0.85, "color": "#334155", "flex": "1 1 auto",
"overflow": "hidden", "textOverflow": "ellipsis", "whiteSpace": "nowrap"}},
_btn("打开", "small", "window.open(" + _json.dumps(open_url) + ",'_blank');"),
_btn("下载", "small", "window.open(" + _json.dumps(dl_url) + ",'_blank');")
]}
def _build_form_action(schema_fields, kind, oid):
"""按 form_schema 生成动态表单操作区(问题通道与人类任务通道通用)。
file 字段 → UiFile 上传框(FileReader 转 base64);text/textarea → 输入框。
提交 → todo_form_submit.dspy(kind 区分通道),落项目根 + 按 target 移动 + 回流。
"""
form_submit_url = entire_url("/pipeline-sdlc/api/todo_form_submit.dspy")
_field_widgets = []
_script_parts = ["var _f={};"]
for _fi in schema_fields:
_fn = _s(_fi.get('name'))
_ft = (_s(_fi.get('type')) or 'text').lower()
_fl = _s(_fi.get('label')) or _fn
_freq = bool(_fi.get('required'))
_wid = 'tf_' + _fn
if _ft == 'file':
_acc = _s(_fi.get('accept'))
_fops = {"name": _wid, "width": "100%", "preview": True}
if _acc:
_fops["accept"] = _acc
_field_widgets.append({
"widgettype": "VBox",
"options": {"width": "100%", "gap": "4px", "padding": "8px 14px 0 14px"},
"subwidgets": [
{"widgettype": "Text",
"options": {"text": _fl + ('(必填)' if _freq else ''),
"cfontsize": 0.75, "color": "#64748b"}},
{"widgettype": "UiFile", "id": _wid, "options": _fops}
]
})
_script_parts.append(
"var _w" + _fn + "=bricks.getWidgetById(" + _json.dumps(_wid) + ",bricks.app);"
"var _v" + _fn + "=(_w" + _fn + "&&typeof _w" + _fn + ".resultValue==='function')?"
"_w" + _fn + ".resultValue():null;"
"if(!_v" + _fn + "){" +
("var _m" + _fn + "=new bricks.Message({title:'请上传文件',"
"message:" + _json.dumps(_fl + " 是必填项,请上传文件后再提交") + "});_m" +
_fn + ".open();return;}" if _freq else "") +
"if(_v" + _fn + "){"
"if(_v" + _fn + ".size>10*1024*1024){var _ms" + _fn +
"=new bricks.Message({title:'文件过大',message:'单个文件不能超过 10MB'});_ms" +
_fn + ".open();return;}"
"_f[" + _json.dumps(_fn + '_filename') + "]=_v" + _fn + ".name;"
"_f[" + _json.dumps(_fn) + "]=await new Promise(function(_res,_rej){"
"var _rd=new FileReader();"
"_rd.onload=function(){_res(_rd.result);};_rd.onerror=_rej;"
"_rd.readAsDataURL(_v" + _fn + ");});}")
else:
_field_widgets.append(_input_box(_wid, _fl + ('(必填)' if _freq else ''),
'请填写' + _fl))
_script_parts.append(
"var _tw" + _fn + "=bricks.getWidgetById(" + _json.dumps(_wid) + ",bricks.app);"
"var _tv" + _fn + "=(_tw" + _fn + "&&(typeof _tw" + _fn +
".resultValue==='function'))?_tw" + _fn + ".resultValue():"
"((_tw" + _fn + "&&_tw" + _fn + ".dom_element&&_tw" + _fn +
".dom_element.value)||'');"
"_tv" + _fn + "=(_tv" + _fn + "===null||_tv" + _fn + "===undefined)?'':"
"String(_tv" + _fn + ").trim();" +
("if(!_tv" + _fn + "){var _mt" + _fn + "=new bricks.Message({title:'请填写',"
"message:" + _json.dumps(_fl + " 是必填项") + "});_mt" + _fn +
".open();return;}" if _freq else "") +
"_f[" + _json.dumps(_fn) + "]=_tv" + _fn + ";")
_id_param = 'task_id' if kind == 'human_task' else 'question_id'
_script_parts.append(
"var _body=new URLSearchParams();"
"_body.append('kind'," + _json.dumps(kind) + ");"
"_body.append(" + _json.dumps(_id_param) + "," + _json.dumps(oid) + ");"
"_body.append('fields',JSON.stringify(_f));"
"var _rp=await fetch(" + _json.dumps(form_submit_url) +
",{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
"body:_body});var d=await _rp.json();" + _tail)
return [{
"widgettype": "VBox",
"options": {"width": "100%", "gap": "0px"},
"subwidgets": _field_widgets + [{
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "12px 14px", "halign": "right"},
"subwidgets": [_btn('提交(在待办中完成)', 'primary', ''.join(_script_parts))]
}]
}]
title = ''
badge = '待办'
badge_color = '#64748b'
sub_lines = []
md = []
action_widgets = []
_file_rows = [] # 输出文件链接行(打开/下载)——待办中出现的输出文件必须可直达
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, context, 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('')
# ── 动态表单(form_schema 由提问方声明):在待办里直接上传文件/填写信息完成任务 ──
_form_schema = None
try:
_ctx_raw = _s(getattr(q, 'context', ''))
if _ctx_raw:
_form_schema = (_json.loads(_ctx_raw) or {}).get('form_schema')
except Exception:
_form_schema = None
_form_fields = (_form_schema or {}).get('fields') or []
if _form_fields:
action_widgets.extend(_build_form_action(_form_fields, 'question', oid))
else:
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, form_schema, 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', '')))
# 动态表单声明(创建方写入,任意产线通用)
_ht_form_fields = []
try:
_ht_fs_raw = _s(getattr(h, 'form_schema', ''))
if _ht_fs_raw:
_ht_form_fields = (_json.loads(_ht_fs_raw) or {}).get('fields') or []
except Exception:
_ht_form_fields = []
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('- 输出文件:`' + os.path.basename(_fp) + '`(项目空间 deliverables 目录)')
_file_rows.append(_file_link_row(os.path.basename(_fp), project_id, _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 == 'opp_report_confirm':
# 商机产线研发报告确认:正文=报告全文(确认人先看内容再拍板)
opp_confirm_url = entire_url("/pipeline-opportunity/api/opp_confirm_report.dspy")
opp_ppt_url = entire_url("/pipeline-opportunity/api/opp_report_ppt.dspy")
rrecs = await sor.sqlExe(
"SELECT id, software, title, content, status, ppt_path FROM opp_reports "
"WHERE confirm_task_id=${t}$", {"t": oid})
await sor.sqlExe("COMMIT", {})
if rrecs:
r0 = rrecs[0]
rep_id = _s(getattr(r0, 'id', ''))
md.append('## 报告信息')
md.append('')
md.append('- 方向:' + (_s(getattr(r0, 'software', '')) or '-')
+ ' | 状态:' + _s(getattr(r0, 'status', '')))
md.append('- 数据来源:数据爬取平台(每条引用可查证)')
md.append('')
md.append('## 报告全文(请审阅后确认)')
md.append('')
_content = _s(getattr(r0, 'content', ''))
if len(_content) > 40000:
_content = _content[:40000] + '\n\n…(内容过长已截断,完整内容见 PPT 下载)'
md.append(_content or '(报告正文为空)')
md.append('')
# PPT 下载按钮(报告以 PPT 存储,确认人可下载审阅)
_ppt_path = _s(getattr(r0, 'ppt_path', ''))
if _ppt_path:
action_widgets.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "4px 14px", "alignItems": "center"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📑 报告已存为 PPT:",
"cfontsize": 0.8, "color": "#64748b", "halign": "left"}},
{"widgettype": "Button", "options": {"label": "下载 PPT", "css": "small"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
"target": "self",
"script": "window.open(" + _json.dumps(opp_ppt_url + "?report_id=" + rep_id) + ",'_blank');"}]}
]
})
else:
md.append('## 报告信息')
md.append('')
md.append('未找到该确认任务对应的研发报告。')
md.append('')
ok_script = (_post_js(opp_confirm_url, [('human_task_id', _json.dumps(oid)),
('decision', "'approve'")]) + _tail)
no_script = (_read_input_js('opp_confirm_comment') +
_post_js(opp_confirm_url, [('human_task_id', _json.dumps(oid)),
('decision', "'reject'"),
('comment', 'cv')]) + _tail)
action_widgets.append(_input_box('opp_confirm_comment', '退回意见(退回时填写,帮助修改)', '可选:修改意见'))
action_widgets.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "12px 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)
]
})
elif task_type == 'bid_delivery_confirm':
# 投标产线交付确认:展示最新标书文件(打开/下载链接)+ 确认按钮
bdoc = []
if project_id:
brecs = await sor.sqlExe(
"SELECT doc_name, file_path, version, total_score, max_score, status, updated_at "
"FROM bid_documents WHERE project_id=${p}$ AND file_path<>'' "
"ORDER BY version DESC LIMIT 1", {"p": project_id})
await sor.sqlExe("COMMIT", {})
if brecs:
bdoc = brecs
if bdoc:
b0 = bdoc[0]
md.append('## 待确认标书')
md.append('')
md.append('- 名称:' + (_s(getattr(b0, 'doc_name', '')) or '-')
+ ' | 版本:v' + _s(getattr(b0, 'version', ''))
+ ' | 评分:' + _s(getattr(b0, 'total_score', '')) + '/'
+ _s(getattr(b0, 'max_score', ''))
+ ' | 状态:' + (_s(getattr(b0, 'status', '')) or '-'))
md.append('')
md.append('请下载标书后核对用印/密封/份数/递交要求,确认无误后点击确认交付。')
md.append('')
_bf = _s(getattr(b0, 'file_path', ''))
if _bf:
_file_rows.append(_file_link_row(os.path.basename(_bf), project_id, _bf))
else:
md.append('## 待确认标书')
md.append('')
md.append('未找到该项目的标书文件记录。')
md.append('')
elif task_type == 'kb_ingest_confirm':
# PM 建议文件入知识库:展示建议详情 + 文件(打开/下载)+ 批准/驳回
_sg = None
try:
_fsj = _s(getattr(h, 'form_schema', ''))
if _fsj:
_sg = (_json.loads(_fsj) or {}).get('suggestion_id') or ''
except Exception:
_sg = ''
sgrecs = []
if _sg:
sgrecs = await sor.sqlExe(
"SELECT kb_id, file_path, file_name, reason, status FROM "
"pipeline_kb_suggestions WHERE id=${i}$", {"i": _sg})
await sor.sqlExe("COMMIT", {})
if sgrecs:
sg0 = sgrecs[0]
_fname = _s(getattr(sg0, 'file_name', '')) or '-'
_fpath = _s(getattr(sg0, 'file_path', ''))
_reason = _s(getattr(sg0, 'reason', '')) or '(未填写理由)'
_kbid = _s(getattr(sg0, 'kb_id', ''))
_kbname = _kbid
if _kbid:
krecs = await sor.sqlExe(
"SELECT name FROM rag_knowledge_bases WHERE id=${k}$", {"k": _kbid})
await sor.sqlExe("COMMIT", {})
if krecs:
_kbname = _s(getattr(krecs[0], 'name', '')) or _kbid
md.append('## 入库建议')
md.append('')
md.append('- 文件:`' + _fname + '`')
md.append('- 目标知识库:' + _kbname + '(ID: ' + _kbid + ')')
md.append('')
md.append('### PM 给出的理由')
md.append('')
md.append(_reason)
md.append('')
md.append('批准后系统将以项目 owner 身份把该文件上传入库(后台解析,稍后可检索)。')
md.append('')
if _fpath:
_file_rows.append(_file_link_row(_fname, project_id, _fpath))
else:
md.append('## 入库建议')
md.append('')
md.append('未找到对应的建议记录。')
md.append('')
kbi_url = entire_url("/pipeline-sdlc/api/kb_ingest_decide.dspy")
ok_script = (_post_js(kbi_url, [('suggestion_id', _json.dumps(_sg or '')),
('human_task_id', _json.dumps(oid)),
('approve', "'1'")]) + _tail)
no_script = (_read_input_js('kb_ingest_comment') +
_post_js(kbi_url, [('suggestion_id', _json.dumps(_sg or '')),
('human_task_id', _json.dumps(oid)),
('approve', "'0'"), ('comment', 'cv')]) +
_tail)
action_widgets.append(_input_box('kb_ingest_comment', '驳回意见(驳回时填写)', '可选:驳回原因'))
action_widgets.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "12px 14px", "halign": "right"},
"subwidgets": [
_btn('批准入库', 'primary', ok_script,
conform={"title": "批准入库", "message": "确认将该文件上传到目标知识库?",
"conform": {"label": "批准"}, "discard": {"label": "再看看"}}),
_btn('驳回', 'small', no_script)
]
})
else:
if not desc:
md.append('## 待办说明')
md.append('')
md.append('(无说明)')
md.append('')
if _ht_form_fields:
# 创建方声明了动态表单:在待办里直接上传/填写完成任务
action_widgets.extend(_build_form_action(_ht_form_fields, 'human_task', oid))
else:
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%"}
})
# 输出文件区(待办中出现的输出文件必须给打开/下载链接)
_file_section = []
if _file_rows:
_file_section = [{
"widgettype": "VBox",
"options": {"width": "100%", "gap": "6px", "padding": "8px 14px 2px 14px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📎 输出文件", "cfontsize": 0.85,
"fontWeight": "bold", "color": "#334155"}}
] + _file_rows
}]
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%", "flex": "1 1 auto", "minHeight": "180px",
"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%"}
}]
}]
}
] + _file_section + action_widgets
}]
}