Skip to content

v2.4.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 03:25
· 3 commits to main since this release

新增

生图产物就地出口:每张图自带复制 / 下载

  • 需求:生成的图是产物,而它此前没有任何出口 —— 灯箱里那个「用别的程序打开」只对磁盘上的绝对路径出现(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)