@zhz8888/dsh-radeon-cloud-patcher 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,585 +1,182 @@
1
1
  # Radeon Cloud Patcher for DeepSeek Harness
2
2
 
3
- 把 [AMD Radeon Cloud](https://amd-aim.github.io/radeon-cloud-docs/) 接入 DeepSeek Harness 的模型选择器,**完整支持模型思考(reasoning)功能**。
3
+ 把 [AMD Radeon Cloud](https://amd-aim.github.io/radeon-cloud-docs/) 接入 DeepSeek Harness 的模型选择器,完整支持模型思考(reasoning)。
4
4
 
5
- 包名 `@zhz8888/dsh-radeon-cloud-patcher`。provider 定义由插件**自带的补丁层**声明,
6
- 由 DSH 自带的 `dsh-llm-pi-ai`(通用 OpenAI 兼容 provider)承载——**不修改 DSH 本体,
7
- 不需要自研适配器,也不往你的 profile 里写任何东西**。
5
+ 包名 `@zhz8888/dsh-radeon-cloud-patcher`。provider 定义由插件**自带的补丁层**声明,传输、思考字段解析、多轮回传与用量计量全部由 DSH 自带的 `dsh-llm-pi-ai` 承担——**不改 DSH 本体,不写你的 profile**。
8
6
 
9
- > 之所以叫 patcher,是因为它靠**补丁层**交付:定义写在插件自己的
10
- > `cordis.plugin.patch.yml` 里。这样做的直接后果有三个,也正是这个设计的目的——
11
- >
12
- > | 现象 | 原因 |
13
- > | --- | --- |
14
- > | 设置页**没有**「删除」按钮 | DSH 只在「该 provider 路径存在于用户层、且不存在于 base 层」时渲染它;定义在插件的 bundle 层,base 层永远有它 |
15
- > | 卸载插件 = 删除供应商 | 定义只活在插件里,profile 中不留副本 |
16
- > | 想删供应商只能卸载插件 | 同上;设置页里也点不到删除 |
7
+ 由此带来三个直接后果:
8
+
9
+ - 设置页那一行**没有**「删除」按钮;
10
+ - **卸载插件 = 删除供应商**,profile 中不留副本;
11
+ - 想删供应商只能卸载插件。
17
12
 
18
13
  ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
19
14
  ![target: DSH](https://img.shields.io/badge/DSH-0.2.x-blue?label=target)
20
- ![Radeon Cloud](https://img.shields.io/badge/API-Radeon%20Cloud-orange)
21
-
22
- ---
23
15
 
24
16
  ## 快速上手
25
17
 
26
- 装好 DSH、手上有 Radeon Cloud 的 API 密钥(`rc-…`),三步就能用起来,
27
- **不需要编辑任何配置文件**:
18
+ 装好 DSH、手上有 Radeon Cloud 的 API 密钥(`rc-…`),三步即可,不需要编辑任何配置文件。
28
19
 
29
- 1. **装插件** —— 桌面版在左侧「插件」页里添加 `@zhz8888/dsh-radeon-cloud-patcher`;
30
- 命令行执行
20
+ 1. **装插件** —— 桌面版在左侧「插件」页添加 `@zhz8888/dsh-radeon-cloud-patcher`;命令行执行
31
21
  `dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher`。
32
- 装不上(pnpm 报 404)就改用[安装](#安装)里的方式三,从源码装同一个包。
33
- 装好后插件列表里显示的条目名是 **Radeon Cloud**(中文界面为「Radeon Cloud 提供方」),
34
- 而不是上面那串包名——名称取自包内的 `locale/`,见[项目结构](#项目结构)。
35
- 2. **重启 DSH** —— 定义写在补丁层的 config 行里,插件管理器对这类变更只会给出
36
- `restart-required`,不会在运行中生效。重启后 provider 就位,没有其它后置步骤。
37
- 3. **填密钥** —— 左下角「账号菜单 → 设置 → 模型」,找到 **Radeon Cloud** 一行,
38
- 点「编辑」,在「API 密钥」里粘贴密钥,点「保存」。
39
-
40
- 装好之后:那一行右侧出现**绿点**(悬停显示「API 密钥已配置」);回到对话,点输入框下方的
41
- 模型选择器,Radeon 的 10 个模型会出现在列表里;选中模型后再选「推理等级」就是思考档位。
42
-
43
- **怎么确认真的装好了** —— 三个现象同时成立,说明插件与它的补丁层都到位:
44
-
45
- | 现象 | 在哪看 |
46
- | --- | --- |
47
- | 模型选择器里出现 Radeon Cloud 的模型 | 输入框下方的模型选择器 |
48
- | 那一行**没有**「删除」按钮 | 设置 → 模型 → Radeon Cloud 行 |
49
- | 那一行**不带**「自定义」标签 | 同上 |
50
-
51
- **没生效怎么办** —— 插件不会静默消失:定义失效时它**直接让启动失败并写明原因**
52
- (profile 里已有同 id 定义、profile 的 `llm-pi-ai` 配置把插件层整份盖掉……),
53
- 逐条处置见[归属与删除](#归属与删除)。想手工核验真正生效的配置,用
54
- `dsh --profile <profile> --dump-config`(`desktop` profile 例外,它由桌面应用独占管理)。
55
-
56
- > 更细的安装方式(npm 源、镜像、GitHub、本地 tarball、手工清单)见[安装](#安装);
57
- > 密钥、模型、档位、图片输入与升级卸载见[使用](#使用)。
58
-
59
- ---
60
-
61
- ## 目录
62
-
63
- - [快速上手](#快速上手)
64
- - [功能](#功能)
65
- - [环境要求](#环境要求)
66
- - [安装](#安装)
67
- - [使用](#使用)
68
- - [归属与删除](#归属与删除)
69
- - [项目结构](#项目结构)
70
- - [常用命令](#常用命令)
71
- - [可用模型](#可用模型)
72
- - [已知限制](#已知限制)
73
- - [兼容性](#兼容性)
74
- - [许可证](#许可证)
75
-
76
- ---
77
-
78
- ## 功能
79
-
80
- ### 规范化模型返回文本的结构
81
-
82
- Radeon 的响应体与流式分片都存在一个容易致错的形态:思考内容的字段名是 `reasoning`,
83
- 但同一份数据里还带一个 `reasoning_content` 占位,且占位值是 `null` 而不是空字符串。
84
- 按字段名直读会取到 `null`,思考内容静默丢失,**全程不产生任何报错**。
22
+ 插件列表里显示的条目名是 **Radeon Cloud**(中文界面「Radeon Cloud 提供方」),不是那串包名。
23
+ 2. **重启 DSH** —— 定义在补丁层的 config 行里,运行中不生效;重启后即就位,没有其它后置步骤。
24
+ 3. **填密钥** —— 左下角「账号菜单 → 设置 → 模型」→ **Radeon Cloud** 行 →「编辑」→ 粘贴密钥 →「保存」。
85
25
 
86
- 本配置接的是 DSH 既有的字段选择逻辑——取第一个「值为非空字符串」的思考字段。
87
- `null` 的类型不是字符串,于是被自然跳过,正确落到 `reasoning`。经此规范化:
26
+ 装好的三个现象同时成立:模型选择器里出现 Radeon 的模型;那一行右侧是**绿点**(悬停「API 密钥已配置」);那一行**没有**「删除」按钮、也不带「自定义」标签。
88
27
 
89
- | 源字段 | 规范化后的结构 |
90
- | --- | --- |
91
- | `delta.content` / `message.content` | 回答 → `text` 内容块 |
92
- | `delta.reasoning` / `message.reasoning` | 思考 → `reasoning` 内容块 |
93
- | `delta.reasoning_content: null` | 跳过,不产生空块 |
94
-
95
- 思考与回答成为并列的两类内容块,在界面上分区呈现,而不是把思考混进正文。
96
-
97
- ### 桥接模型思考方式回传
98
-
99
- 思考不只是「取回来」,还必须能「送回去」——多轮对话中 DSH 会把上一轮的助手消息
100
- 连同思考内容一起回传,否则多轮上下文会断裂。
101
-
102
- - **流式产出**:思考以 `reasoning-delta` 分片实时送达界面,不必等回答生成完毕;
103
- - **对称回传**:回放时使用**读取时所用的同一个字段名**(助手消息上的 `reasoning`),
104
- 而非另换一个字段名,避免读取与回传不对称导致多轮请求被拒;
105
- - **强度可选**:思考强度按模型逐个声明,而非全局统一。接受 `none` 的模型可关闭思考,
106
- 不接受的模型则不提供该档位,而非提供一个会报错的选项;
107
- - **用量归集**:思考 token 计入 `reasoning_tokens`,与回答 token 一并进入用量统计。
108
-
109
- 实测确认:Radeon 的 `usage` 在顶层与 `completion_tokens_details` 两处都回报
110
- `reasoning_tokens` 且数值一致,DSH 读取后者即可正常计量。
111
-
112
- ### 其他
113
-
114
- - 10 个对话模型全部可选,思考档位按模型独立声明
115
- - 视觉模型支持图片输入
116
- - 限流退避遵循服务端 `Retry-After`
117
- - 附带离线校验、回归测试与档位实测工具
118
-
119
- ---
120
-
121
- ## 环境要求
122
-
123
- | 项 | 要求 | 说明 |
124
- | --- | --- | --- |
125
- | DSH | `>=0.2.0-rc.1`(含预发布,无上界) | 由 `peerDependencies` 声明范围,低于下界会被安装器拒绝;上界不写,DSH 升大版本不需要插件跟着重发(见[版本闸门](#版本闸门)) |
126
- | Node.js | `>=22` | 与 `package.json` 的 `engines.node` 一致;仅运行 `scripts/` 与 `test/` 下的脚本时需要 |
127
- | bash | 任意 | 仅 `scripts/radeon-api.sh` 需要 |
128
- | 操作系统 | macOS | `scripts/validate-config.mjs` 按 DSH 桌面版应用包的绝对路径加载校验用的 schema,路径为 `/Applications/DSH Desktop.app/Contents/Resources/app/node_modules`。其余脚本与系统无关 |
129
-
130
- > 配置文件本身(`provider/radeon-cloud.yml`)是纯 YAML,不依赖上述任何运行时。
131
-
132
- ---
28
+ **没生效怎么办** —— 插件不会静默消失:定义失效时它直接让启动失败并写明原因,逐条处置见[归属与删除](#归属与删除)。想核验真正生效的配置,用 `dsh --profile <profile> --dump-config`(`desktop` profile 例外,由桌面应用独占管理)。
133
29
 
134
30
  ## 安装
135
31
 
136
- 四种方式,任选其一。**装完都要重启 DSH**(原因见[快速上手](#快速上手)第 2 步):
137
- 定义由插件自带的补丁层声明,不写入你的 profile,重启后即生效,没有其它后置步骤。
138
-
139
- ### 方式一:DSH 插件管理器(推荐)
140
-
141
- 桌面版:应用左侧「插件」页 → 「添加插件」→ 输入包名或本地目录路径。
142
-
143
- 命令行:`dsh plugin --profile <profile> …` 会把参数**原样转发**给 profile 目录里的 pnpm,
144
- 因此 pnpm 支持的写法它都支持:
32
+ 用 DSH 插件命令从 npm 安装本包:
145
33
 
146
34
  ```bash
147
35
  dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher
148
36
  dsh plugin --profile <profile> remove @zhz8888/dsh-radeon-cloud-patcher # 卸载
149
37
  ```
150
38
 
151
- 装完 `dsh.profile.bundles` 会自动多出本包名——插件管理器按各包的 `dsh.bundle` 声明维护它,
152
- 不必手改 profile 清单。
153
-
154
- > **桌面版的 `desktop` profile 由应用独占管理**:在普通终端里执行
155
- > `dsh plugin --profile desktop …` 会被拒绝(`profile "desktop" is managed exclusively by
156
- > the Electron application`)。两条正路:用应用内的插件页安装,或在应用右上角
157
- > 「打开 DSH 终端」里执行同样的命令。`web` / `tui` / `headless` 等 profile 不受此限。
158
- >
159
- > 若你的源上还没有这个包(pnpm 报 404),用方式三装同一个包。
160
-
161
- ### 方式二:从 npm 源安装
162
-
163
- 方式一走的就是 npm registry;换源只需给 pnpm 传参,或写进 profile 自己的 `.npmrc`:
39
+ 命令行参数会**原样转发**给 profile 目录里的 pnpm,因此换源、指定版本按 pnpm 语法写即可:
164
40
 
165
41
  ```bash
166
- # 单次指定镜像源
167
- dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher \
168
- --registry https://registry.npmmirror.com
169
-
170
- # 或给这个 profile 固定源(.npmrc 是 pnpm 自己的配置文件)
171
- printf 'registry=https://registry.npmmirror.com\n' >> ~/.dsh/profiles/<profile>/.npmrc
172
-
173
- # 指定版本或范围(pnpm 语法)
42
+ dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher --registry https://registry.npmmirror.com
174
43
  dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher@<版本>
175
44
  ```
176
45
 
177
- > 本包的 tarball 里只有插件自己的源码、定义与脚本(不含 `test/` 与 `docs/`),
178
- > **没有安装期构建脚本**,因此换源、离线镜像、内网私服都不会碰到 pnpm 的
179
- > `allowBuilds` 授权那一步。
180
-
181
- ### 方式三:从源码 / GitHub 安装
182
-
183
- 三种源码形态都直接可用,原因同上:本项目零运行时依赖、没有构建步骤。
184
-
185
- ```bash
186
- git clone https://github.com/zhz8888/dsh-radeon-cloud-patcher.git
187
-
188
- # ① 本地目录:pnpm 做 link,仓库里改了代码重启 DSH 即生效(开发本插件时最省事)
189
- dsh plugin --profile <profile> add /绝对路径/dsh-radeon-cloud-patcher
190
-
191
- # ② git 依赖:直接装公开仓库
192
- dsh plugin --profile <profile> add github:zhz8888/dsh-radeon-cloud-patcher
193
-
194
- # ③ 本地 tarball:适合内网分发(npm pack 在仓库目录里执行)
195
- npm pack
196
- dsh plugin --profile <profile> add ./zhz8888-dsh-radeon-cloud-patcher-<版本>.tgz
197
- ```
198
-
199
- > DSH 对「装 git 依赖」有一句提醒:带 `prepare` 脚本的插件会在安装期构建,
200
- > 而 pnpm 默认拦住构建脚本,需要先在 profile 的 `pnpm-workspace.yaml` 里追加
201
- > `allowBuilds` 才能装上。**本插件不属于这种情况**——它没有任何安装期脚本。
202
-
203
- ### 方式四:手工改 profile 清单
204
-
205
- 不用插件管理器时:
206
-
207
- 1. 在 profile 的 `package.json` 里把本包加进 `dependencies`;
208
- 2. 把包名追加进 `dsh.profile.bundles`(顺序即补丁层顺序,放在 `@deepseek-ai/dsh-base`
209
- 与 `@deepseek-ai/dsh-web-app` 之后即可);
210
- 3. 执行 `dsh plugin --profile <profile> install` 安装依赖。
46
+ > 桌面版的 `desktop` profile 由应用独占管理,终端里执行 `dsh plugin --profile desktop …` 会被拒绝;改用应用内的插件页,或在应用右上角「打开 DSH 终端」里执行。
47
+ >
48
+ > 本包的 tarball 只有插件源码、定义与脚本,**没有安装期构建脚本**,不会碰到 pnpm 的 `allowBuilds` 授权那一步。
211
49
 
212
50
  ### 版本闸门
213
51
 
214
- 本包的 DSH 兼容范围是 **`>=0.2.0-rc.1`——只有下界,没有上界**,
215
- 三处声明逐字一致(`peerDependencies`、`engines.dsh`、`dsh.compatibility.dsh`,
216
- 由 `pnpm test:manifest` 断言):
217
-
218
- - DSH 安装器读的是 `peerDependencies`:运行时版本低于下界 → **直接拒绝安装**,
219
- 不会出现「装上了但思考功能失效」;
220
- - **上界故意不写**:写死上界意味着 DSH 每升一个大版本都要重发一版插件去放宽范围,
221
- 代价大于收益。换来的代价是失去「未来版本被挡住」这层保护——改由插件自己的
222
- 响亮失败兜底(定义校验、id 归属判定、启动收尾核验路由是否注册,
223
- 见[归属与删除](#归属与删除));
224
- - `@deepseek-ai/cordis` 同理放宽为 `>=4.0.2`:它**不参与**安装闸门(判定只看
225
- `@deepseek-ai/dsh*` 开头的包),但它是 pnpm 的 peer,写死上界同样会在它升大版本时
226
- 把安装卡住。
227
-
228
- > **预发布的一个细节**:node-semver 规定,带预发布标签的版本只有在范围里存在
229
- > 「元组相同、且自身也带预发布标签」的比较符时才被放行。
230
- > `>=0.2.0-rc.1` 因此只覆盖 `0.2.0` 这一元组的预发布(`0.2.0-rc.2` ✓);
231
- > `0.2.1-alpha.1`、`0.3.0-beta.1` 这类**新元组的预发布**在 npm/pnpm 的默认规则下
232
- > 不被放行(DSH 自己的闸门带 `includePrerelease`,会放行——两者判定不一致时,
233
- > 失败的是 pnpm 那一步)。真遇到时二选一:
234
- >
235
- > ```bash
236
- > # ① 给该元组补一条分支,写回范围(例如到 0.2.1 的预发布)
237
- > # ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0"
238
- >
239
- > # ② 或让 pnpm 放宽 peer 校验(参数照样原样转发)
240
- > dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher \
241
- > --config.strict-peer-dependencies=false
242
- > ```
243
- >
244
- > 稳定版(`0.2.0`、`0.2.1`、`0.3.0`、`1.0.0`…)不受这个细节影响,一律放行。
52
+ DSH 兼容范围 **`>=0.2.0-rc.1`,只有下界、没有上界**,三处声明逐字一致(`peerDependencies`、`engines.dsh`、`dsh.compatibility.dsh`)。DSH 安装器读的是 `peerDependencies`:运行时版本低于下界会被**直接拒绝安装**,不会出现「装上了但功能失效」。上界不写,意味着 DSH 升大版本不必重发插件,代价是失去「未来版本被挡住」这层保护——由插件启动时的响亮失败兜底。
245
53
 
246
- ---
54
+ > 预发布细节:`>=0.2.0-rc.1` 只覆盖 `0.2.0` 这一元组的预发布,`0.2.1-alpha.1` 这类新元组的预发布会卡在 pnpm 那一步(DSH 闸门带 `includePrerelease`,会放行)。真遇到时给该元组补一条范围分支,或加 `--config.strict-peer-dependencies=false`。
247
55
 
248
56
  ## 使用
249
57
 
250
58
  ### 1. 配置 API 密钥
251
59
 
252
- 密钥从 [AMD Radeon Cloud 文档](https://amd-aim.github.io/radeon-cloud-docs/) 里的入口申请
253
- (本项目只覆盖 Public Free Model APIs 的共享端点)。拿到 `rc-…` 之后二选一:
254
-
255
- **方式一:设置页(推荐给日常使用)**
256
-
257
- 左下角「账号菜单 → 设置 → 模型」→ Radeon Cloud 行 →「编辑」→ 在「API 密钥」里粘贴
258
- `rc-…` →「保存」。保存成功后该行右侧出现**绿点**(悬停显示「API 密钥已配置」;
259
- 没配密钥时是红点,提示「API 密钥缺失」)。
260
-
261
- 密钥写进本机凭据存储 `~/.dsh/.credentials.yaml` 的 `RADEON_CLOUD_API_KEY` 条目,
262
- 明文不落配置文件、界面不回显;要换密钥就再次「编辑」并输入新值
263
- (输入框会提示「已配置——输入新值可替换」)。`scripts/` 下的脚本读的是同一个条目,
264
- 因此在设置页填过一次,脚本也就不用再配了。
265
-
266
- **方式二:环境变量(不落盘,适合临时试用与 CI)**
267
-
268
- ```bash
269
- export RADEON_CLOUD_API_KEY=rc-你的密钥
270
- ```
271
-
272
- 凭据服务按「**继承的进程环境** → 插件写入的凭据存储 → 当前目录 `.env` → `$DSH_HOME/.env`」
273
- 依次查找,环境变量优先级最高,一次性的写法也有效:
60
+ 密钥从 [AMD Radeon Cloud 文档](https://amd-aim.github.io/radeon-cloud-docs/) 的入口申请(本项目只覆盖 Public Free Model APIs 共享端点)。
274
61
 
275
- ```bash
276
- RADEON_CLOUD_API_KEY=rc-你的密钥 dsh --profile <profile>
277
- ```
62
+ - **设置页(推荐)**:「设置 → 模型」→ Radeon Cloud →「编辑」→ 粘贴 `rc-…` →「保存」。密钥写进本机凭据存储 `~/.dsh/.credentials.yaml` 的 `RADEON_CLOUD_API_KEY` 条目,明文不落配置文件、界面不回显。仓库脚本读的是同一个条目。
63
+ - **环境变量**(不落盘,适合临时试用与 CI):`RADEON_CLOUD_API_KEY=rc-你的密钥 dsh --profile <profile>`。凭据服务按「进程环境 → 凭据存储 → `.env` → `$DSH_HOME/.env`」依次查找,环境变量优先级最高。
278
64
 
279
- > 注意名字:**插件运行时**读的是 `RADEON_CLOUD_API_KEY`(即 provider 定义里的
280
- > `apiKeyEnv`),而**仓库脚本**用的是 `RADEON_API_KEY`。两者互不影响,
281
- > 脚本那一套见下面的表格。
65
+ > **插件运行时**读的是 `RADEON_CLOUD_API_KEY`(定义里的 `apiKeyEnv`),**仓库脚本**用的是 `RADEON_API_KEY`,两者互不影响。
282
66
 
283
67
  ### 2. 选模型与思考档位
284
68
 
285
- 对话输入框下方点模型选择器 → 选 Radeon Cloud 的模型 → 再选「推理等级」,
286
- 那就是思考档位。档位按模型逐个声明(见[可用模型](#可用模型)):
287
- GLM-5.3-Flash 与 MiniCPM-V-4.7-1B 不接受 `none`,因此没有关闭档位;
288
- `MiniCPM5-2B` 不是推理模型,不会出现档位下拉。
289
- 选了服务端不接受的档位是**硬失败**(400 / 422),不会静默降级。
69
+ 输入框下方的模型选择器里选 Radeon 的模型,再选「推理等级」就是思考档位(按模型逐个声明,见[可用模型](#可用模型))。MiniCPM-V-4.7-1B 与 MiniCPM-V-4.7-35B-A3B 不接受 `none`,没有关闭档位;`MiniCPM5-2B` 非推理模型,不出现档位下拉。选了服务端不接受的档位是**硬失败**(400/422),不会静默降级。
290
70
 
291
71
  ### 3. 图片输入
292
72
 
293
- 支持图片:DeepSeek-V4.1-Flash、DeepSeek-V4-Flash-Vision-Exp、MiMo-V2.6-Flash、
294
- MiniCPM-V-4.7-1B、MiniCPM-V-4.7-35B-A3B、Qwen3.8-27B、Qwen3.8-Flash-Next。
295
- 只收文本:DeepSeek-V4-Flash、GLM-5.3-Flash、MiniCPM5-2B。
73
+ 支持图片:DeepSeek-V4.1-Flash、Gemma-4-31B-IT、MiMo-V2.6-Flash、MiniCPM-V-4.7-1B、MiniCPM-V-4.7-35B-A3B、Qwen3.8-27B、Qwen3.8-Flash-Next。只收文本:DeepSeek-V4-Flash、GLM-5.3-Flash、MiniCPM5-2B。
296
74
 
297
75
  ### 4. 升级与卸载
298
76
 
299
77
  ```bash
300
78
  dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher@latest # 升级
301
- dsh plugin --profile <profile> remove @zhz8888/dsh-radeon-cloud-patcher # 卸载
79
+ dsh plugin --profile <profile> remove @zhz8888/dsh-radeon-cloud-patcher # 卸载 = 删除供应商
302
80
  ```
303
81
 
304
- 升级不需要额外动作:定义由插件层声明,装上新版本重启即生效
305
- (在市场里升级同样要重启,原因与安装一致)。
306
- **卸载即删除供应商**——插件层消失,provider 与设置页那一行一起消失。
82
+ 升级后重启即生效,不需要额外动作。例外:若你曾用 `pnpm install:profile` 把定义合并进 profile(见[归属与删除](#归属与删除)),那份副本会盖住插件层的新定义,升级后要么重跑一次,要么删掉副本让插件独占管理。
307
83
 
308
- > 例外:如果你曾用 `pnpm install:profile` 把定义合并进 profile(见[归属与删除](#归属与删除)),
309
- > profile 里那份副本会**盖住**插件层的新定义——升级后要么重跑一次
310
- > `pnpm install:profile`,要么删掉那份副本,让插件独占管理。
84
+ ### 5. 仓库脚本的环境变量
311
85
 
312
- ### 5. 仓库脚本用的环境变量
313
-
314
- 下面这些只影响仓库里的 `scripts/`(`radeon-api.sh`、`probe-efforts.mjs`),
315
- 与插件运行时无关:
86
+ 只影响 `scripts/`(`radeon-api.sh` / `radeon-api.ps1`、`probe-efforts.mjs`),与插件运行时无关:
316
87
 
317
88
  | 变量 | 作用 | 默认值 |
318
89
  | --- | --- | --- |
319
90
  | `RADEON_API_KEY` | 脚本用的密钥字面值,优先级高于凭据文件 | 无 |
320
- | `RADEON_KEY_REF` | 脚本从凭据文件读取时用的条目名 | `RADEON_CLOUD_API_KEY` |
91
+ | `RADEON_KEY_REF` | 从凭据文件读取时用的条目名 | `RADEON_CLOUD_API_KEY` |
321
92
  | `RADEON_BASE` | 端点基础 URL,可指向独占端点 | `https://developer.amd.com.cn/radeon/api/v1` |
322
93
  | `DSH_HOME` | DSH 数据目录,凭据文件据此定位 | `~/.dsh` |
323
94
 
324
- > 密钥在脚本中只存在于进程变量与 curl 请求头,不写盘、不打印、不出现在输出里。
325
- > 但 shell 历史记录仍会留下 `export RADEON_API_KEY=...` 这一行,请自行注意。
326
-
327
- ---
328
-
329
- ## 归属与删除
330
-
331
- 本插件要求 `radeon-cloud` 这个 id **由它独占**,因此:
332
-
333
- - 设置页那一行**不会出现「删除」按钮**。DSH 的判据是「该 provider 路径存在于用户层、
334
- 且不存在于 base 层」——定义写在插件的 bundle 层,base 层永远有它,按钮因此永不渲染;
335
- - **删除供应商 = 卸载插件**。卸载后插件层消失,provider 随之消失。
336
- 若你曾用 `pnpm install:profile` 把定义合并进 profile,profile 里会留下一份副本
337
- (那份副本的所有权归你,卸载后设置页里也会重新出现删除按钮,可自行清理);
338
- - **「自定义」标签**:DSH 对**任何不在内置模型目录里的 provider** 硬编码了这个标记
339
- (`dsh-llm-pi-ai` 的 `declared: !catalog.has(provider)`),配置层面无从去掉。
340
- 本插件随包带了一个极小的客户端补丁(`client/client.js`),用一条 CSS 规则把
341
- **本 provider 那一行**的标签隐藏掉:
342
-
343
- ```css
344
- li:has(button[aria-label*="(radeon-cloud)"]) span[class*="rowTag"] { display: none !important }
345
- ```
346
-
347
- 代价说清楚:它依赖 DSH 的 DOM 结构与类名片段,DSH 改版后可能静默失效——
348
- 标签重新出现,功能不受影响。
349
-
350
- ### 定义归谁管:启动时的判定
351
-
352
- DSH 的非 insert 补丁是**整体替换**:`llm-pi-ai` 的 config 只要在 profile 里出现过,
353
- 就会整份盖掉插件层的定义。插件启动时静态读 profile / home / 命令行 overlay 三层,
354
- 判定这个 id 现在由谁说了算:
355
-
356
- | 情况 | 结果 |
357
- | --- | --- |
358
- | 没人碰过 `llm-pi-ai` 的 config | 插件层的定义生效,正常启动 |
359
- | profile 里有一份**与本插件一致**的同 id 声明 | 放行,并提示它会在卸载后残留(通常是你在设置页保存过一次、被物化出的副本) |
360
- | profile 里有一份**与之不一致**的同 id 声明 | **启动失败**:报「provider id 冲突」,并指名第一处差异字段 |
361
- | profile 给 `llm-pi-ai` 写了 config 却没有本 provider | **启动失败**:报「定义未生效」,并打印可直接执行的合并命令 |
362
- | 那一层的 config 是 `!!js` 表达式,或本次运行读不到 profile 目录 | 跳过判定并告警——静态判断不了,不等于失败;真正生效的是哪一份以 `dsh --profile <profile> --dump-config` 为准 |
363
-
364
- 后两种是**失败 + 原因**,而不是静默接管或静默消失——这是本项目的取舍:id 冲突要响亮。
365
- 处置方式(按键合并、`--force`、输出示例)见[已知限制](#6-与用户自有-provider-共存)。
366
-
367
- > 想彻底消除标签、不留 DOM 依赖,只能让插件自己注册 provider 路由,也就意味着自己实现
368
- > 传输、思考解析与用量计量。本项目定位是「薄壳 + 由 DSH 自带的 pi-ai 承载」,
369
- > 因此选择了 CSS 隐藏这条路。
370
-
371
- ---
372
-
373
- ## 项目结构
374
-
375
- ```
376
- dsh-radeon-cloud-patcher/
377
- ├── README.md 本文件
378
- ├── AGENTS.md 智能体与协作者指引
379
- ├── LICENSE MIT 许可证
380
- ├── package.json 插件包清单(版本闸门、导出、客户端声明、脚本)
381
- ├── cordis.plugin.patch.yml bundle 补丁:provider 定义 + 本插件行
382
- ├── pnpm-workspace.yaml 本仓库的安装期构建授权(不影响用户安装)
383
- ├── pnpm-lock.yaml 依赖锁文件
384
- ├── .gitignore
385
- │
386
- ├── provider/ 配置本体
387
- │ └── radeon-cloud.yml provider 定义的唯一真源
388
- │
389
- ├── client/ 客户端补丁
390
- │ └── client.js 隐藏本行的「自定义」标签(DSH 客户端模块格式,非 ESM)
391
- │
392
- ├── locale/ 插件页显示名称(DSH 按包内 locale 读取,缺了就回落成 npm 包名)
393
- │ ├── en.json 英文:{"meta":{"title":"Radeon Cloud",…}}
394
- │ └── zh.json 中文:{"meta":{"title":"Radeon Cloud 提供方",…}}
395
- │
396
- ├── src/ 插件实现(薄壳,不含 provider 逻辑)
397
- │ ├── index.js 插件入口:启动校验、所有权判定、收尾核验
398
- │ ├── ownership.js 所有权判定的纯函数(四种归属结局 + dynamic 哨兵、差异定位)
399
- │ ├── merge.js 键级合并的纯函数(补救路径使用)
400
- │ ├── patch-text.js 文本级合并,保留用户注释与格式
401
- │ ├── validate.js 定义的结构校验
402
- │ └── yaml.js 定位 YAML 解析器
403
- │
404
- ├── scripts/ 运维与校验工具
405
- │ ├── sync-bundle-patch.mjs 把定义真源渲染进补丁的生成区块
406
- │ ├── apply-to-profile.mjs 补救路径:按键合并进 profile(冲突时拒绝覆盖)
407
- │ ├── validate-config.mjs 用 llm-pi-ai 真实 schema 校验定义
408
- │ ├── probe-efforts.mjs 实测逐模型 reasoning_effort 档位表
409
- │ └── radeon-api.sh API 调用助手(密钥取自环境变量或凭据存储)
410
- │
411
- ├── test/ 离线测试(自研 runner,无测试框架)
412
- │ ├── *.test.mjs 清单不变式、插件页元信息、合并语义、
413
- │ │ 文本级编辑、所有权判定、启动看护、
414
- │ │ 补丁与客户端一致性
415
- │ ├── verify-reasoning.mjs 思考文本取回的回归测试
416
- │ └── fixtures/stream-reasoning.sse 实测抓取的真实流
417
- │
418
- └── docs/ 技术文档
419
- └── market-submission.md 插件市场投稿与 npm 发布说明
420
- ```
421
-
422
- > 插件壳只做四件事:**分发**(npm 包,可被插件市场安装)、**声明**(provider 定义写在
423
- > 自带的补丁层里,profile 只读不改)、**版本闸门**(版本不匹配直接拒装)、
424
- > **看护**(启动时校验定义、判定 id 归属,不该继续就让插件启动失败并说明原因)。
425
- > 实际的传输、思考字段解析、多轮回传、用量计量仍全部由 DSH 自带的 `llm-pi-ai` 承担。
426
-
427
- ---
428
-
429
- ## 常用命令
430
-
431
- ```bash
432
- # 全量测试(清单不变式、插件页元信息、合并语义、所有权判定、启动看护、补丁一致性、思考字段取回)
433
- pnpm test
434
-
435
- # 只跑启动看护(四种归属结局 + dynamic 跳过)与补丁一致性
436
- pnpm test:guard
437
- pnpm test:patch
438
-
439
- # 只跑插件页显示名称的契约(locale 文件 + exports 模式 + files 白名单)
440
- pnpm test:meta
441
-
442
- # 校验 provider 定义:结构校验 + llm-pi-ai 真实 schema 校验
443
- # 这是判断「字段在当前 DSH 版本里是否仍然有效」的那道校验,建议升级 DSH 后跑一次
444
- pnpm validate
445
-
446
- # 改完 provider/radeon-cloud.yml 后,把定义同步进补丁的生成区块
447
- pnpm sync:patch
448
-
449
- # 补救路径:把定义按键合并进 profile(冲突时拒绝覆盖,--force 才覆盖)
450
- pnpm install:profile:dry
451
-
452
- # 重新实测某个模型支持的思考档位
453
- pnpm probe --only Qwen3.8-27B
454
-
455
- # 拉取实时模型目录
456
- ./scripts/radeon-api.sh GET /models
457
- ```
458
-
459
- ---
95
+ > 密钥只存在于进程变量与请求头,不写盘、不打印;但 shell 历史仍会留下 `export RADEON_API_KEY=...`(PowerShell 里是 `$env:RADEON_API_KEY = '...'`),请自行注意。
96
+ >
97
+ > 两个助手脚本的参数、环境变量与输出契约完全相同:POSIX / Git Bash 用 `./scripts/radeon-api.sh`,Windows 原生用 `./scripts/radeon-api.ps1`(需 PowerShell 7)。`pnpm probe` 在 Windows 上会自动走 `.ps1`。
460
98
 
461
99
  ## 可用模型
462
100
 
463
- > 采集日期:**2026-10-16**
101
+ > 采集日期:**2026-11-09**
464
102
 
465
103
  | 模型 | 上下文 | 图片 | 思考档位 |
466
104
  | --- | --- | --- | --- |
467
105
  | DeepSeek-V4.1-Flash | 1,048,576 | ✅ | off/minimal/low/medium/high/xhigh/max |
468
106
  | DeepSeek-V4-Flash | 1,048,576 | ❌ | off/minimal/low/medium/high/xhigh/max |
469
- | DeepSeek-V4-Flash-Vision-Exp | 1,048,576 | ✅ | off/minimal/low/medium/high/xhigh/max |
470
107
  | MiMo-V2.6-Flash | 1,048,576 | ✅ | off/minimal/low/medium/high/xhigh/max |
471
- | MiniCPM-V-4.7-35B-A3B | 262,144 | ✅ | off/minimal/low/medium/high/xhigh/max |
108
+ | Gemma-4-31B-IT | 262,144 | ✅ | off/minimal/low/medium/high/xhigh/max |
109
+ | GLM-5.3-Flash | 262,144 | ❌ | off/minimal/low/medium/high/xhigh/max |
110
+ | MiniCPM-V-4.7-1B | 262,144 | ✅ | low/medium/high |
111
+ | MiniCPM-V-4.7-35B-A3B | 262,144 | ✅ | low/medium/high |
472
112
  | Qwen3.8-27B | 262,144 | ✅ | off/low/medium/xhigh |
473
113
  | Qwen3.8-Flash-Next | 262,144 | ✅ | off/low/medium/xhigh |
474
- | GLM-5.3-Flash | 262,144 | ❌ | low/medium/high |
475
- | MiniCPM-V-4.7-1B | 262,144 | ✅ | low/medium/high |
476
114
  | MiniCPM5-2B | 131,072 | ❌ | (不返回分离思考,故不提供档位) |
477
115
 
478
- `off` 表示发送 `reasoning_effort: "none"` 关闭思考。
479
- **GLM-5.3-Flash 与 MiniCPM-V-4.7-1B 不接受 `none`**,因此没有关闭档位;Qwen 系靠 `none` 关闭,而**不传档位时仍会思考**(AMD 服务端行为)。
480
- `MiniCPM-V-4.7-35B-A3B` 与 `MiniCPM-V-4.7-1B` 同属 MiniCPM 系,但档位表并不相同——**不要按模型名的相近程度推断档位**。
481
-
482
- > **档位表不在 API 里**(`GET /v1/models` 不返回该信息),只能实测。模型上下架会导致此表过期,
483
- > 请用 `pnpm probe` 复测后再更新 `provider/radeon-cloud.yml`。
484
- >
485
- > `MinerU2.5-Pro` 未收录——它走独立的 `/v1/ocr` 端点,不是对话模型。
116
+ `off` 表示发送 `reasoning_effort: "none"` 关闭思考;Qwen 系靠 `none` 关闭,**不传档位时仍会思考**(AMD 服务端行为)。档位表与厂商、模型名的相近程度无关:同为 MiniCPM 系,`MiniCPM-V-4.7-1B` 与 `MiniCPM-V-4.7-35B-A3B` 只接受 `low/medium/high`,`MiniCPM5-2B` 接受三档却不返回分离思考——**只能逐个实测**。
486
117
 
487
- ---
118
+ > 档位表不在 API 里(`GET /v1/models` 不返回该信息),只能实测;模型上下架会让此表过期,用 `pnpm probe` 复测后再更新 `provider/radeon-cloud.yml`。目录里的 `Clef`、`d3`、`d3-flash` 与 `OmniVoice` 未收录——前三者的输出模态是 `decision`、上下文为 0,后者输出音频,都不是对话模型。
488
119
 
489
120
  ## 已知限制
490
121
 
491
- ### 1. 思考与答案共用同一个 token 预算(最重要)
492
-
493
- Radeon 的 `max_tokens` 同时约束思考和最终回答。预算不足时**回答会变成空字符串**:
494
-
495
- | 配置 | finish_reason | content | reasoning |
496
- | --- | --- | --- | --- |
497
- | `effort=low`, `max_tokens=16` | `length` | **空** | 84 字符 |
498
- | `effort=low`, `max_tokens=120` | `length` | **空** | 584 字符 |
499
- | `effort=low`, `max_tokens=400` | `length` | **空** | 1717 字符 |
500
- | `effort=none`, `max_tokens=64` | `length` | 330 字符 ✅ | 0 |
501
-
502
- **应对**:配置里 `defaultMaxTokens` 已设为 `32768`(pi-ai 默认值),给思考留足余量。
503
- `max_tokens` 是上限而非计费项,实际按产出计费,调高不增加成本。
504
-
505
- 若仍偶发空回答,把该会话的思考档位调低,或临时改用 `off`。
506
-
507
- ### 2. `stop`(停止序列)不可用
508
-
509
- Radeon 网关按白名单重建请求体,`stop` 不在白名单内,会被**静默丢弃**。
510
- DSH 的 `dsh-llm-pi-ai` 会对此抛 `UNSUPPORTED_OPTION`,属于响亮失败,比静默失效好。
122
+ 1. **思考与回答共用同一个 token 预算**:Radeon 的 `max_tokens` 同时约束思考和回答,预算不足时**回答会变成空字符串**(实测 `effort=low`、`max_tokens` ≤ 400 时 `finish_reason=length` 而 content 全空)。配置里 `defaultMaxTokens` 已设为 `32768` 留足余量;`max_tokens` 是上限而非计费项,调高不增加成本。仍偶发空回答就调低该会话档位或临时改用 `off`。
123
+ 2. **`stop` 不可用**:网关按白名单重建请求体,`stop` 被静默丢弃,`dsh-llm-pi-ai` 会抛 `UNSUPPORTED_OPTION`(响亮失败,比静默失效好)。
124
+ 3. **部分模型不接受 `developer` 角色**:10 个模型中有 5 个拒绝(MiniCPM5-2B、MiniCPM-V-4.7-1B、MiniCPM-V-4.7-35B-A3B 报 422;Qwen 系两个报 400),已用 `compat.supportsDeveloperRole: false` 改走 `system`。
125
+ 4. **非法档位是硬失败**(400/422),不会自动降级;档位表来自实测,新增模型请先复测。
126
+ 5. **上游偶发故障**:出现过 `504` 被包装成 HTTP `500` + `code: upstream_error`,DSH 归为可重试的 `SERVER`,重试即可。
511
127
 
512
- ### 3. 部分模型不接受 `developer` 角色
513
-
514
- DSH 默认对 OpenAI 兼容端点使用 `developer` 承载系统提示词,10 个模型中有 6 个会拒绝,
515
- 且报错文本分两种:GLM-5.3-Flash、MiniCPM5-2B、MiniCPM-V-4.7-1B 报 `422 unknown role: developer`;
516
- Qwen 系两个与 MiniCPM-V-4.7-35B-A3B 报 `400 Unexpected message role.`。
517
- 已通过 `compat.supportsDeveloperRole: false` 关闭,改用 `system` 角色。
518
-
519
- ### 4. 档位被拒是硬失败
520
-
521
- 非法档位返回 `400`(Qwen 系)或 `422`(GLM 系),不会自动降级。
522
- 配置里的档位表来自实测,如新增模型请先复测。
128
+ ## 归属与删除
523
129
 
524
- ### 5. 上游偶发故障
130
+ 本插件要求 `radeon-cloud` 这个 id 由它独占:
525
131
 
526
- 实测中出现过 `504 Gateway Timeout`,被包装为 HTTP `500` + `code: upstream_error`。
527
- DSH 将其归为可重试的 `SERVER`。偶发,重试即可。
132
+ - 设置页那一行**不会出现「删除」按钮**——DSH 只在「该 provider 存在于用户层、且不存在于 base 层」时渲染它,而定义写在插件的 bundle 层。
133
+ - **删除供应商 = 卸载插件**。若你曾用 `pnpm install:profile` 合并进 profile,那里会留下一份归你所有的副本(卸载后删除按钮会重新出现,可自行清理)。
134
+ - **「自定义」标签**:DSH 对任何不在内置模型目录里的 provider 硬编码了这个标记,配置层面无从去掉。随包带的极小客户端补丁(`client/client.js`)用一条 CSS 规则隐藏**本 provider 那一行**的标签;它依赖 DSH 的 DOM 结构与类名片段,DSH 改版后可能静默失效——标签重新出现,功能不受影响。
528
135
 
529
- ### 6. 与用户自有 provider 共存
136
+ 启动时插件会静态读 profile / home / 命令行 overlay 三层,判定这个 id 现在由谁说了算:
530
137
 
531
- provider 定义由插件的 bundle 层声明,插件**只读 profile、不写 profile**。
532
- DSH 的非 insert 补丁是整体替换,因此你自己写的 `llm-pi-ai` config 会整份盖掉插件层的定义:
533
- 插件启动时会判定这件事,响亮失败并给出处置,而不是让 provider 无声消失
534
- (判据见[归属与删除](#归属与删除))。
138
+ | 情况 | 结果 |
139
+ | --- | --- |
140
+ | 没人碰过 `llm-pi-ai` 的 config | 插件层定义生效,正常启动 |
141
+ | profile 里有一份**与本插件一致**的同 id 声明 | 放行,提示它会在卸载后残留(通常是设置页保存时物化出的副本) |
142
+ | profile 里有一份**不一致**的同 id 声明 | **启动失败**:报 id 冲突,并指名第一处差异字段 |
143
+ | profile 给 `llm-pi-ai` 写了 config 却没有本 provider | **启动失败**:报定义未生效,并打印可直接执行的合并命令 |
144
+ | config 是 `!!js` 表达式,或读不到 profile 目录 | 跳过判定并告警,以 `--dump-config` 为准 |
535
145
 
536
- 想与自有 provider 共存,就用补救命令做按键合并:
146
+ 后两种是**失败 + 原因**,而不是静默接管或静默消失。要与自有 provider 共存,用补救命令按键合并:
537
147
 
538
148
  ```bash
539
- # 在插件仓库里
540
149
  pnpm install:profile:dry # 演练:打印改动与被保留的其它 provider
541
- pnpm install:profile # 写入(自动备份,断言其余条目与同级键逐字未变)
542
-
543
- # 只装了插件、没有克隆仓库时,脚本在包里;启动失败信息会直接打印完整命令
544
- node ~/.dsh/profiles/<profile>/node_modules/@zhz8888/dsh-radeon-cloud-patcher/scripts/apply-to-profile.mjs \
545
- ~/.dsh/profiles/<profile>/cordis.patch.yml
150
+ pnpm install:profile # 写入(自动备份,冲突时拒绝覆盖,--force 才覆盖)
546
151
  ```
547
152
 
548
- 输出示例:
153
+ 只装了插件、没克隆仓库时,用包内脚本(启动失败信息会打印完整命令):
154
+ `node ~/.dsh/profiles/<profile>/node_modules/@zhz8888/dsh-radeon-cloud-patcher/scripts/apply-to-profile.mjs ~/.dsh/profiles/<profile>/cordis.patch.yml`
549
155
 
550
- ```
551
- 【写入】 ~/.dsh/profiles/desktop/cordis.patch.yml
552
- 新增 providers.radeon-cloud(10 个模型)
553
- 保留的其它 provider:my-corp-proxy, another-one
554
- 顶层条目 12 个,增减 0 个
555
- ```
156
+ 合并粒度是键:只新增或替换 `providers.radeon-cloud`,你写的其它 provider、注释、缩进与键序原样保留(文本级编辑)。⚠️ **不要另写一条 `llm-pi-ai` 的非 insert 补丁**去改该行的其它字段——那会整体替换这一行,把 `providers` 一并带走。
556
157
 
557
- 合并粒度是键:只新增或替换 `providers.radeon-cloud` 这一个键,你添加的其它 provider、
558
- 你写的注释、缩进风格与键序都原样保留(文本级编辑,不重新序列化你的文件)。
559
- 写入前会断言:除 `radeon-cloud` 外的同级 provider 键集合一致、`llm-pi-ai` 之外的顶层条目
560
- 逐字符未变,任一不成立即中止且不落盘;已存在不一致的同 id 定义时**拒绝覆盖**并打印第一处
561
- 差异,确认要覆盖再加 `--force`。
158
+ ## 兼容性
562
159
 
563
- 另有一处要注意:**不要另写一条 `llm-pi-ai` 的非 insert 补丁**去改该行的其它字段——
564
- 那会整体替换这一行,把 `providers` 一并带走。要改就只改你需要的那一个键。
160
+ - 目标 DSH 版本:`>=0.2.0-rc.1`,无上界(三处声明一致)。
161
+ - 收录的模型当前均为 `stability: experimental`,上游接口行为可能变,档位失效时先复测。
162
+ - 仅覆盖 Public Free Model APIs(共享端点);独占端点的基础 URL 每次实例重启都会变化,不在本项目范围内。
163
+ - 客户端补丁依赖 DSH 的 DOM 结构,改版后可能静默失效。
565
164
 
566
- ---
165
+ ## 开发
567
166
 
568
- ## 兼容性
167
+ 零运行时依赖,源码只用 Node 内置模块;测试为自研 runner,无测试框架。运行 `scripts/` 与 `test/` 需要 Node `>=22`;`pnpm validate` 需本机有可用的 DSH 安装(macOS 下按桌面版应用包路径加载 schema,可用 `DSH_MODULES` 覆盖)。要用 PowerShell 版助手或 `pnpm test:ps1` 的行为层,另需 PowerShell 7(`pwsh`)。
569
168
 
570
- - 目标 DSH 版本:`>=0.2.0-rc.1`,**无上界**(三处声明一致,见[版本闸门](#版本闸门))
571
- - 收录的模型当前均为 `stability: experimental`——上游接口行为可能变,档位失效时先复测
572
- - 仅覆盖 Public Free Model APIs(共享端点);独占端点的基础 URL 每次实例重启都会变化,不在本项目范围内
573
- - 客户端补丁依赖 DSH 的 DOM 结构,DSH 改版后可能静默失效(机制与代价见[归属与删除](#归属与删除))
169
+ ```bash
170
+ pnpm test # 全量离线测试(清单不变式 / 插件元信息 / 合并语义 / 所有权判定 / 启动看护 / 补丁一致性 / 思考取回)
171
+ pnpm validate # 用 llm-pi-ai 真实 schema 校验 provider 定义——升级 DSH 后必跑
172
+ pnpm sync:patch # 改完 provider/radeon-cloud.yml 后同步进补丁,否则定义漂移
173
+ pnpm probe --only <模型> # 重新实测思考档位
174
+ ./scripts/radeon-api.sh GET /models # 拉取实时模型目录(POSIX / Git Bash)
175
+ ./scripts/radeon-api.ps1 GET /models # 同上,PowerShell 7
176
+ ```
574
177
 
575
- ---
178
+ 架构约束、补丁层设计理由与发版流程见 [`AGENTS.md`](AGENTS.md) 与 [`docs/market-submission.md`](docs/market-submission.md)。
576
179
 
577
180
  ## 许可证
578
181
 
579
- 本项目以 [MIT 许可证](https://github.com/zhz8888/dsh-radeon-cloud-patcher/blob/main/LICENSE) 开源。
580
-
581
- MIT 许可证允许你自由使用、复制、修改、合并、发布、分发、再许可及销售本项目的副本,
582
- 唯一义务是在所有副本或实质性部分中保留版权声明与许可声明。
583
-
584
- 本软件按「原样」提供,不附带任何形式的担保,包括但不限于对适销性、特定用途适用性
585
- 及不侵权的担保;作者或版权持有人不对使用本软件产生的任何损害承担责任。
182
+ 本项目以 [MIT 许可证](https://github.com/zhz8888/dsh-radeon-cloud-patcher/blob/main/LICENSE) 开源。本软件按「原样」提供,不附带任何形式的担保。
@@ -76,9 +76,9 @@
76
76
  # ── 协议兼容开关 ──────────────────────────────────────────────
77
77
  compat:
78
78
  # ★ 关键:DSH 默认对普通 OpenAI 兼容端点发送 developer 角色,
79
- # 而 10 个模型中有 6 个直接拒绝——GLM-5.3-Flash、MiniCPM5-2B、
80
- # MiniCPM-V-4.7-1B 报 422 unknown role: developer,Qwen 系两个与
81
- # MiniCPM-V-4.7-35B-A3B 报 400 Unexpected message role。
79
+ # 而 10 个模型中有 5 个直接拒绝——MiniCPM5-2B、MiniCPM-V-4.7-1B、
80
+ # MiniCPM-V-4.7-35B-A3B 报 422 unknown role: developer,Qwen 系两个
81
+ # 报 400 Unexpected message role。
82
82
  supportsDeveloperRole: false
83
83
  supportsReasoningEffort: true
84
84
  # Radeon 的网关只按白名单转发字段,thinking_budget /
@@ -89,12 +89,12 @@
89
89
  thinkingFormat: openai
90
90
 
91
91
  # ── 模型目录 ──────────────────────────────────────────────────
92
- # 共收录 10 个对话模型。目录接口里还有 MinerU2.5-Pro,但它的
93
- # context_length 为 0、tools 为 false,走的是独立的 /v1/ocr
94
- # 文档识别端点而非对话接口,故不收录。
92
+ # 共收录 10 个对话模型。目录接口里另有 Clef、d3、d3-flash 与 OmniVoice:
93
+ # 前三者的 output_modalities 是 decision、context_length 为 0,OmniVoice 的
94
+ # 输出是 audio,都不走对话接口,故不收录。
95
95
  #
96
- # 下面的分组按「档位表的形状」而非厂商划分——同一个厂商的模型档位
97
- # 未必相同(MiniCPM 系即是如此),按厂商分组会把读者引向错误预期。
96
+ # 下面的分组按「档位表的形状」而非厂商划分——同一厂商的模型档位未必相同,
97
+ # 按厂商分组会把读者引向错误预期。
98
98
  models:
99
99
  # ── 全档接受:none 可关闭思考 ──
100
100
  - id: DeepSeek-V4.1-Flash
@@ -123,8 +123,8 @@
123
123
  xhigh: xhigh
124
124
  max: max
125
125
 
126
- - id: DeepSeek-V4-Flash-Vision-Exp
127
- name: DeepSeek-V4-Flash-Vision-Exp
126
+ - id: MiMo-V2.6-Flash
127
+ name: MiMo-V2.6-Flash
128
128
  contextWindow: 1048576
129
129
  input: [text, image]
130
130
  reasoningEfforts:
@@ -136,9 +136,9 @@
136
136
  xhigh: xhigh
137
137
  max: max
138
138
 
139
- - id: MiMo-V2.6-Flash
140
- name: MiMo-V2.6-Flash
141
- contextWindow: 1048576
139
+ - id: Gemma-4-31B-IT
140
+ name: Gemma-4-31B-IT
141
+ contextWindow: 262144
142
142
  input: [text, image]
143
143
  reasoningEfforts:
144
144
  off: none
@@ -149,12 +149,10 @@
149
149
  xhigh: xhigh
150
150
  max: max
151
151
 
152
- # 同一个 MiniCPM 系里两个模型档位形状并不相同:35B-A3B 全档接受,
153
- # 1B 只接受 low/medium/high。实测确认两者都返回分离的思考内容。
154
- - id: MiniCPM-V-4.7-35B-A3B
155
- name: MiniCPM-V-4.7-35B-A3B
152
+ - id: GLM-5.3-Flash
153
+ name: GLM-5.3-Flash
156
154
  contextWindow: 262144
157
- input: [text, image]
155
+ input: [text]
158
156
  reasoningEfforts:
159
157
  off: none
160
158
  minimal: minimal
@@ -164,47 +162,47 @@
164
162
  xhigh: xhigh
165
163
  max: max
166
164
 
167
- # ── Qwen 系:无 minimal/max/high,仅 none/low/medium/xhigh ──
168
- # 这两个模型唯一能关闭思考的方式是 none;传入 high 会返回 400。
169
- - id: Qwen3.8-27B
170
- name: Qwen3.8-27B
165
+ # ── 仅 low/medium/high:不接受 none/minimal/xhigh/max ──
166
+ # 不声明 off 键:这两个模型没有「关闭思考」的档位。
167
+ - id: MiniCPM-V-4.7-1B
168
+ name: MiniCPM-V-4.7-1B
171
169
  contextWindow: 262144
172
170
  input: [text, image]
173
171
  reasoningEfforts:
174
- off: none
175
172
  low: low
176
173
  medium: medium
177
- xhigh: xhigh
174
+ high: high
178
175
 
179
- - id: Qwen3.8-Flash-Next
180
- name: Qwen3.8-Flash-Next
176
+ - id: MiniCPM-V-4.7-35B-A3B
177
+ name: MiniCPM-V-4.7-35B-A3B
181
178
  contextWindow: 262144
182
179
  input: [text, image]
183
180
  reasoningEfforts:
184
- off: none
185
181
  low: low
186
182
  medium: medium
187
- xhigh: xhigh
183
+ high: high
188
184
 
189
- # ── 仅 low/medium/high:不接受 none/minimal/xhigh/max ──
190
- # 不声明 off 键:这两个模型没有「关闭思考」的档位。
191
- - id: GLM-5.3-Flash
192
- name: GLM-5.3-Flash
185
+ # ── Qwen 系:无 minimal/max/high,仅 none/low/medium/xhigh ──
186
+ # 这两个模型唯一能关闭思考的方式是 none;传入 high 会返回 400。
187
+ - id: Qwen3.8-27B
188
+ name: Qwen3.8-27B
193
189
  contextWindow: 262144
194
- input: [text]
190
+ input: [text, image]
195
191
  reasoningEfforts:
192
+ off: none
196
193
  low: low
197
194
  medium: medium
198
- high: high
195
+ xhigh: xhigh
199
196
 
200
- - id: MiniCPM-V-4.7-1B
201
- name: MiniCPM-V-4.7-1B
197
+ - id: Qwen3.8-Flash-Next
198
+ name: Qwen3.8-Flash-Next
202
199
  contextWindow: 262144
203
200
  input: [text, image]
204
201
  reasoningEfforts:
202
+ off: none
205
203
  low: low
206
204
  medium: medium
207
- high: high
205
+ xhigh: xhigh
208
206
 
209
207
  # MiniCPM5-2B 接受 low/medium/high,但不返回分离的思考内容
210
208
  # (实测 reasoning 恒为空、reasoning_tokens 为 0)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhz8888/dsh-radeon-cloud-patcher",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "为 DeepSeek Harness 接入 AMD Radeon Cloud,完整支持模型思考(reasoning)功能。provider 定义由插件自带的补丁层声明,交由 DSH 自带的 dsh-llm-pi-ai 承载,不修改 DSH 本体、不写用户 profile,也不自研适配器。",
5
5
  "keywords": [
6
6
  "amd",
@@ -67,13 +67,14 @@
67
67
  "README.md"
68
68
  ],
69
69
  "scripts": {
70
- "test": "node test/manifest.test.mjs && node test/plugin-meta.test.mjs && node test/merge.test.mjs && node test/patch-text.test.mjs && node test/ownership.test.mjs && node test/apply-guard.test.mjs && node test/patch-sync.test.mjs && node test/client-patch.test.mjs && node test/verify-reasoning.mjs",
70
+ "test": "node test/manifest.test.mjs && node test/plugin-meta.test.mjs && node test/merge.test.mjs && node test/patch-text.test.mjs && node test/ownership.test.mjs && node test/apply-guard.test.mjs && node test/patch-sync.test.mjs && node test/client-patch.test.mjs && node test/ps1-parity.test.mjs && node test/verify-reasoning.mjs",
71
71
  "test:manifest": "node test/manifest.test.mjs",
72
72
  "test:meta": "node test/plugin-meta.test.mjs",
73
73
  "test:merge": "node test/merge.test.mjs && node test/patch-text.test.mjs && node test/ownership.test.mjs",
74
74
  "test:guard": "node test/apply-guard.test.mjs",
75
75
  "test:patch": "node test/patch-sync.test.mjs && node test/client-patch.test.mjs",
76
76
  "test:reasoning": "node test/verify-reasoning.mjs",
77
+ "test:ps1": "node test/ps1-parity.test.mjs",
77
78
  "sync:patch": "node scripts/sync-bundle-patch.mjs",
78
79
  "validate": "node scripts/validate-config.mjs",
79
80
  "probe": "node scripts/probe-efforts.mjs",
@@ -44,9 +44,9 @@ retryPolicy:
44
44
  # ── 协议兼容开关 ──────────────────────────────────────────────
45
45
  compat:
46
46
  # ★ 关键:DSH 默认对普通 OpenAI 兼容端点发送 developer 角色,
47
- # 而 10 个模型中有 6 个直接拒绝——GLM-5.3-Flash、MiniCPM5-2B、
48
- # MiniCPM-V-4.7-1B 报 422 unknown role: developer,Qwen 系两个与
49
- # MiniCPM-V-4.7-35B-A3B 报 400 Unexpected message role。
47
+ # 而 10 个模型中有 5 个直接拒绝——MiniCPM5-2B、MiniCPM-V-4.7-1B、
48
+ # MiniCPM-V-4.7-35B-A3B 报 422 unknown role: developer,Qwen 系两个
49
+ # 报 400 Unexpected message role。
50
50
  supportsDeveloperRole: false
51
51
  supportsReasoningEffort: true
52
52
  # Radeon 的网关只按白名单转发字段,thinking_budget /
@@ -57,12 +57,12 @@ compat:
57
57
  thinkingFormat: openai
58
58
 
59
59
  # ── 模型目录 ──────────────────────────────────────────────────
60
- # 共收录 10 个对话模型。目录接口里还有 MinerU2.5-Pro,但它的
61
- # context_length 为 0、tools 为 false,走的是独立的 /v1/ocr
62
- # 文档识别端点而非对话接口,故不收录。
60
+ # 共收录 10 个对话模型。目录接口里另有 Clef、d3、d3-flash 与 OmniVoice:
61
+ # 前三者的 output_modalities 是 decision、context_length 为 0,OmniVoice 的
62
+ # 输出是 audio,都不走对话接口,故不收录。
63
63
  #
64
- # 下面的分组按「档位表的形状」而非厂商划分——同一个厂商的模型档位
65
- # 未必相同(MiniCPM 系即是如此),按厂商分组会把读者引向错误预期。
64
+ # 下面的分组按「档位表的形状」而非厂商划分——同一厂商的模型档位未必相同,
65
+ # 按厂商分组会把读者引向错误预期。
66
66
  models:
67
67
  # ── 全档接受:none 可关闭思考 ──
68
68
  - id: DeepSeek-V4.1-Flash
@@ -91,8 +91,8 @@ models:
91
91
  xhigh: xhigh
92
92
  max: max
93
93
 
94
- - id: DeepSeek-V4-Flash-Vision-Exp
95
- name: DeepSeek-V4-Flash-Vision-Exp
94
+ - id: MiMo-V2.6-Flash
95
+ name: MiMo-V2.6-Flash
96
96
  contextWindow: 1048576
97
97
  input: [text, image]
98
98
  reasoningEfforts:
@@ -104,9 +104,9 @@ models:
104
104
  xhigh: xhigh
105
105
  max: max
106
106
 
107
- - id: MiMo-V2.6-Flash
108
- name: MiMo-V2.6-Flash
109
- contextWindow: 1048576
107
+ - id: Gemma-4-31B-IT
108
+ name: Gemma-4-31B-IT
109
+ contextWindow: 262144
110
110
  input: [text, image]
111
111
  reasoningEfforts:
112
112
  off: none
@@ -117,12 +117,10 @@ models:
117
117
  xhigh: xhigh
118
118
  max: max
119
119
 
120
- # 同一个 MiniCPM 系里两个模型档位形状并不相同:35B-A3B 全档接受,
121
- # 1B 只接受 low/medium/high。实测确认两者都返回分离的思考内容。
122
- - id: MiniCPM-V-4.7-35B-A3B
123
- name: MiniCPM-V-4.7-35B-A3B
120
+ - id: GLM-5.3-Flash
121
+ name: GLM-5.3-Flash
124
122
  contextWindow: 262144
125
- input: [text, image]
123
+ input: [text]
126
124
  reasoningEfforts:
127
125
  off: none
128
126
  minimal: minimal
@@ -132,47 +130,47 @@ models:
132
130
  xhigh: xhigh
133
131
  max: max
134
132
 
135
- # ── Qwen 系:无 minimal/max/high,仅 none/low/medium/xhigh ──
136
- # 这两个模型唯一能关闭思考的方式是 none;传入 high 会返回 400。
137
- - id: Qwen3.8-27B
138
- name: Qwen3.8-27B
133
+ # ── 仅 low/medium/high:不接受 none/minimal/xhigh/max ──
134
+ # 不声明 off 键:这两个模型没有「关闭思考」的档位。
135
+ - id: MiniCPM-V-4.7-1B
136
+ name: MiniCPM-V-4.7-1B
139
137
  contextWindow: 262144
140
138
  input: [text, image]
141
139
  reasoningEfforts:
142
- off: none
143
140
  low: low
144
141
  medium: medium
145
- xhigh: xhigh
142
+ high: high
146
143
 
147
- - id: Qwen3.8-Flash-Next
148
- name: Qwen3.8-Flash-Next
144
+ - id: MiniCPM-V-4.7-35B-A3B
145
+ name: MiniCPM-V-4.7-35B-A3B
149
146
  contextWindow: 262144
150
147
  input: [text, image]
151
148
  reasoningEfforts:
152
- off: none
153
149
  low: low
154
150
  medium: medium
155
- xhigh: xhigh
151
+ high: high
156
152
 
157
- # ── 仅 low/medium/high:不接受 none/minimal/xhigh/max ──
158
- # 不声明 off 键:这两个模型没有「关闭思考」的档位。
159
- - id: GLM-5.3-Flash
160
- name: GLM-5.3-Flash
153
+ # ── Qwen 系:无 minimal/max/high,仅 none/low/medium/xhigh ──
154
+ # 这两个模型唯一能关闭思考的方式是 none;传入 high 会返回 400。
155
+ - id: Qwen3.8-27B
156
+ name: Qwen3.8-27B
161
157
  contextWindow: 262144
162
- input: [text]
158
+ input: [text, image]
163
159
  reasoningEfforts:
160
+ off: none
164
161
  low: low
165
162
  medium: medium
166
- high: high
163
+ xhigh: xhigh
167
164
 
168
- - id: MiniCPM-V-4.7-1B
169
- name: MiniCPM-V-4.7-1B
165
+ - id: Qwen3.8-Flash-Next
166
+ name: Qwen3.8-Flash-Next
170
167
  contextWindow: 262144
171
168
  input: [text, image]
172
169
  reasoningEfforts:
170
+ off: none
173
171
  low: low
174
172
  medium: medium
175
- high: high
173
+ xhigh: xhigh
176
174
 
177
175
  # MiniCPM5-2B 接受 low/medium/high,但不返回分离的思考内容
178
176
  # (实测 reasoning 恒为空、reasoning_tokens 为 0)。
@@ -181,4 +179,4 @@ models:
181
179
  name: MiniCPM5-2B
182
180
  contextWindow: 131072
183
181
  input: [text]
184
- reasoningEfforts: false
182
+ reasoningEfforts: false
@@ -27,8 +27,15 @@ import { fileURLToPath } from 'node:url'
27
27
  const execFileAsync = promisify(execFile)
28
28
  /** 本脚本文件所在目录。 */
29
29
  const HERE = path.dirname(fileURLToPath(import.meta.url))
30
- /** 发起请求的可执行助手脚本路径。 */
31
- const HELPER = path.join(HERE, 'radeon-api.sh')
30
+ /** 发起请求的助手脚本。
31
+ *
32
+ * Windows 上没有 bash,改用同目录的 PowerShell 7 版本:两个脚本遵守同一份
33
+ * 输出契约(响应体走 stdout,状态码走 stderr 的 ---HTTP <code>--- 标记),
34
+ * 所以上面对 stderr 的解析逻辑不用分平台。
35
+ */
36
+ const HELPER = process.platform === 'win32'
37
+ ? { cmd: 'pwsh', prefix: ['-NoProfile', '-File', path.join(HERE, 'radeon-api.ps1')] }
38
+ : { cmd: path.join(HERE, 'radeon-api.sh'), prefix: [] }
32
39
 
33
40
  /** 待探测的候选档位全集。 */
34
41
  const ALL_TIERS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
@@ -124,12 +131,21 @@ async function once(model, effort) {
124
131
  stream: false,
125
132
  })
126
133
  try {
127
- const { stderr } = await execFileAsync(HELPER, ['POST', '/chat/completions', body], {
128
- maxBuffer: 8 * 1024 * 1024,
129
- })
134
+ const { stderr } = await execFileAsync(
135
+ HELPER.cmd,
136
+ [...HELPER.prefix, 'POST', '/chat/completions', body],
137
+ { maxBuffer: 8 * 1024 * 1024 },
138
+ )
130
139
  const code = /---HTTP (\d+)---/.exec(stderr)?.[1]
131
140
  return { status: Number(code) }
132
141
  } catch (err) {
142
+ // pwsh 没装时 execFile 直接 ENOENT,此后每个组合都会得到 -1。
143
+ // 与其把「没测到」写进档位表,不如当场停下并说明怎么装。
144
+ if (err.code === 'ENOENT') {
145
+ console.error(`✗ 找不到 ${HELPER.cmd}。Windows 上本脚本通过 scripts/radeon-api.ps1 发请求,`
146
+ + '请先安装 PowerShell 7,或改用 Git Bash 直接调用 scripts/radeon-api.sh。')
147
+ process.exit(1)
148
+ }
133
149
  const code = /---HTTP (\d+)---/.exec(err.stderr ?? '')?.[1]
134
150
  return { status: code ? Number(code) : -1, error: (err.stderr ?? err.message ?? '').slice(0, 200) }
135
151
  }
@@ -137,7 +153,7 @@ async function once(model, effort) {
137
153
 
138
154
  /** 模型目录中 data 数组的原始条目。 */
139
155
  const catalog = await new Promise((resolve, reject) => {
140
- execFileAsync(HELPER, ['GET', '/models'], { maxBuffer: 8 * 1024 * 1024 })
156
+ execFileAsync(HELPER.cmd, [...HELPER.prefix, 'GET', '/models'], { maxBuffer: 8 * 1024 * 1024 })
141
157
  .then(({ stdout }) => resolve(JSON.parse(stdout).data ?? []))
142
158
  .catch(reject)
143
159
  })
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env pwsh
2
+ #Requires -Version 7.0
3
+ <#
4
+ .SYNOPSIS
5
+ Radeon Cloud API 调用助手(PowerShell 7 版)。
6
+
7
+ .DESCRIPTION
8
+ 与同目录的 radeon-api.sh 行为一致:发起一次已认证请求,把响应体打到标准输出、
9
+ HTTP 状态码以 ---HTTP <code>--- 的形式打到标准错误,使调用方可以分别解析内容与状态。
10
+
11
+ 密钥有两个来源,按优先级取用:
12
+ 1. 环境变量 RADEON_API_KEY —— 直接给出密钥字面值,适合没有安装 DSH、
13
+ 或不想在本机留存凭据文件的使用者;
14
+ 2. DSH 凭据存储 ~/.dsh/.credentials.yaml 中名为 RADEON_CLOUD_API_KEY 的
15
+ 条目(条目名可用 RADEON_KEY_REF 覆盖)—— 适合已在 DSH 的模型设置页
16
+ 录入过密钥的使用者。
17
+ 无论走哪个来源,密钥都只存在于本进程变量与请求头里,
18
+ 不写盘、不打印、不出现在输出中。
19
+
20
+ 用法: ./scripts/radeon-api.ps1 <METHOD> <PATH> [JSON_BODY]
21
+ $env:RADEON_API_KEY = 'rc-xxxx'; ./scripts/radeon-api.ps1 GET /models
22
+ $env:RAW = '1' 时改为逐块输出 SSE,不做缓冲与状态码拆分。
23
+
24
+ .PARAMETER Method
25
+ HTTP 方法,如 GET / POST。
26
+
27
+ .PARAMETER ApiPath
28
+ 相对端点基础 URL 的路径,如 /models。
29
+
30
+ .PARAMETER Body
31
+ 可选的 JSON 请求体。
32
+ #>
33
+ [CmdletBinding()]
34
+ param(
35
+ [Parameter(Position = 0)]
36
+ [string] $Method,
37
+
38
+ [Parameter(Position = 1)]
39
+ [string] $ApiPath,
40
+
41
+ [Parameter(Position = 2)]
42
+ [string] $Body
43
+ )
44
+
45
+ Set-StrictMode -Version Latest
46
+ $ErrorActionPreference = 'Stop'
47
+
48
+ # 统一按 UTF-8 输出:脚本里有中文,Windows 默认代码页会把它们变成乱码。
49
+ try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) } catch { }
50
+
51
+ # 凭据文件位置。DSH_HOME 未设置时回退到默认的 ~/.dsh。
52
+ $CredFile = if ($env:DSH_HOME) {
53
+ Join-Path $env:DSH_HOME '.credentials.yaml'
54
+ }
55
+ else {
56
+ Join-Path $HOME '.dsh' '.credentials.yaml'
57
+ }
58
+ # 要读取的凭据条目名,可用环境变量覆盖以切换密钥。
59
+ $Ref = if ($env:RADEON_KEY_REF) { $env:RADEON_KEY_REF } else { 'RADEON_CLOUD_API_KEY' }
60
+ # 共享端点基础 URL,可用环境变量覆盖以指向独占端点。
61
+ $Base = if ($env:RADEON_BASE) { $env:RADEON_BASE } else { 'https://developer.amd.com.cn/radeon/api/v1' }
62
+
63
+ # 密钥优先取环境变量;未提供时才回退到凭据文件。
64
+ # 这样在完全没有 DSH 凭据文件的机器上,仅设置 RADEON_API_KEY 即可使用。
65
+ $Key = $env:RADEON_API_KEY
66
+
67
+ if (-not $Key) {
68
+ if (-not (Test-Path -LiteralPath $CredFile -PathType Leaf)) {
69
+ [Console]::Error.WriteLine(@"
70
+ 错误:没有可用的 API 密钥。
71
+ 凭据文件 $CredFile 不存在,且环境变量 RADEON_API_KEY 未设置。
72
+ 请任选其一:
73
+ `$env:RADEON_API_KEY = 'rc-你的密钥'
74
+ 或在 DSH 的模型设置页录入密钥,条目名用 $Ref
75
+ "@)
76
+ exit 1
77
+ }
78
+ # 从凭据文件中按条目名取出密钥值。条目名先做正则转义,
79
+ # 避免名字里的特殊字符被当成模式的一部分(bash 版交给 node 处理同一问题)。
80
+ $text = Get-Content -LiteralPath $CredFile -Raw -Encoding utf8
81
+ if ($null -eq $text) { $text = '' }
82
+ $pattern = '(?m)^\s*' + [regex]::Escape($Ref) + ':\s*(\S+)\s*$'
83
+ $match = [regex]::Match($text, $pattern)
84
+ if (-not $match.Success) {
85
+ [Console]::Error.WriteLine("凭据 $Ref 未设置")
86
+ exit 1
87
+ }
88
+ $Key = $match.Groups[1].Value
89
+ }
90
+
91
+ # 三个位置参数依次为方法、路径(相对 Base)、可选的请求体。
92
+ # 与 bash 版一样在缺参时报用法并退出 1,而不是抛异常——
93
+ # 脚本被 probe-efforts.mjs 这类调用方以 execFile 拉起时,抛异常只会留下一行栈。
94
+ if (-not $Method) {
95
+ [Console]::Error.WriteLine('用法: radeon-api.ps1 <METHOD> <PATH> [JSON_BODY]')
96
+ exit 1
97
+ }
98
+ if (-not $ApiPath) {
99
+ [Console]::Error.WriteLine('缺少路径')
100
+ exit 1
101
+ }
102
+
103
+ $Uri = "$Base$ApiPath"
104
+ $IsRaw = $env:RAW -eq '1'
105
+ $Utf8 = [System.Text.UTF8Encoding]::new($false)
106
+
107
+ # 直接用 HttpClient 而不是 Invoke-WebRequest:后者会把响应整体缓冲,
108
+ # 拿不到 RAW 模式要求的「分片一到就写出」,也不能把非 2xx 当正常结果读回来。
109
+ $handler = [System.Net.Http.HttpClientHandler]::new()
110
+ $client = [System.Net.Http.HttpClient]::new($handler)
111
+ # 与 curl 一致:不设超时。上游偶发慢响应时由调用方自己判重试。
112
+ $client.Timeout = [System.Threading.Timeout]::InfiniteTimeSpan
113
+
114
+ try {
115
+ $request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), $Uri)
116
+ $request.Headers.TryAddWithoutValidation('Authorization', "Bearer $Key") | Out-Null
117
+ if ($IsRaw) {
118
+ $request.Headers.TryAddWithoutValidation('Accept', 'text/event-stream') | Out-Null
119
+ }
120
+ if ($Body) {
121
+ $bodyContent = [System.Net.Http.StringContent]::new($Body, $Utf8)
122
+ }
123
+ else {
124
+ # 无请求体时也要带上 Content-Type,与 bash 版的 -H 一致。
125
+ # HttpClient 不允许单独设置内容头,只能挂一个空内容;
126
+ # 代价是多一个 Content-Length: 0,对网关没有影响。
127
+ $bodyContent = [System.Net.Http.ByteArrayContent]::new([byte[]]@())
128
+ }
129
+ $bodyContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::new('application/json')
130
+ $request.Content = $bodyContent
131
+
132
+ $stdout = [Console]::OpenStandardOutput()
133
+
134
+ if ($IsRaw) {
135
+ # 流式模式:响应头一到就返回,分片一到就直接写出。
136
+ # 此模式不追加状态码标记,也不拆分响应体,调用方需自行处理。
137
+ $response = $client.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
138
+ $stream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
139
+ $buffer = [byte[]]::new(8192)
140
+ while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
141
+ $stdout.Write($buffer, 0, $read)
142
+ $stdout.Flush()
143
+ }
144
+ $stream.Dispose()
145
+ $response.Dispose()
146
+ exit 0
147
+ }
148
+
149
+ # 非流式模式:状态码走标准错误、响应体走标准输出,调用方才能分别解析。
150
+ # 非 2xx 也照常输出响应体与状态码,退出码仍为 0——bash 版的 curl 没有 -f,
151
+ # 语义相同;探针脚本正是靠 ---HTTP 4xx--- 判定「档位被拒绝」。
152
+ $response = $client.SendAsync($request).GetAwaiter().GetResult()
153
+ $bodyText = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
154
+ $status = [int]$response.StatusCode
155
+ # 末尾补一个换行,与 bash 版的 echo 一致;空响应体也补。
156
+ $bytes = $Utf8.GetBytes("$bodyText`n")
157
+ $stdout.Write($bytes, 0, $bytes.Length)
158
+ $stdout.Flush()
159
+ [Console]::Error.WriteLine("---HTTP $status---")
160
+ $response.Dispose()
161
+ exit 0
162
+ }
163
+ catch {
164
+ [Console]::Error.WriteLine("错误:请求失败:$($_.Exception.Message)")
165
+ exit 1
166
+ }
167
+ finally {
168
+ $client.Dispose()
169
+ }
File without changes