From 03473e8fbb97eabf184b1fa9d60979a87fcef353 Mon Sep 17 00:00:00 2001 From: ymq Date: Thu, 3 Sep 2026 16:41:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20task=5Fio=5Frender=20=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E2=80=94=E2=80=94md=E4=BA=A4=E4=BB=98?= =?UTF-8?q?=E4=BB=B6MdWidget=E6=B8=B2=E6=9F=93+=E4=B8=8B=E8=BD=BD=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=EF=BC=8Coffice/pdf=E4=B8=8B=E8=BD=BD=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=EF=BC=88=E5=85=A8=E4=BA=A7=E7=BA=BF=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_service/task_io_render.py | 152 +++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 pipeline_service/task_io_render.py diff --git a/pipeline_service/task_io_render.py b/pipeline_service/task_io_render.py new file mode 100644 index 0000000..79db597 --- /dev/null +++ b/pipeline_service/task_io_render.py @@ -0,0 +1,152 @@ +"""任务输入输出的交付件渲染(全产线通用)。 + +规则(2026-09-03 用户要求,所有产线统一): +- md/txt 文件 → MdWidget 渲染正文 + 提供下载链接; +- office/pdf 文件 → 直接提供文件下载链接; +- 无文件的交付件 → 内联文本卡片(旧行为兜底)。 + +下载/打开走 pipeline-sdlc 的 project_file.dspy(按项目ID+路径,含路径穿越/机构隔离校验), +调用方把 entire_url(project_file.dspy) 的结果传入即可跨模块复用。 +""" +import json +import os +from urllib.parse import quote + +MD_EXTS = ('.md', '.txt', '.markdown') +OFFICE_EXTS = ('.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt', '.pdf', + '.odt', '.ods', '.odp') + +# MdWidget 内联渲染上限:超长 md 改走 md_url 拉文件渲染,避免 widget JSON 过大 +_MD_MAX_INLINE = 60000 + + +def classify_file(path): + """返回 'md' / 'office' / 'other' / ''(无路径)。""" + ext = os.path.splitext(path or '')[1].lower() + if ext in MD_EXTS: + return 'md' + if ext in OFFICE_EXTS: + return 'office' + if path: + return 'other' + return '' + + +def _btn(label, css, script): + return {"widgettype": "Button", "options": {"label": label, "css": css}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", + "target": "self", "script": script}]} + + +def _file_row(fname, dl_url, hint=""): + """文件行:📎 文件名 + (提示) + 下载按钮。""" + subs = [ + {"widgettype": "Text", "options": {"text": "\U0001f4ce " + fname, + "cfontsize": 0.85, "color": "#334155", "flex": "1 1 auto", + "overflow": "hidden", "textOverflow": "ellipsis", + "whiteSpace": "nowrap"}}, + ] + if hint: + subs.append({"widgettype": "Text", "options": {"text": hint, + "cfontsize": 0.72, "color": "#94a3b8"}}) + subs.append(_btn("下载", "small", + "window.open(" + json.dumps(dl_url) + ",'_blank');")) + return {"widgettype": "HBox", + "options": {"width": "100%", "gap": "8px", "padding": "6px 10px", + "alignItems": "center", "bgcolor": "#f8fafc", + "border": "1px solid #e2e8f0", "borderRadius": "6px"}, + "subwidgets": subs} + + +def _md_block(content, md_url): + """可滚动 MdWidget:有内联正文用 mdtext,否则 md_url 拉取。""" + opts = {"width": "100%"} + if content and len(content) <= _MD_MAX_INLINE: + opts["mdtext"] = content + elif md_url: + opts["md_url"] = md_url + else: + opts["mdtext"] = content or "(空文档)" + return {"widgettype": "VScrollPanel", + "options": {"css": "filler", "width": "100%", "height": "100%", + "minHeight": "120px", "maxHeight": "480px", + "padding": "6px 12px", "bgcolor": "#ffffff", + "border": "1px solid #e2e8f0", "borderRadius": "6px"}, + "subwidgets": [{"widgettype": "MdWidget", "options": opts}]} + + +def deliverable_widgets(d, proj_file_url): + """单个交付件 → widget 列表(头部行 + 文件区 + 审核意见)。 + + d: dict,键 deliverable_type/title/content/file_path/project_id/ + review_status/quality_score/review_comment。 + proj_file_url: entire_url('/pipeline-sdlc/api/project_file.dspy') 的结果。 + """ + def g(k): + v = d.get(k) + return v if v is not None else '' + + d_type = g('deliverable_type') or '' + d_title = g('title') or '' + content = g('content') or '' + fpath = g('file_path') or '' + pid = g('project_id') or '' + d_status = g('review_status') or '' + d_score = g('quality_score') or '' + d_comment = g('review_comment') or '' + + out = [] + head = [ + {"widgettype": "Text", "options": {"text": d_type, "cfontsize": 0.8, + "color": "#3b82f6"}}, + {"widgettype": "Text", "options": {"text": d_title[:80], "cfontsize": 0.8, + "color": "#1e293b"}}, + {"widgettype": "Filler"}, + ] + if d_status or d_score: + head.append({"widgettype": "Text", "options": { + "text": (d_status + ((" " + str(d_score)) if d_score else '')), + "cfontsize": 0.75, "color": "#94a3b8"}}) + out.append({"widgettype": "HBox", "options": {"gap": "8px", "width": "100%"}, + "subwidgets": head}) + + kind = classify_file(fpath) + if fpath and pid: + fname = os.path.basename(fpath) + open_url = (proj_file_url + "?project_id=" + quote(str(pid)) + + "&path=" + quote(fpath)) + dl_url = open_url + "&download=1" + if kind == 'md': + # md/txt:MdWidget 渲染 + 下载链接 + out.append(_file_row(fname, dl_url, hint="Markdown")) + out.append(_md_block(content, open_url)) + elif kind == 'office': + # office/pdf:仅下载链接 + out.append(_file_row(fname, dl_url, hint="office/pdf 下载查看")) + else: + out.append(_file_row(fname, dl_url, hint="该文件类型需下载后查看")) + if content: + out.append({"widgettype": "Text", "options": { + "text": content[:3000], "cfontsize": 0.78, "color": "#475569", + "wrap": True, "halign": "left"}}) + elif content: + # 无文件:内联文本兜底(旧行为) + out.append({"widgettype": "Text", "options": {"text": content[:5000], + "cfontsize": 0.78, "color": "#475569", "wrap": True, "halign": "left"}}) + else: + out.append({"widgettype": "Text", "options": {"text": "(无交付件内容)", + "cfontsize": 0.78, "color": "#94a3b8"}}) + + if d_comment: + out.append({"widgettype": "Text", "options": { + "text": "审核意见: " + str(d_comment)[:300], "cfontsize": 0.75, + "color": "#f0a040", "wrap": True, "halign": "left"}}) + return out + + +def deliverable_card(d, proj_file_url): + """单个交付件 → 卡片 VBox(供任务详情右侧面板直接追加)。""" + return {"widgettype": "VBox", + "options": {"css": "card", "gap": "4px", "width": "100%", + "padding": "8px"}, + "subwidgets": deliverable_widgets(d, proj_file_url)}