# opp_report_ppt.dspy - 下载报告 PPT(附件) # 入参:report_id。路径只从库读(不接受用户传路径),防路径穿越。 import os from urllib.parse import quote from aiohttp.web_fileresponse import FileResponse uid = await get_user() if not uid: return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}} report_id = ((params_kw or {}).get('report_id') or '').strip() if not report_id: return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 report_id"}} dbname = get_module_dbname('pipeline-opportunity') async with DBPools().sqlorContext(dbname) as sor: recs = await sor.sqlExe( "SELECT title, ppt_path FROM opp_reports WHERE id=${i}$", {"i": report_id}) await sor.sqlExe("COMMIT", {}) if not recs: return {"widgettype": "Message", "options": {"title": "不存在", "message": "报告不存在"}} ppt_path = (getattr(recs[0], 'ppt_path', '') or '').strip() title = getattr(recs[0], 'title', '') or '研发报告' if not ppt_path or not os.path.isfile(ppt_path): return {"widgettype": "Message", "options": { "title": "无 PPT", "message": "该报告尚未生成 PPT(提交人工确认时自动生成)"}} filename = os.path.basename(ppt_path) safe_name = quote(filename) headers = {'Content-Disposition': 'attachment; filename="%s"; filename*=UTF-8\'\'%s' % (filename, safe_name)} return FileResponse(ppt_path, headers=headers)