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