元绘 AI · 开发手册
技术原理 · 工作流程 · 操作说明

开发手册

这一本讲为什么这么做、怎么改、怎么用。具体数值参数不在这里 —— 那些在 docs/长期规范速查.md;逐轮改了什么不在这里 —— 那些在 docs/修改记录.md。 本手册只做总纲和索引,不复制那两份文档的内容,避免同一件事在三个地方各写一遍、改一处漏两处。

纯前端 画布 + 生图
Node 零依赖 代理 /api/*
云库 口令 / 模型配置 / 云同步
本地抠图 ONNX 模型
固定底板 assets/bg-*.jpg 逐像素

手册的维护规则(自己也要守)

  1. 每次发版,在 更新记录 顶部追加一条:日期、版本号、这次改了什么、为什么。
  2. 每加一个新技能 / 新灯型,在 技能清单 加一行,并在 加一个新技能 里确认流程没变。
  3. 不抄参数。要数值就写「见 docs/长期规范速查.md 对应小节」,不要把数字搬过来 —— 搬过来就会有两份。
  4. 一条事实只写一处。同一件事在别处用「见 §某节」指过去。

1去哪儿查什么

四份文档分工明确,先看这张表就不会找错地方。

想知道去哪说明
某个数值参数是多少docs/长期规范速查.md唯一权威。影 / 尺寸 / 抠图 / 真值 / 详情方块 / 色名 / 工具坑都在里面
某一轮改了什么、为什么docs/修改记录.md逐轮流水,最全最长
影的原理与新方案docs/影-新方案(从原片提影).md影为什么必须从原片提
加新灯型时怎么标定影docs/灯型影标定卡.md加灯型只读这一段
原理 / 流程 / 怎么用本手册总纲与索引
最近几轮我这边定了什么规矩.workbuddy/memory/MEMORY.md硬规则索引 + 当日日志

2系统架构

四层,边界很硬:前端负责画和排版,Node 只做转发,云库只存配置,自建模型只做抠图。

浏览器 · 纯前端(yuanhui-canvas/) 画布编排 · 提示词组装 · 生图调用 · 本机归位合成 · 排版 · 导出 index.html + js/*.js + css/ + assets/(含固定底板 bg-1x1.jpg / bg-3x4.jpg) HTTP /api/* server.js · 零依赖代理 读 PORT · 绑 0.0.0.0 · 转发三路 /api/generate · /api/chat · /api/cutout Supabase 云库 只存配置与口令哈希 yh_sync(访问门 __gate__ / 台账 / 工作区) 上游 AI(真实花钱的那路) 生图 doubao-seedream-5-0-pro 生视频 seedance 2.0 用户自己的 Key,走 modelId / baseUrl 配置 自建抠图服务(零成本) ONNX 模型本地推理 · 长边按 2 的幂缩到 2048 串行化(并发两份必崩) 不可用时前端静默降级到规则算法
四层边界:前端不存密钥、云库不存图片、抠图不出网

为什么是「纯前端 + 薄代理」

整站唯一的服务端是 server.js,零第三方依赖,只做三件事:读 PORT 绑 0.0.0.0、把 /api/* 转发到上游、把 /api/cutout 交给本地 Python 推理。这样部署沙箱只需要一个公开端口,不需要数据库;代价是发版形态必须是 Node 服务,发成静态站 /api/* 会全挂(见 发版)。

图片的存储纪律

  • 进 DOM 的 img 一律走缩略图(长边 720);导出、合成、送模型用原图。
  • 大于 2MB 的原图存 IndexedDB,不进 localStorage。
  • 模型返回的 URL 交给 canvas 前,先 fetchAsDataURL 转成本地,否则跨源图会污染画布、toDataURL 直接抛 Tainted canvas。

3一张图的流水线

从点「生成」到拿到成品,中间经过这些站。每一站的产物和判据都能在浏览器控制台读到。

① 组装提示词 图1 原片 + 图2 底板 ② 调生图接口 /api/generate ③ URL 转本地 防跨源污染 ④ 抠图 产品 / 影分开 ⑤ 剥影 只在退路上跑 ⑥ 贴固定底板 逐像素不改动 ⑦ 本机定位 重心线 / 宽度档 ⑧ 排版 / 导出 标题 / 色名 旁路:整张交模型(第 1 张的另一种口径) ② 之后直接跳到 ⑧ —— 背景、产品、影都是模型画的,本机一步都不碰。开关见 §9 控制台诊断口(每站都有读数) __heroFitSource / __heroCutSource / __heroShadeCut / __blockNorm / __heroRect …
①②③④⑤⑥⑦⑧ 对应控制台诊断口;旁路跳过 ⑤⑥⑦

每站的判据

站判据(控制台读)不合格长什么样
③ 转本地画布不抛 Tainted canvas导出直接报错
④ 抠图__heroCutSource = ai 或 field长期 field ⇒ 抠图服务在掉链子
⑤ 剥影__heroShadeCut / __heroShadeCutRejectReject 非空 ⇒ 硬闸挡住了(这是保护,不是 bug)
⑥ 贴底板成品背景行带 vs 底板行带,逐灰阶差差 > 2 ⇒ 背景没对齐
⑦ 定位__heroRect 的 w / 中心宽度不在档位、中心偏离重心线
⑧ 排版__blockNorm(方块尺寸归一化)各块宽度差一大截

4三条像素纪律

整个项目最贵的三条规矩,违反了任何一条,出来的东西「看着差不多」但就是不能用。

① 背景 = 固定底板,逐像素不改

底板是 assets/bg-1x1.jpg(方)/ bg-3x4.jpg(长)。规范是中性灰纵向渐变 #e9eaea → #f4f3f3(跨度 11 灰阶)。实测差 1 灰阶就是 JPEG 量化的锅,别为了「对齐规范」去动底板本身。

要让模型画对渐变,唯一有效的办法是把底板当「样张」一起喂给模型;提示词里写死 RGB 没用。代价是产品会被模型重画 —— 见 §9。

② 产品默认取上传原片的像素

产品不是「生成」的,是「搬」的。只有两处例外,且各自有开关可以一键回退:吸顶灯主图第 1 张与详情 2 的方块整张交模型(见 §9)。

为什么抠图要 fail-closed:合成失败宁可报错也不退回「让模型重画一张」—— 那等于悄悄换掉客户的货。

③ 阴影必须来自原片里那道真实影,绝不许模型画

真值库里只有 49% 的宽扁吸顶灯画影,所以影的处理得按灯型自适应,别按类型硬分档。模型画的影实测只有真值的 1/8。

量影之前先自证尺子 —— 别拿阈值去定影的顶边,剖面只取上沿顶弧那一段列。

细节:阴影原理 · 数值口径见 docs/长期规范速查.md · 影方案见 docs/影-新方案(从原片提影).md

5抠图:两路 + 兜底

抠图是整条流水线里唯一有「算法不确定」的一环,所以它有两条路和一堆保险。

两路

路触发产物读数
AI 抠图/api/cutout 正常ONNX 概率图 → 掩膜__heroCutSource = 'ai'
规则退路接口失败 / 连续失败进冷却背景建模 + 连通块__heroCutSource = 'field'

为什么必须有退路

抠图服务吃内存(几百 MB 级推理),偶尔会被杀掉。退路能跑,只是质量差一些。关键:退路会把「贴着产品的那团影」连成一块收下,于是成品看着体积大、灯罩下沿被切碎。所以退路上多跑一步「剥影」。

剥影的判据演进(两次踩坑,结论写死)

  • v246 排除法 —— 错。 只定义「什么算产品」(明显比背景亮 / 光比不对),其余一律默认算影。 深色或中性色产品正落在「说不清」的中间带里,直接被泛洪吃掉。实测:中性色灯罩被剥 85%,成品里灯罩缺一半。
  • v247 正判法 —— 对。 只有「明确像影」的才剥:影 = 背景的等比压暗,同时满足压暗量够、等比关系成立、亮度比在合理区间。 中间带既不算产品也不算影,一个像素都不动。
  • v247b 去掉压暗上限。 影贴着产品那层压暗很浓,设上限会把浓影全挡在外面,成品里影残留。去掉后影剥得更干净,产品读数一字未变。

两道硬闸(宁可影剥不干净,也绝不吃产品)

  1. 剥完剩余不足基准的 35% ⇒ 一像素不动;
  2. 剥完剩余的最大连通块不足剥之前的 50% ⇒ 一像素不动。

再加两道结构性保护:贴着产品 6px 内不剥;细杆件(横段窄 + 竖段长)= 吊线不剥 —— 吊线颜色跟影几乎一样,只能靠形状救,不保护会被剥掉一大半。

剥影只在退路上跑

代码里有开关:AI 掩膜一个字节都不动。这是为了保住 AI 那条路已有的好成绩,别为了「统一」把好路径也改了。

常见坑

  • 量掩膜 / 产品框之前先画到图上肉眼验,别信纯数值。
  • 别用「只保留最大连通域」当通用手段 —— 会把吸顶式带下挂小熊那类造型切掉挂件。
  • 本地跑 node server.js 起不了子进程(spawnSync … EBUSY)⇒ 本地抠图必然走退路。要验 AI 路径的读数,以线上为准。
数值与判据见 docs/长期规范速查.md → 抠图

6阴影:从原片提

影是唯一「看起来能画、其实不能画」的东西 —— 一画就假。

现役口径

  • 影 = 搬影(PDS):从上传原片里那道真实影按比例搬过来,默认开。
  • 搬影后面接一小段本机尾,让影的收尾自然一点。
  • 灯罩本身不许有投影 —— 影只在天花板那一片,不许糊到灯体上。

量影的纪律

  1. 先自证尺子:先量「免对齐尺」(不依赖阈值)确认测量本身可信,再谈数值。
  2. 别用阈值定影的顶边 —— 原片是渐变,阈值一动整条都变。
  3. 剖面只取上沿顶弧那一段列,别整条列都取。

已知未对齐(记着,别以为已经好了)

搬影在均匀灰带上偏弱:同一张原片上逐列的跨度,真值是 189,我们只有 70 ⇒ 说明我们把横向反差抹平了。影该浓的地方还不够浓。

完整方案见 docs/影-新方案(从原片提影).md;加新灯型见 docs/灯型影标定卡.md

7版本与缓存

改完不升版本号 = 用户看到的是缓存。这是最容易「我改了怎么没变」的原因。

版本三件套(一起动)

  1. 缓存版本 ?v=Nindex.html 里 18 处引用。改了任何 js/css 都要 +1。
  2. 更新日志 changelog.js版本号 +1,条目写用户能懂的短句。
  3. CUT_VER只改抠图逻辑时才 +1。抠图产物按这个号缓存,抠图逻辑变了却没升 = 用户拿旧抠图结果。

更新日志的三条禁令

  • 不许出现安全机制 / 参数 / 密钥 / 口令 / 门禁这类字样(含注释)。
  • 提示词里永远不许出现 **(markdown 星号加粗)。
  • 不许写「优化了体验」这种没有信息量的话。

自检

tools/yuanhui-selfcheck.js 是发版前的闸门,必须全绿。它不只是「跑一下」—— 它里面钉着几十条否定断言(比如某个写法必须已经消失),防止旧实现悄悄回来。

自检有个坑

否定断言跑的是未剥注释的源码 ⇒ 你在注释里写出那个被否定的字面串,自检就会红。改完记得跑。

8访问门与密钥

除了管理员那台电脑,任何电脑都必须手输口令才能进。这是硬要求,不接受例外。

三种入场方式

方式说明
主口令只存 PBKDF2 哈希,源码里看不到明文。config.js 里烤了一份兜底哈希,云端被覆盖也能进。
生成口令管理员自定义 / 自动生成,可设有效期、随时撤销。给别人临时用就走这条。
管理员设备本机被标记后永久免口令,换口令也不受影响。

设计前提:fail-closed

遮罩默认可见,只有访问门模块明确放行才隐藏。加载失败 / 抛异常 / 建不出遮罩 —— 一律保持上锁,绝不放行。「邀请链接自动入场」这条路已下线。

模型配置(密钥)

用户自己的 API Key 存在浏览器本地 + 云库。面板里改完下发到各设备(见 js/team.js)。口令就是提货凭证 —— 知道口令才能取到配置。

怎么把口令给新设备

  1. 在管理员电脑上打开网站免口令直接进。
  2. 密钥 → 🔒 访问安全面板里生成一个口令,可设天数。
  3. 把口令给对方新设备打开网址 → 口令页 → 输入 → 进入。
访问门的实现与台账细节见 js/gate.js;自检里有关于它的守卫条目

9两个全局开关

有两个「整块交给模型还是本机做」的分叉点。搞清它们能省掉很多来回。

开关 A:主图第 1 张走哪条路

口径背景产品阴影位置 / 宽度
整张交模型
当前
模型画模型画模型画(柔和)模型定
本机归位
备选
贴固定底板上传原片像素本机搬影本机统一档

取舍很清楚:整张交模型换来柔和自然的影,代价是背景色值和产品的形状要交给模型;本机归位换来背景逐像素对齐、产品一个像素不改,代价是本机那道影边缘偏硬。选哪条是审美决定,不是技术优劣。

开关 B:详情 2 的方块怎么画

  • 整张交模型(当前):每款方块各叫一次模型画一张 1:1,本机只排版。因为每款单独生成,各块尺寸天然会不一致 ⇒ 本机额外做一次尺寸归一化(量每块最宽行,统一缩放到规范档,只缩不放)。
  • 本机画:留作兜底,某一款模型没出图时接管。
两个开关的实际取值与回退方式见 js/skills.js 顶部注释;字段名 CEILING_HERO_ALLMODEL / SHOW_BLOCK_ALLMODEL

10改一个参数的完整闭环

这是最高频的工作。七步,一步都不能省 —— 跳过第 3 步和第 6 步是绝大多数「改了没生效」的原因。

  1. 先量,别先改写个脚本从真实日志 / 真实图片里读现状。改之前的数是唯一能证明改动有没有用的东西。
  2. 写进权威文件数值参数只写 docs/长期规范速查.md,代码里只写注释指向它。不要两处都写。
  3. 改代码中文一律用编辑器工具改,不要用 shell heredoc(会把中文写花)。改完查行尾,必须全 LF。
  4. 升版本三件套见 §7。改抠图逻辑还要升 CUT_VER。
  5. 自检必须全绿并把旧守卫按新写法更新 —— 别为了让自检过而删断言。
  6. 跑实测本地端到端跑一次,把关键读数(宽度、色差、剥了多少、0 报错)打出来。不合格项要能一条条数出来。
  7. 目视把产物抠出来用眼睛看。断言全绿不等于东西好看 —— 这一步是唯一能发现「数据对、结果难看」的办法。

改中文文件的三条工具坑

  • 不要用 shell heredoc 改含中文的文件 —— 会把中文写花,严重时文件变 0 字节。
  • 编辑工具会把行尾从 LF 改成 CRLF —— 写完必须查 CRLF=0。
  • shell 双引号里的反引号会被执行 —— 先写 .py 文件再跑,别在命令行里拼含反引号的代码。

11加一个新技能 / 新灯型

加灯型 = 新增一个系列,界面按系列自动分组,不用改任何渲染代码。

  1. 建系列新增一个 series(比如「台灯」「落地灯」)。底栏下拉和节点库卡片会自动出现一个分组。
  2. 定主图套系方图 1440×1440 六张 + 长图 1440×1920 五张。1:1 和 3:4 各一套独立提示词,不能共用。
  3. 定详情套系张数与产出见 技能清单。产品展示那张是整张交模型 + 本机尺寸归一化,排版位置按真值墨迹反解,不许凭感觉调字号。
  4. 标定影只读 docs/灯型影标定卡.md。别套用别的灯型的影参数 —— 真值库里只有 49% 的宽扁吸顶灯画影。
  5. 提示词守三条不许出现 **;不写死 RGB(对模型无效);场景图那档要把「产品不许变」写成最强的一条。
  6. 加进技能清单本手册 技能清单 加一行;更新记录 §16 追一条。

尺寸口径别凭感觉

产品占画面多少、中心线在哪,都是从真值成品里量出来的,不是设计出来的。新灯型必须先量真值再写参数 —— 哪怕只取两三张。

12发版

这个项目发过一次错版本,导致线上接口全挂、生图抠图对话全线不可用。所以这一步的参数是死规定。

四个参数一个都不能省

  • 运行语言 = Node(不许 auto)
  • 启动命令 = node server.js
  • 安装命令 = 空(零依赖)
  • 根目录必须有 package.json,且 scripts.start = "node server.js"

发成静态站的后果:被判成 python3 -m http.server,/api/* 全部失效(POST 501 / GET 404),生图、抠图、对话全部不能用。

发完必做(三件事)

  1. 打真接口线上探针打一次生成接口。返回 401 类 = 正常(用的是无效 Key,意思是「链路通、上游已应答」);返回 501/404 = 没起来。
  2. 核对版本直接拉线上的 js,确认 ?v=、开关值、更新日志号都是本次的。
  3. 线上再跑一遍端到端本地跑过不等于线上跑过。桩住接口的自检永远测不出「发成静态站」这条。

发版前的完整闭环

语法检查 → 自检必须全绿 → 各专项体检 → 与线上逐文件比对(有没有不是我改的差异)
→ 发版(Node 形态)→ 线上核对版本 → 线上真接口探针 → 线上端到端再跑一次

多人同时改一个项目时的纪律

部署前先逐文件比对线上版本。若出现不是我改的的差异 ⇒ 先停下来报告,不要默默覆盖掉别人刚发的东西。

13排查手册

按症状查。第一嫌疑永远是「你那个页面是改动之前打开的」。

「我改了,怎么没变」
  1. 先让对方按 F5 刷新。版本号变了浏览器才会重新拉 —— 这是第一嫌疑,占绝大多数。
  2. 确认线上那个文件确实是新版本号(拉一下 js 看 ?v=)。
  3. 看是不是产物本身被缓存了:抠图产物按 CUT_VER 缓存,逻辑改了但没升版本号 = 还是旧结果。
  4. 节点里已有的旧图不会自动重算,要点「生成」重新跑。
「产品不见了 / 抠图不完整」
  1. 看 __heroCutSource:是 field 说明抠图服务在掉链子,先查服务。
  2. 若是退路:看 __heroShadeCut 与 __heroShadeCutReject。剥影吃产品时读数上会出现「剥掉的比例远超影区」。
  3. 量化做法:量成品里产品的外接框宽,和正常档位比。掉一半就是被剥了。
  4. 判据演进见 §5 —— 回到正判法 + 双硬闸。
「阴影不对」
  1. 先问是哪一种:影太硬 / 影太淡 / 影位置错 / 影糊到灯体上。
  2. 影太硬、有明显直边 ⇒ 多半走的是本机搬影那条路(见 §9 开关 A)。
  3. 影糊到灯体上 ⇒ 灯罩的投影没关干净,对应「灯罩不许有投影」那条纪律。
  4. 影整体偏淡 ⇒ 已知问题:横向反差被抹平了,影该浓的地方不够浓。
「背景色值不对」
  1. 量成品背景行带 vs 底板行带,逐灰阶比。差 1 是 JPEG 量化,正常;差 10 以上是背景没对齐。
  2. 如果这张走的是「整张交模型」⇒ 背景本来就是模型画的,色值会飘,这是该口径的固有代价。
  3. 底板文件本身别动 —— 差 1 灰阶去改底板会越改越错。
「产品大小不一致」
  1. 先量每一块的产品宽度占画面宽度的百分比,量化差距,别凭眼睛。
  2. 如果是每款各生成一次的档 ⇒ 尺寸天然不统一,需要本机归一化(只缩不放)。
  3. 如果是本机归位的档 ⇒ 宽度应走统一档;不统一说明归位没跑到,检查 __heroRect.w。
体检脚本红了但不确定是不是真问题
  • 有脚本断言全过却不退出(进程挂着)⇒ 会伪装成超时,超时不算红。
  • 有脚本静默吞异常,把「没测到」伪装成「测出问题」⇒ 遇到可疑的红先单独复测一次。
  • 线上宿主的导航偶发超时 ⇒ 重试即过,导航超时不算产品红。
更细的工具坑清单见 docs/长期规范速查.md → 工具坑

14操作说明

使用者视角,不涉及代码。

第一次用

  1. 打开网址新设备会先看到口令页;管理员那台电脑免口令。
  2. 进「密钥」填模型配置生图和生视频用的 Key、模型、地址。填完下发到你的其他设备。
  3. 选套系底部「技能」里选,比如「卧室吸顶灯主图套系」。
  4. 传原片 + 参考图图1 = 产品原片(必给);图2 = 背景样张(这一档要固定背景时才用)。
  5. 点发送等生成。节点上会出图。

微调成品

选中节点会出现调位面板:方向键挪位置,−/+ 每次 5% 改大小(有上下限,不会缩到离谱)。调完的值会记住。

详情页文字

标题、英文副题、色名都能在底部「标题」面板逐字改。字号和位置锁死(按真值实测反解),面板只能改文字内容,改不了大小和位置 —— 这是有意的,避免改坏版式。

导出

白底那张存 PNG,其余按节点上的格式走。导出用原图,不用缩略图。

一个必须知道的事

已经生成过的图不会自动跟着改动更新。改了任何参数,都要重新点一次「生成」。

15技能清单

当前有 23 个技能:2 个通用技能 + 7 个产品系列,每个产品系列都是「主图套系 + 详情套系 + SKU」三件套。加新技能时在下面加一行。

技能 / 系列产出要点
主图视频3:4 竖版短视频 1 条秒数 4~30 逐秒一档,清晰度 480P / 720P / 1080P;图生视频(主图当第一帧)
生成买家秀8 宫格实拍图普通买家模特、非专业感、同一模特多角度
小吊灯(主图 / 详情 / SKU)方图 6 张(含白底 PNG)+ 长图 5 张;详情 11 张有吊线,吊线的形态是单独一条死规定
卧室吸顶灯(主图 / 详情 / SKU)方图 6 张 + 长图 5 张;详情 11 张必须展示吸顶盘、灯光朝下;第 4 张是场景图
客厅大吊灯 / 餐厅长吊灯(各三件套)同上(方 6 + 长 5;详情 11)吊装悬挂、悬空不画影;场景分别是客厅沙发区、餐厅长桌
床头壁灯(三件套)同上贴墙安装,只有贴墙那一圈接触影;场景以床头为主(床头墙 / 床头自然光 / 床头柜侧视角)
床头台灯 / 客厅落地灯(各三件套)同上必须落在台面 / 地面上,只有底座下一小片接触影

后五个系列由「灯型档案 + 套系生成器」产出:加新灯型 = 在档案里加一条,主图与详情张数、提示词骨架、界面分组都自动生成,不用复制整段提示词。各套系张数与提示词口径见 js/skills.js;单张的具体要求见 docs/长期规范速查.md。

16更新记录

每次发版在这里加一条,最新的在最上面。写清楚「改了什么」和「为什么」,一行一条,不重复正文里已有的细节。

2026-10-20
补齐 5 个品类:大吊灯 / 长吊灯 / 壁灯 / 台灯 / 落地灯
  • 此前只有小吊灯、卧室吸顶灯两个系列有技能入口;另外 5 个灯型只有重心线与宽度档,界面上根本点不到,等于做不了。
  • 做法上没有复制 5 份提示词(那是 110 张文案,必然走样),而是把「随灯型变化的那几项」抽成灯型档案:安装方式、吊线与吸顶盘的有无、影的类型、场景空间、重心线、宽度档;骨架沿用已验收的那套,按档案逐张生成。
  • 每个灯型自动得到主图 11 张(方 6 + 长 5)、详情 11 张、SKU 一张。界面按系列自动分组,渲染代码没有改。
  • 三条按灯型分流的硬约束:安装方式(吊装 / 贴墙 / 台面 / 落地,互不许混)、影(吊灯悬空不画影、壁灯只有贴墙那道、台灯落地灯只有底座下一小片)、位置口径(重心线接近画面底边的灯型,那条线是「落地点」不是「产品中心」,按中心摆会被切掉一半)。
  • 第 1 张是固定渐变底,安装约束只锁姿态不要求出现台面 / 地面,否则会和背景规范打架。
  • 顺手解除两处灯型硬编码:详情方块的提示词改成按节点灯型找对应套系;详情标题按钮改成按技能类型判断。以前每加一个灯型都要回来改这两处。
2026-10-20
助手「接了语言模型却用不了」:选中的那条可能是空的
  • 密钥面板里可以同时存在多条语言模型配置;「当前使用」如果指向其中一条还没填的空配置,助手就会拿着空配置报「未配置」,用户明明接好了却用不了。
  • 新口径:选中那条没有配好就自动回退到第一条配好的;一条都没有才报未配置。
  • 助手对话失败时多显示一行「本次使用:名称 · 模型 · 地址」—— 面板里躺着好几条时,不看这一行根本不知道用的是哪条。永不显示 Key。
  • 排查路径:助手报错 ⇒ 先看这一行是不是他以为的那条 ⇒ 不是就去「密钥」里点「当前使用」。
2026-10-20
修「产品变成一小块贴图」:缩放只许放大,绝不许缩小
  • boss 截图:方图第 2、3 张的产品缩成画面中间一小块,四周一圈硬边方框。
  • 根因是上一版的「缩小 + 铺底」:整张图(连背景)缩进画面中间,四周靠近似底色补,而补色与原背景不是同一条渐变 ⇒ 一眼就是贴上去的;再加上同一张图被处理两次时缩放会累积。
  • 新口径:只放大不缩小。产品偏小 ⇒ 按目标宽度放大(图像溢出画面,四周天然无缝);产品偏大 ⇒ 保持模型原图不动(提示词负责收)。填边代码整套删除。
  • 同一张图处理过就记下来,绝不处理第二次(缩放是乘法的,跑两次就毁)。
  • 体检口加了失败原因(__ceilAlignWhy),以后认不出产品能直接看到是哪一闸拦的,不用猜。
2026-10-20
产品「大小」也改成本机归一:量宽度、按第 1 张同档校准
  • 位置归位后 boss 又报「第 2/3 张的产品高度没达到图 1 的标准」——同一个病根的另一半:大小仍然靠提示词,模型不照做。
  • 做法与位置一致:出图后本机量产品外接框的左右跨度,按「目标 = 画面宽的 66%(与第 1 张主图同档)」整张等比缩放。宽度到位,高度按同一比例自然到位。
  • 为什么锚宽度而不是高度:灯体宽扁,横向跨度由整排灯罩/水晶决定,比纵向那条(吸顶盘与灯罩交界处容易断行)稳得多。
  • 缩放围绕产品中心做,产品不会被顺带挪位;缩小方向四周会露空,用「逐行取左右边缘平均色」的纯背景铺底(拿单列像素拉伸会把墙面噪点拉成竖纹,实测踩过)。
  • 提示词同步:第 2 张不再单独给一档更小的百分比,方/长图的第 2、3 张全部与第 1 张同档;「远景感」只许靠背景留白与景深,不许靠缩小产品。
  • 安全闸:量不到可信跨度、跨度接近满宽(认错东西)、单次缩放超过 45%、缩放幅度不到 4%,任一成立就保持模型原图。
2026-10-20
位置这件事不再交给模型:第 2/3 张出图后由本机归位
  • 连续两轮改提示词都没挪动位置,原因是这四张(方/长图的第 2、3 张)走的是「拿实拍原图做精修」——模型对精修的理解就是照搬原图构图,写进提示词的像素坐标它不跟。
  • 改成出图后本机量一次产品外接框的垂直中心,偏多少就把整张图平移多少;空出来的一条用原图最靠边的真实墙面行做纵向拉伸填上,看不出接缝。
  • 三道安全闸:检测不到可信外接框、偏移太小、平移会让产品顶到画面边,任一成立就一个像素都不动(保留模型原图)。
  • 透明底那张另外改了两处:产品宽度从百分比改成绝对像素区间(百分比模型换算不来),并把「必须展示吸顶盘」改成「照原片,原片没有就不许补」——那句要求正是凭空多出一个吸顶盘的来源。
2026-10-20
第 2/3 张「太靠下」的真根:对齐错了那条线
  • 上一版写的是「灯罩最宽处所在的水平中线」落在重心线上;而吸顶灯最宽的那一行在整只灯的上半部分 —— 把最宽行对在这条线上,整只灯就整体下坠。本机合成一直按「产品外接框中心」对齐,提示词跟它不是一个口径。
  • 现在全套改成「整只灯外接框的垂直中心」对齐重心线,并明写「不是灯罩最宽的那一行」+「上方留白约为下方留白的 0.6~0.8 倍」这条可量判据。
  • 第 1 张原来把 630 写死在方/长共用的那段里,长图那句本应是 710 ⇒ 两句话互相打架;现在数值只在各画幅里出现一次。
  • 长图第 2 张单独抬一档(竖画幅比方图高,同样的百分比看着更小);方图第 6 张透明底收一档,并补「只做抠图不做重绘」的可核对判据。
2026-10-20
整套主图的产品「大小 / 位置」统一成一档
  • 病根不是模型画不准,是每一张的尺寸口径各写各的:第1张给区间、第2张写「偏小、不要放大」、第3张写「比第2张更近」、第5/6张写「与第1张一致」——而第1张本身也是模型画的,等于没有依据。
  • 改成全套共用 ceilGeo(),按张分级给绝对百分比:远景 48~54%、中景与主图 62~68%、白底/透明底同上;重心线仍走 630 / 710。
  • 另加两条通用句:四周留白不少于画面宽度的 8%(治「顶到边」)、上方留白必须小于下方留白(治「靠下」)。
  • 顺带修掉一处互相打架:形状锁里原来写「位置大小与图1 完全一致」,与「按规范重排大小」冲突,已改为「位置大小只认本张规范」。
2026-10-20
方图产品「太靠下、偏大、被改形状」——三个病根一起修
  • 位置:吸顶灯重心线 720/815 是画面正中,而 96 个成品实测中位是 629/711 ⇒ 改成 630/710(方/长)。其余 6 个灯型不动。
  • 自相矛盾:同一张提示词前半段写「720px = 约 42% 高度」、后半段写「720px = 约 50%」,还有一句「上方留白充足」在往下推 ⇒ 统一成一个值,并把「上方留白 < 下方留白」写成可核对的一句。
  • 尺寸:宽度区间 40%~75% 宽了 35 个百分点,模型自然取上限 ⇒ 收到 62%~68%(以 66% 为准),并加「原片在区间内就照原片、绝不放大」兜底。
  • 形状:从形容词换成可核对判据 —— 轮廓剪影必须与原片重合;不许画得更规整更对称;第 2/3/5/6 张加构图锁;负面词补 12 项。
2026-10-20
第 4 张「场景图」为什么一直交回原图

他的原话:「方图第四张不是固定了是根据原图生成场景图吗?我看生成出来的还是原图」。
查下来不是模型不行,是提示词自己打架:通用段落里写「第 2 / 3 / 4 张 …… 保持我原图里的背景、 一律不许重造场景」,而第 4 张的画面要求写的是「生成一个场景图」—— 两条指令正面对撞,模型只会听更保守的那条,于是把原图原样交回来。
另一个帮凶:第 4 张原来写着「优先用实拍,如果我给了第 4 张实拍原图就完全照它来」, 而实际只给图1 时,模型会把「照它来」理解成「照图1 来」。
正解:① 第 4 张单独走「场景分支」,通用段落按张分流(第 1 张固定底板 / 第 2·3 张保持原图 / 第 4 张新建场景 / 第 5 张纯白底),一个分支只说一件事; ② 去掉「优先用实拍」这类条件句,改成写死; ③ 给模型一个可自检的判据(「把产品去掉之后如果还像原图那面空墙,就说明场景没生成,必须重做」)。
教训:提示词里不能出现互相否定的两句话——尤其是「不许重造场景」和「生成场景」这种。 同一份提示词里,同一件事只能有一种说法。

2026-10-20
产品形状锁死:从「一句话」改成「逐项可量」

他反复说过产品被改了样貌。原来那句「不得改变形状和样式」太笼统,模型会自己决定什么算「改变」。 现在改成逐项对照 + 可量阈值:结构(瓣数 / 沟槽数 / 开孔 / 五金件数量与位置)一处不许差、 外接框宽高比偏差与局部偏移给出量化上限、视角与颜色材质同样锁死、只许动背景、出图前自检。
顺带补上一个漏洞:吸顶灯第 1 张走「整张交模型」,它的提示词是自成一篇、 不拼通用段落的 ⇒ 形状硬保护整段漏掉了,而这一张恰恰最容易改产品。现在单独补进这一张。

2026-10-20
图片仓库把「一整份首页」当成照片存了下来

「上传的图莫名变空白、参考图一发就报参数错误」查到底,是两个 bug 叠在一起,都在图片仓库模块里 —— 而它从来没进过自检清单,所以连着几个版本都没拦住。
① 收图时把占位符自己存了进去:先把字段换成占位符、再把「字段」推进收集表(推到的已经是占位符), 于是真正被取来当图片用的是一个占位符对象;它被转成字符串当成网址去请求,服务端老老实实回了首页 ⇒ 仓库里存的一直是同一份首页文件,所有图共用一个 key,读回来当然解不出图。
② 存盘前那份「副本」是假的:只给「带图片数组」的节点做了拷贝,而图片节点没有那个数组 ⇒ 返回的仍是画布本体,抽图时就地把他正在编辑的那张图换成了引用 ⇒ 节点当场空白,参考图也变成非法地址发给上游(上游只回「某个参数不合法」,看不出是哪个)。
教训一:凡是「我要改一份副本」的代码,必须能证明副本与原件不共享任何可变结构;只拷一部分等于没拷。
教训二:模块要自己保证不改调用方的数据(进来先深拷贝),别指望调用方每次都记得先拷贝。
教训三:自检的文件清单要覆盖全部模块 —— 漏掉一个,那个模块里多大的坑都测不出来。

2026-10-16
「机制生效」≠「副作用没发生」

视频冻结那版我只断言了「冻结标记有没有加上/清掉」就上线,却没断言「图片有没有被盖住」, 结果 CSS 选择器写太宽(图片节点与视频节点共用一个类),拖一下全画布变黑。 凡有全局副作用的改动,断言里必须有一条「不该被影响的东西确实没被影响」。

2026-10-16
拖拽卡顿的真正元凶:IndexedDB 写入时的同步深拷贝

之前定位到「保存时序列化整张画布」只对了一半。真用真实鼠标拖拽、给 IDBObjectStore.put 单独打点后量到:60 节点画布每次自动保存卡 409~455 ms, 其中 99.7% 花在这一行。根因是 IndexedDB 写入前要对整个对象做一次 structured clone, 而我们塞进去的是含几十 MB 图片数据的整张画布。修法见 §13。

2026-10-16
SKU 标题支持多行:一行一个规格,一次出多张

原来 SKU 参数是全局一份,所有节点共用,填一次只够一个规格,改第二次第一个就错。 改成参数分两级:标题是 SKU 级(多行,一行一个,写几行出几张), 光色/显指/材质/工艺是 商品级(共用一份)。规格名按「颜色-直径-光色」自动拆解; 原片按文件名里的颜色字样自动配对,配不上退回第一张且不报错。见 §15。

2026-10-16
模型返回的图会过期 ⇒ 生成后立刻转本地

画布上「图片不见了」的真根因:模型给的是临时远程地址,过期或被防盗链拦就加载失败, 浏览器画破图图标。现在生成完立刻转成本地数据(永不过期), 并加了失败兜底(打不开也只提示重新生成,不再出现破图图标)。

2026-10-16
拖动画布时的视频冻结(以及它的两次回归)

播放中的视频会每帧重新合成,多个视频时拖拽掉到 30 fps。改成交互期间冻结、解冻后 恢复播放进度。过程中两次踩坑:① 冻结时别把视频隐藏(画面会变黑,而 pause 本就够用); ② 冻结态可能残留,必须加定时/失焦兜底。见 §21。

2026-10-16
静态资源缓存与压缩

原来所有代码类资源都 no-store,导致版本号形同虚设、每次开页面重下 1.5 MB, 且不压缩。现在带版本号的资源走长期缓存、文本类加 gzip(实测首屏 1539 KB → 535 KB, 再次打开≈0),网页本身仍不缓存以保住「F5 必得新版」。见 §7。

2026-10-16
花费金额:视频也要标、标在居中靠上、只显示元

视频原先没有金额,根因是秒数与清晰度根本没存下来(而价格完全由这两项决定)。 现在生成时存回节点,老视频从元数据反推,读不到就宁可不算。另把金额胶囊改成 居中靠上常显(原先藏在 hover 才出现的按钮条里,等于看不见)。见 §9。

2026-10-16
图片不再进存档 ⇒ 拖拽卡顿从 428ms 降到 0ms

之前只量到「保存时拼字符串」那 98 ms。真用真实鼠标拖、再给数据库写入单独打点才发现: 真凶是 IndexedDB 写入前对整个对象做同步深拷贝,而存档里装着几十 MB 图片数据 (60 节点实测每次松手卡 409~455 ms,其中 99.7% 花在这一行)。 现在图片按内容指纹单独存进独立仓库,存档只留短引用 ⇒ 实测卡顿归零、画质零损失 (字节原样搬,不重编码)。见 §13。

2026-10-20
扫全项目「静默丢数据」,并加一道通用核对

把整个项目里「悄悄把东西剔掉」的写法扫了一遍(filter(Boolean) 共 14 处), 揪出底板样张被静默剔掉——那正是「提示词说两张、实际只发一张」被拒的原因。 现在不静默剔、只记不丢,并在发请求前加一道通用核对: 提示词声明几张 vs 实际发几张,不一致就明着说(对所有技能生效)。 同时说明为什么不逐个改那 93 处空 catch——改动大、易引新 bug, 不如在最后一个关口拦住。见 §10。

2026-10-20
写给自己的教训:清洗类代码不许删用户数据

上一条的「自动修复坏图」里,我写了「拆不出来就置空」—— 结果把老板的图真的删掉了,且补不回来。 现在拆不出来一律原样保留,并加了两条守卫:敢删就报红。 自愈的职责是「修好」,不是「修不好就删」;真要剔除,只能发生在 发往后端之前那一道。宁可让用户看见坏数据、能自己重传, 也绝不替用户删。见 §21。

2026-10-20
诊断口第一次派上用场:三轮猜测换成一次定位

「上传的图不显示」与「生成报 400」其实是同一个病:参考图里混进了一个 对象而不是图片地址。靠读代码猜了三轮全错(猜 blob、猜空数组、猜提示词太长), 最后是前一天加的「把每个字段的形状记下来」的诊断口一次看清。 修法分三道:入口只收字符串、生成前一律剔除非字符串、 页面启动时自动修复画布里已坏的图(不用重新上传)。见 §10。

2026-10-20
接口地址拼错:提示与实现不一致,等于把用户往坑里引

对话报 /api/v3/responses/chat/completions 找不到 —— 服务端只认一种地址写法, 而用户填了另一种,于是拼了两次路径。同时发现界面上写着「填完整地址也兼容」, 那句话是假的 —— 用户是照着它填错的。 改法:服务端认得常见几种端点写法,已含就直接用、不重复拼;提示改成 「填到 /api/v3 就停」。教训:提示与实现必须一致, 不一致的提示比没有提示更糟。见 §11。

2026-10-20
给「参数不合法」装上能看见的眼睛

上游报「某个参数不合法」却不说是哪个,靠读代码猜命中率是 0(连猜三次都没中)。 于是先做诊断再修 bug:转发前把每一个字段的键名 / 类型 / 长度记下来 (不含明文、不含 Key),并新增一个带 key 保护的诊断接口 —— 下一次报错就能一次定位。见 §10。

2026-10-19
加了「运行时体检」,静态全绿不再等于没问题

今天这一串 bug(入口死结 / 只读没撤 / 填不了)静态检查全绿,只有真跑才炸。 于是把运行时体检做成固定脚本(tools/audit-runtime.py): 真点一遍所有入口、真敲字保存、往浏览器里塞 8 种坏数据看会不会崩、 真鼠标拖拽缩放、API 全挂、窄屏溢出。 并补了三条守卫,把「权限判定不许出现两处各说各话」「配置数要走官方读法」 「保存后立刻重算」钉死。见 §10。

2026-10-19
修「进得去却填不了」:两条判定各说各话

入口放行「零配置」后,只读判定还只看管理员标记 ⇒ 人进得来却打不了字。 现在两处用同一套条件:有配置且非管理员 ⇒ 才只读。 另补两处:保存后立刻重算入口、配置计数走官方读法(自己解析会漏旧格式迁移)。 教训:改一条权限/可见性规则,要先 grep 出所有判定点一起改, 同一件事出现两处判定就一定会打架。见 §8。

2026-10-19
密钥面板:严格模式不变,只在「一条配置都没有」时留生路

规则就一句:有配置且本机不是管理员 ⇒ 入口隐藏(与以前一致,外人看不到面板); 一条配置都没有时例外放行 —— 没有配置就没有配置可看, 让人能把第一份填上,填完立刻恢复隐藏。 这样既守住了原来的严格性,又不会「新设备永远配不回来」。见 §8。

2026-10-19
解开「换浏览器就配不了模型」的死结

旧写法把「密钥入口」绑在本机管理员标记上,而标记只存在浏览器本地 —— 于是换浏览器 ⇒ 入口被藏 ⇒ 配不了密钥 ⇒ 永远提示「暂无模型」, 管理员想恢复自己反而被自己的标记锁在门外;更糟的是自检还守着这条, 把缺陷固化成规则。现在:过了门就永远看得见入口,非管理员只读 (密钥仍打码 + 控件禁用 + 增删与导入导出隐藏 + 事件层再拦一道)。 教训:自检守住的规则也可能是错的规则,报红时先问「这条规则本身对不对」。见 §8。

2026-10-19
开发手册挂到线上(顶栏「手册」入口)

之前手册只存在本机磁盘的一个目录里,而那个目录恰好是某个软件的程序安装目录, 软件一升级整个目录被重建,手册就打不开了。现在手册跟着网站一起发布, 顶栏一键打开,不依赖任何本机目录。 手册本体仍以 docs/开发手册.html 为准,每次维护后同步到线上副本。见 §7。

2026-10-16
同类问题第三次:同一个坏数据会流经多个处理点

参考图里的 blob 地址会经过两个处理点,之前只堵了第一个 (收集参考图),第二个(压缩/准备参考图)没堵,坏数据照样流到请求里 ⇒ 上游 400。 现在两处都处理:能本地还原的还原成 dataURL,还原不了的明确报错。 教训:堵住一条路不等于堵住一类问题 —— 同一个坏数据会流经多个处理点, 改数据产生处之外,还要把它的每个消费点都走一遍。见 §21。

2026-10-16
「用旧画布继续生成」报 400:静默兜底把空参考图发出去了

症状是上游只说一句「某个参数不合法」,不说是哪个,极难查。真因是一条链: 图片存档曾把地址换成 blob: → 拦截函数把 blob 变空串 → 过滤后参考图数组变成空 → 仍然发出去(把图生图悄悄退化成文生图)→ 上游 400。 现在宁可明确报错也不静默发空数组,并在页面启动时自动把残留地址修好、并存回。 教训:兜底要么保住语义,要么就该报错;返回空值是最坏的一种。见 §21。

2026-10-16
吸顶灯重心线改回老板权威表(方 720 / 长 815),四处同步

发现「代码 / 提示词正文 / 节点标签 / 自检守卫」四处的重心线数字可能各说各话 (历史上 v163 与 v232 改过两轮口径)。本轮按老板给的权威表一次改全四处, 并让自检守卫按新值卡死 —— 以后任何一处再漂移,自检立刻报红。 锚点定义仍是「产品外接框中心」。见 §9。

2026-10-16
修「用已生成的图继续生成」报 400(图片存档的回归)

图片存档上线后,读档时内部换成了 blob: 地址(省内存),而节点图片要作为参考图 发给火山方舟 ⇒ 后端不认 blob: ⇒ 线上 400。 现在存档仍然只有 14 KB 引用(卡顿不回弹),但读出来还原成 dataURL —— 卡顿的收益来自「存档小」,与读出的格式无关,两者不必二选一。 教训:浏览器内部地址(blob:/objectURL)绝不能出现在要出网的数据里。见 §13。

2026-10-16
主图第 1 张的背景改成「分段可核对」的写法

实测发现模型把上传原片里天花板的光照一起画进背景(上部偏暗 12~24 灰阶、左侧偏暗 47)。 病根不是色值写错,而是只写了一句「上 #e9e9e9 → 下 #f4f4f4 均匀过渡」,模型没有分段参照可核对。 现在给成五段可逐点核对的数值,并明写「背景与原片光照无关、最上一行不许暗 20 灰阶以上」, 负面里再补「背景顶部压暗 / 偏色 / 有光斑 / 把原片光照带进来」。见 §9。

2026-10-15
花费账本加「真实金额」

套系右上角从只显示积分,改成积分 + 约多少元。金额不是用积分换算的(两者没有固定比例), 而是按平台官方刊例价实算:图片按输出像素分档、视频按 token 单价 × token 用量。 价格表独立成 js/price-table.js,改价只动那一个文件。抠图和本机排版不算钱。见 §9。

2026-10-15
品类 × 风格 × 场景 关系图(数据 + 生成器)

按 10 个品类推导出 10 种风格与 12 类场景,每条关系标三档强度:核心 / 适配 / 慎用。 「慎用」这一档最要紧 —— 它不是不行,是容易翻车(比例不对 / 亮度不对 / 风格打架),接单前要先确认。 数据在 data/light_map.py(唯一数据源),页面由 tools/build-light-map.py 生成 ⇒ 以后加品类或调强度只改数据、不动页面。

2026-10-15
新增「主图视频」技能

3:4 竖版图生视频。秒数 4~30 逐秒一档(27 档)、清晰度 480P / 720P / 1080P。 选中技能时底栏自动切到「秒数 / 清晰度」、把图片专用的「尺寸 / 格式 / 张数」收起来、比例锁 3:4、引擎落到视频那档。 这两个值以前在代码里是写死的(恒为 5 秒 / 720P),界面上根本选不了。
模型选择:发送栏的引擎下拉本来就有「图片生成 / 视频生成」两个 tab;「调整后再生成」弹窗里的模型下拉原来只列图片模型, 视频节点在里面会被当成图片重生成 —— 已按 optgroup 分成「图片生成 / 视频生成」两组。
见 技能清单 · 见 §19 视频任务

2026-10-15
详情 2 方块:三个颜色的产品大小统一

每款方块各生成一次,尺寸天然不一致(实测最大比最小大 42%)⇒ 生成后本机量最宽行统一缩放,只缩不放。见 §9。

2026-10-15
小吊灯:产品不会再被影子咬掉一块

剥影判据从「排除法」改成「正判法」,并去掉压暗上限、只留亮度比;加深色产品不被误吃的双硬闸。见 §5。

2026-10-15
主图第 1 张:改回整张交 AI 重画

先试过切本机归位(背景能对齐底板、产品取原片),但本机那道影边缘硬、还拉灰带 ⇒ 恢复整张交模型。见 §9 开关 A。

2026-10-15
场景图第 4 张:长图那档的产品约束补齐到方图同等强度

长图第 4 张原本只写「产品一模一样」,比方图松 ⇒ 补上完整枚举 + AI 感约束。同一条提示词还被详情第 7 张复用,一处受益两处。

2026-10-15
建了这份开发手册

把原理、流程、操作从参数文档和修改记录里抽成总纲,并定下「一条事实只写一处」的维护规则。

更早的改动在哪里
2026-10-15 之前的每一轮改动,逐条记在 docs/修改记录.md(按轮次分节)。本页只放最近几条,保持可读。

17术语表

全站术语只在这里定义一次,别处直接用。

秒数 / 清晰度
视频任务的两个可选参数,见 §19。范围在 js/models.js 的两个常量里。
主图
1:1 方图 1440×1440 六张 + 3:4 长图 1440×1920 五张。电商首图区用的那批。
详情套系
详情页 11 张,1440 宽。
方图 / 长图
方图 = 1:1;长图 = 3:4。两套提示词各自独立,不共用。
固定底板
中性灰纵向渐变的背景图,assets/bg-1x1.jpg / bg-3x4.jpg。逐像素不改。
归位
模型出完图之后,本机把产品抠出来、贴到固定底板上、按规范摆到该在的位置。
整张交模型
不做归位,模型出的整张图直接用。背景、产品、影都是模型画的。
搬影 / PDS
从上传原片里把那道真实影按比例搬过来。影不许模型画。
AI 抠图
ONNX 模型本地推理出的掩膜。
规则退路
抠图服务不可用时的备用算法。质量差一些,会把影连着收下来,所以要多一步剥影。
剥影
在规则退路上,把贴着产品误收进来的影去掉。
中位背景
估计「某处如果没被产品挡住,该是什么颜色」的方法。取邻域中位再插值,别用外圈填充(原片背景是渐变,外圈读数会偏)。
fail-closed
失败时保持关闭/报错,绝不悄悄降级放行。
中间带
像素与背景的差落在一个说不清的范围内。既不算产品也不算影 ⇒ 一个像素都不动。
真值
客户认可的成品图。所有数值参数都从真值量出来,不凭感觉。
三件套
缓存版本号 ?v=、更新日志号、CUT_VER。

19视频任务

视频和图片的流程长得像,实现上完全不是一回事:图片是一次性返回,视频要轮询。

两段式:创建任务 → 轮询

① 创建任务 /api/video/tasks ② 拿到 taskId 不是视频地址 ③ 每 5 秒轮询 /api/video/query ④ 视频地址 最长等 10 分钟 请求体里的四个字段 ratio(比例)· duration(秒数)· resolution(清晰度)· content(提示词 + 可选首帧图) 秒数与清晰度的可选范围写在 js/models.js 的两个常量里,改那里就够
视频两段式:创建拿 id,再轮询拿地址

「写死值」是这一块最大的坑

秒数与清晰度曾经直接写死在代码里(秒数恒为 5、清晰度恒为 720P),界面上看着一切正常,实际上用户根本选不了。 这类坑自检查不出来 —— 因为它不报错,只是默默把用户的意图丢掉。

教训:凡是「用户应该能选」的东西,都要去界面上真的能点到,并用探针抓一次真实请求体确认值真的传出去了。 只断言「函数被调用」不算数。

底栏在视频档的样子

  • 显示:秒数、清晰度。
  • 收起:尺寸、格式、张数(视频没有这些概念)。
  • 锁死:比例固定 3:4,不给改。
  • 自动切:引擎下拉落到「视频生成」那一档,优先用已激活的视频模型配置。

切回图片技能时,视频那两个下拉会自己收回去,不会留下。

20改动总结怎么写

每轮做完对话,除了改产品本身,还要给一份「这轮改了什么 + 改完是什么样」的可视化总结。 这是硬要求,不是可选项 —— 老板看图比看字快得多。

一份总结必须有的四块

块写什么硬要求
症状 → 根因老板报的现象是什么,真实根因是哪一处一个根因管多个症状时,要说清「同一根因」
改了什么文件 + 改哪一段 + 改前是什么、改后是什么写可对照的句子,别写「优化了」
实测读数关键数字:外接框宽、背景色差、请求体字段、自检条数🚨 只许来自真实日志 / 真实像素,不许手抄估算
踩了什么坑这轮踩的坑 + 以前踩过、这轮又踩的(要特别标出)踩过的坑要进 §21,不能只留在对话里

生成方式:一键出图

不想每次现编 HTML,用脚本直接生成(自包含单文件,Apple 风、跟随系统明暗):

python tools/change-summary.py --out out/改动总结.html \
  --title "本轮标题:症状 → 根因" \
  --symptom "背景色值不对 / 产品偏下 / 图2图3产品小 / 形状被改" \
  --cause  "CEILING_HERO_ALLMODEL=true ⇒ 整张交模型" \
  --file "js/skills.js:CEILING_HERO_ALLMODEL" \
  --file "js/canvas.js:剥影判据改成正判法" \
  --metric "抠图框 475px(v246 只有 389)" \
  --metric "自检 336 绿 / 0 红" \
  --pitfall "Write 工具可能写入替换字符" \
  --pitfall "bash 双引号里的反引号会被执行"

--file / --metric / --pitfall 都能重复传,按传的顺序排。

数字从哪来

先跑探针 / 自检,把读数抄进 --metric。不允许「大概」「应该」「差不多」—— 写不出精确数字,说明这一项还没测,那就不该写进总结里。

总结里不要出现的东西

内部常量名、密钥/口令/门禁字样、安全机制描述、以及任何「你放心」「应该没问题」这种没有读数支撑的断言。

21踩过的坑

这个项目踩过的坑,按代价排序。最贵的是「又踩了一次」—— 下面标了 又踩 的都是重犯过的。

〇、把整张图「缩小 + 补边」= 一眼假的贴图 又踩

现象:生成出来的方图第 2、3 张,产品缩成画面中间一小块,四周一圈硬边方框 —— 用户原话「为什么变成贴图了」。

根因:产品偏大时把整张图(连原始背景一起)缩小塞进画面中间,四周只能用近似底色补。补出来的底和原背景不是同一条渐变(哪怕逐行取平均色也一样),边缘必然出现可见方框。更糟的是同一张图被处理两次时缩放会相乘(乘两次就只剩一半多点),越修越小。

正解:只放大、不缩小。产品偏小 ⇒ 按目标宽度放大,图像溢出画面,四周天然无缝;产品偏大 ⇒ 保持模型原图不动,交回提示词去收。缩小时的那套填边代码整段删掉,并且处理过的图要记下来,绝不许处理第二次。

教训:「缩放 + 补背景」在视觉上永远比不过「只放大 + 裁切」 —— 图片是内容,不是可以随便平铺的底色。凡是需要「补一块背景」的方案,先想有没有不需要补的做法。

〇之一、拿「一列像素拉伸」去补背景,墙面噪点被拉成一道道竖纹

现象:「出图后按宽度归一」缩小产品时,四周会露出空白;第一版用「最近的一整列/一整行像素」拉伸填 —— 结果实拍墙面上的细碎噪点被纵向拉成几十条竖纹,一眼假。

正解:先铺一层逐行取左右边缘平均色的纯背景底(横向均匀、只保留纵向渐变),再把缩放后的图盖上去。牺牲一点横向纹理,换来完全看不出接缝。

教训:填边只能用「统计量」(平均值),不能用「单个像素样本」 —— 单个样本里一定混着噪声,一放大就变成结构。

〇、把「最长连续段」当产品外接框,整只灯被切掉一半 新

现象:做「出图后本机把产品挪到重心线」那个功能时,一开始用「所有判定为有产品的行里取最长的一段连续行」当外接框。实测吸顶灯在吸顶盘与灯罩交界处会断一两行,最长的那段只剩灯罩 ⇒ 顶部少算了近 200px、中心虚高 94px ⇒ 整张图被往错误方向移了 94px,比不动更糟。

正解:外接框取第一行到最后一行「有产品」的行;抗噪不靠切段,靠框内有产品的行占比 ≥ 55%(框里大半是空白就说明认错了,多半是把画面上边那道接缝当成了产品顶)⇒ 直接返回「不动」。

教训:检测类改动必须先把框画到图上肉眼过一遍,只看一个数字(中心 638 / 645)两个结果都「看着合理」,只有画出来才看得出漏了吸顶盘。

一、量测被界面元素污染

现象:从截图里量产品,量出「产品宽 84%、高 97%」这种明显离谱的数。红箭头、「查看大图」按钮、选中高亮都被当成了产品。

正解:① 先把掩膜画到图上肉眼验再取数;② 按色度/连通性分割,取最大连通块(红箭头是小块,会被自动排除);③ 采样只取四角那种没有 UI 的位置。

配套:别用「白底阈值」当 alpha 真值 —— 水晶罩的亮面接近白,会被判成断开,掩膜碎成一堆。alpha 才是金标准。

二、中文文件被 shell 写坏 又踩

现象一:用 python -c "..." 追加日志,字符串里的反引号被 bash 执行,日志里的话被删掉、还报错一堆 command not found。

现象二:用 heredoc 改含中文的源文件,中文被写花;严重时文件变 0 字节(先截断再抛错)。

正解:含中文的改动一律用编辑器工具(Write / Edit);shell 里要跑就先写成 .py 文件再执行,别在命令行拼代码。追加日志也走文件。

三、编辑器可能写入替换字符 新

现象:写进源文件的中文注释末尾出现 ��(U+FFFD)。

正解:写完含中文的文件,grep 一遍可疑字符;同时顺手查行尾,必须 CRLF=0。

四、自检的否定断言会盯上注释

现象:自检报某条守卫红,但代码逻辑没问题 —— 因为我在注释里写出了那个被否定的字面串,而否定断言跑的是未剥注释的源码。

正解:注释里引用旧写法要改写(不能原样抄);改完必跑自检。

五、桩没生效 / 探针写错参数名

现象一:桩里写 url.indexOf('/api/cutout'),结果一次都没命中 —— 真实请求是 api/cutout,没有前导斜杠。

现象二:探针里 getSkill('lamp-ceiling') 返回 null,脚本直接崩 —— 套系 ID 写错了。

正解:桩的 URL 匹配用子串(indexOf('cutout'));探针里取技能前先断言 ID 存在,并把探针的失败和产品的失败区分开(别把脚本 bug 报成产品 bug)。

六、「写死值」不报错,最难发现

现象:秒数恒为 5、清晰度恒为 720P,写死在两处。界面上一切正常、自检全绿,但用户根本选不了。

正解:凡是「用户应该能选」的东西,都要真的点得到,并用探针抓一次真实请求体确认值传出去了。只断言「函数被调用」等于没测。

七、发版形态

发成静态站 ⇒ /api/* 全挂(生图/抠图/对话全线不可用),而桩住接口的自检永远测不出这一条。参数见 §12。

八、体检脚本本身不可靠的两类假红

① 断言全过却不退出 ⇒ 伪装成超时,超时不算红;② 静默吞异常 ⇒ 把「没测到」伪装成「测出问题」。遇到可疑的红先单独复测一次。

九、写生成器 / 画 SVG 的四个坑 新

  • 模板里 CSS 含 % ⇒ 不能用 % 格式化(会当占位符炸掉)。改用 __PLACEHOLDER__ 替换。
  • 输出文件被预览进程占用 ⇒ PermissionError。换文件名,或输出名带时间戳。
  • Python 数据模块名不能用连字符(light-map.py 导入直接 ModuleNotFoundError,须写成 light_map.py);手写 sys.path.insert 要 os.path.abspath,相对路径换个 cwd 就失效。
  • SVG viewBox 宽度要按最右列的 x+w 算,不是按列宽相加 —— 写小了整张图被放大、字巨大、溢出容器。连线起点与节点文字要错开,文字放盒子外侧会正好压在连线上糊成一团。

参考实现见 §16 更新记录里那两条;改动总结生成器:tools/change-summary.py

十、重命名要连图标一起看 新

菜单项的显示文字是「图标 + 名称」拼出来的。如果图标本身就是文字(比如 SKU), 把名称改短之后会渲染成 「SKU SKU」。⇒ 改名时把图标一起过一遍。 另外更新日志里写过的旧名要同步改,不能让日志和产品对不上。

十一、两个独立列表的下标不能混用 新

断言「分组顺序」时,我拿分组列表的下标去切技能列表,两套坐标系不同步 ⇒ 报出一堆假红。 ⇒ 只要涉及两个独立列表,统一在同一个列表上取下标。

十二、两套价格并存,混用必错 新

同一个视频模型,官方给两张价格表:按量付费(token 单价)和套餐积分折算(元/秒)。 两者差 1.5 倍,混起来算就会离谱。
规矩:只用按量付费口径,因为我们走的是 API 不是套餐。写价格表时在文件头注明口径。

十三、画幅的像素表必须逐格抄,不要自己推 新

我按「720p 就是短边 720」自己推了各画幅宽高 ⇒ 3:4 推出来比 16:9 便宜 25%; 官方实际是更贵(各画幅像素总量只差一个百分点以内)。差 25% 够把一次预算算崩。
规矩:任何对照表(分辨率 / 画幅 / 型号 / 价格)都必须逐格抄官方文档,不许用规律推导, 并把官方出处写进注释。

十四、整个模块没进自检清单 又踩

现象:「上传的图莫名变空白」连着几个版本都没查到原因 —— 因为出问题的那个模块 根本不在自检的文件清单里,里面再大的坑都不会被点亮(静态检查、体检也都绕开了它)。

正解:① 加新模块时同时把它写进自检清单;每轮收尾对一次「js/ 目录下的文件 == 清单」。 ② 「我要改一份副本」的代码必须能证明副本与原件不共享任何可变结构 —— 只拷一层等于没拷, 对象里的数组/嵌套对象会一起被改到。③ 被调用的模块自己负责深拷贝,别指望调用方每次都记得。

十五、线上探针连不上,不是因为「站点挂了」新

现象:浏览器探针打线上链接直接 ERR_PROXY_CONNECTION_FAILED, 看着像站点不可用,其实同一秒 curl 能正常取到页面。

正解:本机直连会被拦,必须让浏览器显式走本机代理 (proxy={'server': 'http://127.0.0.1:7301'}); --no-proxy-server 与 direct:// 在线上探针里都连不通。 ⚠️ 反过来,测 127.0.0.1 的本地页时要用 no-proxy,别把请求甩给代理。 判据:先 curl 一次 —— curl 通、浏览器不通,就是代理配置问题,不是站点。

十六、探针写错接口,会误判成「功能坏了」新

现象:探针报 Cannot read properties of undefined (reading 'push') 或 reading 'icon',看起来像画布坏了,其实是探针自己调错了口。

两个固定坑:① window.__yh.state 是函数不是对象 ⇒ 必须写 __yh.state().nodes,直接点 .nodes 一定 undefined; ② 图片节点的类型名是 image,不是 img —— 写错会掉进 renderText 兜底分支,于是报 reading 'icon'(DEFS 里没这个类型)。

判据:报错栈里出现 nodes.js ... renderText,就是节点类型写错了,不是渲染逻辑坏了。

十六、提示词里两句话互相否定 新

现象:某一档要「生成场景」,结果却一直是原图。模型没坏 —— 同一份提示词里 一边写「生成场景图」、一边写「不许重造场景」,它只能听一条,而它选的是保守那条。

正解:① 一份提示词里同一件事只能有一种说法;② 不同档位的规则按张分流, 别用一句「第 2 / 3 / 4 张都怎样」把需求相反的档位捆在一起; ③ 后果写成可自检的判据(「去掉产品之后如果还像原图,就说明没生成」), 比写十句形容词都管用。

更细的工具坑清单见 docs/长期规范速查.md → 工具坑