新增
生图产物就地出口:每张图自带复制 / 下载
- 需求:生成的图是产物,而它此前没有任何出口 —— 灯箱里那个「用别的程序打开」只对磁盘上的绝对路径出现(
isFilesystemPath),而生成图要么是ncw://会话附件、要么是内联 data URL,两条都够不着。用户要把一张图贴进别的应用,唯一的做法是求 Agent 调一次SaveImage落盘再自己去找,而他手上明明已经有一张图 - 卡片每张图右上角浮出一排动作,悬停或键盘焦点进来才显形(
group-focus-within而不是group-focus-visible:后者匹配的是容器自己获得焦点,而容器不可聚焦 —— 键盘用户 Tab 到这两颗按钮时,它们会停在看不见的状态上)。取字节抽成纯逻辑image-export.ts,两条路按地址的前缀分,判据不是「能不能 fetch」 data:直取逗号后面那一段,判据落在;base64上:剩下那种(百分号编码的 SVG)交给fetch——data:本身可 fetch,而直接取正文会把%3Csvg…当成 base64 写出去,那是一个坏文件。ncw://走 fetch(附件协议声明了supportFetchAPI、CSP 的connect-src也放行了它)。两条都不能删:落盘之后的生成图走的正是后者,而生成期逐张推来的那几张与旧转录仍是内联 data URL —— 只留一条,正在生成中的图会点不动- 图片按钮与动作条是兄弟而不是父子:图片整个包在
<button>里(键盘要能 Tab 到并回车放大),而按钮套按钮是非法结构 —— 浏览器会把内层那个弹到图片外面,表现为「点复制却打开了灯箱」。原有的放大标随之从右上角挪到左上角,两簇浮层不再互相压住 - 建议文件名只是名字(
image-N,序号是本次调用内的位置,与卡片上「第 N 张」同一个数 —— 同一轮画了四张时,连着存下来的四个默认名各不相同),扩展名由主进程按字节补 - 读失败(附件被删 / 地址取不到)与写失败都只在这颗按钮上闪一下,卡片其余部分不进入异常态;用户在系统对话框里按取消不闪任何状态 —— 给取消也标一个「已保存」或「失败」都是在说谎(同
RewardsOverlay那条)。六条新文案(imageGen.card.image*)与提示词的复制分开:两个动作的对象不同(一段文字 vs 一张几 MB 的图),「已复制」这四个字在同一个卡片里出现两次时,用户要能一眼分出是哪一个
修复
存下来的图顶着 .png,而有的看图工具打不开
- 症状:
app:saveImageFile的扩展名与保存对话框的过滤器都写死成 PNG —— 当时唯一的调用点是渲染层画出来的邀请海报。生成图接上之后,一张 jpeg / webp 会顶着.png落盘:内容没错、名字撒谎,而部分看图工具按扩展名挑解码器,表现是「存下来的图打不开」,且没人会想到是文件名的问题 - 修复:写下去的是原字节,扩展名按魔数判(
imageMimeOfBytes,与转录里ToolOutputImage.mime同一张表、同一条判据),过滤器跟着走;认不出来的字节落.bin,不猜成.png—— 猜错的那一个会被当成 PNG 去解,失败发生在用户双击它的时候 - 补扩展名与换扩展名是同一件事:调用方给的建议名可能不带扩展名,也可能带着一个与实际字节不符的(后者正是这条频道原先的毛病,而它只在存下来的那一个文件上现形,界面上看不出任何异常)
复制图片到剪贴板:一律以 PNG 落地
- 根因:粘贴这条路要跨出本应用,目标程序认的是平台粘贴板里的标准类型。
image/webp(上游爱给)在 macOS 的粘贴板上没有对应类型 —— 直接写 webp 字节,用户会在「在别的应用里粘出来是空的」那一刻才发现,而那时已经对不上这次点击了 - 新增
app:copyImage,经nativeImage.toPNG()换容器(png / jpeg 是纯解码再编码,像素一个不差);解不开的格式当场报错 —— 空图不会让clipboard.write报错,只是粘出来什么都没有,写进去一段谁也读不出的字节比当场说一声「没成」糟得多 - 与
app:copyText分成两条通道,不给它加一个「这次是图」的开关:Electron 里writeText与write(ClipboardItem)是两套 API,走错一条不报错,用户粘出来是一串 base64 文本。通道也不收渲染层自报的 mime,格式由主进程按字节的魔数判(一张自报成 png 的 jpeg 会在粘贴板上被当成 png 贴出去)。同样不走渲染层的navigator.clipboard:仓库里所有复制都经主进程,一处校验、一处失败面 - base64 的校验与解码收进两条通道共用的
decodeImageBase64:这条链路对插件视图同样可达(走的是同一个 preload 桥),不校验就等于「谁都能让主进程往剪贴板或磁盘里放一段由它决定的内容」—— 路径仍由用户选,内容却不该是一段没检查过的东西。两条通道各写一份的话,迟早只有一条被加强,而弱的那个留着的正是这个口子
灯箱的 ✕ 压在窗口关闭键上:点一下关掉的是整个应用
- 症状:非 macOS 上点灯箱右上角的 ✕,整个应用关了。灯箱是
fixed inset-0,必然压在那条 34px 的自绘标题栏上,而右上角常驻着自绘的三颗窗口按钮(WindowControls,z-200),永远盖在灯箱(z-50)之上;关闭键原先写死right-4,正好落在窗口关闭键底下 —— 两颗都是 ✕,用户多半以为是图自己关错了 - 修复:关闭键按
--spacing-window-controls让开那 128px,macOS 那一侧反过来给红绿灯让位(左上角「打开方式」那一簇按 86px,与RewardsOverlay的 header 同一个数) - 灯箱根节点补
app-no-drag:那一块是-webkit-app-region: drag,OS 会吞掉该区域里所有 pointer 事件 —— 不加的话顶部那一条点不动,一按住整个窗口跟着鼠标跑
IS_MAC 让整个模块在 node 环境下加载不了
- 症状:手搓 JSDOM 的那些用例里
vi.stubGlobal要等beforeEach才跑,而模块是在 import 那一刻求值的 —— 少了那道判断,任何一条会渲染到灯箱(它在这里读平台)的用例都会在 import 阶段抛window is not defined,一个文件里的全部用例一起废掉 - 修复:
typeof window !== 'undefined' && window.nextcowork?.platform === 'darwin'。真实渲染进程里走不到那个分支(preload 没挂上时services/ipc.ts会先大声挂掉),那时结果也确实是「非 mac」这一档 —— 让错了边远好过整个文件加载不了。取值仍是 module-level:平台在一个进程的生命周期里不会变
目录补录 Claude Sonnet 5.5
- 费率随官方定价页截图一起给(输入 $2 / 输出 $10 / 5m 缓存写 $2.5 / 1h 写 $4 / 命中 $0.2),五个数与同族 Sonnet 5 逐值相同 —— 这里仍按各行存自己的绝对价,不与相邻那行联动。
source指向官方定价页,而不是那份 2026-08-31 的费率卡 PDF:我们没在那份 PDF 里核到这个型号,挂一份没核过的 PDF 等于伪造证据(同claude-opus-5-5那条的两处刻意不一致) - 窗口与思考预算没有独立依据,按同族 Sonnet 5 推(1M + 32K budget),注释里写明是推的 —— 推错的方向是「预算给多 / 给少一次思考」,不是窗口被压小。抄写校验随之从 14 个 SKU 更到 15、目录从 17 行更到 18
测试
- 新增 2 个测试文件、扩充 2 个,合计 11 项:
image-export(内联 data URL 直取且不绕 fetch、ncw://走 fetch 并把字节原样编回 base64、非 base64 的 data URL 不把正文当字节、几 MB 的图分块编码不爆栈、404 抛出去给按钮闪失败态);lightbox-position(✕ 必须挂在right-window-controls上而不是写死像素、根节点必须声明app-no-drag—— jsdom 不排版量不出矩形,所以钉的是类名) - 生图卡片那一组盯的是送出去的载荷,不是「按钮在不在」:两颗按钮的文案与图标在两种状态下都长得像,而送错的那张图(把第 1 张的字节送给第 2 张那颗按钮)在界面上看不出任何异常,所以断言一律精确比 —— 另含「复制完只有那一张换文案」与「取消保存不闪任何状态」
- 全量套件 6390 项通过(23 skipped),12 项失败全部落在 v2.2.10 起就记在案的既有红项:
document-engine/provider-registry、plugin/document-rpc是测试在库、源文件从未入库(0 test,导入即失败),plugin/document-scope因引用前者连带,environment/session-mux、document-engine/native-host依赖本机python(本机没有,故spawn python ENOENT);两版之间干净基线同样红 typecheck(node + web)、test:release与lint通过(0 error,4 个既有 any warning)