91 lines
2.8 KiB
Python
91 lines
2.8 KiB
Python
|
|
"""notes feishu sidecar:HTTP 包一层 markdown-to-feishu。
|
|||
|
|
|
|||
|
|
POST /convert {md_path, title?, existing_doc_id?}
|
|||
|
|
→ 跑 markdown-to-feishu,parse 最后那段 JSON,返回 {doc_id, url}
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import subprocess
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
from fastapi import FastAPI, HTTPException
|
|||
|
|
from pydantic import BaseModel
|
|||
|
|
|
|||
|
|
logging.basicConfig(level=logging.INFO,
|
|||
|
|
format='%(asctime)s %(levelname)s %(name)s: %(message)s')
|
|||
|
|
log = logging.getLogger('feishu')
|
|||
|
|
|
|||
|
|
app = FastAPI()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.get('/healthz')
|
|||
|
|
def healthz():
|
|||
|
|
return {'ok': True}
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ConvertReq(BaseModel):
|
|||
|
|
md_path: str
|
|||
|
|
title: Optional[str] = None
|
|||
|
|
existing_doc_id: Optional[str] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.post('/convert')
|
|||
|
|
def convert(req: ConvertReq):
|
|||
|
|
md = Path(req.md_path)
|
|||
|
|
if not md.exists():
|
|||
|
|
raise HTTPException(400, f'md not found: {md}')
|
|||
|
|
|
|||
|
|
cmd = ['/usr/local/bin/markdown-to-feishu', str(md), '--as', 'user']
|
|||
|
|
if req.existing_doc_id:
|
|||
|
|
cmd += ['--update', req.existing_doc_id]
|
|||
|
|
if req.title:
|
|||
|
|
cmd += ['--title', req.title]
|
|||
|
|
log.info("run: %s", ' '.join(cmd))
|
|||
|
|
|
|||
|
|
env = os.environ.copy()
|
|||
|
|
# markdown-to-feishu state file 放 PVC,重启不丢
|
|||
|
|
env['MD2FEISHU_STATE_DIR'] = '/data/feishu-state'
|
|||
|
|
Path('/data/feishu-state').mkdir(parents=True, exist_ok=True)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
proc = subprocess.run(
|
|||
|
|
cmd, capture_output=True, text=True, timeout=600, env=env,
|
|||
|
|
cwd=str(md.parent),
|
|||
|
|
)
|
|||
|
|
except subprocess.TimeoutExpired:
|
|||
|
|
raise HTTPException(504, 'markdown-to-feishu timeout (>10min)')
|
|||
|
|
|
|||
|
|
# exit code 2 = embeds 有失败,但 doc 创建成功,仍 parse stdout
|
|||
|
|
if proc.returncode not in (0, 2):
|
|||
|
|
log.warning("md2feishu exit=%d stderr=%s", proc.returncode, proc.stderr[-500:])
|
|||
|
|
raise HTTPException(502, f'md2feishu exit {proc.returncode}: '
|
|||
|
|
f'{proc.stderr.strip()[-400:]}')
|
|||
|
|
|
|||
|
|
# 取 stdout 里最后一段 JSON 对象(script 的 final print)
|
|||
|
|
out = proc.stdout.strip()
|
|||
|
|
# 从后往前找第一个 '{',取到末尾
|
|||
|
|
last_open = out.rfind('{')
|
|||
|
|
if last_open < 0:
|
|||
|
|
raise HTTPException(502, f'md2feishu no json output. stdout tail: {out[-400:]}')
|
|||
|
|
try:
|
|||
|
|
data = json.loads(out[last_open:])
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
raise HTTPException(502, f'md2feishu json parse: {e}; tail: {out[-400:]}')
|
|||
|
|
|
|||
|
|
doc_id = data.get('doc_id')
|
|||
|
|
url = data.get('url')
|
|||
|
|
if not doc_id or not url:
|
|||
|
|
raise HTTPException(502, f'md2feishu missing doc_id/url: {data}')
|
|||
|
|
log.info("ok: doc_id=%s url=%s embeds=%s",
|
|||
|
|
doc_id, url, data.get('embeds_inserted'))
|
|||
|
|
return {
|
|||
|
|
'doc_id': doc_id,
|
|||
|
|
'url': url,
|
|||
|
|
'embeds_inserted': data.get('embeds_inserted', 0),
|
|||
|
|
'embeds_failed': data.get('embeds_failed', 0),
|
|||
|
|
}
|