"""Scheduled-job handler for the Feishu wiki → KB mirror sync. ``support.feishu_kb_sync`` mirrors the configured wiki subtree(s)' docx text into ``support_kb_articles`` (source='feishu_doc') so the retrieval stack grounds on the wiki KB. Seeded by migration 486 on a 6-hour interval; no-ops gracefully until ``support.feishu_kb.root_tokens`` is configured. Idempotent (watermark + per-doc upsert), and serialized with the admin 「立即同步」 button via a distributed lock. """ from __future__ import annotations from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.core.coordinators.policy_notice_kb_coordinator import ( register_policy_token_provider, ) from app.core.distributed_lock import LockBusy, distributed_lock from app.domains.support.feishu_docs import sync_feishu_kb from app.scheduled_jobs.registry import register_scheduled_job # Invert the cross-domain dependency at import time: this handler module is # imported by BOTH app.main and app.worker at startup, so every process that # can run the mirror (cron or the admin button's background task) has the # 制度中心 token provider plugged in. Domains never import coordinators. register_policy_token_provider() @register_scheduled_job( key="support.feishu_kb_sync", display_name="求助 · 飞书文档知识库同步", description=( "增量镜像飞书知识库(wiki)配置子树下的 docx 正文到求助域知识库," "供语义检索/copilot/deflection/MCP 使用。飞书文档仍是唯一编辑处;" "未配置根节点时优雅空跑。" ), ) async def run_support_feishu_kb_sync( db: AsyncSession, config: dict[str, Any], # noqa: ARG001 ) -> dict[str, Any]: try: async with distributed_lock("support_feishu_kb_sync", ttl=600, auto_renew=True): return await sync_feishu_kb(db) except LockBusy: # A manual admin-triggered run is in flight — genuinely nothing to # do (the in-flight run covers this window; 6h cadence + 600s TTL # makes chronic starvation impossible). return {"skipped": "lock_busy"}