153 lines
6.1 KiB
Python
Raw Permalink 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.

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