#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ build_inspection_report.py — 巡检报告 HTML 生成脚本(2026-08-06 落地) ======================================================================== 说明:从对话/看板/Markdown 抽取的巡检数据,按统一模板生成单页 HTML 报告。 套用 2026-08-05 早班报告样式 + 4 维分类筛选交互,零 CDN 依赖。 用法: python scripts/build_inspection_report.py # 例:python scripts/build_inspection_report.py 2026-08-06 早班 输入参数: - date YYYY-MM-DD - shift 早班/午班/晚班 输出: docs/07-项目管理/巡检报告/YYYY-MM/YYYY-MM-DD-{shift}.html ======================================================================== """ import argparse import sys from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent PROJECT_DIR = SCRIPT_DIR.parent DEFAULT_OUT_DIR = PROJECT_DIR / "docs" / "07-项目管理" / "巡检报告" # ============================================================ # 巡检数据(2026-08-06 06:00 早班) # ============================================================ DATA = { "date": "2026-08-06", "shift": "早班", "time_str": "2026-08-06 06:00", "kanban_version": "v1.9.0-DRAFT", "filter_counts": { "all": 11, "p0": 5, "decide": 2, "doing": 1, "server": 8, "code": 5, "security": 4, "history": 5, }, "p0_rows": [ { "id": "P0-1", "name": "/itportal/ 入口 500 错误", "category": "♻️ 历史遗留", "desc": "2026-08-06 06:00 jumpserver-V2 实测仍 500,nginx error: rewrite or internal redirection cycle while internally redirecting to "/itportal/index.html"。根因:容器内 /usr/share/nginx/html/itportal/ 空目录 + nginx location /itportal/ 仍配置 try_files $uri /itportal/index.html。建议:docker compose up -d --force-recreate nginx(前提:commit 21830d5 中 nginx.conf 已删除该 location 块)", }, { "id": "P0-3", "name": "closing_service.py:467 datetime 时区错位", "category": "♻️ 历史遗留", "desc": "datetime.now() - close_timeTypeError: can't subtract offset-naive and offset-aware datetimes。影响 POST /h5/conversations/current/reopen。同文件 18 处 datetime.now() 全是 naive,且 from datetime import datetime, timedelta 未 import timezone。24h 内未触发,但用户使用 H5 重开会话即会爆", }, { "id": "P0-4", "name": "employee_profile_service.py:236 SessionLocal NoneType 🔥", "category": "🆕 位置修正", "desc": "看板 v1.8 行号描述错误:实际错误位置是 app/services/employee_profile_service.py:236 而非 h5_ai_task.py:1198db = SessionLocal() 返回 None → TypeError: 'NoneType' object is not callable。2026-08-05 24h 内累计 10+ 次(01:33 / 03:25 / 04:07 / 04:11 多波次),影响 H5 IT 资产推荐异步推送。同根因 P0-5", }, { "id": "P0-5", "name": "容器 app/constants/ 打包错误 🔥", "category": "🆕 新增", "desc": "容器内 /app/app/constants/__init__.py 11635 bytes 内容是 automation.py(两个文件互换了),/app/app/constants/automation.py 不存在。直接后果:连续 10+ 次 ModuleNotFoundError: No module named 'app.constants.ai_reply_mode' + 同根因连锁导致 P0-4(SessionLocal 绑定失败)。建议:① 临时 docker cp 修补 → ② 长期排查 backend 镜像构建流程", }, { "id": "P0-6", "name": "后端 OAuth 40029 抖动", "category": "ℹ️ 信息类", "desc": "218.75.34.87 等外部 IP 持续试探 /api/auth/validate-sso + 偶发 OAuth code 失效。属 WAF 扫描背景噪声,已 fail2ban 监控。无需主动处理", }, ], "decide_rows": [ { "id": "BLK-A", "name": "企微会议室 Secret", "duration": "⚠️ 26 天(自 2026-07-11)", "owner": "需企业微信管理后台申请。Owner: 平台组", }, { "id": "BLK-B", "name": "ITSM API 授权", "duration": "⚠️ 26 天(自 2026-07-11)", "owner": "需向 ITSM 平台方申请 app_id/app_secret。Owner: 平台组", }, ], "doing_rows": [ { "id": "#81", "name": "敏感词检测 + 语气优化", "status": "v1.2 待排期", "desc": "v1.1 阶段 1 已于 2026-07-28 完成(词库入库 + 后台管理 UI + 12 配套 API)。剩余 "语气优化、灰度策略细化" 待 v1.2 排期", }, ], "server_rows": [ {"name": "Docker 容器", "status": "ok", "label": "正常", "desc": "5/5 healthy:nginx (Up 12h) / backend (Up 17h) / redis (Up 2w) / postgres (Up 3w) / neo4j (Up 3w)。无 Exited/Restarting/Unhealthy"}, {"name": "后端 health", "status": "ok", "label": "正常", "desc": "/health → HTTP 200(容器内 3.9ms);外部 curl http://localhost:8000 000(端口未映射给宿主,nginx 反代访问正常)"}, {"name": "后端 5xx 错误", "status": "fail", "label": "异常", "desc": "24h 内仍见 2 类 Traceback:P0-5 ModuleNotFoundError(10+ 次)+ P0-4 SessionLocal NoneType(10+ 次)"}, {"name": "PostgreSQL", "status": "ok", "label": "正常", "desc": "pg_isready -U postgresaccepting connections"}, {"name": "Redis", "status": "ok", "label": "正常", "desc": "redis_version:7.4.9 / 独立模式 + 密码认证(NOAUTH 正常);已配密码 WKzl7jgKTkeWxFWGIBXfzokm59Gvfr76"}, {"name": "Nginx 配置", "status": "ok", "label": "正常", "desc": "nginx -t → syntax OK;80/443 已 bind"}, {"name": "前端入口", "status": "fail", "label": "异常", "desc": "/itportal/ 500(rewrite cycle);/h5//itdesk/ 200 OK;/ 200(612 bytes root index.html)"}, {"name": "磁盘 / 内存", "status": "ok", "label": "正常", "desc": "130G 可用(13% 用)/ 3.4G used / 11G available / 负载 0.99 / 已运行 54 天"}, ], "code_rows": [ {"name": "🟢 鉴权补漏", "desc": "sensitive_words.py 13 端点已正确加 dependencies=[Depends(require_admin)] (line 47-51)。看板描述脱节,v1.9 已迁移至已完成"}, {"name": "🔴 容器/源码不一致", "desc": "容器内 app/constants/__init__.py = automation.py 内容。排查镜像构建流程,重打 backend 镜像"}, {"name": "🟡 死代码 / 残留", "desc": "console.log 在 src/frontend-*/src 已搜不到。看板描述脱节,v1.9 已迁移至已完成"}, {"name": "🟢 时区隐患", "desc": "closing_service.py 全部 18 处 datetime.now() 全是 naive,无 import timezone。建议全文件改造为 datetime.now(tz=timezone.utc)"}, {"name": "🟢 未使用导入 / 硬编码", "desc": "未发现明显问题(快速扫描未命中);源码层无硬编码(生产 env 路径已隔离)"}, ], "security_rows": [ {"name": "🟡 API 鉴权", "desc": "sensitive_words.py 13 端点已 require_admin;其余 admin API 状态需逐项复核(本次未做全量扫描)"}, {"name": "🟢 敏感数据处理", "desc": "数据库连接串、Redis 密码均在 env 注入,未硬编码到源码"}, {"name": "🟡 依赖包", "desc": "cryptography==42.0.8 / pydantic==2.7.4(2026-08-05 巡检已记录,仍待升级)"}, {"name": "🟡 日志敏感信息", "desc": "生产 logs 记录了用户 OAuth code(如 code=OFVMVR7IB2dgFVWX...),属企微临时凭据,建议在 wecom_service.py:1411 打印前 mask"}, ], "history_rows": [ {"name": "P0-2 sensitive_words.py 13 端点无鉴权", "category": "♻️→✅", "desc": "看板 v1.8 标\"待治理\",2026-08-06 06:00 巡检发现代码已修复(router 加 dependencies=[Depends(require_admin)] line 47-51),v1.9 看板已迁移至已完成区"}, {"name": "P1-1 前端 console.log 残留 208 行", "category": "♻️→✅", "desc": "看板 v1.8 标\"待清理\",2026-08-06 06:00 巡检发现 rg/find 搜不到任何 console.log,v1.9 看板已迁移至已完成区"}, {"name": "P0-4 行号描述修正", "category": "🔄 修正", "desc": "看板 v1.8 写 h5_ai_task.py:1198,实际是 employee_profile_service.py:236同根因 P0-5,建议合并排查"}, {"name": "BLK-A/B 阻塞时长刷新", "category": "📅 +1d", "desc": "自 2026-07-11 起,2026-08-06 已 26 天(较昨日 +1 自然日),远超 3 天阈值"}, {"name": "/itportal/ 500 持续 29 天", "category": "🔥 持续", "desc": "2026-07-08 起至今,今日 06:00 jumpserver-V2 实测仍 500。需 PM 派单部署 commit 21830d5 或清理 nginx location 块"}, ], "pm_alert": [ "🔴 P0-NEW1(constants 包打包错误)必须当日修:直接导致 H5 异步任务全部崩溃(资产推荐推送 NoneType)。修复方向:① 临时 docker cp 修补 → ② 长期重打 backend 镜像", "🔴 /itportal 500 已持续 29 天:根因明确(容器内空 dist + nginx location 块残留)。建议执行 docker compose up -d --force-recreate nginx", "🟡 看板 v1.8.0-FROZEN 2 项描述脱节:P0-2 sensitive_words / P1-1 console.log 实际已修复 / 已清理,v1.9 已修正", "⚠️ BLK-A/B 阻塞已 26 天(+23 天超过 3 天阈值):建议 PM 升级催办或重新评估优先级", "🟡 closing_service.py:467 时区隐患:24h 内未触发,修复成本低(from datetime import timezone + 18 处替换)", ], } # ============================================================ # 样式(复用 2026-08-05 早班报告色板,零 CDN) # ============================================================ CSS = r""" :root { --accent: #07C160; --accent-soft: #E7F8EF; --warn: #FA9D3B; --danger: #FA5151; --info: #10AEFF; --text: #1F2329; --text-soft: #646A73; --bg: #FFFFFF; --bg-soft: #F7F8FA; --border: #DEE0E3; --shadow: 0 2px 12px rgba(0, 0, 0, 0.06); --radius: 8px; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; color: var(--text); background: var(--bg-soft); line-height: 1.6; font-size: 14px; } .container { max-width: 1080px; margin: 0 auto; padding: 32px 24px 64px; } header { background: linear-gradient(135deg, var(--accent) 0%, #06AD56 100%); color: #fff; padding: 32px; border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: 24px; } header h1 { font-size: 26px; font-weight: 600; margin-bottom: 8px; } header .meta { font-size: 13px; opacity: 0.92; } header .meta span { margin-right: 18px; } header .meta strong { font-weight: 600; } h2 { font-size: 18px; font-weight: 600; margin: 32px 0 14px; padding-left: 10px; border-left: 4px solid var(--accent); } .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 8px; } .summary-card { background: #fff; padding: 14px 16px; border-radius: var(--radius); box-shadow: var(--shadow); text-align: center; } .summary-card .num { font-size: 24px; font-weight: 700; color: var(--accent); } .summary-card .label { font-size: 12px; color: var(--text-soft); margin-top: 4px; } .summary-card.warn .num { color: var(--warn); } .summary-card.danger .num { color: var(--danger); } .summary-card.info .num { color: var(--info); } table { width: 100%; border-collapse: collapse; background: #fff; border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; margin-bottom: 24px; } th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; } th { background: var(--bg-soft); font-size: 12px; font-weight: 600; color: var(--text-soft); } tr:last-child td { border-bottom: none; } .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; } .badge.p0 { background: #FEE; color: var(--danger); border: 1px solid var(--danger); } .badge.p1 { background: #FFF3E0; color: var(--warn); border: 1px solid var(--warn); } .badge.decide { background: #E6F4FF; color: var(--info); border: 1px solid var(--info); } .badge.doing { background: var(--accent-soft); color: var(--accent); border: 1px solid var(--accent); } .badge.ok { background: var(--accent-soft); color: var(--accent); } .badge.fail { background: #FEE; color: var(--danger); } code { background: var(--bg-soft); padding: 2px 6px; border-radius: 3px; font-family: "Cascadia Code", Consolas, monospace; font-size: 12px; } .alert { background: #FFF8E6; border-left: 4px solid var(--warn); padding: 12px 16px; margin: 16px 0; border-radius: 4px; } .alert.danger { background: #FEE; border-left-color: var(--danger); } .alert strong { display: block; margin-bottom: 4px; } .legend { color: var(--text-soft); font-size: 12px; margin-top: 4px; margin-bottom: 16px; } footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border); color: var(--text-soft); font-size: 12px; text-align: center; } ul.condensed { list-style: none; padding: 0; } ul.condensed li { padding: 8px 12px; border-bottom: 1px dashed var(--border); background: #fff; } ul.condensed li:last-child { border-bottom: none; } ul.condensed li:first-child { border-radius: var(--radius) var(--radius) 0 0; } ul.condensed li:last-child { border-radius: 0 0 var(--radius) var(--radius); box-shadow: var(--shadow); } .filter-bar { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 8px; padding: 12px 14px; background: #fff; border-radius: var(--radius); box-shadow: var(--shadow); align-items: center; } .filter-bar .filter-label { font-size: 12px; color: var(--text-soft); font-weight: 600; margin-right: 4px; } .filter-btn { cursor: pointer; border: 1px solid var(--border); background: #fff; color: var(--text-soft); padding: 6px 12px; border-radius: 16px; font-size: 12px; font-weight: 600; transition: all 0.15s ease; user-select: none; } .filter-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.08); } .filter-btn.active { color: #fff; border-color: transparent; } .filter-btn[data-cat="all"].active { background: var(--text); } .filter-btn[data-cat="p0"].active { background: var(--danger); } .filter-btn[data-cat="decide"].active { background: var(--info); } .filter-btn[data-cat="doing"].active { background: var(--accent); } .filter-btn[data-cat="server"].active { background: var(--danger); } .filter-btn[data-cat="code"].active { background: #8B5CF6; } .filter-btn[data-cat="security"].active { background: #DC2626; } .filter-btn[data-cat="history"].active { background: #64748B; } .filter-btn .badge-count { display: inline-block; margin-left: 6px; padding: 1px 6px; background: rgba(0,0,0,0.08); border-radius: 8px; font-size: 10px; } .filter-btn.active .badge-count { background: rgba(255,255,255,0.25); } .hidden { display: none !important; } """ # ============================================================ # HTML 渲染 # ============================================================ def render_html(data: dict) -> str: fc = data["filter_counts"] # P0 表格 p0_rows_html = "" for r in data["p0_rows"]: p0_rows_html += f""" {r['id']} {r['name']} {r['category']} {r['desc']} """ # 等决策表格 decide_rows_html = "" for r in data["decide_rows"]: decide_rows_html += f""" {r['id']} {r['name']} {r['duration']} {r['owner']} """ # 进行中表格 doing_rows_html = "" for r in data["doing_rows"]: doing_rows_html += f""" {r['id']} {r['name']} {r['status']} {r['desc']} """ # 服务器状态 server_rows_html = "" for r in data["server_rows"]: server_rows_html += f""" {r['name']} {r['label']} {r['desc']} """ # 代码质量 code_items_html = "\n".join( f'
  • {r["name"]}:{r["desc"]}
  • ' for r in data["code_rows"] ) # 安全 security_items_html = "\n".join( f'
  • {r["name"]}:{r["desc"]}
  • ' for r in data["security_rows"] ) # 对比/关注 history_rows_html = "" for r in data["history_rows"]: history_rows_html += f""" {r['name']} {r['category']} {r['desc']} """ # PM 立即关注 pm_items_html = "\n".join( f'
  • {item}
  • ' for item in data["pm_alert"] ) # 摘要卡片 summary_html = f"""
    {len(data['p0_rows'])}
    🔴 P0 必做
    {len(data['decide_rows'])}
    🟢 等决策
    {len(data['doing_rows'])}
    ⏳ 进行中
    {sum(1 for r in data['server_rows'] if r['status']=='fail')}
    🖥️ 服务器异常项
    """ html = f""" 早班巡检报告 · {data['date']} 06:00 · v3.0

    📋 早班巡检报告 · IT 智能服务台

    📅 {data['time_str']}(GMT+8) 🛰️ 服务器 10.90.5.110 📄 看板 {data['kanban_version']} 🤖 执行 Duckula 📦 看板源 docs/07-项目管理/项目状态看板.md
    {summary_html}
    🔍 分类筛选

    🔴 P0 必做(高优先级)

    本次巡检最高优先级阻塞 / 待修复项

    {p0_rows_html}
    编号任务类别说明

    🟢 等用户决策(阻塞项)

    均超 3 天阈值,需 PM 升级催办

    {decide_rows_html}
    编号任务阻塞时长卡点 / Owner

    ⏳ 进行中 (In Progress)

    {doing_rows_html}
    编号任务状态说明

    🖥️ 服务器运行状态

    {server_rows_html}
    组件状态实测数据

    🔍 代码规范与质量检查

      {code_items_html}

    🔐 安全检查

      {security_items_html}

    🔄 与昨日对比 / PM 关注项

    {history_rows_html}
    条目变化说明

    ⚠️ PM 立即关注

      {pm_items_html}
    📋 报告生成时间:{data['time_str']} · 看板版本 {data['kanban_version']} · Duckula(达寇拉)自动化生成
    📎 数据源:jumpserver-V2 实测 + 后端 24h 日志审计 + 源码扫描 + 容器内文件核对
    🔗 服务地址:https://itsupport.servyou.com.cn/docs/inspection/2026-08/2026-08-06-早班.html
    """ return html def main(): parser = argparse.ArgumentParser(description="Build inspection report HTML") parser.add_argument("date", help="Report date (YYYY-MM-DD)") parser.add_argument("shift", help="Shift (早班/午班/晚班)") args = parser.parse_args() year_month = args.date[:7] # YYYY-MM out_dir = DEFAULT_OUT_DIR / year_month out_dir.mkdir(parents=True, exist_ok=True) out_file = out_dir / f"{args.date}-{args.shift}.html" DATA["date"] = args.date DATA["shift"] = args.shift DATA["time_str"] = f"{args.date} 06:00" if args.shift == "早班" else f"{args.date} 14:00" html = render_html(DATA) out_file.write_text(html, encoding="utf-8") print(f"[OK] Generated: {out_file} ({len(html)} bytes)") if __name__ == "__main__": main()