videoNote / backend /app /routers /feishu.py
zhoujiaangyao
feat(feishu): 支持导入到知识库(Wiki)
2c9f2fe
Raw
History Blame Contribute Delete
5.77 kB
from __future__ import annotations
import logging
import os
from datetime import datetime
from typing import Optional
from fastapi import APIRouter
from pydantic import BaseModel
from app.services import feishu_pusher
from app.services.feishu_config_manager import FeishuConfigManager
from app.services.feishu_service import FeishuError
from app.utils.response import ResponseWrapper as R
logger = logging.getLogger(__name__)
router = APIRouter()
# 后端对外可访问地址:用于把笔记里的 /static、/uploads 相对图片补成绝对链接,
# 让飞书导入时有机会抓到图(与 app/services/note.py 的 BACKEND_BASE_URL 同源)。
_API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost")
_BACKEND_PORT = os.getenv("BACKEND_PORT", "8483")
BACKEND_ORIGIN = f"{_API_BASE_URL}:{_BACKEND_PORT}"
class FeishuConfigRequest(BaseModel):
app_id: Optional[str] = None
app_secret: Optional[str] = None
folder_token: Optional[str] = None
wiki_token: Optional[str] = None # 知识库节点链接/token,填了就导入到该 wiki 节点下
base_url: Optional[str] = None
auto_push: Optional[bool] = None
enabled: Optional[bool] = None
push_backend: Optional[str] = None # "auto" | "rest" | "cli"
cli_path: Optional[str] = None
class FeishuPushRequest(BaseModel):
task_id: str
version_id: Optional[str] = None
def push_task_to_feishu(task_id: str, version_id: Optional[str] = None) -> dict:
"""读取已生成笔记 → 导入飞书文档 → 把结果写回笔记 JSON,返回 feishu 信息。
被「手动推送」接口与「生成后自动推送」共用。任何失败都抛 FeishuError,
由调用方决定是返回错误还是仅记日志(自动推送场景不应中断主流程)。
直接读写原始笔记内容(不走 _read/_write_note_json 的版本归一化),避免把
markdown 字符串就地改写成版本数组、抹掉单版本笔记的 model/style 元信息。
"""
# 延迟导入避免与 note 路由的循环依赖;note 路由不在模块级 import 本模块
from app.routers.note import _pick_markdown_version
from app.db.note_dao import load_note, save_note
data = load_note(task_id)
if data is None:
raise FeishuError(f"笔记不存在或尚未生成完成:{task_id}")
# _pick_markdown_version 兼容旧 str 与多版本 list 两种格式,仅读取不改写
content = _pick_markdown_version(data.get("markdown"), version_id)
if not (content or "").strip():
raise FeishuError("笔记内容为空,无法推送到飞书")
audio_meta = data.get("audio_meta") or {}
raw_info = audio_meta.get("raw_info") or {}
title = audio_meta.get("title") or raw_info.get("title") or f"VideoMemo 笔记 {task_id[:8]}"
result = feishu_pusher.push_markdown(
title=title,
markdown=content,
image_base_url=BACKEND_ORIGIN,
)
feishu_info = {
"url": result.get("url", ""),
"token": result.get("token", ""),
"type": result.get("type", "docx"),
"title": result.get("title", title),
"pushed_at": datetime.now().isoformat(timespec="seconds"),
}
data["feishu"] = feishu_info
save_note(task_id, data)
return feishu_info
def auto_push_if_enabled(task_id: str) -> None:
"""笔记生成成功后调用:开启「自动推送」时把笔记推到飞书。失败只记日志,不影响主流程。"""
try:
if not FeishuConfigManager().is_auto_push_enabled():
return
info = push_task_to_feishu(task_id)
logger.info(f"飞书自动推送成功 (task_id={task_id}) -> {info.get('url')}")
except Exception as e:
logger.warning(f"飞书自动推送失败 (task_id={task_id}):{e}")
# ─── 配置 ────────────────────────────────────────────────────────────────────
@router.get("/feishu_config")
def get_feishu_config():
return R.success(FeishuConfigManager().get_public_config())
@router.post("/feishu_config")
def update_feishu_config(data: FeishuConfigRequest):
cfg = FeishuConfigManager().update_config(
enabled=data.enabled,
auto_push=data.auto_push,
app_id=data.app_id,
app_secret=data.app_secret,
folder_token=data.folder_token,
wiki_token=data.wiki_token,
base_url=data.base_url,
push_backend=data.push_backend,
cli_path=data.cli_path,
)
return R.success(cfg, msg="飞书配置已保存")
@router.post("/feishu_test")
def test_feishu_connection():
try:
result = feishu_pusher.test_connection()
return R.success(result, msg=result.get("message", "连接成功"))
except FeishuError as e:
return R.error(msg=e.message, code=400)
except Exception as e:
logger.error(f"飞书连接测试异常: {e}", exc_info=True)
return R.error(msg=f"连接失败:{e}", code=400)
# ─── 推送笔记 ─────────────────────────────────────────────────────────────────
@router.post("/feishu_push")
def push_note_to_feishu(data: FeishuPushRequest):
try:
info = push_task_to_feishu(data.task_id, data.version_id)
return R.success(info, msg="已推送到飞书文档")
except FeishuError as e:
return R.error(msg=e.message, code=400)
except Exception as e:
logger.error(f"飞书推送失败 (task_id={data.task_id}): {e}", exc_info=True)
return R.error(msg=f"推送失败:{e}", code=500)