feat(workspace): xlsx在线查看/编辑接入(2026-09-18用户要求)

- office_read.dspy: 按扩展名分流——xlsx/xlsm调univer-office的xlsx-to-workbookdata
  返回workbookData;docx原链路不变(顺带f-string改拼接合规)
- office_save.dspy: workbookData/documentData双字段分流,xlsx调workbook-data-to-xlsx回写
- workspace_view/open.dspy: office_exts扩为['.docx','.xlsx','.xlsm'],
  xlsx与docx同享在线查看(mode=view只读)+在线编辑+下载按钮
- 配套: univer-office仓11d83bc(IWorkbookData双向转换器+sheets插件+44断言往返自测)
This commit is contained in:
ymq 2026-09-18 10:59:51 +08:00
parent 0438a9c4b5
commit 964f475067
4 changed files with 54 additions and 30 deletions

View File

@ -39,22 +39,37 @@ if not real_full.startswith(real_ws + os.sep):
if not os.path.isfile(full_path): if not os.path.isfile(full_path):
return {"error": "文件不存在", "markdown": ""} return {"error": "文件不存在", "markdown": ""}
# 读 docx bytes # 按扩展名分流:xlsx → IWorkbookData(表格),其余(docx)→ IDocumentData(文档)
with open(full_path, 'rb') as f: # (2026-09-18 xlsx 在线查看/编辑接入)
docx_bytes = f.read() _is_sheet = file_id.lower().endswith(('.xlsx', '.xlsm'))
# 调 univer-office 转 IDocumentData(格式保真,跳过 markdown) # 读文件 bytes
with open(full_path, 'rb') as f:
file_bytes = f.read()
# 调 univer-office 转换(格式保真,跳过 markdown)
try: try:
timeout = aiohttp.ClientTimeout(total=60) timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post( if _is_sheet:
'http://127.0.0.1:19091/convert/docx-to-docdata', async with session.post(
data=docx_bytes, 'http://127.0.0.1:19091/convert/xlsx-to-workbookdata',
headers={'Content-Type': 'application/octet-stream'}, data=file_bytes,
) as resp: headers={'Content-Type': 'application/octet-stream'},
if resp.status != 200: ) as resp:
return {"error": f"转换失败 HTTP {resp.status}", "documentData": None} if resp.status != 200:
doc_data = await resp.json() return {"error": "转换失败 HTTP " + str(resp.status), "workbookData": None}
return {"documentData": doc_data} workbook_data = await resp.json()
return {"workbookData": workbook_data}
else:
async with session.post(
'http://127.0.0.1:19091/convert/docx-to-docdata',
data=file_bytes,
headers={'Content-Type': 'application/octet-stream'},
) as resp:
if resp.status != 200:
return {"error": "转换失败 HTTP " + str(resp.status), "documentData": None}
doc_data = await resp.json()
return {"documentData": doc_data}
except Exception as e: except Exception as e:
return {"error": str(e), "documentData": None} return {"error": str(e), "documentData": None, "workbookData": None}

View File

@ -5,6 +5,8 @@ import aiohttp
file_id = (params_kw or {}).get('file', '').strip() file_id = (params_kw or {}).get('file', '').strip()
docdata_str = (params_kw or {}).get('documentData', '') or '' docdata_str = (params_kw or {}).get('documentData', '') or ''
# xlsx 保存:前端传 workbookData 字段(2026-09-18 xlsx 在线编辑接入)
workbookdata_str = (params_kw or {}).get('workbookData', '') or ''
uid = await get_user() uid = await get_user()
if not uid: if not uid:
@ -30,13 +32,16 @@ async with DBPools().sqlorContext(dbname) as sor:
if not file_id or not space_dir: if not file_id or not space_dir:
return {"success": False, "error": "未指定文件或无工作空间"} return {"success": False, "error": "未指定文件或无工作空间"}
if not docdata_str: # 按扩展名分流:xlsx 用 workbookData 字段,docx 用 documentData 字段
_is_sheet = file_id.lower().endswith(('.xlsx', '.xlsm'))
_payload_str = workbookdata_str if _is_sheet else docdata_str
if not _payload_str:
return {"success": False, "error": "缺少文档数据"} return {"success": False, "error": "缺少文档数据"}
try: try:
documentData = _json.loads(docdata_str) _payload = _json.loads(_payload_str)
except Exception as e: except Exception as e:
return {"success": False, "error": f"文档数据解析失败: {e}"} return {"success": False, "error": "文档数据解析失败: " + str(e)}
full_path = resolve_workspace_path(project_dir, space_dir, file_id) full_path = resolve_workspace_path(project_dir, space_dir, file_id)
@ -52,24 +57,28 @@ if not os.path.isfile(full_path):
title = os.path.basename(full_path) title = os.path.basename(full_path)
# 调 univer-office 生成 docx(IDocumentData -> docx,保留格式) # 调 univer-office 生成 office 文件(按扩展名分流,保留格式)
# xlsx: IWorkbookData -> xlsx;docx: IDocumentData -> docx
try: try:
timeout = aiohttp.ClientTimeout(total=120) timeout = aiohttp.ClientTimeout(total=120)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post( if _is_sheet:
'http://127.0.0.1:19091/convert/doc-data-to-docx', _url = 'http://127.0.0.1:19091/convert/workbook-data-to-xlsx'
json={'documentData': documentData, 'title': title}, _req_json = {'workbookData': _payload}
) as resp: else:
_url = 'http://127.0.0.1:19091/convert/doc-data-to-docx'
_req_json = {'documentData': _payload, 'title': title}
async with session.post(_url, json=_req_json) as resp:
if resp.status != 200: if resp.status != 200:
return {"success": False, "error": f"导出失败 HTTP {resp.status}"} return {"success": False, "error": "导出失败 HTTP " + str(resp.status)}
docx_bytes = await resp.read() out_bytes = await resp.read()
except Exception as e: except Exception as e:
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}
# 写回工作空间 # 写回工作空间
try: try:
with open(full_path, 'wb') as f: with open(full_path, 'wb') as f:
f.write(docx_bytes) f.write(out_bytes)
return {"success": True, "bytes": len(docx_bytes)} return {"success": True, "bytes": len(out_bytes)}
except Exception as e: except Exception as e:
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}

View File

@ -54,7 +54,7 @@ text_exts = ['.md', '.py', '.js', '.html', '.css', '.json', '.txt', '.xml', '.ya
video_exts = ['.mp4', '.m3u8', '.mpd', '.webm', '.mov', '.avi', '.mkv'] video_exts = ['.mp4', '.m3u8', '.mpd', '.webm', '.mov', '.avi', '.mkv']
audio_exts = ['.mp3', '.wav', '.aac', '.flac', '.m4a', '.ogg'] audio_exts = ['.mp3', '.wav', '.aac', '.flac', '.m4a', '.ogg']
image_exts = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp', '.ico'] image_exts = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp', '.ico']
office_exts = ['.docx'] # 在线编辑(univer-office)。xlsx/ppt 后续接入 Univer sheet/slide 插件 office_exts = ['.docx', '.xlsx', '.xlsm'] # 在线查看/编辑(univer-office)。ppt 后续接入 Univer slide 插件
# 二级 URL 双透传 session_id + pipeline_id(2026-09-17 根因同 workspace_view/upload): # 二级 URL 双透传 session_id + pipeline_id(2026-09-17 根因同 workspace_view/upload):
# 通用助手(pipeline_id=_generic、无 session_id)下漏拼 pipeline_id 会让下游端点 # 通用助手(pipeline_id=_generic、无 session_id)下漏拼 pipeline_id 会让下游端点

View File

@ -139,9 +139,9 @@ if ext in image_exts:
}] }]
} }
# office 文档(docx)→ univer-office 在线查看(mode=view 只读,不显示保存按钮)+ 下载按钮 # office 文档(docx/xlsx)→ univer-office 在线查看(mode=view 只读,不显示保存按钮)+ 下载按钮
# (2026-09-18 用户要求:查看和编辑都在线,另提供下载按钮) # (2026-09-18 用户要求:查看和编辑都在线,另提供下载按钮;同日 xlsx 接入)
office_exts = ['.docx'] office_exts = ['.docx', '.xlsx', '.xlsm']
if ext in office_exts: if ext in office_exts:
univer_url = entire_url("/univer-office/") + "?file=" + quote(file_id) + "&mode=view" + _fq_s univer_url = entire_url("/univer-office/") + "?file=" + quote(file_id) + "&mode=view" + _fq_s
download_url_v = file_url + "&download=1" download_url_v = file_url + "&download=1"