@mokuyoaxis/dsh-iris 0.2.4 → 0.2.6
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/.dsh/skills/iris-use/SKILL.md +2 -2
- package/.dsh/skills/iris-use/references/media-contract.md +2 -2
- package/CHANGELOG.md +38 -1
- package/README.en.md +34 -41
- package/README.md +35 -40
- package/bin/dsh-iris.js +29 -12
- package/docs/AGENT_QUICKSTART.md +11 -13
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CLI_MANAGEMENT.md +5 -1
- package/docs/DSH_COMPATIBILITY.md +38 -8
- package/docs/DSH_PROVIDER_CONNECTIONS.md +34 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +2 -0
- package/docs/FUNCTION_MODEL_POOLS.md +84 -0
- package/docs/HEADLESS_CLI.md +7 -7
- package/docs/HOST_ADAPTER_CONTRACT.md +8 -2
- package/docs/HOST_DOCTOR.md +3 -3
- package/docs/MUSIC.md +2 -2
- package/docs/PROVIDER_HEALTH.md +26 -2
- package/docs/ROADMAP.md +8 -4
- package/docs/SECURITY.md +12 -2
- package/docs/VISION_CLI.md +2 -2
- package/docs/releases/0.2.5.md +33 -0
- package/docs/releases/0.2.6.md +50 -0
- package/lib/actions.js +137 -30
- package/lib/adapters.js +36 -9
- package/lib/api.js +2 -1
- package/lib/client.js +717 -180
- package/lib/composite-vision-routing.js +6 -6
- package/lib/config.js +140 -29
- package/lib/doctor.js +9 -2
- package/lib/dsh-compatibility.js +7 -1
- package/lib/dsh-core-adapter.js +22 -11
- package/lib/dsh-host-adapter.js +32 -29
- package/lib/dsh-provider-config.js +35 -0
- package/lib/dsh-provider-source.js +102 -0
- package/lib/headless-vision.js +5 -5
- package/lib/host-contract.js +1 -0
- package/lib/http-vision-model-adapter.js +5 -2
- package/lib/index.js +54 -35
- package/lib/model-directory-service.js +53 -0
- package/lib/model-directory.js +103 -0
- package/lib/models.js +71 -6
- package/lib/ocr-model-routing.js +2 -2
- package/lib/provider-adapters.js +42 -9
- package/lib/provider-catalog.js +14 -9
- package/lib/provider-config-scope.js +16 -0
- package/lib/provider-config-service.js +82 -13
- package/lib/provider-health.js +33 -0
- package/lib/provider-protocol.js +30 -0
- package/lib/vision-model-routing.js +25 -12
- package/package.json +9 -9
- package/user_guide.md +28 -29
package/docs/VISION_CLI.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 独立视觉 CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
版本要求:**Iris 0.2.6**。发布状态见 [README](../README.md);安装 `npm install @mokuyoaxis/dsh-iris@0.2.6` 后运行 `npx dsh-iris ...`,或从源码运行 `node bin/dsh-iris.js ...`。需要 Node.js ≥ 22、依赖 Sharp;从视频文件抽帧还需要 PATH 中的 ffmpeg、ffprobe,复用既有 Core 帧不需要。
|
|
4
4
|
|
|
5
5
|
## 四个命令
|
|
6
6
|
|
|
@@ -95,7 +95,7 @@ dsh-iris vision summarize \
|
|
|
95
95
|
}
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
省略 `--model-ref` 时按视觉功能策略排序:自动池使用已确认用途的合格模型,手选模式只用 `assignments.vision` 名单,停用不调用。只有本地不可用、明确拒绝或正常空正文才允许下一候选;截断、未知终态、收到正文后的失败、取消和超时停止调用。显式 `--model-ref providerId::modelId` 须在允许范围内且只绑定该账号与模型,失败不会替换;视觉命令不接受裸模型名。模型需启用并声明 vision 能力,未配置时直接失败。自持视觉支持 OpenAI-compatible Chat Completions SSE;CLI 不能执行 DSH 宿主视觉资源。见 [功能模型池](FUNCTION_MODEL_POOLS.md)。
|
|
99
99
|
|
|
100
100
|
成功JSON携带实际`modelRef`、`selectionReason`(`explicit` / `assignment`)、模型身份、正常终态及可用usage;旧记录的`pool`来源仍可读取。OCR实际选择保留在每个成功块上。配置凭据、端点和原始错误响应不会出现在诊断中;模型回答和OCR正文是用户内容。
|
|
101
101
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Iris 0.2.5:旧宿主继续使用当前功能
|
|
2
|
+
|
|
3
|
+
0.2.5恢复当前Iris在旧DSH上的支持,将准确版本范围从2个扩展至12个。已验证的旧宿主可使用同一套Music、图片导入、原生Qwen改图和其他现有功能,无需停留在Iris0.1.4。
|
|
4
|
+
|
|
5
|
+
## DSH版本范围
|
|
6
|
+
|
|
7
|
+
- `0.1.2-rc.1`
|
|
8
|
+
- `0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.5-rc.3`
|
|
9
|
+
- `0.1.7-rc.1`、`0.1.7-rc.2`
|
|
10
|
+
- `0.2.0-rc.1`、`0.2.0-rc.2`
|
|
11
|
+
- `0.2.1-alpha.1`、`0.2.1-alpha.2`、`0.2.1-alpha.3`、`0.2.1-alpha.4`
|
|
12
|
+
|
|
13
|
+
engines、可选客户端peer和Doctor同步使用上述范围。保留旧DSH时仍需Node.js ≥22.0.0;宿主自身要求更高时以宿主为准。未列出的版本需单独验收。见 [兼容矩阵](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.5/docs/DSH_COMPATIBILITY.md)。
|
|
14
|
+
|
|
15
|
+
## 视觉桥接修正
|
|
16
|
+
|
|
17
|
+
默认视觉按宿主实际接口能力启用精确模型路由,修复旧SDK已有相同接口却进入历史路径、可能返回空结果或忽略失败终态的问题。旧版同样确认图片输入能力并处理取消;Core、Provider和数据格式保持,未增加生产依赖。
|
|
18
|
+
|
|
19
|
+
## 安装与升级
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.5
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
将`web`替换为实际profile,更新后重载后端并刷新页面,用Iris宿主诊断确认17工具、3项Skill和客户端版本。独立CLI可使用`npm install @mokuyoaxis/dsh-iris@0.2.5`。
|
|
26
|
+
|
|
27
|
+
从较早版本升级时,六类模型功能都需明确分配;DSH默认视觉也需加入视觉名单。见 [功能分配与升级说明](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.5/docs/FUNCTION_MODEL_ASSIGNMENTS.md)。本版不迁移旧任务、作品或个人配置。
|
|
28
|
+
|
|
29
|
+
## 验证范围
|
|
30
|
+
|
|
31
|
+
12个准确版本及现有根0.1.5-rc.1/组件rc.2混合安装,共13组官方SDK组合通过工具、附件、模型路由、HTTP和界面插槽隔离检查;旧Profile/Boot另通过实际tarball安装和装载。
|
|
32
|
+
|
|
33
|
+
模型响应使用离线夹具,Music/Qwen检查验证协议与作品交付。React/DOM和部分会话数据使用fixture;完整浏览器/移动端交互、草稿写回、热更新和从Iris0.1.4个人数据整体升级仍需实际使用验证。见 [用户指南](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.5/user_guide.md)。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Iris 0.2.6:连接复用、功能模型池与目录同步
|
|
2
|
+
|
|
3
|
+
0.2.6 减少首次配置的重复操作,并根据当前连接的目录和明确失败更新候选状态。安装要求见 [README](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.6/README.md),本版升级方式与验证范围如下。
|
|
4
|
+
|
|
5
|
+
## 配置与模型选择
|
|
6
|
+
|
|
7
|
+
- 工作台可引用 DSH 已有的 API Key 连接,只保存连接标识,操作时取得地址与凭据;也可继续手动配置。缺少宿主接口、OAuth 或自定义认证时明确说明限制。独立 CLI 仍使用显式私有配置。
|
|
8
|
+
- 六类功能可选自动池、仅用所选模型或停用。首次确认模型用途时可同时启用所选功能的自动池,免去再次分配。自动池按登记顺序选择,只有明确未受理才安全切换,没有跨请求轮转。
|
|
9
|
+
- 旧手工名单、缺失/空分配及停用状态不会自动启用或扩池。新发现模型须确认用途;临时指定须在当前功能允许范围内且只调用该项。
|
|
10
|
+
|
|
11
|
+
详见 [连接复用](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.6/docs/DSH_PROVIDER_CONNECTIONS.md) 和 [功能模型池](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.6/docs/FUNCTION_MODEL_POOLS.md)。
|
|
12
|
+
|
|
13
|
+
## 目录与能力检测
|
|
14
|
+
|
|
15
|
+
自动请求前按需同步完整目录,成功缓存 10 分钟,同连接并发合并;失败保留旧快照并短暂退避。目录未列出不证明模型下线,不删除手工配置。当前连接在新提交或实测中明确返回模型不存在时,仅隔离该模型及能力,保留历史成功;核对后可单项实测恢复,旧任务查询不改变此阻断。
|
|
16
|
+
|
|
17
|
+
能力状态卡提供检测直达、配置检查与单模型实测。实测先确认费用,忙碌时防止重复提交;未知结果提示核对已有记录。视频、音乐、转写引导至真实任务,不提交空样本探针。
|
|
18
|
+
|
|
19
|
+
TTS 会预先说明尚未支持的实时 WebSocket/其他语音接口,以及复刻/设计模型的专用音色要求。模型不存在错误提供模型 ID、请求域名及地域/协议排查方向。这些修正不承诺任意账号的模型当前可用,见 [能力健康](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.6/docs/PROVIDER_HEALTH.md)。
|
|
20
|
+
|
|
21
|
+
## 工作台与安全修正
|
|
22
|
+
|
|
23
|
+
工作台统一卡片与控件样式并收紧留白。能力检测、功能池与高级分区默认折叠,保留摘要、草稿及正在执行的请求。关闭时只显示“展开”,打开时只显示“收起”;客户端会原位更新旧样式,避免旧 CSS 残留导致双文案。
|
|
24
|
+
|
|
25
|
+
媒体适配层在截断错误前移除本次已知密钥,覆盖无固定前缀 API Key 的上游回显。`sharp` 最低版本提高至 `0.35.5` 并更新锁文件,处理 librsvg 高危公告;见 [安全边界](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.6/docs/SECURITY.md#图像处理依赖)。
|
|
26
|
+
|
|
27
|
+
## 安装与升级
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.6
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
独立 CLI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @mokuyoaxis/dsh-iris@0.2.6
|
|
37
|
+
npx dsh-iris --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
将 `web` 替换为实际 profile。先备份 Iris 私有数据,升级后让 DSH 服务端加载新代码,再刷新页面;宿主诊断应显示一致的 0.2.6。只刷新网页而服务端仍旧时,策略切换会被暂停并提示重启。
|
|
41
|
+
|
|
42
|
+
Node.js 最低仍为 22.0.0,DSH 声明沿用 0.2.5 的 12 个准确版本,见 [兼容矩阵](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.6/docs/DSH_COMPATIBILITY.md)。连接复用按实际公开服务检测,缺少服务的宿主保留手动配置。
|
|
43
|
+
|
|
44
|
+
读取旧配置不自动迁移。用户主动启用新策略或目录同步后会保存相应可选字段;回退旧版前应使用升级前备份,不能假定旧版识别新的策略对象。已有任务继续按原模型、协议与端点绑定观察或补交付;池中有其他模型也不会使已受理任务重新生成。
|
|
45
|
+
|
|
46
|
+
## 验证范围与保留限制
|
|
47
|
+
|
|
48
|
+
本版验证覆盖离线协议/任务回归、凭据隔离和错误脱敏、DSH 公开服务隔离以及实际 Chromium 中的工作台流程。模型响应使用本地夹具,不能据此宣称真实 TTS 已恢复或保证上游可用率。
|
|
49
|
+
|
|
50
|
+
完整 Android 真机触摸、实际热更新、长任务进程重启接管,以及从 Iris 0.1.4 个人数据整体升级仍需单独验收。作品、回收区和同步缓存仍需用户管理磁盘占用;Music 已有显式 502 重发规则可能增加费用,见 [音乐生成](https://github.com/mokuyoaxis/dsh-iris/blob/v0.2.6/docs/MUSIC.md)。
|
package/lib/actions.js
CHANGED
|
@@ -31,13 +31,21 @@ import { formatOcrResult } from './ocr.js';
|
|
|
31
31
|
import { runOcrRequest } from './ocr-model-routing.js';
|
|
32
32
|
import { ffmpegAvailable, extractFrames, probeVideo } from './media-probe.js';
|
|
33
33
|
import { ensurePrivateDir, writePrivateFile } from './private-storage.js';
|
|
34
|
-
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, supportsImageEdit } from './provider-protocol.js';
|
|
34
|
+
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, supportsImageEdit, supportsModelCapability, dashscopeTtsProtocolIssue, dashscopeTtsInputIssue } from './provider-protocol.js';
|
|
35
35
|
import { musicCandidatesFromCatalog, providerTaskBinding } from './provider-catalog.js';
|
|
36
36
|
import { invokeProviderOperation, providerPollTaskView, prepareProviderInput } from './provider-adapter.js';
|
|
37
37
|
import { createConfiguredProviderAdapter as createProviderAdapter } from './provider-adapters.js';
|
|
38
38
|
import { deriveUserState } from './task-semantics.js';
|
|
39
39
|
import { normalizeGenerationInput } from './generation-input.js';
|
|
40
40
|
import { videoTaskCandidates } from './video-input.js';
|
|
41
|
+
import { normalizeDshProvider, withDshProviderConfig } from './dsh-provider-config.js';
|
|
42
|
+
import { modelDirectorySummary, modelUnavailableReason } from './model-directory.js';
|
|
43
|
+
|
|
44
|
+
const PROVIDER_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
45
|
+
'locate', 'ocr', 'media_summarize', 'task_reobserve', 'task_redeliver', 'task_manual_retry',
|
|
46
|
+
'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model']);
|
|
47
|
+
const AUTO_DIRECTORY_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
48
|
+
'locate', 'ocr', 'media_summarize']);
|
|
41
49
|
|
|
42
50
|
const MEDIA_TYPES = {
|
|
43
51
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
@@ -46,6 +54,7 @@ const MEDIA_TYPES = {
|
|
|
46
54
|
|
|
47
55
|
function createConfiguredProviderAdapter(provider, options = {}) {
|
|
48
56
|
return createProviderAdapter(provider, { ...options,
|
|
57
|
+
onModelAvailability: event => store.recordAvailability(event),
|
|
49
58
|
onRateLimit: options.allowRateLimited ? undefined : store.recordRateLimit });
|
|
50
59
|
}
|
|
51
60
|
|
|
@@ -92,11 +101,14 @@ function throwIfAborted(signal) {
|
|
|
92
101
|
* 不因后续轮询或结果下载失败重提,避免同一请求重复计费。
|
|
93
102
|
*/
|
|
94
103
|
function selectProviders(capability, label, requestedModelRef) {
|
|
104
|
+
const policy = models.functionPolicy(store.assignments()[capability], capability);
|
|
105
|
+
if (policy.mode === 'disabled') throw new Error('iris: 没有可用' + label + '模型:此功能已停用,请在功能池中选择调用方式');
|
|
95
106
|
let providers = store.pickAllFor(capability);
|
|
96
107
|
const requested = String(requestedModelRef || '').trim();
|
|
97
108
|
if (requested) {
|
|
98
|
-
const pool = models.orderedModels(models.
|
|
109
|
+
const pool = models.orderedModels(models.functionModelPool(store.providers(), capability, store.assignments()[capability]), capability, store.assignments()[capability], { includeRateLimited: true, includeUnavailable: true });
|
|
99
110
|
const selected = models.resolvePoolModel(pool, requested, capability);
|
|
111
|
+
if (selected?.unavailableCapabilities?.includes(capability)) throw new Error('iris: ' + modelUnavailableReason());
|
|
100
112
|
if (selected?.rateLimited) throw Object.assign(new Error(store.modelHealth(selected.providerId, selected.id, capability).retryAt
|
|
101
113
|
? 'iris: 指定模型正在限流冷却,请稍后使用或手动实测'
|
|
102
114
|
: 'iris: 指定模型额度或预算耗尽,请手动实测成功后再使用'), {
|
|
@@ -106,7 +118,7 @@ function selectProviders(capability, label, requestedModelRef) {
|
|
|
106
118
|
providers = selected ? providers.filter((p) => p.id === selected.providerId && p[field] === selected.id) : [];
|
|
107
119
|
if (!providers.length) throw new Error('iris: 指定模型未分配给该功能、不可用或不具备' + label + '能力');
|
|
108
120
|
}
|
|
109
|
-
if (!providers.length) throw new Error('iris: 没有可用' + label + '
|
|
121
|
+
if (!providers.length) throw new Error('iris: 没有可用' + label + '模型:请检查连接、已确认的模型用途、协议和冷却状态;手选模式还需选择模型');
|
|
110
122
|
return providers;
|
|
111
123
|
}
|
|
112
124
|
|
|
@@ -161,13 +173,13 @@ async function observeProviderCapability(route, capability, operation, { recordS
|
|
|
161
173
|
const result = await operation();
|
|
162
174
|
if (recordSuccess && result && ['accepted', 'completed', 'pending', 'succeeded'].includes(result.kind)) {
|
|
163
175
|
recordProviderHealthSafe(route.provider.id, route.model, capability, {
|
|
164
|
-
ok: true,
|
|
176
|
+
ok: true, stage: 'poll',
|
|
165
177
|
category: 'success',
|
|
166
178
|
note: result.kind === 'accepted' ? '任务已受理' : '供应商请求成功'
|
|
167
179
|
});
|
|
168
180
|
} else if (result && result.error) {
|
|
169
181
|
recordProviderHealthSafe(route.provider.id, route.model, capability, {
|
|
170
|
-
ok: false,
|
|
182
|
+
ok: false, stage: 'poll',
|
|
171
183
|
category: result.error.category,
|
|
172
184
|
httpStatus: result.error.httpStatus,
|
|
173
185
|
note: result.error.safeMessage
|
|
@@ -176,7 +188,7 @@ async function observeProviderCapability(route, capability, operation, { recordS
|
|
|
176
188
|
return result;
|
|
177
189
|
} catch (error) {
|
|
178
190
|
recordProviderHealthSafe(route.provider.id, route.model, capability, {
|
|
179
|
-
ok: false,
|
|
191
|
+
ok: false, stage: 'poll',
|
|
180
192
|
category: error && error.category,
|
|
181
193
|
httpStatus: error && (error.httpStatus ?? error.status),
|
|
182
194
|
note: String(error && (error.safeMessage || error.message) || error)
|
|
@@ -200,7 +212,8 @@ function recordVisionHealth(errors) {
|
|
|
200
212
|
|
|
201
213
|
async function askSingleVisionWithHealth(host, request) {
|
|
202
214
|
try {
|
|
203
|
-
const result = await runVisionRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
215
|
+
const result = await runVisionRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
216
|
+
onModelAvailability: store.recordAvailability, ...request });
|
|
204
217
|
recordVisionHealth(result.errors);
|
|
205
218
|
if (result.via === 'selfstack') recordProviderHealthSafe(result.backendId, result.model, cap.CAPABILITIES.VISION, {
|
|
206
219
|
ok: true, category: 'success', note: '视觉请求成功'
|
|
@@ -462,7 +475,14 @@ export function listActions() {
|
|
|
462
475
|
export async function runAction(host, name, args, { signal } = {}) {
|
|
463
476
|
const fn = actions[name];
|
|
464
477
|
if (!fn) throw new Error(`iris: 未知动作 "${name}"`);
|
|
465
|
-
|
|
478
|
+
const adapter = actionHost(host);
|
|
479
|
+
const execute = async () => {
|
|
480
|
+
if (AUTO_DIRECTORY_ACTIONS.has(name) && !args?.model) await store.refreshAutomaticDirectories({
|
|
481
|
+
capability: ({ image: 'image-gen', image_edit: 'image-gen', video: 'video-gen', music: 'music-gen', tts: 'tts', transcribe: 'transcribe' })[name] || 'vision', signal
|
|
482
|
+
});
|
|
483
|
+
return fn(adapter, args || {}, { signal });
|
|
484
|
+
};
|
|
485
|
+
return PROVIDER_ACTIONS.has(name) ? withDshProviderConfig(adapter, execute, { signal }) : execute();
|
|
466
486
|
}
|
|
467
487
|
|
|
468
488
|
/* ============ 动作实现 ============ */
|
|
@@ -585,7 +605,7 @@ register('music', async (host, args, { signal }) => {
|
|
|
585
605
|
...(optionalActionText(args.retry_502) !== undefined ? { retry_502: args.retry_502 } : {}),
|
|
586
606
|
...(optionalActionText(args.retry_delay_ms) !== undefined ? { retry_delay_ms: args.retry_delay_ms } : {})
|
|
587
607
|
});
|
|
588
|
-
const routes = musicCandidatesFromCatalog({ providers: store.
|
|
608
|
+
const routes = musicCandidatesFromCatalog({ providers: store.runtimeProviders(), assignments: store.assignments() }, args.model);
|
|
589
609
|
const submitted = await submitMusicForDsh({
|
|
590
610
|
candidates: routes.map(route => ({ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
|
|
591
611
|
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
|
|
@@ -721,7 +741,8 @@ register('locate', async (host, args, { signal }) => {
|
|
|
721
741
|
const p = String(args.image_path || '').trim();
|
|
722
742
|
if (source === 'image_path' && (!p || !path.isAbsolute(p))) throw new Error('iris: image_path 必须是绝对路径');
|
|
723
743
|
const mt = MEDIA_TYPES[path.extname(p).toLowerCase()] || 'image/png';
|
|
724
|
-
const r = await runLocateRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
744
|
+
const r = await runLocateRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
745
|
+
onModelAvailability: store.recordAvailability, model: args.model,
|
|
725
746
|
target: args.target, prepareImage: operationSignal => source === 'artifact_id'
|
|
726
747
|
? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
727
748
|
const { width, height } = r;
|
|
@@ -750,7 +771,7 @@ register('ocr', async (host, args, { signal }) => {
|
|
|
750
771
|
if (!mediaType || mediaType === 'image/gif') throw new Error('iris: 不支持的图片格式(png/jpg/jpeg/webp)');
|
|
751
772
|
}
|
|
752
773
|
const result = await runOcrRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION),
|
|
753
|
-
onRateLimit: store.recordRateLimit,
|
|
774
|
+
onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability,
|
|
754
775
|
prepareImage: operationSignal => source === 'artifact_id'
|
|
755
776
|
? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mediaType, operationSignal),
|
|
756
777
|
chunkHeight: args.chunk_height, overlap: args.overlap, signal });
|
|
@@ -1032,22 +1053,32 @@ register('relook', async (host, args, { signal }) => {
|
|
|
1032
1053
|
/** 列出全部供应商及其模型池(管理 GUI 用;apiKey 只给 hint) */
|
|
1033
1054
|
register('providers_list', async (host, args) => {
|
|
1034
1055
|
const list = store.allProviders();
|
|
1056
|
+
const connections = list.some(provider => provider.dshProvider) && hasHostPort(host, 'providerConnections')
|
|
1057
|
+
? await host.ports.providerConnections.list() : [];
|
|
1035
1058
|
return {
|
|
1036
1059
|
ok: true,
|
|
1060
|
+
setupSuggested: Object.keys(store.assignments()).length === 0 && list.every(provider => models.providerModels(provider).length === 0),
|
|
1037
1061
|
providers: list.map((p) => ({
|
|
1038
1062
|
id: p.id,
|
|
1039
1063
|
name: p.name || p.id,
|
|
1040
1064
|
type: p.type || 'openai',
|
|
1041
|
-
baseUrl: p.
|
|
1065
|
+
baseUrl: p.dshProvider ? (connections.find(item => item.provider === p.dshProvider.provider
|
|
1066
|
+
&& item.settingsNs === p.dshProvider.settingsNs)?.baseUrl || '') : p.baseUrl,
|
|
1067
|
+
...(p.dshProvider ? { dshProvider: p.dshProvider,
|
|
1068
|
+
connection: connections.find(item => item.provider === p.dshProvider.provider && item.settingsNs === p.dshProvider.settingsNs)
|
|
1069
|
+
|| { available: false, reason: 'DSH 原连接不可用,请检查宿主供应商设置' } } : {}),
|
|
1042
1070
|
mediaBaseUrl: p.mediaBaseUrl || '',
|
|
1043
1071
|
enabled: p.enabled !== false,
|
|
1044
1072
|
auth: p.auth === 'none' ? 'none' : 'bearer',
|
|
1045
1073
|
...selectMediaProtocol(p),
|
|
1046
1074
|
visionInput: p.visionInput || null,
|
|
1047
|
-
|
|
1048
|
-
|
|
1075
|
+
directory: modelDirectorySummary(p),
|
|
1076
|
+
apiKeyHint: p.dshProvider ? '由 DSH 管理' : String(p.apiKey || '').length <= 8 ? '****' : String(p.apiKey).slice(0, 3) + '****' + String(p.apiKey).slice(-4),
|
|
1077
|
+
models: models.providerModelsWithDirectory(p).map((m) => ({
|
|
1049
1078
|
id: m.id,
|
|
1050
1079
|
capabilities: m.capabilities,
|
|
1080
|
+
...(m.directoryOnly ? { directoryOnly: true } : {}),
|
|
1081
|
+
...(m.catalogStatus ? { catalogStatus: m.catalogStatus } : {}),
|
|
1051
1082
|
...(m.imageProtocol !== undefined ? { imageProtocol: m.imageProtocol } : {}),
|
|
1052
1083
|
...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
|
|
1053
1084
|
...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}),
|
|
@@ -1064,6 +1095,12 @@ register('providers_list', async (host, args) => {
|
|
|
1064
1095
|
};
|
|
1065
1096
|
});
|
|
1066
1097
|
|
|
1098
|
+
register('providers_host_connections', async host => {
|
|
1099
|
+
if (!hasHostPort(host, 'providerConnections')) return { ok: true, connections: [],
|
|
1100
|
+
note: '当前 DSH 未提供连接复用接口,请手动添加 Iris 供应商' };
|
|
1101
|
+
return { ok: true, connections: await host.ports.providerConnections.list() };
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1067
1104
|
/** 新增/更新供应商 */
|
|
1068
1105
|
register('providers_upsert', async (host, args) => {
|
|
1069
1106
|
const id = args.id || undefined;
|
|
@@ -1073,6 +1110,16 @@ register('providers_upsert', async (host, args) => {
|
|
|
1073
1110
|
for (const key of allowed) {
|
|
1074
1111
|
if (Object.prototype.hasOwnProperty.call(args, key)) patch[key] = args[key];
|
|
1075
1112
|
}
|
|
1113
|
+
if (Object.hasOwn(args, 'dshProvider')) {
|
|
1114
|
+
patch.dshProvider = normalizeDshProvider(args.dshProvider);
|
|
1115
|
+
const source = requireHostPort(host, 'providerConnections', '复用 DSH 供应商连接');
|
|
1116
|
+
const connection = (await source.list()).find(item => item.provider === patch.dshProvider.provider
|
|
1117
|
+
&& item.settingsNs === patch.dshProvider.settingsNs);
|
|
1118
|
+
if (!connection?.available) throw new Error(connection?.reason || 'DSH 连接不可用');
|
|
1119
|
+
if (!['dashscope', 'openai-images', 'openai-chat-images', 'openai-responses-images'].includes(patch.mediaProtocol)) {
|
|
1120
|
+
throw new Error('复用 DSH 连接时请明确选择媒体协议');
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1076
1123
|
if (!id && !Object.prototype.hasOwnProperty.call(patch, 'enabled')) patch.enabled = true;
|
|
1077
1124
|
const saved = store.upsert(patch);
|
|
1078
1125
|
return { ok: true, providerId: saved.id };
|
|
@@ -1098,21 +1145,27 @@ register('providers_set_models', async (host, args) => {
|
|
|
1098
1145
|
register('providers_discover', async (host, args, { signal }) => {
|
|
1099
1146
|
const p = store.providerById(String(args.id || ''));
|
|
1100
1147
|
if (!p) throw new Error('iris: 供应商不存在或未启用');
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const discovered = await invokeProviderOperation(providerAdapter, 'discover', { signal });
|
|
1105
|
-
discoveredModels = discovered.models;
|
|
1106
|
-
} catch (err) {
|
|
1107
|
-
throw new Error('iris: 模型发现失败(保留现有池):' + String(err.message || err));
|
|
1108
|
-
}
|
|
1109
|
-
const entries = discoveredModels.map((model) => ({ id: model.id, capabilities: models.capabilitiesOfDiscoveredModel(model) }));
|
|
1148
|
+
const [updated] = await store.refreshProviderDirectories({ providerId: p.id, force: true, signal });
|
|
1149
|
+
if (updated.error) throw new Error('iris: 模型发现失败(保留现有池):' + updated.error);
|
|
1150
|
+
const entries = updated.directory.models;
|
|
1110
1151
|
const mediaCount = entries.filter((model) => model.capabilities.length).length;
|
|
1111
1152
|
store.mergeDiscoveredModels(p.id, entries);
|
|
1112
|
-
return { ok: true, total:
|
|
1153
|
+
return { ok: true, total: entries.length, media: mediaCount,
|
|
1113
1154
|
text: `[iris] ${p.name}:发现 ${entries.length} 个模型,${mediaCount} 个有能力声明;未标注模型请在模型池中设置能力` };
|
|
1114
1155
|
});
|
|
1115
1156
|
|
|
1157
|
+
/** 只更新目录快照;新增项在管理视图待确认,不改模型用途与功能名单。 */
|
|
1158
|
+
register('providers_sync_directory', async (host, args, { signal }) => {
|
|
1159
|
+
const id = String(args.id || '').trim();
|
|
1160
|
+
if (!store.providerById(id)) throw new Error('iris: 供应商不存在或连接未就绪');
|
|
1161
|
+
const [result] = await store.refreshProviderDirectories({ providerId: id, force: args.force === true, signal });
|
|
1162
|
+
if (result.error) return { ok: true, synced: false, error: result.error,
|
|
1163
|
+
text: '目录同步失败,已保留上次完整目录:' + result.error };
|
|
1164
|
+
return { ok: true, synced: true, refreshed: result.refreshed,
|
|
1165
|
+
text: result.refreshed ? '模型目录已同步;新模型仍需确认用途' : '目录仍在有效期内',
|
|
1166
|
+
directory: modelDirectorySummary(store.providerById(id)) };
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1116
1169
|
/** 能力实测(红色测试图):对某供应商的 vision 模型跑 testVisionCapability */
|
|
1117
1170
|
register('providers_test_vision', async (host, args, { signal }) => {
|
|
1118
1171
|
const p = store.providerById(String(args.id || ''));
|
|
@@ -1121,7 +1174,7 @@ register('providers_test_vision', async (host, args, { signal }) => {
|
|
|
1121
1174
|
if (!vision) return { ok: true, tested: false, text: '该供应商未配置 vision 模型,跳过' };
|
|
1122
1175
|
const model = vision.id;
|
|
1123
1176
|
if (args.confirm_paid !== true) throw new Error('iris: 视觉实测会发起真实供应商请求,请先明确确认');
|
|
1124
|
-
const r = await probeVisionModel(p, model, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal });
|
|
1177
|
+
const r = await probeVisionModel(p, model, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal, onModelAvailability: store.recordAvailability });
|
|
1125
1178
|
store.setModelVerified(p.id, model, cap.CAPABILITIES.VISION, {
|
|
1126
1179
|
ok: r.ok,
|
|
1127
1180
|
note: r.ok ? '认出红色测试图' : (r.error || '未识别出红色'),
|
|
@@ -1146,7 +1199,7 @@ async function probeModel(host, p, modelId, capability, signal) {
|
|
|
1146
1199
|
return { skipped: true, ok: false, note: 'Music 不发起小样本探测;请通过音乐功能提交实际创作要求' };
|
|
1147
1200
|
}
|
|
1148
1201
|
if (capability === cap.CAPABILITIES.VISION) {
|
|
1149
|
-
const r = await probeVisionModel(p, modelId, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal });
|
|
1202
|
+
const r = await probeVisionModel(p, modelId, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal, onModelAvailability: store.recordAvailability });
|
|
1150
1203
|
return {
|
|
1151
1204
|
ok: r.ok,
|
|
1152
1205
|
note: r.ok ? '认出红色测试图' : (r.error || '未识别出红色'),
|
|
@@ -1158,6 +1211,8 @@ async function probeModel(host, p, modelId, capability, signal) {
|
|
|
1158
1211
|
return { skipped: true, ok: false, note: '需真实音频;可用 CLI models test --capability transcribe --input 提供素材进行实测恢复' };
|
|
1159
1212
|
}
|
|
1160
1213
|
if (capability === cap.CAPABILITIES.TTS) {
|
|
1214
|
+
const issue = selectMediaProtocol(p).mediaProtocol === 'dashscope' && dashscopeTtsInputIssue(modelId);
|
|
1215
|
+
if (issue) return { skipped: true, ok: false, note: issue };
|
|
1161
1216
|
const providerAdapter = createConfiguredProviderAdapter(p, { allowRateLimited: true });
|
|
1162
1217
|
const submitted = await invokeProviderOperation(providerAdapter, 'submit', {
|
|
1163
1218
|
capability: 'tts', model: modelId, input: { text: '你好' }, signal
|
|
@@ -1231,6 +1286,14 @@ register('providers_add_model', async (host, args) => {
|
|
|
1231
1286
|
return { ok: true, text: `[iris] 已添加模型 ${modelId}(能力:${(caps || []).join(',') || '未标注'})` };
|
|
1232
1287
|
});
|
|
1233
1288
|
|
|
1289
|
+
register('providers_confirm_model', async (_host, args) => {
|
|
1290
|
+
const entry = store.confirmModelUsage(args.id, args.model_id, args.capabilities, {
|
|
1291
|
+
enableAutomatic: args.enableAutomatic, musicProtocol: args.musicProtocol
|
|
1292
|
+
});
|
|
1293
|
+
return { ok: true, model_id: entry.id, text: args.enableAutomatic
|
|
1294
|
+
? '已确认模型用途,并启用所选功能的自动池' : '已确认模型用途;已有调用方式保持' };
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1234
1297
|
/** 从池移除模型 */
|
|
1235
1298
|
register('providers_remove_model', async (host, args) => {
|
|
1236
1299
|
const ok = store.removeProviderModel(String(args.id || ''), String(args.model_id || ''));
|
|
@@ -1303,22 +1366,62 @@ register('providers_test_model', async (host, args, { signal }) => {
|
|
|
1303
1366
|
register('assignments_get', async (host, args) => {
|
|
1304
1367
|
const providers = store.allProviders();
|
|
1305
1368
|
const configured = new Set(store.providers().map(provider => provider.id));
|
|
1369
|
+
const connections = providers.some(provider => provider.dshProvider) && hasHostPort(host, 'providerConnections')
|
|
1370
|
+
? await host.ports.providerConnections.list() : [];
|
|
1371
|
+
for (const provider of providers) {
|
|
1372
|
+
if (provider.enabled !== false && provider.dshProvider && connections.some(item => item.available
|
|
1373
|
+
&& item.provider === provider.dshProvider.provider && item.settingsNs === provider.dshProvider.settingsNs)) {
|
|
1374
|
+
configured.add(provider.id);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const previewProviders = providers.map(provider => provider.dshProvider ? { ...provider,
|
|
1378
|
+
baseUrl: connections.find(item => item.provider === provider.dshProvider.provider
|
|
1379
|
+
&& item.settingsNs === provider.dshProvider.settingsNs)?.baseUrl } : provider);
|
|
1306
1380
|
const pool = models.modelPool(providers, { includeHostVision: true });
|
|
1307
1381
|
const byCap = {};
|
|
1308
1382
|
for (const m of pool) {
|
|
1309
1383
|
for (const c of m.capabilities) {
|
|
1310
1384
|
if (!byCap[c]) byCap[c] = [];
|
|
1311
|
-
const provider =
|
|
1385
|
+
const provider = previewProviders.find(provider => provider.id === m.providerId);
|
|
1386
|
+
const hostResource = m.ref === models.HOST_VISION_REF;
|
|
1387
|
+
const protocolSupported = hostResource || supportsModelCapability(provider, m.id, c);
|
|
1388
|
+
const modelUnavailable = m.unavailableCapabilities?.includes(c) === true;
|
|
1389
|
+
const connected = hostResource ? hasHostPort(host, 'textModel') : configured.has(m.providerId);
|
|
1312
1390
|
byCap[c].push({ ref: m.ref, id: m.id, providerId: m.providerId,
|
|
1313
1391
|
label: m.ref === models.HOST_VISION_REF ? 'DSH 当前默认模型' : m.id + ' · ' + m.providerId,
|
|
1314
|
-
unavailable:
|
|
1392
|
+
unavailable: !connected || !protocolSupported || m.rateLimited === true || modelUnavailable,
|
|
1393
|
+
modelUnavailable,
|
|
1394
|
+
pendingConfirmation: m.source === 'discovered', protocolSupported,
|
|
1395
|
+
reason: !connected ? '连接未就绪,请检查供应商或宿主服务' : !protocolSupported
|
|
1396
|
+
? (c === cap.CAPABILITIES.TTS && selectMediaProtocol(provider).mediaProtocol === 'dashscope'
|
|
1397
|
+
&& dashscopeTtsProtocolIssue(m.id)) || '当前接口协议不支持此功能'
|
|
1398
|
+
: modelUnavailable ? modelUnavailableReason() : m.rateLimited ? '限流冷却或额度不可用,请稍后或检查账号' : m.source === 'discovered' ? '已发现,待确认模型用途' : '',
|
|
1315
1399
|
...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id) } : {}) });
|
|
1316
1400
|
}
|
|
1317
1401
|
}
|
|
1318
1402
|
// order:归一化的有序分配列表(GUI 直接渲染 failover 顺序,无需处理单字符串旧格式)
|
|
1319
1403
|
const order = {};
|
|
1320
1404
|
for (const c of Object.values(cap.CAPABILITIES)) order[c] = store.assignmentOrder(c);
|
|
1321
|
-
|
|
1405
|
+
const policies = {}, readiness = {};
|
|
1406
|
+
for (const capability of Object.values(cap.CAPABILITIES)) {
|
|
1407
|
+
const assignment = store.assignments()[capability];
|
|
1408
|
+
const policy = models.functionPolicy(assignment, capability);
|
|
1409
|
+
policies[capability] = policy;
|
|
1410
|
+
if (policy.mode === 'auto') order[capability] = models.orderedModels(
|
|
1411
|
+
models.functionModelPool(previewProviders, capability, assignment, { includeHostVision: true }),
|
|
1412
|
+
capability, assignment, { includeRateLimited: true, includeUnavailable: true }).map(model => model.ref);
|
|
1413
|
+
const options = byCap[capability] || [];
|
|
1414
|
+
const candidates = options.filter(model => order[capability].includes(model.ref) && !model.unavailable);
|
|
1415
|
+
let note = '';
|
|
1416
|
+
if (policy.mode === 'disabled') note = '此功能已停用,可选择自动池或手选模型';
|
|
1417
|
+
else if (policy.mode === 'manual' && !order[capability].length) note = '尚未选择模型,可添加所选项或改用自动池';
|
|
1418
|
+
else if (!options.length) note = '请先添加模型,并确认它可用于此功能';
|
|
1419
|
+
else if (!candidates.length) note = options.some(model => model.pendingConfirmation)
|
|
1420
|
+
? '发现的模型尚待确认用途,请在供应商中确认功能'
|
|
1421
|
+
: options.find(model => model.reason)?.reason || '当前没有匹配的候选,请检查功能范围和模型';
|
|
1422
|
+
readiness[capability] = { mode: policy.mode, candidateCount: candidates.length, note };
|
|
1423
|
+
}
|
|
1424
|
+
return { ok: true, assignments: store.assignments(), order, poolByCapability: byCap, policies, readiness };
|
|
1322
1425
|
});
|
|
1323
1426
|
|
|
1324
1427
|
/**
|
|
@@ -1329,6 +1432,10 @@ register('assignments_get', async (host, args) => {
|
|
|
1329
1432
|
register('assignments_set', async (host, args) => {
|
|
1330
1433
|
const capability = String(args.capability || '').trim();
|
|
1331
1434
|
if (!capability) throw new Error('iris: capability 不能为空');
|
|
1435
|
+
if (Object.hasOwn(args, 'mode')) {
|
|
1436
|
+
const policy = store.setFunctionPolicy(capability, args.mode, { hostVision: args.hostVision });
|
|
1437
|
+
return { ok: true, capability, mode: policy.mode, model_refs: store.assignmentOrder(capability) };
|
|
1438
|
+
}
|
|
1332
1439
|
const listArg = Array.isArray(args.model_refs) ? args.model_refs : args.model_ids;
|
|
1333
1440
|
if (Array.isArray(listArg)) {
|
|
1334
1441
|
const refs = listArg.map((x) => String(x || '').trim()).filter(Boolean);
|
|
@@ -1465,7 +1572,7 @@ register('media_summarize', async (host, args, { signal }) => {
|
|
|
1465
1572
|
if (!ffmpegAvailable()) throw new Error('iris: ffmpeg/ffprobe 不可用,无法分析视频(可选安装 ffmpeg 后启用)');
|
|
1466
1573
|
}
|
|
1467
1574
|
const { answer, via, model, sheet, meta, frames } = await runSummaryRequest(host, {
|
|
1468
|
-
onRateLimit: store.recordRateLimit,
|
|
1575
|
+
onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability,
|
|
1469
1576
|
providers: store.pickAllFor(cap.CAPABILITIES.VISION), model: args.model, question: args.question, signal,
|
|
1470
1577
|
async prepareMedia(operationSignal) {
|
|
1471
1578
|
if (source === 'core-artifacts') {
|
package/lib/adapters.js
CHANGED
|
@@ -367,7 +367,15 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
|
|
|
367
367
|
body: JSON.stringify({ model, input: { text, voice } })
|
|
368
368
|
});
|
|
369
369
|
const body = await readJson(res);
|
|
370
|
-
if (!res.ok)
|
|
370
|
+
if (!res.ok) {
|
|
371
|
+
const error = providerError(res.status, body, '合成失败', res.headers);
|
|
372
|
+
if (res.status === 404 && (/NotFound\.Model|model_not_found/i.test(error.providerCode)
|
|
373
|
+
|| /model (?:not exist|can not be found|.*does not exist)/i.test(String(error.rootCause)))) {
|
|
374
|
+
error.category = 'invalid_parameter';
|
|
375
|
+
error.message = `当前语音接口找不到模型 ${model}(${new URL(apiBase).hostname});请核对模型 ID、所属地域及调用协议。上游:${error.rootCause}`;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
371
379
|
const audio = body && body.output && body.output.audio;
|
|
372
380
|
const url = audio && (audio.url || audio.audio_url);
|
|
373
381
|
const b64 = audio && typeof audio.data === 'string' ? audio.data : null;
|
|
@@ -510,6 +518,7 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
510
518
|
const found = new Map();
|
|
511
519
|
const maxPages = dashscope ? 50 : 1;
|
|
512
520
|
let loaded = 0;
|
|
521
|
+
let expectedTotal = null;
|
|
513
522
|
for (let page = 1; page <= maxPages; page++) {
|
|
514
523
|
const url = new URL(endpoint);
|
|
515
524
|
if (dashscope) {
|
|
@@ -519,14 +528,24 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
519
528
|
const res = await fetch(url, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
|
|
520
529
|
const body = await readJson(res);
|
|
521
530
|
if (!res.ok) throw providerError(res.status, body, '模型发现失败', res.headers);
|
|
531
|
+
if (!body || body.success === false) throw new ProviderContractError('模型目录响应无效,已保留上次完整目录', {
|
|
532
|
+
stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
|
|
533
|
+
});
|
|
522
534
|
const output = body && body.output;
|
|
523
535
|
const items = (output && Array.isArray(output.models) && output.models)
|
|
524
536
|
|| (body && Array.isArray(body.data) && body.data)
|
|
525
|
-
|| (Array.isArray(body && body.models) ? body.models :
|
|
537
|
+
|| (Array.isArray(body && body.models) ? body.models : null);
|
|
538
|
+
if (!items) throw new ProviderContractError('模型目录缺少模型数组,已保留上次完整目录', {
|
|
539
|
+
stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
|
|
540
|
+
});
|
|
526
541
|
loaded += items.length;
|
|
527
542
|
for (const item of items) {
|
|
528
|
-
const
|
|
529
|
-
if (
|
|
543
|
+
const rawId = typeof item === 'string' ? item : item?.id || item?.model;
|
|
544
|
+
if (typeof rawId !== 'string' || !rawId.trim()) throw new ProviderContractError('模型目录包含无效模型 ID,已保留上次完整目录', {
|
|
545
|
+
stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
|
|
546
|
+
});
|
|
547
|
+
const id = rawId.trim();
|
|
548
|
+
if (found.has(id)) continue;
|
|
530
549
|
const capabilities = Array.isArray(item?.capabilities)
|
|
531
550
|
? item.capabilities.filter((value) => typeof value === 'string' && value)
|
|
532
551
|
: [];
|
|
@@ -538,17 +557,25 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
538
557
|
if (!Array.isArray(item?.capabilities)) {
|
|
539
558
|
if (responseModalities.includes('image')) capabilities.push('IG');
|
|
540
559
|
if (responseModalities.includes('video')) capabilities.push('VG');
|
|
541
|
-
|
|
542
|
-
if (requestModalities.
|
|
560
|
+
// 音频理解不证明支持文件转写;混合视频/图像/文本输出也不证明独立TTS。
|
|
561
|
+
if (requestModalities.length === 1 && requestModalities[0] === 'text'
|
|
562
|
+
&& responseModalities.length === 1 && responseModalities[0] === 'audio') capabilities.push('TTS');
|
|
543
563
|
if (requestModalities.includes('image') && responseModalities.includes('text')) capabilities.push('VU');
|
|
544
564
|
}
|
|
545
565
|
const normalizedCapabilities = [...new Set(capabilities)];
|
|
546
566
|
found.set(id, normalizedCapabilities.length ? { id, capabilities: normalizedCapabilities } : id);
|
|
547
567
|
}
|
|
548
568
|
if (!dashscope) break;
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
569
|
+
const total = output?.total === undefined ? expectedTotal : Number(output.total);
|
|
570
|
+
if (total !== null && (!Number.isSafeInteger(total) || total < 0
|
|
571
|
+
|| (expectedTotal !== null && total !== expectedTotal) || loaded > total || (!items.length && loaded < total)
|
|
572
|
+
|| (loaded === total && found.size !== loaded))) {
|
|
573
|
+
throw new ProviderContractError('模型目录分页不完整或总数变化,已保留上次完整目录', {
|
|
574
|
+
stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
if (total !== null) expectedTotal = total;
|
|
578
|
+
if ((total !== null && loaded === total) || (total === null && items.length < 100)) break;
|
|
552
579
|
if (page === maxPages) throw new Error('iris: 模型目录超过安全分页上限,已保留原模型池');
|
|
553
580
|
}
|
|
554
581
|
return [...found.values()];
|
package/lib/api.js
CHANGED
|
@@ -66,7 +66,8 @@ function providerPublic(p) {
|
|
|
66
66
|
mediaBaseUrl: p.mediaBaseUrl || '',
|
|
67
67
|
enabled: p.enabled !== false,
|
|
68
68
|
...selectMediaProtocol(p),
|
|
69
|
-
apiKeyHint: keyHint(p.apiKey),
|
|
69
|
+
apiKeyHint: p.dshProvider ? '由 DSH 管理' : keyHint(p.apiKey),
|
|
70
|
+
...(p.dshProvider ? { dshProvider: p.dshProvider } : {}),
|
|
70
71
|
capabilities: capabilitiesOf(p),
|
|
71
72
|
// 迁移/推断结果是否与原始声明不同(UI 提示旧配置已推断)
|
|
72
73
|
capabilityInferred: !Array.isArray(p.capabilities) || p.capabilities.length === 0,
|