@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.
Files changed (45) 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 +27 -1
  5. package/README.en.md +34 -44
  6. package/README.md +34 -44
  7. package/bin/dsh-iris.js +8 -0
  8. package/docs/AGENT_QUICKSTART.md +11 -13
  9. package/docs/ARCHITECTURE.md +3 -1
  10. package/docs/CLI_MANAGEMENT.md +11 -3
  11. package/docs/DSH_COMPATIBILITY.md +69 -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 +11 -7
  16. package/docs/HOST_DOCTOR.md +5 -3
  17. package/docs/IMAGE_IMPORT.md +49 -0
  18. package/docs/ROADMAP.md +6 -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/docs/releases/0.2.5.md +33 -0
  24. package/lib/actions.js +32 -12
  25. package/lib/adapters.js +5 -2
  26. package/lib/client.js +28 -29
  27. package/lib/command-service.js +16 -1
  28. package/lib/config.js +16 -22
  29. package/lib/doctor.js +4 -10
  30. package/lib/dsh-compatibility.js +16 -0
  31. package/lib/dsh-core-adapter.js +7 -1
  32. package/lib/dsh-host-adapter.js +22 -29
  33. package/lib/host-runtime.js +1 -0
  34. package/lib/image-edit-input.js +7 -5
  35. package/lib/image-import.js +61 -0
  36. package/lib/index.js +39 -15
  37. package/lib/models.js +9 -3
  38. package/lib/ocr-model-routing.js +1 -0
  39. package/lib/provider-adapters.js +11 -2
  40. package/lib/provider-catalog.js +6 -8
  41. package/lib/provider-config-service.js +5 -5
  42. package/lib/provider-protocol.js +6 -0
  43. package/lib/vision-model-routing.js +22 -8
  44. package/package.json +9 -8
  45. 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: 指定的模型复合引用不可用或不具备' + label + '能力');
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: selectImageProtocol(p, m.id) } : {}),
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 visionProviders = store.pickAllFor(cap.CAPABILITIES.VISION, { includeRateLimited: true }).filter((x) => x.id === p.id);
1107
- if (!visionProviders.length) return { ok: true, tested: false, text: '该供应商未配置 vision 模型,跳过' };
1108
- const model = configuredModel(visionProviders[0], cap.CAPABILITIES.VISION);
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(visionProviders[0], model, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal });
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 pool = models.modelPool(store.allProviders());
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
- byCap[c].push({ ref: m.ref, id: m.id, providerId: m.providerId, label: m.id + ' · ' + m.providerId });
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.keys(byCap)) order[c] = store.assignmentOrder(c);
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: [{ text: prompt }] }] },
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.3';
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.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('、') + ';请核对所装 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
+ }
@@ -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)
@@ -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, { defaults, requiresRoute }) {
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
- let route = {};
102
- if (requiresRoute) {
103
- // rc.2 路由检查前就停止取消请求;旧桥接继续由宿主处理 signal,
104
- // 其 fallback 语义待共享调用控制迁移时统一。
105
- if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('已取消');
106
- const selection = defaults.currentSelection();
107
- if (typeof selection?.provider !== 'string' || !selection.provider.trim()
108
- || typeof selection?.model !== 'string' || !selection.model.trim()) {
109
- throw visionRouteError('DSH 默认模型没有明确路由;请在 Iris 配置视觉模型');
110
- }
111
- // 默认文本选择只有明确声明 image 输入时才可成为视觉候选。
112
- const { provider, model } = selection;
113
- const info = await llm.resolveModelInfo(provider, model, signal);
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: undefined,
125
- model: undefined,
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
- // rc.2 的 Runtime 把 Adapter 异常转换为 finish,而不是单独的 error 事件。
138
- if (requiresRoute && chunk?.type === 'finish') {
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
- const requiresRoute = /^0\.2\./.test(version);
223
- if (!requiresRoute || defaults && typeof defaults.currentSelection === 'function'
215
+ // 旧 SDK 同样提供精确路由;按接口探测,避免用宿主版本号误选历史路径。
216
+ if (defaults && typeof defaults.currentSelection === 'function'
224
217
  && typeof llm.resolveModelInfo === 'function') {
225
- ports.visionModel = dshVisionPort(llm, { defaults, requiresRoute });
218
+ ports.visionModel = dshVisionPort(llm, defaults);
226
219
  } else unavailable.visionModel = {
227
- kind: 'unavailable', reason: 'DSH 0.2 视觉桥接需要默认模型选择和精确图片能力元数据'
220
+ kind: 'unavailable', reason: 'DSH 视觉桥接需要默认模型选择和精确图片能力元数据'
228
221
  };
229
222
  } else {
230
223
  unavailable.textModel = unavailableFor(llm, 'DSH llm 需要 stream');
@@ -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',