[{"data":1,"prerenderedAt":1933},["ShallowReactive",2],{"\u002F2026-04-17-30agent":3,"\u002F2026-04-17-30agent-rel":453},{"id":4,"title":5,"body":6,"column":437,"date":438,"description":12,"extension":439,"hero_image":440,"meta":441,"navigation":442,"path":443,"seo":444,"series_id":440,"severity":440,"stem":445,"summary":446,"tags":447,"__hash__":452},"posts\u002F2026-04-17-30个自定义agent什么时候值得单独造.md","造到第 30 个 agent，我开始怀疑该不该造",{"type":7,"value":8,"toc":426},"minimark",[9,13,21,25,30,38,44,47,77,80,94,97,101,107,114,134,141,144,148,151,157,177,182,218,224,250,256,296,303,306,309,312,347,350,353,364,367,370,373,399,402,405,419,422],[10,11,12],"p",{},"我的 Claude agent 定义库里现在有 30 份。reviewer 配对给了每种编程语言；流程 agent 分了规划、设计、测试、审查五个阶段；还有 5 个领域特化（数据库、医疗、性能等）。问题是：多少才够？什么时候不该造新的？",[10,14,15,16,20],{},"答案不是\"能不能造\"，而是",[17,18,19],"strong",{},"约束密度","。",[22,23,24],"h2",{"id":24},"三类成立条件",[26,27,29],"h3",{"id":28},"_1-语言成对的-reviewer-和-build-resolver最明显成立","1. 语言成对的 reviewer 和 build-resolver——最明显成立",[10,31,32,33,37],{},"TypeScript、Rust、Go、Java、Python、C++、Kotlin……每种语言都配一对。typescript-reviewer 关注类型安全、异步正确、XSS 防护；rust-build-resolver 关注借用检查器、生命周期、",[34,35,36],"code",{},"cargo"," 依赖。",[10,39,40,41,20],{},"为什么分离？因为 ",[17,42,43],{},"error pattern 真的不一样",[10,45,46],{},"typescript-reviewer 的 HIGH 检查点包括：",[48,49,50,58,64,74],"ul",{},[51,52,53,54,57],"li",{},"非空断言滥用（",[34,55,56],{},"value!"," 后没有前置守卫）",[51,59,60,63],{},[34,61,62],{},"as"," 类型转换绕过检查",[51,65,66,69,70,73],{},[34,67,68],{},"forEach"," 里用 ",[34,71,72],{},"async","（不会等待）",[51,75,76],{},"宽松的 TypeScript 编译器设置",[10,78,79],{},"这些对 Rust 没有意义。Rust 则需要：",[48,81,82,85,88,91],{},[51,83,84],{},"借用冲突（immutable 活跃时不能 mutable）",[51,86,87],{},"生命周期边界太短",[51,89,90],{},"从引用后移出所有权",[51,92,93],{},"trait 实现不匹配",[10,95,96],{},"混在一个通用 reviewer 里会产生两个后果：要么给出泛泛而谈的\"检查类型\"之类的意见，要么为了照顾所有语言而膨胀到无用。所以这类成立。",[26,98,100],{"id":99},"_2-流程-agent思考方式必须隔离","2. 流程 agent——思考方式必须隔离",[10,102,103,104,20],{},"planner、architect、tdd-guide、code-reviewer、security-reviewer 分别对应的是不同的",[17,105,106],{},"思维模式",[10,108,109,110,113],{},"planner 的工作是",[17,111,112],{},"规模化","。它把一个模糊的需求分解成有依赖关系的、分阶段的步骤。格式包括：",[48,115,116,119,122,125,128,131],{},[51,117,118],{},"Requirements 列表",[51,120,121],{},"Architecture Changes 清单",[51,123,124],{},"多个 Phase，每个 Phase 有多个步骤",[51,126,127],{},"每步都标注 Action、Why、Dependencies、Risk",[51,129,130],{},"Testing Strategy",[51,132,133],{},"Success Criteria",[10,135,136,137,140],{},"code-reviewer 的工作是",[17,138,139],{},"细节校验","。它逐行读代码，按优先级分类问题（CRITICAL \u002F HIGH \u002F MEDIUM \u002F LOW），给出代码片段和修复建议。",[10,142,143],{},"如果混在一个 agent 里，它们会互相干扰：规划时陷入代码细节，审查时又开始重新规划。分离后，planner 可以大局思考而不被小语法问题打断，reviewer 可以专注代码质量而不被\"这个步骤设计对吗\"的大问题分神。",[26,145,147],{"id":146},"_3-领域-agent条件性成立必须约束足够密","3. 领域 agent——条件性成立，必须约束足够密",[10,149,150],{},"database-reviewer 是这类的好例子。它不只是\"你是 PostgreSQL 专家\"，而是定义了：",[10,152,153,156],{},[17,154,155],{},"核心职责","：",[48,158,159,162,165,168,171,174],{},[51,160,161],{},"Query Performance",[51,163,164],{},"Schema Design",[51,166,167],{},"Security & RLS",[51,169,170],{},"Connection Management",[51,172,173],{},"Concurrency",[51,175,176],{},"Monitoring",[10,178,179,156],{},[17,180,181],{},"诊断命令",[183,184,189],"pre",{"className":185,"code":186,"language":187,"meta":188,"style":188},"language-bash shiki shiki-themes github-light github-dark","psql -c \"SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;\"\npsql -c \"SELECT relname, pg_size_pretty(...) FROM pg_stat_user_tables ORDER BY ...;\"\n","bash","",[34,190,191,208],{"__ignoreMap":188},[192,193,196,200,204],"span",{"class":194,"line":195},"line",1,[192,197,199],{"class":198},"sScJk","psql",[192,201,203],{"class":202},"sj4cs"," -c",[192,205,207],{"class":206},"sZZnC"," \"SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;\"\n",[192,209,211,213,215],{"class":194,"line":210},2,[192,212,199],{"class":198},[192,214,203],{"class":202},[192,216,217],{"class":206}," \"SELECT relname, pg_size_pretty(...) FROM pg_stat_user_tables ORDER BY ...;\"\n",[10,219,220,223],{},[17,221,222],{},"关键原则","（不只是建议，是规则）：",[48,225,226,229,232,235,238,241,244,247],{},[51,227,228],{},"索引外键——没有例外",[51,230,231],{},"部分索引用于软删除",[51,233,234],{},"覆盖索引避免表查",[51,236,237],{},"队列用 SKIP LOCKED",[51,239,240],{},"游标分页替代 OFFSET",[51,242,243],{},"批量插入，禁止循环内逐行插入",[51,245,246],{},"事务保持短",[51,248,249],{},"锁顺序一致",[10,251,252,255],{},[17,253,254],{},"反模式检查清单","（10+ 条）：",[48,257,258,264,275,281,284,287,293],{},[51,259,260,263],{},[34,261,262],{},"SELECT *"," 在生产代码",[51,265,266,267,270,271,274],{},"ID 用 ",[34,268,269],{},"int","（应该 ",[34,272,273],{},"bigint","）",[51,276,277,280],{},[34,278,279],{},"timestamp"," 无时区",[51,282,283],{},"随机 UUID 做主键",[51,285,286],{},"未参数化查询",[51,288,289,292],{},[34,290,291],{},"GRANT ALL"," 给应用用户",[51,294,295],{},"RLS 策略每行调函数",[10,297,298,299,302],{},"这些",[17,300,301],{},"不是建议","，而是具体的、可验证的、在 PostgreSQL 世界里遵循的法则。database-reviewer 存在是因为它编码了这个领域的专有知识。",[10,304,305],{},"对比一个不该独立存在的例子：假设有个 \"api-design-reviewer\" agent，系统提示只有\"检查 API 设计\"加几条通用的\"RESTful 最佳实践\"。没有针对这个项目的路由规范、没有特定的错误响应格式检查、没有该业务领域的约束……那个 agent 其实不该独立——它只是拿 Claude 通用能力换了个称呼，增加了调度成本却没有添加任何专有约束。",[22,307,308],{"id":308},"过度设计的信号",[10,310,311],{},"一个 agent 定义如果满足以下条件，很可能不该独立存在：",[313,314,315,321,327,333],"ol",{},[51,316,317,320],{},[17,318,319],{},"系统提示是\"你是 X 专家\"加 2-3 条泛泛要求","。真正有价值的 agent 定义会包含诊断步骤、命令、检查清单。",[51,322,323,326],{},[17,324,325],{},"没有输出格式或流程","。强 agent 会规定\"输出必须包含这些节，用这个表格格式\"之类的约束。没有就说明它没有对问题空间的深入理解。",[51,328,329,332],{},[17,330,331],{},"check list 是通用的，不是该领域特有的","。code-reviewer 的\"检查大函数\"对所有语言都有意义，但 typescript-reviewer 还会检查特定的 async 陷阱。如果一个 agent 的检查清单完全是通用的，它不该独立。",[51,334,335,338,339,342,343,346],{},[17,336,337],{},"没有诊断命令","。database-reviewer 给出了 ",[34,340,341],{},"pg_stat_statements"," 查询、",[34,344,345],{},"EXPLAIN ANALYZE"," 步骤。如果 agent 定义里全是文字建议没有可执行的步骤，它没有足够的专有约束。",[22,348,349],{"id":349},"调度的代价",[10,351,352],{},"多 agent 不是免费的。每调用一个 agent 意味着：",[48,354,355,358,361],{},[51,356,357],{},"上下文切换",[51,359,360],{},"一次新的 LLM 推理（可能是更贵的模型）",[51,362,363],{},"并行时的并发管理",[10,365,366],{},"所以不值得的 agent 必须合并。比如\"README-updater\"和\"changelog-updater\"如果约束差不多，就应该合并成单一的 doc-updater。实际上 doc-updater 确实存在，而且它的约束包括：输出结构（docs\u002FCODEMAPS\u002F 的特定文件组织）、代码地图格式、验证步骤。这足以支撑独立存在。",[22,368,369],{"id":369},"实际计数",[10,371,372],{},"30 份 agent 的分布：",[48,374,375,381,387,393],{},[51,376,377,380],{},[17,378,379],{},"语言 reviewer\u002Fbuild-resolver 对","：8 对（TypeScript\u002FRust\u002FGo\u002FJava\u002FPython\u002FC++\u002FKotlin\u002FPyTorch），共 16 份",[51,382,383,386],{},[17,384,385],{},"流程 agent","：planner、architect、tdd-guide、code-reviewer、security-reviewer，共 5 份",[51,388,389,392],{},[17,390,391],{},"领域特化","：database-reviewer、healthcare-reviewer、performance-optimizer，共 3 份",[51,394,395,398],{},[17,396,397],{},"功能专用","：doc-updater、e2e-runner、refactor-cleaner、chief-of-staff、loop-operator、harness-optimizer、docs-lookup，共 7 份",[10,400,401],{},"前三类的成立都有明确的约束理由。第四类是工具性的，每个都解决具体问题（文档更新、E2E 测试、死代码清理）。",[10,403,404],{},"反过来想，如果要造第 31 个 agent，需要问：",[48,406,407,410,413,416],{},[51,408,409],{},"它处理的问题空间有专有模式吗？",[51,411,412],{},"能列出 5 条以上该领域特有的规则或检查点吗？",[51,414,415],{},"有具体的诊断或验证步骤吗？",[51,417,418],{},"还是只是\"我是 X 专家\"加通用建议？",[10,420,421],{},"只要有一条答\"不\"，那就应该把这个职责并入现有 agent，而不是新造。",[423,424,425],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":188,"searchDepth":210,"depth":210,"links":427},[428,434,435,436],{"id":24,"depth":210,"text":24,"children":429},[430,432,433],{"id":28,"depth":431,"text":29},3,{"id":99,"depth":431,"text":100},{"id":146,"depth":431,"text":147},{"id":308,"depth":210,"text":308},{"id":349,"depth":210,"text":349},{"id":369,"depth":210,"text":369},"工程手记","2026-04-17","md",null,{},true,"\u002F2026-04-17-30agent",{"title":5,"description":12},"2026-04-17-30个自定义agent什么时候值得单独造","独立 agent 的价值来自它携带的专有约束量，不是职责名称听起来有多不同。",[448,449,450,451],"agent","Claude","系统设计","编排","KvP3OfLg88tMxT95J-2wcPa_uPo7FY7YDnwFt34AsuM",[454,651,1008],{"id":455,"title":456,"body":457,"column":437,"date":636,"description":637,"extension":439,"hero_image":440,"meta":638,"navigation":442,"path":639,"seo":640,"series_id":440,"severity":440,"stem":641,"summary":642,"tags":643,"__hash__":650},"posts\u002F2026-09-10-Ai项目群的工程化分水岭.md","AI 项目做多以后，真正难的是把边界接起来",{"type":7,"value":458,"toc":625},[459,466,469,472,476,480,490,493,496,512,515,519,538,541,545,554,557,561,564,567,587,590,594,597,600,603,606,609,612,616,619,622],[10,460,461,462,465],{},"这段时间回看 ",[34,463,464],{},"\u002FUsers\u002Fxingye\u002FAi"," 下的项目，最明显的变化不是项目数量增加，而是问题的重心变了。",[10,467,468],{},"早期问题集中在“功能能不能跑起来”：语音推理、网页生成、桌面端启动、小程序页面、视频服务。进入平台化阶段以后，问题变成了另一组：任务是否可追踪，失败是否能恢复，费用是否能对账，版本是否能回滚，用户是否知道发生了什么。",[10,470,471],{},"这是一条从功能开发走向系统工程的分水岭。",[22,473,475],{"id":474},"一项目群已经形成几条清晰的产品线","一、项目群已经形成几条清晰的产品线",[26,477,479],{"id":478},"agent-与多租户平台","Agent 与多租户平台",[10,481,482,485,486,489],{},[34,483,484],{},"yun-claw"," 与 ",[34,487,488],{},"yun-claude"," 的记录显示，平台主线已经覆盖用户容器、模型接入、画布工作流、代理渠道、后台管理和运营工具。近期提交涉及画布自绘 Select、资产大图预览、代理激活码申请，以及渠道注册页的差异化展示。",[10,491,492],{},"这些功能看起来分属不同页面，底层却共享一套问题：用户身份、资源权限、异步任务和状态反馈必须保持一致。只改页面，不补状态契约，功能就会在跨页面操作时暴露断点。",[26,494,495],{"id":495},"内容生产流水线",[10,497,498,501,502,501,505,485,508,511],{},[34,499,500],{},"ai-write","、",[34,503,504],{},"instantlink-site",[34,506,507],{},"ai-auto-edit",[34,509,510],{},"ai-digital-human-workflow"," 分别覆盖写作、漫剧、自动剪辑和数字人视频。提交记录里反复出现几个关键词：任务网关、切片下载、共享卷、FFmpeg、提示词优化、失败提示和构建复现。",[10,513,514],{},"这说明内容生产已经不是单次调用模型，而是一条有中间产物的流水线。文本、图片、视频、音频和项目资产都有生命周期，任何一步的状态丢失都会让用户无法判断下一步该做什么。",[26,516,518],{"id":517},"交付网关与桌面端","交付、网关与桌面端",[10,520,521,501,524,501,527,501,530,533,534,537],{},[34,522,523],{},"newCodex",[34,525,526],{},"new-openclaw",[34,528,529],{},"openclaw",[34,531,532],{},"服务器管理"," 和 ",[34,535,536],{},"SSHTS"," 的记录，集中在更新、恢复、远程诊断、单实例生命周期和离线环境上。这里的核心指标不是“新版本发布了没有”，而是安装失败能否定位、更新中断能否恢复、客户机器能否留下可审计证据。",[10,539,540],{},"这也是为什么桌面端工程会逐渐靠近运维工程：安装器、后台服务、日志、配置和回滚必须一起设计。",[26,542,544],{"id":543},"网关计费与数据观察","网关、计费与数据观察",[10,546,547,485,550,553],{},[34,548,549],{},"NewAPI",[34,551,552],{},"new-api"," 的近期记录包括按模型统计 Token 消耗、扣费量、时间粒度聚合、昨日活跃用户和订单搜索。网关不只是转发请求，它还承担了模型选择、消耗记录、后台观察和问题追踪。",[10,555,556],{},"一旦计费数据进入用户体验，接口成功与业务成功就不再是同一个概念。请求返回 200，只能说明一次网络调用完成；扣费、产物落盘和任务终态是否一致，需要单独核对。",[22,558,560],{"id":559},"二git-记录里最有价值的变化是边界被写出来了","二、Git 记录里最有价值的变化，是“边界被写出来了”",[10,562,563],{},"多个项目的文档都在补同一类内容：部署手册、验收清单、测试报告、故障复盘、模型环境快照和用户协议。",[10,565,566],{},"这类文档的价值不在于把代码再讲一遍，而在于把系统边界写清楚：",[48,568,569,572,575,578,581,584],{},[51,570,571],{},"输入是什么，哪些字段必须存在；",[51,573,574],{},"状态有哪些，什么条件才能进入下一状态；",[51,576,577],{},"外部服务超时后，如何判断请求是否已经执行；",[51,579,580],{},"产物写入失败时，是否允许重试；",[51,582,583],{},"更新中断后，哪个版本可以安全恢复；",[51,585,586],{},"用户看到的成功，是否和后台记录的成功一致。",[10,588,589],{},"当这些问题没有答案时，系统只能靠人工经验维持。一旦项目数量增加，经验就会变成不可复制的隐性依赖。",[22,591,593],{"id":592},"三下一阶段的重点不是再加一个入口","三、下一阶段的重点不是再加一个入口",[10,595,596],{},"从现有项目的提交和文档可以看到，下一阶段更值得投入的是四个基础能力。",[10,598,599],{},"第一，统一任务状态。不同项目都在处理异步任务，但状态命名、失败原因和重试策略不应各自发明。",[10,601,602],{},"第二，统一产物账本。一个任务产生了哪些文件、消耗了哪些资源、是否对用户可见，需要能从提交到终态完整追踪。",[10,604,605],{},"第三，统一故障出口。401、超时、部分成功、未知执行和版本回滚，应该有明确的用户反馈与后台证据。",[10,607,608],{},"第四，统一发布验收。构建通过不代表线上可用，应该把健康检查、代理路径、真实端口、关键日志和浏览器操作放到同一份验收清单里。",[10,610,611],{},"项目多以后，工程能力不再体现在“能写多少功能”，而体现在“能否让不同功能遵守同一套边界”。",[22,613,615],{"id":614},"四我会保留的判断","四、我会保留的判断",[10,617,618],{},"AI 项目最容易被低估的部分，不是模型调用，而是模型调用之后的系统。",[10,620,621],{},"生成只是起点。身份、任务、费用、存储、交付、恢复和反馈，决定了一个功能能不能进入真实使用。",[10,623,624],{},"当 Git 提交开始频繁出现“补状态”“补恢复”“补验收”“补审计”时，这不是工程变慢了，而是产品从演示阶段进入了可运营阶段。",{"title":188,"searchDepth":210,"depth":210,"links":626},[627,633,634,635],{"id":474,"depth":210,"text":475,"children":628},[629,630,631,632],{"id":478,"depth":431,"text":479},{"id":495,"depth":431,"text":495},{"id":517,"depth":431,"text":518},{"id":543,"depth":431,"text":544},{"id":559,"depth":210,"text":560},{"id":592,"depth":210,"text":593},{"id":614,"depth":210,"text":615},"2026-09-10","这段时间回看 \u002FUsers\u002Fxingye\u002FAi 下的项目，最明显的变化不是项目数量增加，而是问题的重心变了。",{},"\u002F2026-09-10-ai",{"title":456,"description":637},"2026-09-10-Ai项目群的工程化分水岭","从多个 AI 项目的 Git 记录和设计文档看，工程难点已经从“能不能生成”转向任务、计费、交付、恢复与用户反馈能否形成闭环。",[644,645,646,647,648,649],"AI 平台","Agent","内容生产","网关","桌面端","工程化","DzOpOX92EsanPp8pLNTusbzBmKH3WOTf3qcaZAFmhM8",{"id":652,"title":653,"body":654,"column":437,"date":995,"description":658,"extension":439,"hero_image":440,"meta":996,"navigation":442,"path":997,"seo":998,"series_id":440,"severity":440,"stem":999,"summary":1000,"tags":1001,"__hash__":1007},"posts\u002F2026-08-15-上线前我把整个仓库审了一遍.md","上线前，我把整个仓库审了一遍",{"type":7,"value":655,"toc":984},[656,659,662,666,669,689,692,695,699,702,705,708,711,715,718,721,787,790,796,800,806,809,812,819,823,826,837,840,844,847,855,858,871,874,877,881,884,891,897,900,904,907,912,929,934,948,951,954,958,961,981],[10,657,658],{},"上线前做了一次全量静态审查。",[10,660,661],{},"本文只讲方法和结论分布。审查报告本身不入库，也不在这里复述——那份报告带内网拓扑、密钥存放位置和尚未修复问题的文件行号证据，公开它就等于公开一份攻击者的地图。",[22,663,665],{"id":664},"一审查怎么进行","一、审查怎么进行",[10,667,668],{},"三个部分：",[313,670,671,677,683],{},[51,672,673,676],{},[17,674,675],{},"人工列清单。"," 先按「一个系统可能从哪些地方被打穿」列出检查面，而不是按目录扫。",[51,678,679,682],{},[17,680,681],{},"四个并行只读探查代理。"," 每个代理负责一组检查面，只读，不允许改任何文件。",[51,684,685,688],{},[17,686,687],{},"关键证据人工逐条复核。"," 代理解释现象可以，但「这条算不算风险、算多重」由人定。",[10,690,691],{},"第三步是必要的。代理很容易把「写法不理想」报成「高危」，也很容易漏掉需要跨文件才能看出的问题。它适合做的是穷举和取证，不适合定级。",[10,693,694],{},"审查方式记为：静态审查（人工 + 4 个并行只读探查代理）+ 关键证据人工逐条复核。",[22,696,698],{"id":697},"二只读约束的作用","二、只读约束的作用",[10,700,701],{},"让探查代理只能读，有两个好处。",[10,703,704],{},"一是没有副作用。审查过程中仓库保持原样，报告的结论可以对着同一个 commit 复现。",[10,706,707],{},"二是省掉了「代理顺手改一下」的干扰。审查和修改混在一起，会出现「报告说有问题的地方已经被改过」的状态，后续复核就无法对着证据走。",[10,709,710],{},"修改是审查之后单独一轮的事。",[22,712,714],{"id":713},"三20-条与四个级别","三、20 条与四个级别",[10,716,717],{},"产出 20 条风险，编号 R01 到 R20。每一条有：位置、触发条件、影响、修复优先级。",[10,719,720],{},"按修复优先级分四档：",[722,723,724,740],"table",{},[725,726,727],"thead",{},[728,729,730,734,737],"tr",{},[731,732,733],"th",{},"优先级",[731,735,736],{},"条数",[731,738,739],{},"排期",[741,742,743,755,766,777],"tbody",{},[728,744,745,749,752],{},[746,747,748],"td",{},"P0",[746,750,751],{},"2",[746,753,754],{},"本周内",[728,756,757,760,763],{},[746,758,759],{},"P1",[746,761,762],{},"6",[746,764,765],{},"两周内",[728,767,768,771,774],{},[746,769,770],{},"P2",[746,772,773],{},"5",[746,775,776],{},"一个月内",[728,778,779,782,785],{},[746,780,781],{},"P3",[746,783,784],{},"7",[746,786,776],{},[10,788,789],{},"另外有一个独立的「严重级别」列，按严重 \u002F 高 \u002F 中 \u002F 低分：1 \u002F 4 \u002F 7 \u002F 8。两列不对应是有意的——「有多严重」和「该多快修」是两件事。一条严重但需要停机才能修的问题，排期上反而不能放在本周。",[10,791,792,793,20],{},"整体评级：",[17,794,795],{},"高",[22,797,799],{"id":798},"四结论里最值得记的一句","四、结论里最值得记的一句",[801,802,803],"blockquote",{},[10,804,805],{},"未发现 SQL 注入、越权与 IDOR、存储型 XSS 等直接可利用的应用漏洞。",[10,807,808],{},"这一句和「整体评级高」摆在一起看，是这个月里最值得记的一条工程结论。",[10,810,811],{},"风险分布和直觉不一致。业务代码那一侧没查出可直接利用的漏洞——那部分每天在改、每天在跑、每天有人看。问题更多集中在业务代码之外：构建、部署、配置、凭据流转、依赖引入、运行时加固。",[10,813,814,815,818],{},"这些东西的共同点是",[17,816,817],{},"平时不跑","。部署脚本一天执行几次，Dockerfile 改一次放很久，集群配置是照着文档抄的。没有日常反馈的东西，问题会一直留在那里，而且不会有人因为「它今天没出事」而去核对它。",[22,820,822],{"id":821},"五修复的排期方式","五、修复的排期方式",[10,824,825],{},"路线图按三档排：P0 本周、P1 两周内、P2\u002FP3 一个月内。",[10,827,828,829,832,833,836],{},"排期依据不是严重级别，是两件事：",[17,830,831],{},"能不能立即修","，和",[17,834,835],{},"修它会不会动到线上","。一条严重但需要停机或改基础设施配置的问题，得等一个发布窗口；一条中等但改动只在一处代码里的问题，可以马上做。P2\u002FP3 是加固项，攒到下一个版本一起走。",[10,838,839],{},"这样做的好处是排期可执行。「两个月内修完 20 条」听起来完整，实际会卡在第一条需要停机的问题上。",[22,841,843],{"id":842},"六报告为什么不入库","六、报告为什么不入库",[10,845,846],{},"仓库的忽略规则里专门有一条：",[183,848,853],{"className":849,"code":851,"language":852},[850],"language-text","# 安全审查报告：含内网拓扑、密钥存放路径与未修复漏洞的 file:line 证据，绝不入库\n","text",[34,854,851],{"__ignoreMap":188},[10,856,857],{},"三条理由，最后一条最关键：",[48,859,860,863,866],{},[51,861,862],{},"内网拓扑",[51,864,865],{},"密钥存放位置",[51,867,868],{},[17,869,870],{},"未修复漏洞的精确定位",[10,872,873],{},"前两条是常识。第三条是这类文档的特例：一份「待修复清单」在修完之前，本身就是攻击说明书。而它又必须有精确定位，否则修不动。",[10,875,876],{},"所以这份文档的流转方式和代码相反。代码进仓库、留历史；它不进仓库，修完一条删一条，修完的结论改记到提交信息里。",[22,878,880],{"id":879},"七审查之后同一天修的东西","七、审查之后，同一天修的东西",[10,882,883],{},"审查和修复放在同一天做，这是这次的一个做法。",[10,885,886,887,890],{},"当天修的一批，主题集中在一类：",[17,888,889],{},"不报错的失败","。队列投递端断线后不重连、依赖返回的包装形状读错导致判定恒为假、入队不带权重插到最前面、参数格式不对被上游全部拒收、重试链被自己的去重压短、管理操作的幂等键缺失、用户可控的文件名直接进对象存储键、唯一漏掉来源校验的 IPC handler、并发删除后返回假成功、过期验证码被复活、临时文件不清理。",[10,892,893,894,20],{},"这些已经有单独一篇记录，这里只提它们的共性：",[17,895,896],{},"都不抛异常",[10,898,899],{},"审查报告里那些「不进仓库」的条目和这些「当天修完」的条目，其实是同一批检查过出来的。区别只在于前者需要动基础设施、要等窗口，后者可以立刻改。",[22,901,903],{"id":902},"八静态审查的边界","八、静态审查的边界",[10,905,906],{},"这一轮做的是静态审查，能覆盖和不能覆盖的东西都比较明确。",[10,908,909],{},[17,910,911],{},"能覆盖：",[48,913,914,917,920,923,926],{},[51,915,916],{},"配置与依赖：镜像标签、依赖来源、加密参数、权限配置。",[51,918,919],{},"错误处理：异常的吞掉与上抛、失败路径有没有留痕。",[51,921,922],{},"凭据流转：什么地方读、什么地方写、会不会进构建产物。",[51,924,925],{},"部署清单：运行用户、资源限制、网络策略、滚动更新参数。",[51,927,928],{},"代码层面的模式：路径拼接、命令拼接、输入校验的缺失。",[10,930,931],{},[17,932,933],{},"覆盖不到：",[48,935,936,939,942,945],{},[51,937,938],{},"运行时竞态。两个请求同时到、并发删除、租约过期，这些要在真实并发下才暴露。",[51,940,941],{},"真实的流量特征。哪些入口真的被外部打到、参数实际长什么样，静态看只能推测。",[51,943,944],{},"业务逻辑漏洞。越权取数据这类问题需要理解「这个人该不该看到这条」，而业务意图不在代码里。",[51,946,947],{},"组合链。单看每一步都合规，串起来才是问题——这类要动态测试或者真实对抗。",[10,949,950],{},"第三类是最容易被静态审查漏掉的：代码里所有校验都写了，但校验的规则和业务规则不一致。",[10,952,953],{},"所以静态审查的产出不是「系统安全了」，而是「在它能看见的那一面里，还剩哪些要做」。",[22,955,957],{"id":956},"九一次审查真正留下的东西","九、一次审查真正留下的东西",[10,959,960],{},"审完当天修掉的那批，留下的是代码。报告里那些没修的，留下的是三样东西：",[313,962,963,969,975],{},[51,964,965,968],{},[17,966,967],{},"一份带精确定位和优先级的清单","，不进仓库，逐条消掉。",[51,970,971,974],{},[17,972,973],{},"一个判断","：风险集中的地方和日常注意力集中的地方不一样。业务代码天天改，反而干净；构建部署配置平时没人动。",[51,976,977,980],{},[17,978,979],{},"一次复核的习惯","：审查的结论要能对着一个 commit 复现，所以探查只读、修改单独一轮。",[10,982,983],{},"第 2 条比第 1 条重要。清单会修完，但「平时不跑的东西没人核对」这件事不会自己消失。",{"title":188,"searchDepth":210,"depth":210,"links":985},[986,987,988,989,990,991,992,993,994],{"id":664,"depth":210,"text":665},{"id":697,"depth":210,"text":698},{"id":713,"depth":210,"text":714},{"id":798,"depth":210,"text":799},{"id":821,"depth":210,"text":822},{"id":842,"depth":210,"text":843},{"id":879,"depth":210,"text":880},{"id":902,"depth":210,"text":903},{"id":956,"depth":210,"text":957},"2026-08-15",{},"\u002F2026-08-15",{"title":653,"description":658},"2026-08-15-上线前我把整个仓库审了一遍","一次上线前的静态安全审查：四个并行只读代理加人工逐条复核，产出 20 条分级风险与三档修复路线图。本文记录方法，不复述条目。",[1002,1003,1004,1005,1006],"安全审查","代码审查","风险分级","修复排期","Agent 协作","bnkC_2bMe8OfgB00HIfDUS9bdRwLUZb_WqDBvXtq__M",{"id":1009,"title":1010,"body":1011,"column":437,"date":1919,"description":1015,"extension":439,"hero_image":440,"meta":1920,"navigation":442,"path":1921,"seo":1922,"series_id":440,"severity":440,"stem":1923,"summary":1924,"tags":1925,"__hash__":1932},"posts\u002F2026-08-02-首页改成八屏.md","首页改成八屏",{"type":7,"value":1012,"toc":1906},[1013,1016,1022,1025,1029,1032,1042,1051,1054,1057,1065,1072,1078,1084,1143,1146,1165,1170,1175,1178,1184,1188,1191,1194,1202,1205,1208,1214,1217,1245,1248,1362,1365,1371,1375,1378,1394,1408,1411,1427,1430,1433,1437,1443,1446,1449,1455,1458,1508,1512,1515,1518,1521,1527,1530,1533,1538,1544,1548,1551,1554,1560,1564,1569,1610,1620,1623,1652,1657,1660,1710,1716,1719,1776,1788,1791,1795,1798,1801,1806,1809,1812,1817,1821,1824,1894,1897,1903],[10,1014,1015],{},"这个博客的首页原来是连续滚动。改成了整屏分页，一页一件事，共八屏：",[183,1017,1020],{"className":1018,"code":1019,"language":852},[850],"首屏 \u002F 自我介绍 01 \u002F 自我介绍 02 \u002F 做过的事 \u002F 精选复盘 \u002F 项目 \u002F 专栏 \u002F 联系\n",[34,1021,1019],{"__ignoreMap":188},[10,1023,1024],{},"一屏容纳是逐档实测调出来的，不是估的。",[22,1026,1028],{"id":1027},"一吸附用谁的","一、吸附用谁的",[10,1030,1031],{},"第一个决定是吸附用哪套机制。",[10,1033,1034,1035,1038,1039,156],{},"原生有 ",[34,1036,1037],{},"scroll-snap","，CSS 几行就能写。但这里用不了——页面上的平滑滚动是 Lenis 提供的，它用 JS 驱动 ",[34,1040,1041],{},"scrollTop",[801,1043,1044],{},[10,1045,1046,1047,1050],{},"吸附用 Lenis 自带的 Snap 而不是 CSS scroll-snap——Lenis 是用 JS 驱动 scrollTop 的，和原生吸附会互相抢控制权、滚起来发抖。Snap 挂在同一个 Lenis 实例上没有这个问题，",[34,1048,1049],{},"onSnapComplete"," 还能驱动指示器。",[10,1052,1053],{},"两个机制都想控制滚动位置，结果就是抖。",[10,1055,1056],{},"挂在同一个实例上之后，吸附完成后还能拿到回调，右侧的页码指示器直接跟着它更新。",[22,1058,1060,1061,1064],{"id":1059},"二mandatory-是错的","二、",[34,1062,1063],{},"mandatory"," 是错的",[10,1066,1067,1068,1071],{},"第一版用 ",[34,1069,1070],{},"type: 'mandatory'","。用户反馈手感是「被抢走了」。",[10,1073,1074,1075,1077],{},"原因是 ",[34,1076,1063],{}," 在用户还在滑的时候就强行把页面拽向最近一页。滑动过程被插手，手感不是吸附，是失控。",[10,1079,1080,1081,156],{},"改成 ",[34,1082,1083],{},"proximity",[183,1085,1089],{"className":1086,"code":1087,"language":1088,"meta":188,"style":188},"language-ts shiki shiki-themes github-light github-dark","type: 'proximity',\ndistanceThreshold: '30%',\nduration: 1.1,\ndebounce: 500,\n","ts",[34,1090,1091,1106,1118,1130],{"__ignoreMap":188},[192,1092,1093,1096,1100,1103],{"class":194,"line":195},[192,1094,1095],{"class":198},"type",[192,1097,1099],{"class":1098},"sVt8B",": ",[192,1101,1102],{"class":206},"'proximity'",[192,1104,1105],{"class":1098},",\n",[192,1107,1108,1111,1113,1116],{"class":194,"line":210},[192,1109,1110],{"class":198},"distanceThreshold",[192,1112,1099],{"class":1098},[192,1114,1115],{"class":206},"'30%'",[192,1117,1105],{"class":1098},[192,1119,1120,1123,1125,1128],{"class":194,"line":431},[192,1121,1122],{"class":198},"duration",[192,1124,1099],{"class":1098},[192,1126,1127],{"class":202},"1.1",[192,1129,1105],{"class":1098},[192,1131,1133,1136,1138,1141],{"class":194,"line":1132},4,[192,1134,1135],{"class":198},"debounce",[192,1137,1099],{"class":1098},[192,1139,1140],{"class":202},"500",[192,1142,1105],{"class":1098},[10,1144,1145],{},"三条参数各管一件事：",[48,1147,1148,1153,1159],{},[51,1149,1150,1152],{},[34,1151,1083],{}," 只在停下来且已经接近边界时才轻推一把。",[51,1154,1155,1158],{},[34,1156,1157],{},"distanceThreshold: '30%'"," 定义「接近」——落点离边界不到 30% 视口高才吸附，停在页面中间就让它停着。",[51,1160,1161,1164],{},[34,1162,1163],{},"debounce: 500"," 等惯性完全停下来再判断，不在滑行中途插手。",[10,1166,1167,1169],{},[34,1168,1135],{}," 从 220 提到 500：",[801,1171,1172],{},[10,1173,1174],{},"debounce 太小会在惯性还没停时就抢着吸附，手感发涩。",[10,1176,1177],{},"实测结果：",[183,1179,1182],{"className":1180,"code":1181,"language":852},[850],"滑动全程 0 反向帧（原来会被拽回）\n停在离边界 306px（超过 30% 阈值）时保持不动\n停在边界附近才对齐整页\n",[34,1183,1181],{"__ignoreMap":188},[22,1185,1187],{"id":1186},"三一屏装不装得下","三、一屏装不装得下",[10,1189,1190],{},"分页的前提是内容能装进一屏。这一条逐档量过。",[10,1192,1193],{},"首屏原高 921px：",[48,1195,1196,1199],{},[51,1197,1198],{},"1440×900 超出 89px",[51,1200,1201],{},"1366×768 超出 201px",[10,1203,1204],{},"精选复盘 788px，在 1366×768 超出 88px。",[10,1206,1207],{},"两个方向可选：砍内容，或者按档收紧间距。选了后者，分三档：",[183,1209,1212],{"className":1210,"code":1211,"language":852},[850],"min-width: 1024px and max-height: 820px   深压\nmin-width: 1024px and max-height: 900px   只压首屏\nmin-width: 1024px and min-height: 1000px  放大间距吃大屏留白\n",[34,1213,1211],{"__ignoreMap":188},[10,1215,1216],{},"中间那档的调整方式值得记一笔。为了挤出 27px，第一版直接砍了字号：",[183,1218,1222],{"className":1219,"code":1220,"language":1221,"meta":188,"style":188},"language-css shiki shiki-themes github-light github-dark","font-size: clamp(46px, 9.2vw, 132px)  →  clamp(30px, 5.4vw, 62px)\n","css",[34,1223,1224],{"__ignoreMap":188},[192,1225,1226,1230,1233,1236,1239,1242],{"class":194,"line":195},[192,1227,1229],{"class":1228},"s9eBZ","font-size",[192,1231,1232],{"class":1098},": clamp(46px, 9",[192,1234,1235],{"class":198},".2vw",[192,1237,1238],{"class":1098},", 132px)  →  clamp(30px, 5",[192,1240,1241],{"class":198},".4vw",[192,1243,1244],{"class":1098},", 62px)\n",[10,1246,1247],{},"砍掉一半多。首屏标题是整页的主体，砍字号会让整屏塌下来。改成全部从内边距要：",[183,1249,1251],{"className":1219,"code":1250,"language":1221,"meta":188,"style":188},"@media (min-width: 1024px) and (max-height: 900px) {\n  .page-hero :deep(.hero) { padding-top: 20px; padding-bottom: 24px; }\n  .page-hero :deep(.hero-foot) { margin-top: 26px; }\n}\n",[34,1252,1253,1294,1334,1357],{"__ignoreMap":188},[192,1254,1255,1259,1262,1265,1267,1270,1273,1276,1279,1281,1284,1286,1289,1291],{"class":194,"line":195},[192,1256,1258],{"class":1257},"szBVR","@media",[192,1260,1261],{"class":1098}," (",[192,1263,1264],{"class":202},"min-width",[192,1266,1099],{"class":1098},[192,1268,1269],{"class":202},"1024",[192,1271,1272],{"class":1257},"px",[192,1274,1275],{"class":1098},") ",[192,1277,1278],{"class":1257},"and",[192,1280,1261],{"class":1098},[192,1282,1283],{"class":202},"max-height",[192,1285,1099],{"class":1098},[192,1287,1288],{"class":202},"900",[192,1290,1272],{"class":1257},[192,1292,1293],{"class":1098},") {\n",[192,1295,1296,1299,1302,1305,1308,1311,1313,1316,1318,1321,1324,1326,1329,1331],{"class":194,"line":210},[192,1297,1298],{"class":198},"  .page-hero",[192,1300,1301],{"class":1098}," :deep(",[192,1303,1304],{"class":198},".hero",[192,1306,1307],{"class":1098},") { ",[192,1309,1310],{"class":202},"padding-top",[192,1312,1099],{"class":1098},[192,1314,1315],{"class":202},"20",[192,1317,1272],{"class":1257},[192,1319,1320],{"class":1098},"; ",[192,1322,1323],{"class":202},"padding-bottom",[192,1325,1099],{"class":1098},[192,1327,1328],{"class":202},"24",[192,1330,1272],{"class":1257},[192,1332,1333],{"class":1098},"; }\n",[192,1335,1336,1338,1340,1343,1345,1348,1350,1353,1355],{"class":194,"line":431},[192,1337,1298],{"class":198},[192,1339,1301],{"class":1098},[192,1341,1342],{"class":198},".hero-foot",[192,1344,1307],{"class":1098},[192,1346,1347],{"class":202},"margin-top",[192,1349,1099],{"class":1098},[192,1351,1352],{"class":202},"26",[192,1354,1272],{"class":1257},[192,1356,1333],{"class":1098},[192,1358,1359],{"class":194,"line":1132},[192,1360,1361],{"class":1098},"}\n",[10,1363,1364],{},"只有 700px 可用高度那一档才让一档字号。三档实测：",[183,1366,1369],{"className":1367,"code":1368,"language":852},[850],"1440×900    132px\n1920×1080   132px\n1366×768     92.9px\n",[34,1370,1368],{"__ignoreMap":188},[22,1372,1374],{"id":1373},"四检测判据写错过一次","四、检测判据写错过一次",[10,1376,1377],{},"判断页面有没有溢出，第一版用的是：",[183,1379,1381],{"className":1086,"code":1380,"language":1088,"meta":188,"style":188},"scrollHeight > clientHeight\n",[34,1382,1383],{"__ignoreMap":188},[192,1384,1385,1388,1391],{"class":194,"line":195},[192,1386,1387],{"class":1098},"scrollHeight ",[192,1389,1390],{"class":1257},">",[192,1392,1393],{"class":1098}," clientHeight\n",[10,1395,1396,1397,1400,1401,533,1404,1407],{},"这个判据永远为假。页面用的是 ",[34,1398,1399],{},"min-height","，内容超了页面会自己长高——",[34,1402,1403],{},"scrollHeight",[34,1405,1406],{},"clientHeight"," 一起变大，比值不变。",[10,1409,1410],{},"改成直接比可用高度：",[183,1412,1414],{"className":1086,"code":1413,"language":1088,"meta":188,"style":188},"\u002F\u002F 检测判据从 scrollHeight > clientHeight 改为直接比可用高度——\n\u002F\u002F 页面用的是 min-height，内容超了页面会自己长高，前者永远比不出溢出\n",[34,1415,1416,1422],{"__ignoreMap":188},[192,1417,1418],{"class":194,"line":195},[192,1419,1421],{"class":1420},"sJ8bj","\u002F\u002F 检测判据从 scrollHeight > clientHeight 改为直接比可用高度——\n",[192,1423,1424],{"class":194,"line":210},[192,1425,1426],{"class":1420},"\u002F\u002F 页面用的是 min-height，内容超了页面会自己长高，前者永远比不出溢出\n",[10,1428,1429],{},"这个错误值得单独说：判据本身写错了，所以「三档零溢出」这个结论在修正之前是不成立的。",[10,1431,1432],{},"修正之后三档均为 8\u002F8 页零溢出。",[22,1434,1436],{"id":1435},"五不分页的三种情况","五、不分页的三种情况",[183,1438,1441],{"className":1439,"code":1440,"language":852},[850],"窄屏（\u003C 1024px）\n矮视口（\u003C 620px）\n减弱动效（prefers-reduced-motion）\n",[34,1442,1440],{"__ignoreMap":188},[10,1444,1445],{},"三种都退回连续滚动。",[10,1447,1448],{},"前两种有实测依据：",[183,1450,1453],{"className":1451,"code":1452,"language":852},[850],"实测 390px 下自我介绍 1448px、精选复盘 1114px，强行一屏只会截断内容\n",[34,1454,1452],{"__ignoreMap":188},[10,1456,1457],{},"第三种是硬要求。整屏吸附会强制改变滚动位置，这和「尊重减弱动效偏好」直接冲突。CSS 里也要同步禁用：",[183,1459,1461],{"className":1219,"code":1460,"language":1221,"meta":188,"style":188},"@media (prefers-reduced-motion: reduce) {\n  .page { min-height: 0; }\n  .pager { display: none; }\n}\n",[34,1462,1463,1470,1487,1504],{"__ignoreMap":188},[192,1464,1465,1467],{"class":194,"line":195},[192,1466,1258],{"class":1257},[192,1468,1469],{"class":1098}," (prefers-reduced-motion: reduce) {\n",[192,1471,1472,1475,1478,1480,1482,1485],{"class":194,"line":210},[192,1473,1474],{"class":198},"  .page",[192,1476,1477],{"class":1098}," { ",[192,1479,1399],{"class":202},[192,1481,1099],{"class":1098},[192,1483,1484],{"class":202},"0",[192,1486,1333],{"class":1098},[192,1488,1489,1492,1494,1497,1499,1502],{"class":194,"line":431},[192,1490,1491],{"class":198},"  .pager",[192,1493,1477],{"class":1098},[192,1495,1496],{"class":202},"display",[192,1498,1099],{"class":1098},[192,1500,1501],{"class":202},"none",[192,1503,1333],{"class":1098},[192,1505,1506],{"class":194,"line":1132},[192,1507,1361],{"class":1098},[22,1509,1511],{"id":1510},"六分页之后每页只剩半屏内容","六、分页之后，每页只剩半屏内容",[10,1513,1514],{},"八屏分配完之后，出现一个反向问题：每页内容太少。",[10,1516,1517],{},"实测 1440×900 下多数页只填了 47%~63%，「做过的事」那页 63% 是空的。",[10,1519,1520],{},"既然分了页，每页就得撑得住。逐页补实：",[183,1522,1525],{"className":1523,"code":1524,"language":852},[850],"做过的事：加页首概览条（项目数\u002F时间跨度\u002F人手），每项补详细说明、关键指标与技术标签。308px → 548px\n项目：3 张卡补到 6 张，两行三列。362px → 511px\n联系：补引言、about 与 timeline 两个入口、站点信息带。452px → 791px\n专栏：每张卡补该栏最新一篇标题。512px → 597px\n自我介绍 01：四张能力卡各补首组的真实条目。504px → 596px\n自我介绍 02：补技术栈横带。522px → 603px\n",[34,1526,1524],{"__ignoreMap":188},[10,1528,1529],{},"填充率从 47%~63% 提到 61%~100%。",[10,1531,1532],{},"同一轮里把两个区块的入场动画也补了常驻循环，因为：",[801,1534,1535],{},[10,1536,1537],{},"实测入场动画本来就在跑（轴线 scaleX 0→0.99、圆点带回弹、卡片 clip-path 从 78% 揭到 4%、指标读数往上跳），问题是太隐蔽：一次性、1 秒内结束、触发点又在区块刚露头时，等正眼看过去通常已经播完。",[183,1539,1542],{"className":1540,"code":1541,"language":852},[850],"触发点 top 88% → 95%，轴线 0.9s → 1.25s，圆点与节点错峰 0.11 → 0.16\n",[34,1543,1541],{"__ignoreMap":188},[22,1545,1547],{"id":1546},"七断点从-900-降到-820","七、断点从 900 降到 820",[10,1549,1550],{},"第一版把「矮视口」定在 900px 高。1440×900 的屏幕因此被当成矮视口，白白砍掉了列表摘要——精选复盘从 788px 缩到 389px。",[10,1552,1553],{},"而 1440×900 明明有空间。断点降到 820：",[183,1555,1558],{"className":1556,"code":1557,"language":852},[850],"820 以下   深压\n900 以下   只压首屏\n1000 以上  放大间距吃掉大屏留白\n",[34,1559,1557],{"__ignoreMap":188},[22,1561,1563],{"id":1562},"八两个-css-优先级问题","八、两个 CSS 优先级问题",[10,1565,1566],{},[17,1567,1568],{},"首屏被挤成一个框。",[183,1570,1572],{"className":1219,"code":1571,"language":1221,"meta":188,"style":188},".page { padding: 24px 0; }\n.page-hero { padding: 0; }\n",[34,1573,1574,1595],{"__ignoreMap":188},[192,1575,1576,1579,1581,1584,1586,1588,1590,1593],{"class":194,"line":195},[192,1577,1578],{"class":198},".page",[192,1580,1477],{"class":1098},[192,1582,1583],{"class":202},"padding",[192,1585,1099],{"class":1098},[192,1587,1328],{"class":202},[192,1589,1272],{"class":1257},[192,1591,1592],{"class":202}," 0",[192,1594,1333],{"class":1098},[192,1596,1597,1600,1602,1604,1606,1608],{"class":194,"line":210},[192,1598,1599],{"class":198},".page-hero",[192,1601,1477],{"class":1098},[192,1603,1583],{"class":202},[192,1605,1099],{"class":1098},[192,1607,1484],{"class":202},[192,1609,1333],{"class":1098},[10,1611,1612,1615,1616,1619],{},[34,1613,1614],{},".page-hero { padding: 0 }"," 写在媒体查询之前，被后面同优先级的 ",[34,1617,1618],{},".page { padding: ... }"," 覆盖了。首屏于是四边都留出内边距，看起来像一个框。",[10,1621,1622],{},"修法是在每一档里重新声明一次：",[183,1624,1626],{"className":1219,"code":1625,"language":1221,"meta":188,"style":188},"\u002F* 首屏必须满幅出血。这行不能省——上面那条 .page 同优先级且更靠后，\n   会把顶部声明的 .page-hero { padding: 0 } 覆盖掉。 *\u002F\n.page-hero { padding: 0; }\n",[34,1627,1628,1633,1638],{"__ignoreMap":188},[192,1629,1630],{"class":194,"line":195},[192,1631,1632],{"class":1420},"\u002F* 首屏必须满幅出血。这行不能省——上面那条 .page 同优先级且更靠后，\n",[192,1634,1635],{"class":194,"line":210},[192,1636,1637],{"class":1420},"   会把顶部声明的 .page-hero { padding: 0 } 覆盖掉。 *\u002F\n",[192,1639,1640,1642,1644,1646,1648,1650],{"class":194,"line":431},[192,1641,1599],{"class":198},[192,1643,1477],{"class":1098},[192,1645,1583],{"class":202},[192,1647,1099],{"class":1098},[192,1649,1484],{"class":202},[192,1651,1333],{"class":1098},[10,1653,1654],{},[17,1655,1656],{},"标题被大屏规则压小。",[10,1658,1659],{},"大屏档里有一条：",[183,1661,1663],{"className":1219,"code":1662,"language":1221,"meta":188,"style":188},".page :deep(.big) { font-size: clamp(34px, 3.4vw, 56px); }\n",[34,1664,1665],{"__ignoreMap":188},[192,1666,1667,1669,1671,1674,1676,1678,1680,1683,1686,1689,1691,1694,1697,1700,1702,1705,1707],{"class":194,"line":195},[192,1668,1578],{"class":198},[192,1670,1301],{"class":1098},[192,1672,1673],{"class":198},".big",[192,1675,1307],{"class":1098},[192,1677,1229],{"class":202},[192,1679,1099],{"class":1098},[192,1681,1682],{"class":202},"clamp",[192,1684,1685],{"class":1098},"(",[192,1687,1688],{"class":202},"34",[192,1690,1272],{"class":1257},[192,1692,1693],{"class":1098},", ",[192,1695,1696],{"class":202},"3.4",[192,1698,1699],{"class":1257},"vw",[192,1701,1693],{"class":1098},[192,1703,1704],{"class":202},"56",[192,1706,1272],{"class":1257},[192,1708,1709],{"class":1098},"); }\n",[10,1711,1712,1713,1715],{},"首屏主标题的类名也是 ",[34,1714,1673],{},"。1920×1080 下它从 132px 被压到 56px，首屏整个塌掉。",[10,1717,1718],{},"改成排除首屏：",[183,1720,1722],{"className":1219,"code":1721,"language":1221,"meta":188,"style":188},"\u002F* 必须排除首屏——hero 里的 .big 是那个巨大的主标题，\n   被这条命中会从 132px 压到 56px，整个首屏塌掉 *\u002F\n.page:not(.page-hero) :deep(.big) { font-size: clamp(34px, 3.4vw, 56px); }\n",[34,1723,1724,1729,1734],{"__ignoreMap":188},[192,1725,1726],{"class":194,"line":195},[192,1727,1728],{"class":1420},"\u002F* 必须排除首屏——hero 里的 .big 是那个巨大的主标题，\n",[192,1730,1731],{"class":194,"line":210},[192,1732,1733],{"class":1420},"   被这条命中会从 132px 压到 56px，整个首屏塌掉 *\u002F\n",[192,1735,1736,1739,1741,1743,1746,1748,1750,1752,1754,1756,1758,1760,1762,1764,1766,1768,1770,1772,1774],{"class":194,"line":431},[192,1737,1738],{"class":198},".page:not",[192,1740,1685],{"class":1098},[192,1742,1599],{"class":198},[192,1744,1745],{"class":1098},") :deep(",[192,1747,1673],{"class":198},[192,1749,1307],{"class":1098},[192,1751,1229],{"class":202},[192,1753,1099],{"class":1098},[192,1755,1682],{"class":202},[192,1757,1685],{"class":1098},[192,1759,1688],{"class":202},[192,1761,1272],{"class":1257},[192,1763,1693],{"class":1098},[192,1765,1696],{"class":202},[192,1767,1699],{"class":1257},[192,1769,1693],{"class":1098},[192,1771,1704],{"class":202},[192,1773,1272],{"class":1257},[192,1775,1709],{"class":1098},[10,1777,1778,1779,20,1782,1784,1785,1787],{},"两个问题的成因相同：",[17,1780,1781],{},"同一个类名在两种语义下被复用",[34,1783,1578],{}," 既是容器也是列表页的内边距来源，",[34,1786,1673],{}," 既是区块小标题也是首屏主标题。分开之后就没这个问题。",[10,1789,1790],{},"修完实测：hero 左 0、底 1、右 10（右侧那 10px 是站点滚动条本身的宽度），标题 132px 满值，8\u002F8 页仍零溢出。",[22,1792,1794],{"id":1793},"九加载页期间的一个时序漏洞","九、加载页期间的一个时序漏洞",[10,1796,1797],{},"这个站有一个启动加载页，首次访问时盖几秒。加载页盖着的时候，入场动画被暂停了——否则动画会在遮挡期间自己跑完。",[10,1799,1800],{},"副作用是页面切换的幕布动画也被冻住：",[801,1802,1803],{},[10,1804,1805],{},"实测（加载页 3s 时点击）：t=745 路由已切到 \u002Fcolumns 而幕布 display:none，t=1406 幕布才出现并扫过。",[10,1807,1808],{},"路由在没有幕布的情况下切换了，等加载页结束、时间线恢复之后，被冻住的补间才补播。",[10,1810,1811],{},"结论是补一行守卫：加载页盖着时直接放行，不放幕布。理由是：",[801,1813,1814],{},[10,1815,1816],{},"真实用户点不到——加载页是全屏遮罩。但浏览器前进\u002F后退、程序化跳转仍可能在窗口内触发导航。",[22,1818,1820],{"id":1819},"十整屏分页的账","十、整屏分页的账",[10,1822,1823],{},"这套改动最后收敛成一张数字表：",[722,1825,1826,1836],{},[725,1827,1828],{},[728,1829,1830,1833],{},[731,1831,1832],{},"项目",[731,1834,1835],{},"数值",[741,1837,1838,1846,1854,1862,1870,1878,1886],{},[728,1839,1840,1843],{},[746,1841,1842],{},"页数",[746,1844,1845],{},"8",[728,1847,1848,1851],{},[746,1849,1850],{},"吸附类型",[746,1852,1853],{},"proximity，阈值 30%，防抖 500ms",[728,1855,1856,1859],{},[746,1857,1858],{},"吸附时长",[746,1860,1861],{},"1.1s",[728,1863,1864,1867],{},[746,1865,1866],{},"分页生效条件",[746,1868,1869],{},"宽 ≥ 1024px 且 高 ≥ 620px 且 未开启减弱动效",[728,1871,1872,1875],{},[746,1873,1874],{},"溢出",[746,1876,1877],{},"三档均 0\u002F8",[728,1879,1880,1883],{},[746,1881,1882],{},"填充率",[746,1884,1885],{},"61%~100%（改前 47%~63%）",[728,1887,1888,1891],{},[746,1889,1890],{},"首屏标题",[746,1892,1893],{},"132px（1440×900、1920×1080）、92.9px（1366×768）",[10,1895,1896],{},"每一条都有一次实测在后面。这类改动没别的办法——「一屏装不装得下」这件事只能量，量完才知道该压间距还是该砍内容。",[10,1898,1899,1900,1902],{},"中间还有两处是判断上的错，不是量的问题：一是用 ",[34,1901,1063],{}," 吸附（把用户的滑动抢走了），二是把「矮视口」定在 900（把有空间的屏幕当成没空间）。前者的修法是换模式，后者的修法是挪断点。",[423,1904,1905],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":188,"searchDepth":210,"depth":210,"links":1907},[1908,1909,1911,1912,1913,1914,1915,1916,1917,1918],{"id":1027,"depth":210,"text":1028},{"id":1059,"depth":210,"text":1910},"二、mandatory 是错的",{"id":1186,"depth":210,"text":1187},{"id":1373,"depth":210,"text":1374},{"id":1435,"depth":210,"text":1436},{"id":1510,"depth":210,"text":1511},{"id":1546,"depth":210,"text":1547},{"id":1562,"depth":210,"text":1563},{"id":1793,"depth":210,"text":1794},{"id":1819,"depth":210,"text":1820},"2026-08-02",{},"\u002F2026-08-02",{"title":1010,"description":1015},"2026-08-02-首页改成八屏","把博客首页从连续滚动改成八屏整屏分页。吸附用 Lenis 自带能力而不是 CSS 原生，断点按实测调整，每一档的溢出与填充率都有数字。",[1926,1927,1928,1929,1930,1931],"GSAP","Lenis","滚动吸附","响应式断点","页面结构","prefers-reduced-motion","2o9dKTJYPwCLxF__bLLN46O2x_0PMJoQgyZSxZpO428",1789212573211]