@mokuyoaxis/dsh-iris 0.2.3 → 0.2.5
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-compose-media/references/workflows.md +1 -1
- package/.dsh/skills/iris-use/SKILL.md +2 -1
- package/.dsh/skills/iris-use/references/media-contract.md +12 -2
- package/CHANGELOG.md +27 -1
- package/README.en.md +34 -44
- package/README.md +34 -44
- package/bin/dsh-iris.js +8 -0
- package/docs/AGENT_QUICKSTART.md +11 -13
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/CLI_MANAGEMENT.md +11 -3
- package/docs/DSH_COMPATIBILITY.md +69 -0
- package/docs/DSH_RC2_ADAPTATION.md +4 -2
- package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +63 -0
- package/docs/HEADLESS_CLI.md +10 -8
- package/docs/HOST_ADAPTER_CONTRACT.md +11 -7
- package/docs/HOST_DOCTOR.md +5 -3
- package/docs/IMAGE_IMPORT.md +49 -0
- package/docs/ROADMAP.md +6 -2
- package/docs/VISION_CLI.md +3 -3
- package/docs/VISION_MODEL.md +1 -1
- package/docs/WORKBENCH_ARTIFACTS.md +4 -2
- package/docs/releases/0.2.4.md +27 -0
- package/docs/releases/0.2.5.md +33 -0
- package/lib/actions.js +32 -12
- package/lib/adapters.js +5 -2
- package/lib/client.js +28 -29
- package/lib/command-service.js +16 -1
- package/lib/config.js +16 -22
- package/lib/doctor.js +4 -10
- package/lib/dsh-compatibility.js +16 -0
- package/lib/dsh-core-adapter.js +7 -1
- package/lib/dsh-host-adapter.js +22 -29
- package/lib/host-runtime.js +1 -0
- package/lib/image-edit-input.js +7 -5
- package/lib/image-import.js +61 -0
- package/lib/index.js +39 -15
- package/lib/models.js +9 -3
- package/lib/ocr-model-routing.js +1 -0
- package/lib/provider-adapters.js +11 -2
- package/lib/provider-catalog.js +6 -8
- package/lib/provider-config-service.js +5 -5
- package/lib/provider-protocol.js +6 -0
- package/lib/vision-model-routing.js +22 -8
- package/package.json +9 -8
- package/user_guide.md +27 -26
package/lib/actions.js
CHANGED
|
@@ -23,7 +23,7 @@ import { registerMedia, mediaLinksOf, webBase } from './media.js';
|
|
|
23
23
|
import { RED_TEST_IMAGE } from './vision.js';
|
|
24
24
|
import { runVisionRequest, readVisionImageFile, probeVisionModel, visionImageFromDataUrl } from './vision-model-routing.js';
|
|
25
25
|
import { HOST_CONTRACT_VERSION, HOST_PORTS, defineHostAdapter, hasHostPort, requireHostPort } from './host-contract.js';
|
|
26
|
-
import { coreSnapshotForDsh, cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, submitMusicForDsh, submitProviderTaskForDsh, watchProviderTaskForDsh } from './dsh-core-adapter.js';
|
|
26
|
+
import { coreSnapshotForDsh, importImageForDsh, cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, submitMusicForDsh, submitProviderTaskForDsh, watchProviderTaskForDsh } from './dsh-core-adapter.js';
|
|
27
27
|
import { runLocateRequest, runSummaryRequest } from './composite-vision-routing.js';
|
|
28
28
|
import { normalizeSummarySource, summaryFrameRecords, summaryTimelineLabel } from './summary-input.js';
|
|
29
29
|
import { normalizeVisionImageSource } from './vision-input.js';
|
|
@@ -31,7 +31,7 @@ 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 } from './provider-protocol.js';
|
|
34
|
+
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, supportsImageEdit } 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';
|
|
@@ -49,6 +49,11 @@ function createConfiguredProviderAdapter(provider, options = {}) {
|
|
|
49
49
|
onRateLimit: options.allowRateLimited ? undefined : store.recordRateLimit });
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function imageRoutingForModel(provider, modelId) {
|
|
53
|
+
const routing = selectImageProtocol(provider, modelId);
|
|
54
|
+
return { ...routing, editSupported: supportsImageEdit(routing.mediaProtocol, modelId) };
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
const EMPTY_ACTION_HOST = defineHostAdapter({
|
|
53
58
|
id: 'iris-hostless',
|
|
54
59
|
ports: {},
|
|
@@ -90,7 +95,7 @@ function selectProviders(capability, label, requestedModelRef) {
|
|
|
90
95
|
let providers = store.pickAllFor(capability);
|
|
91
96
|
const requested = String(requestedModelRef || '').trim();
|
|
92
97
|
if (requested) {
|
|
93
|
-
const pool = models.modelPool(store.providers());
|
|
98
|
+
const pool = models.orderedModels(models.modelPool(store.providers()), capability, store.assignments()[capability], { includeRateLimited: true });
|
|
94
99
|
const selected = models.resolvePoolModel(pool, requested, capability);
|
|
95
100
|
if (selected?.rateLimited) throw Object.assign(new Error(store.modelHealth(selected.providerId, selected.id, capability).retryAt
|
|
96
101
|
? 'iris: 指定模型正在限流冷却,请稍后使用或手动实测'
|
|
@@ -99,7 +104,7 @@ function selectProviders(capability, label, requestedModelRef) {
|
|
|
99
104
|
});
|
|
100
105
|
const field = models.CAP_FIELD[capability];
|
|
101
106
|
providers = selected ? providers.filter((p) => p.id === selected.providerId && p[field] === selected.id) : [];
|
|
102
|
-
if (!providers.length) throw new Error('iris:
|
|
107
|
+
if (!providers.length) throw new Error('iris: 指定模型未分配给该功能、不可用或不具备' + label + '能力');
|
|
103
108
|
}
|
|
104
109
|
if (!providers.length) throw new Error('iris: 没有可用' + label + '供应商——先在 Iris 设置里添加、标注并分配模型');
|
|
105
110
|
return providers;
|
|
@@ -462,6 +467,15 @@ export async function runAction(host, name, args, { signal } = {}) {
|
|
|
462
467
|
|
|
463
468
|
/* ============ 动作实现 ============ */
|
|
464
469
|
|
|
470
|
+
register('image_import', async (_host, args, { signal }) => {
|
|
471
|
+
if (args.image_path !== undefined && args.image_bytes !== undefined) throw new Error('图片导入只能选择一个来源');
|
|
472
|
+
const { artifact } = await importImageForDsh({ imagePath: args.image_path, imageBytes: args.image_bytes, signal });
|
|
473
|
+
const imageUrl = '/iris/api/core/artifact/' + artifact.id + '/media';
|
|
474
|
+
const text = `已导入图片(${artifact.metadata.width}×${artifact.metadata.height}),原文件保留。\n作品ID:${artifact.id}`;
|
|
475
|
+
return { ok: true, storage: 'core', artifact, artifactId: artifact.id, artifactIds: [artifact.id], imageUrl, text,
|
|
476
|
+
blocks: [{ type: 'text', text: text + `\n图片:${imageUrl}` }] };
|
|
477
|
+
});
|
|
478
|
+
|
|
465
479
|
/* ---------- 🖼️ 画图 ---------- */
|
|
466
480
|
async function imageAction(host, args, { signal }, editing = false) {
|
|
467
481
|
if (editing && !args.source_artifact_id) throw new Error('iris: 改图必须提供 source_artifact_id');
|
|
@@ -1038,7 +1052,7 @@ register('providers_list', async (host, args) => {
|
|
|
1038
1052
|
...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
|
|
1039
1053
|
...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}),
|
|
1040
1054
|
...(m.capabilities.includes(cap.CAPABILITIES.VISION) ? { visionInputEffective: visionInputLimits(p, m.id) } : {}),
|
|
1041
|
-
...(m.capabilities.includes(cap.CAPABILITIES.IMAGE) ? { imageRouting:
|
|
1055
|
+
...(m.capabilities.includes(cap.CAPABILITIES.IMAGE) ? { imageRouting: imageRoutingForModel(p, m.id) } : {}),
|
|
1042
1056
|
verified: m.verified || null,
|
|
1043
1057
|
health: Object.fromEntries(m.capabilities.map((capability) => [
|
|
1044
1058
|
capability, store.modelHealth(p.id, m.id, capability)
|
|
@@ -1103,11 +1117,11 @@ register('providers_discover', async (host, args, { signal }) => {
|
|
|
1103
1117
|
register('providers_test_vision', async (host, args, { signal }) => {
|
|
1104
1118
|
const p = store.providerById(String(args.id || ''));
|
|
1105
1119
|
if (!p) throw new Error('iris: 供应商不存在');
|
|
1106
|
-
const
|
|
1107
|
-
if (!
|
|
1108
|
-
const model =
|
|
1120
|
+
const vision = models.providerModels(p).find(m => m.capabilities.includes(cap.CAPABILITIES.VISION));
|
|
1121
|
+
if (!vision) return { ok: true, tested: false, text: '该供应商未配置 vision 模型,跳过' };
|
|
1122
|
+
const model = vision.id;
|
|
1109
1123
|
if (args.confirm_paid !== true) throw new Error('iris: 视觉实测会发起真实供应商请求,请先明确确认');
|
|
1110
|
-
const r = await probeVisionModel(
|
|
1124
|
+
const r = await probeVisionModel(p, model, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal });
|
|
1111
1125
|
store.setModelVerified(p.id, model, cap.CAPABILITIES.VISION, {
|
|
1112
1126
|
ok: r.ok,
|
|
1113
1127
|
note: r.ok ? '认出红色测试图' : (r.error || '未识别出红色'),
|
|
@@ -1287,17 +1301,23 @@ register('providers_test_model', async (host, args, { signal }) => {
|
|
|
1287
1301
|
|
|
1288
1302
|
/** 查询:全部分配 + 每个能力可用的模型池(client 渲染下拉/排序) */
|
|
1289
1303
|
register('assignments_get', async (host, args) => {
|
|
1290
|
-
const
|
|
1304
|
+
const providers = store.allProviders();
|
|
1305
|
+
const configured = new Set(store.providers().map(provider => provider.id));
|
|
1306
|
+
const pool = models.modelPool(providers, { includeHostVision: true });
|
|
1291
1307
|
const byCap = {};
|
|
1292
1308
|
for (const m of pool) {
|
|
1293
1309
|
for (const c of m.capabilities) {
|
|
1294
1310
|
if (!byCap[c]) byCap[c] = [];
|
|
1295
|
-
|
|
1311
|
+
const provider = providers.find(provider => provider.id === m.providerId);
|
|
1312
|
+
byCap[c].push({ ref: m.ref, id: m.id, providerId: m.providerId,
|
|
1313
|
+
label: m.ref === models.HOST_VISION_REF ? 'DSH 当前默认模型' : m.id + ' · ' + m.providerId,
|
|
1314
|
+
unavailable: m.ref !== models.HOST_VISION_REF && (!configured.has(m.providerId) || m.rateLimited === true),
|
|
1315
|
+
...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id) } : {}) });
|
|
1296
1316
|
}
|
|
1297
1317
|
}
|
|
1298
1318
|
// order:归一化的有序分配列表(GUI 直接渲染 failover 顺序,无需处理单字符串旧格式)
|
|
1299
1319
|
const order = {};
|
|
1300
|
-
for (const c of Object.
|
|
1320
|
+
for (const c of Object.values(cap.CAPABILITIES)) order[c] = store.assignmentOrder(c);
|
|
1301
1321
|
return { ok: true, assignments: store.assignments(), order, poolByCapability: byCap };
|
|
1302
1322
|
});
|
|
1303
1323
|
|
package/lib/adapters.js
CHANGED
|
@@ -189,7 +189,7 @@ export function applyImageSize({ protocol, mode, size }) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/** 新版多模态同步协议:响应中直接返回图片 URL。 */
|
|
192
|
-
export async function generateImageMultimodal({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
192
|
+
export async function generateImageMultimodal({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
193
193
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
194
194
|
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'multimodal-sync', size });
|
|
195
195
|
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/multimodal-generation/generation`, {
|
|
@@ -198,7 +198,10 @@ export async function generateImageMultimodal({ key, baseUrl, model, prompt, siz
|
|
|
198
198
|
signal: timeoutSignal(timeoutMs, signal),
|
|
199
199
|
body: JSON.stringify({
|
|
200
200
|
model,
|
|
201
|
-
input: { messages: [{ role: 'user', content: [
|
|
201
|
+
input: { messages: [{ role: 'user', content: [
|
|
202
|
+
...(image ? [{ image: 'data:' + image.mediaType + ';base64,' + Buffer.from(image.bytes).toString('base64') }] : []),
|
|
203
|
+
{ text: prompt }
|
|
204
|
+
] }] },
|
|
202
205
|
parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 }
|
|
203
206
|
})
|
|
204
207
|
});
|
package/lib/client.js
CHANGED
|
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
21
21
|
var exports = module.exports;
|
|
22
22
|
|
|
23
23
|
var React = require('react');
|
|
24
|
-
var IRIS_CLIENT_VERSION = '0.2.
|
|
24
|
+
var IRIS_CLIENT_VERSION = '0.2.5';
|
|
25
25
|
var HOST_CLIENT_PROTOCOL_VERSION = 0;
|
|
26
26
|
var reportedSeats = [];
|
|
27
27
|
|
|
@@ -858,7 +858,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
858
858
|
React.createElement('select', { value: m.musicProtocol || '', onChange: function (e) { setMusicProtocol(m, e.target.value); } },
|
|
859
859
|
React.createElement('option', { value: '' }, '请选择音乐协议'),
|
|
860
860
|
React.createElement('option', { value: 'gemini-web-music' }, 'Chat Completions(内嵌音频)'))) : null,
|
|
861
|
-
caps.includes('vision') || (caps.includes('image-gen') && m.imageRouting && m.imageRouting.
|
|
861
|
+
caps.includes('vision') || (caps.includes('image-gen') && m.imageRouting && m.imageRouting.editSupported) ? React.createElement(VisionInputEditor, { key: 'vision-input', provider: p, model: m,
|
|
862
862
|
onNote: props.onNote, onDone: props.onDone }) : null,
|
|
863
863
|
blocked ? React.createElement('span', { className: 'iris-pm-ver failed ' + healthClass(blocked), title: healthText(blocked) }, rateLimitLabel(blocked)) : null,
|
|
864
864
|
React.createElement('button', { className: 'iris-pm-btn danger', title: '从池移除', onClick: function () { props.removeModel(p, m); } }, '✕'));
|
|
@@ -937,7 +937,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
937
937
|
placeholder: '最长边 px(可选)', value: dimensionPair[0], onChange: function (e) { dimensionPair[1](e.target.value); } }),
|
|
938
938
|
React.createElement('button', { className: 'iris-pm-btn', onClick: function () { save(false); } }, '保存预算'),
|
|
939
939
|
React.createElement('button', { className: 'iris-pm-btn', onClick: function () { save(true); } }, '恢复继承')),
|
|
940
|
-
React.createElement('span', { className: 'iris-pm-note' }, '用于看图、OCR
|
|
940
|
+
React.createElement('span', { className: 'iris-pm-note' }, '用于看图、OCR 每块输入、定位和改图的发送副本。作品原图保留,定位结果换算回原图。'));
|
|
941
941
|
}
|
|
942
942
|
|
|
943
943
|
function ProviderEndpointEditor(props) {
|
|
@@ -1175,8 +1175,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1175
1175
|
if (!r.ok || !r.d) return;
|
|
1176
1176
|
var opts = (r.d.poolByCapability && r.d.poolByCapability[capability]) || [];
|
|
1177
1177
|
var ord = (r.d.order && r.d.order[capability]) || [];
|
|
1178
|
-
|
|
1179
|
-
var assignedRef = ord[0] ||
|
|
1178
|
+
opts = ord.map(function (ref) { return opts.find(function (option) { return option.ref === ref; }); }).filter(Boolean);
|
|
1179
|
+
var assignedRef = ord[0] || '';
|
|
1180
1180
|
var assignedOpt = opts.find(function (o) { return o.ref === assignedRef; });
|
|
1181
1181
|
// 不再自动填 vals.model:留空 = 用能力分配的解析结果(避免绕过分配)
|
|
1182
1182
|
setModelInfo({ assigned: assignedOpt ? assignedOpt.label : '', assignedRef: assignedRef, options: opts, loaded: true });
|
|
@@ -1203,11 +1203,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1203
1203
|
if (open) {
|
|
1204
1204
|
var inputs = fields.map(function (f) {
|
|
1205
1205
|
var node;
|
|
1206
|
-
if (f.key === 'model' && capability
|
|
1206
|
+
if (f.key === 'model' && capability) {
|
|
1207
1207
|
node = React.createElement('select', { value: vals.model || '', onChange: function (e) { setVal('model', e.target.value); } },
|
|
1208
|
-
React.createElement('option', { value: '' }, '
|
|
1208
|
+
React.createElement('option', { value: '' }, '按功能分配顺序'),
|
|
1209
1209
|
((modelInfo && modelInfo.options) || []).map(function (option) {
|
|
1210
|
-
return React.createElement('option', { key: option.ref, value: option.ref }, option.label);
|
|
1210
|
+
return React.createElement('option', { key: option.ref, value: option.ref, disabled: option.unavailable }, option.label + (option.unavailable ? ' · 当前不可用' : ''));
|
|
1211
1211
|
}));
|
|
1212
1212
|
return React.createElement('div', { key: f.key, className: 'iris-act-field' }, React.createElement('label', {}, f.label), node);
|
|
1213
1213
|
}
|
|
@@ -1254,15 +1254,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1254
1254
|
modelBar = React.createElement('div', { className: 'iris-act-field' },
|
|
1255
1255
|
React.createElement('label', {},
|
|
1256
1256
|
React.createElement('span', { className: 'iris-pm-dot ' + healthClass(cardHealth) }, cardHealth.retryAt ? '◷' : HEALTH_META[cardHealth.status].symbol),
|
|
1257
|
-
' 模型:' + (shown ||
|
|
1257
|
+
' 模型:' + (shown || '未分配') + (available ? '' : '(无可用模型)')),
|
|
1258
1258
|
React.createElement('div', { className: 'iris-wb-muted' },
|
|
1259
|
-
healthText(cardHealth) + ' · 改分配 →
|
|
1259
|
+
healthText(cardHealth) + ' · 改分配 → 上方「功能分配(调用顺序)」'));
|
|
1260
1260
|
}
|
|
1261
1261
|
body = React.createElement('div', { className: 'iris-act-body' },
|
|
1262
1262
|
modelBar,
|
|
1263
1263
|
inputs,
|
|
1264
1264
|
React.createElement('button', { className: 'iris-act-run', disabled: run === 'running', onClick: submit },
|
|
1265
|
-
run === 'running' ? '执行中…' : '执行'),
|
|
1265
|
+
run === 'running' ? '执行中…' : (props.submitLabel || '执行')),
|
|
1266
1266
|
result);
|
|
1267
1267
|
}
|
|
1268
1268
|
var headStatus = capability ? cardHealth.status : 'verified';
|
|
@@ -1282,6 +1282,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1282
1282
|
|
|
1283
1283
|
/* ---- 操作卡片注册表(单一来源:设置页全量 / 泡泡常用子集 / 选择器三处共用) ---- */
|
|
1284
1284
|
var CARD_DEFS = [
|
|
1285
|
+
{ action: 'image_import', title: '📥 导入图片', submitLabel: '保存到作品', fields: [
|
|
1286
|
+
{ key: 'image_path', label: '图片(静态 PNG/JPEG/WebP,最大20 MiB;保存副本,保留原文件)', type: 'file', accept: 'image/png,image/jpeg,image/webp' }] },
|
|
1285
1287
|
{ action: 'image', title: '🎨 画图', capability: 'image-gen', fields: [
|
|
1286
1288
|
{ key: 'prompt', label: '提示词', type: 'textarea', placeholder: '详细描述要生成的图片' },
|
|
1287
1289
|
{ key: 'size', label: '尺寸 (可选)', placeholder: '如 1024*1024' },
|
|
@@ -1383,7 +1385,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1383
1385
|
var opt = opts.find(function (o) { return o.ref === ref; });
|
|
1384
1386
|
return React.createElement('span', { key: ref, className: 'iris-cap-chip' },
|
|
1385
1387
|
React.createElement('b', {}, (i + 1) + '.'),
|
|
1386
|
-
opt ? opt.label : ref,
|
|
1388
|
+
opt ? opt.label + (opt.unavailable ? ' · 当前不可用' : '') : ref + ' · 配置已失效',
|
|
1387
1389
|
i > 0 ? React.createElement('button', {
|
|
1388
1390
|
className: 'iris-pm-btn', title: '上移',
|
|
1389
1391
|
onClick: function () { var n = list.slice(); var t = n.splice(i, 1)[0]; n.splice(i - 1, 0, t); save(row.cap, n); }
|
|
@@ -1399,17 +1401,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1399
1401
|
});
|
|
1400
1402
|
return React.createElement('div', { key: row.cap, className: 'iris-cap-row' },
|
|
1401
1403
|
React.createElement('span', { className: 'iris-cap-name' }, row.label),
|
|
1402
|
-
chips.length ? chips : React.createElement('span', { className: 'iris-cap-auto' },
|
|
1404
|
+
chips.length ? chips : React.createElement('span', { className: 'iris-cap-auto' }, '尚未分配模型'),
|
|
1403
1405
|
available.length ? React.createElement('select', {
|
|
1404
1406
|
className: 'iris-pm-model-sel', value: '',
|
|
1405
1407
|
onChange: function (e) { if (e.target.value) save(row.cap, list.concat([e.target.value])); }
|
|
1406
1408
|
},
|
|
1407
|
-
React.createElement('option', { value: '' }, '+
|
|
1409
|
+
React.createElement('option', { value: '' }, '+ 分配模型'),
|
|
1408
1410
|
available.map(function (o) { return React.createElement('option', { key: o.ref, value: o.ref }, o.label); })) : null,
|
|
1409
1411
|
list.length ? React.createElement('button', {
|
|
1410
|
-
className: 'iris-pm-btn', title: '
|
|
1412
|
+
className: 'iris-pm-btn', title: '清空分配后,该功能不再发起新调用',
|
|
1411
1413
|
onClick: function () { save(row.cap, []); }
|
|
1412
|
-
}, '
|
|
1414
|
+
}, '清空分配') : null);
|
|
1413
1415
|
}));
|
|
1414
1416
|
}
|
|
1415
1417
|
|
|
@@ -1563,7 +1565,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1563
1565
|
}
|
|
1564
1566
|
|
|
1565
1567
|
function galleryKindLabel(kind) {
|
|
1566
|
-
return ({ __proto__: null, 'generated-image': '生成图片', 'generated-video': '生成视频', 'generated-audio': '生成音频',
|
|
1568
|
+
return ({ __proto__: null, 'imported-image': '导入图片', 'generated-image': '生成图片', 'generated-video': '生成视频', 'generated-audio': '生成音频',
|
|
1567
1569
|
'tts-audio': '合成语音', transcript: '转写文本', 'video-frame': '视频帧', crop: '裁剪图片',
|
|
1568
1570
|
'cropped-image': '裁剪图片', 'pixel-diff': '差异热图', 'diff-heatmap': '差异热图', 'html-screenshot': 'HTML 截图',
|
|
1569
1571
|
'legacy-output': '旧版作品' })[kind] || kind;
|
|
@@ -1779,19 +1781,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1779
1781
|
var running = result && result.status === 'running';
|
|
1780
1782
|
React.useEffect(function () {
|
|
1781
1783
|
var active = true;
|
|
1782
|
-
postAction('
|
|
1784
|
+
postAction('assignments_get', {}).then(function (response) {
|
|
1783
1785
|
if (!active) return;
|
|
1784
1786
|
if (!response.ok || !response.d.ok) throw new Error('模型列表读取失败');
|
|
1785
1787
|
var choices = [];
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
var health = entry.health && entry.health['image-gen'];
|
|
1792
|
-
choices.push({ id: provider.id + '::' + entry.id, label: provider.name + ' · ' + entry.id,
|
|
1793
|
-
blocked: !!(health && health.rateLimited) });
|
|
1794
|
-
});
|
|
1788
|
+
var pool = response.d.poolByCapability && response.d.poolByCapability['image-gen'] || [];
|
|
1789
|
+
(response.d.order && response.d.order['image-gen'] || []).forEach(function (ref) {
|
|
1790
|
+
var entry = pool.find(function (item) { return item.ref === ref; });
|
|
1791
|
+
if (!entry || !entry.imageRouting || !entry.imageRouting.editSupported) return;
|
|
1792
|
+
choices.push({ id: ref, label: entry.label, blocked: entry.unavailable === true });
|
|
1795
1793
|
});
|
|
1796
1794
|
setModels(choices);
|
|
1797
1795
|
}).catch(function (error) { if (active) { setModels([]); setResult({ status: 'failed', text: error.message }); } });
|
|
@@ -1831,7 +1829,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1831
1829
|
React.createElement('label', {}, '改图模型', React.createElement('select', { 'aria-label': '改图模型', value: model, disabled: !!running,
|
|
1832
1830
|
onChange: function (e) { setModel(e.target.value); } }, React.createElement('option', { value: '' }, '按当前顺序选择可用改图模型'),
|
|
1833
1831
|
(models || []).map(function (entry) { return React.createElement('option', { key: entry.id, value: entry.id, disabled: entry.blocked }, entry.label + (entry.blocked ? ' · 冷却或额度耗尽' : '')); }))),
|
|
1834
|
-
models && !models.some(function (entry) { return !entry.blocked; }) ? React.createElement('div', { role: 'status' }, '
|
|
1832
|
+
models && !models.some(function (entry) { return !entry.blocked; }) ? React.createElement('div', { role: 'status' }, '暂无可用改图模型,请在生图名单分配聊天生图或 DashScope Qwen 改图模型,并检查模型是否可用。') : null,
|
|
1835
1833
|
React.createElement('div', { className: 'iris-gallery-actions' },
|
|
1836
1834
|
React.createElement('button', { className: 'iris-clean-btn', type: 'button', disabled: !!running || !prompt.trim() || !models || !models.some(function (entry) { return !entry.blocked; }), onClick: execute }, running ? '编辑中…' : '生成修改版'),
|
|
1837
1835
|
running ? React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: cancel }, '停止等待') : null),
|
|
@@ -1956,6 +1954,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1956
1954
|
var blocked = busy || mutating;
|
|
1957
1955
|
|
|
1958
1956
|
return React.createElement('div', {},
|
|
1957
|
+
React.createElement(ActionCard, CARD_DEFS.find(function (card) { return card.action === 'image_import'; })),
|
|
1959
1958
|
React.createElement('div', { className: 'iris-gallery-tools' },
|
|
1960
1959
|
React.createElement('span', { className: 'iris-clean-note' },
|
|
1961
1960
|
'匹配 ' + data.total + ' 个作品 · Core ' + data.counts.core + ' · 旧版 ' + data.counts.legacy + ' · 清任务历史不会删除'),
|
|
@@ -2687,7 +2686,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
2687
2686
|
React.createElement('div', { className: 'iris-wb-sec' }, '能力健康'),
|
|
2688
2687
|
React.createElement(CapabilityHealthOverview, {}),
|
|
2689
2688
|
React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。429 冷却显示琥珀色,额度或预算耗尽显示红色;点击模型能力可实测恢复。不会后台探测。'),
|
|
2690
|
-
React.createElement('div', { className: 'iris-wb-sec' }, '
|
|
2689
|
+
React.createElement('div', { className: 'iris-wb-sec' }, '功能分配(调用顺序)'),
|
|
2691
2690
|
React.createElement(CapabilityAssigner, {}),
|
|
2692
2691
|
React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
|
|
2693
2692
|
React.createElement(BubbleCardPicker, {}),
|
package/lib/command-service.js
CHANGED
|
@@ -21,11 +21,12 @@ import { videoTaskCandidates } from './video-input.js';
|
|
|
21
21
|
import { waitForCoreTask } from './task-wait.js';
|
|
22
22
|
import { planCoreDeletion, planCoreCleanup, quarantineCoreFiles, inspectCoreTransactions, restoreCoreTransaction } from './core-maintenance.js';
|
|
23
23
|
import { deliveryCacheNeedsProvider } from './core-delivery-cache.js';
|
|
24
|
+
import { readImportImage } from './image-import.js';
|
|
24
25
|
|
|
25
26
|
export const COMMAND_CONTRACT_VERSION = 0;
|
|
26
27
|
export const CORE_COMMANDS = Object.freeze([
|
|
27
28
|
'crop', 'media.diff', 'media.frames', 'media.html', 'task.inspect', 'task.list', 'task.wait', 'task.observe', 'task.reobserve', 'task.redeliver', 'task.cancel', 'task.retry',
|
|
28
|
-
'task.inspect-many', 'artifact.inspect', 'artifact.inspect-many', 'artifact.list', 'artifact.export', 'artifact.export-many', 'artifact.rebuild',
|
|
29
|
+
'task.inspect-many', 'artifact.import', 'artifact.inspect', 'artifact.inspect-many', 'artifact.list', 'artifact.export', 'artifact.export-many', 'artifact.rebuild',
|
|
29
30
|
'core.delete', 'core.cleanup', 'core.transactions', 'core.restore'
|
|
30
31
|
]);
|
|
31
32
|
|
|
@@ -109,6 +110,20 @@ export function createCommandService(runtime, ports = {}) {
|
|
|
109
110
|
|
|
110
111
|
async function execute(name, input) {
|
|
111
112
|
if (!CORE_COMMANDS.includes(name)) throw commandError('IRIS_COMMAND_UNKNOWN', `未知 Command:${String(name)}`);
|
|
113
|
+
if (name === 'artifact.import') {
|
|
114
|
+
exactInput(input, ['image_path', 'image_bytes']);
|
|
115
|
+
const hasPath = input.image_path !== undefined;
|
|
116
|
+
if (hasPath === (input.image_bytes !== undefined)) {
|
|
117
|
+
throw commandError('IRIS_COMMAND_INPUT_INVALID', '图片导入须且只能提供image_path或image_bytes之一');
|
|
118
|
+
}
|
|
119
|
+
const filePath = hasPath ? localImage(input) : undefined;
|
|
120
|
+
return runtime.run('execute', async ({ dataRoot, signal }) => {
|
|
121
|
+
const source = await readImportImage({ filePath, imageBytes: input.image_bytes }, signal);
|
|
122
|
+
signal.throwIfAborted();
|
|
123
|
+
const artifact = createCoreArtifact(dataRoot, { ...source, kind: 'imported-image' });
|
|
124
|
+
return Object.freeze({ contractVersion: COMMAND_CONTRACT_VERSION, command: name, artifact });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
112
127
|
if (name === 'core.delete' || name === 'core.cleanup') {
|
|
113
128
|
exactInput(input, name === 'core.delete' ? ['task_ids', 'artifact_ids', 'confirm_delete'] : ['paths', 'older_than_ms', 'confirm_delete']);
|
|
114
129
|
if (input.confirm_delete !== undefined && typeof input.confirm_delete !== 'boolean') throw commandError('IRIS_COMMAND_INPUT_INVALID', 'confirm_delete 必须是布尔值');
|
package/lib/config.js
CHANGED
|
@@ -183,37 +183,29 @@ export function providerById(id) {
|
|
|
183
183
|
const FIELD_OF = models.CAP_FIELD;
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
187
|
-
* 否则从全局模型池挑第一个有能力标签的模型。
|
|
186
|
+
* 严格选择:仅从功能分配名单取第一个可用模型。
|
|
188
187
|
* 返回该模型所属 provider 的副本,并把该能力的模型字段设为选中模型。
|
|
189
188
|
* 向后兼容:调用方继续读 provider.apiKey / provider.imageModel 等。
|
|
190
189
|
* 无可用模型 → null(不再兜底到任意 provider)。
|
|
191
190
|
*/
|
|
192
191
|
export function pickFor(capability) {
|
|
193
|
-
|
|
194
|
-
let m = null;
|
|
195
|
-
for (const ref of assignmentOrder(capability)) {
|
|
196
|
-
m = pool.find((x) => !x.rateLimited && x.ref === ref && x.capabilities.includes(capability));
|
|
197
|
-
if (m) break;
|
|
198
|
-
}
|
|
199
|
-
if (!m && capability !== cap.CAPABILITIES.MUSIC) m = models.pickModel(pool, capability);
|
|
200
|
-
if (!m) return null;
|
|
201
|
-
const p = providerById(m.providerId);
|
|
202
|
-
if (!p) return null;
|
|
203
|
-
const field = FIELD_OF[capability];
|
|
204
|
-
return field ? { ...p, [field]: m.id } : p;
|
|
192
|
+
return pickAllFor(capability)[0] || null;
|
|
205
193
|
}
|
|
206
194
|
|
|
207
195
|
/**
|
|
208
196
|
* 同一能力的有序 provider 列表(供 failover 依次尝试):
|
|
209
|
-
*
|
|
197
|
+
* 只使用已分配顺序,不补入全局池。
|
|
210
198
|
* 一个 provider 若池里有多个该能力模型,会返回多条(各自带不同模型)。
|
|
211
199
|
*/
|
|
212
200
|
export function pickAllFor(capability, { includeRateLimited = false } = {}) {
|
|
213
|
-
const pool = models.modelPool(providers());
|
|
201
|
+
const pool = models.modelPool(providers(), { includeHostVision: true });
|
|
214
202
|
const chosen = models.orderedModels(pool, capability, assignments()[capability], { includeRateLimited });
|
|
215
203
|
const out = [];
|
|
216
204
|
for (const m of chosen) {
|
|
205
|
+
if (m.ref === models.HOST_VISION_REF) {
|
|
206
|
+
out.push({ id: models.HOST_VISION_REF, visionModel: m.id });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
217
209
|
const p = providerById(m.providerId);
|
|
218
210
|
if (!p) continue;
|
|
219
211
|
const field = FIELD_OF[capability];
|
|
@@ -233,11 +225,12 @@ const resolveAssignment = models.resolvePoolModel;
|
|
|
233
225
|
export function assignmentOrder(capability) {
|
|
234
226
|
const a = assignments()[capability];
|
|
235
227
|
if (!a) return [];
|
|
236
|
-
const pool = models.modelPool(
|
|
228
|
+
const pool = models.modelPool(allProviders(), { includeHostVision: true });
|
|
237
229
|
const refs = [];
|
|
238
230
|
for (const raw of (Array.isArray(a) ? a : [a])) {
|
|
239
231
|
const m = resolveAssignment(pool, raw, capability);
|
|
240
232
|
if (m && !refs.includes(m.ref)) refs.push(m.ref);
|
|
233
|
+
else if (!m && typeof raw === 'string' && models.parseModelRef(raw) && !refs.includes(raw)) refs.push(raw);
|
|
241
234
|
}
|
|
242
235
|
return refs;
|
|
243
236
|
}
|
|
@@ -245,7 +238,7 @@ export function assignmentOrder(capability) {
|
|
|
245
238
|
/** 设置某能力分配的模型 id(需在全局池中存在且具备该能力) */
|
|
246
239
|
export function setAssignment(capability, ref) {
|
|
247
240
|
const c = load();
|
|
248
|
-
const pool = models.modelPool(
|
|
241
|
+
const pool = models.modelPool(allProviders(), { includeHostVision: true });
|
|
249
242
|
const m = resolveAssignment(pool, ref, capability);
|
|
250
243
|
if (!m) return false;
|
|
251
244
|
if (!c.assignments) c.assignments = {};
|
|
@@ -256,7 +249,7 @@ export function setAssignment(capability, ref) {
|
|
|
256
249
|
|
|
257
250
|
/**
|
|
258
251
|
* 设置某能力的有序 failover 列表(阶段 6 条目 4):每个模型须在全局池中存在且具备该能力。
|
|
259
|
-
* 空数组 =
|
|
252
|
+
* 空数组 = 未分配,不自动选择。重复 id 自动去重。
|
|
260
253
|
*/
|
|
261
254
|
export function setAssignmentOrder(capability, modelRefs) {
|
|
262
255
|
const c = load();
|
|
@@ -268,7 +261,7 @@ export function setAssignmentOrder(capability, modelRefs) {
|
|
|
268
261
|
}
|
|
269
262
|
return true;
|
|
270
263
|
}
|
|
271
|
-
const pool = models.modelPool(
|
|
264
|
+
const pool = models.modelPool(allProviders(), { includeHostVision: true });
|
|
272
265
|
const refs = [];
|
|
273
266
|
for (const raw of modelRefs) {
|
|
274
267
|
const m = resolveAssignment(pool, raw, capability);
|
|
@@ -281,7 +274,7 @@ export function setAssignmentOrder(capability, modelRefs) {
|
|
|
281
274
|
return true;
|
|
282
275
|
}
|
|
283
276
|
|
|
284
|
-
/**
|
|
277
|
+
/** 清除功能分配;此后不再发起该功能的新调用。 */
|
|
285
278
|
export function clearAssignment(capability) {
|
|
286
279
|
const c = load();
|
|
287
280
|
if (c.assignments && c.assignments[capability]) {
|
|
@@ -451,7 +444,8 @@ export function providerHealthSnapshot({ now = Date.now() } = {}) {
|
|
|
451
444
|
for (const capability of Object.values(cap.CAPABILITIES)) {
|
|
452
445
|
const candidates = pickAllFor(capability, { includeRateLimited: true }).map((provider) => {
|
|
453
446
|
const modelId = String(provider[FIELD_OF[capability]] || '');
|
|
454
|
-
const health =
|
|
447
|
+
const health = provider.id === models.HOST_VISION_REF ? { status: 'configured', source: 'configuration' }
|
|
448
|
+
: modelHealth(provider.id, modelId, capability, { now });
|
|
455
449
|
return { providerId: provider.id, modelId, ...health };
|
|
456
450
|
});
|
|
457
451
|
const status = aggregateHealthStates(candidates.map((item) => item.status));
|
package/lib/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
+
import { isSupportedDshVersion, SUPPORTED_DSH_VERSIONS } from './dsh-compatibility.js';
|
|
4
5
|
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { spawnSync } from 'node:child_process';
|
|
@@ -331,13 +332,6 @@ function summarizeChecks(checks) {
|
|
|
331
332
|
};
|
|
332
333
|
}
|
|
333
334
|
|
|
334
|
-
function dshVersionSupported(value) {
|
|
335
|
-
/* 当前 0.2 候选只支持经真实安装服务隔离验收的准确 rc.2。
|
|
336
|
-
Iris 0.1.4 的旧宿主窗口保留在历史发行中,不用于放行当前源码。 */
|
|
337
|
-
const version = String(value || '').trim().replace(/\s+/g, '');
|
|
338
|
-
return /^0\.2\.0-rc\.2(?:\+.*)?$/.test(version);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
335
|
function names(value) {
|
|
342
336
|
return [...new Set((Array.isArray(value) ? value : []).map((item) => String(item || '').trim()).filter(Boolean))];
|
|
343
337
|
}
|
|
@@ -379,12 +373,12 @@ export function hostDoctor(host, evidence = {}, options = {}) {
|
|
|
379
373
|
if (snapshot.host.id === 'deepseek-harness') {
|
|
380
374
|
if (snapshot.host.version === 'unknown') {
|
|
381
375
|
checks.push(result('dsh-version', 'warn', '无法从当前进程识别 DSH 版本',
|
|
382
|
-
'使用 dsh --version
|
|
383
|
-
} else if (
|
|
376
|
+
'使用 dsh --version 核对;已验证版本为 DSH ' + SUPPORTED_DSH_VERSIONS.join('、') + '。'));
|
|
377
|
+
} else if (isSupportedDshVersion(snapshot.host.version)) {
|
|
384
378
|
checks.push(result('dsh-version', 'ok', 'DSH ' + snapshot.host.version + ' 位于已验证支持范围'));
|
|
385
379
|
} else {
|
|
386
380
|
checks.push(result('dsh-version', 'error', 'DSH ' + snapshot.host.version + ' 超出已验证支持范围',
|
|
387
|
-
'
|
|
381
|
+
'当前支持 DSH ' + SUPPORTED_DSH_VERSIONS.join('、') + ';请核对所装 Iris 的兼容矩阵及 Node 要求,其他版本须另行验收。'));
|
|
388
382
|
}
|
|
389
383
|
}
|
|
390
384
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** 已完成宿主接口验收的版本;manifest/Doctor/验收脚本共用同一边界。 */
|
|
4
|
+
export const SUPPORTED_DSH_VERSIONS = Object.freeze([
|
|
5
|
+
'0.1.2-rc.1',
|
|
6
|
+
'0.1.5-rc.1', '0.1.5-rc.2', '0.1.5-rc.3',
|
|
7
|
+
'0.1.7-rc.1', '0.1.7-rc.2',
|
|
8
|
+
'0.2.0-rc.1', '0.2.0-rc.2',
|
|
9
|
+
'0.2.1-alpha.1', '0.2.1-alpha.2', '0.2.1-alpha.3', '0.2.1-alpha.4'
|
|
10
|
+
]);
|
|
11
|
+
export const DSH_SUPPORT_RANGE = SUPPORTED_DSH_VERSIONS.join(' || ');
|
|
12
|
+
|
|
13
|
+
export function isSupportedDshVersion(value) {
|
|
14
|
+
const version = String(value || '').trim().split('+')[0];
|
|
15
|
+
return SUPPORTED_DSH_VERSIONS.includes(version);
|
|
16
|
+
}
|
package/lib/dsh-core-adapter.js
CHANGED
|
@@ -68,6 +68,12 @@ function withReader(callback, signal) {
|
|
|
68
68
|
return withRuntime('reader', callback, signal);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** 本地图片与会话附件都交给同一导入Command,不创建生成任务。 */
|
|
72
|
+
export function importImageForDsh(input = {}) {
|
|
73
|
+
return withWriter((_runtime, commands) => commands.execute('artifact.import', input.imageBytes !== undefined
|
|
74
|
+
? { image_bytes: input.imageBytes } : { image_path: input.imagePath }), input.signal);
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
/**
|
|
72
78
|
* 将 DSH 图片输入投影到共享 crop Command,再把中性 Artifact 读成宿主可保存的字节。
|
|
73
79
|
* bytes 输入先成为 host-input Artifact,输出通过 derived-from 保留关系。
|
|
@@ -285,7 +291,7 @@ export function retryProviderTaskForDsh(input = {}) {
|
|
|
285
291
|
return withWriter(async (runtime) => {
|
|
286
292
|
const commands = createCommandService(runtime, {
|
|
287
293
|
resolveTaskCandidates: ({ capability, modelRef }) => {
|
|
288
|
-
const catalog = { providers: allProviders(),
|
|
294
|
+
const catalog = { providers: allProviders(), assignments: assignments() };
|
|
289
295
|
const ref = modelRef || undefined;
|
|
290
296
|
const candidates = capability === 'video'
|
|
291
297
|
? videoCandidatesFromCatalog(catalog, ref)
|
package/lib/dsh-host-adapter.js
CHANGED
|
@@ -93,37 +93,30 @@ function visionRouteError(message) {
|
|
|
93
93
|
return Object.assign(new Error(message), { code: 'IRIS_HOST_CAPABILITY_INCOMPATIBLE' });
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function dshVisionPort(llm,
|
|
96
|
+
function dshVisionPort(llm, defaults) {
|
|
97
97
|
return Object.freeze({
|
|
98
98
|
id: 'dsh-global',
|
|
99
99
|
model: '全局视觉模型',
|
|
100
100
|
async analyze({ question, ref, signal }) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('已取消');
|
|
115
|
-
if (info?.provider !== provider || info?.id !== model || !Array.isArray(info.inputModalities)
|
|
116
|
-
|| !info.inputModalities.includes('image')) {
|
|
117
|
-
throw visionRouteError('DSH 默认模型未确认支持图片;请在 Iris 配置视觉模型或选择已声明图片输入的宿主模型');
|
|
118
|
-
}
|
|
119
|
-
route = { provider, model };
|
|
101
|
+
if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('已取消');
|
|
102
|
+
const selection = defaults.currentSelection();
|
|
103
|
+
if (typeof selection?.provider !== 'string' || !selection.provider.trim()
|
|
104
|
+
|| typeof selection?.model !== 'string' || !selection.model.trim()) {
|
|
105
|
+
throw visionRouteError('DSH 默认模型没有明确路由;请在 Iris 配置视觉模型');
|
|
106
|
+
}
|
|
107
|
+
// 默认文本选择只有明确声明 image 输入时才可成为视觉候选。
|
|
108
|
+
const { provider, model } = selection;
|
|
109
|
+
const info = await llm.resolveModelInfo(provider, model, signal);
|
|
110
|
+
if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('已取消');
|
|
111
|
+
if (info?.provider !== provider || info?.id !== model || !Array.isArray(info.inputModalities)
|
|
112
|
+
|| !info.inputModalities.includes('image')) {
|
|
113
|
+
throw visionRouteError('DSH 默认模型未确认支持图片;请在 Iris 配置视觉模型或选择已声明图片输入的宿主模型');
|
|
120
114
|
}
|
|
121
115
|
const chunks = [];
|
|
122
116
|
for await (const chunk of llm.stream({
|
|
123
117
|
sessionId: undefined,
|
|
124
|
-
provider
|
|
125
|
-
model
|
|
126
|
-
...route,
|
|
118
|
+
provider,
|
|
119
|
+
model,
|
|
127
120
|
signal,
|
|
128
121
|
messages: [{
|
|
129
122
|
role: 'user',
|
|
@@ -134,8 +127,8 @@ function dshVisionPort(llm, { defaults, requiresRoute }) {
|
|
|
134
127
|
}]
|
|
135
128
|
})) {
|
|
136
129
|
if (chunk?.type === 'error') throw visionRouteError('DSH 视觉调用失败;请检查宿主模型状态');
|
|
137
|
-
//
|
|
138
|
-
if (
|
|
130
|
+
// Runtime 也会把 Adapter 异常转换为 finish,不能接受失败前的部分文本。
|
|
131
|
+
if (chunk?.type === 'finish') {
|
|
139
132
|
if (chunk.reason?.kind === 'error') throw visionRouteError('DSH 视觉调用失败;请检查宿主模型状态');
|
|
140
133
|
if (chunk.reason?.kind === 'aborted') throw visionRouteError('DSH 视觉调用已取消');
|
|
141
134
|
}
|
|
@@ -219,12 +212,12 @@ export function createDshHostAdapter(ctx, { version = detectDshVersion() } = {})
|
|
|
219
212
|
? () => defaults.currentSelection()
|
|
220
213
|
: () => null
|
|
221
214
|
});
|
|
222
|
-
|
|
223
|
-
if (
|
|
215
|
+
// 旧 SDK 同样提供精确路由;按接口探测,避免用宿主版本号误选历史路径。
|
|
216
|
+
if (defaults && typeof defaults.currentSelection === 'function'
|
|
224
217
|
&& typeof llm.resolveModelInfo === 'function') {
|
|
225
|
-
ports.visionModel = dshVisionPort(llm,
|
|
218
|
+
ports.visionModel = dshVisionPort(llm, defaults);
|
|
226
219
|
} else unavailable.visionModel = {
|
|
227
|
-
kind: 'unavailable', reason: 'DSH
|
|
220
|
+
kind: 'unavailable', reason: 'DSH 视觉桥接需要默认模型选择和精确图片能力元数据'
|
|
228
221
|
};
|
|
229
222
|
} else {
|
|
230
223
|
unavailable.textModel = unavailableFor(llm, 'DSH llm 需要 stream');
|
package/lib/host-runtime.js
CHANGED