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