[{"data":1,"prerenderedAt":1398},["ShallowReactive",2],{"\u002F2026-04-12":3,"\u002F2026-04-12-rel":815},{"id":4,"title":5,"body":6,"column":798,"date":799,"description":12,"extension":800,"hero_image":801,"meta":802,"navigation":803,"path":804,"seo":805,"series_id":801,"severity":801,"stem":806,"summary":807,"tags":808,"__hash__":814},"posts\u002F2026-04-12-三端联调的顺序问题.md","三端并行开发，谁先接谁？",{"type":7,"value":8,"toc":786},"minimark",[9,13,17,20,23,41,44,71,82,85,90,96,102,108,122,127,137,140,144,149,154,159,170,174,203,208,241,252,374,377,381,386,391,395,403,407,418,422,462,473,483,503,506,510,515,520,524,539,543,554,558,597,600,614,617,620,652,657,660,755,758,761,775,782],[10,11,12],"p",{},"三端（后台、小程序、设备）并行开发时，联调顺序直接决定返工量。这里讲的是 ToyNet 项目的实践：为什么不能先接设备再接小程序，以及各阶段的桩数据策略。",[14,15,16],"h2",{"id":16},"为什么后台必须先行",[10,18,19],{},"后台 API 是唯一的事实源。小程序和设备都依赖后台数据，这种依赖关系不可逆转。",[10,21,22],{},"ToyNet 的架构很清楚：",[24,25,26,30,33],"ul",{},[27,28,29],"li",{},"后台提供 REST API（用户、商品、订单、设备管理）和 MQTT 消息队列（设备指令下发）",[27,31,32],{},"小程序是 API 消费者（调用 REST 接口）",[27,34,35,36,40],{},"设备是 MQTT 消费者（订阅 ",[37,38,39],"code",{},"toynet\u002Fdevice\u002F{deviceSn}\u002Fcommand"," 主题、执行命令、上报状态）",[10,42,43],{},"这意味着小程序开发的第一天就需要后台 API 存在。不是非得完全实现，但至少得有契约——定义好接口名、参数、返回值格式。设备也是一样，需要后台先把 MQTT 消息格式定下来。",[10,45,46,50,51,54,55,58,59,62,63,66,67,70],{},[47,48,49],"strong",{},"反序的代价","。如果先做设备再做小程序，会发生什么？小程序开发期间没有可读的数据。具体例子：小程序要显示\"设备列表\"和\"设备状态\"，调的是 ",[37,52,53],{},"\u002Fapi\u002Fv1\u002Fdevices\u002Flist","。如果后台这个接口还不存在，小程序工程师得猜测设备状态的格式——也许是 ",[37,56,57],{},"{ status: 'online' }","，也许是 ",[37,60,61],{},"{ online: true }","，也许还要加 ",[37,64,65],{},"battery"," 和 ",[37,68,69],{},"lastSeenAt","。用错了格式渲染页面，即使设备硬件工作正常，小程序也没法验证这个流程。几周后后台实现了，返回格式和小程序猜的不一样，小程序要改、测试、再验证设备——这一圈返工没有价值，本可以避免。",[10,72,73,74,77,78,81],{},"类似的问题还会出现在 MQTT 指令格式上。先做设备时，硬件工程师定义了命令格式，比如 ",[37,75,76],{},"{ action: 'play', audioId: 123 }","。后台做到这一步时，根据产品需求改成了 ",[37,79,80],{},"{ cmd: 'play_audio', params: { id: 123 } }","。小程序要跟着改，设备固件也要改。如果顺序对了，后台先定好格式，这些改动就不会出现。",[14,83,84],{"id":84},"可行的分阶段顺序",[86,87,89],"h3",{"id":88},"第1阶段后台自测","第1阶段：后台自测",[10,91,92,95],{},[47,93,94],{},"目标","：后台 6 个核心模块全部能跑，包括鉴权、设备管理、商品、订单、AI、MQTT。",[10,97,98,101],{},[47,99,100],{},"在 ToyNet 里的对应","：Plan 1–6（2026-04-09 到 2026-04-10）。",[10,103,104,107],{},[47,105,106],{},"桩数据策略","：",[24,109,110,113,116,119],{},[27,111,112],{},"用 Jest 单测 + 集成测试验证每个 service",[27,114,115],{},"MongoDB 用 mongodb-memory-server 在内存中启动，每次测试自动清理",[27,117,118],{},"不需要真实设备，MQTT broker 可以离线或用 mock 实现",[27,120,121],{},"test fixtures 里写好标准数据：用户对象、设备对象、订单对象，每个测试复用",[10,123,124,107],{},[47,125,126],{},"具体做法",[128,129,134],"pre",{"className":130,"code":132,"language":133},[131],"language-text","server\u002Ftests\u002Funit\u002Fmodules\u002Fdevices\u002Fdevices.service.test.js\nserver\u002Ftests\u002Fintegration\u002Fdevices.test.js\nnpm test -- --coverage\n","text",[37,135,132],{"__ignoreMap":136},"",[10,138,139],{},"验收标准是覆盖率 ≥80%，所有 API 路由至少走过一遍。这时候后台是独立的黑盒，没有外部依赖。",[86,141,143],{"id":142},"第2阶段后台小程序","第2阶段：后台+小程序",[10,145,146,148],{},[47,147,94],{},"：小程序能从后台读数据、写数据，覆盖游客浏览和登录用户操作。",[10,150,151,153],{},[47,152,100],{},"：Phase 1–3（2026-04-12 到 2026-04-13 期间）。",[10,155,156,107],{},[47,157,158],{},"依赖关系",[24,160,161,164,167],{},[27,162,163],{},"Phase 1（骨架）需要后台 Plan 1（基础：auth、middleware、request client）",[27,165,166],{},"Phase 2（首页+商品）需要后台 Plan 1 + Plan 4（products API）",[27,168,169],{},"Phase 3（下单+支付）需要后台 Plan 1 + Plan 4（orders\u002Fpayments API）",[10,171,172,107],{},[47,173,106],{},[24,175,176,179,182,189,200],{},[27,177,178],{},"后台完全真实：真实 MongoDB，真实 API 服务",[27,180,181],{},"小程序在微信开发者工具中：用假 WeChat openid（例如 test-user-001）写死在开发配置里，调用真实后台 API",[27,183,184,185,188],{},"支付模块：后台在测试环境下返回 mock 支付结果（检测 ",[37,186,187],{},"NODE_ENV=development"," 时跳过真实微信支付 SDK，直接返回 success）",[27,190,191,192,195,196,199],{},"认证用工具函数 ",[37,193,194],{},"requireLogin()","，小程序 service 的 ",[37,197,198],{},"request()"," 自动读 localStorage 里的 fake token，后台鉴权中间件识别这个 test token 就返回授权",[27,201,202],{},"不真实的数据（设备状态、MQTT 消息）用固定 fixture 返回",[10,204,205,107],{},[47,206,207],{},"具体流程",[209,210,211,218,224,231,238],"ol",{},[27,212,213,214,217],{},"启动后台 dev 服务器 ",[37,215,216],{},"npm run dev","，默认连接本地 MongoDB",[27,219,220,221],{},"打开微信开发者工具，小程序项目配置指向 ",[37,222,223],{},"http:\u002F\u002Flocalhost:3000",[27,225,226,227,230],{},"小程序登录页点\"授权\"，调后台 ",[37,228,229],{},"\u002Fauth\u002Flogin","，后台返回 token，小程序存入 localStorage",[27,232,233,234,237],{},"跳转首页，调 ",[37,235,236],{},"\u002Fproducts?limit=10","，后台返回商品列表，小程序渲染轮播图",[27,239,240],{},"找到接口缺失或返回格式不对，后台补齐、更新 Swagger 文档，小程序跟着改",[10,242,243,244,247,248,251],{},"这个阶段没有真实设备，完全是前后端的对接。设备相关的代码（比如 Phase 2 里的 ",[37,245,246],{},"services\u002Fdevices.ts"," 里的 ",[37,249,250],{},"getDeviceList()","）是空实现或返回 mock 数据，例如：",[128,253,257],{"className":254,"code":255,"language":256,"meta":136,"style":136},"language-typescript shiki shiki-themes github-light github-dark","export async function getDeviceList() {\n  if (process.env.NODE_ENV === 'test') {\n    return { list: [{ id: 'mock-dev-1', name: '玩具1', status: 'online' }], total: 1 }\n  }\n  return request({ url: '\u002Fdevices', skipAuth: false })\n}\n","typescript",[37,258,259,282,305,338,344,368],{"__ignoreMap":136},[260,261,264,268,271,274,278],"span",{"class":262,"line":263},"line",1,[260,265,267],{"class":266},"szBVR","export",[260,269,270],{"class":266}," async",[260,272,273],{"class":266}," function",[260,275,277],{"class":276},"sScJk"," getDeviceList",[260,279,281],{"class":280},"sVt8B","() {\n",[260,283,285,288,291,295,298,302],{"class":262,"line":284},2,[260,286,287],{"class":266},"  if",[260,289,290],{"class":280}," (process.env.",[260,292,294],{"class":293},"sj4cs","NODE_ENV",[260,296,297],{"class":266}," ===",[260,299,301],{"class":300},"sZZnC"," 'test'",[260,303,304],{"class":280},") {\n",[260,306,308,311,314,317,320,323,326,329,332,335],{"class":262,"line":307},3,[260,309,310],{"class":266},"    return",[260,312,313],{"class":280}," { list: [{ id: ",[260,315,316],{"class":300},"'mock-dev-1'",[260,318,319],{"class":280},", name: ",[260,321,322],{"class":300},"'玩具1'",[260,324,325],{"class":280},", status: ",[260,327,328],{"class":300},"'online'",[260,330,331],{"class":280}," }], total: ",[260,333,334],{"class":293},"1",[260,336,337],{"class":280}," }\n",[260,339,341],{"class":262,"line":340},4,[260,342,343],{"class":280},"  }\n",[260,345,347,350,353,356,359,362,365],{"class":262,"line":346},5,[260,348,349],{"class":266},"  return",[260,351,352],{"class":276}," request",[260,354,355],{"class":280},"({ url: ",[260,357,358],{"class":300},"'\u002Fdevices'",[260,360,361],{"class":280},", skipAuth: ",[260,363,364],{"class":293},"false",[260,366,367],{"class":280}," })\n",[260,369,371],{"class":262,"line":370},6,[260,372,373],{"class":280},"}\n",[10,375,376],{},"这样既能让页面渲染通过，又不依赖真实设备存在。",[86,378,380],{"id":379},"第3阶段后台设备","第3阶段：后台+设备",[10,382,383,385],{},[47,384,94],{},"：设备能接收后台指令、上报状态。小程序和设备可以不通信，但后台要能驱动设备。",[10,387,388,390],{},[47,389,100],{},"：Phase 4（设备详情、扫码绑定）。",[10,392,393,107],{},[47,394,158],{},[24,396,397,400],{},[27,398,399],{},"Phase 4 需要后台 Plan 3（devices\u002FMQTT 模块）",[27,401,402],{},"Phase 4 不需要 Phase 2\u002F3 完全完成，但需要后台 device 模型已定义",[10,404,405,107],{},[47,406,106],{},[24,408,409,412,415],{},[27,410,411],{},"后台：真实 MQTT broker（EMQX 或阿里云\u002F腾讯云 MQTT 服务），真实设备在线或可控上线",[27,413,414],{},"小程序：可以继续用假数据调试 UI，设备相关的 API 调用和页面暂时 mock 返回",[27,416,417],{},"关键路径是后台 → MQTT broker → 设备的指令下发和状态回复，这一段必须真实",[10,419,420,107],{},[47,421,207],{},[209,423,424,427,433,439],{},[27,425,426],{},"启动后台 MQTT 服务（连接真实 broker）",[27,428,429,430,432],{},"连接真实设备到 broker（设备订阅 ",[37,431,39],{}," 主题）",[27,434,435,436],{},"后台代码调用 ",[37,437,438],{},"publishDeviceCommand(deviceSn, { cmd: 'set_volume', params: { volume: 50 } })",[27,440,441,442],{},"验证：\n",[24,443,444,447,450,453,459],{},[27,445,446],{},"MQTT broker 收到消息",[27,448,449],{},"设备接收到消息",[27,451,452],{},"设备执行（音量调到 50）",[27,454,455,456],{},"设备上报反馈到后台 ",[37,457,458],{},"toynet\u002Fdevice\u002F{deviceSn}\u002Fstatus",[27,460,461],{},"后台记录状态变化",[10,463,464,465,468,469,472],{},"这个阶段会发现的问题通常是：MQTT topic 路径理解偏差（后台发到 ",[37,466,467],{},"device\u002F123\u002Fcmd","，设备监听的是 ",[37,470,471],{},"device\u002F123\u002Fcommand","）、命令参数类型错误（发的是 string 但设备期望 number）、设备离线处理逻辑不对、消息丢失重试机制缺失。这些问题不会影响后台+小程序的对接（那是上一阶段已验证的），只能来自设备硬件的理解差异。",[10,474,475,478,479,482],{},[47,476,477],{},"离线设备的处理","。Plan 3 设计了一个细节：设备离线时，",[37,480,481],{},"POST \u002Fdevices\u002F:id\u002Fcommand"," 仍然接受请求，返回 422 错误码 42240，告诉客户端\"设备离线，指令已入队但不保证送达\"。这样做是为了解耦小程序和设备的强依赖关系——小程序不用关心设备是否在线，后台负责队列和重试。真实测试时：",[209,484,485,488,491,494,497,500],{},[27,486,487],{},"准备一台设备，上线后发一条指令，确认执行成功",[27,489,490],{},"拔掉设备电源，让它离线",[27,492,493],{},"再发一条指令，应该收到 42240 错误，指令被入队",[27,495,496],{},"给设备重新通电，它上线",[27,498,499],{},"确认设备优先处理队列里的待发指令",[27,501,502],{},"验证指令执行顺序和完整性",[10,504,505],{},"这个测试不复杂，但很关键。它验证了后台的异步处理能力，这对小程序的用户体验很重要——用户不会在\"点了按钮设备没反应\"时尴尬，后台会自动重试。",[86,507,509],{"id":508},"第4阶段三端","第4阶段：三端",[10,511,512,514],{},[47,513,94],{},"：小程序操作 → 后台处理 → 设备执行 → 实时反馈给小程序，完整闭环。",[10,516,517,519],{},[47,518,100],{},"：Phase 5（录音上传、MQTT 推送到设备）。",[10,521,522,107],{},[47,523,158],{},[24,525,526,529,532],{},[27,527,528],{},"Phase 5 需要 Phase 4 的设备模块已稳定",[27,530,531],{},"需要后台 Plan 5（AI 对话）+ Plan 3（MQTT 服务）已完成",[27,533,534,535,538],{},"需要设备硬件支持 ",[37,536,537],{},"play_custom_audio"," 命令（可能需要固件升级，这是前期沟通清楚的）",[10,540,541,107],{},[47,542,106],{},[24,544,545,548,551],{},[27,546,547],{},"全真实：真实小程序用户、真实后台、真实 MQTT broker、真实设备",[27,549,550],{},"不用 mock，不用假数据",[27,552,553],{},"唯一的前置条件是：设备硬件能力清单已确认（支持哪些命令、指令超时时间、失败重试策略）",[10,555,556,107],{},[47,557,207],{},[209,559,560,563,569,572,582,585,591,594],{},[27,561,562],{},"小程序用户点\"录制\"按钮，录音 5 秒钟",[27,564,565,566],{},"上传音频文件到后台 ",[37,567,568],{},"POST \u002Fapi\u002Fv1\u002Frecordings\u002Fupload",[27,570,571],{},"后台接收、持久化音频（存 COS 或本地），返回音频 URL",[27,573,574,575,578,579],{},"后台业务逻辑获取该用户绑定的所有设备，对每一台设备发 MQTT 指令：",[37,576,577],{},"device\u002F{sn}\u002Fcommand"," 消息内容 ",[37,580,581],{},"{ cmd: 'play_custom_audio', params: { url: 'https:\u002F\u002F...', duration: 5 } }",[27,583,584],{},"设备收到消息，下载音频，开始播放",[27,586,587,588],{},"设备每秒上报播放状态（进度、音量、是否完成）到后台 MQTT 主题 ",[37,589,590],{},"device\u002F{sn}\u002Fstatus",[27,592,593],{},"后台通过 WebSocket 或 Server-Sent Event 推送状态更新给小程序",[27,595,596],{},"小程序实时显示设备播放进度条、播放完成提示",[10,598,599],{},"这个阶段是三端各司其职的验证。一旦前三个阶段都通过了，这里通常没有架构问题——问题可能来自：",[24,601,602,605,608,611],{},[27,603,604],{},"硬件支持度（设备固件不支持某个命令）",[27,606,607],{},"网络延迟（MQTT 消息延迟超过 1 秒）",[27,609,610],{},"音频格式不兼容（设备不支持某种编码）",[27,612,613],{},"存储容量（音频文件过大）",[10,615,616],{},"但这些都不是联调顺序的问题，都是已知的工程约束。",[14,618,619],{"id":619},"为什么这个顺序正确",[209,621,622,628,634,640,646],{},[27,623,624,627],{},[47,625,626],{},"依赖关系清晰","：每一步都在前一步的基础上加新功能，不会无限反复。后台是基座，小程序依赖后台 API，设备依赖后台 MQTT，没有循环依赖。",[27,629,630,633],{},[47,631,632],{},"快速反馈","：每个阶段都能独立验证，不用等所有功能都做完。第 1 阶段后端工程师就能 100% 确认\"后台自己能跑\"。第 2 阶段小程序工程师就能 100% 确认\"小程序能调后台 API\"。没有\"可能、应该、估计\"。",[27,635,636,639],{},[47,637,638],{},"问题隔离","：如果第 N 阶段失败，问题一定在这一步加的新东西里，前面的层都已经验证过。例如第 3 阶段如果设备收不到指令，不用怀疑后台 API（已在第 2 阶段通过了），直接看 MQTT 通信。这样定位问题的时间从\"无限\"缩短到\"可控\"。",[27,641,642,645],{},[47,643,644],{},"资源利用","：设备成本高（采购、维护、通电、运输、故障修理）。如果先做设备再做小程序，小程序开发期间所有设备都得 24\u002F7 开着，浪费电，加速磨损。如果后台+小程序联调期间设备全关着，开发速度反而快。",[27,647,648,651],{},[47,649,650],{},"并行开发","：三个角色（后端、小程序、硬件）可以真正并行。后端做 Plan 1-2 时，小程序工程师看 Swagger 文档设计页面框架；硬件工程师看 MQTT topic 设计固件消息处理。等后端出了初版 API，小程序才开始集成；硬件到了 Plan 3 才开始真实测试。如果反序，硬件工程师得等小程序做完才能知道\"产品需要什么\"，这是典型的串行瓶颈。",[10,653,654,656],{},[47,655,49],{},"。如果先做设备再做小程序，流程变成：设备完成 → 小程序根据设备现状开发 → 发现产品需求与设备能力不匹配 → 改硬件或改产品需求 → 小程序重做。这一圈返工，设备的每次改动都要测试，成本指数级上升。而且到了后期，后台 API 还没对接上来，小程序用 mock 数据做了一堆假验证，等真接口来了又全得改。",[14,658,659],{"id":659},"每个阶段的验收输出物",[24,661,662,695,717,736],{},[27,663,664,107,667],{},[47,665,666],{},"第1阶段",[24,668,669,672,675,678,684],{},[27,670,671],{},"后台源码（完整目录结构）",[27,673,674],{},"Jest 覆盖率报告（≥80%）",[27,676,677],{},"Swagger API 文档（自动生成，所有路由都有示例）",[27,679,680,683],{},[37,681,682],{},".env.example"," 和数据库初始化脚本",[27,685,686,687,690,691,694],{},"通过标准：",[37,688,689],{},"npm test"," 全部 PASS，",[37,692,693],{},"npm start"," 能启动",[27,696,697,107,700],{},[47,698,699],{},"第2阶段",[24,701,702,705,708,711,714],{},[27,703,704],{},"小程序源码（所有 Phase 1-3 功能代码）",[27,706,707],{},"Jest 测试报告（services 层覆盖率 ≥80%）",[27,709,710],{},"真机测试截图和视频（登录、首页、搜索、下单、支付成功页）",[27,712,713],{},"小程序 Swagger 文档（调用了哪些后台接口）",[27,715,716],{},"通过标准：真机打开小程序能正常浏览商品、能登录、能加购",[27,718,719,107,722],{},[47,720,721],{},"第3阶段",[24,723,724,727,730,733],{},[27,725,726],{},"设备 MQTT 通信日志（broker 收到的所有消息 + 时间戳）",[27,728,729],{},"设备指令执行记录（对于每条指令记录：发出时刻、设备接收时刻、执行结果、完成时刻）",[27,731,732],{},"离线重试测试报告（设备离线时发指令、再上线的执行顺序）",[27,734,735],{},"通过标准：指令下发成功率 100%，设备离线重试有效",[27,737,738,107,741],{},[47,739,740],{},"第4阶段",[24,742,743,746,749,752],{},[27,744,745],{},"端到端完整流程录屏 3 分钟（从小程序用户点录制，到设备播放完成，中间显示所有 MQTT 消息）",[27,747,748],{},"性能指标（从小程序点击到设备反馈的延迟、音频传输成功率）",[27,750,751],{},"边界情况测试报告（设备离线、网络断线、文件过大、格式不支持等）",[27,753,754],{},"通过标准：真机录音并推送到设备，设备成功播放，小程序显示进度",[14,756,757],{"id":757},"何时切换到真实数据",[10,759,760],{},"每个阶段 mock 和真实的分界线很明确，不要超前也不要延后：",[24,762,763,769],{},[27,764,765,768],{},[47,766,767],{},"不要延后","：后台 API 已写好了还继续用 mock，小程序就永远发现不了接口格式不匹配的问题。",[27,770,771,774],{},[47,772,773],{},"不要超前","：设备硬件还没到，不用强行对接真实 MQTT broker，浪费时间调试网络问题。",[10,776,777,778,781],{},"经验是：",[47,779,780],{},"看依赖关系","。小程序需要后台 API 才能测，但小程序的 UI 测试不需要真设备；设备需要后台 MQTT 才能下指令，但这时候小程序可以继续 mock。按这个粒度切分，三个角色都不会被卡住。",[783,784,785],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .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":136,"searchDepth":284,"depth":284,"links":787},[788,789,795,796,797],{"id":16,"depth":284,"text":16},{"id":84,"depth":284,"text":84,"children":790},[791,792,793,794],{"id":88,"depth":307,"text":89},{"id":142,"depth":307,"text":143},{"id":379,"depth":307,"text":380},{"id":508,"depth":307,"text":509},{"id":619,"depth":284,"text":619},{"id":659,"depth":284,"text":659},{"id":757,"depth":284,"text":757},"工程手记","2026-04-12","md",null,{},true,"\u002F2026-04-12",{"title":5,"description":12},"2026-04-12-三端联调的顺序问题","三端并行开发时顺序错了导致大量返工；可行顺序是后台自测→后台+小程序→后台+设备→三端，理由是后台是唯一事实源。",[809,810,811,812,813],"联调","项目管理","微信小程序","物联网","MQTT","K1xidurgY1T0nHQz_Hdm21bPL984lc8_rPJD3qsgp80",[816,1120,1264],{"id":817,"title":818,"body":819,"column":798,"date":1107,"description":823,"extension":800,"hero_image":801,"meta":1108,"navigation":803,"path":1109,"seo":1110,"series_id":801,"severity":801,"stem":1111,"summary":1112,"tags":1113,"__hash__":1119},"posts\u002F2026-07-29-一年八千次提交AI辅助开发工作流.md","一年八千次提交，我是怎么干的",{"type":7,"value":820,"toc":1090},[821,824,827,830,834,837,840,843,847,850,853,867,870,874,877,880,883,887,890,893,913,916,920,923,934,937,941,944,958,961,964,968,971,974,978,981,984,987,991,994,997,1020,1026,1029,1036,1039,1042,1045,1052,1055,1058,1084,1087],[10,822,823],{},"2026 上半年，主要项目累计提交 7787 次（其中 newCodex 因为是 fork 项目，1922 次提交含上游历史；纯新增代码的项目是 yun-claude、yun-claw、new-openclaw 等）。提交数看起来多，但每个提交背后的价值不在数量，而在流程的可重复性。这篇文章把工作流写清楚。",[14,825,826],{"id":826},"流程的六个环节",[10,828,829],{},"整个开发周期分六步：需求澄清 → 设计文档审阅 → 拆实施计划 → 分阶段实现 → 代码审查 → 故障归档。每一步都有明确的输入输出和验证方式。",[86,831,833],{"id":832},"_1-需求澄清","1. 需求澄清",[10,835,836],{},"开始前问清楚，不要猜。典型问题：这个功能要处理哪些用户场景？边界条件是什么？现有系统的哪部分会受影响？",[10,838,839],{},"这一步的产出是一份结构化的需求文档，包括功能范围、约束条件、风险假设。不是长篇幅的铺垫，而是一份清单式的澄清记录。",[10,841,842],{},"在 new-openclaw 项目里，这一步通常是用 Claude 的 superpowers:brainstorming skill 来展开。提问方式很重要：我会列出已知条件，然后问\"这个设计下会遇到什么问题？\"而不是\"你觉得应该怎么做？\"让 AI 在我的约束框架内工作。",[86,844,846],{"id":845},"_2-设计文档与人工审阅","2. 设计文档与人工审阅",[10,848,849],{},"这是整个流程里最关键的检查点。花一小时审一份 200 行的设计文档，比花五小时审一份 2000 行的代码便宜得多。而且回头修改设计的成本远低于修改实现。",[10,851,852],{},"new-openclaw 项目现有 30 份设计文档（specs 目录），每份文档都包括：",[24,854,855,858,861,864],{},[27,856,857],{},"范围：这个设计覆盖什么、不覆盖什么",[27,859,860],{},"决策与权衡：为什么选这个方案，放弃了什么",[27,862,863],{},"接口契约：如果涉及多个模块，清晰定义每个边界",[27,865,866],{},"风险清单：已知的坑和防护措施",[10,868,869],{},"写完设计文档以后，我会读一遍、问几个\"为什么\"，然后提出修改意见。这一步排除了 80% 的方向错误。常见的修改方向有：缩小范围（第一个版本不用处理那么多边界情况）、明确约束（系统资源、网络延迟、并发数的假设）、补充防护（熔断、限流、幂等）。",[86,871,873],{"id":872},"_3-实施计划与-dod-定义","3. 实施计划与 DoD 定义",[10,875,876],{},"设计文档定下来以后，拆成实施计划。计划的粒度是\"一个可验证的功能单元\"——通常是一个小时到半天的工作量，完成后能单独验证成功。",[10,878,879],{},"计划文档里必须写清完成定义（DoD，Definition of Done）。不是\"实现登录功能\"，而是\"写出能拒绝无效格式的登录端点、覆盖单点故障下的重试、有端到端的冒烟测试\"。",[10,881,882],{},"new-openclaw 项目现有 36 份实施计划（plans 目录），跨度从一周的 S0 阶段（骨架 + Mock 后台）到数周的 S1 阶段（真实业务后台）。每份计划都带着清晰的 checklist，这样我在执行时能随时问 Claude：\"下一步应该是什么？\"而不是脑子里模糊地记着进度。",[86,884,886],{"id":885},"_4-分阶段实现与逐步验证","4. 分阶段实现与逐步验证",[10,888,889],{},"有了计划以后，按顺序实现。关键是每一步都要有验证：单元测试、集成测试、或者一个小的 end-to-end 冒烟测试。验证不通过就停在这一步，不往下推。",[10,891,892],{},"这一步会用到三个代理：",[24,894,895,901,907],{},[27,896,897,900],{},[47,898,899],{},"superpowers:test-driven-development"," —— 先写测试，再写实现",[27,902,903,906],{},[47,904,905],{},"superpowers:subagent-driven-development"," —— 复杂任务拆成独立的子任务，并行推进",[27,908,909,912],{},[47,910,911],{},"superpowers:systematic-debugging"," —— 遇到测试失败，用这个代理追根溯源，不要盲目修改代码",[10,914,915],{},"实施过程中如果发现设计假设错了（比如某个接口响应时间远超预期，或者并发场景下出现竞态条件），就停下来回到第 2 步重新审视设计，而不是继续往下推。",[86,917,919],{"id":918},"_5-代码审查","5. 代码审查",[10,921,922],{},"实现完成以后，不是立即合并，而是过一遍 code-reviewer 代理。审查的重点不在代码风格（那个自动工具做），而在：",[24,924,925,928,931],{},[27,926,927],{},"这段代码实现的是设计文档里的哪一部分？偏离了吗？",[27,929,930],{},"错误处理有没有遗漏？边界情况有没有考虑？",[27,932,933],{},"有没有意外改动无关的代码？",[10,935,936],{},"审查通常能抓住两类问题。一类是逻辑问题：某个条件判断漏了一个分支，或者并发场景下两个操作的顺序反了。另一类是\"设计和实现对不上\"：实现了一个设计里没提到的特性，或者某个约束（比如\"这个值不能为空\"）没在代码里强制。",[86,938,940],{"id":939},"_6-故障归档","6. 故障归档",[10,942,943],{},"系统上线以后，bug 是难免的。重要的是怎么处理它。new-openclaw 项目有一套 bug 知识库规范（CLAUDE.md 里定义），每个 bug 修好以后都要新建一份独立文档，包括：",[24,945,946,949,952,955],{},[27,947,948],{},"现象和复现路径",[27,950,951],{},"根因分析：为什么会发生，触发链路是什么",[27,953,954],{},"修复方法：改了什么，为什么选这个方案",[27,956,957],{},"预防措施：代码改进、测试添加、还是构建期检查",[10,959,960],{},"80 份 bug 文档（截至 5 月中旬）不是问题的多，而是追根溯源的记录的多。下次遇到类似现象，能直接查库而不是重新排查。",[14,962,963],{"id":963},"三条铁律",[86,965,967],{"id":966},"_1-假设必须显式声明","1. 假设必须显式声明",[10,969,970],{},"不要猜。不清楚的地方就问，把问题写成澄清清单。\"这个 API 能处理多大的请求体？\"、\"离线场景下要缓存多长时间？\"、\"错误重试间隔是指数退避还是固定时间？\"。",[10,972,973],{},"这些问题看起来小，但决定了实现的复杂度和测试用例的多少。猜错了会导致前期设计精美，但方向错误，后面要推倒重来。",[86,975,977],{"id":976},"_2-修改必须精确","2. 修改必须精确",[10,979,980],{},"只改需要改的部分。这听起来像常识，但在实际工作中容易出现\"顺手改一下边上的代码\"的情况 —— 格式不规范了，变量命名不一致了，某个函数太长了，\"顺便\"重构一下。",[10,982,983],{},"结果是一个改动影响了五个文件，代码审查花了双倍时间，引入了新 bug 的风险。",[10,985,986],{},"规则是：改动必须对应需求的某一行。格式、风格、无关的重构，单独立项，不要混在功能改动里。",[86,988,990],{"id":989},"_3-成功标准必须可验证","3. 成功标准必须可验证",[10,992,993],{},"\"添加验证\"这个说法是模糊的。改写成：\"写出一个测试用例，输入非法邮箱格式，验证 API 返回 400；输入合法邮箱，验证返回 200 和预期数据结构\"。",[10,995,996],{},"每个 plan 文档里的 DoD 都是这样写的。拿一个阶段做例子：",[998,999,1000,1003],"blockquote",{},[10,1001,1002],{},"S0 阶段的 DoD：",[209,1004,1005,1008,1014,1017],{},[27,1006,1007],{},"openapi\u002Fapi-v1.yaml 包含 spec 全部 11 个端点的字段级 schema，可被 swagger-ui 加载 ✓",[27,1009,1010,1011,1013],{},"backend\u002F Mock 服务 ",[37,1012,693],{}," 后能响应全部端点，返回符合契约的 mock 数据 ✓",[27,1015,1016],{},"launcher\u002F Rust 项目能编译为 launcher.exe，运行后完成所有阶段扫描并输出 diagnostics.json ✓",[27,1018,1019],{},"至少一个 end-to-end 冒烟测试通过（launcher 上报 → mock 后台收到 → 审计日志记录） ✓",[10,1021,1022,1023,1025],{},"每一条都能通过一个具体的命令来验证。\"能工作\"太模糊，\"运行 ",[37,1024,689],{}," 且所有测试通过\"才是可验证的。",[14,1027,1028],{"id":1028},"流程失效的情况",[10,1030,1031,1032,1035],{},"这套流程在一个关键点会失效：",[47,1033,1034],{},"需求本身没想清楚时","。",[10,1037,1038],{},"我遇到过的例子是这样的。客户说\"要支持 USB 存储检测\"，这个需求很清楚，所以设计文档写了 20 多页，规划了三个阶段，列出了 30 多个 test case。然后两周后，客户补充说\"哦对了，还要处理网络驱动器\"。",[10,1040,1041],{},"这时前面的设计和计划都要回头改。检测逻辑复杂了，测试场景翻倍，阶段划分要调整。这不是流程的问题，这是需求的问题。流程本身反而帮助我及时暴露了这个风险 —— 如果没有设计文档，可能要到代码审查阶段，甚至系统上线以后才发现这个遗漏。",[10,1043,1044],{},"应对办法是在第 1 步（需求澄清）多花时间。列出你想到的所有场景，问\"还有其他我忽略的情况吗？\"。不是要求完全预测未来，而是把已知的不确定性显式写出来，而不是假设需求是固定的。",[10,1046,1047,1048,1051],{},"另一个失效的情况是",[47,1049,1050],{},"设计和实现的沟通不畅","。如果设计文档是给另一个人读的（或者给 AI 代理读的），但执行者没有理解透彻，实现出来会偏离设计。预防办法是在开始实施前，再过一遍设计文档，确认\"我清楚要做什么\"。",[14,1053,1054],{"id":1054},"提交数字背后的故事",[10,1056,1057],{},"为什么能积累 7787 次提交？不是因为每次都在写新功能。真实的分布大概是：",[24,1059,1060,1066,1072,1078],{},[27,1061,1062,1065],{},[47,1063,1064],{},"功能实现","：40%",[27,1067,1068,1071],{},[47,1069,1070],{},"设计文档编写和迭代","：25%",[27,1073,1074,1077],{},[47,1075,1076],{},"测试编写","：20%",[27,1079,1080,1083],{},[47,1081,1082],{},"bug 修复与回归测试","：15%",[10,1085,1086],{},"关键是这些提交都有上下文。每个提交的 message 都指向一个设计文档或一个 plan 的某个环节，或者一个 bug 记录。下次有问题要追查根因时，能快速定位到那个提交，看当时的设计决策是什么。",[10,1088,1089],{},"另外，有 80 份 bug 记录和 66 份设计 + 计划文档（30 specs + 36 plans）这件事本身说明了一点：文档不是负担，文档是工作的实际产出。代码只是文档的一个落地形式。",{"title":136,"searchDepth":284,"depth":284,"links":1091},[1092,1100,1105,1106],{"id":826,"depth":284,"text":826,"children":1093},[1094,1095,1096,1097,1098,1099],{"id":832,"depth":307,"text":833},{"id":845,"depth":307,"text":846},{"id":872,"depth":307,"text":873},{"id":885,"depth":307,"text":886},{"id":918,"depth":307,"text":919},{"id":939,"depth":307,"text":940},{"id":963,"depth":284,"text":963,"children":1101},[1102,1103,1104],{"id":966,"depth":307,"text":967},{"id":976,"depth":307,"text":977},{"id":989,"depth":307,"text":990},{"id":1028,"depth":284,"text":1028},{"id":1054,"depth":284,"text":1054},"2026-07-29",{},"\u002F2026-07-29-ai",{"title":818,"description":823},"2026-07-29-一年八千次提交AI辅助开发工作流","从需求澄清到故障归档，一套系统化的 AI 辅助开发流程：先设计文档过审，再分阶段实施验证，最后代码审查与故障存档，三条铁律支撑整个流程。",[1114,1115,1116,1117,1118],"Claude","工作流","AI 辅助开发","代码审查","文档驱动","DN7sqbGmg2UybSvRXTMLW-dj0UURXET3KsSNeW_1V9I",{"id":1121,"title":1122,"body":1123,"column":798,"date":1251,"description":1127,"extension":800,"hero_image":801,"meta":1252,"navigation":803,"path":1253,"seo":1254,"series_id":801,"severity":801,"stem":1255,"summary":1256,"tags":1257,"__hash__":1263},"posts\u002F2026-07-20-远程桌面管理器DPAPI与60万次迭代.md","密码，不该由我保管",{"type":7,"value":1124,"toc":1245},[1125,1128,1132,1135,1138,1141,1155,1158,1162,1165,1168,1171,1174,1177,1188,1191,1194,1197,1203,1209,1215,1225,1239,1242],[10,1126,1127],{},"远程服务器资料管理工具需要妥善保存账户凭据。这个工具采用两层加密设计：本机存储用 Windows DPAPI，备份使用基于密码的密钥派生。两层各有边界，理解这些边界对使用决策至关重要。",[14,1129,1131],{"id":1130},"第一层dpapi-的便利与代价","第一层：DPAPI 的便利与代价",[10,1133,1134],{},"本机存储的凭据使用 Windows DPAPI（Data Protection API）加密，由当前 Windows 用户身份保护。这是 Windows 内置的用户级加密机制，密钥由操作系统管理，与登录用户的 SID 和机器的本地安全数据库绑定。不需要用户记一个额外的主密码，启动应用即可直接使用保存的凭据。",[10,1136,1137],{},"DPAPI 的好处显而易见：启动应用即用，无需输入密码，用户体验最优。代价是它的加密密钥锁定在当前用户、当前机器。凭据无法直接迁移到另一台电脑或另一个 Windows 用户——这不是工具的限制，而是 DPAPI 的设计约束。Windows 操作系统就是这样设计的，其他工具也无法绕过。",[10,1139,1140],{},"实际操作中的含义很明确：",[24,1142,1143,1146,1149,1152],{},[27,1144,1145],{},"重装 Windows 前必须先导出备份。原有凭据会因为用户 SID 变化和机器密钥更新而无法解密，即便登录同一账户也不行。",[27,1147,1148],{},"更换电脑前需要先创建备份并妥善保存备份密码，目标电脑上导入时需要重新输入这个备份密码。",[27,1150,1151],{},"在同一电脑上切换 Windows 用户登录，旧用户的凭据对新用户完全不可见，因为加密密钥是用户级的。",[27,1153,1154],{},"多用户共享一台电脑的场景下，凭据不会跨用户暴露。",[10,1156,1157],{},"这些限制会在实际使用中暴露出来——比如忙于工作时重装系统忽略了导出，或者在公用工作电脑上多个人使用。正因为如此，工具强制要求提供备份机制。备份不是可选功能，而是必需的。",[14,1159,1161],{"id":1160},"第二层备份加密的固定迭代设计","第二层：备份加密的固定迭代设计",[10,1163,1164],{},"备份采用 PBKDF2-SHA256（基于密码的密钥派生函数 2，使用 SHA-256 哈希）加密，固定执行 600,000 次迭代。用户创建备份时设置一个密码，导入时输入这个密码。PBKDF2 通过重复应用哈希函数来增加破解难度，迭代次数越多，从密码派生密钥所需的计算时间越长，攻击者进行暴力破解也需要投入成倍的计算资源。",[10,1166,1167],{},"600,000 次迭代的来源是什么？这是一个有意识的设计决定，而不是随意选择。迭代次数越多，暴力破解的成本越高，但加密和解密的耗时也越长。设计者需要在两者之间找到平衡点：够强以抵御现代硬件的破解能力，又不能强到让普通用户的导入操作变得难以忍受。600,000 次这个数字反映的是这个平衡的结果。",[10,1169,1170],{},"为什么固定而非可配置？这是一个纪律问题。可配置听起来更灵活、更给用户掌控权，但在实践中会诱使用户为了更快的备份导入速度而降低迭代次数，从而削弱安全强度。安全不应该由便利让步。人们往往倾向于选择快速方案，尤其当他们没有安全专业知识时。固定的迭代次数消除了这种选择权，保证了所有备份都有相同的防护等级。工具的职责是做出最合理的决定，而不是把这个决定推给用户。",[14,1172,1173],{"id":1173},"性能实测与数据解读",[10,1175,1176],{},"在 Windows 11 构建机上，对 1 MiB 大小的备份数据进行加密与解密的完整往返，预热缓存后的连续五次耗时分别为：108.919 ms、104.079 ms、100.418 ms、94.933 ms、103.470 ms。中位数为 103.470 ms。这意味着从你按下\"导入备份\"到凭据被解密并加载到内存，大约需要 100 毫秒的等待时间。",[10,1178,1179,1180,1183,1184,1187],{},"这组数据收集的目的是",[47,1181,1182],{},"记录性能表现","。它提供了一个具体的参考：用户在 Windows 11 系统上可以预期导入备份的延迟大约是这个量级。这对评估工具的可用性很有用。但它明确",[47,1185,1186],{},"不","作为调整迭代次数的依据。这种表述听起来有些冗余，但它是设计纪律的一部分——必须写下来的目的是防止后续有人看到\"100 毫秒确实有点慢\"就建议降低迭代次数。防止的是这样的推理：因为性能数据显示延迟不够快，所以降低迭代次数。这个逻辑链条在安全工程中是禁止的。",[10,1189,1190],{},"相反，如果实践证明 100 毫秒对用户体验构成问题，正确的做法是要么接受这个成本作为安全性的代价，要么在未来硬件更新换代后自然加速。绝不是削弱密钥派生强度。性能和安全的权衡应该在上层的需求决策中做，而不是在密码学参数中做。",[14,1192,1193],{"id":1193},"工具的明确边界",[10,1195,1196],{},"这个工具的安全设计有明确的保护范围和限制：",[10,1198,1199,1202],{},[47,1200,1201],{},"DPAPI 层的限制","：本机凭据的安全性最终依赖于 Windows 用户密码。如果 Windows 账户被破解，攻击者用该账户登录电脑，DPAPI 解密会自动进行。如果用户以管理员身份运行工具（虽然不需要管理员权限），攻击者获得管理员权限后理论上也可能绕过某些保护。安全链的强度由最弱的一环决定——如果你的 Windows 用户密码很弱，或者电脑物理上被他人访问，DPAPI 的保护就名存实亡。",[10,1204,1205,1208],{},[47,1206,1207],{},"备份密码的强度","：导入备份时用户设置的密码决定了备份的抗暴力破解能力。PBKDF2 提供的防护再强，也无法弥补一个简单密码的缺陷。\"123456\"这样的备份密码，在 600,000 次迭代和现代 GPU 的破解能力面前，可能在几秒到几分钟内被破解。",[10,1210,1211,1214],{},[47,1212,1213],{},"系统策略的约束","：工具运行在 Windows 系统上，不会绕过任何系统级的安全机制。Windows SmartScreen 对未签名程序的警告、远程桌面连接的安全确认对话、Group Policy 的限制——这些工具都无法绕过。安装包为未签名的内部制品，Windows SmartScreen 会在首次运行时显示\"未知发布者\"警告。这不是工具的缺陷，而是系统安全策略的正常行为。",[10,1216,1217,1220,1221,1224],{},[47,1218,1219],{},"加密设计的范围","：这套两层加密设计防的是",[47,1222,1223],{},"离线攻击","——攻击者获得了备份文件或本机的加密数据，在没有用户交互的情况下尝试破解。它防不了的情况：",[24,1226,1227,1230,1233,1236],{},[27,1228,1229],{},"备份密码通过社工或偷看被直接获取",[27,1231,1232],{},"备份文件在网络传输过程中被中间人截获（如果使用不安全的传输方式）",[27,1234,1235],{},"凭据被恶意软件在内存中窃取（工具启动后、密码解密到内存这段时间内）",[27,1237,1238],{},"Windows 账户本身被已经登录电脑的恶意软件控制",[10,1240,1241],{},"对这些风险的防护需要用户在安全习惯和网络安全措施上自行补足——设置强密码、在信任的网络上操作、定期更新系统补丁、使用反恶意软件工具。",[10,1243,1244],{},"使用这套工具前要明确：本机凭据带来了便利，代价是将安全依赖在 Windows 用户身份上；备份凭据提供了迁移能力，代价是密码强度必须由用户自己把关。都不是\"一次设置永久安全\"的方案，都需要持续的安全意识和维护。",{"title":136,"searchDepth":284,"depth":284,"links":1246},[1247,1248,1249,1250],{"id":1130,"depth":284,"text":1131},{"id":1160,"depth":284,"text":1161},{"id":1173,"depth":284,"text":1173},{"id":1193,"depth":284,"text":1193},"2026-07-20",{},"\u002F2026-07-20-dpapi60",{"title":1122,"description":1127},"2026-07-20-远程桌面管理器DPAPI与60万次迭代","两层加密保护远程桌面凭据，本机用 DPAPI 便利性换易用性，备份用 PBKDF2 固定 60 万迭代；为什么迭代次数不可配置，性能数据如何解读。",[1258,1259,1260,1261,1262],"Windows","DPAPI","PBKDF2","凭据存储","加密设计","TzKUoZoFLtAsO_VcB1TOOdWOpwcoDj6AUwEdS6d7pv0",{"id":1265,"title":1266,"body":1267,"column":798,"date":1385,"description":1271,"extension":800,"hero_image":801,"meta":1386,"navigation":803,"path":1387,"seo":1388,"series_id":801,"severity":801,"stem":1389,"summary":1390,"tags":1391,"__hash__":1397},"posts\u002F2026-07-17-把设计文档变成可讲的演示.md","转不成演示的设计文档，本来就没讲清",{"type":7,"value":1268,"toc":1379},[1269,1272,1278,1281,1284,1287,1290,1308,1315,1319,1322,1328,1338,1346,1349,1352,1355,1358,1361,1364,1367,1370,1373,1376],[10,1270,1271],{},"一年积累了 200 多份设计文档，最初想法是直接拿这些文档去讲。结果发现这些东西不适合讲。设计文档是给人写的，演示文稿是给人听的，形式完全不同。",[10,1273,1274,1275,1035],{},"解决这个问题的思路不是\"写个通用 PPT 编辑器\"，而是\"从结构化文档一键生成演示\"。本质差异在这里——编辑器要处理用户的任意编辑行为，演示工具只要转换",[47,1276,1277],{},"已有的结构",[14,1279,1280],{"id":1280},"为什么选单向转换而不是编辑器",[10,1282,1283],{},"设计文档有稳定的模板：背景、方案、架构、取舍、参考。这个顺序不是随意的，恰好就是讲一个设计时的叙述顺序。",[10,1285,1286],{},"用户不需要\"先生成再改\"，需要的是\"文档秒变幻灯片\"。一旦你改，就回到编辑器的坑里去了——要支持拖拽、删除、排版，工作量爆炸，而且多数用户不会调，生成好的东西就是定版。",[10,1288,1289],{},"这个判断来自实际数据。我的工具做两个决策：",[209,1291,1292,1302],{},[27,1293,1294,1297,1298,1301],{},[47,1295,1296],{},"产物是自包含 HTML","，不是 Office 文件（",[37,1299,1300],{},".pptx"," 需要可编辑格式，门槛高；HTML 在浏览器里就能放映，自包含意味着内联了所有 CSS、JS、图片）。",[27,1303,1304,1307],{},[47,1305,1306],{},"内容由 LLM 生成，不开放编辑面板","（用一个\"预览挑选\"的两阶段流程，让用户在 3 种风格的封面里选一个，然后生成整份）。",[10,1309,1310,1311,1314],{},"这两个约束听起来很严格，实际上契合了需求的本质：",[47,1312,1313],{},"文档的目的是记录决策，演示的目的是讲述决策","。不需要演示过程中再改决策，改了就回到文档去改。",[14,1316,1318],{"id":1317},"什么样的结构能转什么样的不能","什么样的结构能转、什么样的不能",[10,1320,1321],{},"设计文档要是能一键转成演示，必须满足几个条件。",[10,1323,1324,1327],{},[47,1325,1326],{},"架构图可以直接映射","。我的演示工具支持 AI 生图，也支持用户指定图片 URL。当文档里写了架构设计时，LLM 看到这个描述会生成对应的图，然后内联到演示文稿里。舞台是固定的 1920×1080，所有图片容器有最小尺寸约束（GSAP 时间轴动画要能在任意时间点求值，不能靠动态布局）。",[10,1329,1330,1333,1334,1337],{},[47,1331,1332],{},"关键数据用表格记录最不容易犯错","。表格这种结构容易转化——",[260,1335,1336],{},"待补：具体表格如何转成演示页的实现规则","。如果文档里的数据以段落文字形式写，转成演示时就卡住了，LLM 要从自然语言反推结构。",[10,1339,1340,107,1343],{},[47,1341,1342],{},"取舍（trade-off）部分",[260,1344,1345],{},"待补：设计文档中的取舍说明如何映射成演示内容的规则",[10,1347,1348],{},"一个更深的观察：文档转不出来好演示，通常不是演示工具的问题，是文档本身没讲清楚。",[10,1350,1351],{},"我见过一类项目文档，有 20 多份模板文件，但只要换个配色其他全一样。这说明什么？说明那些\"模板\"实际上没有结构差异，只有视觉差异。结构不清，自然转不出演示。或者反过来说，如果要证明自己的文档结构是清晰的，试试能不能一键转成演示——转不出来就是信号，说明需要先梳理文档本身。",[14,1353,1354],{"id":1354},"工具的硬约束",[10,1356,1357],{},"演示不同于其他产物，有独特的硬约束。我的工具选择了 1920×1080 的固定舞台，所有动画用 GSAP 时间轴。这些看起来像限制，实际上是为了保证可靠性。",[10,1359,1360],{},"固定舞台尺寸意味着设计者写风格预设时要算好留白和排版，不能寄希望于\"容器自适应就行了\"。GSAP 时间轴的特点是能在任意时间点求值（渲染管线会 seek 到任意帧截图），不能用 CSS animation 这种依赖真实时间流逝的东西。这限制了动效，但换来的是确定性——动效一定会在预期时间点发生，不会因为网络卡而错位。",[10,1362,1363],{},"生成流程分两个阶段，有个细节很实用。第一阶段只生成 3 个风格的封面单页，第二阶段才生成整份演示文稿。这个设计看似多一步，实际上优雅地解决了两个问题。一是给用户选择风格的机会（不是非此即彼的\"生成或不生成\"）；二是掩盖异步生图的等待时间（生图 1-2 分钟，正好被用户在选择封面时吸收了）。",[14,1365,1366],{"id":1366},"从文档能否转演示看结构清晰度",[10,1368,1369],{},"最后回到起点。为什么要做这个工具？",[10,1371,1372],{},"表面原因是 200 多份文档用演讲方式讲会更有力。深层原因是这个过程本身就是对文档质量的检验。",[10,1374,1375],{},"一份设计文档如果结构清晰——背景交代得清，方案对比得充分，架构图画得明确，取舍理由说得透彻——那转成演示就是平移内容，不费劲。转不出来、或者转出来很别扭，就是信号，说明某个环节讲得不够好。",[10,1377,1378],{},"这个反馈机制比任何 review 注释都直白。\"这个表述为什么转不成幻灯片？\"往往能逼出真实的问题——\"哦，因为我其实还没想清楚这个方案为什么比另一个好\"。",{"title":136,"searchDepth":284,"depth":284,"links":1380},[1381,1382,1383,1384],{"id":1280,"depth":284,"text":1280},{"id":1317,"depth":284,"text":1318},{"id":1354,"depth":284,"text":1354},{"id":1366,"depth":284,"text":1366},"2026-07-17",{},"\u002F2026-07-17",{"title":1266,"description":1271},"2026-07-17-把设计文档变成可讲的演示","不做通用PPT编辑器，只做文档→演示的单向转换；关键在识别文档的稳定结构。",[1392,1393,1394,1395,1396],"演示文稿","文档结构","设计工具","HTML","GSAP","Reh87_TKXZ8nl5BhDhHzVOONAzwRS-Im8LTYrIfeuNk",1785406912236]