pipeline-sdlc/wwwroot/api/workspace_view.dspy

156 lines
6.5 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.

# workspace_view.dspy - 查看文件:txt/md/json 用 MdWidget 渲染,其他文档下载查看
import os
import json as _json
from urllib.parse import quote, unquote
def _decode_id(s):
# 前端 id 可能被多层编码(Tree 懒加载一次 + encodeURIComponent 一次),
# 循环解码到无 % 为止,兼容任意编码层数(2026-09-03 中文文件名"文件不存在"根因)
for _ in range(4):
if '%' not in s:
break
s2 = unquote(s)
if s2 == s:
break
s = s2
return s
file_id = _decode_id((params_kw or {}).get('id', '').strip())
uid = await get_user()
if not uid:
uid = 'user-01'
session_id = (params_kw or {}).get('session_id', '') or ''
pipeline_id = (params_kw or {}).get('pipeline_id', '') or ''
dbname = get_module_dbname('pipeline-sdlc')
async with DBPools().sqlorContext(dbname) as sor:
# 产线隔离:跨产线项目视为无项目(与弹窗入口一致,防绕过)
project_dir, _ = await get_project_dir_pl(sor, uid, session_id, pipeline_id)
space_dir, _ = await get_space_dir(sor, uid, session_id)
# 通用会话(pipeline_id=_generic):锁用户专属目录 _general/{uid},
# 不进入任何项目的工作空间(2026-09-08 用户要求)
if pipeline_id == '_generic':
_gd = generic_workspace_dir(uid)
os.makedirs(_gd, exist_ok=True)
project_dir = _gd
space_dir = _gd
if not file_id or file_id == '__root__':
return {"widgettype": "Message", "options": {"title": "查看失败", "message": "未指定文件"}}
full_path = resolve_workspace_path(project_dir, space_dir, file_id)
# 路径穿越校验
real_ws = os.path.realpath(space_dir)
real_full = os.path.realpath(full_path)
if not real_full.startswith(real_ws + os.sep):
return {"widgettype": "Message", "options": {"title": "错误", "message": "非法路径"}}
if not os.path.isfile(full_path):
return {"widgettype": "Message", "options": {"title": "查看失败", "message": "文件不存在: " + file_id}}
name = os.path.basename(full_path)
ext = os.path.splitext(name)[1].lower()
# 二级 URL 必须与本级同参数(session_id + pipeline_id 双透传,2026-09-17 根因同 upload_url):
# 通用助手(pipeline_id=_generic、无 session_id)下漏拼 pipeline_id 会让 workspace_file.dspy
# 回退全局项目指针 → 在错误空间找文件 → MdWidget 显示「文件不存在」JSON。
_fq = []
if session_id:
_fq.append("session_id=" + session_id)
if pipeline_id:
_fq.append("pipeline_id=" + pipeline_id)
_fq_s = ("&" + "&".join(_fq)) if _fq else ""
file_url = entire_url("/pipeline-sdlc/api/workspace_file.dspy") + "?id=" + quote(file_id) + _fq_s
# txt/md/json → VScrollPanel 包裹 MdWidget 渲染(可滚动,长文档不截断;json 直接显示正文不下载)
md_exts = ['.md', '.txt', '.json']
if ext in md_exts:
if ext == '.json':
# json 包代码块渲染(marked 按 pre 显示正文)
try:
with open(full_path, 'r', encoding='utf-8', errors='replace') as fh:
body = fh.read(200000)
except Exception as e:
return {"widgettype": "Message", "options": {"title": "查看失败", "message": str(e)[:200]}}
return {
"widgettype": "PopupWindow",
"options": {"title": name + " (查看)", "width": "85%", "height": "85%", "auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "VScrollPanel",
"options": {"css": "filler", "width": "100%", "height": "100%"},
"subwidgets": [{
"widgettype": "MdWidget",
"options": {"mdtext": "```json\n" + body + "\n```", "width": "100%"}
}]
}]
}
return {
"widgettype": "PopupWindow",
"options": {"title": name + " (查看)", "width": "85%", "height": "85%", "auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "VScrollPanel",
"options": {"css": "filler", "width": "100%", "height": "100%"},
"subwidgets": [{
"widgettype": "MdWidget",
"options": {"md_url": file_url, "width": "100%"}
}]
}]
}
# 媒体文件 → 直接用播放控件打开(2026-09-18 用户要求:查看即播放,不再让下载)
# 控件均已注册进 bricks.Factory(videoplayer.js/audio.js/image.js),声明式 widgetBuild 可解析;
# workspace_file.dspy 已用 FileResponse 提供媒体流(支持 Range 请求,可拖动进度)。
video_exts = ['.mp4', '.m3u8', '.mpd', '.webm', '.mov', '.avi', '.mkv']
audio_exts = ['.mp3', '.wav', '.aac', '.flac', '.m4a', '.ogg']
image_exts = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp', '.ico']
if ext in video_exts:
return {
"widgettype": "PopupWindow",
"options": {"title": name + " (播放)", "width": "70%", "height": "70%", "auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "VideoPlayer",
"options": {"url": file_url, "width": "100%", "height": "100%"}
}]
}
if ext in audio_exts:
return {
"widgettype": "PopupWindow",
"options": {"title": name + " (播放)", "width": "50%", "height": "30%", "auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "AudioPlayer",
"options": {"url": file_url, "width": "100%", "height": "100%"}
}]
}
if ext in image_exts:
return {
"widgettype": "PopupWindow",
"options": {"title": name + " (查看)", "width": "70%", "height": "70%", "auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "Image",
"options": {"url": file_url, "width": "100%", "height": "100%"}
}]
}
# 其他 → 下载查看
download_url = file_url + "&download=1"
download_script = (
"window.open(" + _json.dumps(download_url) + ",'_blank');"
)
return {
"widgettype": "PopupWindow",
"options": {"title": name, "cwidth": 34, "cheight": 12, "auto_open": True, "resizable": True},
"subwidgets": [
{"widgettype": "Text", "options": {"otext": "该文件类型需下载后查看", "text": "该文件类型需下载后查看", "i18n": True, "cfontsize": 1, "padding": "16px", "halign": "middle"}},
{"widgettype": "Button", "options": {"label": "下载文件", "css": "primary"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": download_script}]}
]
}