Files
wecom_it_smart_desk/docs/07-项目管理/巡检报告/2026-08/2026-08-05-早班.html
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

516 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='14' font-size='14'>📋</text></svg>">
<title>早班巡检报告 · 2026-08-05 06:00 · v2.5</title>
<style>
: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: 16px;
border-radius: var(--radius);
box-shadow: var(--shadow);
text-align: center;
}
.summary-card .num { font-size: 30px; font-weight: 700; line-height: 1; }
.summary-card .label { font-size: 12px; color: var(--text-soft); margin-top: 6px; }
.summary-card.p0 { border-top: 3px solid var(--danger); }
.summary-card.p1 { border-top: 3px solid var(--warn); }
.summary-card.decide { border-top: 3px solid var(--info); }
.summary-card.doing { border-top: 3px solid var(--accent); }
.summary-card.server { border-top: 3px solid var(--danger); }
.summary-card.info { border-top: 3px solid var(--text-soft); }
table {
width: 100%;
border-collapse: collapse;
background: #fff;
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
margin-bottom: 12px;
}
thead { background: var(--bg-soft); }
th, td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
th { font-weight: 600; color: var(--text-soft); font-size: 12px; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--accent-soft); }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.badge.p0 { background: #FFEDED; color: var(--danger); }
.badge.p1 { background: #FFF4E5; color: var(--warn); }
.badge.decide { background: #E5F6FF; color: var(--info); }
.badge.doing { background: var(--accent-soft); color: var(--accent); }
.badge.ok { background: #E7F8EF; color: var(--accent); }
.badge.warn { background: #FFF4E5; color: var(--warn); }
.badge.fail { background: #FFEDED; color: var(--danger); }
.alert {
background: #FFF8E5;
border-left: 4px solid var(--warn);
padding: 12px 16px;
border-radius: 4px;
margin: 12px 0;
font-size: 13px;
}
.alert.danger {
background: #FFEDED;
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;
}
code {
background: var(--bg-soft);
padding: 2px 6px;
border-radius: 3px;
font-family: "Cascadia Code", Consolas, monospace;
font-size: 12px;
}
ul.condensed { list-style: none; padding: 0; }
ul.condensed li {
padding: 6px 0;
border-bottom: 1px dashed var(--border);
}
ul.condensed li:last-child { border-bottom: none; }
/* ===== 分类筛选按钮 ===== */
.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="p1"].active { background: var(--warn); }
.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; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>📋 早班巡检报告 · IT 智能服务台</h1>
<div class="meta">
<span>📅 <strong>2026-08-05 06:00</strong>GMT+8</span>
<span>🛰️ 服务器 <strong>10.90.5.110</strong></span>
<span>📄 看板 <strong>v1.7</strong> (08-03)</span>
<span>🤖 执行 <strong>Duckula</strong></span>
</div>
</header>
<!-- 分类筛选 -->
<div class="filter-bar">
<span class="filter-label">🔍 分类筛选</span>
<button class="filter-btn active" data-cat="all">全部<span class="badge-count">9</span></button>
<button class="filter-btn" data-cat="p0">🔴 P0<span class="badge-count">4</span></button>
<button class="filter-btn" data-cat="p1">🟡 P1<span class="badge-count">1</span></button>
<button class="filter-btn" data-cat="decide">🟢 等决策<span class="badge-count">2</span></button>
<button class="filter-btn" data-cat="doing">⏳ 进行中<span class="badge-count">1</span></button>
<button class="filter-btn" data-cat="server">🖥️ 服务器<span class="badge-count">9</span></button>
<button class="filter-btn" data-cat="code">🔍 代码<span class="badge-count">4</span></button>
<button class="filter-btn" data-cat="security">🔐 安全<span class="badge-count">5</span></button>
<button class="filter-btn" data-cat="history">🔄 对比/关注<span class="badge-count">6</span></button>
</div>
<!-- P0 必做 -->
<h2 data-section="p0">🔴 P0 必做(高优先级)</h2>
<p class="legend">本次巡检最高优先级阻塞 / 待修复项</p>
<table>
<thead>
<tr><th>编号</th><th>任务</th><th>类别</th><th>说明</th></tr>
</thead>
<tbody>
<tr>
<td><span class="badge p0">P0-1</span></td>
<td><code>/itportal/</code> 入口 500 错误复燃</td>
<td>♻️ 历史遗留</td>
<td>看板 v1.7 标 "commit 21830d5 已修复(待部署)" 08-03;但 <strong>实际未部署</strong>。实测:<code>/itportal/</code> → 500<code>/itportal</code> → 404。根因:nginx <code>location /itportal/</code> (line 133/306) 仍指向空 dist。dist 自 2026-07-08 空 28 天</td>
</tr>
<tr>
<td><span class="badge p0">P0-2</span></td>
<td><code>sensitive_words.py</code> 13 端点无鉴权</td>
<td>♻️ 历史遗留</td>
<td>GET/POST/PUT/DELETE /sensitive-words*、/privacy-patterns*、/moderation-logs[/stats]、/moderation-config <strong>全部缺 @require_admin</strong>。08-04 已指出未治理。完全可复用 08-03 troubleshooting_templates.py 治理方案</td>
</tr>
<tr>
<td><span class="badge p0">P0-3</span></td>
<td><code>closing_service.py:467</code> datetime 时区错位</td>
<td>🆕 新增</td>
<td><code>datetime.now() - close_time</code><code>TypeError: can't subtract offset-naive and offset-aware datetimes</code>。影响 <code>POST /h5/conversations/current/reopen</code>。2026-08-03 16:08 连续 3 次 500</td>
</tr>
<tr>
<td><span class="badge p0">P0-4</span></td>
<td><code>h5_ai_task.py:1198</code> SessionLocal NoneType</td>
<td>🆕 新增</td>
<td><code>_push_asset_recommends</code><code>get_employee_profile_service()</code><code>db = SessionLocal()</code> 返回 None → <code>TypeError: 'NoneType' object is not callable</code>。影响 IT 资产推荐异步推送。2026-08-04 01:36 + 02:29 两次</td>
</tr>
</tbody>
</table>
<!-- P1 重要 -->
<h2 data-section="p1">🟡 P1 重要</h2>
<table>
<thead><tr><th>编号</th><th>任务</th><th>类别</th><th>说明</th></tr></thead>
<tbody>
<tr>
<td><span class="badge p1">P1-1</span></td>
<td>前端 console 残留 208 行</td>
<td>🆕 新增</td>
<td><code>frontend-h5/src</code> 26 文件 + <code>frontend-agent/src</code> 14 文件含 console.log/warn/debug。建议 vite 构建时 <code>terser drop_console: true</code> + 加 lint 规则</td>
</tr>
</tbody>
</table>
<!-- 等用户决策 -->
<h2 data-section="decide">🟢 等用户决策(阻塞项)</h2>
<p class="legend">均超 3 天阈值,需 PM 升级催办</p>
<table>
<thead><tr><th>编号</th><th>任务</th><th>阻塞时长</th><th>卡点 / Owner</th></tr></thead>
<tbody>
<tr>
<td><span class="badge decide">BLK-A</span></td>
<td>企微会议室 Secret</td>
<td>⚠️ 25 天(自 2026-07-11</td>
<td>需企业微信管理后台申请。Owner: 平台组</td>
</tr>
<tr>
<td><span class="badge decide">BLK-B</span></td>
<td>ITSM API 授权</td>
<td>⚠️ 25 天(自 2026-07-11</td>
<td>需向 ITSM 平台方申请 app_id/app_secret。Owner: 平台组</td>
</tr>
</tbody>
</table>
<!-- 进行中 -->
<h2 data-section="doing">⏳ 进行中 (In Progress)</h2>
<table>
<thead><tr><th>编号</th><th>任务</th><th>状态</th><th>说明</th></tr></thead>
<tbody>
<tr>
<td><span class="badge doing">#81</span></td>
<td>敏感词检测 + 语气优化</td>
<td>v1.2 待排期</td>
<td>v1.1 阶段 1 已于 2026-07-28 完成(词库入库 + 后台管理 UI + 12 配套 API)。剩余"语气优化、灰度策略细化"待 v1.2 排期</td>
</tr>
</tbody>
</table>
<!-- 服务器状态 -->
<h2 data-section="server">🖥️ 服务器运行状态</h2>
<table>
<thead><tr><th>组件</th><th>状态</th><th>实测数据</th></tr></thead>
<tbody>
<tr><td>Docker 容器</td><td><span class="badge ok">正常</span></td><td>5/5 healthynginx (Up 15h) / backend (Up 35h) / redis (Up 2w) / postgres (Up 3w) / neo4j (Up 3w)。无 Exited/Restarting/Unhealthy</td></tr>
<tr><td>后端 health</td><td><span class="badge ok">正常</span></td><td><code>/health</code> → HTTP 200 in 3.9ms<code>/docs</code> → 200</td></tr>
<tr><td>后端 5xx 错误</td><td><span class="badge fail">异常</span></td><td>24h 内 3 类真实 Traceback(见 P0-3 / P0-4</td></tr>
<tr><td>PostgreSQL</td><td><span class="badge ok">正常</span></td><td><code>pg_isready</code> → accepting connections</td></tr>
<tr><td>Redis</td><td><span class="badge ok">正常</span></td><td><code>redis_version:7.4.9</code> / 独立模式 + 密码认证</td></tr>
<tr><td>Nginx 配置</td><td><span class="badge ok">正常</span></td><td><code>nginx -t</code> → syntax OK</td></tr>
<tr><td>前端入口</td><td><span class="badge fail">异常</span></td><td><code>/itportal/</code> <strong>500</strong><code>/itportal</code> <strong>404</strong><code>/itdesk</code><code>/itagent</code><code>/itadmin</code> 200 OK</td></tr>
<tr><td>磁盘 / 内存</td><td><span class="badge ok">正常</span></td><td>130G 可用(13% 用)/ 15.7G→3.5G used / 负载 0.99 / 已运行 53 天</td></tr>
<tr><td>日志</td><td><span class="badge ok">正常</span></td><td><code>wecom-it-desk.log</code> 9.1M (34294 行)5 个轮转日志各 21M,轮转正常</td></tr>
</tbody>
</table>
<!-- 代码规范与质量 -->
<h2 data-section="code">🔍 代码规范与质量检查</h2>
<table>
<thead><tr><th>问题</th><th>位置</th><th>建议</th></tr></thead>
<tbody>
<tr>
<td>console.log 残留 208 行 / 40 文件</td>
<td><code>frontend-h5/src</code>(26) + <code>frontend-agent/src</code>(14)</td>
<td>加 lint 规则禁用 production console;或构建时 <code>terser drop_console: true</code></td>
</tr>
<tr>
<td><code>datetime.now()</code> 混用 naive/aware</td>
<td><code>closing_service.py</code>: 7 处 + SQL 比较 3 处</td>
<td>引入项目内统一工具函数 <code>utcnow()</code> 替代 <code>datetime.now()</code>DB 列 <code>TIMESTAMPTZ</code> 强制 aware 语义</td>
</tr>
<tr>
<td>历史残留目录</td>
<td><code>/opt/wecom-it-desk/app.bak-evening</code><code>dist-backup-2026-06-21</code><code>backups/</code><code>_backups/</code></td>
<td>清理以减少运维误判(避免"宿主残留目录误导校验"陷阱)</td>
</tr>
<tr>
<td>后端无 print() 调试残留</td>
<td><code>src/backend/app/**</code></td>
<td>grep 排除后通过</td>
</tr>
</tbody>
</table>
<!-- 安全检查 -->
<h2 data-section="security">🔐 安全检查</h2>
<table>
<thead><tr><th>风险</th><th>位置</th><th>修复</th></tr></thead>
<tbody>
<tr>
<td><span class="badge p0">P0</span> 13 端点全部无鉴权</td>
<td><code>src/backend/app/api/admin/sensitive_words.py</code></td>
<td>全部加 <code>Depends(get_current_user)</code> + <code>@require_admin</code>,参照 troubleshooting_templates.py 治理</td>
</tr>
<tr>
<td><span class="badge ok">OK</span> CORS 配置合理</td>
<td><code>main.py</code> CORSMiddleware</td>
<td><code>allow_origins=settings.cors_origins_list</code> / <code>allow_credentials=True</code> / methods 限 5 种 / headers 仅 3 个。需验证 cors_origins_list 配置源未放开 *</td>
</tr>
<tr>
<td><span class="badge ok">OK</span> 前端 <code>src/**</code> 无硬编码密钥</td>
<td>grep <code>api_key/access_token/app_secret</code></td>
<td>仅命中 <code>node_modules</code>,通过</td>
</tr>
<tr>
<td><span class="badge warn">建议</span> 依赖过期</td>
<td><code>cryptography==42.0.8</code><code>pydantic==2.7.4</code></td>
<td>升至 cryptography ≥ 43.x / pydantic ≥ 2.8.x 并跑回归</td>
</tr>
<tr>
<td><span class="badge ok">OK</span> 日志未见敏感信息</td>
<td>后端日志扫描</td>
<td>仅含 errcode、code masked、stack,通过</td>
</tr>
</tbody>
</table>
<!-- PM 立即关注 -->
<h2 data-section="history">⚠️ PM 立即关注</h2>
<div class="alert danger">
<strong>1. 【最关键】/itportal/ 500 错误已持续 28 天</strong>
看板 v1.7 标 "commit 21830d5 已修复(待部署)",但 jumpserver 实测仍未部署。建议:① 立即部署 commit 21830d5<code>docker compose up -d --force-recreate nginx</code>);② 或评估 /itportal/ 入口是否还需保留(看板记忆未提,按设计 #81 PRD 应走 /itdesk/ H5 入口)
</div>
<div class="alert danger">
<strong>2. sensitive_words.py 13 端点零鉴权(同昨日巡检结论,未治理)</strong>
与 08-03 troubleshooting_templates.py 5 端点是同类问题。建议立即复制其治理方案:GET → get_current_userPOST/PUT/DELETE → @require_admin
</div>
<div class="alert danger">
<strong>3. 3 类后端真实 5xx 24h 内集中爆发</strong>
① closing_service.py:467 → 改 utcnow() 并保证 DB 列 aware<br>
② h5_ai_task.py:1198 → 检查为什么 get_employee_profile_service() 没拿到 db<br>
③ OAuth2 errcode=40029 凌晨 3 次 → 评估是否企微回调 code 短时过期,或客户端重复刷新
</div>
<div class="alert">
<strong>4. 2 项长期阻塞(25 天)需升级</strong>
企微会议室 Secret + ITSM API 授权已超 3 天阈值。建议 PM 主动对接平台组负责人,或调整为"降级方案 + P0 重启"
</div>
<div class="alert">
<strong>5. 看板分区缺失</strong>
v1.7 仅"进行中/待开始/暂停"三个分区,无 P0/P1/等决策正式分区,导致巡检定位困难。建议 PM 提一版 v1.8 恢复分级分区(草稿已生成:<code>docs/07-项目管理/项目状态看板.md</code> v1.8 草稿)
</div>
<!-- vs 昨日对比 -->
<h2 data-section="history">🔄 与昨日(2026-08-04)对比</h2>
<table>
<thead><tr><th>指标</th><th>08-04 巡检</th><th>08-05 巡检</th><th>变化</th></tr></thead>
<tbody>
<tr><td>容器健康</td><td>5/5 healthy</td><td>5/5 healthy</td><td><span class="badge ok">持平</span></td></tr>
<tr><td>/itportal/ 入口</td><td>🔴 500</td><td>🔴 500</td><td><span class="badge fail">未修复</span></td></tr>
<tr><td>sensitive_words 鉴权</td><td>🔴 12 端点缺失</td><td>🔴 13 端点缺失</td><td><span class="badge fail">未治理</span></td></tr>
<tr><td>5xx 错误</td><td>未列</td><td>🔴 3 类新发现</td><td><span class="badge fail">新增</span></td></tr>
<tr><td>阻塞项时长</td><td>24 天</td><td>25 天</td><td><span class="badge warn">+1 天</span></td></tr>
<tr><td>依赖版本</td><td>cryptography 42.0.8 / pydantic 2.7.4</td><td>同上</td><td><span class="badge warn">未升级</span></td></tr>
</tbody>
</table>
<!-- 相关链接 -->
<h2 data-section="history">🔗 相关链接</h2>
<ul class="condensed">
<li>📄 看板:<code>docs/07-项目管理/项目状态看板.md</code> v1.7 (08-03)</li>
<li>📋 看板 v1.8 草稿:<code>docs/07-项目管理/项目状态看板.md</code> v1.8-DRAFT(待审核)</li>
<li>📂 巡检报告目录:<code>docs/07-项目管理/巡检报告/</code></li>
<li>🤖 自动化记录:<code>.workbuddy/automations/automation-1782986180887/memory.md</code></li>
<li>📦 工作记忆:<code>.workbuddy/memory/MEMORY.md</code> + <code>YYYY-MM-DD.md</code></li>
</ul>
<footer>
报告生成时间 2026-08-05 06:00 (GMT+8) · 自动化任务 ID: automation-1782986180887 · 执行者: Duckula 🦆<br>
下次巡检:2026-09:00(早班) · 数据源: jumpserver-V2 实测 + 本地代码扫描
</footer>
</div>
<script>
// ===== 版本标识(用于浏览器识别新版本)=====
console.log('%c📋 早班巡检报告 v2.5 (2026-08-05) - 修复 .hidden CSS 选择器', 'color:#07C160;font-weight:bold');
console.log('如看不到此日志,请按 Ctrl+Shift+R 强刷');
// ===== 分类筛选交互 =====
(function() {
const h2s = Array.from(document.querySelectorAll('h2[data-section]'));
// 给每个 h2 后到下一个 h2(或 footer)前的兄弟节点打上 section 标记
h2s.forEach((h2) => {
const cat = h2.dataset.section;
let sib = h2.nextElementSibling;
while (sib && sib.tagName !== 'H2' && sib.tagName !== 'FOOTER' && sib.tagName !== 'SCRIPT') {
sib.dataset.sectionGroup = cat;
sib = sib.nextElementSibling;
}
});
function filter(cat) {
// 切换 h2 显隐
h2s.forEach((h2) => {
const show = (cat === 'all' || h2.dataset.section === cat);
h2.classList.toggle('hidden', !show);
});
// 切换 h2 下的兄弟节点显隐
document.querySelectorAll('[data-section-group]').forEach((el) => {
const show = (cat === 'all' || el.dataset.sectionGroup === cat);
el.classList.toggle('hidden', !show);
});
// 切换按钮 active 状态
document.querySelectorAll('.filter-btn').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.cat === cat);
});
// 持久化到 URL hash,方便分享链接(不滚动,保持当前位置)
history.replaceState(null, '', cat === 'all' ? location.pathname : '#' + cat);
}
// 绑定按钮点击
document.querySelectorAll('.filter-btn').forEach((btn) => {
btn.addEventListener('click', () => filter(btn.dataset.cat));
});
// 初始化:从 URL hash 恢复筛选状态
const initCat = (location.hash || '#all').slice(1);
if (['all', 'overview', 'p0', 'p1', 'decide', 'doing', 'server', 'code', 'security', 'history'].includes(initCat)) {
filter(initCat);
}
// 键盘快捷键:数字 1-9 切换分类
const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
const map = ['p0', 'p1', 'decide', 'doing', 'server', 'code', 'security', 'history', 'overview', 'all'];
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
const idx = keys.indexOf(e.key);
if (idx >= 0 && map[idx]) {
filter(map[idx]);
}
});
})();
</script>
</body>
</html>