#!/usr/bin/env python3 # -*- coding: utf-8 -*- """作业16 · 生成「知识同步失败」告警文案模板(含 trace / 任务 ID) 用法: python hw16_01_知识同步失败告警文案.py python hw16_01_知识同步失败告警文案.py \ --dataset-id ds-abc --document-id doc-xyz \ --task-id celery-task-001 --trace-id trace-langfuse-001 \ --error "embedding timeout" --level P2 输出: data/homework16/alert_templates_knowledge_sync.md data/homework16/alert_sample_.txt """ from __future__ import annotations import argparse import os from datetime import datetime from pathlib import Path from typing import Any def out_dir() -> Path: env = os.environ.get("HW16_OUT") if env: p = Path(env) p.mkdir(parents=True, exist_ok=True) return p repo = Path(__file__).resolve().parents[2] / "data" / "homework16" repo.mkdir(parents=True, exist_ok=True) return repo def build_templates(ctx: dict[str, str]) -> dict[str, str]: """返回多通道告警模板:值班群 / 邮件 / 工单 / 升级""" ids_block = ( f"dataset_id={ctx['dataset_id']} document_id={ctx['document_id']}\n" f"task_id={ctx['task_id']} trace_id={ctx['trace_id']}" ) return { "oncall_im": f"""【知识同步失败 · {ctx['level']}】 时间:{ctx['time']} 环境:{ctx['env']} 应用/知识库:{ctx['app_name']} / {ctx['dataset_name']} 现象:文档索引失败({ctx['stage']} → error) 错误:{ctx['error']} {ids_block} 已做:{ctx['action_taken']} 需要:{ctx['need_help']} Runbook:作业16_向量恢复演练Runbook_cursor.md """, "email": f"""Subject: [{ctx['level']}] 知识同步失败 — {ctx['dataset_name']} — {ctx['env']} 知识库文档未能完成向量同步,请值班同学按 Runbook 处理。 - 时间:{ctx['time']} - 环境:{ctx['env']} - 应用:{ctx['app_name']} - 知识库:{ctx['dataset_name']} ({ctx['dataset_id']}) - 文档:{ctx['document_name']} ({ctx['document_id']}) - 阶段:{ctx['stage']} - 错误摘要:{ctx['error']} - Celery task_id:{ctx['task_id']} - Langfuse trace_id:{ctx['trace_id']} - 向量后端:{ctx['vector_store']} - 重试次数:{ctx['retry_count']} / 最大 {ctx['max_retry']} 建议动作: 1. 查 worker 日志:grep {ctx['document_id']} / {ctx['task_id']} 2. 查 Redis dataset 队列 LLEN(作业15) 3. 若 embedding/向量库故障 → 按作业16 恢复演练;否则 UI「重试」或 API 重建索引 4. 超过 {ctx['max_retry']} 次失败 → 转人工(manual)状态 --- 本邮件由 hw16_01 模板生成,勿直接回复。 """, "ticket": f"""标题:知识同步失败 [{ctx['level']}] {ctx['dataset_name']} | 字段 | 值 | |------|-----| | env | {ctx['env']} | | app_name | {ctx['app_name']} | | dataset_id | {ctx['dataset_id']} | | document_id | {ctx['document_id']} | | task_id | {ctx['task_id']} | | trace_id | {ctx['trace_id']} | | stage | {ctx['stage']} | | error | {ctx['error']} | | vector_store | {ctx['vector_store']} | | retry | {ctx['retry_count']}/{ctx['max_retry']} | 复现:上传同名文件或 API 触发 re-index 关联:作业15 Worker Runbook、作业16 向量恢复演练 """, "escalation": f"""【升级 · 知识同步 P1】 {ctx['time']} {ctx['env']} 知识库「{ctx['dataset_name']}」批量索引失败,影响检索可用性。 失败文档数:{ctx['failed_count']} dataset_id={ctx['dataset_id']} 样例 task_id={ctx['task_id']} trace_id={ctx['trace_id']} 向量层:{ctx['vector_store']} 错误:{ctx['error']} 请平台负责人在 30 分钟内确认:向量库健康 / 备份恢复 / 是否暂停写入。 """, } def master_markdown(templates: dict[str, str], ctx: dict[str, str]) -> str: lines = [ "# 知识同步失败 · 告警文案模板(作业16 生成)", "", f"> 生成时间:`{ctx['time']}` · 环境:`{ctx['env']}`", "", "## 占位符说明", "", "| 占位符 | 含义 | 获取方式 |", "|--------|------|----------|", "| `dataset_id` | 知识库 ID | Dify 控制台 URL 或 `GET /datasets` |", "| `document_id` | 文档 ID | 知识库文档列表 API |", "| `task_id` | Celery 任务 ID | worker 日志 / Redis;格式如 UUID |", "| `trace_id` | Langfuse Trace | Langfuse UI 或 Dify 日志 ops_trace |", "| `stage` | 状态机阶段 | parsing/indexing/error 等 |", "", "## 1. 值班群(IM)", "", "```text", templates["oncall_im"].rstrip(), "```", "", "## 2. 邮件", "", "```text", templates["email"].rstrip(), "```", "", "## 3. 工单", "", templates["ticket"], "", "## 4. 升级(P1 批量失败)", "", "```text", templates["escalation"].rstrip(), "```", "", "## 5. Prometheus / Alertmanager 注解(可选)", "", "```yaml", "annotations:", " summary: \"知识同步失败 {{ $labels.dataset_id }}\"", " description: |", " env: " + ctx["env"], " document_id: {{ $labels.document_id }}", " task_id: {{ $labels.task_id }}", " trace_id: {{ $labels.trace_id }}", " error: {{ $labels.error_type }}", "```", "", ] return "\n".join(lines) def main() -> None: parser = argparse.ArgumentParser(description="生成知识同步失败告警文案") parser.add_argument("--env", default=os.environ.get("DIFY_ENV", "dify-hw15 / 隔离")) parser.add_argument("--level", default="P2", choices=["P3", "P2", "P1"]) parser.add_argument("--app-name", default="客服Agent-测试") parser.add_argument("--dataset-id", default="") parser.add_argument("--dataset-name", default="测试知识库") parser.add_argument("--document-id", default="") parser.add_argument("--document-name", default="产品手册.pdf") parser.add_argument("--task-id", default="") parser.add_argument("--trace-id", default="") parser.add_argument("--stage", default="indexing") parser.add_argument("--error", default="embedding upstream timeout after 120s") parser.add_argument("--vector-store", default="weaviate") parser.add_argument("--retry-count", default="2") parser.add_argument("--max-retry", default="3") parser.add_argument("--failed-count", default="1") parser.add_argument("--action-taken", default="已查 worker 日志,Redis dataset 队列无消费") parser.add_argument("--need-help", default="确认 embedding 服务 / 是否执行向量恢复演练") args = parser.parse_args() ctx: dict[str, str] = { "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "env": args.env, "level": args.level, "app_name": args.app_name, "dataset_id": args.dataset_id, "dataset_name": args.dataset_name, "document_id": args.document_id, "document_name": args.document_name, "task_id": args.task_id, "trace_id": args.trace_id, "stage": args.stage, "error": args.error, "vector_store": args.vector_store, "retry_count": args.retry_count, "max_retry": args.max_retry, "failed_count": args.failed_count, "action_taken": args.action_taken, "need_help": args.need_help, } templates = build_templates(ctx) od = out_dir() md_path = od / "alert_templates_knowledge_sync.md" md_path.write_text(master_markdown(templates, ctx), encoding="utf-8") sample_path = od / f"alert_sample_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" sample_path.write_text(templates["oncall_im"], encoding="utf-8") print(f"Wrote {md_path}") print(f"Wrote {sample_path}") print("\n--- 值班群预览 ---\n") print(templates["oncall_im"]) if __name__ == "__main__": main()