"""Feishu wiki docx KB mirror source (source='feishu_doc'). Adds mirror-provenance columns to ``support_kb_articles`` so the Feishu wiki KB subtree can be mirrored into the existing retrieval stack (semantic + keyword search, copilot grounding, deflection, MCP) without copying authorship anywhere: the wiki stays the ONLY editing surface and the sync (``app/domains/support/feishu_docs.py``) owns these rows. Also seeds the ``support.feishu_kb_sync`` scheduled job (6h interval). The handler no-ops gracefully until ``support.feishu_kb.root_tokens`` is configured in the admin KB page. Revision ID: 486 Revises: 485 """ from __future__ import annotations import uuid import sqlalchemy as sa from alembic import op revision = "486" down_revision = "485" branch_labels = None depends_on = None _JOB_NAME = "support.feishu_kb_sync" def upgrade() -> None: op.add_column( "support_kb_articles", sa.Column("external_obj_token", sa.String(64), nullable=True), ) op.add_column( "support_kb_articles", sa.Column("external_url", sa.String(512), nullable=True), ) op.add_column( "support_kb_articles", sa.Column("external_root_token", sa.String(64), nullable=True), ) # obj_edit_time watermark — epoch SECONDS (the wiki API returns seconds, # unlike the helpdesk ticket API's milliseconds). op.add_column( "support_kb_articles", sa.Column("external_edited_at", sa.BigInteger(), nullable=True), ) # One article per mirrored doc; MySQL/TiDB unique indexes admit multiple # NULLs, so native manual/ai_ticket rows are unaffected. op.create_index( "ux_support_kb_external_obj", "support_kb_articles", ["external_obj_token"], unique=True, ) bind = op.get_bind() job_exists = bind.execute( sa.text("SELECT 1 FROM scheduled_jobs WHERE name = :n LIMIT 1"), {"n": _JOB_NAME}, ).scalar() if not job_exists: bind.execute( sa.text( """ INSERT INTO scheduled_jobs (id, name, display_name, description, handler_key, trigger_type, interval_seconds, cron_expr, timezone, enabled, config, run_count, failure_count, created_at, updated_at) VALUES (:id, :name, :display_name, :description, :handler_key, 'interval', 21600, NULL, 'UTC', 1, '{}', 0, 0, NOW(), NOW()) """ ), { # Pre-mint UUID — TiDB's UUID() is unreliable (see # 077_seed_jira_integration.py / scheduled_jobs no-dash bug). "id": str(uuid.uuid4()), "name": _JOB_NAME, "display_name": "求助 · 飞书文档知识库同步", "description": ( "增量镜像飞书知识库(wiki)配置子树下的 docx 正文到求助域知识库," "供语义检索/copilot/deflection/MCP 使用。飞书文档仍是唯一编辑处;" "未配置根节点时优雅空跑。每 6 小时一次,按 obj_edit_time 增量。" ), "handler_key": _JOB_NAME, }, ) def downgrade() -> None: bind = op.get_bind() bind.execute( sa.text("DELETE FROM scheduled_jobs WHERE name = :n"), {"n": _JOB_NAME}, ) op.drop_index("ux_support_kb_external_obj", table_name="support_kb_articles") op.drop_column("support_kb_articles", "external_edited_at") op.drop_column("support_kb_articles", "external_root_token") op.drop_column("support_kb_articles", "external_url") op.drop_column("support_kb_articles", "external_obj_token")