feat(office): 补提交 univer-office 办公代理(已部署上线)

/office/* 反向代理到本机 univer-office Node 服务(9091),承接 markdown<->IDocumentData 转换 + docx/xlsx/ppt 导出。含 office_proxy.py + config.json 路由 + pipeline_app 注册 + restart-pipeline.sh
This commit is contained in:
yumoqing 2026-08-21 17:28:47 +08:00
parent c973a20fd1
commit b849eb3e92
4 changed files with 83 additions and 0 deletions

51
app/office_proxy.py Normal file
View File

@ -0,0 +1,51 @@
"""office_proxy - 把 /office/* 请求转发到本机 univer-office Node 服务(9091)。
univer-office 承担 markdown<->IDocumentData 转换与 docx/xlsx/ppt 导出
这些能力是 Node 生态(Univer + docx/exceljs/pptxgenjs)Python 侧无法直接实现
故通过 ahserver startswiths 机制做反向代理
路由映射
/office/render -> http://127.0.0.1:9091/render (markdown -> IDocumentData)
/office/extract -> http://127.0.0.1:9091/extract (IDocumentData -> 纯文本)
/office/export/docx -> http://127.0.0.1:9091/export/docx (markdown -> docx 二进制)
"""
import aiohttp
from aiohttp import web
from appPublic.registerfunction import registerFunction
from appPublic.log import error, debug
OFFICE_BACKEND = 'http://127.0.0.1:9091'
# hop-by-hop 头,转发时需剔除
_HOP_BY_HOP = {
'host', 'content-length', 'transfer-encoding', 'connection',
'keep-alive', 'upgrade', 'proxy-authenticate', 'proxy-authorization',
'te', 'trailers',
}
async def office_proxy(request, params_kw, *args, **env):
"""把 /office/* 转发到 9091。args = leading 之后的路径段,如 ('render',) 或 ('export','docx')。"""
path = '/'.join(args)
url = f'{OFFICE_BACKEND}/{path}'
debug(f'office_proxy: {request.method} {url}')
body = await request.read()
headers = {k: v for k, v in request.headers.items() if k.lower() not in _HOP_BY_HOP}
try:
timeout = aiohttp.ClientTimeout(total=180)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.request(request.method, url, headers=headers, data=body) as resp:
resp_body = await resp.read()
resp_headers = {
k: v for k, v in resp.headers.items()
if k.lower() not in _HOP_BY_HOP and k.lower() != 'content-encoding'
}
return web.Response(body=resp_body, status=resp.status, headers=resp_headers)
except Exception as e:
error(f'office_proxy failed: {e}')
return web.Response(body=f'office backend error: {e}'.encode(), status=502)
registerFunction('office_proxy', office_proxy)

View File

@ -39,6 +39,7 @@ def init():
from tenant.init import load_tenant
from ktv_adapter import load_ktv_adapter
from global_func import set_globalvariable
import office_proxy # 注册 /office/* 转发函数univer-office Node 服务代理)
try:
from smssend import load_smssend
except ImportError:

View File

@ -51,6 +51,10 @@
{
"leading": "/i18n_getmsgs",
"registerfunction": "i18n"
},
{
"leading": "/office/",
"registerfunction": "office_proxy"
}
]
},

27
restart-pipeline.sh Normal file
View File

@ -0,0 +1,27 @@
#!/bin/bash
# 重启 pipeline-app避免 pkill -f pipeline_app 自杀:精确匹配 python 主进程路径)
cd /d/pipeline/pipeline-app
echo "=== 杀旧进程 ==="
if [ -f pipeline.pid ]; then
kill $(cat pipeline.pid) 2>/dev/null
fi
pkill -f "app/pipeline_app.py" 2>/dev/null
sleep 3
if pgrep -f "app/pipeline_app.py" > /dev/null; then
echo "still running, force kill"
pkill -9 -f "app/pipeline_app.py" 2>/dev/null
sleep 2
fi
rm -f pipeline.pid pipeline-web.pid pipeline-worker-*.pid
echo "=== 启动 ==="
bash start.sh
sleep 6
echo "=== 进程 ==="
ps aux | grep "app/pipeline_app.py" | grep -v grep | head -2
echo "=== 日志 tail ==="
tail -15 logs/pipeline.log 2>/dev/null