[{"data":1,"prerenderedAt":2917},["ShallowReactive",2],{"\u002F2026-07-24":3,"\u002F2026-07-24-rel":501},{"id":4,"title":5,"body":6,"column":483,"date":484,"description":12,"extension":485,"hero_image":486,"meta":487,"navigation":488,"path":489,"seo":490,"series_id":486,"severity":486,"stem":491,"summary":492,"tags":493,"__hash__":500},"posts\u002F2026-07-24-引入的第二天我决定不再跟随上游.md","引入的第二天，我决定不再跟随上游",{"type":7,"value":8,"toc":474},"minimark",[9,13,16,19,71,74,79,82,92,95,100,106,109,113,116,137,140,151,154,160,163,272,275,278,282,285,304,307,310,329,332,335,339,342,368,371,381,384,388,391,394,400,403,409,412,419,422,426,433,436,439,445,449,452,461,464,471],[10,11,12],"p",{},"上一篇（07-22）刚定下「前端全量原样保留、定期人工同步上游」。",[10,14,15],{},"二十小时后，「定期同步」这条被废掉了。",[10,17,18],{},"时间线可以精确到小时：",[20,21,22,35],"table",{},[23,24,25],"thead",{},[26,27,28,32],"tr",{},[29,30,31],"th",{},"时间",[29,33,34],{},"事件",[36,37,38,47,55,63],"tbody",{},[26,39,40,44],{},[41,42,43],"td",{},"07-22 14:43",[41,45,46],{},"接入决策文档定稿，写着「定期人工同步」",[26,48,49,52],{},[41,50,51],{},"07-22 16:11",[41,53,54],{},"fork 首次进仓库",[26,56,57,60],{},[41,58,59],{},"07-23 11:20",[41,61,62],{},"图运行设计文档定稿，前置决策第一条就是脱钩",[26,64,65,68],{},[41,66,67],{},"07-23 11:42 \u002F 11:57 \u002F 12:32",[41,69,70],{},"三个提交把「图运行 + 脱钩」一起落进主干",[10,72,73],{},"从写下调解决定到写下推翻决定，隔了约 20.5 小时。",[75,76,78],"h2",{"id":77},"一推翻的理由","一、推翻的理由",[10,80,81],{},"原文不长，两句：",[83,84,85],"blockquote",{},[10,86,87,91],{},[88,89,90],"strong",{},"脱钩上游","：不再跟随上游同步。登记文件降级为引入历史存档，「最小改动\u002F过滤不删」纪律废除，前端代码从此按自有代码演进。",[10,93,94],{},"而脱钩的书面理由，写在登记文件的顶部：",[83,96,97],{},[10,98,99],{},"决策背景：我们的产品形态（服务端图运行 \u002F DAG 调度）与上游的「逐节点手动推演」哲学根本分叉，同步收益趋近于零而冲突成本持续上升；商业授权为纸质合同（含 CLA 再许可权），脱钩无法律障碍。",[10,101,102,103],{},"两句合起来是一条判据：",[88,104,105],{},"同步的收益，取决于我要不要改它给我的东西。",[10,107,108],{},"如果我的改动都在外围——加个鉴权、换个存储——那上游每次更新都是净收益，合进来就行。但如果我要改的正是它的执行语义，那上游的每次前进都会加深冲突，同步从收益变成了成本。",[75,110,112],{"id":111},"二具体冲突是什么","二、具体冲突是什么",[10,114,115],{},"三条前置决策，第一条是脱钩，第二条解释了冲突在哪：",[83,117,118],{},[119,120,122],"ol",{"start":121},2,[123,124,125,128,129,132,133,136],"li",{},[88,126,127],{},"路线 B + 服务端 Worker","：执行层不复用旧自研画布引擎运行时（",[88,130,131],{},"其端口模型与新前端的松散连线哲学冲突","），新写贴合新前端数据模型的图调度；但",[88,134,135],{},"编排基建照抄旧引擎设计","（BullMQ 队列、lease \u002F 恢复、计费包装模式——已验证过）。服务端形态也已被旧画布验证，不做客户端调度中间品。",[10,138,139],{},"这句话里有两个反对称的决定，值得拆开看。",[10,141,142,145,146,150],{},[88,143,144],{},"不复用的是运行时。"," 旧自研引擎有一套端口模型：节点声明具名输入输出端口，连接受类型校验，不兼容的端口连不上。新前端没有端口——它的连线就是 ",[147,148,149],"code",{},"{ fromNodeId, toNodeId }","，语义只有「上游产物是下游的素材」。",[10,152,153],{},"两套模型接不上。想复用旧运行时，就得给新前端的图补出端口信息，而补不出来——原始数据里没有。",[10,155,156,159],{},[88,157,158],{},"照抄的是编排基建。"," 队列、租约、心跳、恢复循环、计费包装，这些全部沿用旧引擎的设计。理由写在括号里：「已验证过」。",[10,161,162],{},"具体到实现，这一部分是逐项搬的：",[20,164,165,178],{},[23,166,167],{},[26,168,169,172,175],{},[29,170,171],{},"机制",[29,173,174],{},"旧引擎",[29,176,177],{},"新实现",[36,179,180,195,205,216,226,237,249,262],{},[26,181,182,185,190],{},[41,183,184],{},"队列名",[41,186,187],{},[147,188,189],{},"canvas-workflow-runs",[41,191,192],{},[147,193,194],{},"canvas-graph-runs",[26,196,197,200,203],{},[41,198,199],{},"租约时长",[41,201,202],{},"60 秒",[41,204,202],{},[26,206,207,210,213],{},[41,208,209],{},"心跳间隔",[41,211,212],{},"15 秒",[41,214,215],{},"20 秒",[26,217,218,221,224],{},[41,219,220],{},"恢复循环周期",[41,222,223],{},"30 秒",[41,225,223],{},[26,227,228,231,234],{},[41,229,230],{},"停滞判定",[41,232,233],{},"300 秒（队列里待太久就重新入队）",[41,235,236],{},"同",[26,238,239,242,247],{},[41,240,241],{},"入队幂等",[41,243,244],{},[147,245,246],{},"jobId = runId",[41,248,236],{},[26,250,251,254,260],{},[41,252,253],{},"计费包装",[41,255,256,257],{},"台账状态机 ",[147,258,259],{},"planned → charged → settled \u002F refund",[41,261,236],{},[26,263,264,267,270],{},[41,265,266],{},"单副本并发",[41,268,269],{},"2（生产环境 20）",[41,271,236],{},[10,273,274],{},"租约、心跳、恢复这一套要解决的问题是：调度进程崩了怎么办。做法是让「正在执行」这个状态带一个有效期，执行者定期续期；一旦停止续期，另一个实例就能接管。",[10,276,277],{},"计费包装那层解决的是另一件事：调度器不许碰钱。节点执行时通过一个包装过的客户端扣费，包装层记台账，状态机的每一步都落库，于是「扣了没结算」「退了没记」这类状态可以事后对账。",[75,279,281],{"id":280},"三保住的边界","三、保住的边界",[10,283,284],{},"脱钩之后，第一期的范围写成了五条目标：",[83,286,287],{},[119,288,289,292,295,298,301],{},[123,290,291],{},"画布工具栏新增「运行」：全图运行；选中节点时为「运行到此节点」（只跑该节点及其未完成上游）。",[123,293,294],{},"服务端 Worker 执行：关浏览器照样跑完，重启可恢复，不重复执行、不重复扣费。",[123,296,297],{},"节点级状态可视：排队 \u002F 运行中 \u002F 完成 \u002F 失败角标 + 顶部运行进度；失败节点展示可读错误与重试。",[123,299,300],{},"运行记录落库：快照、每节点状态、产物、计费关联可追踪。",[123,302,303],{},"结果对账：用户不在线时产生的结果，下次打开画布自动回填。",[10,305,306],{},"第二条是这一期的核心。「关浏览器照样跑完」意味着执行必须在服务端，不能在客户端。这也解释了为什么不复用上游的调度——它是给「用户坐在画布前逐个点节点」设计的，那种形态下浏览器就是执行环境。",[10,308,309],{},"非目标同样是四条，其中第三条最长：",[83,311,312],{},[313,314,315,318,323,326],"ul",{},[123,316,317],{},"不做多人协同、不做可配置并发上限（沿用「就绪即并发」）。",[123,319,320],{},[88,321,322],{},"不引入端口 \u002F 类型校验 UI——连线保持上游的松散语义（这是保留该前端的核心理由）。",[123,324,325],{},"不做运行详情独立页面（进度浮层 + 节点角标够用，详情页二期）。",[123,327,328],{},"旧自研画布引擎不删除、不改动，仅作参考代码。",[10,330,331],{},"第二条是这次决策的边界线。脱钩的是「跟随上游」，不是「保留前端」——保留前端的理由正是它的松散连线，那么这条不能反悔。如果哪天要给连线加类型校验，等于承认保错了，得回到第一天重选。",[10,333,334],{},"第四条是给旧引擎的处置：不删、不动。它现在的作用是参考代码——编排基建那部分是照着它抄的。",[75,336,338],{"id":337},"四崩溃与重复执行怎么处理","四、崩溃与重复执行怎么处理",[10,340,341],{},"服务端执行最容易出问题的地方是「执行者中途没了」。设计里分成几条路：",[313,343,344,350,356,362],{},[123,345,346,349],{},[88,347,348],{},"worker 崩溃","：租约过期，另一个实例接管。",[123,351,352,355],{},[88,353,354],{},"取消","：运行置取消，待执行的节点一并置取消；正在跑的视频任务走既有的取消链路。",[123,357,358,361],{},[88,359,360],{},"不自动重跑","：租约过期后接管的实例只做对账，不重新执行节点。要重跑由用户手动点重试。这一条是「一期从简」，因为自动重跑要先解决「上次到底跑没跑完」。",[123,363,364,367],{},[88,365,366],{},"不双扣","：每个节点有一个幂等键，任何路径都不产生第二次扣费。",[10,369,370],{},"验收标准里有三条是直接对着这些路径写的：",[372,373,378],"pre",{"className":374,"code":376,"language":377},[375],"language-text","点运行后关闭浏览器，重开画布可见运行完成且产物就位\n删除 worker 进程后运行继续完成，无重复扣费\n重复点运行（同一请求 id）不产生第二个 run\n","text",[147,379,376],{"__ignoreMap":380},"",[10,382,383],{},"第二条用的是「删掉进程」这种验证方式，而不是注入一个异常。前者更接近真实故障。",[75,385,387],{"id":386},"五纪律怎么废","五、纪律怎么废",[10,389,390],{},"废止一条纪律，落到文件上是一组具体动作。",[10,392,393],{},"登记文件的标题从",[372,395,398],{"className":396,"code":397,"language":377},[375],"# canvas-web 上游同步记录\n",[147,399,397],{"__ignoreMap":380},[10,401,402],{},"改成",[372,404,407],{"className":405,"code":406,"language":377},[375],"# canvas-web 上游引入历史（已脱钩，存档）\n",[147,408,406],{"__ignoreMap":380},[10,410,411],{},"「上游同步流程」那一节标为已废止，原来的频率说明用删除线划掉，后面加一句「已脱钩，不再执行」。改动清单那一节标题加上「已停止维护」，原来的登记要求划掉，留一句「以下为脱钩前的引入期改动存档」。",[10,413,414,415,418],{},"引入时那句「前端代码原样保留」也加了限定：「",[88,416,417],{},"引入初期","前端代码原样保留」。",[10,420,421],{},"这件事单独占一个实施任务，任务描述里写清了改哪几句、怎么改。把「废止纪律」当成一项要交付的工作，是因为纪律如果只在新文档里宣布作废、旧文件还挂着原有的要求，下一次有人翻到旧文件就会照它做。",[75,423,425],{"id":424},"六这不是朝令夕改","六、这不是朝令夕改",[10,427,428,429,432],{},"隔一天改主意，看上去像反复。区分点在于",[88,430,431],{},"被推翻的是哪一条","。",[10,434,435],{},"前一天定的三条里，两条一直没变：前端全量保留、不修改它的设计与交互。变的是第三条「定期人工同步上游」。",[10,437,438],{},"而这条之所以要变，是因为另一件事在同期确定了：执行要放在服务端，做成 DAG 调度。这件事一旦定下来，同步的性质就变了——从「拿上游的新功能」变成「不断解决我和上游之间的语义冲突」。",[10,440,441,442],{},"「引入的目标已经达成」这种说法我没有写在当时的文档里，事后也不想补上去。当时的书面理由就是那条收益成本判断：",[88,443,444],{},"产品形态与上游哲学分叉之后，同步收益趋近于零，冲突成本持续上升。",[75,446,448],{"id":447},"七脱钩之后","七、脱钩之后",[10,450,451],{},"值得一提的是后面发生了什么，因为它验证了这条判断的方向。",[10,453,454,455,457,458,432],{},"这套 fork 出来的前端后来又经历了两轮变化，最后整个 fork 被移除——前端改由自研重写，队列名从 ",[147,456,194],{}," 换成了 ",[147,459,460],{},"canvas-flow-runs",[10,462,463],{},"所以「脱钩」不等于「稳定了」。它只是把一件事确定下来：这个模块从此按自有代码演进，好坏都由自己承担。",[10,465,466,467,470],{},"而 07-23 那天真正保住的，是判断里的另一半——",[88,468,469],{},"编排基建照抄旧引擎","。队列、租约、恢复、计费台账这套模式一直用到了今天的画布运行上。脱钩的是前端血缘，不是自己的工程积累。",[10,472,473],{},"这两件事分开看，是那一天最值得记的部分：丢掉的是「别人的演进」，留下的是「自己验证过的东西」。判断的依据不是血缘远近，是有没有跑过。",{"title":380,"searchDepth":121,"depth":121,"links":475},[476,477,478,479,480,481,482],{"id":77,"depth":121,"text":78},{"id":111,"depth":121,"text":112},{"id":280,"depth":121,"text":281},{"id":337,"depth":121,"text":338},{"id":386,"depth":121,"text":387},{"id":424,"depth":121,"text":425},{"id":447,"depth":121,"text":448},"Agent 平台","2026-07-24","md",null,{},true,"\u002F2026-07-24",{"title":5,"description":12},"2026-07-24-引入的第二天我决定不再跟随上游","前一天刚定下「前端原样保留、定期同步上游」，二十小时后就废掉了同步这条。触发点是一条冲突：上游的松散连线与旧引擎的端口模型接不上。",[494,495,496,497,498,499],"开源引入","脱钩","DAG 调度","BullMQ","lease","架构决策","9ScepKcFB7xo_lsqtEin1HvIMnicAVeDNO14MnI56oQ",[502,1215,1863],{"id":503,"title":504,"body":505,"column":483,"date":1200,"description":509,"extension":485,"hero_image":486,"meta":1201,"navigation":488,"path":1202,"seo":1203,"series_id":486,"severity":486,"stem":1204,"summary":1205,"tags":1206,"__hash__":1214},"posts\u002F2026-08-31-阈值不能推只能量.md","阈值不能推，只能量",{"type":7,"value":506,"toc":1191},[507,510,513,517,520,525,528,531,537,540,543,548,551,566,593,599,602,606,617,620,626,629,632,635,640,643,646,707,710,713,776,779,784,788,791,797,800,803,807,810,816,819,825,828,831,838,844,847,850,856,859,902,905,909,912,915,921,924,927,931,937,999,1002,1100,1103,1106,1154,1157,1161,1164,1174,1177,1184,1187],[10,508,509],{},"知识库的检索参数有三组：分块阈值、召回阈值、精排阈值。这一天把三组都动了一遍，每一组都留下了实测数据。",[10,511,512],{},"起因是一个看不太出来的现象：知识库好像没被用上。",[75,514,516],{"id":515},"一分块中位数-212-字","一、分块：中位数 212 字",[10,518,519],{},"先量现状。生产库里的分块统计：",[83,521,522],{},[10,523,524],{},"13244 个分块中位数只有 212 字，而目标 2400 字，62% 的块不足 300 字。",[10,526,527],{},"目标块大小是 2400 字，实际交付的是 212。差了十倍。",[10,529,530],{},"分块器原来的逻辑是「一个标题一个块」。这在正常文档上没问题，但清单型、模板型文档里，几乎每一行列表项都会被标题识别逻辑认成标题：",[372,532,535],{"className":533,"code":534,"language":377},[375],"1. 原本想达成什么？\n",[147,536,534],{"__ignoreMap":380},[10,538,539],{},"这一行被当成标题，于是自己成了一个块。一篇文档被切成几十个几十字的碎片，注入给模型的全是碎片。",[10,541,542],{},"更严重的情况在连续列表项之间没有正文时。老实现让标题行只活在「面包屑」元数据里，不写进块正文。于是被误判成标题的那一行文字直接消失：",[83,544,545],{},[10,546,547],{},"标题行只活在面包屑里，整行文字直接丢失（那类文档的四个核心问题在索引里根本不存在）。",[10,549,550],{},"两处改动：",[372,552,556],{"className":553,"code":554,"language":555,"meta":380,"style":380},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F 攒够了才在这里切：标题是「首选切点」，不是「强制切点」。\n","ts",[147,557,558],{"__ignoreMap":380},[559,560,563],"span",{"class":561,"line":562},"line",1,[559,564,554],{"class":565},"sJ8bj",[372,567,569],{"className":553,"code":568,"language":555,"meta":380,"style":380},"\u002F\u002F 不变量：每一行输入都要落进某个块的正文，面包屑只是附加元数据。\n\u002F\u002F 老实现让标题行只活在面包屑里，于是被 detectHeading 误判成标题的列表项\n\u002F\u002F （「1. 原本想达成什么？」这类）整行文字就没了——连续几个列表项时只留得住最后一条。\n\u002F\u002F 与面包屑重复一次可以接受，丢字不行。\n",[147,570,571,576,581,587],{"__ignoreMap":380},[559,572,573],{"class":561,"line":562},[559,574,575],{"class":565},"\u002F\u002F 不变量：每一行输入都要落进某个块的正文，面包屑只是附加元数据。\n",[559,577,578],{"class":561,"line":121},[559,579,580],{"class":565},"\u002F\u002F 老实现让标题行只活在面包屑里，于是被 detectHeading 误判成标题的列表项\n",[559,582,584],{"class":561,"line":583},3,[559,585,586],{"class":565},"\u002F\u002F （「1. 原本想达成什么？」这类）整行文字就没了——连续几个列表项时只留得住最后一条。\n",[559,588,590],{"class":561,"line":589},4,[559,591,592],{"class":565},"\u002F\u002F 与面包屑重复一次可以接受，丢字不行。\n",[10,594,595,596,432],{},"标题从「强制切点」降为「首选切点」：缓冲区不足阈值时，标题并入当前块，不切。同时标题行本身写进正文，成为一条不变量——",[88,597,598],{},"每一行输入都要落进某个块的正文",[10,600,601],{},"实测效果：一篇文档从 3 块 52\u002F67\u002F100 字（四个核心问题全丢）变成 1 块 235 字，内容完整。",[75,603,605],{"id":604},"二按比例推算推出了全场最差点","二、按比例推算，推出了全场最差点",[10,607,608,609,612,613,616],{},"第一版把阈值定成 ",[147,610,611],{},"maxChars × 0.6","。生产 ",[147,614,615],{},"maxChars"," 是 2400，算出来是 1440。",[10,618,619],{},"结果整篇文档并成一个块。上线后实测检索：",[372,621,624],{"className":622,"code":623,"language":377},[375],"5 篇文档 6 个查询，每篇取最佳命中分再平均\n  阈值 0（纯按小节切） 0.7046\n  阈值 250            0.6750\n  阈值 1440（线上）    0.4978\n",[147,625,623],{"__ignoreMap":380},[10,627,628],{},"1440 正好是最差的那个。",[10,630,631],{},"同一篇文档对「核心四问」这个查询，切成小节时得分 0.77，并成整块时只有 0.33——在全库 113 块里排到第 109 名。内容修好了，却再也检索不到。",[10,633,634],{},"根因在生产用的向量模型上：",[83,636,637],{},[10,638,639],{},"生产 embedding 模型对「主题聚焦的小段」打分远高于「整篇文档」。",[10,641,642],{},"原来那个「一个标题一个块」的设计，主题纯度是对的。推翻它是错的判断。真正的缺陷只有一条——标题行被丢弃。",[10,644,645],{},"最终取值 250：",[372,647,649],{"className":553,"code":648,"language":555,"meta":380,"style":380},"\u002F**\n * 缺省小节合并阈值。250 是在生产 embedding 模型上实测标定的，不是拍脑袋：\n * 5 篇文档 6 个查询，取每篇的最佳命中分做平均——\n *   阈值 0（纯按小节切）0.7046 \u002F 250 → 0.6750 \u002F 1440（= maxChars×0.6）→ 0.4978\n * 这个模型对「主题聚焦的小段」打分远高于「整篇文档」……\n * 所以阈值必须小，千万别再按 maxChars 的比例去推——那样在生产的 2400 上会算出 1440，\n * 正好是最差点。\n * 取 250 而不是 0：排序只差 4%，但块从几十字变成 250~330 字，\n * 同样召回 6 段能多喂两三倍的正文。\n *\u002F\n",[147,650,651,656,661,666,671,677,683,689,695,701],{"__ignoreMap":380},[559,652,653],{"class":561,"line":562},[559,654,655],{"class":565},"\u002F**\n",[559,657,658],{"class":561,"line":121},[559,659,660],{"class":565}," * 缺省小节合并阈值。250 是在生产 embedding 模型上实测标定的，不是拍脑袋：\n",[559,662,663],{"class":561,"line":583},[559,664,665],{"class":565}," * 5 篇文档 6 个查询，取每篇的最佳命中分做平均——\n",[559,667,668],{"class":561,"line":589},[559,669,670],{"class":565}," *   阈值 0（纯按小节切）0.7046 \u002F 250 → 0.6750 \u002F 1440（= maxChars×0.6）→ 0.4978\n",[559,672,674],{"class":561,"line":673},5,[559,675,676],{"class":565}," * 这个模型对「主题聚焦的小段」打分远高于「整篇文档」……\n",[559,678,680],{"class":561,"line":679},6,[559,681,682],{"class":565}," * 所以阈值必须小，千万别再按 maxChars 的比例去推——那样在生产的 2400 上会算出 1440，\n",[559,684,686],{"class":561,"line":685},7,[559,687,688],{"class":565}," * 正好是最差点。\n",[559,690,692],{"class":561,"line":691},8,[559,693,694],{"class":565}," * 取 250 而不是 0：排序只差 4%，但块从几十字变成 250~330 字，\n",[559,696,698],{"class":561,"line":697},9,[559,699,700],{"class":565}," * 同样召回 6 段能多喂两三倍的正文。\n",[559,702,704],{"class":561,"line":703},10,[559,705,706],{"class":565}," *\u002F\n",[10,708,709],{},"取 250 而不是 0 的理由是这段话里第二重要的部分：排序只差 4%，但每个块从几十字变成两三百字，同样召回 6 段能多喂几倍的正文。",[10,711,712],{},"代码里还留了一条兜底：",[372,714,716],{"className":553,"code":715,"language":555,"meta":380,"style":380},"\u002F\u002F 取 min：maxChars 很小的配置（测试里 300）不能让阈值反超块大小本身\nconst minChunkChars =\n  opts.minChunkChars ?? Math.min(DEFAULT_MIN_CHUNK_CHARS, Math.floor(maxChars * 0.6));\n",[147,717,718,723,736],{"__ignoreMap":380},[559,719,720],{"class":561,"line":562},[559,721,722],{"class":565},"\u002F\u002F 取 min：maxChars 很小的配置（测试里 300）不能让阈值反超块大小本身\n",[559,724,725,729,733],{"class":561,"line":121},[559,726,728],{"class":727},"szBVR","const",[559,730,732],{"class":731},"sj4cs"," minChunkChars",[559,734,735],{"class":727}," =\n",[559,737,738,742,745,748,752,755,758,761,764,767,770,773],{"class":561,"line":583},[559,739,741],{"class":740},"sVt8B","  opts.minChunkChars ",[559,743,744],{"class":727},"??",[559,746,747],{"class":740}," Math.",[559,749,751],{"class":750},"sScJk","min",[559,753,754],{"class":740},"(",[559,756,757],{"class":731},"DEFAULT_MIN_CHUNK_CHARS",[559,759,760],{"class":740},", Math.",[559,762,763],{"class":750},"floor",[559,765,766],{"class":740},"(maxChars ",[559,768,769],{"class":727},"*",[559,771,772],{"class":731}," 0.6",[559,774,775],{"class":740},"));\n",[10,777,778],{},"以及一条守卫测试，专门防「有人再推一遍公式」：",[83,780,781],{},[10,782,783],{},"加一条守卫测试钉死生产口径（阈值退回按比例推算即变红），防止将来有人再推一遍公式又回到 1440。",[75,785,787],{"id":786},"三召回阈值会把整轮清零","三、召回阈值：会把整轮清零",[10,789,790],{},"分块改大之后，绝对余弦分整体下移。而召回阈值还是旧值 0.5：",[372,792,795],{"className":793,"code":794,"language":377},[375],"正确命中落在 0.43~0.63，旧值会把最高分 0.488 的查询整轮清零——\n检索到了正确文档却被门槛全部丢弃，用户看到的就是「知识库没被使用」。\n",[147,796,794],{"__ignoreMap":380},[10,798,799],{},"这条解释了我一开始看到的那个现象。检索其实命中了，只是分数没过门槛，于是整轮被丢掉，模型什么也没拿到。",[10,801,802],{},"改成 0.35。",[75,804,806],{"id":805},"四精排从-912-到-1212","四、精排：从 9\u002F12 到 12\u002F12",[10,808,809],{},"同一天启用了 cross-encoder 精排。A\u002FB 实测：",[372,811,814],{"className":812,"code":813,"language":377},[375],"12 条查询 A\u002FB 实测：\n  纯向量  命中 9\u002F12，整轮零注入 1 次\n  开精排  命中 12\u002F12，整轮零注入 0 次，目标文档 10 条排第 1\n",[147,815,813],{"__ignoreMap":380},[10,817,818],{},"修好的都是「换了说法」的查询——向量检索的固有短板。举一个例子：",[372,820,823],{"className":821,"code":822,"language":377},[375],"「我想要复刻爆款视频…使用画布」注入 0 段 → 4 段\n（相关文档被向量埋在后面，精排提到第 2 名）\n",[147,824,822],{"__ignoreMap":380},[10,826,827],{},"精排启用时把两个阈值也一起改了。这是当天最曲折的一处。",[10,829,830],{},"第一版把精排阈值设成 0.15。发版后跑完整测试，出现随机漏召。",[10,832,833,834,837],{},"原因是精排的",[88,835,836],{},"绝对分不稳定","：",[372,839,842],{"className":840,"code":841,"language":377},[375],"同一查询同一候选集，「金字塔原理怎么做到结论先行」两次实测 0.251 与 0.135——\n排名都稳定第 1，只是分数漂了近一半。0.15 卡在中间，于是第二次整轮零注入。\n",[147,843,841],{"__ignoreMap":380},[10,845,846],{},"排名是稳定的，分数会漂。所以不能用绝对分当门槛去「把关质量」。",[10,848,849],{},"阈值扫描：",[372,851,854],{"className":852,"code":853,"language":377},[375],"0.02~0.10 均 12\u002F12 零丢弃，0.15\u002F0.20 → 11\u002F12 丢 1 次\n",[147,855,853],{"__ignoreMap":380},[10,857,858],{},"最终取 0.05，并把职责写清楚：",[372,860,862],{"className":553,"code":861,"language":555,"meta":380,"style":380},"\u002F**\n * 精排阈值。**它的职责只是扔掉垃圾，不是把关质量**——质量由排序保证：\n * 12 条查询里精排把正确目标全部放进了前 3 名（10 条第 1）。\n *\n * 取 0.05 而不是更高，是因为**精排的绝对分不稳定**……\n * 观测到的正确目标最低分 0.131，取 0.05 留约 60% 余量；\n * 垃圾档在 0.014~0.025，仍被干净滤掉。\n *\u002F\n",[147,863,864,868,873,878,883,888,893,898],{"__ignoreMap":380},[559,865,866],{"class":561,"line":562},[559,867,655],{"class":565},[559,869,870],{"class":561,"line":121},[559,871,872],{"class":565}," * 精排阈值。**它的职责只是扔掉垃圾，不是把关质量**——质量由排序保证：\n",[559,874,875],{"class":561,"line":583},[559,876,877],{"class":565}," * 12 条查询里精排把正确目标全部放进了前 3 名（10 条第 1）。\n",[559,879,880],{"class":561,"line":589},[559,881,882],{"class":565}," *\n",[559,884,885],{"class":561,"line":673},[559,886,887],{"class":565}," * 取 0.05 而不是更高，是因为**精排的绝对分不稳定**……\n",[559,889,890],{"class":561,"line":679},[559,891,892],{"class":565}," * 观测到的正确目标最低分 0.131，取 0.05 留约 60% 余量；\n",[559,894,895],{"class":561,"line":685},[559,896,897],{"class":565}," * 垃圾档在 0.014~0.025，仍被干净滤掉。\n",[559,899,900],{"class":561,"line":691},[559,901,706],{"class":565},[10,903,904],{},"观测到的垃圾档在 0.014~0.025，正确目标最低 0.131。0.05 落在两者之间，两边都有余量。",[75,906,908],{"id":907},"五超时静默降级最贵","五、超时：静默降级最贵",[10,910,911],{},"同一批里还修了超时。",[10,913,914],{},"精排失败会自动降级回向量序，不阻断聊天。这个设计是对的——但有代价：",[372,916,919],{"className":917,"code":918,"language":377},[375],"法律知识库 30 块共 4.5 万字，冷调用实测 2263ms，只剩 737ms 余量。\n完整测试里有一条查询当场超时降级。\n超时是静默的，日志不报错，用户侧表现为「有时准有时不准」，极难归因。\n",[147,920,918],{"__ignoreMap":380},[10,922,923],{},"「有时准有时不准」这句话在这一天的上下文里已经出现第二次了——早上是参考图，这里是检索。",[10,925,926],{},"超时从 3000 毫秒提到 8000，给冷调用 3.5 倍余量。真卡死仍有降级兜底。",[75,928,930],{"id":929},"六三个阈值放在一起","六、三个阈值放在一起",[10,932,933,934],{},"三条修正的形式不同，结论是同一条：",[88,935,936],{},"阈值不能推导，只能测量。",[20,938,939,955],{},[23,940,941],{},[26,942,943,946,949,952],{},[29,944,945],{},"参数",[29,947,948],{},"推导值",[29,950,951],{},"实测值",[29,953,954],{},"推导错在哪",[36,956,957,971,985],{},[26,958,959,962,965,968],{},[41,960,961],{},"分块合并阈值",[41,963,964],{},"1440（按比例 0.6）",[41,966,967],{},"250",[41,969,970],{},"假设「块越大越好」，而该模型偏好主题聚焦的小段",[26,972,973,976,979,982],{},[41,974,975],{},"召回阈值",[41,977,978],{},"0.5（惯例值）",[41,980,981],{},"0.35",[41,983,984],{},"分块改动后分数分布整体下移，旧门槛把命中全丢",[26,986,987,990,993,996],{},[41,988,989],{},"精排阈值",[41,991,992],{},"0.15（留余量）",[41,994,995],{},"0.05",[41,997,998],{},"绝对分本身会漂近一半，拿漂移量当门槛必然随机漏召",[10,1000,1001],{},"三个值都有实测数字支撑，也都配了守卫测试。其中两条测试写的是「不许回到某个值」，而不是「必须等于某值」：",[372,1003,1005],{"className":553,"code":1004,"language":555,"meta":380,"style":380},"it(\"向量阈值不得回到会整轮清零的 0.5\", () => {\n  expect(DEFAULT_KB_MIN_SCORE).toBeLessThanOrEqual(0.4);\n});\n\nit(\"精排阈值不得高到会随分数漂移漏召——观测最低正确分 0.131\", () => {\n  expect(DEFAULT_KB_RERANK_MIN_SCORE).toBeLessThanOrEqual(0.1);\n});\n",[147,1006,1007,1027,1051,1056,1061,1076,1096],{"__ignoreMap":380},[559,1008,1009,1012,1014,1018,1021,1024],{"class":561,"line":562},[559,1010,1011],{"class":750},"it",[559,1013,754],{"class":740},[559,1015,1017],{"class":1016},"sZZnC","\"向量阈值不得回到会整轮清零的 0.5\"",[559,1019,1020],{"class":740},", () ",[559,1022,1023],{"class":727},"=>",[559,1025,1026],{"class":740}," {\n",[559,1028,1029,1032,1034,1037,1040,1043,1045,1048],{"class":561,"line":121},[559,1030,1031],{"class":750},"  expect",[559,1033,754],{"class":740},[559,1035,1036],{"class":731},"DEFAULT_KB_MIN_SCORE",[559,1038,1039],{"class":740},").",[559,1041,1042],{"class":750},"toBeLessThanOrEqual",[559,1044,754],{"class":740},[559,1046,1047],{"class":731},"0.4",[559,1049,1050],{"class":740},");\n",[559,1052,1053],{"class":561,"line":583},[559,1054,1055],{"class":740},"});\n",[559,1057,1058],{"class":561,"line":589},[559,1059,1060],{"emptyLinePlaceholder":488},"\n",[559,1062,1063,1065,1067,1070,1072,1074],{"class":561,"line":673},[559,1064,1011],{"class":750},[559,1066,754],{"class":740},[559,1068,1069],{"class":1016},"\"精排阈值不得高到会随分数漂移漏召——观测最低正确分 0.131\"",[559,1071,1020],{"class":740},[559,1073,1023],{"class":727},[559,1075,1026],{"class":740},[559,1077,1078,1080,1082,1085,1087,1089,1091,1094],{"class":561,"line":679},[559,1079,1031],{"class":750},[559,1081,754],{"class":740},[559,1083,1084],{"class":731},"DEFAULT_KB_RERANK_MIN_SCORE",[559,1086,1039],{"class":740},[559,1088,1042],{"class":750},[559,1090,754],{"class":740},[559,1092,1093],{"class":731},"0.1",[559,1095,1050],{"class":740},[559,1097,1098],{"class":561,"line":685},[559,1099,1055],{"class":740},[10,1101,1102],{},"这样写是因为真正的风险不是「有人改了数值」，而是「有人又推了一遍公式」。测试防的是后者。",[10,1104,1105],{},"还有一条测试值得抄下来：",[372,1107,1109],{"className":553,"code":1108,"language":555,"meta":380,"style":380},"it(\"精排阈值必须低于向量阈值——两者不是同一个量纲\", () => {\n  \u002F\u002F 精排是 cross-encoder 分，向量是余弦分，拿同一个数去卡两边必然有一边错\n  expect(DEFAULT_KB_RERANK_MIN_SCORE).toBeLessThan(DEFAULT_KB_MIN_SCORE);\n});\n",[147,1110,1111,1126,1131,1150],{"__ignoreMap":380},[559,1112,1113,1115,1117,1120,1122,1124],{"class":561,"line":562},[559,1114,1011],{"class":750},[559,1116,754],{"class":740},[559,1118,1119],{"class":1016},"\"精排阈值必须低于向量阈值——两者不是同一个量纲\"",[559,1121,1020],{"class":740},[559,1123,1023],{"class":727},[559,1125,1026],{"class":740},[559,1127,1128],{"class":561,"line":121},[559,1129,1130],{"class":565},"  \u002F\u002F 精排是 cross-encoder 分，向量是余弦分，拿同一个数去卡两边必然有一边错\n",[559,1132,1133,1135,1137,1139,1141,1144,1146,1148],{"class":561,"line":583},[559,1134,1031],{"class":750},[559,1136,754],{"class":740},[559,1138,1084],{"class":731},[559,1140,1039],{"class":740},[559,1142,1143],{"class":750},"toBeLessThan",[559,1145,754],{"class":740},[559,1147,1036],{"class":731},[559,1149,1050],{"class":740},[559,1151,1152],{"class":561,"line":589},[559,1153,1055],{"class":740},[10,1155,1156],{},"同一个数量级、看起来可比，实际是两个不同的量纲。这种错觉在阈值调参里很常见，能用一条断言拦下来比写在注释里可靠。",[75,1158,1160],{"id":1159},"七评测集的缺口","七、评测集的缺口",[10,1162,1163],{},"这一轮所有数字来自临时扫的查询集：5 篇文档 6 个查询、12 条查询。",[10,1165,1166,1167,1170,1171,432],{},"仓库里确实有一个召回评测脚本（用户问题、期望命中的文档名、recall@K \u002F precision@K \u002F MRR 双模式），但它的标注集还是占位内容——两条用例，",[147,1168,1169],{},"kbIds"," 的值是字面量 ",[147,1172,1173],{},"\u003C替换为真实 kbId>",[10,1175,1176],{},"也就是说，这一轮的标定用的数据集没有入库。数字留在代码注释、配置和环境变量说明里，跑分的脚本和查询集没有留下。",[10,1178,1179,1180,1183],{},"这是这次工作里最该补上的一环。",[88,1181,1182],{},"阈值有实测依据，但依据本身不可复现。"," 下次有人想调整，只能重新扫一遍查询。",[10,1185,1186],{},"补的做法是明确的：把这一轮用的查询集和期望结果补进评测脚本的标注集，让它成为下一次调参的起点。参数变更时先跑评测再改值，改完之后把新的分数写进注释——注释里的数字应该来自一次可复现的运行，而不是一次性的手测。",[1188,1189,1190],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":380,"searchDepth":121,"depth":121,"links":1192},[1193,1194,1195,1196,1197,1198,1199],{"id":515,"depth":121,"text":516},{"id":604,"depth":121,"text":605},{"id":786,"depth":121,"text":787},{"id":805,"depth":121,"text":806},{"id":907,"depth":121,"text":908},{"id":929,"depth":121,"text":930},{"id":1159,"depth":121,"text":1160},"2026-08-31",{},"\u002F2026-08-31",{"title":504,"description":509},"2026-08-31-阈值不能推只能量","知识库检索质量的三次修正：分块阈值按比例推算算出了全场最差点，精排阈值设高了会随分数漂移随机漏召，超时余量不够会静默降级。三次都靠实测数据定值。",[1207,1208,1209,1210,1211,1212,1213],"知识库","RAG","分块","检索","cross-encoder","阈值标定","评测","VbN-ARGFFeAmQMRZf2jG-4SfdtN22uzMLyCrc3dCuA8",{"id":1216,"title":1217,"body":1218,"column":483,"date":1849,"description":1222,"extension":485,"hero_image":486,"meta":1850,"navigation":488,"path":1851,"seo":1852,"series_id":486,"severity":486,"stem":1853,"summary":1854,"tags":1855,"__hash__":1862},"posts\u002F2026-08-20-画布这一步深链运行历史与续跑.md","画布这一步：深链、运行历史与续跑",{"type":7,"value":1219,"toc":1839},[1220,1223,1226,1230,1233,1236,1242,1245,1259,1273,1276,1280,1283,1289,1292,1299,1302,1311,1314,1318,1321,1326,1359,1362,1367,1387,1394,1397,1401,1404,1493,1496,1499,1503,1506,1524,1533,1536,1539,1543,1546,1549,1569,1572,1577,1580,1612,1615,1711,1714,1720,1727,1731,1734,1737,1775,1782,1785,1791,1794,1799,1803,1806,1815,1821,1827,1833,1836],[10,1221,1222],{},"画布是这套平台里最复杂的界面。节点、连线、参数、批量框、运行状态，任何一件事都要在同一个视口里表达清楚。",[10,1224,1225],{},"这一天补齐八项操作。每一项都不难，难的是它们之间不能互相打架。",[75,1227,1229],{"id":1228},"一url-是唯一事实来源","一、URL 是唯一事实来源",[10,1231,1232],{},"原先画布是个单页状态机：打开就是列表，点进去切到编辑器，刷新回列表。",[10,1234,1235],{},"改成路由：",[372,1237,1240],{"className":1238,"code":1239,"language":377},[375],"\u002Fcanvas            列表\n\u002Fcanvas\u002F:id        编辑器\n",[147,1241,1239],{"__ignoreMap":380},[10,1243,1244],{},"四个行为一起对齐：",[313,1246,1247,1250,1253,1256],{},[123,1248,1249],{},"深链直达编辑器。",[123,1251,1252],{},"刷新不回列表。",[123,1254,1255],{},"后退回列表，而不是退出画布模块。",[123,1257,1258],{},"打开画布写入地址栏。",[10,1260,1261,1264,1265,1268,1269,1272],{},[147,1262,1263],{},"popstate"," 监听已有的解析函数，",[147,1266,1267],{},"pushState"," \u002F ",[147,1270,1271],{},"replaceState"," 写入。",[10,1274,1275],{},"这条改动看起来只是加了个路由，实际改变的是状态的归属：画布 ID 从组件内的 ref 变成了 URL 的一部分。后面几项都依赖它——运行历史要能链到具体的运行，批量框要能被分享，都要求「当前在看哪张画布」是一个可以从外部确定的量。",[75,1277,1279],{"id":1278},"二运行历史","二、运行历史",[10,1281,1282],{},"一个新面板，两个接口：",[372,1284,1287],{"className":1285,"code":1286,"language":377},[375],"GET \u002Fapi\u002Fcanvas-flow\u002Fruns?flowId=…      列表\nGET \u002Fapi\u002Fcanvas-flow\u002Fruns\u002F:id           快照\n",[147,1288,1286],{"__ignoreMap":380},[10,1290,1291],{},"列表默认取 10 条。点开某一条，用它的快照推导出画布上每个节点的状态，覆盖显示。",[10,1293,1294,1295,1298],{},"这个面板的数据来源和实时运行状态用的是同一套 ",[147,1296,1297],{},"nodeStates","——历史记录不是另一条平行显示，而是把画布切到那一次运行的视角。",[10,1300,1301],{},"登录态走单一来源：",[372,1303,1305],{"className":553,"code":1304,"language":555,"meta":380,"style":380},"\u002F\u002F 登录态只有 authToken() 一个来源（有守卫测试盯着，别直读 localStorage）\n",[147,1306,1307],{"__ignoreMap":380},[559,1308,1309],{"class":561,"line":562},[559,1310,1304],{"class":565},[10,1312,1313],{},"这条注释是守卫测试抓出来的结果，不是提前的设计。",[75,1315,1317],{"id":1316},"三复制粘贴","三、复制粘贴",[10,1319,1320],{},"复制粘贴里有两个决定值得记。",[10,1322,1323],{},[88,1324,1325],{},"用应用内剪贴板，不碰系统剪贴板。",[372,1327,1329],{"className":553,"code":1328,"language":555,"meta":380,"style":380},"let clipboard: FlowClipboardPayload | null = null;\n",[147,1330,1331],{"__ignoreMap":380},[559,1332,1333,1336,1339,1342,1345,1348,1351,1354,1356],{"class":561,"line":562},[559,1334,1335],{"class":727},"let",[559,1337,1338],{"class":740}," clipboard",[559,1340,1341],{"class":727},":",[559,1343,1344],{"class":750}," FlowClipboardPayload",[559,1346,1347],{"class":727}," |",[559,1349,1350],{"class":731}," null",[559,1352,1353],{"class":727}," =",[559,1355,1350],{"class":731},[559,1357,1358],{"class":740},";\n",[10,1360,1361],{},"粘贴的内容是节点和边，格式带版本号。走系统剪贴板意味着把画布的 JSON 写进用户的剪贴板，用户去别处粘贴会看到一堆结构数据。应用内的模块级变量没有这个问题。",[10,1363,1364],{},[88,1365,1366],{},"没选节点时不拦截。",[372,1368,1370],{"className":553,"code":1369,"language":555,"meta":380,"style":380},"\u002F\u002F Cmd\u002FCtrl+C：复制选中的节点。\n\u002F\u002F 没选节点就不拦截——用户可能正在复制节点产物里的文字，抢了就是坏默认行为\n\u002F\u002F Cmd\u002FCtrl+V：粘贴。应用内剪贴板为空时同样放行系统默认行为\n",[147,1371,1372,1377,1382],{"__ignoreMap":380},[559,1373,1374],{"class":561,"line":562},[559,1375,1376],{"class":565},"\u002F\u002F Cmd\u002FCtrl+C：复制选中的节点。\n",[559,1378,1379],{"class":561,"line":121},[559,1380,1381],{"class":565},"\u002F\u002F 没选节点就不拦截——用户可能正在复制节点产物里的文字，抢了就是坏默认行为\n",[559,1383,1384],{"class":561,"line":583},[559,1385,1386],{"class":565},"\u002F\u002F Cmd\u002FCtrl+V：粘贴。应用内剪贴板为空时同样放行系统默认行为\n",[10,1388,1389,1390,1393],{},"这两条合起来是一个原则：",[88,1391,1392],{},"快捷键只在它有明确意图时生效","。用户按 Cmd+C 时可能是想复制提示词里的文字，此时抢过来是损失。",[10,1395,1396],{},"粘贴到画布时重新生成 ID（节点 ID 最多重试 20 次防碰撞），偏移 24 像素，粘贴的结果进入撤销栈并成为新的选中项。",[75,1398,1400],{"id":1399},"四一键整理布局","四、一键整理布局",[10,1402,1403],{},"按依赖深度分层的纯函数，不引布局引擎：",[372,1405,1407],{"className":553,"code":1406,"language":555,"meta":380,"style":380},"\u002F**\n * 一键整理布局：按依赖深度分层的纯函数。\n *\n * 不引 dagre\u002Felk——画布的图是小规模 DAG（上限 100 节点），\n * 「上游在左、下游在右、同层竖排」这一条规则就够读顺一张乱图，\n * 引一个布局引擎为它的边缘能力买单不值。\n *\u002F\nconst COLUMN_GAP = 380;\nconst ROW_GAP = 240;\nconst ORIGIN = { x: 40, y: 60 };\n",[147,1408,1409,1413,1418,1422,1427,1432,1437,1441,1455,1469],{"__ignoreMap":380},[559,1410,1411],{"class":561,"line":562},[559,1412,655],{"class":565},[559,1414,1415],{"class":561,"line":121},[559,1416,1417],{"class":565}," * 一键整理布局：按依赖深度分层的纯函数。\n",[559,1419,1420],{"class":561,"line":583},[559,1421,882],{"class":565},[559,1423,1424],{"class":561,"line":589},[559,1425,1426],{"class":565}," * 不引 dagre\u002Felk——画布的图是小规模 DAG（上限 100 节点），\n",[559,1428,1429],{"class":561,"line":673},[559,1430,1431],{"class":565}," * 「上游在左、下游在右、同层竖排」这一条规则就够读顺一张乱图，\n",[559,1433,1434],{"class":561,"line":679},[559,1435,1436],{"class":565}," * 引一个布局引擎为它的边缘能力买单不值。\n",[559,1438,1439],{"class":561,"line":685},[559,1440,706],{"class":565},[559,1442,1443,1445,1448,1450,1453],{"class":561,"line":691},[559,1444,728],{"class":727},[559,1446,1447],{"class":731}," COLUMN_GAP",[559,1449,1353],{"class":727},[559,1451,1452],{"class":731}," 380",[559,1454,1358],{"class":740},[559,1456,1457,1459,1462,1464,1467],{"class":561,"line":697},[559,1458,728],{"class":727},[559,1460,1461],{"class":731}," ROW_GAP",[559,1463,1353],{"class":727},[559,1465,1466],{"class":731}," 240",[559,1468,1358],{"class":740},[559,1470,1471,1473,1476,1478,1481,1484,1487,1490],{"class":561,"line":703},[559,1472,728],{"class":727},[559,1474,1475],{"class":731}," ORIGIN",[559,1477,1353],{"class":727},[559,1479,1480],{"class":740}," { x: ",[559,1482,1483],{"class":731},"40",[559,1485,1486],{"class":740},", y: ",[559,1488,1489],{"class":731},"60",[559,1491,1492],{"class":740}," };\n",[10,1494,1495],{},"同列保持用户原有的上下相对顺序。这一条是这类功能能不能用的分界线——整理完之后用户还得能认出自己的图。",[10,1497,1498],{},"节点上限 100，单批最大 50 项，展开后的总任务上限 200。这几个数决定了上面那个判断成立：在这个规模内，一条规则够用。",[75,1500,1502],{"id":1501},"五便签不能做成节点","五、便签不能做成节点",[10,1504,1505],{},"画布上要有地方写注释。最自然的做法是加一个「便签节点」，但它的实现方式正好相反：",[372,1507,1509],{"className":553,"code":1508,"language":555,"meta":380,"style":380},"\u002F**\n * 画布便签：纯注释，不参与调度、连线与计费。\n *\u002F\n",[147,1510,1511,1515,1520],{"__ignoreMap":380},[559,1512,1513],{"class":561,"line":562},[559,1514,655],{"class":565},[559,1516,1517],{"class":561,"line":121},[559,1518,1519],{"class":565}," * 画布便签：纯注释，不参与调度、连线与计费。\n",[559,1521,1522],{"class":561,"line":583},[559,1523,706],{"class":565},[372,1525,1527],{"className":553,"code":1526,"language":555,"meta":380,"style":380},"\u002F\u002F 不是 xyflow 节点——它不参与连线、调度与计费，做成节点要在注册表、校验器、执行器三处逐一开豁免，成本远高于一张自绘卡片。\n",[147,1528,1529],{"__ignoreMap":380},[559,1530,1531],{"class":561,"line":562},[559,1532,1526],{"class":565},[10,1534,1535],{},"做成节点意味着它要在三处地方被显式排除。而排除逻辑每加一处，将来就多一处要维护的例外。做成一张画在流坐标系里的自绘卡片，这些例外一个都不需要。",[10,1537,1538],{},"契约里限制条数 100、单条 2000 字。",[75,1540,1542],{"id":1541},"六批量框","六、批量框",[10,1544,1545],{},"批量框是「框内的子图跑 N 遍」。它带来的第一件事是边界规则：",[10,1547,1548],{},"框内节点的输出不能接到框外，反过来可以。",[372,1550,1552],{"className":553,"code":1551,"language":555,"meta":380,"style":380},"\u002F\u002F 批量框边界规则：框内节点的输出不能接到框外（或另一个框）。\n\u002F\u002F 框内子图每份各跑一遍，往外接意味着下游要收 N 份——运行时不支持这种收束；\n\u002F\u002F 反方向（框外 → 框内）合法：同一上游共享给每份拷贝。\n",[147,1553,1554,1559,1564],{"__ignoreMap":380},[559,1555,1556],{"class":561,"line":562},[559,1557,1558],{"class":565},"\u002F\u002F 批量框边界规则：框内节点的输出不能接到框外（或另一个框）。\n",[559,1560,1561],{"class":561,"line":121},[559,1562,1563],{"class":565},"\u002F\u002F 框内子图每份各跑一遍，往外接意味着下游要收 N 份——运行时不支持这种收束；\n",[559,1565,1566],{"class":561,"line":583},[559,1567,1568],{"class":565},"\u002F\u002F 反方向（框外 → 框内）合法：同一上游共享给每份拷贝。\n",[10,1570,1571],{},"拒绝时的文案要给下一步：",[83,1573,1574],{},[10,1575,1576],{},"批量框内的节点不能连到框外：框内每份各跑一遍，产物会直接进素材库。要串联处理就把目标节点也拖进框里。",[10,1578,1579],{},"第二件事是删除节点的连带处理。节点被删掉之后，批量框里会留下幽灵成员，而运行创建时会拒绝整张图：",[372,1581,1583],{"className":553,"code":1582,"language":555,"meta":380,"style":380},"\u002F**\n * 从所有批量框成员里剔除已删除的节点；成员清空的框一并删除。\n *\n * 删除节点必须同步清理：残留的幽灵成员会让 run-create 直接拒绝整张画布\n * （「批量框引用了图中不存在的节点」），用户面对的是一张再也跑不起来的图。\n *\u002F\n",[147,1584,1585,1589,1594,1598,1603,1608],{"__ignoreMap":380},[559,1586,1587],{"class":561,"line":562},[559,1588,655],{"class":565},[559,1590,1591],{"class":561,"line":121},[559,1592,1593],{"class":565}," * 从所有批量框成员里剔除已删除的节点；成员清空的框一并删除。\n",[559,1595,1596],{"class":561,"line":583},[559,1597,882],{"class":565},[559,1599,1600],{"class":561,"line":589},[559,1601,1602],{"class":565}," * 删除节点必须同步清理：残留的幽灵成员会让 run-create 直接拒绝整张画布\n",[559,1604,1605],{"class":561,"line":673},[559,1606,1607],{"class":565}," * （「批量框引用了图中不存在的节点」），用户面对的是一张再也跑不起来的图。\n",[559,1609,1610],{"class":561,"line":679},[559,1611,706],{"class":565},[10,1613,1614],{},"第三件事是撤销栈。批量框要进快照：",[372,1616,1618],{"className":553,"code":1617,"language":555,"meta":380,"style":380},"export interface GraphSnapshot {\n  readonly nodes: readonly CanvasFlowNode[];\n  readonly edges: readonly CanvasFlowEdge[];\n  readonly selected: readonly string[];\n  \u002F** 批量框。撤销\u002F重做要连它一起回放，否则撤销删框后节点回来了框没了 *\u002F\n  readonly batchGroups: readonly CanvasFlowBatchGroup[];\n}\n",[147,1619,1620,1633,1653,1669,1685,1690,1706],{"__ignoreMap":380},[559,1621,1622,1625,1628,1631],{"class":561,"line":562},[559,1623,1624],{"class":727},"export",[559,1626,1627],{"class":727}," interface",[559,1629,1630],{"class":750}," GraphSnapshot",[559,1632,1026],{"class":740},[559,1634,1635,1638,1642,1644,1647,1650],{"class":561,"line":121},[559,1636,1637],{"class":727},"  readonly",[559,1639,1641],{"class":1640},"s4XuR"," nodes",[559,1643,1341],{"class":727},[559,1645,1646],{"class":727}," readonly",[559,1648,1649],{"class":750}," CanvasFlowNode",[559,1651,1652],{"class":740},"[];\n",[559,1654,1655,1657,1660,1662,1664,1667],{"class":561,"line":583},[559,1656,1637],{"class":727},[559,1658,1659],{"class":1640}," edges",[559,1661,1341],{"class":727},[559,1663,1646],{"class":727},[559,1665,1666],{"class":750}," CanvasFlowEdge",[559,1668,1652],{"class":740},[559,1670,1671,1673,1676,1678,1680,1683],{"class":561,"line":589},[559,1672,1637],{"class":727},[559,1674,1675],{"class":1640}," selected",[559,1677,1341],{"class":727},[559,1679,1646],{"class":727},[559,1681,1682],{"class":731}," string",[559,1684,1652],{"class":740},[559,1686,1687],{"class":561,"line":673},[559,1688,1689],{"class":565},"  \u002F** 批量框。撤销\u002F重做要连它一起回放，否则撤销删框后节点回来了框没了 *\u002F\n",[559,1691,1692,1694,1697,1699,1701,1704],{"class":561,"line":679},[559,1693,1637],{"class":727},[559,1695,1696],{"class":1640}," batchGroups",[559,1698,1341],{"class":727},[559,1700,1646],{"class":727},[559,1702,1703],{"class":750}," CanvasFlowBatchGroup",[559,1705,1652],{"class":740},[559,1707,1708],{"class":561,"line":685},[559,1709,1710],{"class":740},"}\n",[10,1712,1713],{},"第四件事是状态聚合。批量框内一个节点对应多行执行状态，显示取哪个：",[372,1715,1718],{"className":1716,"code":1717,"language":377},[375],"failed > running > pending > cancelled > succeeded > idle\n",[147,1719,1717],{"__ignoreMap":380},[10,1721,1722,1723,1726],{},"原先只让 ",[147,1724,1725],{},"failed"," 优先。结果是第一份先成功、第二份还在跑的时候，节点就提前显示成「成功」。",[75,1728,1730],{"id":1729},"七续跑不重复扣费","七、续跑不重复扣费",[10,1732,1733],{},"这一项是这八项里唯一涉及钱的。",[10,1735,1736],{},"失败或被取消的运行，可以续跑。实现方式不是「重跑一遍」：",[372,1738,1740],{"className":553,"code":1739,"language":555,"meta":380,"style":380},"\u002F**\n * 单节点重试：给失败\u002F被取消的运行造一个「续跑」运行。成功节点的行原样回填\n * （产物、billingRef、时间戳都保留）——executor 的调度器见到 succeeded 行\n * 会直接当作上游已就绪，不会重新执行，也就不会重复扣费；其余节点\n * （failed \u002F cancelled \u002F pending）重置成全新的 pending 行，正常调度重跑。\n * 不修改原运行：重试是一条新的 CanvasFlowRun，历史记录保持完整。\n *\u002F\n",[147,1741,1742,1746,1751,1756,1761,1766,1771],{"__ignoreMap":380},[559,1743,1744],{"class":561,"line":562},[559,1745,655],{"class":565},[559,1747,1748],{"class":561,"line":121},[559,1749,1750],{"class":565}," * 单节点重试：给失败\u002F被取消的运行造一个「续跑」运行。成功节点的行原样回填\n",[559,1752,1753],{"class":561,"line":583},[559,1754,1755],{"class":565}," * （产物、billingRef、时间戳都保留）——executor 的调度器见到 succeeded 行\n",[559,1757,1758],{"class":561,"line":589},[559,1759,1760],{"class":565}," * 会直接当作上游已就绪，不会重新执行，也就不会重复扣费；其余节点\n",[559,1762,1763],{"class":561,"line":673},[559,1764,1765],{"class":565}," * （failed \u002F cancelled \u002F pending）重置成全新的 pending 行，正常调度重跑。\n",[559,1767,1768],{"class":561,"line":679},[559,1769,1770],{"class":565}," * 不修改原运行：重试是一条新的 CanvasFlowRun，历史记录保持完整。\n",[559,1772,1773],{"class":561,"line":685},[559,1774,706],{"class":565},[10,1776,1777,1778,1781],{},"关键在于复用判定落在行状态上，而不是「这次运行是新是旧」。所以续跑不需要额外的豁免逻辑：被判成功的节点带着原来的 ",[147,1779,1780],{},"billingRef","，调度器看到它就不再执行。",[10,1783,1784],{},"预估只算子集，余额检查同理。接口层有幂等入口（按用户 + 请求 ID 查重）和归属校验。不可重试的两种情形给出明确文案：",[372,1786,1789],{"className":1787,"code":1788,"language":377},[375],"这次运行已全部成功，没有可重试的节点\n运行还没结束，等它终结后再重试\n",[147,1790,1788],{"__ignoreMap":380},[10,1792,1793],{},"界面上的按钮从「重试」改成「重试失败节点」，带一句说明：",[83,1795,1796],{},[10,1797,1798],{},"成功节点的产物直接沿用，只有失败的节点会重新执行并计费。",[75,1800,1802],{"id":1801},"八八项之间的关系","八、八项之间的关系",[10,1804,1805],{},"单独看每一项，都是常规功能。放在一起时，出现了几条贯穿的取舍：",[10,1807,1808,1811,1812,1814],{},[88,1809,1810],{},"状态的归属要单一。"," 画布 ID 放 URL；运行状态用同一套 ",[147,1813,1297],{},"；撤销栈是唯一的变更入口。三处都收成一个来源之后，「刷新之后看到什么」才有确定答案。",[10,1816,1817,1820],{},[88,1818,1819],{},"例外要少。"," 便签不做成节点，就是为了避免在注册表、校验器、执行器三处开豁免。每开一处例外，就多一处将来会忘记的地方。",[10,1822,1823,1826],{},[88,1824,1825],{},"别抢用户的操作。"," 没选节点时不拦 Cmd+C；整理布局保留同列原有顺序；批量框拒绝时给下一步而不是只报错。",[10,1828,1829,1832],{},[88,1830,1831],{},"涉及钱的判定落在状态上。"," 续跑复用靠行状态，不靠运行的新旧；批量份数由框上显式配置，预估与执行读同一个函数。",[10,1834,1835],{},"最后一条是这一天唯一和故障档案有关的部分。同一天里，批量链路翻出一处行约定矛盾和一处从未命中的推断分支，两处的成因都是「两侧各写一份」。八项补齐之后，取值入口都比之前更集中——这不是巧合，是同一件事的两个方向。",[1188,1837,1838],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":380,"searchDepth":121,"depth":121,"links":1840},[1841,1842,1843,1844,1845,1846,1847,1848],{"id":1228,"depth":121,"text":1229},{"id":1278,"depth":121,"text":1279},{"id":1316,"depth":121,"text":1317},{"id":1399,"depth":121,"text":1400},{"id":1501,"depth":121,"text":1502},{"id":1541,"depth":121,"text":1542},{"id":1729,"depth":121,"text":1730},{"id":1801,"depth":121,"text":1802},"2026-08-20",{},"\u002F2026-08-20",{"title":1217,"description":1222},"2026-08-20-画布这一步深链运行历史与续跑","一次补齐八项画布操作：URL 深链、小地图、运行历史、复制粘贴、批量框、一键整理布局、便签，以及失败运行的单节点续跑。",[1856,1857,1858,1859,1860,1861],"画布","工作流","URL状态","撤销重做","批量执行","单节点重试","eA-RfotRTWBXXppeiZFtcguPXjtpnVRWrkWgI5LKfY0",{"id":1864,"title":1865,"body":1866,"column":483,"date":2904,"description":1870,"extension":485,"hero_image":486,"meta":2905,"navigation":488,"path":2906,"seo":2907,"series_id":486,"severity":486,"stem":2908,"summary":2909,"tags":2910,"__hash__":2916},"posts\u002F2026-08-08-同一张能力表抄了三份.md","同一张能力表，抄了三份",{"type":7,"value":1867,"toc":2896},[1868,1871,1874,1877,1881,1884,2035,2038,2044,2050,2053,2121,2124,2128,2131,2134,2156,2159,2162,2168,2183,2186,2190,2193,2196,2202,2205,2333,2336,2345,2348,2355,2566,2569,2573,2584,2587,2590,2651,2654,2715,2718,2751,2755,2758,2761,2767,2774,2777,2784,2791,2795,2798,2825,2831,2834,2884,2890,2893],[10,1869,1870],{},"用户反馈：设置里选完模型，分辨率档不对——Seedance-2.5 只有 480P 和 720P；另外 2.5 的上游支持到 29 秒，故事板那边应该跟着放开。",[10,1872,1873],{},"第一句说得对，2.5 确实只有 480p 和 720p。第二句也对，它确实支持 4~29 秒。",[10,1875,1876],{},"两条都对，说明我们发出去的档位和上游支持的对不上。查下去发现，同一份能力表被抄了三份。",[75,1878,1880],{"id":1879},"一三处各自漂移","一、三处各自漂移",[10,1882,1883],{},"后端有一张权威表，写清了每个模型支持哪些分辨率、哪些时长：",[372,1885,1887],{"className":553,"code":1886,"language":555,"meta":380,"style":380},"const MODEL_RESOLUTIONS: Record\u003CVideoModel, readonly VideoResolution[]> = {\n  \"seedance-2\": [\"480p\", \"720p\", \"1080p\", \"4k\"],\n  \"seedance-2-fast\": [\"480p\", \"720p\"],\n  \"seedance-2-mini\": [\"480p\", \"720p\"],\n  \"seedance-2.5\": [\"480p\", \"720p\"],\n  \"kling-v3\": [\"720p\", \"1080p\"],\n  \"minimax-h3\": [\"2k\", \"768p\"],\n};\n",[147,1888,1889,1924,1953,1968,1983,1998,2013,2030],{"__ignoreMap":380},[559,1890,1891,1893,1896,1898,1901,1904,1907,1910,1913,1916,1919,1922],{"class":561,"line":562},[559,1892,728],{"class":727},[559,1894,1895],{"class":731}," MODEL_RESOLUTIONS",[559,1897,1341],{"class":727},[559,1899,1900],{"class":750}," Record",[559,1902,1903],{"class":740},"\u003C",[559,1905,1906],{"class":750},"VideoModel",[559,1908,1909],{"class":740},", ",[559,1911,1912],{"class":727},"readonly",[559,1914,1915],{"class":750}," VideoResolution",[559,1917,1918],{"class":740},"[]> ",[559,1920,1921],{"class":727},"=",[559,1923,1026],{"class":740},[559,1925,1926,1929,1932,1935,1937,1940,1942,1945,1947,1950],{"class":561,"line":121},[559,1927,1928],{"class":1016},"  \"seedance-2\"",[559,1930,1931],{"class":740},": [",[559,1933,1934],{"class":1016},"\"480p\"",[559,1936,1909],{"class":740},[559,1938,1939],{"class":1016},"\"720p\"",[559,1941,1909],{"class":740},[559,1943,1944],{"class":1016},"\"1080p\"",[559,1946,1909],{"class":740},[559,1948,1949],{"class":1016},"\"4k\"",[559,1951,1952],{"class":740},"],\n",[559,1954,1955,1958,1960,1962,1964,1966],{"class":561,"line":583},[559,1956,1957],{"class":1016},"  \"seedance-2-fast\"",[559,1959,1931],{"class":740},[559,1961,1934],{"class":1016},[559,1963,1909],{"class":740},[559,1965,1939],{"class":1016},[559,1967,1952],{"class":740},[559,1969,1970,1973,1975,1977,1979,1981],{"class":561,"line":589},[559,1971,1972],{"class":1016},"  \"seedance-2-mini\"",[559,1974,1931],{"class":740},[559,1976,1934],{"class":1016},[559,1978,1909],{"class":740},[559,1980,1939],{"class":1016},[559,1982,1952],{"class":740},[559,1984,1985,1988,1990,1992,1994,1996],{"class":561,"line":673},[559,1986,1987],{"class":1016},"  \"seedance-2.5\"",[559,1989,1931],{"class":740},[559,1991,1934],{"class":1016},[559,1993,1909],{"class":740},[559,1995,1939],{"class":1016},[559,1997,1952],{"class":740},[559,1999,2000,2003,2005,2007,2009,2011],{"class":561,"line":679},[559,2001,2002],{"class":1016},"  \"kling-v3\"",[559,2004,1931],{"class":740},[559,2006,1939],{"class":1016},[559,2008,1909],{"class":740},[559,2010,1944],{"class":1016},[559,2012,1952],{"class":740},[559,2014,2015,2018,2020,2023,2025,2028],{"class":561,"line":685},[559,2016,2017],{"class":1016},"  \"minimax-h3\"",[559,2019,1931],{"class":740},[559,2021,2022],{"class":1016},"\"2k\"",[559,2024,1909],{"class":740},[559,2026,2027],{"class":1016},"\"768p\"",[559,2029,1952],{"class":740},[559,2031,2032],{"class":561,"line":691},[559,2033,2034],{"class":740},"};\n",[10,2036,2037],{},"前端有一份手抄的副本。它早就漂了，而且漂在两个方向。",[10,2039,2040,2043],{},[88,2041,2042],{},"多出的档位。"," 设置页的取档逻辑是写死的规则：「H3 两档、其余一律四档」。于是 2.5、fast、mini 这三个只有 480p 和 720p 的模型，界面上给出 1080P 和 4K。用户选了，选中即报未配价，或者直接被上游拒。",[10,2045,2046,2049],{},[88,2047,2048],{},"少掉的档位。"," Kling 只支持 720p 和 1080p，界面给的是四档。反过来的情况也存在：如果某个模型的档位比默认四档更多，界面也显示不出来。",[10,2051,2052],{},"时长那一处漂得更彻底。前端和后端各写了一份 4~15 的夹取：",[372,2054,2056],{"className":553,"code":2055,"language":555,"meta":380,"style":380},"\u002F\u002F 前端\nreturn Math.max(4, Math.min(15, value));\n\n\u002F\u002F 后端\nreturn Math.max(4, Math.min(15, value));\n",[147,2057,2058,2063,2090,2094,2099],{"__ignoreMap":380},[559,2059,2060],{"class":561,"line":562},[559,2061,2062],{"class":565},"\u002F\u002F 前端\n",[559,2064,2065,2068,2070,2073,2075,2078,2080,2082,2084,2087],{"class":561,"line":121},[559,2066,2067],{"class":727},"return",[559,2069,747],{"class":740},[559,2071,2072],{"class":750},"max",[559,2074,754],{"class":740},[559,2076,2077],{"class":731},"4",[559,2079,760],{"class":740},[559,2081,751],{"class":750},[559,2083,754],{"class":740},[559,2085,2086],{"class":731},"15",[559,2088,2089],{"class":740},", value));\n",[559,2091,2092],{"class":561,"line":583},[559,2093,1060],{"emptyLinePlaceholder":488},[559,2095,2096],{"class":561,"line":589},[559,2097,2098],{"class":565},"\u002F\u002F 后端\n",[559,2100,2101,2103,2105,2107,2109,2111,2113,2115,2117,2119],{"class":561,"line":673},[559,2102,2067],{"class":727},[559,2104,747],{"class":740},[559,2106,2072],{"class":750},[559,2108,754],{"class":740},[559,2110,2077],{"class":731},[559,2112,760],{"class":740},[559,2114,751],{"class":750},[559,2116,754],{"class":740},[559,2118,2086],{"class":731},[559,2120,2089],{"class":740},[10,2122,2123],{},"2.5 传 29 秒，被悄悄砍成 15。用户看不出为什么变短——没有任何提示，界面上显示的就是 15 秒。",[75,2125,2127],{"id":2126},"二被砍掉的秒数后面跟着钱","二、被砍掉的秒数后面跟着钱",[10,2129,2130],{},"时长那一处还不只是显示问题。",[10,2132,2133],{},"故事板按固定秒数切板。代码里是一个常量：",[372,2135,2137],{"className":553,"code":2136,"language":555,"meta":380,"style":380},"export const SHOT_SECONDS = 15;\n",[147,2138,2139],{"__ignoreMap":380},[559,2140,2141,2143,2146,2149,2151,2154],{"class":561,"line":562},[559,2142,1624],{"class":727},[559,2144,2145],{"class":727}," const",[559,2147,2148],{"class":731}," SHOT_SECONDS",[559,2150,1353],{"class":727},[559,2152,2153],{"class":731}," 15",[559,2155,1358],{"class":740},[10,2157,2158],{},"按 15 秒一切。2.5 一板能放 29 秒，硬按 15 秒切，一集会被切成两倍数量的板。",[10,2160,2161],{},"板数翻倍就是出图与出片的费用翻倍。这不是理论推算——用户选 2.5 的动机就是长板数少切，结果切得和短时长模型一样多，还多花一倍钱。",[10,2163,2164,2167],{},[147,2165,2166],{},"comic-subshot.ts"," 里留着上限常量的注释，写明了它只是「模型未知时的保守默认」：",[372,2169,2171],{"className":553,"code":2170,"language":555,"meta":380,"style":380},"\u002F\u002F 上限只是「模型未知时的保守默认」——真正的上限逐模型不同（seedance-2.5 能到 29 秒），\n\u002F\u002F 调用方应把该模型的最大秒数作为 maxBoardSec 传进来。写死 15 的话，选了 2.5 也只切 15 秒一板。\n",[147,2172,2173,2178],{"__ignoreMap":380},[559,2174,2175],{"class":561,"line":562},[559,2176,2177],{"class":565},"\u002F\u002F 上限只是「模型未知时的保守默认」——真正的上限逐模型不同（seedance-2.5 能到 29 秒），\n",[559,2179,2180],{"class":561,"line":121},[559,2181,2182],{"class":565},"\u002F\u002F 调用方应把该模型的最大秒数作为 maxBoardSec 传进来。写死 15 的话，选了 2.5 也只切 15 秒一板。\n",[10,2184,2185],{},"调用方没传。",[75,2187,2189],{"id":2188},"三提示词长度四个数一个是实测出来的","三、提示词长度：四个数，一个是实测出来的",[10,2191,2192],{},"同一批里还有一个更贵的限制：提示词长度上限。",[10,2194,2195],{},"上游对提示词有硬上限，超过直接拒。各模型不一样，而这些值在很长一段时间里没有集中维护。表现是一条线上的完整失败：",[372,2197,2200],{"className":2198,"code":2199,"language":377},[375],"模型 seedance-2.5 的提示词不能超过 5000 个字符，当前为 18545 个字符\n",[147,2201,2199],{"__ignoreMap":380},[10,2203,2204],{},"接入本身没问题，是缺了长度约束。补完之后这张表长这样：",[372,2206,2208],{"className":553,"code":2207,"language":555,"meta":380,"style":380},"const MODEL_PROMPT_LIMITS: Partial\u003CRecord\u003CVideoModel, number>> = {\n  \"minimax-h3\": 7000,\n  \"seedance-2\": 2500,\n  \"seedance-2-fast\": 2500,\n  \"seedance-2-mini\": 2500,\n  \"seedance-2.5\": 5000,\n  \u002F\u002F 20260809 实测：发 2553 字被硬拒 `prompt: size must be between 0 and 2500`\n  \"kling-v3\": 2500,\n};\nexport const PROMPT_LIMIT_FALLBACK = 50000;\n",[147,2209,2210,2243,2256,2267,2277,2287,2298,2303,2313,2317],{"__ignoreMap":380},[559,2211,2212,2214,2217,2219,2222,2224,2227,2229,2231,2233,2236,2239,2241],{"class":561,"line":562},[559,2213,728],{"class":727},[559,2215,2216],{"class":731}," MODEL_PROMPT_LIMITS",[559,2218,1341],{"class":727},[559,2220,2221],{"class":750}," Partial",[559,2223,1903],{"class":740},[559,2225,2226],{"class":750},"Record",[559,2228,1903],{"class":740},[559,2230,1906],{"class":750},[559,2232,1909],{"class":740},[559,2234,2235],{"class":731},"number",[559,2237,2238],{"class":740},">> ",[559,2240,1921],{"class":727},[559,2242,1026],{"class":740},[559,2244,2245,2247,2250,2253],{"class":561,"line":121},[559,2246,2017],{"class":1016},[559,2248,2249],{"class":740},": ",[559,2251,2252],{"class":731},"7000",[559,2254,2255],{"class":740},",\n",[559,2257,2258,2260,2262,2265],{"class":561,"line":583},[559,2259,1928],{"class":1016},[559,2261,2249],{"class":740},[559,2263,2264],{"class":731},"2500",[559,2266,2255],{"class":740},[559,2268,2269,2271,2273,2275],{"class":561,"line":589},[559,2270,1957],{"class":1016},[559,2272,2249],{"class":740},[559,2274,2264],{"class":731},[559,2276,2255],{"class":740},[559,2278,2279,2281,2283,2285],{"class":561,"line":673},[559,2280,1972],{"class":1016},[559,2282,2249],{"class":740},[559,2284,2264],{"class":731},[559,2286,2255],{"class":740},[559,2288,2289,2291,2293,2296],{"class":561,"line":679},[559,2290,1987],{"class":1016},[559,2292,2249],{"class":740},[559,2294,2295],{"class":731},"5000",[559,2297,2255],{"class":740},[559,2299,2300],{"class":561,"line":685},[559,2301,2302],{"class":565},"  \u002F\u002F 20260809 实测：发 2553 字被硬拒 `prompt: size must be between 0 and 2500`\n",[559,2304,2305,2307,2309,2311],{"class":561,"line":691},[559,2306,2002],{"class":1016},[559,2308,2249],{"class":740},[559,2310,2264],{"class":731},[559,2312,2255],{"class":740},[559,2314,2315],{"class":561,"line":697},[559,2316,2034],{"class":740},[559,2318,2319,2321,2323,2326,2328,2331],{"class":561,"line":703},[559,2320,1624],{"class":727},[559,2322,2145],{"class":727},[559,2324,2325],{"class":731}," PROMPT_LIMIT_FALLBACK",[559,2327,1353],{"class":727},[559,2329,2330],{"class":731}," 50000",[559,2332,1358],{"class":740},[10,2334,2335],{},"这几个数的来源不同，注释里写清了哪个是实测的：",[83,2337,2338],{},[10,2339,2340,2341,2344],{},"seedance-2.5 的 5000 是",[88,2342,2343],{},"实测出来的，文档只字未提","：线上一条 18545 字的脚本被拒，报文写「提示词不能超过 5000 个字符」。同批实测另两条渠道收 16500 字照样 200，所以这是 2.5 独有的限制，不能推广到整个系列。加新渠道前先拿超长 prompt 打一次，别等线上炸。",[10,2346,2347],{},"这段话是这张表里唯一带出处的一条。其余几个数是按上游文档填的，文档没提的只能等线上撞。",[10,2349,2350,2351,2354],{},"长度约束补齐之后，还有一个配套的预算计算。原先只有 H3 有预算，其余模型返回 ",[147,2352,2353],{},"null","，等于完全不限：",[372,2356,2358],{"className":553,"code":2357,"language":555,"meta":380,"style":380},"\u002F** 每秒成片对应的脚本篇幅。15 秒 → 7500 字，是人肉审稿与出片效果都合适的密度。 *\u002F\nexport const CHARS_PER_SECOND = 500;\n\u002F** 留给用户自己追加修改的余量：脚本刚好顶满上限时，用户加一句就被上游拒了。 *\u002F\nconst PROMPT_BUDGET_MARGIN = 500;\n\nexport function scriptCharBudget(targetModel: string | undefined, durationSec?: number): number | null {\n  const hardLimit = modelPromptHardLimit(targetModel);\n  if (!durationSec || !Number.isFinite(durationSec) || durationSec \u003C= 0) return hardLimit;\n  const byDuration = Math.round(durationSec * CHARS_PER_SECOND);\n  return hardLimit ? Math.min(byDuration, hardLimit) : byDuration;\n}\n",[147,2359,2360,2365,2381,2386,2399,2403,2451,2467,2515,2538,2561],{"__ignoreMap":380},[559,2361,2362],{"class":561,"line":562},[559,2363,2364],{"class":565},"\u002F** 每秒成片对应的脚本篇幅。15 秒 → 7500 字，是人肉审稿与出片效果都合适的密度。 *\u002F\n",[559,2366,2367,2369,2371,2374,2376,2379],{"class":561,"line":121},[559,2368,1624],{"class":727},[559,2370,2145],{"class":727},[559,2372,2373],{"class":731}," CHARS_PER_SECOND",[559,2375,1353],{"class":727},[559,2377,2378],{"class":731}," 500",[559,2380,1358],{"class":740},[559,2382,2383],{"class":561,"line":583},[559,2384,2385],{"class":565},"\u002F** 留给用户自己追加修改的余量：脚本刚好顶满上限时，用户加一句就被上游拒了。 *\u002F\n",[559,2387,2388,2390,2393,2395,2397],{"class":561,"line":589},[559,2389,728],{"class":727},[559,2391,2392],{"class":731}," PROMPT_BUDGET_MARGIN",[559,2394,1353],{"class":727},[559,2396,2378],{"class":731},[559,2398,1358],{"class":740},[559,2400,2401],{"class":561,"line":673},[559,2402,1060],{"emptyLinePlaceholder":488},[559,2404,2405,2407,2410,2413,2415,2418,2420,2422,2424,2427,2429,2432,2435,2438,2441,2443,2445,2447,2449],{"class":561,"line":679},[559,2406,1624],{"class":727},[559,2408,2409],{"class":727}," function",[559,2411,2412],{"class":750}," scriptCharBudget",[559,2414,754],{"class":740},[559,2416,2417],{"class":1640},"targetModel",[559,2419,1341],{"class":727},[559,2421,1682],{"class":731},[559,2423,1347],{"class":727},[559,2425,2426],{"class":731}," undefined",[559,2428,1909],{"class":740},[559,2430,2431],{"class":1640},"durationSec",[559,2433,2434],{"class":727},"?:",[559,2436,2437],{"class":731}," number",[559,2439,2440],{"class":740},")",[559,2442,1341],{"class":727},[559,2444,2437],{"class":731},[559,2446,1347],{"class":727},[559,2448,1350],{"class":731},[559,2450,1026],{"class":740},[559,2452,2453,2456,2459,2461,2464],{"class":561,"line":685},[559,2454,2455],{"class":727},"  const",[559,2457,2458],{"class":731}," hardLimit",[559,2460,1353],{"class":727},[559,2462,2463],{"class":750}," modelPromptHardLimit",[559,2465,2466],{"class":740},"(targetModel);\n",[559,2468,2469,2472,2475,2478,2481,2484,2487,2490,2493,2496,2498,2501,2504,2507,2510,2512],{"class":561,"line":691},[559,2470,2471],{"class":727},"  if",[559,2473,2474],{"class":740}," (",[559,2476,2477],{"class":727},"!",[559,2479,2480],{"class":740},"durationSec ",[559,2482,2483],{"class":727},"||",[559,2485,2486],{"class":727}," !",[559,2488,2489],{"class":740},"Number.",[559,2491,2492],{"class":750},"isFinite",[559,2494,2495],{"class":740},"(durationSec) ",[559,2497,2483],{"class":727},[559,2499,2500],{"class":740}," durationSec ",[559,2502,2503],{"class":727},"\u003C=",[559,2505,2506],{"class":731}," 0",[559,2508,2509],{"class":740},") ",[559,2511,2067],{"class":727},[559,2513,2514],{"class":740}," hardLimit;\n",[559,2516,2517,2519,2522,2524,2526,2529,2532,2534,2536],{"class":561,"line":697},[559,2518,2455],{"class":727},[559,2520,2521],{"class":731}," byDuration",[559,2523,1353],{"class":727},[559,2525,747],{"class":740},[559,2527,2528],{"class":750},"round",[559,2530,2531],{"class":740},"(durationSec ",[559,2533,769],{"class":727},[559,2535,2373],{"class":731},[559,2537,1050],{"class":740},[559,2539,2540,2543,2546,2549,2551,2553,2556,2558],{"class":561,"line":703},[559,2541,2542],{"class":727},"  return",[559,2544,2545],{"class":740}," hardLimit ",[559,2547,2548],{"class":727},"?",[559,2550,747],{"class":740},[559,2552,751],{"class":750},[559,2554,2555],{"class":740},"(byDuration, hardLimit) ",[559,2557,1341],{"class":727},[559,2559,2560],{"class":740}," byDuration;\n",[559,2562,2564],{"class":561,"line":2563},11,[559,2565,1710],{"class":740},[10,2567,2568],{},"按 15 秒算出来是 7500 字——上限撤销之后，脚本直接写到 1.8 万字，那也是生成耗时 193 秒的原因。",[75,2570,2572],{"id":2571},"四抄一份是必要的那就测试它","四、抄一份是必要的，那就测试它",[10,2574,2575,2576,2579,2580,2583],{},"前端为什么不能直接读后端那张表：",[147,2577,2578],{},"apps\u002Fweb"," 不能 import ",[147,2581,2582],{},"apps\u002Fapi","。而节点面板必须知道「这个模型支持哪些分辨率、哪些时长」，才能只给出跑得通的选项。",[10,2585,2586],{},"抄一份是必要的。那就把「不许走样」变成断言。",[10,2588,2589],{},"新增的共享包文件头写明了它的性质和守卫：",[372,2591,2593],{"className":553,"code":2592,"language":555,"meta":380,"style":380},"\u002F**\n * 生图 \u002F 视频的**上游真实能力**表。\n *\n * 这是 apps\u002Fapi 里两张表的镜像……\n *\n * **为什么要抄一份**：前端（apps\u002Fweb）不能 import apps\u002Fapi，而节点面板必须知道\n * 「这个模型支持哪些分辨率、哪些时长」才能只给出跑得通的选项。\n * 抄一份就有走样的风险，所以 apps\u002Fapi 里有一条对比测试逐项核对两边——\n * 谁改了上游表而没同步这里，测试立刻红。\n *\n * 硬规矩：这里只允许出现上游真支持的取值。多给一个选项，用户就会选到一个必然失败的组合。\n *\u002F\n",[147,2594,2595,2599,2604,2608,2613,2617,2622,2627,2632,2637,2641,2646],{"__ignoreMap":380},[559,2596,2597],{"class":561,"line":562},[559,2598,655],{"class":565},[559,2600,2601],{"class":561,"line":121},[559,2602,2603],{"class":565}," * 生图 \u002F 视频的**上游真实能力**表。\n",[559,2605,2606],{"class":561,"line":583},[559,2607,882],{"class":565},[559,2609,2610],{"class":561,"line":589},[559,2611,2612],{"class":565}," * 这是 apps\u002Fapi 里两张表的镜像……\n",[559,2614,2615],{"class":561,"line":673},[559,2616,882],{"class":565},[559,2618,2619],{"class":561,"line":679},[559,2620,2621],{"class":565}," * **为什么要抄一份**：前端（apps\u002Fweb）不能 import apps\u002Fapi，而节点面板必须知道\n",[559,2623,2624],{"class":561,"line":685},[559,2625,2626],{"class":565}," * 「这个模型支持哪些分辨率、哪些时长」才能只给出跑得通的选项。\n",[559,2628,2629],{"class":561,"line":691},[559,2630,2631],{"class":565}," * 抄一份就有走样的风险，所以 apps\u002Fapi 里有一条对比测试逐项核对两边——\n",[559,2633,2634],{"class":561,"line":697},[559,2635,2636],{"class":565}," * 谁改了上游表而没同步这里，测试立刻红。\n",[559,2638,2639],{"class":561,"line":703},[559,2640,882],{"class":565},[559,2642,2643],{"class":561,"line":2563},[559,2644,2645],{"class":565}," * 硬规矩：这里只允许出现上游真支持的取值。多给一个选项，用户就会选到一个必然失败的组合。\n",[559,2647,2649],{"class":561,"line":2648},12,[559,2650,706],{"class":565},[10,2652,2653],{},"同时把取值这件事收成单点：",[372,2655,2657],{"className":553,"code":2656,"language":555,"meta":380,"style":380},"\u002F**\n * 前端渲染面板、后端校验参数都走这一个函数——两边各写一份判断，\n * 迟早出现「界面给得出、服务端不认」的组合。\n *\u002F\nexport function resolveDynamicOptions(kind: \"videoResolution\" | \"videoDuration\", params): readonly { value, label }[]\n",[147,2658,2659,2663,2668,2673,2677],{"__ignoreMap":380},[559,2660,2661],{"class":561,"line":562},[559,2662,655],{"class":565},[559,2664,2665],{"class":561,"line":121},[559,2666,2667],{"class":565}," * 前端渲染面板、后端校验参数都走这一个函数——两边各写一份判断，\n",[559,2669,2670],{"class":561,"line":583},[559,2671,2672],{"class":565}," * 迟早出现「界面给得出、服务端不认」的组合。\n",[559,2674,2675],{"class":561,"line":589},[559,2676,706],{"class":565},[559,2678,2679,2681,2683,2686,2688,2691,2693,2696,2698,2701,2703,2706,2708,2710,2712],{"class":561,"line":673},[559,2680,1624],{"class":727},[559,2682,2409],{"class":727},[559,2684,2685],{"class":750}," resolveDynamicOptions",[559,2687,754],{"class":740},[559,2689,2690],{"class":1640},"kind",[559,2692,1341],{"class":727},[559,2694,2695],{"class":1016}," \"videoResolution\"",[559,2697,1347],{"class":727},[559,2699,2700],{"class":1016}," \"videoDuration\"",[559,2702,1909],{"class":740},[559,2704,2705],{"class":1640},"params",[559,2707,2440],{"class":740},[559,2709,1341],{"class":727},[559,2711,1646],{"class":727},[559,2713,2714],{"class":740}," { value, label }[]\n",[10,2716,2717],{},"细粒度那一档也保留了两份清单，且是有意不同：",[372,2719,2721],{"className":553,"code":2720,"language":555,"meta":380,"style":380},"\u002F**\n * 注意它与下面的 MODEL_DURATION_OPTIONS 是两回事、且**故意不同**：\n * 后者是「按钮行」的精简清单（seedance 只列 7 档，避免一排按钮太长），\n * 而滑块是细粒度选择，应当覆盖服务端真正接受的全部档位——\n * 否则用户拖不到 7\u002F9\u002F11\u002F13\u002F14 秒，白白削掉能力。\n *\u002F\n",[147,2722,2723,2727,2732,2737,2742,2747],{"__ignoreMap":380},[559,2724,2725],{"class":561,"line":562},[559,2726,655],{"class":565},[559,2728,2729],{"class":561,"line":121},[559,2730,2731],{"class":565}," * 注意它与下面的 MODEL_DURATION_OPTIONS 是两回事、且**故意不同**：\n",[559,2733,2734],{"class":561,"line":583},[559,2735,2736],{"class":565}," * 后者是「按钮行」的精简清单（seedance 只列 7 档，避免一排按钮太长），\n",[559,2738,2739],{"class":561,"line":589},[559,2740,2741],{"class":565}," * 而滑块是细粒度选择，应当覆盖服务端真正接受的全部档位——\n",[559,2743,2744],{"class":561,"line":673},[559,2745,2746],{"class":565}," * 否则用户拖不到 7\u002F9\u002F11\u002F13\u002F14 秒，白白削掉能力。\n",[559,2748,2749],{"class":561,"line":679},[559,2750,706],{"class":565},[75,2752,2754],{"id":2753},"五假绿的七条用例","五、假绿的七条用例",[10,2756,2757],{},"修的过程中还撞到一个测试问题，值得单独记。",[10,2759,2760],{},"改动完成后跑测试，有一组用例报了这个警告：",[372,2762,2765],{"className":2763,"code":2764,"language":377},[375],"This might cause false positive tests\n",[147,2766,2764],{"__ignoreMap":380},[10,2768,2769,2770,2773],{},"追下去发现是真的假绿。供应商迁移之后，配置加载在某些条件下会抛错，而那个抛错发生在 ",[147,2771,2772],{},"try"," 之外，成了未处理的 rejection。用例本身并不感知异常，于是照样通过。",[10,2775,2776],{},"具体是 7 条。",[10,2778,2779,2780,2783],{},"修法是让配置加载把异常收敛成一个明确的错误类型（缺 key 重试没有意义，归为永久失败），并在两个相关测试文件的模块加载阶段注入测试用 key。注入位置也有讲究——放 ",[147,2781,2782],{},"beforeEach"," 会因为跨文件执行顺序失效。",[10,2785,2786,2787,2790],{},"这件事和主题有关：",[88,2788,2789],{},"同一份能力表抄三份会漂，同一份配置在三个地方加载也会。"," 假绿的七条用例，测的是「配置能加载」，而配置在测试环境里根本没被加载。",[75,2792,2794],{"id":2793},"六收口之后","六、收口之后",[10,2796,2797],{},"同一批里还顺手修了两处同源问题：",[313,2799,2800,2811],{},[123,2801,2802,2803,2806,2807,2810],{},"漫剧设置页那份模型列表是前端手写的副本，早已和后端漂开：列出「Seedance-2.0 Pro」和「可灵 V2」两个根本不存在的 ID，而真实的 ID 是 ",[147,2804,2805],{},"seedance-2-fast"," 和 ",[147,2808,2809],{},"kling-v3","。选中即被出片接口的 zod 拒掉。改成读取后端的模型清单接口。",[123,2812,2813,2814,1268,2817,2820,2821,2824],{},"长篇项目的设置页没有把 ",[147,2815,2816],{},"videoModel",[147,2818,2819],{},"videoResolution"," 传给设置组件，",[147,2822,2823],{},"onSave"," 也没往上收。用户选完模型点保存，没有报错，刷新之后回退到原来的值。",[10,2826,2827,2828,432],{},"三处问题的形式不同，成因是同一个：",[88,2829,2830],{},"同一份事实存在多个副本，而且没有一处是权威",[10,2832,2833],{},"收口之后的结构是三份，每一份都有明确职责：",[20,2835,2836,2849],{},[23,2837,2838],{},[26,2839,2840,2843,2846],{},[29,2841,2842],{},"位置",[29,2844,2845],{},"职责",[29,2847,2848],{},"守卫",[36,2850,2851,2862,2873],{},[26,2852,2853,2856,2859],{},[41,2854,2855],{},"后端能力表",[41,2857,2858],{},"权威来源",[41,2860,2861],{},"被镜像表逐项对比",[26,2863,2864,2867,2870],{},[41,2865,2866],{},"共享包镜像表",[41,2868,2869],{},"跨端取值",[41,2871,2872],{},"对比测试（改一边不改另一边即红）",[26,2874,2875,2878,2881],{},[41,2876,2877],{},"前端 UI 清单",[41,2879,2880],{},"只影响展示形态",[41,2882,2883],{},"值必须来自共享包，不许写字面量",[10,2885,2886,2887],{},"硬规矩只有一条，写在镜像表里：",[88,2888,2889],{},"只允许出现上游真支持的取值。多给一个选项，用户就会选到一个必然失败的组合。",[10,2891,2892],{},"反过来说，少给一个选项的代价同样实在——2.5 的 29 秒被砍到 15，用户看到的是「这个模型没比别的强」，看不到的是我们没把它的能力交出去。",[1188,2894,2895],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":380,"searchDepth":121,"depth":121,"links":2897},[2898,2899,2900,2901,2902,2903],{"id":1879,"depth":121,"text":1880},{"id":2126,"depth":121,"text":2127},{"id":2188,"depth":121,"text":2189},{"id":2571,"depth":121,"text":2572},{"id":2753,"depth":121,"text":2754},{"id":2793,"depth":121,"text":2794},"2026-08-08",{},"\u002F2026-08-08",{"title":1865,"description":1870},"2026-08-08-同一张能力表抄了三份","上游模型的分辨率、时长、提示词长度散落在三处各自维护，各自漂移。一次用户反馈把三处一起翻出来：多出的档位、被砍掉的秒数、翻倍的板数。",[2911,2912,2913,2914,2915],"模型能力","配置漂移","契约","前端后端一致","提示词长度","xrC9lTC00ghKpLq5ZuDyYhLFe2fpcs3SgCXkd9QeigE",1789212573229]