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