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