npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-grsai-canvas

v0.11.0

Published

DSH 右侧栏「画布」插件:无限画布 + 直接在图上标注 + 接 GrsAI(gpt-image-2 / nano-banana)按标注改图,结果放在原图旁边对比,画布自动存到工作区

Readme

dsh-grsai-canvas · DSH 右侧栏「画布」插件

在 DSH 网页 GUI 的右侧栏里加一块无限画布,用来跟 AI 一起改图:把图片摆上画布,直接在图上画箭头/框/文字, 一键「按标注改图」,结果放在原图旁边方便对比,继续迭代;画布自动存在工作区里,刷新不丢。

交互模型参考 Cowart(tldraw 无限画布 + 标注驱动改图)。

后端用的是 image.grsai.ai 页面自己在用的接口:

| 用途 | 请求 | | --- | --- | | 建任务 | POST https://grsai.dakka.com.cn/v1/api/generate,Authorization: Bearer <APIKEY>,body {model, prompt, images[], aspectRatio, replyType:"async"} → {id} | | 查结果 | GET https://grsai.dakka.com.cn/v1/api/result?id=<id> → {status, progress, results:[{url}]},约 3 秒轮询一次 |

浏览器不直连上游:全部经插件在宿主的 /grsai-canvas/* 代理转发 —— 绕开 CORS,结果图也不会跨域污染画布 (所以合成图能安全 toDataURL() 保存)。

画布怎么用

摆图:+图片 选文件 / 把图拖进来 / Ctrl+V 粘贴 / 工作区 从会话工作区挑。图片是画布上的对象: 选择工具下可拖动位置、拉四角缩放(默认锁比例,按住 Shift 自由缩放)。

看:滚轮以光标为中心缩放;空格拖动、中键拖动或右键拖动平移;适应 把所有图缩到可见;100% 原大;0/1 快捷键。 底纹点阵随缩放变化,便于判断位置。

标注(画在图片上,按图片归一化坐标粘住,不烧进像素):

| 工具 | 键 | 说明 | | --- | --- | --- | | 选择/移动 | V | 选中、拖动、拉角缩放 | | 画笔 | B | 自由涂抹,左侧可调颜色和粗细 | | 箭头 | A | 从起点拖到终点 | | 矩形框 | R | 框住要改的区域 | | 椭圆 | O | 圈出要改的对象 | | 文字 | T | 点一下图,输入文字,回车确认 | | 橡皮 | E | 擦掉点到的标注 |

Ctrl+Z 撤销 / Ctrl+Shift+Z(或 Ctrl+Y)重做;Delete 删除选中的图;Esc 取消选中; 左侧工具栏的垃圾桶按钮清空这张图上的标注。快捷键只在鼠标位于画布上时生效,不会抢走对话输入框的按键。

生成:写提示词 → 按标注改图(选中图时,提交「原图 + 标注」合成图)或 生成新图(没选图时文生图)。 结果会自动落到原图右边并选中,原图和标注都保留,可以继续改。勾选「只提交标注范围(裁剪)」则只把标注框内的区域发出去; 勾选「附加「按标注改」说明」会自动补一句「按图中标注修改,其余保持原样」。

参考图:选中某张图点 参考,它右上角会显示「参考图」角标,提交时会作为参考图一起发给模型(不计入要修改的主图)。

多图:画布上可以并存任意多张图,自由摆放;生成结果也当作普通图片对象。

Skills 模板(可选套路):右上角 Skills 按钮里有一组内置套路,共 22 个(证件照·白底 / 证件照·蓝底 / 证件照·红底 / 抠图去背景 / 换背景 / 换天空 / 去水印 / 局部修复 / 局部重绘 / 高清修复 / 去网纹 / 商品白底图 / 商品场景图 / 换装·职业装 / 老照片修复上色 / 人脸修复 / 转插画风格 / 转风格 / 线稿上色 / 留白配图(本地加字) / 扩图补全 / 重构构图)。选中才生效,不选就是普通改图;选中后模板文字排在提示词最前面,模板自带的参数(ratio/background/quality)一并套上。自定义模板存在工作区的 画布/skills.json(随项目走),也可以在面板里新建/删除。 字段规范、取值域、提示词写法规范、自检清单和坑,单独一篇 → Skills模板规范.md。

大图:提交前会把合成图(以及参考图)等比缩到长边不超过 SUBMIT_MAX_SIDE(2048), 按钮边上显示实际提交尺寸。这一步是必须的:画布上完全可能出现 9419×15000 这种图,按原始分辨率合成会做出 上百 MB 的 PNG,浏览器 canvas 内存与请求体都撑不住(表现是 Failed to fetch);上游模型也吃不下。 标注按归一化坐标等比跟随,缩小不改变位置关系。整包超过 MAX_SUBMIT_BYTES(24 MB)时会就地拦下并给出建议。

尺寸按模型换算(0.3.0):上游的 aspectRatio 同一个字段,不同模型吃的格式不同 —— gpt-image-2 / -vip / 2.5 系列要像素值(1024x1024),nano-banana* 要比例(3:4),minimax-h3 要方向词(portrait/landscape/square,且没有 auto)。给 vip 发 3:4,上游直接判 failed:gpt-image-2-vip model does not support 3:4 parameter, please use pixel value input, for example: 1024x1024。 实现:上游站点的尺寸表整张抄进 MODEL_SIZES(14 个型号)+ SIZE_LABELS(官方标注如 864x1152 (3:4, 1K)),「尺寸」框的候选值随模型变;nearestSize() 做比例 ↔ 像素就近换算(同比例取 1K 那档),buildGenerateBody() 是唯一拼请求体的地方、提交前必定 normalizeSize() 一次;换模型与套模板都走同一套换算(证件照模板的 3:4 在 vip 上 → 864x1152,在 nano-banana 上仍是 3:4);上游若仍回「不支持这个尺寸」,sizeErrorFix() 自动改成合法值并给中文提示。认不出的模型名一律原样发,不擅自改用户填的值。

模型菜单(0.3.1):模型那一格是「文本框 + ▾」。点 ▾ 弹出 MODEL_CHOICES 菜单,一次列全 17 个型号(含 gpt-image-2.5* 别名),每项标出它认的尺寸格式,当前项打勾,点一行即切换、尺寸自动换算。刻意不用 <datalist>:浏览器会按输入框里已有的文字过滤候选,框里写着 gpt-image-2-vip 时下拉就只剩这一条(用户反馈「模型怎么只剩一个了」),而且 datalist 没法标格式。文本框仍可自由输入任意模型名。

原图比例(0.3.4):「尺寸」默认是**「原图比例」(内部值 SOURCE_SIZE = 'source',显示中文),提交时按实际要发出去那张图**的宽高比就近换算 —— 有主图用主图、勾了裁剪用裁出来那块、只发参考图用第一张参考图,都没有则退到模型兜底值。此前默认 auto(上游爱给 1:1 方图)、且套过模板后那个具体尺寸会被写进 prefs 一直粘着,竖图进去方图出来就是这么来的。

尺寸菜单与成品尺寸(0.5.0):「尺寸」框右边的 ▾ 弹出按比例分组的全量档位菜单(刻意不用原生 <datalist>:它会被框里已有的字过滤成一个,看着像"尺寸被固定了")。菜单底部是一排常用成品尺寸快捷值,顶部是「原图比例」与 auto。 「成品尺寸」(customSize)优先级最高:你明确要 1080x1440,插件就让上游按最接近的档位渲染、回来后在本地精确缩放到你要的尺寸(resizeDataUrl(),imageSmoothingQuality='high')—— 交付出去的文件尺寸就是你要的那个。

文字图层(0.6.0):图像模型"写"出来的是像素,放大必糊。正解是让它出干净底图(Skills 里有「留白配图」),文字用本机字体在本地矢量绘制:数据挂在图片对象上(image.texts,归一化中心坐标 + 占图高比例的字号 → 换尺寸/导出都不跑位),drawImageTexts() 同一份代码服务屏幕预览与导出,存/下载 时(renderExportDataUrl())按原图自然分辨率重绘,所以字永远清晰。支持本机字体检测(isFontAvailable/availableFonts)与「导入字体」(.ttf/.otf/.woff2 → FontFace,仅本次会话有效)。提交给上游默认不带文字(免得模型去重画),想带上就勾选。

分层导出(0.9.0):底图是位图、文字是矢量,烧在一起就只剩一张成品图有用了。文字面板里新增 导出尺寸 与 分层导出,一次落盘三个文件 —— layerPaths() 从保存路径派生出 成品 / <名>-底图.png / <名>.texts.json;renderExportDataUrl(image, { onlyBase, size }) 是唯一的合成出口:onlyBase 不画文字(尺寸又没变时直接返回原 dataUrl,连一次编解码都省),size 按目标分辨率重画(drawImageTexts 用的是 h × size 与 nx × w,所以底图放大必糊、文字不会)。parseExportSize() 复用 parseHumanSize 但只收 mode === 'pixel' 的写法(比例/方向词定不出像素,一律当留空),再走 fitScale() 兜住浏览器画布上限。textLayerManifest() 输出的是参数而非像素(归一化坐标 + 占图高比例字号 + 字体/字重/对齐/颜色/描边/行高),并记下导出尺寸与每个字体在本机是否可用(isFontAvailable)—— 换台机器重出时一眼看得出会 fallback 成什么。落盘仍走既有的 /ws-save 宿主路由,JSON 用 data:application/json,<encodeURIComponent>(宿主 decodeDataUrl 的非 base64 分支走 decodeURIComponent,中文不坏)。

尺寸可以写人话(0.7.0):parseHumanSize() 认 像素(1080x1440 / 1080×1440 / 1080*1440 / 1080 1440)、比例(3:4 / 3/4)、方向词(竖版/竖屏/横版/横屏/方图)、常用名(小红书/朋友圈/视频号/公众号/头像/壁纸/A4/封面/证件照…)与单边数字(按方图);convertSizeForModel() 把它换算成当前模型认识的值并给出人话提示。

坑:数字解析里的分隔符不能写成可选 —— 早先写成 [x×*]? 时 1024 被拆成 10×24。

实物尺寸与 DPI(0.8.0):做印刷/实物交付的人可以直接写带单位的尺寸 —— 210x297mm / 21x29.7cm / 8.5x11in / 210×297 毫米(大小写、前后空格都容忍),由 parseUnitSize() 按 DPI 折成像素(默认 300,在「设置」里改,范围 36–2400)。 不写单位仍然是像素(210x297 = 210×297 像素),这是刻意的:不破坏老用法;裸数字偏小时框下面会提醒「要按毫米算请写 210x297mm」。另外,若上游档位表里没有比例正好吻合的值(A4 是 1:1.414,最近的是 2:3),提示里会写明本地那次缩放会有约百分之几的拉伸,而不是让人拿到变形的成品还不知道原因。 尺寸超出浏览器画布上限时自动换算,而不是报错拦下(fitScale() + MAX_OUTPUT_SIDE = 12000 / MAX_OUTPUT_PIXELS = 60M,超限的图根本建不出 canvas,以前要等提交完才发现导不出来):实物尺寸降 DPI(海报 800x2000mm @300DPI 的 9449×23622 超限 → 自动降到 150 DPI = 4724×11811 像素,成品实际尺寸不变),裸像素则等比缩小并说明缩到了多少。

插件自我更新(0.4.0,0.4.3 起自动检测):内置默认更新源 DEFAULT_UPDATE_URL = https://b.mumu.bio/huabu/version.json —— 「更新地址」留空就用它,装好后打开画布约 2.5 秒自动检查一次,有新版本时设置按钮变成「设置 ⬆x.y.z」并在状态栏留一句(只检查,从不自动安装;不想联网就勾掉「启动时自动检查」)。也可以填自己的地址,或走手动流程:「③ 插件更新」→「检查更新 / 立即更新 / 回滚上一版」。发布侧用 deploy\make-update-manifest.ps1 生成带 sha256 的 version.json(或用 deploy\update-site.html 拖 zip 生成),把 zip 与它一起传到任意静态站 —— 发布步骤见 更新发布说明.md。宿主侧 /update/status|check|apply|rollback 路由 + lib/update.js(零依赖 zip 解压、sha256 必校验、路径越界拒绝、只写插件目录、先备份可回滚);更新后客户端刷新页面生效、宿主需重启 DSH。

让模型听懂标注:grsai 是图像模型,不是会看图的 agent —— 只给它一根红箭头,它大概率当装饰。所以提交时会自动附上 describeMarks() 生成的文字说明:每条标注被翻译成「什么形状 + 在画面的哪个位置 + 什么含义」(箭头点明"所指处即目标"), 图上用文字工具写的内容会被原样抬进提示词当指令,并要求"只改标注处、其余保持原样、输出不留任何标注痕迹"。 另外提交图里的标注会带一层深色底衬,避免在浅色/深色背景上看不清。实践建议:箭头指位置 + 文字说改成什么,两条一起给。

存:画布自动保存到 <会话工作区>/画布/state.json(改动后约 1.2 秒落盘,状态栏显示时间)。单张图可以 存(写进工作区,默认 画布输出/…)或 下载。清空 会清空整个画布(可撤销)。

大屏:右上角 ⤢ 把标签页弹成浮动面板(可拖大小),再配合 DSH 右侧栏自带的展开/全屏按钮; 生成面板右上角的 ▾ 可以把它收起来,把画布让出来。

提交前自检(0.8.0):点提交前,preflightChecks() 会按当前表单列一遍最容易忘写的要求,每条都能一键补进提示词: 「保持主体(五官/姿态/构图/颜色)完全不变」「输出清晰锐利、不要模糊/拉伸变形」「不要添加文字/水印/logo/边框」, 以及场景化的几条:带了参考图没说明用途、勾了裁剪(标注痕迹与位置都不会传过去)、画了箭头却没写提示词、 图上有本地文字图层却没告诉模型"别画字"(这条会直接给一个「套用留白配图」按钮)。已经写过的(含 Skills 模板里的原话)不会重复提醒。

把提示词同步到工作区(0.8.0):画布会把「正在编辑的提示词 + 文字图层 + 标注类型」防抖写进 <工作区>/画布/draft.json (只有文字,不含图片数据),这样对话里的 AI 能读到你在写什么、帮你检查漏了哪些要求。默认开启,「设置」里可关。

设置(APIKEY / 接口地址 / 模型)

没填 KEY 时,右上角按钮显示为**「填 APIKEY」**并高亮,画布右下也有「⚠ 还没填 APIKEY」的入口,首次打开会自动弹出设置面板。

| 字段 | 说明 | 存储 | | --- | --- | --- | | ① GrsAI APIKEY | grsai.com 后台的 KEY,每次请求放在 x-grsai-key 头里 | 浏览器 localStorage(dsh.grsai-canvas.apikey) | | ② 接口地址 | 默认 https://grsai.dakka.com.cn;只填域名/路径前缀,插件拼 /v1/api/generate、/v1/api/result。客户端用 x-grsai-base 头告诉宿主,宿主只接受 http/https | 浏览器 localStorage(prefs 里的 apiBase) | | 模型 | 可直接输入任意模型名,下拉里是常用值 | prefs |

两项都只存在本机浏览器,不写进配置文件、不进日志;每台机器第一次用都要填一次。

两种装载方式(只能选一种)

DSH 的 profile 有两种把插件接进去的办法,本插件都支持,但同时用会被 DSH 拒绝(同一个包不能来自两个装载源):

A. 补丁层(本机现在用的):在 <DSH_HOME>\profiles\web\cordis.patch.yml 里写一行绝对路径。

- insert:
    - id: grsai-canvas
      name: '<绝对路径>/dsh-grsai-canvas/lib/index.js'
  • 优点:零拷贝、改源码立刻生效(客户端半边刷页即换);缺点:路径写死在这台机器上。
  • 停用/卸载:把该文件改回 [](或删掉那两行),重启 DSH,不留其他痕迹。

B. 装成 bundle(deploy\install.cmd,推荐给别人用):把包拷进 DSH home,并按包名挂载。

  • 优点:没有绝对路径,目录随便放、装完就能删下载目录;缺点:需要重启 DSH。
  • 安装位置(默认,可用 -ProfileLocal 改成后者):
    • <DSH_HOME>\profiles\node_modules\dsh-grsai-canvas ← 默认
    • <DSH_HOME>\profiles\web\node_modules\dsh-grsai-canvas ← -ProfileLocal

为什么默认放上级的 profiles\node_modules:DSH 解析 bundle 名时,安装锚点找不到就从 profile 目录往上走 Node 的查找路径, 所以放上一级照样能解析到(已实测);而 pnpm 只在 profile 目录里干活,碰不到上级那个目录 —— 万一以后有人在那台机器上装了 pnpm 并跑 dsh plugin … install,profile 自己的 node_modules 可能被当作多余依赖清掉,而 bundle 列表里还留着这一行, DSH 就会因为解析不到而起不来。放上一级可以彻底避开这个坑。

宿主半边(lib/index.js)改动后需要重启 DSH 才生效;客户端半边(lib/client.js)由 DSH 的模块系统按内容哈希发版, 也建议一起重启最稳。

版本不一致会怎样

客户端启动时会先调 GET /ping 拿宿主的 version 与 features(能力清单):

  • 宿主够新 → 正常读写画布存档、自动保存;features 里有 skills 时才启用自定义模板(/debug 同理);
  • 宿主缺某项能力(客户端被 HMR 更新、宿主没重启时是常态)→ 缺哪项就禁用哪项并提示"重启一次 DSH",不会抛"未知路由";
  • 宿主偏旧(典型场景:DSH 的客户端 HMR 先把浏览器半边换成新版,Node 半边还在跑旧代码)→ 自动保存与恢复静默关闭, 状态栏显示「宿主旧版:没存」,生成面板里显示一条 ⚠ 提示,其余功能(加图、标注、改图、存单图)照常。重启 DSH 后两边一致。

装到别的电脑

插件是零依赖的纯 JS(宿主 + 浏览器两半都手写成成品:没有构建步骤、不联网、不需要 pnpm), 搬过去只要「放对位置 + 让 DSH 知道它」。

前提:目标机器上 DSH 已经能用(dsh web 至少跑过一次,%USERPROFILE%\.dsh\profiles\web 已存在)。

方式 A:一键安装(推荐)

把整个 dsh-grsai-canvas 目录(或发布包 dsh-grsai-canvas-vX.Y.Z.zip)拷到目标机器 —— 放哪、叫什么名字都行 —— 然后:

双击 dsh-grsai-canvas\install.cmd        (根目录那个;deploy\install.cmd 作用相同)

或者命令行:

powershell -ExecutionPolicy Bypass -File deploy\install.ps1
powershell -ExecutionPolicy Bypass -File deploy\install.ps1 -DshHome D:\dsh-home -Profile web
powershell -ExecutionPolicy Bypass -File deploy\install.ps1 -ProfileLocal
powershell -ExecutionPolicy Bypass -File deploy\install.ps1 -UsePnpm   # 有 pnpm 时走官方 dsh plugin add

脚本做的事(全部可逆):

  1. 把包拷到 <DSH_HOME>\profiles\node_modules\dsh-grsai-canvas(默认;-ProfileLocal 则放 profile 自己的 node_modules);profile 不存在时会先让 dsh 自己初始化出来;
  2. 把 dsh-grsai-canvas 追加到 <profile>\package.json 的 dsh.profile.bundles(改前自动备份 package.json.bak, 写成 UTF-8 无 BOM —— 带 BOM 会让 dsh 的 JSON.parse 拒绝启动);
  3. 顺手把 deploy\ 一起拷进目标目录,所以下载目录删掉也能卸载;
  4. 校验文件齐不齐;
  5. 如果该 profile 的 cordis.patch.yml 里已经有 grsai-canvas 行,会拒绝安装并提示先删掉(见上面「两种装载方式」)。

装完重启 DSH 即可。卸载:双击安装目录里的 deploy\uninstall.cmd(或原目录里的那份)。

  • 卸载会把两处可能的安装位置(profiles\node_modules 和 <profile>\node_modules)都清掉,不用记当初装在哪;
  • 从安装目录里运行时,脚本会把"删除自己所在目录"这件事交给一个退出后启动的外部进程,避免 cmd.exe 读不到自己的批处理;
  • 源码目录的名字、放在哪都不影响安装(校验的是 package.json 里的包名),装完可以删掉源目录。

方式 B:手动(cordis.patch.yml 一行)

见上面「两种装载方式」的 A。两种方式只能选一种;切换时先把另一种撤掉再重启。

搬过去之后

APIKEY 不随插件走(只存在每台机器的浏览器 localStorage 里),所以每台电脑第一次用都要在「设置」里填一次。 画布存档在各自的会话工作区里,也不跟着走。

结构

dsh-grsai-canvas/
├── 交接文档.md            # 交接文档:架构、数据模型、怎么改怎么验、坑、变更历史
├── 画布插件说明.md         # 使用者视角的完整说明(尺寸换算表、文字图层、一键更新)
├── Skills模板规范.md      # Skills 模板的字段规范 + 提示词写法 + 自检清单
├── 更新发布说明.md         # 发布方:更新站怎么部署、清单怎么生成、30 秒自检、排错
├── install.cmd           # 一键安装(双击这个)
├── uninstall.cmd         # 一键卸载
├── README-FIRST.txt      # 给第一次用的人看的 3 步速查
├── package.json          # dsh.bundle.patch + dsh.client(platform: web, 导出 ./client)
├── cordis.patch.yml      # 作为独立 bundle 安装时用的入口行
├── deploy/
│   ├── install.ps1       # 一键安装(拷包 + 注册 bundle)
│   ├── install.cmd       # 双击用
│   ├── make-update-manifest.ps1  # 发布用:算 sha256 生成 version.json
│   ├── update-site.html          # 发布页:给人看 + 本机拖 zip 生成清单(单文件零依赖)
│   ├── uninstall.ps1
│   └── uninstall.cmd
├── lib/
│   ├── index.js          # 宿主半边:/grsai-canvas/* 路由(上游代理、工作区读写、画布存档、更新)
│   ├── update.js         # 自我更新:零依赖 zip 解压 + 校验 + 备份 + 回滚
│   └── client.js         # 浏览器半边:window.__ModuleLoader__ 成品 bundle,只依赖平台自带的 react
└── README.md

宿主路由一览:

| 路由 | 作用 | | --- | --- | | GET /grsai-canvas/ping | 自检:插件名、版本、能力清单 features、当前解析到的工作区根(客户端据此优雅降级) | | GET /grsai-canvas/skills | 读画布「Skills 模板」{ok, path, presets} | | POST /grsai-canvas/skills | 写模板(过滤掉缺 id/名称/正文的条目,上限 1 MB) | | GET /grsai-canvas/debug | 排错:最近 40 条上游交互(状态码 + 响应摘要,不含 KEY) | | GET /grsai-canvas/state | 读画布存档 {ok, path, state},没有就是 state: null | | POST /grsai-canvas/state | 写画布存档(JSON,上限 48 MB,写前校验) | | GET /grsai-canvas/ws-list?session= | 列出工作区里的图片(深度 4、最多 800 个,跳过点目录与 node_modules) | | GET /grsai-canvas/ws-file?path=&session= | 读一张图(限制在工作区内) | | POST /grsai-canvas/ws-save | {path, dataUrl} 写回工作区 | | GET /grsai-canvas/update/status | 更新用:本机现状 {loaded, disk, name, dir, backups}(不联网) | | POST /grsai-canvas/update/check | 拉清单比对版本 {current, version, notes, sha256, newer}(清单必须带 sha256) | | POST /grsai-canvas/update/apply | 下载 → 校验 → 解压 → 先备份再覆盖 {from, to, files, backup, restartNeeded} | | POST /grsai-canvas/update/rollback | 从最近一次备份恢复 | | POST /grsai-canvas/generate | 转发建任务(KEY 走 x-grsai-key 头) | | GET /grsai-canvas/result?id= | 转发查结果 | | GET /grsai-canvas/image?url= | 把远程结果图取回为字节,避免画布跨域污染 |

工作区根的解析优先级:环境变量 DSH_GRSAI_CANVAS_DIR → 该会话 header 的 cwd → 沙箱策略工作区根 → 进程工作目录。

依赖面:宿主只用 node:fs / node:path 和全局 fetch(Node 18+);客户端只 require("react"),其余全是标准浏览器 API。

界面挂载点

  • sidebar.right.pane.tab / sidebar.right.pane.tab.title:画布标签页本体与标题(key = 包名)。
  • sidebar.footer.action:侧栏左下角「画布」按钮,点一下打开标签页。
  • tab 类型 grsai-canvas(注册进 ctx.sidebarRightTabs),带一个引导页入口;⤢ 通过 ctx.sidebarRight.float(tabId) 弹成浮动面板。

对端 DSH 版本差异时降级而不是崩:座位不存在就不激活(只影响界面),float 不存在会被 try/catch 吃掉。

自测

_test/ 下有 10 套离线测试(宿主路由、客户端渲染与分支、画布几何/标注绘制、指针交互、尺寸换算、 更新逻辑与路由、发布页、profile 组装),直接 node 跑即可,详见 _test/README.md。 其中客户端三套要真 React(装在 <工作区>\.dshtools);compose-check.mjs 会拿 _test/profile/cordis.patch.yml 里的挂载行去比对当前插件目录,所以它要在源码所在的工作区原位跑。