"""任务输入输出的交付件渲染(全产线通用)。 规则(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)}