notes: 加一键转飞书文档 (sidecar markdown-to-feishu)
deploy notes / build-and-deploy (push) Failing after 2m2s
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:
@@ -0,0 +1,90 @@
|
||||
"""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),
|
||||
}
|
||||
Reference in New Issue
Block a user