[{"data":1,"prerenderedAt":1590},["ShallowReactive",2],{"post-ecom-detail-ai-part3-pitfalls":3,"post-nav-posts":1225,"post-project-ecom-detail-ai-part3-pitfalls":1249},{"id":4,"title":5,"body":6,"cover":1211,"date":1212,"description":80,"extension":1213,"links":1214,"meta":1215,"navigation":348,"path":1216,"project":33,"seo":1217,"slug":1218,"stem":1219,"summary":1220,"tags":1221,"visible":348,"__hash__":1224},"posts\u002Fposts\u002Fecom-detail-ai-part3-pitfalls.md","一个人做 AI 产品，我踩过的 9 个真实的坑",{"type":7,"value":8,"toc":1199},"minimark",[9,27,44,49,55,61,67,74,121,129,135,138,145,151,155,160,165,170,253,260,264,269,274,279,478,493,497,502,507,516,675,678,682,687,696,701,852,855,859,864,873,882,1008,1023,1027,1045,1065,1068,1072,1085,1090,1107,1110,1117,1123,1127,1132,1137,1150,1154,1170,1173,1179,1184,1195],[10,11,12],"blockquote",{},[13,14,15,16,21,22,26],"p",{},"系列说明：这是「AI 详情页生成工具」系列的第 3 篇，踩坑复盘。前两篇分别讲了",[17,18,20],"a",{"href":19},"\u002Fblog\u002Fecom-detail-ai-part1-architecture","架构","（多模型 Agent 流水线）和",[17,23,25],{"href":24},"\u002Fblog\u002Fecom-detail-ai-part2-wails","搭建","（Wails + Go + Vue3），这篇全是血泪：每一个坑都对应项目里一段现在还在跑的代码。文中代码节选自项目源码，有删减。",[13,28,29,30,34,35,39,40,43],{},"先交代背景：我一个人做的桌面端 AI 工具（",[17,31,33],{"href":32},"\u002Fprojects\u002Fecom-detail-ai","ecom-detail-ai","），上传 1 张商品图，自动跑一条\"读图→规划→生图→文案→排版→自检\"的流水线，产出整套淘宝详情页。LLM 按 token 计费、生图按张计费，全是用户自己的账号出钱。这个业态决定了踩坑的两大主题：",[36,37,38],"strong",{},"幻觉","和",[36,41,42],{},"钱","。",[45,46,48],"h2",{"id":47},"坑-1让-ai-在图里写字它给你一屏甲骨文","坑 1：让 AI 在图里写字，它给你一屏甲骨文",[13,50,51,54],{},[36,52,53],{},"现象","：详情页大量依赖文字排版（标题、卖点、参数标注）。早期我按直觉写提示词：\"画面左上角写上'轻松收纳'四个艺术字\"。Seedream 和 GLM-Image 交出来的图，字形扭曲、笔画乱炖，放大看像甲骨文他舅——生成式模型画文字至今依然是重灾区，而且越\"艺术字\"越完蛋。",[13,56,57,60],{},[36,58,59],{},"原因","：扩散模型不是\"写字\"，是在\"画字的形状\"，字符结构对它来说就是纹理。你禁不掉它画糊。",[13,62,63,66],{},[36,64,65],{},"解法","：彻底把文字从 AI 手里收走，三层设防：",[68,69,70],"ol",{},[71,72,73],"li",{},"规划提示词写入铁律，场景描述不得携带\"画文字\"的意图：",[75,76,81],"pre",{"className":77,"code":78,"language":79,"meta":80,"style":80},"language-go shiki shiki-themes github-dark github-light","const planNoTextRule = \"画面中禁止出现任何文字、数字、参数标注、标语或标签贴纸；\" +\n    \"prompt 只描述商品、场景、氛围与构图，不得要求画面书写、展示或标注任何文字\" +\n    \"（商品自身印刷的文字除外，那部分会被逐字保留）。\"\n","go","",[82,83,84,107,115],"code",{"__ignoreMap":80},[85,86,89,93,97,100,104],"span",{"class":87,"line":88},"line",1,[85,90,92],{"class":91},"spKkM","const",[85,94,96],{"class":95},"sTU5a"," planNoTextRule",[85,98,99],{"class":91}," =",[85,101,103],{"class":102},"skb7c"," \"画面中禁止出现任何文字、数字、参数标注、标语或标签贴纸；\"",[85,105,106],{"class":91}," +\n",[85,108,110,113],{"class":87,"line":109},2,[85,111,112],{"class":102},"    \"prompt 只描述商品、场景、氛围与构图，不得要求画面书写、展示或标注任何文字\"",[85,114,106],{"class":91},[85,116,118],{"class":87,"line":117},3,[85,119,120],{"class":102},"    \"（商品自身印刷的文字除外，那部分会被逐字保留）。\"\n",[68,122,123,126],{"start":109},[71,124,125],{},"渲染品质段再显式禁一次。注意第一版措辞是\"无文字水印\"，模型可能理解为\"不要水印\"然后照画文字——禁令必须精确。现在的写法：\"画面中不得出现任何文字、数字、标注、标签贴纸（商品本身印刷的文字除外）\"。",[71,127,128],{},"真实文字由本机 Go 排版引擎用系统 CJK 字体后期叠加。",[13,130,131,134],{},[36,132,133],{},"反直觉的点","：加了这个\"AI 画面零文字\"约束后，出图质量的方差明显变小了。文字是生成图上最容易露怯的元素，拿掉它，整体观感反而更\"贵\"。",[13,136,137],{},"下面是修复后的实际效果——AI 只负责画面（干净得一个字都没有），参数表和所有文字都是本机排版引擎用真实字体叠上去的：",[13,139,140],{},[141,142],"img",{"alt":143,"src":144},"详情切片：参数表与文案由排版引擎叠加，画面区无任何 AI 生成文字","https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai-part3-pitfalls\u002Fslice-02.jpg?imageMogr2\u002Fformat\u002Fwebp\u002Fquality\u002F88",[13,146,147],{},[148,149,150],"em",{},"详情切片：参数表与文案由排版引擎叠加，画面区无任何 AI 生成文字。",[45,152,154],{"id":153},"坑-2同参数重试是烧钱行为重试必须带诊断","坑 2：同参数重试是烧钱行为，重试必须带诊断",[13,156,157,159],{},[36,158,53],{},"：自检环节发现图不合格就重试一次，听起来很合理。但第一版重试是原样重跑，结果第二张和第一张犯一模一样的错——商品左上角的提手照样变形。",[13,161,162,164],{},[36,163,59],{},"：同样的 prompt + 同样的模型，采样温度再高也只是换种方式错。",[13,166,167,169],{},[36,168,65],{},"：自检输出结构化诊断标签（product_drift \u002F structure_deform \u002F light_off \u002F text_error \u002F scene_mismatch），重渲染时把上一轮的失败原因翻译成人话回灌进 prompt：",[75,171,173],{"className":77,"code":172,"language":79,"meta":80,"style":80},"\u002F\u002F 同参数重跑没有意义：重试必须带上上一轮的自检诊断\nprompt := basePrompt\nif lastCheck != nil {\n    prompt += \"\\n注意：上一轮生成存在问题：\" + checkTagsSummary(lastCheck) +\n        \"。重新生成时务必修正，其余要求不变。\"\n}\n",[82,174,175,181,193,210,241,247],{"__ignoreMap":80},[85,176,177],{"class":87,"line":88},[85,178,180],{"class":179},"si27w","\u002F\u002F 同参数重跑没有意义：重试必须带上上一轮的自检诊断\n",[85,182,183,187,190],{"class":87,"line":109},[85,184,186],{"class":185},"shWlK","prompt ",[85,188,189],{"class":91},":=",[85,191,192],{"class":185}," basePrompt\n",[85,194,195,198,201,204,207],{"class":87,"line":117},[85,196,197],{"class":91},"if",[85,199,200],{"class":185}," lastCheck ",[85,202,203],{"class":91},"!=",[85,205,206],{"class":95}," nil",[85,208,209],{"class":185}," {\n",[85,211,213,216,219,222,225,228,231,235,238],{"class":87,"line":212},4,[85,214,215],{"class":185},"    prompt ",[85,217,218],{"class":91},"+=",[85,220,221],{"class":102}," \"",[85,223,224],{"class":95},"\\n",[85,226,227],{"class":102},"注意：上一轮生成存在问题：\"",[85,229,230],{"class":91}," +",[85,232,234],{"class":233},"sqoU-"," checkTagsSummary",[85,236,237],{"class":185},"(lastCheck) ",[85,239,240],{"class":91},"+\n",[85,242,244],{"class":87,"line":243},5,[85,245,246],{"class":102},"        \"。重新生成时务必修正，其余要求不变。\"\n",[85,248,250],{"class":87,"line":249},6,[85,251,252],{"class":185},"}\n",[13,254,255,256,259],{},"\"商品与原图不一致、光影违和\"这种具体指控，模型是听进去的。同类思想也用在了首屏双候选上：两个候选必须是",[36,257,258],{},"不同构图","（规划构图 vs 正面平视），不同参数的候选才叫候选，同参数抽两次卡叫赌博。",[45,261,263],{"id":262},"坑-3429-限流把整条流水线打瘫","坑 3：429 限流把整条流水线打瘫",[13,265,266,268],{},[36,267,53],{},"：新注册的图像生成账号 RPM 低到感人（glm-image 初期几乎只能串行），一晚上批量测试，任务大面积失败，报错全是\"速率限制\"。",[13,270,271,273],{},[36,272,59],{},"：Agent 流水线各环节的调用频率天然不均，生图一步连发 N 张，直接撞墙；撞墙后代码里的错误判断又识别不出这是\"可以等的错\"还是\"该死的错\"。",[13,275,276,278],{},[36,277,65],{},"：错误特征识别 + 指数退避 + 可取消的等待：",[75,280,282],{"className":77,"code":281,"language":79,"meta":80,"style":80},"var rateLimitBackoff = []time.Duration{10 * time.Second, 20 * time.Second,\n    40 * time.Second, 60 * time.Second}\n\nfunc isRateLimitError(err error) bool {\n    msg := err.Error()\n    return strings.Contains(msg, \"速率限制\") || strings.Contains(msg, \"rate limit\") ||\n        strings.Contains(msg, \"429\") || strings.Contains(msg, \"RequestLimitExceeded\") ||\n        strings.Contains(msg, \"Throttling\")\n}\n",[82,283,284,327,344,350,376,392,428,458,473],{"__ignoreMap":80},[85,285,286,289,292,295,298,301,304,307,310,313,316,319,322,324],{"class":87,"line":88},[85,287,288],{"class":91},"var",[85,290,291],{"class":185}," rateLimitBackoff ",[85,293,294],{"class":91},"=",[85,296,297],{"class":185}," []",[85,299,300],{"class":233},"time",[85,302,303],{"class":185},".",[85,305,306],{"class":233},"Duration",[85,308,309],{"class":185},"{",[85,311,312],{"class":95},"10",[85,314,315],{"class":91}," *",[85,317,318],{"class":185}," time.Second, ",[85,320,321],{"class":95},"20",[85,323,315],{"class":91},[85,325,326],{"class":185}," time.Second,\n",[85,328,329,332,334,336,339,341],{"class":87,"line":109},[85,330,331],{"class":95},"    40",[85,333,315],{"class":91},[85,335,318],{"class":185},[85,337,338],{"class":95},"60",[85,340,315],{"class":91},[85,342,343],{"class":185}," time.Second}\n",[85,345,346],{"class":87,"line":117},[85,347,349],{"emptyLinePlaceholder":348},true,"\n",[85,351,352,355,358,361,365,368,371,374],{"class":87,"line":212},[85,353,354],{"class":91},"func",[85,356,357],{"class":233}," isRateLimitError",[85,359,360],{"class":185},"(",[85,362,364],{"class":363},"sdxZB","err",[85,366,367],{"class":91}," error",[85,369,370],{"class":185},") ",[85,372,373],{"class":91},"bool",[85,375,209],{"class":185},[85,377,378,381,383,386,389],{"class":87,"line":243},[85,379,380],{"class":185},"    msg ",[85,382,189],{"class":91},[85,384,385],{"class":185}," err.",[85,387,388],{"class":233},"Error",[85,390,391],{"class":185},"()\n",[85,393,394,397,400,403,406,409,411,414,416,418,420,423,425],{"class":87,"line":249},[85,395,396],{"class":91},"    return",[85,398,399],{"class":185}," strings.",[85,401,402],{"class":233},"Contains",[85,404,405],{"class":185},"(msg, ",[85,407,408],{"class":102},"\"速率限制\"",[85,410,370],{"class":185},[85,412,413],{"class":91},"||",[85,415,399],{"class":185},[85,417,402],{"class":233},[85,419,405],{"class":185},[85,421,422],{"class":102},"\"rate limit\"",[85,424,370],{"class":185},[85,426,427],{"class":91},"||\n",[85,429,431,434,436,438,441,443,445,447,449,451,454,456],{"class":87,"line":430},7,[85,432,433],{"class":185},"        strings.",[85,435,402],{"class":233},[85,437,405],{"class":185},[85,439,440],{"class":102},"\"429\"",[85,442,370],{"class":185},[85,444,413],{"class":91},[85,446,399],{"class":185},[85,448,402],{"class":233},[85,450,405],{"class":185},[85,452,453],{"class":102},"\"RequestLimitExceeded\"",[85,455,370],{"class":185},[85,457,427],{"class":91},[85,459,461,463,465,467,470],{"class":87,"line":460},8,[85,462,433],{"class":185},[85,464,402],{"class":233},[85,466,405],{"class":185},[85,468,469],{"class":102},"\"Throttling\"",[85,471,472],{"class":185},")\n",[85,474,476],{"class":87,"line":475},9,[85,477,252],{"class":185},[13,479,480,481,484,485,488,489,492],{},"三个细节：退避等待必须用 ",[82,482,483],{},"select"," 同时监听 ",[82,486,487],{},"ctx.Done()","，否则用户点了取消还得干等 60 秒；退避状态要推进度事件（\"触发限流，20 秒后重试（2\u002F4）\"），用户看到会理解，看不到会以为程序死了；默认渲染并发直接锁 1，等账号配额养起来再调。另外各家的限流错误文案五花八门，",[82,490,491],{},"isRateLimitError"," 这种\"错误特征识别\"函数要留好持续补词的空间。",[45,494,496],{"id":495},"坑-4模型评委爱放水服务端必须设硬门禁","坑 4：模型评委爱放水，服务端必须设硬门禁",[13,498,499,501],{},[36,500,53],{},"：自检提示词让视觉模型当质检员判 pass\u002Ffail，模型非常\"自信\"，肉眼可见商品变形了，它照样给 pass:true。",[13,503,504,506],{},[36,505,59],{},"：多模态模型当判官有系统性的乐观偏置，你问它\"这图合格吗\"，它倾向于说合格。",[13,508,509,511,512,515],{},[36,510,65],{},"：不信任模型的结论，只信任模型的",[36,513,514],{},"评分素材","。模型输出保持度分数和诊断标签后，服务端再做一道确定性门限：",[75,517,519],{"className":77,"code":518,"language":79,"meta":80,"style":80},"\u002F\u002F enforceCheckGate：模型判 pass 之外再做一道硬校验\nconst checkFidelityGate = 0.95\n\nfunc enforceCheckGate(r CheckResult) CheckResult {\n    for _, tg := range r.Tags {\n        switch tg {\n        case TagProductDrift, TagStructureDeform, TagTextError:\n            r.Pass = false   \u002F\u002F 硬伤 tag 一票否决\n        }\n    }\n    if r.Fidelity > 0 && r.Fidelity \u003C checkFidelityGate {\n        r.Pass = false\n    }\n    return r\n}\n",[82,520,521,526,538,542,564,580,588,596,609,614,620,646,657,662,670],{"__ignoreMap":80},[85,522,523],{"class":87,"line":88},[85,524,525],{"class":179},"\u002F\u002F enforceCheckGate：模型判 pass 之外再做一道硬校验\n",[85,527,528,530,533,535],{"class":87,"line":109},[85,529,92],{"class":91},[85,531,532],{"class":95}," checkFidelityGate",[85,534,99],{"class":91},[85,536,537],{"class":95}," 0.95\n",[85,539,540],{"class":87,"line":117},[85,541,349],{"emptyLinePlaceholder":348},[85,543,544,546,549,551,554,557,559,562],{"class":87,"line":212},[85,545,354],{"class":91},[85,547,548],{"class":233}," enforceCheckGate",[85,550,360],{"class":185},[85,552,553],{"class":363},"r",[85,555,556],{"class":233}," CheckResult",[85,558,370],{"class":185},[85,560,561],{"class":233},"CheckResult",[85,563,209],{"class":185},[85,565,566,569,572,574,577],{"class":87,"line":243},[85,567,568],{"class":91},"    for",[85,570,571],{"class":185}," _, tg ",[85,573,189],{"class":91},[85,575,576],{"class":91}," range",[85,578,579],{"class":185}," r.Tags {\n",[85,581,582,585],{"class":87,"line":249},[85,583,584],{"class":91},"        switch",[85,586,587],{"class":185}," tg {\n",[85,589,590,593],{"class":87,"line":430},[85,591,592],{"class":91},"        case",[85,594,595],{"class":185}," TagProductDrift, TagStructureDeform, TagTextError:\n",[85,597,598,601,603,606],{"class":87,"line":460},[85,599,600],{"class":185},"            r.Pass ",[85,602,294],{"class":91},[85,604,605],{"class":95}," false",[85,607,608],{"class":179},"   \u002F\u002F 硬伤 tag 一票否决\n",[85,610,611],{"class":87,"line":475},[85,612,613],{"class":185},"        }\n",[85,615,617],{"class":87,"line":616},10,[85,618,619],{"class":185},"    }\n",[85,621,623,626,629,632,635,638,640,643],{"class":87,"line":622},11,[85,624,625],{"class":91},"    if",[85,627,628],{"class":185}," r.Fidelity ",[85,630,631],{"class":91},">",[85,633,634],{"class":95}," 0",[85,636,637],{"class":91}," &&",[85,639,628],{"class":185},[85,641,642],{"class":91},"\u003C",[85,644,645],{"class":185}," checkFidelityGate {\n",[85,647,649,652,654],{"class":87,"line":648},12,[85,650,651],{"class":185},"        r.Pass ",[85,653,294],{"class":91},[85,655,656],{"class":95}," false\n",[85,658,660],{"class":87,"line":659},13,[85,661,619],{"class":185},[85,663,665,667],{"class":87,"line":664},14,[85,666,396],{"class":91},[85,668,669],{"class":185}," r\n",[85,671,673],{"class":87,"line":672},15,[85,674,252],{"class":185},[13,676,677],{},"判分标准和提示词里写死的规则（\"fidelity\u003C0.95 或出现硬伤 tag 时 pass=false\"）两边对齐，模型就算口头放水，数字和标签也拦得住。这个\"LLM 打分 + 确定性门限\"的三明治结构，适用于一切用 LLM 做评估的场景。",[45,679,681],{"id":680},"坑-5批量任务连环烧钱直到我装了断路器","坑 5：批量任务连环烧钱，直到我装了\"断路器\"",[13,683,684,686],{},[36,685,53],{},"：内测同事导入了 60 个 SKU 的批量任务，密钥过期了。第一个任务鉴权失败，第二个、第三个……六十个任务排着队把能调的接口挨个调挂，钱和情绪双双受损。",[13,688,689,691,692,695],{},[36,690,59],{},"：任务队列对\"系统性失败\"和\"个别失败\"没有区分能力。个别任务失败是常态，但",[36,693,694],{},"同一批次连续失败","几乎必然意味着环境问题（密钥失效、余额耗尽、断网）——这时候继续跑就是纯烧钱。",[13,697,698,700],{},[36,699,65],{},"：调度器里加了一个断路器，规则简单粗暴——同批次连续 3 条 failed，整个批次自动暂停：",[75,702,704],{"className":77,"code":703,"language":79,"meta":80,"style":80},"const batchConsecFailPause = 3\n\nswitch task.Status {\ncase \"done\":\n    s.batchFail[task.BatchID] = 0        \u002F\u002F 成功一次，计数清零\ncase \"failed\":\n    s.batchFail[task.BatchID]++\n    if s.batchFail[task.BatchID] >= batchConsecFailPause {\n        s.db.Model(&model.GenerationBatch{}).Where(\"id = ?\", task.BatchID).\n            Update(\"status\", \"paused\")   \u002F\u002F 停止该批出队\n    }\n}   \u002F\u002F canceled 不计入连续失败\n",[82,705,706,718,722,730,741,753,762,770,783,818,840,844],{"__ignoreMap":80},[85,707,708,710,713,715],{"class":87,"line":88},[85,709,92],{"class":91},[85,711,712],{"class":95}," batchConsecFailPause",[85,714,99],{"class":91},[85,716,717],{"class":95}," 3\n",[85,719,720],{"class":87,"line":109},[85,721,349],{"emptyLinePlaceholder":348},[85,723,724,727],{"class":87,"line":117},[85,725,726],{"class":91},"switch",[85,728,729],{"class":185}," task.Status {\n",[85,731,732,735,738],{"class":87,"line":212},[85,733,734],{"class":91},"case",[85,736,737],{"class":102}," \"done\"",[85,739,740],{"class":185},":\n",[85,742,743,746,748,750],{"class":87,"line":243},[85,744,745],{"class":185},"    s.batchFail[task.BatchID] ",[85,747,294],{"class":91},[85,749,634],{"class":95},[85,751,752],{"class":179},"        \u002F\u002F 成功一次，计数清零\n",[85,754,755,757,760],{"class":87,"line":249},[85,756,734],{"class":91},[85,758,759],{"class":102}," \"failed\"",[85,761,740],{"class":185},[85,763,764,767],{"class":87,"line":430},[85,765,766],{"class":185},"    s.batchFail[task.BatchID]",[85,768,769],{"class":91},"++\n",[85,771,772,774,777,780],{"class":87,"line":460},[85,773,625],{"class":91},[85,775,776],{"class":185}," s.batchFail[task.BatchID] ",[85,778,779],{"class":91},">=",[85,781,782],{"class":185}," batchConsecFailPause {\n",[85,784,785,788,791,793,796,799,801,804,807,810,812,815],{"class":87,"line":475},[85,786,787],{"class":185},"        s.db.",[85,789,790],{"class":233},"Model",[85,792,360],{"class":185},[85,794,795],{"class":91},"&",[85,797,798],{"class":233},"model",[85,800,303],{"class":185},[85,802,803],{"class":233},"GenerationBatch",[85,805,806],{"class":185},"{}).",[85,808,809],{"class":233},"Where",[85,811,360],{"class":185},[85,813,814],{"class":102},"\"id = ?\"",[85,816,817],{"class":185},", task.BatchID).\n",[85,819,820,823,825,828,831,834,837],{"class":87,"line":616},[85,821,822],{"class":233},"            Update",[85,824,360],{"class":185},[85,826,827],{"class":102},"\"status\"",[85,829,830],{"class":185},", ",[85,832,833],{"class":102},"\"paused\"",[85,835,836],{"class":185},")   ",[85,838,839],{"class":179},"\u002F\u002F 停止该批出队\n",[85,841,842],{"class":87,"line":622},[85,843,619],{"class":185},[85,845,846,849],{"class":87,"line":648},[85,847,848],{"class":185},"}   ",[85,850,851],{"class":179},"\u002F\u002F canceled 不计入连续失败\n",[13,853,854],{},"三个口径设计：成功一条就清零（说明只是偶发失败）；用户取消不计入（那是主动行为不是故障）；暂停不杀进程，已付费跑完的任务照常结算，只拦后续出队。UI 上给\"疑似密钥\u002F余额问题\"的提示，用户检查后点继续，队列重新入队恢复。任何会自动消耗真金白银的批处理系统，都应该在第一天就装这个断路器。",[45,856,858],{"id":857},"坑-6状态机的竞态白烧一张图-01-元事小脸被打得疼","坑 6：状态机的竞态，白烧一张图 0.1 元（事小，脸被打得疼）",[13,860,861,863],{},[36,862,53],{},"：偶发一种诡异日志：任务状态是 canceled，但产物目录里躺着刚生成的图。用户在\"排队中\"点了取消，任务还是跑了。",[13,865,866,868,869,872],{},[36,867,59],{},"：调度器出队后 ",[82,870,871],{},"go runTask(id)","，而 runTask 开头要查库、读配置，有好几毫秒窗口；用户恰好在这个窗口点取消——取消逻辑看任务还没跑起来，直接改库置 canceled，dispatcher 也根本不知道要拦这个 goroutine。两边都没错，合起来错了。",[13,874,875,877,878,881],{},[36,876,65],{},"：把\"开始执行\"做成一次",[36,879,880],{},"条件更新","，用数据库解决竞态：",[75,883,885],{"className":77,"code":884,"language":79,"meta":80,"style":80},"\u002F\u002F 条件更新（仅 pending 才置 running）：保证「排队中取消」必然生效\nres := s.db.Model(&model.GenerationTask{}).\n    Where(\"id = ? AND status = ?\", taskID, \"pending\").\n    Updates(map[string]interface{}{\"status\": \"running\", \"started_at\": nowMillis()})\nif res.RowsAffected == 0 {\n    return   \u002F\u002F 已被取消或已到终态：不执行、不产生任何成本\n}\n",[82,886,887,892,918,937,983,997,1004],{"__ignoreMap":80},[85,888,889],{"class":87,"line":88},[85,890,891],{"class":179},"\u002F\u002F 条件更新（仅 pending 才置 running）：保证「排队中取消」必然生效\n",[85,893,894,897,899,902,904,906,908,910,912,915],{"class":87,"line":109},[85,895,896],{"class":185},"res ",[85,898,189],{"class":91},[85,900,901],{"class":185}," s.db.",[85,903,790],{"class":233},[85,905,360],{"class":185},[85,907,795],{"class":91},[85,909,798],{"class":233},[85,911,303],{"class":185},[85,913,914],{"class":233},"GenerationTask",[85,916,917],{"class":185},"{}).\n",[85,919,920,923,925,928,931,934],{"class":87,"line":117},[85,921,922],{"class":233},"    Where",[85,924,360],{"class":185},[85,926,927],{"class":102},"\"id = ? AND status = ?\"",[85,929,930],{"class":185},", taskID, ",[85,932,933],{"class":102},"\"pending\"",[85,935,936],{"class":185},").\n",[85,938,939,942,944,947,950,953,956,959,962,964,967,970,972,975,977,980],{"class":87,"line":212},[85,940,941],{"class":233},"    Updates",[85,943,360],{"class":185},[85,945,946],{"class":91},"map",[85,948,949],{"class":185},"[",[85,951,952],{"class":91},"string",[85,954,955],{"class":185},"]",[85,957,958],{"class":91},"interface",[85,960,961],{"class":185},"{}{",[85,963,827],{"class":102},[85,965,966],{"class":185},": ",[85,968,969],{"class":102},"\"running\"",[85,971,830],{"class":185},[85,973,974],{"class":102},"\"started_at\"",[85,976,966],{"class":185},[85,978,979],{"class":233},"nowMillis",[85,981,982],{"class":185},"()})\n",[85,984,985,987,990,993,995],{"class":87,"line":243},[85,986,197],{"class":91},[85,988,989],{"class":185}," res.RowsAffected ",[85,991,992],{"class":91},"==",[85,994,634],{"class":95},[85,996,209],{"class":185},[85,998,999,1001],{"class":87,"line":249},[85,1000,396],{"class":91},[85,1002,1003],{"class":179},"   \u002F\u002F 已被取消或已到终态：不执行、不产生任何成本\n",[85,1005,1006],{"class":87,"line":430},[85,1007,252],{"class":185},[13,1009,1010,1011,1014,1015,1018,1019,1022],{},"谁先改库谁赢，输家看到 ",[82,1012,1013],{},"RowsAffected == 0"," 安静退出。取消排队中任务同样用条件更新（",[82,1016,1017],{},"WHERE status='pending'"," 才置 canceled），双向都安全。教训：",[36,1020,1021],{},"进程间\u002F协程间的状态仲裁，与其靠内存锁和时序推理，不如把数据库当线性化寄存器用","——桌面应用自带 SQLite，这资源不用白不用。",[45,1024,1026],{"id":1025},"坑-7本地文件当服务入参被目录穿越和文件消失教育了两次","坑 7：本地文件当服务入参，被目录穿越和文件消失教育了两次",[13,1028,1029,1032,1033,1036,1037,1040,1041,1044],{},[36,1030,1031],{},"现象 A","：前端要预览数据目录里各处的生成图，我挂了个 ",[82,1034,1035],{},"\u002Flocal\u002F"," 静态服务，安全同学（我自己）一眼看到没做路径清洗——",[82,1038,1039],{},"\u002Flocal\u002F..\u002F..\u002Fetc\u002Fpasswd"," 全家桶。\n",[36,1042,1043],{},"现象 B","：用户创建任务后把商品原图挪走了\u002F重命名了，重试时报\"商品图不存在\"。任务还活着，素材先死了。",[13,1046,1047,1049,1050,1052,1053,1056,1057,1060,1061,1064],{},[36,1048,65],{},"：A：",[82,1051,1035],{}," handler 统一 ",[82,1054,1055],{},"filepath.Clean"," 后强制校验 ",[82,1058,1059],{},"strings.HasPrefix(full, dataDir+separator)","，不在目录内一律 404。B：",[36,1062,1063],{},"创建任务时就把商品图复制进数据目录","（内容 sha1 去重），任务永远引用入库副本；导入失败才降级用原路径（只影响缩略图，不阻塞任务）。",[13,1066,1067],{},"顺带一提第三方服务的坑：阿里云商品分割接口只吃公网 URL，本地文件得先转存它的临时 OSS，返回的结果图 URL 也是带过期签名的临时地址，必须立即下载转存本机。对接任何云 API，先问自己三个问题：入参收不收本地文件？返回的链接活多久？endpoint 是哪个区域的（imageseg 只有上海域名，抄文档写北京直接查无此域名）。",[45,1069,1071],{"id":1070},"坑-8一个人写代码架构靠自觉是不可能的","坑 8：一个人写代码，架构靠自觉是不可能的",[13,1073,1074,1076,1077,1080,1081,1084],{},[36,1075,53],{},"：项目快速迭代两周后，我发现 ",[82,1078,1079],{},"app.go","（Wails 绑定层）里开始出现直接调 gorm 的函数，",[82,1082,1083],{},"internal\u002Fagent"," 包里开始有人（还是我）import database。分层在 PPT 上，不在代码里。",[13,1086,1087,1089],{},[36,1088,59],{},"：单人项目没人 review，\"就这一次破例\"是常态。写进文档的规范保质期约等于零。",[13,1091,1092,1094,1095,1098,1099,1102,1103,1106],{},[36,1093,65],{},"：把架构规则编译成",[36,1096,1097],{},"会在 CI 里变红的测试","。项目里的 ",[82,1100,1101],{},"internal\u002Fguard\u002F"," 包用 Go AST 解析源码做断言：绑定层不得 import gorm、model 层必须是叶子依赖、新依赖必须登记进 ",[82,1104,1105],{},"deps.yaml"," 且双向校验（清单里有的必须真实存在，存在的必须登记）。",[13,1108,1109],{},"护栏本身还有一条元规则，是护栏工程最精髓的一句话：",[10,1111,1112],{},[13,1113,1114],{},[36,1115,1116],{},"任何用正则\u002FAST 解析源码做断言的护栏，解析到 0 个结果必须报错，而非当作\"通过\"静默放行。",[13,1118,1119,1120,1122],{},"否则你把 ",[82,1121,1079],{}," 改名了，\"app.go 不许碰数据库\"这条护栏就会在零解析的状态下天天报绿——护栏瞎了，比没有护栏更危险，因为它提供虚假的安全感。所以每条护栏找不到解析目标时的报错文案都是统一的：\"写法可能已变更，请同步更新护栏解析规则\"。",[45,1124,1126],{"id":1125},"坑-9批量导入的垃圾进垃圾出被一个-csv-编码教做人","坑 9：批量导入的\"垃圾进垃圾出\"，被一个 CSV 编码教做人",[13,1128,1129,1131],{},[36,1130,53],{},"：内测用户兴冲冲导入 60 个 SKU，任务列表里商品名全是\"鍟嗗搧\"这类火星文；还有人导入后一半行离奇消失，来问是不是 bug。",[13,1133,1134,1136],{},[36,1135,59],{},"：国内 Excel\u002FWPS 另存 CSV 默认 GBK 编码，按 UTF-8 解析自然全是乱码；\"行消失\"则是图片路径列缺失或相对路径基准搞错，数据被静默跳过了。",[13,1138,1139,1141,1142,1145,1146,1149],{},[36,1140,65],{},"：与其做编码嗅探（GBK\u002FUTF-8 误判率不低，判错就是整批数据污染），不如把规则收紧并显性化——",[36,1143,1144],{},"只收 UTF-8","，检测到非法编码直接报错提示\"另存为 CSV UTF-8 后重试\"；相对路径明确以 CSV 所在目录为基准；最重要的一个设计是",[36,1147,1148],{},"先预览再落库","：弹确认层展示有效行数、被跳过的行及原因、整批预估金额，用户确认后才建任务，且单批上限 200 行。批量入口的原则：宁可让用户多点一次确认，也不给他一个\"静默吃错数据\"的黑盒。",[45,1151,1153],{"id":1152},"尾声一些数字和一句总结","尾声：一些数字和一句总结",[1155,1156,1157,1160,1163],"ul",{},[71,1158,1159],{},"单套 5 图详情页实测约 8 分半，成本折算几毛钱到一块多（token + 出图张数逐任务落库，界面明示估算值）；",[71,1161,1162],{},"以上 9 个坑里，绝大多数发生在有真实用户之前的内测期，值钱；",[71,1164,1165,1166,1169],{},"最大的一条心得：",[36,1167,1168],{},"和 LLM 协作的全部工程智慧，浓缩成一句话就是\"别信它的输出，接管它的手脚\"","——输出必须过 schema 解析和确定性门限，手脚必须限定在结构化 JSON 和白名单事实源里，它负责聪明，你的代码负责靠谱。",[13,1171,1172],{},"最后放一张\"接管手脚\"的集大成产物——白底主图。它压根不走生成模型：抠图产物直接铺白，本机合成，零成本零变形：",[13,1174,1175],{},[141,1176],{"alt":1177,"src":1178},"白底主图：抠图产物本地铺白合成，不走生成模型","https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai-part3-pitfalls\u002Fmain-white.jpg",[13,1180,1181],{},[148,1182,1183],{},"白底主图：抠图产物本地铺白合成，不走生成模型。",[13,1185,1186,1187,1190,1191,1194],{},"系列前两篇：",[17,1188,1189],{"href":19},"第 1 篇《我用 Go 搭了一条 AI Agent 流水线》","、",[17,1192,1193],{"href":24},"第 2 篇《Wails v2 实战：用 Go + Vue3 做一个真正能用的 AI 桌面应用》","。欢迎聊聊你踩过的 AI 产品的坑。",[1196,1197,1198],"style",{},"html pre.shiki code .spKkM, html code.shiki .spKkM{--shiki-default:#F97583;--shiki-light:#D73A49}html pre.shiki code .sTU5a, html code.shiki .sTU5a{--shiki-default:#79B8FF;--shiki-light:#005CC5}html pre.shiki code .skb7c, html code.shiki .skb7c{--shiki-default:#9ECBFF;--shiki-light:#032F62}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 .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .si27w, html code.shiki .si27w{--shiki-default:#6A737D;--shiki-light:#6A737D}html pre.shiki code .shWlK, html code.shiki .shWlK{--shiki-default:#E1E4E8;--shiki-light:#24292E}html pre.shiki code .sqoU-, html code.shiki .sqoU-{--shiki-default:#B392F0;--shiki-light:#6F42C1}html pre.shiki code .sdxZB, html code.shiki .sdxZB{--shiki-default:#FFAB70;--shiki-light:#E36209}",{"title":80,"searchDepth":109,"depth":109,"links":1200},[1201,1202,1203,1204,1205,1206,1207,1208,1209,1210],{"id":47,"depth":109,"text":48},{"id":153,"depth":109,"text":154},{"id":262,"depth":109,"text":263},{"id":495,"depth":109,"text":496},{"id":680,"depth":109,"text":681},{"id":857,"depth":109,"text":858},{"id":1025,"depth":109,"text":1026},{"id":1070,"depth":109,"text":1071},{"id":1125,"depth":109,"text":1126},{"id":1152,"depth":109,"text":1153},"https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai-part3-pitfalls\u002Fsop-process.png?imageMogr2\u002Fthumbnail\u002F1440x\u002Fformat\u002Fwebp\u002Fquality\u002F88","2026-10-14","md",null,{},"\u002Fposts\u002Fecom-detail-ai-part3-pitfalls",{"title":5,"description":80},"ecom-detail-ai-part3-pitfalls","posts\u002Fecom-detail-ai-part3-pitfalls","AI 在图里写甲骨文、同参数重试纯烧钱、429 把流水线打瘫、模型评委爱放水、批量任务连环烧钱直到装了断路器……9 个坑，每一个都对应一段现在还在跑的代码。",[1222,1223],"AI 应用","踩坑复盘","azoQznqsDbfo1E5IVG1l15WZQLSbfBTaLOFHhoSBIj0",[1226,1230,1234,1238,1239,1242,1245],{"title":1227,"slug":1228,"date":1229},"不引 Ghostscript：我用纯 Go 给 PDF 做了一场「压缩手术」","pure-go-pdf-compression","2026-10-24",{"title":1231,"slug":1232,"date":1233},"Wails v3 升级实战：桌面提醒客户端从 v2 升到 v3，踩了 10 个不报错的坑","wails-v3-upgrade","2026-10-23",{"title":1235,"slug":1236,"date":1237},"用 RAG + FAISS 构建古诗词 AI 取名系统：从向量检索到六层质量过滤","chinese-poetry-rag","2026-10-15",{"title":5,"slug":1218,"date":1212},{"title":1240,"slug":1241,"date":1212},"Wails v2 实战：用 Go + Vue3 做一个真正能用的 AI 桌面应用","ecom-detail-ai-part2-wails",{"title":1243,"slug":1244,"date":1212},"我用 Go 搭了一条 AI Agent 流水线：从 1 张商品图到一整套淘宝详情页","ecom-detail-ai-part1-architecture",{"title":1246,"slug":1247,"date":1248},"AI 时代，如何把一个需求真正落地成应用？——一条\"文档驱动 + 实测修订\"的实战路径","ai-requirement-landing","2026-10-13",{"id":1250,"title":1251,"architecture":1252,"body":1253,"category":1500,"client":1501,"cover":1502,"date":1503,"demo":1214,"demoMobile":1214,"description":80,"extension":1213,"featured":348,"features":1504,"github":1214,"manual":1521,"meta":1574,"modules":1214,"navigation":348,"order":1575,"path":32,"seo":1576,"slug":33,"stem":1577,"summary":1578,"techStack":1579,"visible":348,"year":1588,"__hash__":1589},"projects\u002Fprojects\u002Fecom-detail-ai.md","电商详情页 AI 生成客户端","https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai\u002Fecom-detail-architecture.svg",{"type":7,"value":1254,"toc":1492},[1255,1258,1261,1264,1267,1270,1273,1276,1332,1335,1411,1422,1431,1439,1442,1482,1485],[45,1256,1257],{"id":1257},"缘起",[13,1259,1260],{},"帮人上过一款连衣裙的详情页。找外包要等好几天，自己上手就得同时会 PS、会布光、还会写文案，我三样都不精。第一版做出来光影像贴上去的，商品边缘还走了形。",[13,1262,1263],{},"当时想要的东西其实很朴素：扔一张挂拍图进去，别问我品类、别问我风格，直接出一套能用的。",[45,1265,1266],{"id":1266},"背景",[13,1268,1269],{},"现成的 AI 工具大多停在模板拼贴：光影有贴图感、商品容易变形，号称一键生成，实际仍要用户逐项选品类、选风格、选模板，等于把设计判断又还给了不会设计的人。而详情页本身是一整套东西——主图、场景图、白底图、详情长图、切片、文案，每一样尺寸和规则都不一样，分开做就要分开对齐。要压缩的是这一整段决策：上传一张商品图，不选品类、不选风格，直接得到一整套带真实感光影渲染的详情页。",[45,1271,1272],{"id":1272},"方案",[13,1274,1275],{},"一个装在电脑上、双击就能用的客户端，核心是一条会自己拿主意的生成流水线：",[1155,1277,1278,1284,1290,1296,1302,1308,1314,1320,1326],{},[71,1279,1280,1283],{},[36,1281,1282],{},"十步全自动，中途不用管","：抠图 → 读图理解 → 自主规划（出几张图、每张什么场景与光影、页面分几块）→ 逐张渲染 → 画面描述 → 文案 → 本机排版合成 → 标准卡片 → 自检 → 完成。每一步的状态和中间产物都会落盘，界面上按时间线看得见进度，任务可以取消、关掉再开还能接着跑",[71,1285,1286,1289],{},[36,1287,1288],{},"真实感出图","：基于原图做编辑式生成，而不是把商品抠出来贴进模板；多张参考图输入保住商品一致性。出图引擎可在 Seedream \u002F GLM-Image 之间切换，主选是拿真实商品盲测定的",[71,1291,1292,1295],{},[36,1293,1294],{},"关键文字交给本机，不交给模型","：卖点、参数这类文字由本机排版引擎叠加（内置可免费商用的中文字体，不消耗模型额度，可编辑、可导出），生成模型不承担关键文字的渲染",[71,1297,1298,1301],{},[36,1299,1300],{},"自检闭环","：出完自动拿成品跟原图比——查商品有没有走形、光影是否违和、图内有没有错字；没过就带着诊断重做，还是没过就标成「待人工确认」并在结果页置顶提示",[71,1303,1304,1307],{},[36,1305,1306],{},"标准卡片产线","：主图卡 + 参数卡 × 3 规格。取色优先级为 单张卡手动改色 > 任务级模板 > 类目默认模板 > 全局默认 > 按商品图派生的配色——模板只管结构，颜色默认跟着商品走",[71,1309,1310,1313],{},[36,1311,1312],{},"商品类目可以自己管","：内置 11 组，能增、能删、能改，改完直接保存下来。类目还额外带着这个类目的参数项提示、默认卡片样式，以及敏感类目下强制关闭人像场景的硬规则（无条件压过全局的人像开关）",[71,1315,1316,1319],{},[36,1317,1318],{},"批量生成","：表格文件或整个文件夹一次导入、整批排队，共用同一套队列按设定的并发数（1-3）跑；单条失败不影响其它，同一批连续 3 条失败会自动停下并提示排查",[71,1321,1322,1325],{},[36,1323,1324],{},"密钥自带，成本透明","：智谱 \u002F DeepSeek \u002F 火山方舟 \u002F 阿里云四家各一把密钥，都是必需的；费用直接结算在用户自己的模型账号上，每个任务开跑前明示预估金额",[71,1327,1328,1331],{},[36,1329,1330],{},"导出即合规","：主图 800×800（含白底图）+ 详情 750px 切片（单张目标不超过 500KB）+ 文案 + 压缩包，导出前先预检尺寸和体积；生成来源的标记在切片、压缩全过程都不会被剥离，另在图上用角标显式标出",[45,1333,1334],{"id":1334},"技术架构",[1336,1337,1338,1351],"table",{},[1339,1340,1341],"thead",{},[1342,1343,1344,1348],"tr",{},[1345,1346,1347],"th",{},"模块",[1345,1349,1350],{},"技术选型",[1352,1353,1354,1363,1371,1379,1387,1395,1403],"tbody",{},[1342,1355,1356,1360],{},[1357,1358,1359],"td",{},"后端",[1357,1361,1362],{},"Go + GORM + SQLite（纯 Go 驱动，交叉编译无 CGO）",[1342,1364,1365,1368],{},[1357,1366,1367],{},"桌面运行时",[1357,1369,1370],{},"Wails v2（WebView2 \u002F WKWebView），无边框窗口 + 托盘 + 崩溃落盘",[1342,1372,1373,1376],{},[1357,1374,1375],{},"前端",[1357,1377,1378],{},"Vue3 + Vite + Element Plus + Pinia，暗色优先设计令牌",[1342,1380,1381,1384],{},[1357,1382,1383],{},"模型网关",[1357,1385,1386],{},"OpenAI 兼容协议统一接入（chat + 图像生成），模型位\u002F降级顺位\u002F价格表全部配置化",[1342,1388,1389,1392],{},[1357,1390,1391],{},"抠图",[1357,1393,1394],{},"阿里云商品分割 API（≈0.002 元\u002F次，直出白底图）",[1342,1396,1397,1400],{},[1357,1398,1399],{},"排版",[1357,1401,1402],{},"Go 侧 750px 位图合成 + CJK 字体渲染",[1342,1404,1405,1408],{},[1357,1406,1407],{},"工程化",[1357,1409,1410],{},"架构护栏（AST 断言）+ 依赖登记制 + golden 像素基线测试 + OpenSpec 治理",[13,1412,1413,1414,1417,1418,1421],{},"数据侧 6 张表（Product \u002F GenerationTask \u002F GenerationBatch \u002F TaskStep \u002F GeneratedAsset \u002F ExportRecord），其中 ",[82,1415,1416],{},"TaskStep"," 按",[36,1419,1420],{},"图片实例","多行存储，各自携带状态与产物引用——图级断点续跑和\"已完成部分不重复计费\"都依赖这个粒度。",[13,1423,1424,1425,1427,1428,1430],{},"工程约定编译成了会红的检查：分层越界（",[82,1426,1079],{}," 不直接 import gorm）、模型注册双向校验、新增依赖必须登记 ",[82,1429,1105],{},"、渲染层禁 import node 能力、禁硬编码色值，且护栏解析到 0 个结果会 Fatal 而非静默放行。冒烟测试定位为本地验证——三平台 CI 只做静态检查与编译，不启动图形界面。",[13,1432,1433,1434,1438],{},"本项目由 ",[17,1435,1437],{"href":1436},"\u002Fprojects\u002Fwails-bailer-starter","Wails 桌面端脚手架基座"," 实例化而来——运行时能力与管道契约直接复用，业务面全部在此长出。",[45,1440,1441],{"id":1441},"成果",[1155,1443,1444,1454,1464,1470,1476],{},[71,1445,1446,1449,1450,1453],{},[36,1447,1448],{},"从 3-7 天到 8 分半","：单套（5 图）真实密钥全流程实测 ",[36,1451,1452],{},"8 分 27 秒","，落在「≤10 分钟出整套详情页」的目标内",[71,1455,1456,1459,1460,1463],{},[36,1457,1458],{},"它要替换掉的那件事","：外包一套 300-1500 元、3-7 天交付；这里单套模型消耗理想值约 ",[36,1461,1462],{},"1 元","，算上 1-3 次单张重做的真实预期 1-2 元——费用结算在用户自己的模型账号上，中间不过手",[71,1465,1466,1469],{},[36,1467,1468],{},"出图主选是盲测翻盘定的","：Seedream 4.5 把商品结构复刻到近乎完美，GLM-Image 重绘成\"同类不同款\"——据此调换了出图位顺位",[71,1471,1472,1475],{},[36,1473,1474],{},"自检不是摆设","：成品自检比对原图查商品走形、光影违和与图内错字，未通过带诊断重做；仍不通过就置顶标记「待人工确认」，而不是悄悄交付",[71,1477,1478,1481],{},[36,1479,1480],{},"拿到手就是装完能用的安装包","：Mac 出带拖拽引导的安装包、Windows 出安装器，目标机器不需要 Go \u002F Node.js 运行时",[45,1483,1484],{"id":1484},"体验",[13,1486,1487,1488,1491],{},"内测阶段的桌面客户端，目前主要是我自己在用——",[36,1489,1490],{},"暂无公开下载与在线演示，源码也未公开","。想先看它是怎么搭起来的，可以读本页下方列出的相关技术文章——架构、工程化与踩坑各有落笔。",{"title":80,"searchDepth":109,"depth":109,"links":1493},[1494,1495,1496,1497,1498,1499],{"id":1257,"depth":109,"text":1257},{"id":1266,"depth":109,"text":1266},{"id":1272,"depth":109,"text":1272},{"id":1334,"depth":109,"text":1334},{"id":1441,"depth":109,"text":1441},{"id":1484,"depth":109,"text":1484},"ai","个人项目","https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai\u002Fecom-detail-hero.webp","2026-09-29",[1505,1509,1513,1517],{"title":1506,"description":1507,"image":1508},"1 张挂拍图 → 一整套场景渲染","左侧用户上传的挂拍原图，右侧由程序自己规划并渲染的逆光石墙与窗边场景，商品结构保持一致","https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai\u002Fecom-detail-input2out.webp",{"title":1510,"description":1511,"image":1512},"详情长图 · 750px 本机排版合成","关键文字在出图后由本机排版叠加，生成模型不承担文字渲染；按平台高度限制自动切片","https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai\u002Fecom-detail-longpage.webp",{"title":1514,"description":1515,"image":1516},"主图导出 · 场景图与白底图","阿里云商品分割直出白底图，800×800 主图 + AI 标识角标 + ZIP 打包","https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai\u002Fecom-detail-mainimgs.webp",{"title":1518,"description":1519,"image":1520},"首次启动向导 · 四把密钥各司其职","感知自检 \u002F 规划文案 \u002F 图像生成 \u002F 抠图四家服务商均为必需，内置注册指引与一键跳转控制台","https:\u002F\u002Fname.os.bennywen.xyz\u002Fbennywen.xyz\u002Fassets\u002Fecom-detail-ai\u002Fecom-detail-wizard.webp",{"specs":1522,"modules":1541,"deploy":1565,"limits":1569},[1523,1526,1529,1532,1535,1538],{"label":1524,"value":1525},"交付形态","Windows \u002F macOS 桌面客户端，装完即用，目标机器不需要另装运行环境",{"label":1527,"value":1528},"单套产出","主图 + 白底图 + 详情长图切片 + 文案，一套实测约 8 分半",{"label":1530,"value":1531},"使用成本","一套约 1-2 元，结算在你自己名下的模型账号，开跑前先给预估金额",{"label":1533,"value":1534},"风格决策","不用选品类、不用选风格——程序读图后自己定，也可在看图后手动指定",{"label":1536,"value":1537},"批量能力","表格或文件夹一次导入一整批商品排队生成，单批上限 200 条",{"label":1539,"value":1540},"隐私","商品图只发往你自己账号下的模型服务；长图排版与打包在本机完成",[1542,1545,1548,1551,1554,1557,1560,1562],{"name":1543,"desc":1544},"读图","拿到商品图先看懂它是什么——品类、材质、结构，以及有哪些值得讲的点",{"name":1546,"desc":1547},"自主规划","自己决定这套详情页要出几张图、每张配什么场景与光影、页面分几块",{"name":1549,"desc":1550},"场景渲染","按规划逐张生成，商品结构保持一致，不是把商品抠出来贴进模板",{"name":1552,"desc":1553},"文字排版","卖点与参数文字在本机排版合成，不吃模型的额度，也可以自己改",{"name":1555,"desc":1556},"成品自检","出完自动跟原图比对，发现商品走形、光影违和或图内错字就带诊断重做",{"name":1558,"desc":1559},"导出打包","一次拿到主图、白底图、详情长图切片与文案，尺寸体积先自检再打包",{"name":1318,"desc":1561},"整批商品排队跑，单条失败不影响其它；连环失败自动停下提示排查",{"name":1563,"desc":1564},"成本预估","开跑前估算这一套要花多少钱，钱花在哪一步看得见",[1566,1567,1568],"内测阶段的桌面客户端：装包后双击即用，不需要装数据库，也不需要自己的服务器","首次启动有一个配置向导，带你领四家模型服务的密钥（读图、规划、出图、抠图各一把）","生成在云端跑，不吃本机算力；长图排版与打包在本机完成",[1570,1571,1572,1573],"Mac 上关掉窗口即退出，没有常驻托盘（Windows 上可以收进托盘后台继续生成）","编辑只到「模块」这一层——增删、排序、换图可以，没有元素级的拖拽排版","商品模特图暂时没有：真人像生成涉及合规，留到下一版评估","在设置页手动换过模型服务商之后，成本估算仍按原来的价格显示，需要重过一遍向导才会同步",{},16,{"title":1251,"description":80},"projects\u002Fecom-detail-ai","面向淘宝\u002F天猫中小卖家与美工——1 张商品图进，不选品类不选风格，8 分半出一整套详情页，单套模型消耗约 1-2 元",[1580,1581,1582,1583,1584,1585,1586,1587],"Go","GORM","SQLite","Wails","Vue3","Element Plus","Vite","Pinia","2026","gD63yNSExkuy6opU0htv0KjL2bv-jAcGuYWjwHk1L7w",1792857942415]