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