# 生图桥 dsh-image-bridge [仓库门面](../README.md) · [English manual](manual.en.md) 给**没有作图能力的模型**接上外部生图模型。在「设置 → 生图桥」配好至少一家提供商,模型就多出 `image_generate`(生图)与 `image_edit`(改图)两个工具,生成的图片直接显示在对话里。 - 可以配**多家**提供商,各有地址、密钥与模型。 - 生图与改图各选一个主用模型,各自还能配一个备用。 - 当前模型看不见图片也没关系——它照样能替你画出来。 ## 安装 **从 npm 装(推荐,一条命令):** ```sh dsh plugin --profile web add kkj-luna-dsh-image-bridge ``` **想跟最新提交**(npm 上还没发到那一版时):`dsh plugin --profile web add github:kkj-luna/dsh-image-bridge` 两种远程装法都会把插件行插入 profile 配置层(行 id `image-bridge-v4`,同时就是设置页写入的命名空间)。**装完重启 `dsh web`**,再打开「设置 → 生图桥」。 包名 `kkj-luna-dsh-image-bridge`,版本 `0.4.3`。名字带作者前缀是为了在 npm 上唯一(`dsh-image-bridge` 已被他人占用);设置页徽标只显示仓库名 `dsh-image-bridge`。 ### 开发本插件 改代码时用本地目录装:克隆到本机,然后用**仓库根目录**(注意是根目录,不是子目录): ```sh dsh plugin --profile web add <克隆下来的仓库绝对路径> ``` 它会写成 `link:` 依赖,指向你本地那份源码——**改完 `index.js` / `client.js` 重启 `dsh web` 就能看到效果**(插件代码不会被热重载;配置的改动是另一条通道,不受影响)。 两条注意: - **别移动或重命名那个目录**:链接指着原路径,移了就失效,得从新路径重新装一次。 - **别把本地源码和 npm 版本混装**:两种装法的包名相同,`dsh plugin add` 会报"已安装"。要切换先 `remove` 再 `add`。 改完想验证功能没坏,仓库根有六套离线验收(`npm test`,无需联网、无需真实密钥)。 ### 安装的三种情形 **首次安装。** 按上面一条命令装完、重启 `dsh web`,再打开「设置 → 生图桥」。此时列表里只有一张卡:名字「默认提供商」、带「现有配置」小标(旧单服务商配置合成的那张,地址 `https://api.openai.com/v1`、生图模型 `gpt-image-1`、尺寸 `1024x1024`,都来自 Host 的 Config schema 默认值,因为 `cordis.patch.yml` 刻意不写 `config`)。四个下拉是禁用的,下面一行写着「先添加并测试连接一个模型提供商」——**这是正常的新装状态,不是坏了**。最省事的走法就是编辑这张卡:填 API Key → 测试连接 → 保存 → 回「模型」区块选模型。 **装过、要更新。** npm 装的知识 npm 上的最新版本为准:重新 `dsh plugin --profile web add kkj-luna-dsh-image-bridge` 再重启 `dsh web` 即可,不必先卸载。本地目录装的(见上面「开发本插件」)则是**源目录里的文件改了就是最新的**——覆盖文件或 `git pull` 后重启就行。 两种情况都**不会丢配置**:用户配置存在 profile 的那一行上(行 id `image-bridge-v4`),不在插件目录里,换包名或覆盖文件都不影响它。反过来,本地目录装的时候**别移动或重命名那个目录**——链接指着原路径,移了就得从新路径重装一次。 **卸载。** 在插件管理器里停用或移除本 bundle。**配置与凭据都不会被自动清理**:那一行配置(提供商列表、模型路由、连接超时)随行一起消失,而密钥仍留在 `$DSH_HOME/.credentials.yaml`——这是有意的,见下面「三条安全约束」的第三条。想彻底清干净,就自己删掉那份文件里 `DSH_IMAGE_BRIDGE_*` 开头的行。 ## 你能做什么 ### 生图与改图 - **生图**:模型调用 `image_generate(prompt)`,图片直接落进对话,显示为图片卡片。 - **改图**:`image_edit` 按提示词修改一张已有图片——图片取自会话工作区里的文件路径,支持 PNG / JPEG / WebP / GIF,改图请求以 multipart 发出。 - **两个工具都能覆盖参数**:临时指定 `size`,或用 `model` 指定这一次用哪个模型(留空则按设置页的模型路由选)。 - **图片留痕**:除了对话里的图片卡片,还会另存一份文件到工作区(见「图片去哪了」)。 模型能传的参数只有这些,没有别的: | 工具 | 参数 | 必填 | 说明 | | --- | --- | --- | --- | | `image_generate` | `prompt` | 是 | 图片内容的描述,越具体越好 | | | `size` | 否 | 覆盖这一次的尺寸,留空用设置页的全局尺寸 | | `image_edit` | `prompt` | 是 | 要怎么改,例如「把背景换成夜晚的街道」 | | | `image_path` | 是 | 待修改图片的路径,工作区里的相对路径或绝对路径都行 | | | `size` | 否 | 输出尺寸;留空则用设置里配置的尺寸(默认 `1024x1024`),**总是会发出去**——不会沿用原图尺寸 | 两个工具都带 `model`(选填),用来覆盖这一次的模型选择。 ### 多服务商与模型路由 - **多家提供商**:A 家的模型生图、B 家的模型改图,也可以只配一家;每家有各自的地址、密钥、模型列表。 - **四个槽位**:生图与改图各有一个**主用**与一个**备用**,四者都能跨服务商自由搭配。 - **主用失败自动改备用**,并把换用原因如实说出来(见「什么时候换备用模型」)。 - **配置随时改、随时生效**:提供商、模型路由与连接超时都是每次调用重读,改完不必重启。 - 你没动过的时候也不会乱换:从没选过服务商时,用列表第一个是预期行为,不会弹警告。 ### 模型候选的呈现与筛选 - **候选来自**所有「已经保存过模型列表」的提供商的**并集**,按提供商分组显示,**模型名一个字都不改**。 - **默认只列生图模型**,判定刻意做得窄(见「生图模型怎么筛」);勾「显示全部模型」就能看到语言模型,或用「手动输入…」直接手填一个。 - **已选中的模型不会消失**:即使它看起来不像生图模型,也会补进选项里。 - 每个下拉末尾都有**「手动输入…」**(保留值,不写进配置)。 ### 凭据与安全 - **密钥不落配置**:每个提供商一把密钥,存在 DSH 凭据服务里,配置文件里只留凭据名 `DSH_IMAGE_BRIDGE_KEY_`,不写配置、不回传浏览器。 - **环境变量优先**:多服务商模式下每个提供商读**它自己**那个名字的环境变量(id `zhipu` → `DSH_IMAGE_BRIDGE_KEY_ZHIPU`),旧回退模式读 `DSH_IMAGE_BRIDGE_API_KEY`;凭据解析**不跨名字回退**。 - **没有显式密钥就不发请求**,「测试连接」也一样——不会拿别家的密钥去问这个地址。 - **服务商返回的图片地址要过关**(协议、环回、私网、体积都有限制),而你自己填的接口地址允许本机。 - 细节都在「三条安全约束」。 ### 诊断与排查 - 一份 **Host 启动自述**(`$DSH_HOME/dsh-image-bridge-startup.json`):`apply` 走到哪一步、七个依赖服务是否就位、生效配置、**当前实际在用哪个提供商**、失败原因;只写凭据名,没有密钥。 - 一份 **浏览器日志**(`$DSH_HOME/dsh-image-bridge-client.log`):制表符分隔的时间、级别、事件、明细,**绝不记录密钥**。 - 依赖解析失败时另有 `$DSH_HOME/dsh-image-bridge-degraded.json`。先看这三份文件,再看「排查」。 ## 配置 入口是「设置 → 生图桥」。页面从上到下是:页头 → 提供商卡片列表 → 虚线「+ 添加模型提供商」→「模型」区块 →「全局图片尺寸」。 ### 页头 `[图标] 生图桥 dsh-image-bridge v0.4.0`,下面一句副标题。徽标是 GitHub 仓库链接(新标签页打开),只显示仓库名、不带 `@local/` 前缀;图标随主题着色。 ### 图标有两个出处(别改错地方) | 出处 | 长什么样 | 颜色从哪来 | | --- | --- | --- | | `icon.svg`(随包分发) | **近黑底 + 渐变蓝图形**:近黑圆角方块 + 蓝色线条图形——插件列表/市场里显示的就是它 | 底色近黑 `#111318`(略偏蓝,以免在纯黑主题里浮起来),图形描边引用文件内的 `` 渐变 `#8ecdff → #4a9eff`。渐变最亮端必须够亮(验收有亮度下限断言),否则黑底上会糊成一块 | | `client.js` 的 `GLYPH_SHAPES`(内联) | 页头那一行的 22px 图形,以及设置页左侧导航里认领来的标记 | **只用 `currentColor`**:导航那一行是靠 CSS `mask-image` 上色的(mask 只读 alpha),所以内联图形**不许**出现任何硬编码颜色,否则深色主题下会变成一块不跟主题的色斑 | 两处的**几何数据必须逐条一致**(画框 + 太阳 + 桥面/拱/两个桥墩),验收里有跨文件断言盯着;`icon.svg` 里那块底色与渐变**不属于图形组**,导航遮罩只取 `` 里的图形——把底色也塞进内联图形会让导航渲染成一个实心方块。断言的范围也是这么划的:**颜色纪律只约束内联图形,`icon.svg` 允许有自己的配色**。 ### 提供商卡片 每张卡片占**一行**:状态灯 + 名称(下面一行更浅的小字写状态,失败时再多一行原因)+ 右侧三个按钮。 ``` ● 智谱 [测试] [编辑] [删除] 已连接 ● other [测试] [编辑] [删除] 连接失败 连接失败:401 密钥被拒绝 ``` | 元素 | 说明 | | --- | --- | | 状态灯 | 绿=这家能用(**两种来源**,见下);红=**本次会话**里测试/获取失败过;灰=没成功过也没测过(悬停有说明) | | 名称 | 建卡片时填的名字;留空时显示派生出来的 id | | 状态小字 | 与状态灯同源,**四档**:本次真测过 →「已连接」;绿灯但本次没测过 →「以前可用(已保存模型列表,本次未测试)」;红配「连接失败」;灰配「未测试」 | | 失败原因 | 只有失败时多这一行;**原因按 provider 归属**,A 家失败不会串到 B 家 | | 按钮 | 测试 / 编辑 / 删除(删除是两段式:先「删除」,再「确认删除」) | 刷新页面后绿灯依然成立:本次没有测试记录时,用「这家保存过模型列表」当**持久信号**——但它在界面上**明说自己是"以前可用"**,不再写成"已连接"。这是有意的:那个信号只证明曾经成功过,密钥可能早已被吊销,而界面并不会去重新验证;把两件事说成一句话就是**过承诺**。失败状态只留在内存里,不写进配置。接口地址、模型、密钥状态都收在「编辑」浮层里。 卡片上**没有**「设为当前」按钮:主用/备用已经按角色指定了服务商(`{ providerId, model }`),「当前生效服务商」对用户没有意义。 列表底部是虚线边框的「+ 添加模型提供商」。点它、或点卡片「编辑」,会打开一个居中浮层(半透明遮罩 + 面板,层级 `z-index: 1200`,压在宿主浮层之上)。**关闭有四条路**:右上角 ×、取消、点遮罩、Esc。打开时焦点会落进浮层、**Tab 不会跑到浮层背后**,关闭后回到原来那个元素;未保存的 Key 草稿也不会因为误按 Esc 丢掉(菜单展开时 Esc 先收菜单,不连带关浮层)。 ### 编辑浮层 浮层只管服务商本身,「用哪个模型」在下面的「模型」区块里选。 | 字段 / 按钮 | 说明 | | --- | --- | | 名称 | 显示名,也是 id 的派生来源 | | 接口地址 | OpenAI 兼容基础地址,例如 `https://api.openai.com/v1` | | API Key | 密码框,placeholder 随是否配过变化;密钥存在凭据服务里,不写配置、不回传浏览器 | | 测试连接 | `GET {接口地址}/models`,不产生生图费用;成功后顺手把这一家的模型列出来 | | 获取可用模型 | 单独再拉一次;列表里已有模型时文案变成「重新获取可用模型」 | | 模型列表 | 标题「可用模型(9 / 20)」+ 一行「已隐藏非生图模型: 11」+「显示全部模型」开关;这里只用于确认 Key 与查看模型,列表里没有选择按钮 | | 取消 / 保存 | 保存把名称、地址、已获取的模型列表写进 `providers`,密钥写进凭据服务 | **测试连接** —— `GET {接口地址}/models`,只判断可达性与密钥是否被接受,**不产生生图费用**。成功时 Host 顺手把模型列表带回来,界面直接列出来,不必再点第二次。404 算**可达**(这家可能不提供模型列表,不影响生图),401/403 才算密钥问题,500 等算**不可达**;200 但不是合法 JSON 或没有 `data` 数组**仍算可达**——可达性结论不受回包格式影响。 **获取可用模型** —— 同样 `GET {接口地址}/models`、`Authorization: Bearer`、**15 秒超时**,只取 `data[].id`。非 2xx、非 JSON、没有 `data` 数组都会报错并给出状态码。 ### 「模型」区块 四个下拉,按生图/修图分成两对,每对是**主用 + 备用**: ``` 生图模型 [主模型 ▾] 工具调用时优先使用。 [备用模型 ▾] 主模型连不上时自动改用(备用最多再等 30 秒)。 ``` - **备用两行没有可见标签**(下拉里已经写着「备用模型」),但各带等价的 `aria-label`,读屏仍能区分这四个控件。 - 下拉是**自绘菜单**:点外关闭、Esc、上下键 + 回车、Home/End,关闭后焦点回触发器;带 `role=listbox` / `role=option` 与 `aria-activedescendant`;面板有最大高度,键盘移动会滚入视野。 - **选项是所有已获取过模型列表的服务商的并集**,按服务商分组(分组标题形如 `— OTHER —`),**模型名一个字都不改**。每项同时带 `{ providerId, model }`,所以生图可以走 A 家、备用走 B 家。 - 每个下拉末尾固定「**手动输入…**」(保留值 `__dib_manual__`,**不写进配置**);明确选中它才露出文本框,**手填时走当前生效服务商**。 - **空状态**(没有任何服务商提供过模型列表):四个下拉禁用并显示各自的占位,**不会**把四个文本框一起展开,只给一行引导「先添加并测试连接一个模型提供商」。 - 区块里还有「**连接超时(秒)**」输入框(默认 15,用户填秒、代码内部换算毫秒),含义见「连接超时(秒)与调用超时」。 - 改完点**「保存模型设置」**落盘。 ### 全局图片尺寸 | 选项 | 说明 | | --- | --- | | `1024x1024` | 默认,1:1 | | `1024x1536` / `1536x1024` | 2:3 竖版 / 3:2 横版 | | `768x1024` / `1024x768` | 3:4 竖版 / 4:3 横版 | | `512x512` | 1:1 小图 | | `1792x1024` / `1024x1792` | 16:9 横版 / 9:16 竖版 | | 自定义… | 保留值 `__dib_custom_size__`,选中才露出文本框 | 对所有提供商生效,用自己的「保存尺寸」按钮单独保存。**当前值一定出现在选项里**:配置里存着非预设值(例如手填的 `1280*720`)时,下拉自动进入自定义模式并回显原值,不会显示空白、也不会被悄悄改成别的值。比例只写在选项文案里,真正发出去的值始终是尺寸本身。 ### 生图模型怎么筛 默认只列生图模型(实测一把 Key 有约 200 个模型,绝大多数是语言/向量/语音模型)。规则**刻意做得窄**: 1. 把模型名按非字母数字切成 token,**逐 token 比对**——绝不做整名子串匹配,否则 `qwen-mt-uni`、`qwen3.7-text-embedding-flash` 会成片误判; 2. token 含 `image` 判为生图; 3. 再认一组专有词:`t2i`、`i2i`、`flux`、`kolors`、`dall`/`dalle`、`seedream`、`cogview`、`diffusion`、`sdxl`、`sd3`、`wanx`、`midjourney`、`imagen`(其中一部分接受版本号后缀,如 `flux1`、`cogview4`)。 同一套判定也决定四个下拉的选项。勾上「显示全部模型」就能看到语言模型,或者用「手动输入…」手填。 ### 凭据名与升级兼容 每个提供商有自己的凭据名: ``` DSH_IMAGE_BRIDGE_KEY_ ID = provider.id 转大写、非字母数字一律换成下划线 ``` `provider.id` 由名称派生:小写、非 `[a-z0-9]` 折叠成 `-`、去首尾 `-`、截断 40 字符;纯中文名折叠为空时改用接口地址的主机名,再不行用 `provider`(同名再追加 `-2`、`-3`…)。**Host 与客户端用同一套规则**——两端归一结果只要有一处不同,就会出现「界面按 A 名报密钥未配置,工具却去 B 名取密钥」。服务商记录里的 `keyRef` 显式填了合法名字时优先用它,留空才按 id 推导。 升级兼容:`providers` 为空而旧 `baseUrl`/`model`(或 `editModel`)有值时,设置页会把旧配置合成一张卡片——名字「默认提供商」、带「现有配置」小标、id `default`,凭据名沿用 `DSH_IMAGE_BRIDGE_API_KEY`——工具也仍然走旧字段,**老的密钥继续有效**。所以从单提供商版本升上来的用户不会看到"配置丢了"。 ### 目录结构 **v0.3.0 起,仓库根目录就是插件包**——这是"一条命令安装"(`dsh plugin add github:kkj-luna/dsh-image-bridge`)的前提:pnpm 取 git 依赖时只会打仓库根那个包,没法指定子目录。 ```text dsh-image-bridge/ ← 仓库根 = 插件包根 ├── package.json 清单:bundle patch、dsh.client、图标、显示元数据、files 白名单 ├── cordis.patch.yml 挂载声明:行 id(= 设置页写入的命名空间)与包名;刻意不写 config ├── index.js Host 半:两个工具、多提供商解析、附件与落盘、test/models/log 通道、启动自述与降级诊断 ├── client.js 客户端半:自定义工具卡片 + 设置页(页头、卡片、浮层、模型区块、尺寸) ├── locale/{zh,en}.json 插件卡片上的显示名与简介 ├── icon.svg 插件图标(近黑底 + 渐变蓝图形,随包分发;几何数据与内联图标同源) ├── README.md 仓库门面(GitHub 首页 / npm 页) ├── docs/manual.md 本文件(完整手册);README.en.md 与 manual.en.md 是对应英文版 ├── docs/marketplace.md 上架插件市场的条目文件与 PR 步骤 ├── CHANGELOG.md 变更记录 ├── LICENSE MIT ├── .npmignore 打包排除项(见下) └── .dib-verify/ 测试与验收套件(随仓库走,**不进包**) ``` 不在这份清单里的文件属于仓库而不是包本身:`.dib-verify/`(验收套件)、`docs/*.png`(截图与 logo)、`.ref-*.tmp*`(参考 README 的本地缓存)都由 `.npmignore` 挡在包外。装到用户机器上的只有跑起来需要的东西。 ## 使用流程 1. 点「+ 添加模型提供商」,填名称、接口地址、API Key。 2. 点**「测试连接」**(或「获取可用模型」)。 3. 点浮层的**「保存」**。 4. 回设置页在「模型」区块选生图/改图的主用与备用模型,点「保存模型设置」。 5. 让模型画图。 > **最容易困惑的一点**:下拉的候选来自 provider 记录里**已经保存**的模型列表。所以顺序是 > **「测试连接 / 获取可用模型」→ 点「保存」→ 主页下拉才出现这一家的模型**。 > 只测试、没保存就关掉浮层,这些模型**不会**进入候选。 ## 行为细节 ### 当前生效的是哪一个 三级回退,工具调用用的是**同一套规则**: | 级别 | 条件 | 生效对象 | | --- | --- | --- | | `active` | `activeProviderId` 命中 `providers` 里的某一项 | 那一项 | | `first` | 没命中(为空,或指向已被改名/删除的条目)而列表非空 | `providers[0]` | | `legacy` | `providers` 为空 | 旧的 `baseUrl`/`model`/`editModel`,凭据名 `DSH_IMAGE_BRIDGE_API_KEY` | 界面上不再有切换入口,但字段照旧保存与回退。服务商没填接口地址时直接报错,并指出是哪一家。 **二级回退会明确告诉你。** `activeProviderId` 指向的那家被改名或删除后,工具照常出图(用的是列表第一个),但结果里会多一条警告,点名"原本选的是哪个、这次实际用的是哪个"。这是**计费相关**的事实:你以为在用 A 家,请求其实打到了 B 家、走 B 家的密钥与额度,不该只有启动自述里的 `level: first` 知道。反过来,如果你**从没选过**(字段为空),用列表第一个本就是预期行为,不会有这条警告。 ### 什么时候换备用模型 模型解析顺序:工具调用里显式给的 `model` 参数 → 「模型」区块配的**主用模型**(用它自己 providerId 的地址与凭据)→ 当前生效服务商的 `imageModel`/`editModel` → 旧的 `baseUrl`/`model`。备用**只来自**「模型」区块配的备用模型;没配、或它指向的服务商不可用,就等于没有备用。 **三种情况换备用**(然后只试备用这一次): 1. **连不上**:DNS / TCP / TLS / 代理在连接超时内没把请求发出去(连接阶段超时),或网络层直接报错; 2. **非 2xx**,含回包不是合法 JSON; 3. **回包 200 却解析不出任何可用图片**——从用户视角这就是这次没出图。 **两种不换**: - **用户主动取消**(`exec.signal` 已中止)——取消不是失败; - **图片已产出、只是下载失败**——已经计费,再生成一次就是重复计费。 **不再按响应时间换备用。** 早先那套"请求发出后多久没拿到响应头就换人"的预算(`fallbackAfterMs`)已整体删除:同步生图本来就要几十秒(实测 42–61 秒才回响应头),用"快慢"判断服务商行不行既做不准,也会把正常的慢生成误杀。请求一旦发出就不再计时,之后的等待由整次调用的上限兜底。 备用的结果如实说出:成功时提示 `生图「主」未能成功(原因),已改用备用模型「备」`;两次都失败时同时给出**两次尝试各自的模型名与原因**。**两家都连不上时,错误以「模型连接失败」开头**,并提示去「设置 → 生图桥」检查地址,或点「测试连接」确认可达性。 ### 连接超时(秒)与调用超时 设置页上只有一个时间设置:「**连接超时(秒)**」,默认 **15**。用户填**秒**,代码内部自动换算毫秒。它只衡量一件事:**请求有没有发出去**——DNS 解析、TCP 连接、TLS 握手、代理。超过它还没发出去就算这家连不上,**改用备用模型**;报错时也会写清"连接阶段超过了多少毫秒仍未把请求发出去"。 - **备用模型的连接上限固定 30 秒**,不是设置项:备用是最后一条路,预算不该再让用户调参。 - **请求一旦发出就不再计时**:服务商憋多久都照等,由**整次调用的上限**兜底。所以「地址填错/对端下线」最多让你等 15 秒就换人,而正常的慢生成不会被误判。 - 两次尝试各有自己的连接预算:主用用你填的秒数,备用用固定的 30 秒。 - 两家都连不上时,错误里写的是「**模型连接失败**」+ 两次尝试各自的原因,而不是一句泛泛的失败。 - 已经发出去过的请求**不会重发**:非幂等的 POST 重发一次,服务商可能已经生成并计费两次。 **整次调用的上限**(`timeoutMs`,默认 **180000** 毫秒)与连接超时是两件事:它管的是"这一次工具调用总共允许多久",超过就中止,错误里写明超了多少毫秒。这个值**不在设置页上**:它和 `saveToWorkspace`、`outputDir` 一样属于"只在配置里"的字段。它和连接超时一样**每次调用重读**——旧版把它当成挂载时的快照,改了没反应,现在改完即时生效。 ### 三条安全约束(有意为之) 1. **没有显式密钥就不发请求。** 「测试连接 / 获取可用模型」只认请求里显式给的 `apiKey`,或合法 `keyRef` 指向的凭据;两者都没有时 Host **一个字节都不发**,绝不回退到当前生效服务商的密钥——否则会把 A 的密钥发到 B 的地址上。地址同理**不回退**:只填了密钥没填地址时直接报错,不会拿别家的地址配上你这把密钥。工具调用同理,密钥只在它自己的 ref 下解析,**刻意不跨名字回退**。 2. **服务商返回的图片地址要过关。** 有些接口不是回图片数据,而是回一个让客户端去下载的 URL。那个地址是**对方给的**,所以只允许 `http`/`https`,并拒绝环回与私有网段(云元数据 `169.254.169.254`、`10.x`、`192.168.x` 等)——否则一个被控制的服务商就能借你的进程去探内网。回包正文也有上限(32 MiB),下载的图片同样限时限长(15 秒、20 MiB)。注意这**只约束对方给的地址**:你自己在设置页填的接口地址允许本机(本地生图服务、本地 mock 都是正当用法),只要协议是 http/https。 3. **删除提供商不清理凭据。** 删除是配置操作,不该顺手毁掉一份可能要复用的密钥。确实想清掉,就自己从 `$DSH_HOME/.credentials.yaml` 删掉那一行。 ### 图片去哪了 1. 提交为**持久图片附件**——对话里以图片卡片显示。 2. 另存 `generated-images/image-<时间戳>-<提示词片段>.png`。目录可以用 `outputDir` 改,但解析结果必须落在会话工作区内,否则拒绝写入。这一步能不能跳过由 `saveToWorkspace` 决定(默认开)。 `outputDir` 与 `saveToWorkspace` 同样**不在设置页上**,只在配置里(profile 那一行的 `config`,见「目录结构」里 `cordis.patch.yml` 的说明)。 ### 生成中会显示等了多久 生图/改图进行中,卡片上那句「正在生成图片…」/「正在修改图片…」后面会跟一个**每秒递增的秒数**(例如 `正在生成图片… 12s`,`edit` 同理),生成结束即消失。首帧就是 `0s`(不会先空一秒),语言包里的键是 `running.elapsed`(zh / en 同格式 `{seconds}s`)。这段占位是**用户唯一能看到本卡片内容**的时机——对话稳定后这张卡片会被宿主折叠/隐藏(见下)。 ### 模型看不见图片时 按**当前调用路由是否声明图片输入能力**分叉: - 声明了 `image` 输入 → 图片作为 image 内容块随工具结果下发,模型直接看到; - 没声明 → 只下发文本信封与文件路径(否则 DSH 适配器会对不支持图片输入的路由抛 `UNSUPPORTED_CONTENT`)。 **两种情况界面上的图片卡片都会显示**——它读的是工具结果里持久化的图片引用,与模型视觉能力无关。 ## 排查 先看三份自诊断,几乎所有的失败都能在这里读到原因: | 文件 | 内容 | | --- | --- | | `$DSH_HOME/dsh-image-bridge-startup.json` | Host 每次激活覆写:`steps`、`services`、`config`、`configSource`、**当前实际在用哪个服务商**(`provider`)、失败原因。只写凭据名,没有密钥 | | `$DSH_HOME/dsh-image-bridge-client.log` | 浏览器侧制表符分隔日志(时间、级别、事件、明细),**绝不记录密钥** | | `$DSH_HOME/dsh-image-bridge-degraded.json` | 仅在依赖解析失败时出现 | `provider.level` 告诉你三级回退**实际落在哪一级**(`active` / `first` / `legacy`),`provider.requested` 是你当初选的那个 id。只看配置列表分不清「配了」和「真的在用」,这两个字段就是为了回答后者。 `steps` 是 `apply` 走过的主线,按顺序读:正常时是「Config schema 就绪 → 两个工具已注册 → 测试通道已挂载」;工具那一步缺失时,下一行会写真因(例如 `dsh-tools 未解析:两个工具未注册`)与修复建议——**依赖缺失时不注册工具,也绝不静默降级**:参数编译不出 JSON Schema 就不给工具,让你从自述里看到这件事,而不是给你一个参数静默失效的工具。 `services` 是七项依赖是否就位,缺哪项就有哪项的症状: | 键 | 为 true 时可用 | 为 false 时会怎样 | | --- | --- | --- | | `tools` | 注册那两个工具 | 一个工具都没有,「设置」里其它部分照常 | | `attachments` | 图片作为持久附件进对话 | 出图后无法提交附件,卡片不出现 | | `fs` | 把图片另存进工作区、读改图的原图 | 落盘与改图取图失败 | | `credentials` | 存取 API Key | 密钥读不到 → 报「没有可用的 API Key」 | | `llm` | 判断当前路由有没有视觉能力 | 退化成只下发文本信封 | | `webServer` / `connection` | 设置页的「测试连接 / 获取可用模型 / 日志」通道 | 页面能打开,但一按按钮就报通道失败 | `services` 全是 false 通常说明这份自述来自**验收桩环境**(或插件被单独 apply 过一次),不是真实会话里跑出来的——真实会话里 `tools`、`fs`、`credentials` 这些本该是 true。 | 症状 | 怎么办 | | --- | --- | | 「没有可用的 API Key」 | 这张卡片没填、也没存过密钥,**请求压根没发**;与「密钥被拒绝(401/403)」是两回事。填好 Key 点「保存」再试 | | 401/403 密钥被拒绝 | 密钥不对,或该服务不接受 `Authorization: Bearer`。注意**环境变量优先于凭据服务**:多服务商模式下每个服务商读**它自己**那个名字的环境变量(如 id `zhipu` → `DSH_IMAGE_BRIDGE_KEY_ZHIPU`),旧回退模式是 `DSH_IMAGE_BRIDGE_API_KEY` | | 报 404 | 通常是这家不提供 `/models`,不影响生图;若生图本身 404,说明接口路径不是 OpenAI 兼容形态 | | 四个下拉是空的(禁用) | 还没保存过模型列表:测试连接 / 获取可用模型 → **保存**。注意「手动输入…」是下拉**内部**的条目,下拉禁用时够不到它——先把列表存上再选它 | | 有候选但看不到我要的模型 | 默认只显示生图模型,勾「显示全部模型」或「手动输入…」;刚测到的新模型要先回浮层点保存;也可能属于另一家服务商,展开对应分组找 | | 设置里没有「生图桥」 | 查插件是否启用;看客户端日志里 `client apply` 是否 fatal;看有没有 degraded 文件 | | **装完/改了代码没生效** | **必须重启 `dsh web`**:Node 会缓存已加载的模块,热重载默认忽略 `node_modules`,所以改 `index.js` 之后光刷新页面没用;`client.js` 同理。**配置**是另一条通道(设置页保存走热更新),不受影响。重启前先确认 `plugin_manager` 那边确实是启用状态 | | 改了 `timeoutMs` / `connectTimeoutSeconds` 没反应 | 两者都是每次调用重读,保存后立刻生效;若毫无变化,确认改的是**这个 profile** 的那一行 `config`(设置页与 profile 写的是同一行,id `image-bridge-v4`),并看启动自述里的 `config` 是不是你改后的值 | | 主模型连不上、备用也连不上 | 错误会写「模型连接失败」并列出两次尝试各自的原因。先点「测试连接」确认这两家的接口地址是否可达;连接超时以秒计(默认 15 秒,备用固定 30 秒),跨境冷连接通常几百毫秒,超时说明地址/网络有问题 | | 「模型路由指向的服务商「X」不在服务商列表里」 | 你选过的那个服务商**改过名字或已被删除**(路由里存的是它的旧 id)。**一个请求都没发**。去「模型」区块把这一槽位重新选一次即可。这条刻意做成硬报错:早先的版本会静默改用"当前生效服务商"的模型名去问错的地址,换回一句 `404 Model not exist`,真正的错因完全看不出来 | | 选了模型但生图报错 | 看错误里点名的是主模型还是备用:只提一个 → 备用没生效(没配,或指向的服务商缺地址/缺密钥);提了两个 → 两次都试过,按两段原因分别查;提示「已改用备用模型」→ 这次出图的是备用 | ## 实现要点 给维护者,一行一条: | 关注点 | 做法 | | --- | --- | | 依赖解析 | profile 里是链接,Node 按真实路径解析裸模块名;按「普通解析 → CLI 入口位置 → `DSH_PROFILE_DIR` → `DSH_HOME` → cwd」逐个基准找 dsh 的 `node_modules`,全失败则优雅降级并写出自诊断 | | 工具定义 | 优先 `defineTool`(带参数校验),解析不到时退化为恒等函数 | | 图片值结构 | 与内置 `read_image` 完全一致 | | 界面与模型分叉 | `output.render` 决定模型看到什么(按视觉能力分叉),`output.presentationMeta` 决定界面看到什么 | | 文件写入边界 | 用 `node:fs`(`ctx.fs` 没有二进制写入),但写入前强制校验目标在会话工作区内 | | 设置页读写 | `remote.settings.update` 要求字段标 `volatile()`;`remote.credentials.set` 的 ref 必须匹配 `^[A-Za-z_][A-Za-z0-9_]*$` | | 归一责任两端一致 | `provider.id` 的 slug 化两端各做一次且规则逐字一致;凭据名由归一后的 id 派生 | | 凭据解析不跨名字 | 只在自己那个 ref 下解析(凭据服务 → 同名环境变量),RPC 只认 payload 里的 `apiKey` 或合法 `keyRef` | | 三级回退只有一套 | `activeProviderId` 命中 → `providers[0]` → 旧字段,Host 与界面用同一规则 | | 模型列表 RPC | `GET {baseUrl}/models`、15 秒超时、只取 `data[].id`;它只是设置页的辅助接口,不能让不响应的服务商把设置页卡住 | | 浮层实现 | 没有 primitives 就没有 portal,用 `position:fixed` 就地铺满视口,`z-index` 1200 稳压宿主浮层(Modal/灯箱 1000、Toast/Tooltip 1100、菜单 100/101);四条关闭路径刻意不受 `disabled` 约束 | | 生图模型筛选 | 按 token 判定,不做整名子串匹配;当前已选中的模型即使不像生图也补进选项,避免选择「消失」 | | 模型选择只有主页一处状态 | 四个下拉与 `modelRoutes` 一一对应,`