@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.
Files changed (43) hide show
  1. package/.dsh/skills/iris-compose-media/references/workflows.md +1 -1
  2. package/.dsh/skills/iris-use/SKILL.md +2 -1
  3. package/.dsh/skills/iris-use/references/media-contract.md +12 -2
  4. package/CHANGELOG.md +16 -1
  5. package/README.en.md +28 -23
  6. package/README.md +28 -23
  7. package/bin/dsh-iris.js +8 -0
  8. package/docs/AGENT_QUICKSTART.md +7 -7
  9. package/docs/ARCHITECTURE.md +3 -1
  10. package/docs/CLI_MANAGEMENT.md +11 -3
  11. package/docs/DSH_COMPATIBILITY.md +43 -0
  12. package/docs/DSH_RC2_ADAPTATION.md +4 -2
  13. package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +63 -0
  14. package/docs/HEADLESS_CLI.md +10 -8
  15. package/docs/HOST_ADAPTER_CONTRACT.md +10 -7
  16. package/docs/HOST_DOCTOR.md +5 -3
  17. package/docs/IMAGE_IMPORT.md +49 -0
  18. package/docs/ROADMAP.md +4 -2
  19. package/docs/VISION_CLI.md +3 -3
  20. package/docs/VISION_MODEL.md +1 -1
  21. package/docs/WORKBENCH_ARTIFACTS.md +4 -2
  22. package/docs/releases/0.2.4.md +27 -0
  23. package/lib/actions.js +32 -12
  24. package/lib/adapters.js +5 -2
  25. package/lib/client.js +28 -29
  26. package/lib/command-service.js +16 -1
  27. package/lib/config.js +16 -22
  28. package/lib/doctor.js +4 -10
  29. package/lib/dsh-compatibility.js +10 -0
  30. package/lib/dsh-core-adapter.js +7 -1
  31. package/lib/host-runtime.js +1 -0
  32. package/lib/image-edit-input.js +7 -5
  33. package/lib/image-import.js +61 -0
  34. package/lib/index.js +39 -15
  35. package/lib/models.js +9 -3
  36. package/lib/ocr-model-routing.js +1 -0
  37. package/lib/provider-adapters.js +11 -2
  38. package/lib/provider-catalog.js +6 -8
  39. package/lib/provider-config-service.js +5 -5
  40. package/lib/provider-protocol.js +6 -0
  41. package/lib/vision-model-routing.js +22 -8
  42. package/package.json +9 -8
  43. 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.3';
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.mediaProtocol === 'openai-chat-images') ? React.createElement(VisionInputEditor, { key: 'vision-input', provider: p, model: m,
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
- if (capability === 'music-gen') opts = opts.filter(function (option) { return ord.includes(option.ref); });
1179
- var assignedRef = ord[0] || (capability !== 'music-gen' && opts[0] && opts[0].ref) || '';
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 === 'music-gen') {
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: '' }, '按 Music 分配顺序'),
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 || (capability === 'music-gen' ? '未分配' : '自动')) + (available ? '' : '(无可用模型)')),
1257
+ ' 模型:' + (shown || '未分配') + (available ? '' : '(无可用模型)')),
1258
1258
  React.createElement('div', { className: 'iris-wb-muted' },
1259
- healthText(cardHealth) + ' · 改分配 → 上方「能力分配(failover 顺序)」'));
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' }, row.cap === 'music-gen' ? '尚未分配音乐模型' : '自动(按池顺序)'),
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: '' }, '+ 加入 failover'),
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
- }, '恢复自动') : null);
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('providers_list', {}).then(function (response) {
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
- (response.d.providers || []).forEach(function (provider) {
1787
- if (!provider.enabled) return;
1788
- (provider.models || []).forEach(function (entry) {
1789
- if ((entry.capabilities || []).indexOf('image-gen') < 0
1790
- || !entry.imageRouting || entry.imageRouting.mediaProtocol !== 'openai-chat-images') return;
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' }, '暂无可用改图模型,请在模型设置中配置聊天生图协议,或手动实测恢复模型。') : null,
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' }, '能力分配(failover 顺序)'),
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, {}),
@@ -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
- * 严格选择(阶段 6 模型池):优先用「已分配模型」(有序列表取第一个可用),
187
- * 否则从全局模型池挑第一个有能力标签的模型。
186
+ * 严格选择:仅从功能分配名单取第一个可用模型。
188
187
  * 返回该模型所属 provider 的副本,并把该能力的模型字段设为选中模型。
189
188
  * 向后兼容:调用方继续读 provider.apiKey / provider.imageModel 等。
190
189
  * 无可用模型 → null(不再兜底到任意 provider)。
191
190
  */
192
191
  export function pickFor(capability) {
193
- const pool = models.modelPool(providers());
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
- * 已分配顺序优先(阶段 6 条目 4),其余按池顺序补齐(去重)。
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(providers());
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(providers());
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
- * 空数组 = 清除分配(回退池顺序自动选择)。重复 id 自动去重。
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(providers());
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 = modelHealth(provider.id, modelId, capability, { now });
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 核对;当前候选仅支持 DSH 0.2.0-rc.2。'));
383
- } else if (dshVersionSupported(snapshot.host.version)) {
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
- '当前候选需要 DSH 0.2.0-rc.2;旧 DSH 0.1.2 线或 0.1.5-rc.1 使用 Iris @mokuyoaxis/dsh-iris@0.1.4。其他版本须另行验收。'));
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
+ }
@@ -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(), ...(capability === 'music' ? { assignments: assignments() } : {}) };
294
+ const catalog = { providers: allProviders(), assignments: assignments() };
289
295
  const ref = modelRef || undefined;
290
296
  const candidates = capability === 'video'
291
297
  ? videoCandidatesFromCatalog(catalog, ref)
@@ -10,6 +10,7 @@ export const IRIS_PLUGIN_ID = '@mokuyoaxis/dsh-iris';
10
10
  export const HOST_CLIENT_PROTOCOL_VERSION = 0;
11
11
 
12
12
  export const EXPECTED_IRIS_TOOLS = Object.freeze([
13
+ 'iris_import_image',
13
14
  'iris_draw_image',
14
15
  'iris_edit_image',
15
16
  'iris_generate_video',
@@ -1,20 +1,22 @@
1
1
  'use strict';
2
- /** 聊天改图只接受当前 Core 中的静态图片;输入原字节不进入 Task 记录。 */
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.adapter.protocol !== 'openai-chat-images')) {
14
- throw invalid('指定模型的图片协议不支持聊天改图,请选择 openai-chat-images 模型');
15
+ && !supported(candidate))) {
16
+ throw invalid('指定模型的图片协议不支持此类改图,请选择聊天生图模型或 DashScope Qwen 原生改图模型');
15
17
  }
16
- const selected = candidates.filter(candidate => candidate.adapter.protocol === 'openai-chat-images');
17
- if (!selected.length) throw invalid('未配置可用的 openai-chat-images 生图模型,无法编辑原图');
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
+ }