diff --git a/wwwroot/api/project_file.dspy b/wwwroot/api/project_file.dspy index 913a9c1..653eace 100644 --- a/wwwroot/api/project_file.dspy +++ b/wwwroot/api/project_file.dspy @@ -4,15 +4,32 @@ # 安全:仅登录用户;解析后校验文件真实路径必须落在该项目目录内(防路径穿越/跨项目越权)。 import os -from urllib.parse import quote +from urllib.parse import quote, unquote from aiohttp.web_fileresponse import FileResponse + +def _decode_path(s): + # 前端 bricks.tget 会对已编码的 query 值再 encodeURIComponent 一次(双重编码), + # 服务端框架只解一层 → 残留 %XX 编码态中文 → 项目名/文件名校验失败, + # 误报「非法路径」或「文件不存在」(2026-09-16 投标标书 md 在线查看实测根因; + # 下载按钮走 window.open 单层编码不受影响,故只有「在线查看」中招)。 + # 循环 unquote 到无 % 为止,兼容任意编码层数(对齐 workspace_view.dspy 的 _decode_id)。 + for _ in range(4): + if '%' not in s: + break + s2 = unquote(s) + if s2 == s: + break + s = s2 + return s + + uid = await get_user() if not uid: return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}} -project_id = ((params_kw or {}).get('project_id') or '').strip() -fpath = ((params_kw or {}).get('path') or '').strip() +project_id = _decode_path(((params_kw or {}).get('project_id') or '').strip()) +fpath = _decode_path(((params_kw or {}).get('path') or '').strip()) download = ((params_kw or {}).get('download') or '').strip() if not project_id: