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