@mokuyoaxis/dsh-iris 0.2.3 → 0.2.4
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 +16 -1
- package/README.en.md +28 -23
- package/README.md +28 -23
- package/bin/dsh-iris.js +8 -0
- package/docs/AGENT_QUICKSTART.md +7 -7
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/CLI_MANAGEMENT.md +11 -3
- package/docs/DSH_COMPATIBILITY.md +43 -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 +10 -7
- package/docs/HOST_DOCTOR.md +5 -3
- package/docs/IMAGE_IMPORT.md +49 -0
- package/docs/ROADMAP.md +4 -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/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 +10 -0
- package/lib/dsh-core-adapter.js +7 -1
- 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 +25 -17
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.4';
|
|
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('、') + ';旧 DSH 0.1.2 线或 0.1.5-rc.1 使用 Iris @mokuyoaxis/dsh-iris@0.1.4。其他版本须另行验收。'));
|
|
388
382
|
}
|
|
389
383
|
}
|
|
390
384
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** 已完成宿主接口验收的版本;manifest/Doctor/验收脚本共用同一边界。 */
|
|
4
|
+
export const SUPPORTED_DSH_VERSIONS = Object.freeze(['0.2.0-rc.2', '0.2.1-alpha.4']);
|
|
5
|
+
export const DSH_SUPPORT_RANGE = SUPPORTED_DSH_VERSIONS.join(' || ');
|
|
6
|
+
|
|
7
|
+
export function isSupportedDshVersion(value) {
|
|
8
|
+
const version = String(value || '').trim().split('+')[0];
|
|
9
|
+
return SUPPORTED_DSH_VERSIONS.includes(version);
|
|
10
|
+
}
|
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/host-runtime.js
CHANGED
package/lib/image-edit-input.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
/**
|
|
2
|
+
/** 改图只接受当前 Core 中的静态图片;输入原字节不进入 Task 记录。 */
|
|
3
3
|
import sharp from 'sharp';
|
|
4
4
|
import { inspectCoreArtifact, readCoreArtifactBytes } from './core-artifacts.js';
|
|
5
5
|
import { waitForModelWork } from './model-call-runtime.js';
|
|
6
|
+
import { supportsImageEdit } from './provider-protocol.js';
|
|
6
7
|
|
|
7
8
|
function invalid(message) {
|
|
8
9
|
return Object.assign(new Error(message), { code: 'IRIS_IMAGE_EDIT_INPUT_INVALID' });
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function imageEditCandidates(candidates) {
|
|
13
|
+
const supported = candidate => supportsImageEdit(candidate.adapter.protocol, candidate.providerModel);
|
|
12
14
|
if (candidates.some(candidate => candidate.selectionReason === 'explicit'
|
|
13
|
-
&& candidate
|
|
14
|
-
throw invalid('
|
|
15
|
+
&& !supported(candidate))) {
|
|
16
|
+
throw invalid('指定模型的图片协议不支持此类改图,请选择聊天生图模型或 DashScope Qwen 原生改图模型');
|
|
15
17
|
}
|
|
16
|
-
const selected = candidates.filter(
|
|
17
|
-
if (!selected.length) throw invalid('
|
|
18
|
+
const selected = candidates.filter(supported);
|
|
19
|
+
if (!selected.length) throw invalid('当前生图名单没有可用的聊天生图或 DashScope Qwen 改图模型');
|
|
18
20
|
return selected;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { open } from 'node:fs/promises';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
|
|
6
|
+
export const MAX_IMPORT_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
7
|
+
const TYPES = Object.freeze({ png: 'image/png', jpeg: 'image/jpeg', webp: 'image/webp' });
|
|
8
|
+
|
|
9
|
+
function invalid(message) {
|
|
10
|
+
return Object.assign(new Error(message), { code: 'IRIS_IMAGE_IMPORT_INVALID' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function checkSize(size) {
|
|
14
|
+
if (!size || size > MAX_IMPORT_IMAGE_BYTES) throw invalid('导入图片必须非空,且不超过20 MiB');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 只读一次有界快照;后续校验和保存使用同一份字节,不再复制变化中的源路径。 */
|
|
18
|
+
export async function readImportImage({ filePath, imageBytes }, signal) {
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
let bytes;
|
|
21
|
+
if (filePath) {
|
|
22
|
+
let file;
|
|
23
|
+
try {
|
|
24
|
+
file = await open(filePath, 'r');
|
|
25
|
+
const stat = await file.stat();
|
|
26
|
+
if (!stat.isFile()) throw invalid('导入来源必须是普通图片文件');
|
|
27
|
+
checkSize(stat.size);
|
|
28
|
+
bytes = Buffer.alloc(stat.size);
|
|
29
|
+
let offset = 0;
|
|
30
|
+
while (offset < bytes.length) {
|
|
31
|
+
signal.throwIfAborted();
|
|
32
|
+
const { bytesRead } = await file.read(bytes, offset, Math.min(65536, bytes.length - offset), offset);
|
|
33
|
+
if (!bytesRead) throw invalid('读取期间图片发生变化,请重新导入');
|
|
34
|
+
offset += bytesRead;
|
|
35
|
+
}
|
|
36
|
+
const extra = await file.read(Buffer.alloc(1), 0, 1, offset);
|
|
37
|
+
if (extra.bytesRead) throw invalid('读取期间图片发生变化,请重新导入');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
signal.throwIfAborted();
|
|
40
|
+
if (error.code === 'IRIS_IMAGE_IMPORT_INVALID') throw error;
|
|
41
|
+
throw invalid('图片无法读取,请选择可读的普通文件');
|
|
42
|
+
} finally { await file?.close(); }
|
|
43
|
+
} else {
|
|
44
|
+
if (!(imageBytes instanceof Uint8Array)) throw invalid('导入图片内容必须是字节数据');
|
|
45
|
+
checkSize(imageBytes.byteLength);
|
|
46
|
+
bytes = Buffer.from(imageBytes);
|
|
47
|
+
}
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
let metadata;
|
|
50
|
+
try { metadata = await sharp(bytes).metadata(); }
|
|
51
|
+
catch (_) {
|
|
52
|
+
signal.throwIfAborted();
|
|
53
|
+
throw invalid('无法读取图片,请使用有效的静态PNG、JPEG或WebP');
|
|
54
|
+
}
|
|
55
|
+
signal.throwIfAborted();
|
|
56
|
+
const mediaType = TYPES[metadata.format];
|
|
57
|
+
if (!mediaType || metadata.pages > 1 || !metadata.width || !metadata.height) {
|
|
58
|
+
throw invalid('仅支持静态PNG、JPEG或WebP,不支持动画');
|
|
59
|
+
}
|
|
60
|
+
return { bytes, mediaType, metadata: { width: metadata.width, height: metadata.height } };
|
|
61
|
+
}
|