[{"data":1,"prerenderedAt":1005},["ShallowReactive",2],{"\u002F2026-04-05-toynet-iotmqtt":3,"\u002F2026-04-05-toynet-iotmqtt-rel":419},{"id":4,"title":5,"body":6,"column":401,"date":402,"description":12,"extension":403,"hero_image":404,"meta":405,"navigation":406,"path":407,"seo":408,"series_id":404,"severity":404,"stem":409,"summary":410,"tags":411,"__hash__":418},"posts\u002F2026-04-05-toynet-IoT后端从零到MQTT接入.md","设备还没接上，后端先做什么？",{"type":7,"value":8,"toc":394},"minimark",[9,13,16,20,23,26,49,52,55,58,64,78,81,86,97,100,105,116,119,122,137,144,148,151,154,164,175,197,200,214,217,243,246,253,336,343,346,349,352,355,387,390],[10,11,12],"p",{},"玩具联网项目需要同时支撑设备接入、厂商管理、订单与分销、小程序。搭建后端时，如果简单按\"设备\"→\"产品\"→\"订单\"的业务流程顺序实现，很快会碰到一个问题：设备连接上来上报数据，但不知道这个设备属于谁、属于哪个厂商，调试无从下手。",[10,14,15],{},"这不是代码问题，是架构依赖顺序的问题。",[17,18,19],"h2",{"id":19},"为什么顺序错了调试会卡住",[10,21,22],{},"设备上线后会通过 MQTT 上报心跳、状态、日志。这些数据要写入数据库，必须记录设备序列号和设备 ID。而设备 ID 是 MongoDB ObjectId，它存在于 Device 模型里。",[10,24,25],{},"Device 模型包含三个关键外键：",[27,28,29,37,43],"ul",{},[30,31,32,36],"li",{},[33,34,35],"code",{},"manufacturerId","（该设备属于哪个厂商）",[30,38,39,42],{},[33,40,41],{},"productId","（该设备属于哪个商品型号）",[30,44,45,48],{},[33,46,47],{},"userId","（当前绑定的用户，null 表示未绑定）",[10,50,51],{},"当你没有先定义 Manufacturer 和 User 数据结构时，Device 记录就悬空了。设备连接时，你能看到日志来了，但无法回答\"这是谁的设备\"。更重要的是，Device 模型的权限检查逻辑会失效——后续要实现\"用户只能查看自己的设备\"，需要依赖 userId 字段；要实现\"厂商只能看自己的设备\"，需要 manufacturerId。没有这两个模型，权限层就没法建。",[17,53,54],{"id":54},"正确的搭建顺序",[10,56,57],{},"从下往上分三层：",[10,59,60],{},[61,62,63],"strong",{},"第一层：所有制基础（必须最先）",[27,65,66,69,72,75],{},[30,67,68],{},"User 模型（小程序用户）",[30,70,71],{},"Admin 模型（后台管理员）",[30,73,74],{},"Manufacturer 模型（厂商入驻）",[30,76,77],{},"Role 模型（角色权限）",[10,79,80],{},"这一层定义了\"谁是谁\"。后续所有数据都需要通过 userId、adminId 或 manufacturerId 来标记所有权和权限。",[10,82,83],{},[61,84,85],{},"第二层：设备与商品（依赖第一层）",[27,87,88,91,94],{},[30,89,90],{},"Product 模型（商品\u002F型号定义）",[30,92,93],{},"Device 模型（具体设备实例，绑定到 User）",[30,95,96],{},"MQTT 服务（设备通信协议）",[10,98,99],{},"Device 需要 manufacturerId 来记录厂商关系，这样权限检查才有依据。Product 同样需要 manufacturerId。",[10,101,102],{},[61,103,104],{},"第三层：业务流程（依赖前两层）",[27,106,107,110,113],{},[30,108,109],{},"Order 模型（订单）",[30,111,112],{},"FinanceRecord 模型（财务结算）",[30,114,115],{},"Distribution 模型逻辑（二级分销）",[10,117,118],{},"订单需要引用 User、Product、Device，财务结算需要引用 Manufacturer。没有前两层，这一层无法运转。",[10,120,121],{},"为什么不能倒过来？如果先建 Order 和 Device，再补 User 和 Manufacturer，你会发现：",[123,124,125,128,131,134],"ol",{},[30,126,127],{},"Device 里的 manufacturerId 和 userId 无法被正确校验",[30,129,130],{},"MQTT 设备上线后，无法判断数据属于哪个 User 或 Manufacturer",[30,132,133],{},"设备日志（DeviceLog）要记录归属关系，但缺少参考数据",[30,135,136],{},"后续添加用户和厂商时，已有的设备记录需要大规模回填或迁移",[10,138,139,140,143],{},"总结一句：",[61,141,142],{},"数据的所有权明确之前，不要让设备开始说话","。",[17,145,147],{"id":146},"mqtt-主题设计三层隔离","MQTT 主题设计：三层隔离",[10,149,150],{},"一旦用户、厂商、设备三个模型都存在了，MQTT 通信才真正能跑起来。",[10,152,153],{},"设计主题结构时，我采用三级层次：",[155,156,161],"pre",{"className":157,"code":159,"language":160},[158],"language-text","toynet\u002Fdevice\u002F{deviceSn}\u002Fstatus       # 设备上报状态\ntoynet\u002Fdevice\u002F{deviceSn}\u002Fheartbeat    # 心跳\ntoynet\u002Fdevice\u002F{deviceSn}\u002Fcommand      # 下发指令\ntoynet\u002Fdevice\u002F{deviceSn}\u002Fresponse     # 指令响应\ntoynet\u002Fdevice\u002F{deviceSn}\u002Fai\u002Faudio     # 语音数据流\n","text",[33,162,159],{"__ignoreMap":163},"",[10,165,166,167,170,171,174],{},"为什么是 ",[33,168,169],{},"deviceSn"," 而不是 ",[33,172,173],{},"deviceId","？两个考虑：",[123,176,177,183],{},[30,178,179,182],{},[61,180,181],{},"硬件视角","：SN（序列号）是设备的全球唯一标识，出厂时就确定。即使设备还没入库、还没绑定用户，SN 也是不变的。这样 MQTT broker 可以在设备连接时就验证 SN。",[30,184,185,188,189,192,193,196],{},[61,186,187],{},"权限隔离","：MQTT ACL（Access Control List）规则可以基于 SN 前缀做批量配置。比如\"厂商 A 的所有设备 SN 以 ",[33,190,191],{},"TOY-A-"," 开头\"，就可以一条规则 ",[33,194,195],{},"toynet\u002Fdevice\u002FTOY-A-+\u002F+"," 授予厂商 A 的连接账号。",[10,198,199],{},"再往上一层想，虽然主题里没有显式写 manufacturerId 或 userId，但通过 Device 模型的正向查询，MQTT 服务收到消息后可以：",[123,201,202,205,208,211],{},[30,203,204],{},"解析 deviceSn",[30,206,207],{},"查库找到 Device 记录",[30,209,210],{},"获取 manufacturerId、userId、productId",[30,212,213],{},"校验消息来源（设备证书或 token）是否有权限修改这个设备",[10,215,216],{},"这样做的好处：",[27,218,219,225,231,237],{},[30,220,221,224],{},[61,222,223],{},"设备独立性","：设备只需要知道自己的 SN，不需要关心自己属于哪个用户",[30,226,227,230],{},[61,228,229],{},"厂商隔离","：后台在 MQTT broker 层可以为每个厂商的账号设置主题权限",[30,232,233,236],{},[61,234,235],{},"灵活迁移","：设备转移给另一个用户时，只需更新 Device.userId，MQTT 主题不变",[30,238,239,242],{},[61,240,241],{},"调试友好","：指定 deviceSn 就能看完整的上报链路",[17,244,245],{"id":245},"指令下发与离线队列",[10,247,248,249,252],{},"设备接收指令通过 ",[33,250,251],{},"toynet\u002Fdevice\u002F{deviceSn}\u002Fcommand"," 这个 topic。指令格式：",[155,254,258],{"className":255,"code":256,"language":257,"meta":163,"style":163},"language-json shiki shiki-themes github-light github-dark","{\n  \"cmdId\": \"a1b2c3d4-e5f6-...\",\n  \"cmd\": \"play_story\",\n  \"params\": { \"storyId\": \"001\" },\n  \"timestamp\": 1712600000\n}\n","json",[33,259,260,269,286,299,319,330],{"__ignoreMap":163},[261,262,265],"span",{"class":263,"line":264},"line",1,[261,266,268],{"class":267},"sVt8B","{\n",[261,270,272,276,279,283],{"class":263,"line":271},2,[261,273,275],{"class":274},"sj4cs","  \"cmdId\"",[261,277,278],{"class":267},": ",[261,280,282],{"class":281},"sZZnC","\"a1b2c3d4-e5f6-...\"",[261,284,285],{"class":267},",\n",[261,287,289,292,294,297],{"class":263,"line":288},3,[261,290,291],{"class":274},"  \"cmd\"",[261,293,278],{"class":267},[261,295,296],{"class":281},"\"play_story\"",[261,298,285],{"class":267},[261,300,302,305,308,311,313,316],{"class":263,"line":301},4,[261,303,304],{"class":274},"  \"params\"",[261,306,307],{"class":267},": { ",[261,309,310],{"class":274},"\"storyId\"",[261,312,278],{"class":267},[261,314,315],{"class":281},"\"001\"",[261,317,318],{"class":267}," },\n",[261,320,322,325,327],{"class":263,"line":321},5,[261,323,324],{"class":274},"  \"timestamp\"",[261,326,278],{"class":267},[261,328,329],{"class":274},"1712600000\n",[261,331,333],{"class":263,"line":332},6,[261,334,335],{"class":267},"}\n",[10,337,338,339,342],{},"每条指令都有唯一的 ",[33,340,341],{},"cmdId","（UUID），用于追踪执行状态。指令类型包括播放故事、播放音乐、设置音量、运动控制等。",[10,344,345],{},"当设备离线时，server 端收不到 MQTT 连接，无法直接下发。但不能简单丢弃指令——应该先入库，标记为 pending，等设备上线后重新发送。这就需要一个 pending command queue，可以存在 Device.pendingCommands 字段或单独的 CommandQueue 表。",[10,347,348],{},"心跳机制是定位离线状态的基础：设备每 30 秒上报一次心跳，server 收到心跳就更新 Device.lastHeartbeat 和 status='online'。90 秒没收到心跳，自动标记 status='offline'。这样即使没有正式的连接状态通知，也能通过心跳超时推断设备状态。",[17,350,351],{"id":351},"从设计到运行",[10,353,354],{},"总结一下这个架构的关键约束：",[123,356,357,363,369,375,381],{},[30,358,359,362],{},[61,360,361],{},"所有权层必须先建","：User、Manufacturer 决定后续数据的访问边界。",[30,364,365,368],{},[61,366,367],{},"设备必须有归属","：Device 必须绑定 manufacturerId（出厂时），后续可选绑定 userId（用户扫码时）。",[30,370,371,374],{},[61,372,373],{},"MQTT 主题用 deviceSn","：保持设备的全局唯一性和权限隔离。",[30,376,377,380],{},[61,378,379],{},"心跳定期刷新状态","：不依赖复杂的连接管理，简单可靠。",[30,382,383,386],{},[61,384,385],{},"指令带 ID 追踪","：cmdId 让后续的 AI 对话模块（Plan 5）可以与指令执行绑定。",[10,388,389],{},"这个顺序不是教科书上的\"标准\"，而是在这个具体项目里，设备数据要有明确的所有权和权限边界这个需求推导出来的。如果只有单一厂商、单一用户类型，顺序可以更灵活。但一旦要支撑多厂商入驻和用户私密性，底层模型的依赖关系就会很快锁死搭建顺序。",[391,392,393],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}",{"title":163,"searchDepth":271,"depth":271,"links":395},[396,397,398,399,400],{"id":19,"depth":271,"text":19},{"id":54,"depth":271,"text":54},{"id":146,"depth":271,"text":147},{"id":245,"depth":271,"text":245},{"id":351,"depth":271,"text":351},"工程手记","2026-04-05","md",null,{},true,"\u002F2026-04-05-toynet-iotmqtt",{"title":5,"description":12},"2026-04-05-toynet-IoT后端从零到MQTT接入","设备接入系统如何确保数据有归属，为什么用户和厂商模块必须先建，MQTT 主题设计如何支撑权限隔离。",[412,413,414,415,416,417],"IoT","MQTT","Node.js","Koa","MongoDB","系统设计","n-oGSEyhQWTIXH_mJqBzF0pnjPGc-GqJi62Zu0HiHcY",[420,726,870],{"id":421,"title":422,"body":423,"column":401,"date":713,"description":427,"extension":403,"hero_image":404,"meta":714,"navigation":406,"path":715,"seo":716,"series_id":404,"severity":404,"stem":717,"summary":718,"tags":719,"__hash__":725},"posts\u002F2026-07-29-一年八千次提交AI辅助开发工作流.md","一年八千次提交，我是怎么干的",{"type":7,"value":424,"toc":696},[425,428,431,434,439,442,445,448,452,455,458,472,475,479,482,485,488,492,495,498,518,521,525,528,539,542,546,549,563,566,569,573,576,579,583,586,589,592,596,599,602,626,633,636,642,645,648,651,658,661,664,690,693],[10,426,427],{},"2026 上半年，主要项目累计提交 7787 次（其中 newCodex 因为是 fork 项目，1922 次提交含上游历史；纯新增代码的项目是 yun-claude、yun-claw、new-openclaw 等）。提交数看起来多，但每个提交背后的价值不在数量，而在流程的可重复性。这篇文章把工作流写清楚。",[17,429,430],{"id":430},"流程的六个环节",[10,432,433],{},"整个开发周期分六步：需求澄清 → 设计文档审阅 → 拆实施计划 → 分阶段实现 → 代码审查 → 故障归档。每一步都有明确的输入输出和验证方式。",[435,436,438],"h3",{"id":437},"_1-需求澄清","1. 需求澄清",[10,440,441],{},"开始前问清楚，不要猜。典型问题：这个功能要处理哪些用户场景？边界条件是什么？现有系统的哪部分会受影响？",[10,443,444],{},"这一步的产出是一份结构化的需求文档，包括功能范围、约束条件、风险假设。不是长篇幅的铺垫，而是一份清单式的澄清记录。",[10,446,447],{},"在 new-openclaw 项目里，这一步通常是用 Claude 的 superpowers:brainstorming skill 来展开。提问方式很重要：我会列出已知条件，然后问\"这个设计下会遇到什么问题？\"而不是\"你觉得应该怎么做？\"让 AI 在我的约束框架内工作。",[435,449,451],{"id":450},"_2-设计文档与人工审阅","2. 设计文档与人工审阅",[10,453,454],{},"这是整个流程里最关键的检查点。花一小时审一份 200 行的设计文档，比花五小时审一份 2000 行的代码便宜得多。而且回头修改设计的成本远低于修改实现。",[10,456,457],{},"new-openclaw 项目现有 30 份设计文档（specs 目录），每份文档都包括：",[27,459,460,463,466,469],{},[30,461,462],{},"范围：这个设计覆盖什么、不覆盖什么",[30,464,465],{},"决策与权衡：为什么选这个方案，放弃了什么",[30,467,468],{},"接口契约：如果涉及多个模块，清晰定义每个边界",[30,470,471],{},"风险清单：已知的坑和防护措施",[10,473,474],{},"写完设计文档以后，我会读一遍、问几个\"为什么\"，然后提出修改意见。这一步排除了 80% 的方向错误。常见的修改方向有：缩小范围（第一个版本不用处理那么多边界情况）、明确约束（系统资源、网络延迟、并发数的假设）、补充防护（熔断、限流、幂等）。",[435,476,478],{"id":477},"_3-实施计划与-dod-定义","3. 实施计划与 DoD 定义",[10,480,481],{},"设计文档定下来以后，拆成实施计划。计划的粒度是\"一个可验证的功能单元\"——通常是一个小时到半天的工作量，完成后能单独验证成功。",[10,483,484],{},"计划文档里必须写清完成定义（DoD，Definition of Done）。不是\"实现登录功能\"，而是\"写出能拒绝无效格式的登录端点、覆盖单点故障下的重试、有端到端的冒烟测试\"。",[10,486,487],{},"new-openclaw 项目现有 36 份实施计划（plans 目录），跨度从一周的 S0 阶段（骨架 + Mock 后台）到数周的 S1 阶段（真实业务后台）。每份计划都带着清晰的 checklist，这样我在执行时能随时问 Claude：\"下一步应该是什么？\"而不是脑子里模糊地记着进度。",[435,489,491],{"id":490},"_4-分阶段实现与逐步验证","4. 分阶段实现与逐步验证",[10,493,494],{},"有了计划以后，按顺序实现。关键是每一步都要有验证：单元测试、集成测试、或者一个小的 end-to-end 冒烟测试。验证不通过就停在这一步，不往下推。",[10,496,497],{},"这一步会用到三个代理：",[27,499,500,506,512],{},[30,501,502,505],{},[61,503,504],{},"superpowers:test-driven-development"," —— 先写测试，再写实现",[30,507,508,511],{},[61,509,510],{},"superpowers:subagent-driven-development"," —— 复杂任务拆成独立的子任务，并行推进",[30,513,514,517],{},[61,515,516],{},"superpowers:systematic-debugging"," —— 遇到测试失败，用这个代理追根溯源，不要盲目修改代码",[10,519,520],{},"实施过程中如果发现设计假设错了（比如某个接口响应时间远超预期，或者并发场景下出现竞态条件），就停下来回到第 2 步重新审视设计，而不是继续往下推。",[435,522,524],{"id":523},"_5-代码审查","5. 代码审查",[10,526,527],{},"实现完成以后，不是立即合并，而是过一遍 code-reviewer 代理。审查的重点不在代码风格（那个自动工具做），而在：",[27,529,530,533,536],{},[30,531,532],{},"这段代码实现的是设计文档里的哪一部分？偏离了吗？",[30,534,535],{},"错误处理有没有遗漏？边界情况有没有考虑？",[30,537,538],{},"有没有意外改动无关的代码？",[10,540,541],{},"审查通常能抓住两类问题。一类是逻辑问题：某个条件判断漏了一个分支，或者并发场景下两个操作的顺序反了。另一类是\"设计和实现对不上\"：实现了一个设计里没提到的特性，或者某个约束（比如\"这个值不能为空\"）没在代码里强制。",[435,543,545],{"id":544},"_6-故障归档","6. 故障归档",[10,547,548],{},"系统上线以后，bug 是难免的。重要的是怎么处理它。new-openclaw 项目有一套 bug 知识库规范（CLAUDE.md 里定义），每个 bug 修好以后都要新建一份独立文档，包括：",[27,550,551,554,557,560],{},[30,552,553],{},"现象和复现路径",[30,555,556],{},"根因分析：为什么会发生，触发链路是什么",[30,558,559],{},"修复方法：改了什么，为什么选这个方案",[30,561,562],{},"预防措施：代码改进、测试添加、还是构建期检查",[10,564,565],{},"80 份 bug 文档（截至 5 月中旬）不是问题的多，而是追根溯源的记录的多。下次遇到类似现象，能直接查库而不是重新排查。",[17,567,568],{"id":568},"三条铁律",[435,570,572],{"id":571},"_1-假设必须显式声明","1. 假设必须显式声明",[10,574,575],{},"不要猜。不清楚的地方就问，把问题写成澄清清单。\"这个 API 能处理多大的请求体？\"、\"离线场景下要缓存多长时间？\"、\"错误重试间隔是指数退避还是固定时间？\"。",[10,577,578],{},"这些问题看起来小，但决定了实现的复杂度和测试用例的多少。猜错了会导致前期设计精美，但方向错误，后面要推倒重来。",[435,580,582],{"id":581},"_2-修改必须精确","2. 修改必须精确",[10,584,585],{},"只改需要改的部分。这听起来像常识，但在实际工作中容易出现\"顺手改一下边上的代码\"的情况 —— 格式不规范了，变量命名不一致了，某个函数太长了，\"顺便\"重构一下。",[10,587,588],{},"结果是一个改动影响了五个文件，代码审查花了双倍时间，引入了新 bug 的风险。",[10,590,591],{},"规则是：改动必须对应需求的某一行。格式、风格、无关的重构，单独立项，不要混在功能改动里。",[435,593,595],{"id":594},"_3-成功标准必须可验证","3. 成功标准必须可验证",[10,597,598],{},"\"添加验证\"这个说法是模糊的。改写成：\"写出一个测试用例，输入非法邮箱格式，验证 API 返回 400；输入合法邮箱，验证返回 200 和预期数据结构\"。",[10,600,601],{},"每个 plan 文档里的 DoD 都是这样写的。拿一个阶段做例子：",[603,604,605,608],"blockquote",{},[10,606,607],{},"S0 阶段的 DoD：",[123,609,610,613,620,623],{},[30,611,612],{},"openapi\u002Fapi-v1.yaml 包含 spec 全部 11 个端点的字段级 schema，可被 swagger-ui 加载 ✓",[30,614,615,616,619],{},"backend\u002F Mock 服务 ",[33,617,618],{},"npm start"," 后能响应全部端点，返回符合契约的 mock 数据 ✓",[30,621,622],{},"launcher\u002F Rust 项目能编译为 launcher.exe，运行后完成所有阶段扫描并输出 diagnostics.json ✓",[30,624,625],{},"至少一个 end-to-end 冒烟测试通过（launcher 上报 → mock 后台收到 → 审计日志记录） ✓",[10,627,628,629,632],{},"每一条都能通过一个具体的命令来验证。\"能工作\"太模糊，\"运行 ",[33,630,631],{},"npm test"," 且所有测试通过\"才是可验证的。",[17,634,635],{"id":635},"流程失效的情况",[10,637,638,639,143],{},"这套流程在一个关键点会失效：",[61,640,641],{},"需求本身没想清楚时",[10,643,644],{},"我遇到过的例子是这样的。客户说\"要支持 USB 存储检测\"，这个需求很清楚，所以设计文档写了 20 多页，规划了三个阶段，列出了 30 多个 test case。然后两周后，客户补充说\"哦对了，还要处理网络驱动器\"。",[10,646,647],{},"这时前面的设计和计划都要回头改。检测逻辑复杂了，测试场景翻倍，阶段划分要调整。这不是流程的问题，这是需求的问题。流程本身反而帮助我及时暴露了这个风险 —— 如果没有设计文档，可能要到代码审查阶段，甚至系统上线以后才发现这个遗漏。",[10,649,650],{},"应对办法是在第 1 步（需求澄清）多花时间。列出你想到的所有场景，问\"还有其他我忽略的情况吗？\"。不是要求完全预测未来，而是把已知的不确定性显式写出来，而不是假设需求是固定的。",[10,652,653,654,657],{},"另一个失效的情况是",[61,655,656],{},"设计和实现的沟通不畅","。如果设计文档是给另一个人读的（或者给 AI 代理读的），但执行者没有理解透彻，实现出来会偏离设计。预防办法是在开始实施前，再过一遍设计文档，确认\"我清楚要做什么\"。",[17,659,660],{"id":660},"提交数字背后的故事",[10,662,663],{},"为什么能积累 7787 次提交？不是因为每次都在写新功能。真实的分布大概是：",[27,665,666,672,678,684],{},[30,667,668,671],{},[61,669,670],{},"功能实现","：40%",[30,673,674,677],{},[61,675,676],{},"设计文档编写和迭代","：25%",[30,679,680,683],{},[61,681,682],{},"测试编写","：20%",[30,685,686,689],{},[61,687,688],{},"bug 修复与回归测试","：15%",[10,691,692],{},"关键是这些提交都有上下文。每个提交的 message 都指向一个设计文档或一个 plan 的某个环节，或者一个 bug 记录。下次有问题要追查根因时，能快速定位到那个提交，看当时的设计决策是什么。",[10,694,695],{},"另外，有 80 份 bug 记录和 66 份设计 + 计划文档（30 specs + 36 plans）这件事本身说明了一点：文档不是负担，文档是工作的实际产出。代码只是文档的一个落地形式。",{"title":163,"searchDepth":271,"depth":271,"links":697},[698,706,711,712],{"id":430,"depth":271,"text":430,"children":699},[700,701,702,703,704,705],{"id":437,"depth":288,"text":438},{"id":450,"depth":288,"text":451},{"id":477,"depth":288,"text":478},{"id":490,"depth":288,"text":491},{"id":523,"depth":288,"text":524},{"id":544,"depth":288,"text":545},{"id":568,"depth":271,"text":568,"children":707},[708,709,710],{"id":571,"depth":288,"text":572},{"id":581,"depth":288,"text":582},{"id":594,"depth":288,"text":595},{"id":635,"depth":271,"text":635},{"id":660,"depth":271,"text":660},"2026-07-29",{},"\u002F2026-07-29-ai",{"title":422,"description":427},"2026-07-29-一年八千次提交AI辅助开发工作流","从需求澄清到故障归档，一套系统化的 AI 辅助开发流程：先设计文档过审，再分阶段实施验证，最后代码审查与故障存档，三条铁律支撑整个流程。",[720,721,722,723,724],"Claude","工作流","AI 辅助开发","代码审查","文档驱动","DN7sqbGmg2UybSvRXTMLW-dj0UURXET3KsSNeW_1V9I",{"id":727,"title":728,"body":729,"column":401,"date":857,"description":733,"extension":403,"hero_image":404,"meta":858,"navigation":406,"path":859,"seo":860,"series_id":404,"severity":404,"stem":861,"summary":862,"tags":863,"__hash__":869},"posts\u002F2026-07-20-远程桌面管理器DPAPI与60万次迭代.md","密码，不该由我保管",{"type":7,"value":730,"toc":851},[731,734,738,741,744,747,761,764,768,771,774,777,780,783,794,797,800,803,809,815,821,831,845,848],[10,732,733],{},"远程服务器资料管理工具需要妥善保存账户凭据。这个工具采用两层加密设计：本机存储用 Windows DPAPI，备份使用基于密码的密钥派生。两层各有边界，理解这些边界对使用决策至关重要。",[17,735,737],{"id":736},"第一层dpapi-的便利与代价","第一层：DPAPI 的便利与代价",[10,739,740],{},"本机存储的凭据使用 Windows DPAPI（Data Protection API）加密，由当前 Windows 用户身份保护。这是 Windows 内置的用户级加密机制，密钥由操作系统管理，与登录用户的 SID 和机器的本地安全数据库绑定。不需要用户记一个额外的主密码，启动应用即可直接使用保存的凭据。",[10,742,743],{},"DPAPI 的好处显而易见：启动应用即用，无需输入密码，用户体验最优。代价是它的加密密钥锁定在当前用户、当前机器。凭据无法直接迁移到另一台电脑或另一个 Windows 用户——这不是工具的限制，而是 DPAPI 的设计约束。Windows 操作系统就是这样设计的，其他工具也无法绕过。",[10,745,746],{},"实际操作中的含义很明确：",[27,748,749,752,755,758],{},[30,750,751],{},"重装 Windows 前必须先导出备份。原有凭据会因为用户 SID 变化和机器密钥更新而无法解密，即便登录同一账户也不行。",[30,753,754],{},"更换电脑前需要先创建备份并妥善保存备份密码，目标电脑上导入时需要重新输入这个备份密码。",[30,756,757],{},"在同一电脑上切换 Windows 用户登录，旧用户的凭据对新用户完全不可见，因为加密密钥是用户级的。",[30,759,760],{},"多用户共享一台电脑的场景下，凭据不会跨用户暴露。",[10,762,763],{},"这些限制会在实际使用中暴露出来——比如忙于工作时重装系统忽略了导出，或者在公用工作电脑上多个人使用。正因为如此，工具强制要求提供备份机制。备份不是可选功能，而是必需的。",[17,765,767],{"id":766},"第二层备份加密的固定迭代设计","第二层：备份加密的固定迭代设计",[10,769,770],{},"备份采用 PBKDF2-SHA256（基于密码的密钥派生函数 2，使用 SHA-256 哈希）加密，固定执行 600,000 次迭代。用户创建备份时设置一个密码，导入时输入这个密码。PBKDF2 通过重复应用哈希函数来增加破解难度，迭代次数越多，从密码派生密钥所需的计算时间越长，攻击者进行暴力破解也需要投入成倍的计算资源。",[10,772,773],{},"600,000 次迭代的来源是什么？这是一个有意识的设计决定，而不是随意选择。迭代次数越多，暴力破解的成本越高，但加密和解密的耗时也越长。设计者需要在两者之间找到平衡点：够强以抵御现代硬件的破解能力，又不能强到让普通用户的导入操作变得难以忍受。600,000 次这个数字反映的是这个平衡的结果。",[10,775,776],{},"为什么固定而非可配置？这是一个纪律问题。可配置听起来更灵活、更给用户掌控权，但在实践中会诱使用户为了更快的备份导入速度而降低迭代次数，从而削弱安全强度。安全不应该由便利让步。人们往往倾向于选择快速方案，尤其当他们没有安全专业知识时。固定的迭代次数消除了这种选择权，保证了所有备份都有相同的防护等级。工具的职责是做出最合理的决定，而不是把这个决定推给用户。",[17,778,779],{"id":779},"性能实测与数据解读",[10,781,782],{},"在 Windows 11 构建机上，对 1 MiB 大小的备份数据进行加密与解密的完整往返，预热缓存后的连续五次耗时分别为：108.919 ms、104.079 ms、100.418 ms、94.933 ms、103.470 ms。中位数为 103.470 ms。这意味着从你按下\"导入备份\"到凭据被解密并加载到内存，大约需要 100 毫秒的等待时间。",[10,784,785,786,789,790,793],{},"这组数据收集的目的是",[61,787,788],{},"记录性能表现","。它提供了一个具体的参考：用户在 Windows 11 系统上可以预期导入备份的延迟大约是这个量级。这对评估工具的可用性很有用。但它明确",[61,791,792],{},"不","作为调整迭代次数的依据。这种表述听起来有些冗余，但它是设计纪律的一部分——必须写下来的目的是防止后续有人看到\"100 毫秒确实有点慢\"就建议降低迭代次数。防止的是这样的推理：因为性能数据显示延迟不够快，所以降低迭代次数。这个逻辑链条在安全工程中是禁止的。",[10,795,796],{},"相反，如果实践证明 100 毫秒对用户体验构成问题，正确的做法是要么接受这个成本作为安全性的代价，要么在未来硬件更新换代后自然加速。绝不是削弱密钥派生强度。性能和安全的权衡应该在上层的需求决策中做，而不是在密码学参数中做。",[17,798,799],{"id":799},"工具的明确边界",[10,801,802],{},"这个工具的安全设计有明确的保护范围和限制：",[10,804,805,808],{},[61,806,807],{},"DPAPI 层的限制","：本机凭据的安全性最终依赖于 Windows 用户密码。如果 Windows 账户被破解，攻击者用该账户登录电脑，DPAPI 解密会自动进行。如果用户以管理员身份运行工具（虽然不需要管理员权限），攻击者获得管理员权限后理论上也可能绕过某些保护。安全链的强度由最弱的一环决定——如果你的 Windows 用户密码很弱，或者电脑物理上被他人访问，DPAPI 的保护就名存实亡。",[10,810,811,814],{},[61,812,813],{},"备份密码的强度","：导入备份时用户设置的密码决定了备份的抗暴力破解能力。PBKDF2 提供的防护再强，也无法弥补一个简单密码的缺陷。\"123456\"这样的备份密码，在 600,000 次迭代和现代 GPU 的破解能力面前，可能在几秒到几分钟内被破解。",[10,816,817,820],{},[61,818,819],{},"系统策略的约束","：工具运行在 Windows 系统上，不会绕过任何系统级的安全机制。Windows SmartScreen 对未签名程序的警告、远程桌面连接的安全确认对话、Group Policy 的限制——这些工具都无法绕过。安装包为未签名的内部制品，Windows SmartScreen 会在首次运行时显示\"未知发布者\"警告。这不是工具的缺陷，而是系统安全策略的正常行为。",[10,822,823,826,827,830],{},[61,824,825],{},"加密设计的范围","：这套两层加密设计防的是",[61,828,829],{},"离线攻击","——攻击者获得了备份文件或本机的加密数据，在没有用户交互的情况下尝试破解。它防不了的情况：",[27,832,833,836,839,842],{},[30,834,835],{},"备份密码通过社工或偷看被直接获取",[30,837,838],{},"备份文件在网络传输过程中被中间人截获（如果使用不安全的传输方式）",[30,840,841],{},"凭据被恶意软件在内存中窃取（工具启动后、密码解密到内存这段时间内）",[30,843,844],{},"Windows 账户本身被已经登录电脑的恶意软件控制",[10,846,847],{},"对这些风险的防护需要用户在安全习惯和网络安全措施上自行补足——设置强密码、在信任的网络上操作、定期更新系统补丁、使用反恶意软件工具。",[10,849,850],{},"使用这套工具前要明确：本机凭据带来了便利，代价是将安全依赖在 Windows 用户身份上；备份凭据提供了迁移能力，代价是密码强度必须由用户自己把关。都不是\"一次设置永久安全\"的方案，都需要持续的安全意识和维护。",{"title":163,"searchDepth":271,"depth":271,"links":852},[853,854,855,856],{"id":736,"depth":271,"text":737},{"id":766,"depth":271,"text":767},{"id":779,"depth":271,"text":779},{"id":799,"depth":271,"text":799},"2026-07-20",{},"\u002F2026-07-20-dpapi60",{"title":728,"description":733},"2026-07-20-远程桌面管理器DPAPI与60万次迭代","两层加密保护远程桌面凭据，本机用 DPAPI 便利性换易用性，备份用 PBKDF2 固定 60 万迭代；为什么迭代次数不可配置，性能数据如何解读。",[864,865,866,867,868],"Windows","DPAPI","PBKDF2","凭据存储","加密设计","TzKUoZoFLtAsO_VcB1TOOdWOpwcoDj6AUwEdS6d7pv0",{"id":871,"title":872,"body":873,"column":401,"date":992,"description":877,"extension":403,"hero_image":404,"meta":993,"navigation":406,"path":994,"seo":995,"series_id":404,"severity":404,"stem":996,"summary":997,"tags":998,"__hash__":1004},"posts\u002F2026-07-17-把设计文档变成可讲的演示.md","转不成演示的设计文档，本来就没讲清",{"type":7,"value":874,"toc":986},[875,878,884,887,890,893,896,914,921,925,928,934,944,953,956,959,962,965,968,971,974,977,980,983],[10,876,877],{},"一年积累了 200 多份设计文档，最初想法是直接拿这些文档去讲。结果发现这些东西不适合讲。设计文档是给人写的，演示文稿是给人听的，形式完全不同。",[10,879,880,881,143],{},"解决这个问题的思路不是\"写个通用 PPT 编辑器\"，而是\"从结构化文档一键生成演示\"。本质差异在这里——编辑器要处理用户的任意编辑行为，演示工具只要转换",[61,882,883],{},"已有的结构",[17,885,886],{"id":886},"为什么选单向转换而不是编辑器",[10,888,889],{},"设计文档有稳定的模板：背景、方案、架构、取舍、参考。这个顺序不是随意的，恰好就是讲一个设计时的叙述顺序。",[10,891,892],{},"用户不需要\"先生成再改\"，需要的是\"文档秒变幻灯片\"。一旦你改，就回到编辑器的坑里去了——要支持拖拽、删除、排版，工作量爆炸，而且多数用户不会调，生成好的东西就是定版。",[10,894,895],{},"这个判断来自实际数据。我的工具做两个决策：",[123,897,898,908],{},[30,899,900,903,904,907],{},[61,901,902],{},"产物是自包含 HTML","，不是 Office 文件（",[33,905,906],{},".pptx"," 需要可编辑格式，门槛高；HTML 在浏览器里就能放映，自包含意味着内联了所有 CSS、JS、图片）。",[30,909,910,913],{},[61,911,912],{},"内容由 LLM 生成，不开放编辑面板","（用一个\"预览挑选\"的两阶段流程，让用户在 3 种风格的封面里选一个，然后生成整份）。",[10,915,916,917,920],{},"这两个约束听起来很严格，实际上契合了需求的本质：",[61,918,919],{},"文档的目的是记录决策，演示的目的是讲述决策","。不需要演示过程中再改决策，改了就回到文档去改。",[17,922,924],{"id":923},"什么样的结构能转什么样的不能","什么样的结构能转、什么样的不能",[10,926,927],{},"设计文档要是能一键转成演示，必须满足几个条件。",[10,929,930,933],{},[61,931,932],{},"架构图可以直接映射","。我的演示工具支持 AI 生图，也支持用户指定图片 URL。当文档里写了架构设计时，LLM 看到这个描述会生成对应的图，然后内联到演示文稿里。舞台是固定的 1920×1080，所有图片容器有最小尺寸约束（GSAP 时间轴动画要能在任意时间点求值，不能靠动态布局）。",[10,935,936,939,940,943],{},[61,937,938],{},"关键数据用表格记录最不容易犯错","。表格这种结构容易转化——",[261,941,942],{},"待补：具体表格如何转成演示页的实现规则","。如果文档里的数据以段落文字形式写，转成演示时就卡住了，LLM 要从自然语言反推结构。",[10,945,946,949,950],{},[61,947,948],{},"取舍（trade-off）部分","：",[261,951,952],{},"待补：设计文档中的取舍说明如何映射成演示内容的规则",[10,954,955],{},"一个更深的观察：文档转不出来好演示，通常不是演示工具的问题，是文档本身没讲清楚。",[10,957,958],{},"我见过一类项目文档，有 20 多份模板文件，但只要换个配色其他全一样。这说明什么？说明那些\"模板\"实际上没有结构差异，只有视觉差异。结构不清，自然转不出演示。或者反过来说，如果要证明自己的文档结构是清晰的，试试能不能一键转成演示——转不出来就是信号，说明需要先梳理文档本身。",[17,960,961],{"id":961},"工具的硬约束",[10,963,964],{},"演示不同于其他产物，有独特的硬约束。我的工具选择了 1920×1080 的固定舞台，所有动画用 GSAP 时间轴。这些看起来像限制，实际上是为了保证可靠性。",[10,966,967],{},"固定舞台尺寸意味着设计者写风格预设时要算好留白和排版，不能寄希望于\"容器自适应就行了\"。GSAP 时间轴的特点是能在任意时间点求值（渲染管线会 seek 到任意帧截图），不能用 CSS animation 这种依赖真实时间流逝的东西。这限制了动效，但换来的是确定性——动效一定会在预期时间点发生，不会因为网络卡而错位。",[10,969,970],{},"生成流程分两个阶段，有个细节很实用。第一阶段只生成 3 个风格的封面单页，第二阶段才生成整份演示文稿。这个设计看似多一步，实际上优雅地解决了两个问题。一是给用户选择风格的机会（不是非此即彼的\"生成或不生成\"）；二是掩盖异步生图的等待时间（生图 1-2 分钟，正好被用户在选择封面时吸收了）。",[17,972,973],{"id":973},"从文档能否转演示看结构清晰度",[10,975,976],{},"最后回到起点。为什么要做这个工具？",[10,978,979],{},"表面原因是 200 多份文档用演讲方式讲会更有力。深层原因是这个过程本身就是对文档质量的检验。",[10,981,982],{},"一份设计文档如果结构清晰——背景交代得清，方案对比得充分，架构图画得明确，取舍理由说得透彻——那转成演示就是平移内容，不费劲。转不出来、或者转出来很别扭，就是信号，说明某个环节讲得不够好。",[10,984,985],{},"这个反馈机制比任何 review 注释都直白。\"这个表述为什么转不成幻灯片？\"往往能逼出真实的问题——\"哦，因为我其实还没想清楚这个方案为什么比另一个好\"。",{"title":163,"searchDepth":271,"depth":271,"links":987},[988,989,990,991],{"id":886,"depth":271,"text":886},{"id":923,"depth":271,"text":924},{"id":961,"depth":271,"text":961},{"id":973,"depth":271,"text":973},"2026-07-17",{},"\u002F2026-07-17",{"title":872,"description":877},"2026-07-17-把设计文档变成可讲的演示","不做通用PPT编辑器，只做文档→演示的单向转换；关键在识别文档的稳定结构。",[999,1000,1001,1002,1003],"演示文稿","文档结构","设计工具","HTML","GSAP","Reh87_TKXZ8nl5BhDhHzVOONAzwRS-Im8LTYrIfeuNk",1785406912236]