"""Seed the automation-exception echo cron The employee-facing half of the epic: tell the person who was waiting that the thing they needed now works, and tell the owner when a resolved problem comes back. No new columns — PR1 left ``subject_notified_at`` and ``escalated_at`` on ``autoexc_exceptions`` for exactly this, and the notification category comes from the template key, which is registered in code. Seeded ENABLED. A disabled seed is how a feature ships and is never switched on; the 2026-09-05 ledger audit found 35 such rows. Revision ID: 622 Revises: 621 """ from __future__ import annotations import uuid import sqlalchemy as sa from alembic import op revision = "622" # The real head on ``origin/main``, re-checked immediately before push. down_revision = "621" branch_labels = None depends_on = None JOBS = "scheduled_jobs" _JOB = ( "automation_exception.echo", "自动化异常 · 回音与复发升级", # 20 past each hour: after the collector (:00) and the triage (:35) # have had a tick, so an exception resolved this hour is echoed in # the same hour rather than waiting for the next one. "20 * * * *", "两件事,方向相反。回音:一次没走通的自动化背后通常有个人在等 —— 修好之后告诉他们一声。" "复发升级:一个被解决之后又回来的问题说明上次处置没解决根因,告诉负责人并带上建议的下一步。" "只在真的修好时发且只发一次;不新增打扰类目(走既有 lifecycle 类目,员工已有的静音与" "免打扰设置天然管住它);不把员工无法处置的失败细节告诉他。", ) def upgrade() -> None: bind = op.get_bind() inspector = sa.inspect(bind) if JOBS not in inspector.get_table_names(): return handler, display, cron, desc = _JOB bind.execute( sa.text( f"INSERT INTO {JOBS} " "(id, name, display_name, description, handler_key, " " trigger_type, cron_expr, timezone, enabled, config, " " run_count, failure_count, created_at, updated_at) " "SELECT :id, :name, :display, :desc, :handler, 'cron', " ":cron, 'UTC', 1, :config, 0, 0, NOW(), NOW() " "FROM DUAL WHERE NOT EXISTS " f"(SELECT 1 FROM {JOBS} WHERE handler_key = :handler)" ), { "id": str(uuid.uuid4()), "name": handler, "display": display, "desc": desc, "handler": handler, "cron": cron, "config": "{}", }, ) def downgrade() -> None: bind = op.get_bind() inspector = sa.inspect(bind) if JOBS not in inspector.get_table_names(): return bind.execute( sa.text(f"DELETE FROM {JOBS} WHERE handler_key = :handler"), {"handler": _JOB[0]}, )