notes: 加一键转飞书文档 (sidecar markdown-to-feishu)
deploy notes / build-and-deploy (push) Failing after 2m2s

- backend: POST /api/recordings/:id/feishu → 拼 markdown (总结在最上 + 附件链接到转录/录音 + 转写全文) → 写 /data/feishu-tmp/<id>/ → HTTP POST 到 feishu sidecar
- 复用:已有 feishu_doc_id 时 --update 同一个 doc,前端按钮文案变「↻ 重新生成」
- schema 加 feishu_doc_id + feishu_url 两列(ALTER TABLE 兼容旧 db)
- LLM prompt 改:行动项用 markdown checkbox `- [ ] 谁·做什么·何时`
- sidecar apps/notes/feishu: node:20 + python3 + python3-markdown + @larksuite/cli + COPY 自己的 markdown-to-feishu script + FastAPI /convert
- k8s: deployment 加 feishu container 共享 PVC;lark-cli-creds Secret 挂 /root/.lark-cli/config.json
- CI: 主 image --no-cache(cube 规矩),sidecar 保留 layer cache(chromium-free,但 apt/npm 也大)
- 前端: content 头部加「📤 一键转飞书文档」按钮;已转过显示飞书链接 + 按钮变重生成
This commit is contained in:
Fam Zheng
2026-05-17 22:16:13 +01:00
parent 3a34fbdfd8
commit 68671784f6
8 changed files with 1327 additions and 11 deletions
+90
View File
@@ -0,0 +1,90 @@
"""notes feishu sidecarHTTP 包一层 markdown-to-feishu。
POST /convert {md_path, title?, existing_doc_id?}
→ 跑 markdown-to-feishuparse 最后那段 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),
}