DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

lance808 /

lance808/dsh-image-tool

Verified

给 DeepSeek Harness 补上生图能力:把任意 OpenAI 兼容的生图服务接成平台工具,模型在对话里直接出图。

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@c7d11767

dsh-image-tool

给 DeepSeek Harness 补上生图与改图能力:把任意 OpenAI 兼容的生图服务接成平台工具, 模型在对话里说要画图或改图,图片就以平台原生图片卡出现在对话中。

An image generation & editing plugin for DeepSeek Harness — any OpenAI-compatible image API, exposed as native platform tools.

  • 生图 API 与对话模型 API 完全独立 —— 用 DeepSeek 聊天、用别处画图,各配各的密钥
  • 两个工具:generate_image 生成新图;image_edit 基于已有图做修改(局部重绘 / 换风格 / 扩图)
  • 连续改图:image_edit 不传原图就取本会话最近一张 —— 「把这张图改成…」可以直接连着说
  • 对话里就是图 —— 注册平台工具卡,缩略图、点击全屏、下载、在工作区打开展开即用
  • 不引入任何私有内容块 —— 只用平台自己的 image 块,纯文本模型也能安全经过
  • 调用前弹窗确认(生图与改图各自一个开关)—— 真实计费,不希望你不在场时被刷
  • 完整的设置页 —— DSH「设置 → 图像生成」里填接口、密钥、默认参数与保存目录
  • 一键识别上游模型 —— 设置页点「识别上游模型」即从 {apiUrl}/models 拉取列表并选择
  • 外部调用接口 —— /api/image-tool/*,不想经过模型时由别的程序直接调
  • 不依赖构建 —— 宿主半是普通 ESM,浏览器半是手写的 ModuleLoader bundle

目录

  • 1. 它解决了什么问题
  • 2. 安装
  • 3. 配置
  • 4. 使用
  • 5. 安全与隐私
  • 6. 关于"纯文本模型看到的占位文本"
  • 7. 项目结构
  • 8. 开发与测试
  • 9. 已知限制
  • 10. 许可证

1. 它解决了什么问题

DeepSeek Harness 里没有"生图协议"这条通道。LLM 适配器的契约是 stream() → 文本 + 工具调用,图片想进入对话,只有一条正路:

由工具结果携带平台的 { type: 'image', attachment } 内容块。

所以"补全生图接口"不是发明一个协议,而是把图片接进既有的内容块体系。 本插件做的就是这件事:注册一个工具,工具内部打生图接口,结果渲染成平台自己的图片块。

为什么不能用私有内容块:平台的消息适配器不会跳过未知块 —— 一个自定义块类型 会让整个回合以 UNSUPPORTED_CONTENT 失败,连文本都拿不到。用平台自己的 image 块, 纯文本模型与多模态模型就能共用同一条链路,无需任何配置分叉。

为什么还要自己画卡片:平台内置的图片卡只认 read_image 这一个工具名 (ui-tool 的 imageCardModel 写死了这一点)。别的工具落到通用卡片,而通用卡片会把 非文本内容块 JSON.stringify 之后显示 —— 也就是说,只把 image 块塞进结果、 不注册工具卡的话,用户在对话里看到的是一坨附件引用 JSON,而不是图片。 本插件注册 tool.call.toolview 的 generate_image / image_edit 两个 key,把图真正画出来 (缩略图、点击全屏、下载、在工作区打开),并且不声明 tool.call.images 子槽 —— 那个子槽已被内置的 read_image 声明,一个子槽只能由一个 entry 声明。

改图为什么是另一个工具:generate_image 与 image_edit 的必填项不同(改图必须有一张原图)、 上游端点不同(/images/generations 收 JSON,/images/edits 收 multipart/form-data)、 闸门也不同(改图确认框要写明改的是哪张图)。合成一个带 action 参数的工具有百害而无一利。


2. 安装

node scripts/install-into-profile.mjs desktop

参数是 profile 名(不传则读 DSH_PROFILE,再不然默认 web)。脚本做三件事,全部幂等:

  1. ~/.dsh/plugins/dsh-image-tool → 本目录(人工可读的"安装位")
  2. <profile>/node_modules/dsh-image-tool → ①(Node 的解析路径,缺了加载不起来)
  3. <profile>/package.json 的 dependencies + dsh.profile.bundles

装完 重启 DSH。

为什么不用 dsh plugin add:那条路依赖 dsh CLI 与 pnpm,很多只有桌面端的环境没有。 本插件是纯 JS、只有一个普通依赖,两层目录链接 + 一行 profile 声明就够了,不需要构建步骤。

脚本对已存在的链接不会静默覆盖:如果目标指向别处,它会报错并打印当前值与期望值, 让你自己决定 —— 默默覆盖等于替你做了一个你不知情的决定。


3. 配置

3.1 在 DSH 设置页里填(推荐)

设置 → 图像生成,一个完整表单:

分组 内容
接口 接口地址、API Key、生图模型、改图模型(可点「识别上游模型」拉取列表选择)
默认出图参数 尺寸、改图尺寸(均为 1K / 2K / 4K 分组下拉)、张数
图片保存 默认保存路径
调用前确认 每次生图 / 改图前向我确认(两个独立开关)
高级 超时、单张大小上限、四个可选透传项

它由插件的浏览器半(client.js)注册到 settings.section 槽位,数据走插件自己的 同源路由 /api/image-tool/config。

密钥永远不会回传到这个页面:只显示 已设置(…abcd),输入框留空即"不修改", 清除必须点专门的按钮。

浏览器半是手写的 ModuleLoader bundle,没有构建步骤 —— React 由外壳提供, 本插件不带任何前端依赖。

识别上游模型

「生图模型」右侧的 识别上游模型 按钮会拉取 {apiUrl}/models,把结果去重排序后 列成下拉框,选中即填入输入框(仍然要点「保存」才生效)。

几个刻意的选择:

  • 不自动请求:只在点按钮时才打上游。打开设置页就自动查,等于每次进设置都替你 发一次你没要求的网络请求,而且会在你还没填完 Key 的时候就报错。
  • 超时 20 秒:列表接口不该让人等几十秒;慢到这个程度就报错,让你手动填写。
  • 失败原因如实回报:分清"Key 被拒(HTTP 401)"和"这个上游根本不提供 /models",后者要明确提示手动填写。
  • 兼容多种回包形态:{data:[{id}]}、{data:["x"]}、{models:[...]}、裸数组, 以及用 name / model 代替 id 的网关。
  • 识别结果不落盘:它只是帮你把值填进输入框;配置以点「保存」写入的为准。
  • 改过接口地址或密钥后列表作废:保存成功会清空已识别的列表,避免选到一个不属于当前上游的模型。

3.2 用附带的 CLI

node scripts/configure.mjs                 # 交互式逐项询问
node scripts/configure.mjs --show          # 查看当前状态(含权威的 hasKey)
node scripts/configure.mjs --api-url=https://api.openai.com/v1 --api-key=sk-xxx --model=gpt-image-1
node scripts/configure.mjs --set count=2   # 改单个字段(可重复出现)
node scripts/configure.mjs --clear-key     # 清除已保存的密钥

适合脚本化或在没有浏览器时使用。它把 settings RPC(连同本机 Cookie 铸造)包成一条命令。

3.3 或者直接调 settings API

配置就是一个标准的 settings 命名空间 image-tool:

// POST /api/settings/update
{ "ns": "image-tool",
  "patch": { "apiUrl": "https://api.openai.com/v1", "apiKey": "sk-...", "model": "gpt-image-1" } }

⚠️ 服务方法签名是位置参数 update(ns, patch),与 RPC 端点的 { ns, patch } 信封不是一回事。

3.4 字段

字段 默认 说明
apiUrl '' 生图接口基址,如 https://api.openai.com/v1。允许本机(http://127.0.0.1:7860 这类本地服务是正当用法)
apiKey '' 生图服务密钥。与对话模型凭据无关
model '' 生图模型名;留空则不发送 model 字段,由上游默认。设置页可点「识别上游模型」从 {apiUrl}/models 拉取列表后选择
editModel '' 改图模型名;留空沿用 model。有些服务商的生图与改图是两个模型名,共用一个字段会让改图必然 404
size '' 默认尺寸。设置页是分组下拉:1K / 2K / 4K 三档,覆盖 1:1、4:3、3:2、16:10、16:9、21:9 电影,以及各自的竖屏方向(3:4 / 2:3 / 10:16 / 9:16 / 9:21),另附 OpenAI 官方固定尺寸。需要列表外的特殊尺寸时,在对话里让模型传 size 参数
editSize '' 改图尺寸;留空沿用 size,两处都留空则完全不发送 size(由上游按原图尺寸处理 —— 改图通常希望这样)
count 1 默认张数(1~8)
savePath '' 默认保存目录;留空保持当前会话的 generated-images/,支持绝对目录或相对会话工作目录的目录
confirmBeforeGenerate true 生图前弹窗确认
confirmBeforeEdit true 改图前弹窗确认(独立开关;确认框里会写明改的是哪张图)
timeoutMs 180000 单次调用上限。生图是同步接口,实测几十秒很正常
maxImageBytes 20971520 单张图片字节上限(同时是改图读取原图的上限)
quality / style / background / outputFormat '' 可选透传项,只在你填了值时才发给上游;生图与改图都带

配置是 live 的:改完即时生效,不需要重启,下一次调用就用新值。

3.5 端点约定

生图打 {apiUrl}/images/generations(OpenAI 兼容;如果 apiUrl 已经写到这个端点上, 不会重复拼接):

{ "prompt": "...", "n": 1, "model": "...", "size": "...", "response_format": "b64_json" }

改图打 {apiUrl}/images/edits,multipart/form-data,字段为 model / prompt / size / n / response_format / image(二进制)/ mask(可选,二进制)。 把 apiUrl 填成生图端点也不会拼错 —— 解析时会先把 /images/generations 退掉。

为什么不走 generations + 参考图字段(如 input_image):那类字段各家网关支持不一, OpenAI 形态的中转会直接以 Unknown parameter: 'input_image' 拒绝; /images/edits 反而是每个 OpenAI 兼容中继都实现的 portable 路径。

两种请求的回包都支持三种现实形态:data[].b64_json、data[].url、data[].image_url.url。 媒体类型按魔数嗅探,不信任上游声明的字段。


4. 使用

4.1 对话工具 generate_image

模型在对话里调用即可出图:

用户:画一只在雨里等公交的猫
模型:→ generate_image({ prompt: "一只在雨里等公交的猫", size: "1024x1024" })

生成的图片以平台原生 image 块进入对话,并在界面上渲染成图片卡 (缩略图、点击全屏、下载、在工作区打开)。保存路径会立即显示在工具结果里, 不需要额外询问:

第 1 张已生成至 ./generated-images/generated-1730000000000-1.png
已保存到:D:\...\generated-images\generated-1730000000000-1.png

落盘位置规则:

  • savePath 留空 → 保存到当前会话工作目录的 generated-images/
  • savePath 填了 → 保存到该目录(不存在时自动创建)
  • 目录不可写 / 拿不到会话工作目录 → 只存附件库,并在结果里说明原因

尺寸:设置页的下拉覆盖常用档位与比例(含竖屏与电影画幅);需要列表外的特殊尺寸时, 在对话里直接说,模型会把 size 传给工具 ——

用户:用 3200x1440 画一张雪山日出
模型:→ generate_image({ prompt: "雪山日出", size: "3200x1440" })

这条路径不受下拉限制。若设置页里的当前值是手填的特殊尺寸,下拉会把它单独列成 「…(当前值,不在预设内)」,不会因为找不到匹配项而显示空白。

4.2 改图工具 image_edit

基于一张已有图片做修改,走 /images/edits(multipart 上传原图):

{ "prompt": "把背景换成夜晚,加几盏路灯", "image": "generated-images/generated-1730000000000-1.png" }

原图(image 参数)有三种写法,按这个顺序判定:

写法 例子 说明
省略 {"prompt": "改成水彩风"} 取本会话最近一张图 —— 连续改图就靠它
附件 ID {"image": "sha256:…"} 指定会话里某一张(含你自己上传的图)
工作目录路径 {"image": "generated-images/…png"} 读磁盘上的文件,必须在当前会话工作目录内

"最近一张"同时覆盖两种情况:模型上一轮刚生成的图,以及你自己上传到对话里的参考图。 用了哪张会在结果里写明(原图来源:…),你可以核对模型改的是不是你以为的那张 —— 这是花钱之前最值得拦住的一种误解,所以确认弹窗里也会带上它。

局部重绘(mask):

{ "prompt": "把这个区域换成一片星空", "mask": "masks/sky.png" }

mask 与 image 一样支持附件 ID 或工作目录路径。约定是 PNG、与原图同尺寸、 透明处就是允许改动的范围;媒体类型不是 PNG 时放行但在结果里给一句提示 (有些中继接受别的写法,硬拦反而挡住了能用的路径)。

尺寸:editSize 与 size 都留空时完全不发送 size 字段,由上游按原图尺寸处理 —— 这通常是改图最不容易出错的行为。

连续改图的完整例子:

用户:画一只在雨里等公交的猫
模型:→ generate_image({ prompt: "一只在雨里等公交的猫" })
用户:把这张图改成晴天
模型:→ image_edit({ prompt: "改成晴天,阳光透过云层" })      # 没传 image → 取最近一张
用户:再给它加一把伞
模型:→ image_edit({ prompt: "给猫加一把黄色的伞" })          # 又取刚改出来的那张

4.3 另存工具 save_generated_image

把已生成的图片另存到别处,不重新生图、不计费:

{ "path": "images/天空之城.png" }

不传 attachmentId 时保存当前会话最近一张图片;指定时从生成结果里取 id:

{ "path": "images/另一份.png", "attachmentId": "sha256:..." }

从当前会话的图片块恢复附件引用,因此不依赖进程内索引,重启后仍可用。 只接受工作目录内部路径、不覆盖已有文件、不转换编码;省略扩展名时补上实际格式。

4.4 外部调用接口

供不需要经过模型的调用方使用:

# 配置状态(不回显密钥,只给 hasKey 与末四位指纹)
GET  /api/image-tool/status

# 读 / 写配置(设置页用的就是这两个)
GET  /api/image-tool/config
POST /api/image-tool/config      { "apiUrl": "...", "apiKey": "...", "clearApiKey": false, ... }

# 识别上游可用模型(设置页的「识别上游模型」按钮用的就是它)
GET  /api/image-tool/models      → { ok, models: ["..."], endpoint, count }

# 生图;includeData=1 时额外返回 base64 字节
POST /api/image-tool/generate
     { "prompt": "一只猫", "size": "1024x1024", "count": 1 }

# 改图;HTTP 调用必须传 cwd(路径边界),image 用工作目录内的相对路径
POST /api/image-tool/edit
     { "cwd": "D:\\work", "image": "input.png", "prompt": "改成夜景", "mask": "mask.png" }

# 读取本次挂载期间生成的附件字节(需要同一会话鉴权 Cookie)
GET /api/image-tool/image?id=<attachmentId>
GET /api/image-tool/image?id=<attachmentId>&download=1   # 带 Content-Disposition,可浏览器另存

生成与改图的响应形状完全一致:images[] 包含完整 ref、fetchUrl 与 downloadUrl; 改图额外带 origin(用了哪张原图)。外部调用方不需要为"这次是改图"换一套解析逻辑。

/image 的引用索引最多保存 200 条,插件重新挂载(含配置更新)或进程重启后失效。 长期保留字节请用 includeData=1 并自行保存。

外部 HTTP 调用没有会话工作目录,所以 /generate 不落盘;/edit 需要按路径读原图, 因此要在请求体里显式给出 cwd —— 我们既无法替你判断"这个路径在不在允许范围内", 也不该拿进程当前目录去猜。给了 cwd 就与工具路径一样落盘。

⚠️ 鉴权是自己做的,不是白捡的

这一点很容易想当然,而想错的后果是安全漏洞:

挂在 webServer 上的自定义 prefix 路由会抢在 /api 的会话鉴权之前 —— webServer 按最长前缀派发,本插件的前缀比 /api 更长。

所以这些端点不会自动继承 /api/* 的鉴权。本插件显式向 connection 服务要判定 (connection.requestRejection(request));拿不到 connection 时不假装安全: 只提供只读面,配置写入、生图与上游模型查询一律 403。

工具路径与 HTTP 路径共用同一份 runGenerate / runEdit,所以协议、校验、SSRF 关卡、 附件落地完全一致,不存在"两条路行为不同"。唯一差别是:确认弹窗只加在工具路径上 —— 那个闸门要防的是"模型自作主张花钱生图";HTTP 调用是调用方主动发起的明确意图, 再弹一次窗既问错了人,也会让自动化无法工作。

本插件不含任何聊天平台相关内容,是通用的生图工具。


5. 安全与隐私

⚠️ 密钥的落盘语义(请务必知道)

apiKey 标记为 role('secret'),它保证的是:

  • ✅ 不会回传到浏览器:平台的 settings 投影会把它结构化剥离,API 只报告 secrets: [{ path: ['apiKey'], set: true }],前端永远拿不到原值。
  • ❌ 不是加密存储:它会以明文写进 <profile>/cordis.patch.yml 里那条 entry 的 config 段。该文件的权限是默认 ACL(本机用户与管理员可读),不是 0600。

如果这个取舍不可接受:请只填权限受限的 Key,或改用 DSH 的凭据服务自行托管密钥。

清除密钥要用 unset,不是写空串

把 apiKey 写成 "" 不会清除它 —— secret 字段把空串当作"本次没有改动", 这是平台为避免"没渲染出来的密钥被空表单误删"而定的语义。正确做法:

node scripts/configure.mjs --clear-key
# 等价于 settings/mutate 的 { op: 'unset', path: ['apiKey'] }

两条地址的可信度是分开对待的

地址 来源 策略
apiUrl 你自己填的 允许本机/私网。本地生图服务(SD WebUI、ComfyUI、one-api…)是正当用法,拦掉等于把本地部署判死
回包里的图片 url 上游给的 过 SSRF 关卡:只允许 http/https,拒绝环回、私网、链路本地(含 169.254.169.254 云元数据)、.local、IPv6 的 fc/fd/fe80/::1 及 IPv4-mapped 形态

把这两条混成一条,不是把本地用户拦死,就是把内网暴露出去。

图片下载另有约束:禁止自动重定向(避免校验过的公网地址跳进内网)、 按流累计校验大小(不只是信任 Content-Length)、并接受调用方的取消信号。

其他

  • 没有显式 Key 时一个字节都不发(不会拿别处的凭据顶上)。
  • 回包正文有 32 MiB 上限,图片下载有 15 秒 / maxImageBytes 上限。
  • 不做自动重试:生图按张计费,盲目重试会在"其实已经出图、只是回包慢"时重复计费。 失败就把原因讲清楚,由你决定要不要再来一次。
  • 另存工具限定在工作目录内,拒绝覆盖已有文件,且会逐段拒绝符号链接目录。
  • 改图会把原图字节上传给上游(这正是 /images/edits 的工作方式), 而且它只读当前会话工作目录内的文件 —— 想改别处的图,先复制进来。

6. 关于"纯文本模型看到的占位文本"

如果当前对话模型只接受文本,平台会在派发前把每个 image 块替换成一段占位:

[image omitted because this model accepts text only; attachment sha256:...]

这是正常行为,不是错误 —— 图片已经生成并落进附件库,界面上照样显示图片卡 (卡片读的是工具结果的附件引用与 presentationMeta,与模型能不能看图无关), 只是这个模型看不到像素。换成多模态模型就能直接看到。

之所以要这么设计:平台的消息适配器不会跳过未知块,而用一个私有块去渲染图片会让 整个回合失败。用平台自己的 image 块,两类模型就能共用同一条链路,无需任何配置。 改图工具同理:把图交给外部服务改,根本不依赖当前模型有没有视觉能力。


7. 项目结构

dsh-image-tool/
├── package.json                dsh.bundle.patch + dsh.client(宿主半 + 浏览器半)
├── cordis.patch.yml            bundle patch;entry id 必须是 image-tool
├── client.js                   浏览器半:注册「图像生成」设置页(settings.section)
│                               + generate_image / image_edit 的工具卡(tool.call.toolview)
│                               手写 ModuleLoader bundle,无构建步骤
├── lib/
│   ├── index.js                入口:Config schema / apply(step() 逐步容错)/
│   │                           runGenerate + runEdit / imageToolOutput(render + presentationMeta)
│   │                           落盘:makeDiskSaver + cwdFromExecution + makeImageFetcher
│   ├── generate.js             协议核心:生图 JSON / 改图 multipart 两条请求的构造与发送、
│   │                           回包解析 / SSRF 关卡 / 附件与磁盘落地 / 上游模型列表
│   │                           (纯逻辑,不碰 ctx,可离线测)
│   ├── image-source.js         改图输入解析:会话最近一张 / 附件 ID / 工作目录路径(含越界与
│   │                           符号链接防护),mask 与警告
│   ├── routes.js               外部接口 + 鉴权 + /image 取字节 + /models 识别 + /edit
│   ├── save.js                 另存:会话图片恢复 / 路径限制 / 不覆盖写入
│   └── diag.js                 诊断日志
├── scripts/
│   ├── configure.mjs           配置读写 CLI(自包含 Cookie 铸造)
│   ├── install-into-profile.mjs 安装进 profile(幂等)
│   ├── mock-image-api.mjs      本地假服务(返回**真实编码**的 PNG;生图 + 改图两个端点)
│   ├── test-generate.mjs       协议:生图 JSON / 改图 multipart / 回包解析 / SSRF / 超时 / 模型列表
│   ├── test-image-source.mjs   原图解析:路径越界 / 符号链接 / 魔数判定 / 最近一张 / mask
│   ├── test-plugin.mjs         插件契约:命名空间 / volatile / secret / 确认闸门 /
│   │                           两个工具的形状 / 配置接口 / 落盘 / 改图与 /edit 路由
│   ├── test-client.mjs         浏览器半:ModuleLoader 加载 / 槽位 / 工具卡渲染(图片、下载、
│   │                           打开、错误态)/ 设置页 / 主题 / 不碰密钥
│   ├── test-save.mjs           另存:引用恢复 / 越界拒绝 / 不覆盖 / 扩展名校验
│   └── test-mock-api.mjs       用插件自己的解析器验证 mock 服务
├── README.md
└── LICENSE                     MIT

为什么 entry id 必须是 image-tool:DSH 的 settings 命名空间由 loader entry 的 id 推导,写成包名 dsh-image-tool 会让 settings.update({ns:'image-tool'}) 永远抛 No configurable plugin entry "image-tool",于是"在设置里填 API"这条路直接断掉。


8. 开发与测试

npm test                     # 五套全跑(不需要 DSH、不联网、不花钱)
npm run test:generate        # 协议:生图 JSON / 改图 multipart / 回包解析 / SSRF 关卡 / 超时
npm run test:image-source    # 改图原图解析:路径越界 / 符号链接 / 魔数 / 最近一张 / mask
npm run test:plugin          # 插件契约:命名空间名 / volatile / secret / render / 确认闸门 / 配置 / 落盘
npm run test:client          # 浏览器半:ModuleLoader 加载 / 槽位注册 / 工具卡渲染 / 不写死颜色 / 不碰密钥

# 端到端(用本地假服务,仍然不花钱)
npm run mock:api             # 另开一个终端:起假服务(默认 18577,返回真实合法 PNG)
npm run test:mock            # 用插件自己的解析器验证 mock 的输出,再走一遍完整链路

scripts/mock-image-api.mjs 返回的是真正编码出来的 PNG(按请求的 size 生成正确尺寸), 不是随机字节 —— 否则附件服务的魔数嗅探与解码校验根本不会被触发,验收结论也就不可信。 它同时实现 /images/generations(JSON)与 /images/edits(真的解析 multipart), 后者用另一套色板,于是"改图分支到底有没有生效"可以靠字节差异直接验出来。 提示词里含 __fail__ 会返回 401、含 __nolink__ 会返回空 data,用于验证错误路径。

诊断日志:$TMPDIR/dsh-image-tool.log(可用 DSH_IMAGE_TOOL_DIAG 覆盖;设为空串则关闭)。 DSH 本身没有独立日志目录,所以插件「启动失败」的原因只能靠它 —— 这正是 apply 里 做能力探测的动机。

关于 mock 的宽容度

测试里的 mock 必须如实抛错。项目早期因为这个吃过亏:mock 里 ctx.get('webServer') 返回 undefined 而不是抛错,于是"直接读未声明依赖"这个写法 一路溜进真实环境,把整个插件变成「启动失败」——而使用者只看到插件卡上那一个词。

只有能抓住 bug 的测试才算测试。验证修法有效的方式:临时把代码改回错误写法 → 测试应当 FAIL → 还原 → PASS。


9. 已知限制

  • 可选服务必须用 ctx.inject,不能直接 ctx.get('x') / ctx.x。 这是本项目踩过的最贵的坑:访问未在 inject 里声明的服务会抛 cannot get property "webServer" without inject,而这行如果在 apply 里, 整个插件的 entry 就直接变成「启动失败」—— 使用者只看到插件卡上那一个词, 看不到任何原因。只有 Web profile 才有 webServer,所以它天然是可选依赖:

    // ❌ 抛错:webServer 不在 inject 里
    const webServer = ctx.get('webServer') ?? ctx.webServer
    // ✅ 可选依赖的正确写法
    ctx.inject(['connection', 'webServer'], routeCtx => { const ws = routeCtx.get('webServer') })
    

    inject = [...](模块导出)是硬依赖:缺了插件就不激活。 ctx.inject([...], cb) 才是"可用时才执行"。另外 apply 里每一步都独立 try/catch (见 lib/index.js 的 step())—— 工具挂不上不该拖垮路由,反之亦然。

  • 改代码要重启 DSH;改配置不用。 这两条是真的不一样:

    • 配置走 settings.describe(),是每次调用现读的运行时值(applies: 'live'),写入即生效;
    • 代码是模块,Node 会缓存已加载的 ESM(浏览器半同理,由外壳的模块加载器持有)。 只改配置文件不会重新加载 lib/*.js 或 client.js。
    • 另外每次 volatile 更新都会重新执行 apply,所以诊断日志里会看到多次 apply: —— 这是平台的正常行为,不是重复装载(loader entry 只有一条)。 ⚠️ 正因如此,工具与路由的注册必须经 ctx.effect:同一层里重名注册会直接失败, 症状是插件卡显示「启动失败」,而且第一次是好的、动过配置之后才坏。
  • 浏览器半依赖外壳的槽位与主题契约(settings.section、tool.call.toolview、 --dsw-alias-*、宿主提供 react)。这些是平台内部约定,跨大版本升级时可能变; npm run test:client 会在加载层先把最典型的几种破裂挡下来。 全屏浮层还需要 react-dom 的 createPortal:拿不到时降级成就地 fixed 渲染 (功能可用,但浮层可能被祖先容器的 transform / overflow 裁切),不会让卡片崩掉 —— 卡片渲染抛错会被宿主永久弃用,代价远大于少一个浮层。

  • 确认通道依赖 userQuestions:没有它的 composition 里,开启确认会拒绝生图并说明原因 (fail-closed:宁可画不出来,也不要在"说了要问却没问"的情况下花钱)。

  • URL 过滤不是完整的 DNS 出站隔离:当前检查 URL 主机名与 IP 字面量, 不做 DNS 解析校验;不可信上游的域名解析仍需由部署环境的网络策略限制。

  • /image 引用索引不是持久存储:配置更新导致重新挂载或进程重启后, 旧 fetchUrl 会返回 404。长期保存请用 includeData=1。

  • 另存的是附件里的版本:图片附件可能经过平台规范化(缩放/重编码), 自动落盘保存的才是上游原图。

  • 改图的原图只能来自会话或工作目录:不接受远程 URL(那等于让模型指挥服务端去请求 任意地址)。要改远程图,先把它下载进工作目录。 工作目录内的路径会做词法 + realpath 双重校验,符号链接/junction 指向盘外时会被拒绝。

  • 改图的 mask 只做软校验:OpenAI 约定 mask 必须是 PNG、与原图同尺寸、透明处为编辑区; 本插件在不是 PNG 时会给一句提示,但不会替你拦下 —— 不同中继的接受度差别很大, 硬拦反而挡住了能用的路径。另外原图与 mask 的尺寸一致性由上游判定, 这里不做像素级比对(那需要引入图像库)。

  • /edit 的 HTTP 调用必须自带 cwd:会话工作目录在 HTTP 路径上并不存在, 而"哪些路径允许读"必须有一个边界,插件不会拿进程当前目录去猜。

  • --set 的值里不能有前导 --:参数解析把 -- 开头当作下一个 flag。 值里真需要时用 --set key=--value 形式(带 = 的那种解析路径不受影响)。


10. 许可证

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit c7d11767a9de

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout