dsh-grsai-canvas
v0.11.0
Published
DSH 右侧栏「画布」插件:无限画布 + 直接在图上标注 + 接 GrsAI(gpt-image-2 / nano-banana)按标注改图,结果放在原图旁边对比,画布自动存到工作区
Maintainers
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脚本做的事(全部可逆):
- 把包拷到
<DSH_HOME>\profiles\node_modules\dsh-grsai-canvas(默认;-ProfileLocal则放 profile 自己的node_modules);profile 不存在时会先让dsh自己初始化出来; - 把
dsh-grsai-canvas追加到<profile>\package.json的dsh.profile.bundles(改前自动备份package.json.bak, 写成 UTF-8 无 BOM —— 带 BOM 会让 dsh 的JSON.parse拒绝启动); - 顺手把
deploy\一起拷进目标目录,所以下载目录删掉也能卸载; - 校验文件齐不齐;
- 如果该 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 里的挂载行去比对当前插件目录,所以它要在源码所在的工作区原位跑。
