# 审计 — 北京时间展示统一 + 资源恢复 Telegram 去重（2026-06-08）

**Changelog**:
- `docs/changelog/2026-06-08-beijing-time-display-unified.md`
- `docs/changelog/2026-06-08-telegram-recovery-dedup-timezone.md`

**触发原因**: Bug 修复（恢复通知秒级重复、Telegram/UI 时间非北京时间）+ 展示层时区统一

## 审计范围

| 文件 | 行数 | 状态 |
|------|------|------|
| `server/utils/display_time.py` | 27 | ☑ 已审 |
| `server/infrastructure/telegram/__init__.py` | ~320 | ☑ 已审（`_now_cn` 段） |
| `server/background/ip_allowlist_refresh.py` | ~100 | ☑ 已审（刷新时间） |
| `server/api/settings.py` | ~1570 | ☑ 已审（Telegram 测试消息） |
| `server/api/agent.py` | ~385 | ☑ 已审（恢复去重） |
| `frontend/src/utils/datetime.ts` | 85 | ☑ 已审 |
| `frontend/src/utils/status.ts` | 36 | ☑ 已审 |
| `frontend/src/composables/push/labels.ts` | 68 | ☑ 已审 |
| `frontend/src/composables/useWebSocket.ts` | ~190 | ☑ 已审 |
| `frontend/src/pages/AuditPage.vue` | ~196 | ☑ 已审 |
| `frontend/src/pages/AlertsPage.vue` | ~190 | ☑ 已审 |
| `frontend/src/pages/CommandsPage.vue` | ~393 | ☑ 已审 |
| `frontend/src/pages/RetriesPage.vue` | ~180 | ☑ 已审 |
| `frontend/src/pages/SchedulesPage.vue` | ~533 | ☑ 已审 |
| `frontend/src/pages/ScriptsPage.vue` | ~1315 | ☑ 已审（时间列） |
| `frontend/src/pages/ScriptRunsPage.vue` | ~681 | ☑ 已审 |
| `frontend/src/pages/ServersPage.vue` | ~1400 | ☑ 已审（导出文件名） |
| `frontend/src/components/credentials/CredentialsManager.vue` | ~436 | ☑ 已审 |
| `frontend/src/components/servers/ServerInlineDetail.vue` | ~158 | ☑ 已审 |
| `tests/test_display_time.py` | 18 | ☑ 已审 |
| `tests/test_telegram_time_format.py` | 20 | ☑ 已审 |
| `tests/test_agent_recovery_dedup.py` | 42 | ☑ 已审 |

## 审计8步结果

### Step 1: 登记 ✅

两项用户反馈：① Telegram 恢复时间显示 UTC；② 同一指标 1 秒内连发多条恢复通知。

### Step 2: 全文 Read ✅

上述文件全文走读；`broadcast_recovery` / `websocket.py` 交叉确认恢复无 Telegram cooldown 设计意图（恢复应即时推送，根因在 Redis 成员重复）。

### Step 3: 规则扫描 H

| ID | 规则 | 命中点 | 初判 |
|----|------|--------|------|
| H1 | 静默吞错 | `agent.py` Redis 告警跟踪 `except: debug` | SAFE — 已有模式，告警主路径仍广播 |
| H2 | 时区一致性 | `parseApiDateTime` 假定 naive = UTC | SAFE — 与 `sync_log_repo`、ORM `_utcnow` 一致 |
| H3 | 去重完整性 | Redis legacy `mem:92` 多条 | SAFE — `_detect_recovery` 按 metric 去重 + srem 全清 |
| H4 | XSS / 注入 | 前端 `formatDateTimeBeijing` 仅格式化 | SAFE — Vue 文本插值，无 `v-html` |
| H5 | 密钥泄露 | `display_time` / `datetime.ts` | SAFE — 无凭据 |
| H6 | CUD 审计 | 无新 CUD 路径 | N/A |
| H7 | 多 worker | 恢复 Telegram 仍无 Redis 锁 | LOW — 单心跳内重复已根治；跨 worker 同毫秒双 POST 概率极低，接受 |

### Step 4: Closure表

| H | 判定 | 依据 |
|---|------|------|
| H1 | SAFE | 跟踪失败不影响 `broadcast_alert`；日志可见 |
| H2 | SAFE | DB/API 约定 UTC；文档与 changelog 已说明 |
| H3 | SAFE | 单测 `test_detect_recovery_dedupes_legacy_metric_value_entries` |
| H4 | SAFE | 展示层数值格式化 |
| H5 | SAFE | — |
| H6 | N/A | — |
| H7 | ACCEPT | 根因修复；若后续再现可加 `recovery_sent:{id}:{metric}` SETNX |

### Step 5: 入口表

| 入口 | 变更 |
|------|------|
| `POST /api/agent/heartbeat` | Redis `alerts:{id}` 成员格式、恢复检测 |
| Telegram 出站 | `_now_cn` → `format_beijing_now` |
| `POST .../telegram/test` | 测试消息时间 |
| 前端各列表页 | 只读展示，无新 API |

### Step 6: 输入→Sink

- Agent `system_info` → 阈值比较 → Redis set / `broadcast_recovery` → Telegram：无新 sink；去重降低 Telegram 频率。
- API `created_at` 字符串 → `parseApiDateTime` → `Intl` 北京时间：只读展示。

### Step 7: 归类

```
display_time.py          27行  2H  0 FINDING
agent.py (recovery)      ~50行  3H  0 FINDING
datetime.ts              85行  2H  0 FINDING
前端 10 文件             ~40行  1H  0 FINDING
测试 3 文件              80行  0H  0 FINDING
──────────────────────────────────────────
总计                     8H   0 FINDING（1 ACCEPT）
```

### Step 8: DoD ✅

- [x] 恢复通知同一指标单次心跳最多 1 条 Telegram
- [x] Telegram / 审计页 / 告警页等绝对时间为北京时间
- [x] DB 仍存 UTC，未改调度/漂移计算
- [x] `pytest` 7 项通过（`test_display_time` / `test_telegram_time_format` / `test_agent_recovery_dedup`）
- [x] `npm run type-check` 通过
- [ ] 生产部署与浏览器终验（待用户批准 deploy）

## FINDING 列表

无。H7 为风险接受项，非 FINDING。

## 结论

☑ **审计通过，0 FINDING，可进入部署门控**（需 commit + `pre_deploy_check.sh` + 前端 build）
