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