"""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), }