@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/index.js CHANGED
@@ -309,6 +309,30 @@ export async function apply(ctx) {
309
309
  return { blocks };
310
310
  }
311
311
 
312
+ safeRegister(host, {
313
+ name: 'iris_import_image',
314
+ description: 'Save a local image or a current-session image attachment as a persistent Iris image Artifact for later viewing, editing and export. Provide exactly one of image_path or attachment_id. Supports static PNG/JPEG/WebP up to 20 MiB, preserves the source, and never calls a model.',
315
+ parameters: {
316
+ type: 'object',
317
+ properties: {
318
+ image_path: { type: 'string', description: 'Absolute path to an image readable by the DSH host' },
319
+ attachment_id: { type: 'string', description: 'Image attachment from the current session, alternative to image_path' }
320
+ },
321
+ required: []
322
+ },
323
+ output: { schema: { type: 'object' }, render: (_args, value) => value.blocks },
324
+ isConcurrencySafe: () => true,
325
+ async execute(args, exec = {}) {
326
+ const imagePath = String(args.image_path || '').trim();
327
+ const attachmentId = String(args.attachment_id || '').trim();
328
+ if (Boolean(imagePath) === Boolean(attachmentId)) throw new Error('图片导入须选择image_path或attachment_id之一');
329
+ const input = imagePath ? { image_path: imagePath } : {
330
+ image_bytes: (await resolveVisionImageInput(dshHost(ctx), exec, { attachment_id: attachmentId }, exec.signal)).bytes
331
+ };
332
+ return runAction(dshHost(ctx), 'image_import', input, { signal: exec.signal });
333
+ }
334
+ });
335
+
312
336
  /* ---------- 🖼️ 画图 ---------- */
313
337
  safeRegister(host, {
314
338
  name: 'iris_draw_image',
@@ -321,7 +345,7 @@ export async function apply(ctx) {
321
345
  prompt: { type: 'string', description: 'Detailed visual description of the image to create' },
322
346
  size: { type: 'string', description: "Output size; DashScope uses 'W*H' (e.g. '1024*1024'), OpenAI uses 'WxH'. Default provider default." },
323
347
  n: { type: 'string', description: "Number of images, default '1'" },
324
- model: { type: 'string', description: 'Override with a providerId::modelId reference (legacy bare model id also accepted)' }
348
+ model: { type: 'string', description: 'Optional assigned providerId::modelId; selects only this item (legacy bare model id also accepted)' }
325
349
  },
326
350
  required: ['prompt']
327
351
  },
@@ -383,12 +407,12 @@ export async function apply(ctx) {
383
407
 
384
408
  safeRegister(host, {
385
409
  name: 'iris_edit_image',
386
- description: 'Edit an existing Core image by Artifact ID and an instruction using a configured openai-chat-images model. Returns new Core Artifacts and DSH image attachments; preserves the source image and records its provenance.',
410
+ description: 'Edit an existing Core image by Artifact ID and an instruction using an assigned chat-image model or native DashScope Qwen edit model. Returns new Core Artifacts and DSH image attachments; preserves the source image and records its provenance.',
387
411
  parameters: {
388
412
  type: 'object', properties: {
389
413
  source_artifact_id: { type: 'string', description: 'Core Artifact ID of the source PNG, JPEG or WebP image (up to 20 MiB)' },
390
414
  prompt: { type: 'string', description: 'Describe the changes and what should be preserved in the source image' },
391
- model: { type: 'string', description: 'Optional providerId::modelId; image protocol must be openai-chat-images' },
415
+ model: { type: 'string', description: 'Optional assigned providerId::modelId; selects only this item; supports openai-chat-images or DashScope Qwen edit/max/plus and 2.0/pro models' },
392
416
  n: { type: 'string', description: "Number of images, default '1'" }
393
417
  }, required: ['source_artifact_id', 'prompt']
394
418
  },
@@ -419,7 +443,7 @@ export async function apply(ctx) {
419
443
  resolution: { type: 'string', description: "s2v only: output tier '480P' (default) or '720P'" },
420
444
  size: { type: 'string', description: "t2v/i2v only: output size 'W*H', e.g. '1280*720' (default)" },
421
445
  duration: { type: 'number', description: 't2v/i2v only: duration in seconds, if the model supports it' },
422
- model: { type: 'string', description: 'Override with a providerId::modelId reference (legacy bare model id accepted)' }
446
+ model: { type: 'string', description: 'Optional assigned providerId::modelId; selects only this item (legacy bare model id accepted)' }
423
447
  },
424
448
  required: []
425
449
  },
@@ -468,7 +492,7 @@ export async function apply(ctx) {
468
492
  description: 'Generate music using a model explicitly assigned to Music. Returns audio artifact IDs and playback links. Lyrics, vocals and duration are prompt intentions. Configured 502 retries may generate again.',
469
493
  parameters: { type: 'object', properties: {
470
494
  prompt: { type: 'string', description: 'Describe the music, including any lyrics, vocals, mood or intended duration' },
471
- model: { type: 'string', description: 'Assigned providerId::modelId reference' },
495
+ model: { type: 'string', description: 'Optional assigned providerId::modelId; selects only this item' },
472
496
  timeout_ms: { type: 'integer', description: 'Per-attempt timeout; default 300000 ms' },
473
497
  retry_502: { type: 'integer', description: '502 regeneration retries, 0–5; default 2; each retry consumes another request' },
474
498
  retry_delay_ms: { type: 'integer', description: 'Delay between 502 retries; default 5000 ms' }
@@ -491,7 +515,7 @@ export async function apply(ctx) {
491
515
  properties: {
492
516
  text: { type: 'string', description: 'Text to speak' },
493
517
  voice: { type: 'string', description: "Voice name, e.g. 'Cherry' (DashScope qwen-tts voices)" },
494
- model: { type: 'string', description: 'Override with a providerId::modelId reference (legacy bare model id accepted)' }
518
+ model: { type: 'string', description: 'Optional assigned providerId::modelId; selects only this item (legacy bare model id accepted)' }
495
519
  },
496
520
  required: ['text']
497
521
  },
@@ -528,7 +552,7 @@ export async function apply(ctx) {
528
552
  type: 'object',
529
553
  properties: {
530
554
  audio_path: { type: 'string', description: 'Absolute path to the audio file (wav/mp3)' },
531
- model: { type: 'string', description: 'Override with a providerId::modelId transcription reference (legacy bare model id accepted)' }
555
+ model: { type: 'string', description: 'Optional assigned transcription providerId::modelId; selects only this item (legacy bare model id accepted)' }
532
556
  },
533
557
  required: ['audio_path']
534
558
  },
@@ -640,7 +664,7 @@ export async function apply(ctx) {
640
664
  target_width: { type: 'integer', description: 'Frame width for sampling (default 640)' },
641
665
  transcribe: { type: 'boolean', description: 'Auto-transcribe the audio track for video_path (default true unless transcribe_text is provided). Existing frames only consume explicit text.' },
642
666
  transcribe_text: { type: 'string', description: 'Optional existing transcript text; does not create a transcription task' },
643
- model: { type: 'string', description: 'Override the configured vision model id' }
667
+ model: { type: 'string', description: 'Optional item from the vision assignment list (providerId::modelId or host:current-vision); selects only this item' }
644
668
  },
645
669
  required: []
646
670
  },
@@ -702,7 +726,7 @@ export async function apply(ctx) {
702
726
  name: `iris-sheet-${Date.now()}.png`
703
727
  });
704
728
  if (exec.signal?.aborted) throw exec.signal.reason;
705
- const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 全局视觉模型';
729
+ const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 当前默认模型';
706
730
  return {
707
731
  frames: summaryFrameRecords(frames),
708
732
  blocks: [
@@ -731,7 +755,7 @@ export async function apply(ctx) {
731
755
  image_path: { type: 'string', description: 'Absolute path to the image file (png/jpg/jpeg/webp)' },
732
756
  artifact_id: { type: 'string', description: 'Existing Core image Artifact ID in the current profile (alternative to image_path)' },
733
757
  question: { type: 'string', description: 'Question or extraction request about the image; defaults to a detailed description' },
734
- model: { type: 'string', description: 'Override the configured vision model id (default qwen-vl-plus)' }
758
+ model: { type: 'string', description: 'Optional item from the vision assignment list (providerId::modelId or host:current-vision); selects only this item' }
735
759
  },
736
760
  required: []
737
761
  },
@@ -769,7 +793,7 @@ export async function apply(ctx) {
769
793
  properties: {
770
794
  attachment_id: { type: 'string', description: 'The attachment_id of an image that appeared in this session or was generated by iris' },
771
795
  question: { type: 'string', description: 'New question about the image pixels' },
772
- model: { type: 'string', description: 'Override the configured vision model id' }
796
+ model: { type: 'string', description: 'Optional item from the vision assignment list (providerId::modelId or host:current-vision); selects only this item' }
773
797
  },
774
798
  required: ['attachment_id', 'question']
775
799
  },
@@ -934,7 +958,7 @@ export async function apply(ctx) {
934
958
  artifact_id: { type: 'string', description: 'Existing Core image Artifact ID in the current profile (alternative to image_path or attachment_id)' },
935
959
  attachment_id: { type: 'string', description: 'Session attachment id of an image (alternative to image_path)' },
936
960
  target: { type: 'string', description: 'The object or UI element to locate, e.g. "send button"' },
937
- model: { type: 'string', description: 'Override the vision model for this locate call (e.g. qwen3-vl-235b-a22b-thinking for higher grounding precision; default is the provider visionModel, typically qwen-vl-plus)' }
961
+ model: { type: 'string', description: 'Optional item from the vision assignment list (providerId::modelId or host:current-vision); selects only this item' }
938
962
  },
939
963
  required: ['target']
940
964
  },
@@ -1041,7 +1065,7 @@ export async function apply(ctx) {
1041
1065
  }
1042
1066
  });
1043
1067
 
1044
- console.log('[iris] tools registered: iris_draw_image, iris_edit_image, iris_generate_video, iris_generate_music, iris_speak_text, iris_transcribe_audio, iris_task_status, iris_look_at_image, iris_relook_attachment, iris_crop, iris_pixel_diff, iris_locate, iris_html_screenshot, iris_long_ocr, iris_video_frames, iris_media_summarize');
1068
+ console.log('[iris] tools registered: iris_import_image, iris_draw_image, iris_edit_image, iris_generate_video, iris_generate_music, iris_speak_text, iris_transcribe_audio, iris_task_status, iris_look_at_image, iris_relook_attachment, iris_crop, iris_pixel_diff, iris_locate, iris_html_screenshot, iris_long_ocr, iris_video_frames, iris_media_summarize');
1045
1069
  };
1046
1070
 
1047
1071
  /* ---------------- 视觉路由(M3 补全):look/relook 共用执行器 + 降级链 ---------------- */
@@ -1151,7 +1175,7 @@ export async function sessionAttachmentRef(host, exec, attachmentId) {
1151
1175
  /**
1152
1176
  * 看图问答(阶段 1:VisionBackend 降级链)。
1153
1177
  * 只取声明/推断出 vision 能力的自持栈 provider(严格选择),按序组装后端链,
1154
- * 全局视觉模型兜底;每个后端的失败现场保留在 errors 里。
1178
+ * 仅使用视觉功能的明确分配;宿主默认模型也需显式加入,每个后端失败保留在errors里。
1155
1179
  * @returns {{answer:string,via:'selfstack'|'global',model?:string,backendId:string}}
1156
1180
  */
1157
1181
  export async function askVision(host, { question, dataUrl, image, prepareImage, signal, model, budget }) {
@@ -1174,6 +1198,6 @@ export async function runVisionTool(host, exec, { origin, model, question, dataU
1174
1198
  model
1175
1199
  });
1176
1200
  const label = origin === 'relook' ? '重看回答' : '看图回答';
1177
- const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 全局视觉模型';
1201
+ const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 当前默认模型';
1178
1202
  return `[iris] ${label}(${usedModel || '默认'} · ${viaLabel}):\n${answer}` + (artifactId ? `\nCore Artifact: ${artifactId}` : '');
1179
1203
  }
package/lib/models.js CHANGED
@@ -26,6 +26,11 @@ export const CAP_FIELD = {
26
26
 
27
27
  const MODEL_REF_SEP = '::';
28
28
 
29
+ /** Explicit host resource; never a Provider account or an implicit fallback. */
30
+ export const HOST_VISION_REF = 'host:current-vision';
31
+ export const HOST_VISION_MODEL = Object.freeze({ ref: HOST_VISION_REF, id: 'current-vision',
32
+ providerId: HOST_VISION_REF, capabilities: Object.freeze([CAPABILITIES.VISION]), source: 'host' });
33
+
29
34
  /** 稳定的模型复合引用;provider/model 分别编码,避免同名模型跨账号碰撞。 */
30
35
  export function modelRef(providerId, modelId) {
31
36
  return encodeURIComponent(String(providerId || '')) + MODEL_REF_SEP + encodeURIComponent(String(modelId || ''));
@@ -142,6 +147,7 @@ export function resolveModelRef(provider, capability, requested = '') {
142
147
 
143
148
  /** 共享旧分配对象、裸名称和复合引用的解析,池顺序决定同名匹配。 */
144
149
  export function resolvePoolModel(pool, raw, capability) {
150
+ if (raw === HOST_VISION_REF) return pool.find(m => m.ref === raw && m.capabilities.includes(capability)) || null;
145
151
  let providerId = '';
146
152
  let modelId = '';
147
153
  if (raw && typeof raw === 'object') {
@@ -156,7 +162,7 @@ export function resolvePoolModel(pool, raw, capability) {
156
162
  && m.capabilities.includes(capability)) || null;
157
163
  }
158
164
 
159
- /** DSH 与 CLI 共用的候选顺序:分配优先、按池顺序补齐并去重。 */
165
+ /** DSH 与 CLI 共用的执行名单:只按明确分配排序并去重。 */
160
166
  export function orderedModels(pool, capability, assignments, { includeRateLimited = false } = {}) {
161
167
  const ordered = [];
162
168
  const seen = new Set();
@@ -170,7 +176,6 @@ export function orderedModels(pool, capability, assignments, { includeRateLimite
170
176
  for (const raw of (Array.isArray(assignments) ? assignments : (assignments ? [assignments] : []))) {
171
177
  add(resolvePoolModel(pool, raw, capability));
172
178
  }
173
- if (capability !== CAPABILITIES.MUSIC) for (const model of pool) add(model);
174
179
  return ordered;
175
180
  }
176
181
 
@@ -192,9 +197,10 @@ export function selectionReasonForModel(pool, capability, assignments, modelOrRe
192
197
  }
193
198
 
194
199
  /** 全局模型池:合并所有 provider 的模型 */
195
- export function modelPool(providers) {
200
+ export function modelPool(providers, { includeHostVision = false } = {}) {
196
201
  const pool = [];
197
202
  for (const p of providers || []) pool.push(...providerModels(p));
203
+ if (includeHostVision) pool.push(HOST_VISION_MODEL);
198
204
  return pool;
199
205
  }
200
206
 
@@ -12,6 +12,7 @@ export async function runOcrRequest(host, { providers = [], model, image, prepar
12
12
  const settings = normalizeOcrSettings({ chunkHeight, overlap, maxDimension, maxInvocations });
13
13
  const operation = createModelOperation({ signal, budget: { timeoutMs: budget.timeoutMs, maxInvocations: settings.maxInvocations } });
14
14
  try {
15
+ if (operation.signal.aborted) throw new ModelPortError('IRIS_MODEL_ABORTED');
15
16
  const inputs = new Map();
16
17
  const candidates = buildVisionModelCandidates(host, { providers, model, onRateLimit, prepareImages: true,
17
18
  prepareImageRequest: createVisionInputObserver(inputs) });
@@ -10,7 +10,7 @@ import * as defaultTransport from './adapters.js';
10
10
  import { ProviderContractError, providerErrorRecord } from './provider-contract.js';
11
11
  import { writePrivateFile } from './private-storage.js';
12
12
  import { defineProviderAdapter } from './provider-adapter.js';
13
- import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError } from './provider-protocol.js';
13
+ import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError, supportsImageEdit } from './provider-protocol.js';
14
14
  import { modelRateLimit, reportModelRateLimit, isFreeQuotaExhausted } from './provider-health.js';
15
15
  import { prepareVisionImage, visionInputLimits } from './vision-image-input.js';
16
16
 
@@ -132,6 +132,15 @@ function operationsForDashScope(provider, transport) {
132
132
  ...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
133
133
  };
134
134
  if (request.capability === 'image') {
135
+ let image;
136
+ if (input.image) {
137
+ if (!supportsImageEdit('dashscope', request.model)) throw new ProviderContractError('该 DashScope 模型尚未接入原生改图', {
138
+ stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
139
+ });
140
+ const limits = visionInputLimits(provider, request.model);
141
+ image = await prepareVisionImage(input.image, { ...limits, maxBytes: Math.min(limits.maxBytes, 10 * 1024 * 1024) },
142
+ { signal: request.signal, orient: true });
143
+ }
135
144
  if (transport.dashscopeImageMode(request.model) === 'legacy-async') {
136
145
  const remoteTaskId = await transport.submitImage({
137
146
  ...common, prompt: input.prompt, size: input.size, n: input.n
@@ -139,7 +148,7 @@ function operationsForDashScope(provider, transport) {
139
148
  return { kind: 'accepted', remoteTaskId };
140
149
  }
141
150
  const urls = await transport.generateImageMultimodal({
142
- ...common, prompt: input.prompt, size: input.size, n: input.n
151
+ ...common, prompt: input.prompt, size: input.size, n: input.n, ...(image ? { image } : {})
143
152
  });
144
153
  return {
145
154
  kind: 'completed', value: { kind: 'urls', items: urls },
@@ -58,20 +58,19 @@ const CANDIDATE_CAP_LABEL = Object.freeze({
58
58
  [CAPABILITIES.MUSIC]: '音乐'
59
59
  });
60
60
 
61
- /** 与稳定配置层相同的“手工顺序优先、池序补齐”候选规则。 */
61
+ /** 与 DSH 相同的严格功能名单;CLI 不附加宿主资源。 */
62
62
  function candidatesFromCatalog(catalog, requestedModelRef, capability) {
63
63
  const label = CANDIDATE_CAP_LABEL[capability];
64
64
  if (!label) fail('IRIS_PROVIDER_TASK_OBSERVE_UNSUPPORTED', '当前 Headless 候选链只开放图片、视频、语音与转写能力');
65
65
  const providers = catalog.providers.filter(isConfiguredProvider);
66
66
  const configuredPool = modelPool(providers).filter((item) => item.capabilities.includes(capability));
67
- const pool = capability === CAPABILITIES.MUSIC
68
- ? orderedModels(configuredPool, capability, catalog.assignments?.[capability], { includeRateLimited: true }) : configuredPool;
67
+ const pool = orderedModels(configuredPool, capability, catalog.assignments?.[capability], { includeRateLimited: true });
69
68
  const requested = String(requestedModelRef || '').trim();
70
69
  if (requested) {
71
70
  const parsed = parseModelRef(requested);
72
71
  if (!parsed && requested.includes('::')) fail('IRIS_PROVIDER_MODEL_INVALID', 'model_ref 格式无效');
73
72
  const exact = resolvePoolModel(pool, requested, capability);
74
- if (!exact) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `指定模型不存在、未启用或没有${label}能力`);
73
+ if (!exact) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `指定模型未分配给${label}功能、未启用或没有所需能力`);
75
74
  const provider = providers.find((item) => item.id === exact.providerId);
76
75
  if (exact.rateLimited) fail('IRIS_PROVIDER_MODEL_RATE_LIMITED', modelRateLimit(provider, exact.id)?.until
77
76
  ? '指定模型正在限流冷却,请稍后使用或手动实测'
@@ -82,14 +81,13 @@ function candidatesFromCatalog(catalog, requestedModelRef, capability) {
82
81
  })]);
83
82
  }
84
83
  const assignment = catalog.assignments?.[capability];
85
- const ordered = orderedModels(pool, capability, assignment);
86
- if (!ordered.length) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', capability === CAPABILITIES.MUSIC
87
- ? '请先给 Music 功能分配可用音乐模型' : `provider-config 中没有可用${label}模型`);
84
+ const ordered = pool.filter(model => !model.rateLimited);
85
+ if (!ordered.length) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `请先给${label}功能分配当前入口可用的模型`);
88
86
  return Object.freeze(ordered.map((item) => Object.freeze({
89
87
  provider: providers.find((provider) => provider.id === item.providerId),
90
88
  model: item.id,
91
89
  modelRef: item.ref,
92
- selectionReason: selectionReasonForModel(pool, capability, assignment, item)
90
+ selectionReason: selectionReasonForModel(configuredPool, capability, assignment, item)
93
91
  })));
94
92
  }
95
93
 
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot } from './provider-catalog.js';
5
5
  import { isKnownCapability } from './capability.js';
6
- import { providerModels, modelPool, parseModelRef, capabilitiesOfDiscoveredModel, hasConfiguredModelCapabilities, resolvePoolModel } from './models.js';
6
+ import { providerModels, modelPool, parseModelRef, HOST_VISION_REF, capabilitiesOfDiscoveredModel, hasConfiguredModelCapabilities, resolvePoolModel } from './models.js';
7
7
  import { atomicWritePrivate, privateSibling, ensurePrivateDir } from './private-storage.js';
8
8
  import { createConfiguredProviderAdapter } from './provider-adapters.js';
9
9
  import { selectImageProtocol, normalizeImageProtocol, normalizeMusicProtocol } from './provider-protocol.js';
@@ -22,7 +22,7 @@ const caps = value => {
22
22
  };
23
23
 
24
24
  function assignmentSnapshot(catalog) {
25
- const pool = modelPool(catalog.providers);
25
+ const pool = modelPool(catalog.providers, { includeHostVision: true });
26
26
  return Object.fromEntries(Object.entries(catalog.assignments || {}).map(([capability, value]) => {
27
27
  const refs = (Array.isArray(value) ? value : [value]).map(ref => resolvePoolModel(pool, ref, capability));
28
28
  return [safe(capability), { model_refs: refs.filter(Boolean).map(model => safe(model.ref)), invalidRefs: refs.filter(ref => !ref).length }];
@@ -59,7 +59,7 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
59
59
  const issues = [];
60
60
  const ids = catalog.providers.map(provider => provider.id);
61
61
  if (new Set(ids).size !== ids.length) issues.push('duplicate_provider_id');
62
- const pool = modelPool(catalog.providers);
62
+ const pool = modelPool(catalog.providers, { includeHostVision: true });
63
63
  for (const provider of catalog.providers) {
64
64
  try { normalizeVisionInputLimits(provider.visionInput); } catch (_) { issues.push('invalid_vision_input'); }
65
65
  for (const model of provider.models || []) {
@@ -201,8 +201,8 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
201
201
  if (command === 'assignments.clear') delete catalog.assignments[input.capability];
202
202
  else {
203
203
  if (!Array.isArray(input.model_refs) || new Set(input.model_refs).size !== input.model_refs.length) fail('model_refs 必须是不重复的复合引用数组');
204
- const pool = modelPool(catalog.providers);
205
- for (const ref of input.model_refs) if (!parseModelRef(ref) || !resolvePoolModel(pool, ref, input.capability)) fail('分配模型不存在或没有该能力');
204
+ const pool = modelPool(catalog.providers, { includeHostVision: true });
205
+ for (const ref of input.model_refs) if ((!parseModelRef(ref) && ref !== HOST_VISION_REF) || !resolvePoolModel(pool, ref, input.capability)) fail('分配模型不存在或没有该能力');
206
206
  catalog.assignments[input.capability] = input.model_refs;
207
207
  }
208
208
  } else fail('未知配置命令');
@@ -63,6 +63,12 @@ export function selectImageProtocol(provider, modelId) {
63
63
  return protocol === 'auto' ? selectMediaProtocol(provider) : { mediaProtocol: protocol, protocolInferred: false };
64
64
  }
65
65
 
66
+ /** 已接入的带图编辑协议;这里只判断传输,不授予模型 image-gen 能力。 */
67
+ export function supportsImageEdit(protocol, modelId) {
68
+ return protocol === 'openai-chat-images' || (protocol === 'dashscope'
69
+ && /^qwen-image-(?:edit(?:-(?:max|plus))?|2\.0(?:-pro)?)(?:-\d{4}-\d{2}-\d{2})?$/.test(String(modelId || '')));
70
+ }
71
+
66
72
  /** 给已绑定图片模型的 Adapter/binding 投影协议,不改写账号默认值。 */
67
73
  export function providerForImageModel(provider, modelId) {
68
74
  return { ...provider, ...selectImageProtocol(provider, modelId) };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  /** Host 边界:显式配置选型、字节解析和结果投影。 */
3
- import { resolveModelRef, parseModelRef } from './models.js';
3
+ import { resolveModelRef, parseModelRef, HOST_VISION_REF } from './models.js';
4
4
  import { providerApiKey } from './provider-protocol.js';
5
5
  import { createHttpVisionModelPort } from './http-vision-model-adapter.js';
6
6
  import { prepareDshVisionModelPort } from './dsh-vision-model-adapter.js';
@@ -79,18 +79,31 @@ export function createConfiguredVisionModelPort(provider, model, { allowRateLimi
79
79
  export function buildVisionModelCandidates(host, { providers = [], model, allowRateLimited = false, onRateLimit, prepareImages = false,
80
80
  orientImages = false, prepareImageRequest } = {}) {
81
81
  const candidates = [], seen = new Set();
82
- for (const provider of providers.filter(Boolean)) {
83
- const ref = resolveModelRef(provider, 'vision', model);
82
+ let selected = providers.filter(Boolean);
83
+ const requested = String(model || '').trim();
84
+ if (requested) {
85
+ const parsed = parseModelRef(requested);
86
+ const provider = selected.find(p => p.id === HOST_VISION_REF ? requested === HOST_VISION_REF
87
+ : parsed ? parsed.providerId === p.id && parsed.modelId === p.visionModel : requested === p.visionModel);
88
+ if (!provider) throw new ModelPortError('IRIS_MODEL_UNAVAILABLE', { stage: 'prepare', invocation: 'not_invoked' });
89
+ selected = [provider];
90
+ }
91
+ if (!selected.length) throw new ModelPortError('IRIS_MODEL_UNAVAILABLE', { stage: 'prepare', invocation: 'not_invoked' });
92
+ for (const provider of selected) {
93
+ if (provider.id === HOST_VISION_REF) {
94
+ const hostCandidate = deferredDshPort(host, { prepareImages, orientImages, prepareImageRequest });
95
+ candidates.push({ ...hostCandidate, id: 'dsh-vision:default', via: 'global', backendId: 'dsh-global', model: 'DSH 当前默认模型' });
96
+ continue;
97
+ }
98
+ const ref = resolveModelRef(provider, 'vision', provider.visionModel);
84
99
  const parsed = parseModelRef(ref);
85
100
  // 显式模型会把同账号的多份池记录解析为同一复合引用,只保留首次候选。
86
101
  if (parsed && seen.has(ref)) continue;
87
102
  if (parsed) seen.add(ref);
88
- const port = createConfiguredVisionModelPort(provider, model, { allowRateLimited, onRateLimit, prepareImages, orientImages, prepareImageRequest });
103
+ const port = createConfiguredVisionModelPort(provider, provider.visionModel, { allowRateLimited, onRateLimit, prepareImages, orientImages, prepareImageRequest });
89
104
  const id = port.describe().identity.backendId;
90
105
  candidates.push({ port, id, via: 'selfstack', backendId: provider.id, model: parsed?.modelId || '' });
91
106
  }
92
- const hostCandidate = deferredDshPort(host, { prepareImages, orientImages, prepareImageRequest });
93
- candidates.push({ ...hostCandidate, id: 'dsh-vision:default', via: 'global', backendId: 'dsh-global', model: '宿主视觉模型' });
94
107
  return candidates;
95
108
  }
96
109
 
@@ -137,6 +150,7 @@ function projectError(record, candidates) {
137
150
  /** prepareImage 的信号覆盖文件/会话读取;函数开始时即建立整体预算。 */
138
151
  export async function runVisionOperation(host, { providers, model, signal, budget = VISION_BUDGET, onRateLimit, prepareImages = false,
139
152
  orientImages = false, prepareImageRequest }, consume) {
153
+ if (signal?.aborted) throw new ModelPortError('IRIS_MODEL_ABORTED');
140
154
  const candidates = buildVisionModelCandidates(host, { providers, model, onRateLimit, prepareImages, orientImages, prepareImageRequest });
141
155
  const operation = createModelOperation({ signal, budget: { timeoutMs: budget.timeoutMs, maxInvocations: candidates.length } });
142
156
  try {
@@ -167,8 +181,8 @@ export async function runVisionRequest(host, { providers, model, question, image
167
181
  /** 付费确认在动作入口;红图实测只有一个供应商,不退到 Host 冒充其能力。 */
168
182
  export async function probeVisionModel(provider, model, image, { signal, timeoutMs = 15000 } = {}) {
169
183
  try {
170
- const [candidate] = buildVisionModelCandidates(null, { providers: [provider], model, allowRateLimited: true });
171
- const result = await completeVision([candidate.port], { prompt: '这张图片是什么颜色?请只回答颜色名称(中文)。', image },
184
+ const port = createConfiguredVisionModelPort(provider, model, { allowRateLimited: true });
185
+ const result = await completeVision([port], { prompt: '这张图片是什么颜色?请只回答颜色名称(中文)。', image },
172
186
  { signal, budget: { ...VISION_BUDGET, timeoutMs } });
173
187
  return { ok: /红|red/i.test(result.text), answer: result.text };
174
188
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mokuyoaxis/dsh-iris",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Multi-provider image, video, music, and speech generation, transcription, visual understanding, and prompt optimization with an integrated Iris workbench for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -34,7 +34,7 @@
34
34
  ],
35
35
  "dsh": {
36
36
  "engines": {
37
- "dsh": "0.2.0-rc.2"
37
+ "dsh": "0.2.0-rc.2 || 0.2.1-alpha.4"
38
38
  },
39
39
  "bundle": {
40
40
  "patch": "./cordis.patch.yml"
@@ -65,7 +65,8 @@
65
65
  ],
66
66
  "license": "MIT",
67
67
  "engines": {
68
- "node": ">=22.0.0"
68
+ "node": ">=22.0.0",
69
+ "dsh": "0.2.0-rc.2 || 0.2.1-alpha.4"
69
70
  },
70
71
  "scripts": {
71
72
  "test": "node scripts/test.mjs",
@@ -73,11 +74,11 @@
73
74
  "prepublishOnly": "npm test"
74
75
  },
75
76
  "peerDependencies": {
76
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
77
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
78
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
79
- "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
80
- "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2"
77
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2 || 0.2.1-alpha.4",
78
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2 || 0.2.1-alpha.4",
79
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2 || 0.2.1-alpha.4",
80
+ "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2 || 0.2.1-alpha.4",
81
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2 || 0.2.1-alpha.4"
81
82
  },
82
83
  "peerDependenciesMeta": {
83
84
  "@deepseek-ai/dsh-client-locale": {
package/user_guide.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # dsh-iris 用户指南
2
2
 
3
- 本指南说明如何安装、配置和使用 Iris 0.2.3。升级范围见 [CHANGELOG](CHANGELOG.md) 和 [发布说明](docs/releases/0.2.3.md)。
3
+ 本指南对应Iris 0.2.4。发布状态见 [README](README.md),升级范围见 [CHANGELOG](CHANGELOG.md) 和 [发布说明](docs/releases/0.2.4.md)。
4
4
 
5
- 0.2.2 的能力来自明确功能标签、旧能力字段绑定或供应商能力元数据,账号和名称不再自动赋能。已有明确配置保留;仅有裸账号的配置需要先给实际模型设置功能,再加入有序候选池。
5
+ 功能来自明确标签、旧能力字段绑定或供应商能力元数据,账号和名称不会自动赋能。0.2.4还要求把模型明确加入对应功能名单;空名单停止新调用。已有配置保留,详见 [升级说明](docs/FUNCTION_MODEL_ASSIGNMENTS.md)。
6
6
 
7
7
  ## 安装
8
8
 
9
- 先运行 `dsh --version`。Iris `0.2.3` 仅声明支持 DSH `0.2.0-rc.2`;旧 DSH `>=0.1.2-rc.1 <0.1.3-0` 或 `0.1.5-rc.1` 使用固定版本 Iris `0.1.4`。验证边界见[适配说明](docs/DSH_RC2_ADAPTATION.md)。还需要 PATH 中的 `pnpm`,以及至少一个媒体或视觉服务供应商。Iris 0.2.x 要求 Node.js 22.0.0 或更高版本,旧 npm 0.1.4 的历史要求为 20.10.0;如果所用 DSH 版本要求更高,以 DSH 为准。Iris 0.1.1 及后续 0.1.x 不兼容 DSH 0.1.0/0.1.1 的旧客户端 Runtime。
9
+ 先运行 `dsh --version`。Iris 0.2.4支持DSH `0.2.0-rc.2` 或 `0.2.1-alpha.4`;Iris0.2.0–0.2.3仅支持rc.2。旧DSH `>=0.1.2-rc.1 <0.1.3-0` 或 `0.1.5-rc.1` 固定使用Iris0.1.4,见 [兼容矩阵](docs/DSH_COMPATIBILITY.md)。还需要PATH中的pnpm及至少一个所需功能的供应商。Iris最低Node.js为22.0.0,宿主要求更高时以宿主为准;旧npm0.1.4的历史要求为20.10.0。
10
10
 
11
11
  在上述旧 DSH 范围内,从 npm 安装固定稳定版到 Web profile:
12
12
 
@@ -17,12 +17,12 @@ dsh web
17
17
 
18
18
  npm 上无 scope 的 `dsh-iris` 是另一款插件;安装或更新 Iris Media 时必须使用完整 scoped 包名。
19
19
 
20
- 只下载旧稳定包可运行 `npm pack @mokuyoaxis/dsh-iris@0.1.4`,安装为普通 npm 依赖可运行 `npm install @mokuyoaxis/dsh-iris@0.1.4`;后者不会自动加入 DSH profile。固定版本仍可在 latest 更新后安装,0.1.4 不包含新的 Core 媒体 CLI。DSH rc.2 使用 0.2.3。
20
+ 只下载旧稳定包可运行 `npm pack @mokuyoaxis/dsh-iris@0.1.4`,安装为普通npm依赖可运行 `npm install @mokuyoaxis/dsh-iris@0.1.4`;后者不会自动加入DSH profile。0.1.4不包含新的Core媒体CLI。
21
21
 
22
- 在 DSH `0.2.0-rc.2` 上,从 npm 安装:
22
+ 在DSH rc.2或alpha.4上,固定安装0.2.4:
23
23
 
24
24
  ```bash
25
- dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.3
25
+ dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.4
26
26
  dsh web
27
27
  ```
28
28
 
@@ -30,7 +30,7 @@ dsh web
30
30
 
31
31
  ### 独立 CLI
32
32
 
33
- 无需 DSH,在自己的项目安装 `npm install @mokuyoaxis/dsh-iris@0.2.3`,通过 `npx dsh-iris --help` 查看入口。详见 [Headless CLI](docs/HEADLESS_CLI.md) 和 [视觉 CLI](docs/VISION_CLI.md)。
33
+ 无需DSH,在自己的项目安装 `npm install @mokuyoaxis/dsh-iris@0.2.4`,通过 `npx dsh-iris --help` 查看入口。发布状态见README,完整说明见 [Headless CLI](docs/HEADLESS_CLI.md) 和 [视觉CLI](docs/VISION_CLI.md)。
34
34
 
35
35
  其他 DSH 版本,包括 `0.2.0` 正式版与其他 rc,须另行验收。让 Agent 协助安装时,可使用[简要 Prompt](docs/AGENT_QUICKSTART.md)。
36
36
 
@@ -44,6 +44,12 @@ dsh --profile web --dump-config
44
44
 
45
45
  输出中应出现 `@mokuyoaxis/dsh-iris` 配置层和唯一行 ID `mokuyoaxis-dsh-iris`。DSH 的插件命令和 profile 机制见[官方说明](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md)。
46
46
 
47
+ ## 导入已有图片(0.2.4)
48
+
49
+ 在作品区展开“导入图片”,选择上传文件、会话附件或宿主路径,点击“保存到作品”。支持最多20 MiB的静态PNG/JPEG/WebP,保存原字节副本、保留原文件,不需要供应商或模型分配。成功后可使用图片卡的看图、OCR、改图及下载。
50
+
51
+ 导入要求Iris0.2.4,0.2.3没有此工具。Agent入口为 `iris_import_image`,CLI入口为 `artifact import`;上传临时副本与持久作品的区别及示例见 [图片导入](docs/IMAGE_IMPORT.md)。
52
+
47
53
  ## 最快配置:DashScope
48
54
 
49
55
  打开“设置 → Iris 工作台”,然后:
@@ -55,7 +61,7 @@ dsh --profile web --dump-config
55
61
  5. 展开刚添加的供应商,点击“发现模型”。
56
62
  6. 若模型显示“未标注功能”,点击其“设置功能”,勾选已确认支持的功能并保存。
57
63
 
58
- 如果只有一个供应商,可以先保留“能力分配”为自动。Iris 会从模型池中为画图、视频、语音、转写和视觉能力选择第一个匹配模型。需要固定模型或设置故障转移顺序时,再到“能力分配(failover 顺序)”中调整。
64
+ 0.2.4中,单个供应商也需要在“功能分配(调用顺序)”为所需功能选择模型;未分配时不调用模型池。0.2.3仅Music采用严格名单,升级影响见 [功能模型分配](docs/FUNCTION_MODEL_ASSIGNMENTS.md)。
59
65
 
60
66
  保存供应商和修改能力分配都会立即生效,不需要重启 DSH。
61
67
 
@@ -129,10 +135,10 @@ Iris 先按画图、视频、语音、转写和视觉功能组织候选池,再
129
135
 
130
136
  每项能力都可以设置一个有序的“供应商 + 模型”列表。界面显示的是模型名和供应商,配置中保存为 `providerId::modelId` 复合引用,因此不同供应商的同名模型不会冲突。
131
137
 
132
- - 图片、视频、语音、转写与视觉未手工分配时,从模型池第一个匹配项开始;音乐仅使用明确分配名单。
133
- - 手工列表中的模型优先,并按列表顺序尝试。
134
- - 除音乐外,模型池中其余具备该能力的模型仍会作为后续候选。
135
- - 点击“恢复自动”会清除手工顺序;音乐清空名单后需重新分配才能生成。
138
+ - 六类功能仅使用各自分配名单,每次按名单顺序选择,不补入其他模型。
139
+ - 临时指定模型须已分配,本次只调用该项。
140
+ - “清空分配”停止新调用,模型、任务与作品保留;已有任务按原绑定观察和补交付。
141
+ - 视觉可明确加入“DSH 当前默认模型”并排序;未加入时不使用宿主视觉。
136
142
 
137
143
  生成类故障转移只覆盖上传、提交和同步生成阶段。远端一旦受理异步任务,Iris 就会继续跟踪这个任务,不会因轮询或下载失败自动重提,以免重复生成和计费。
138
144
 
@@ -172,9 +178,9 @@ Core 图片卡可直接点击“看图”或“OCR”;结果可以复制、下
172
178
 
173
179
  ### 按 Artifact ID 改图
174
180
 
175
- Core 静态 PNG/JPEG/WebP 图片卡可点击“改图”,填写修改指令,再选择聊天生图模型。点击“生成修改版”才调用模型,修改版保存为关联原图的新作品,原图保留;可继续看图、OCR、下载或编辑。
181
+ Core 静态 PNG/JPEG/WebP 图片卡可点击“改图”,填写修改指令,再选择已分配的带图编辑模型。点击“生成修改版”才调用模型,修改版保存为关联原图的新作品,原图保留;可继续看图、OCR、下载或编辑。
176
182
 
177
- Agent 可调用 `iris_edit_image`,提供 `source_artifact_id`、`prompt` 与可选 `model_ref`。CLI 使用 `run image` 的同名字段,详见 [Artifact 聊天改图](docs/CLI_MANAGEMENT.md#artifact-聊天改图)。仅支持 `openai-chat-images` 的带图生成,不提供 mask、多参考图或连续编辑历史。
183
+ Agent可调用 `iris_edit_image`,提供 `source_artifact_id`、`prompt` 与可选 `model`;CLI使用 `run image`,模型字段为 `model_ref`,见 [Artifact改图](docs/CLI_MANAGEMENT.md#artifact-聊天改图)。支持openai-chat-images带图生成,0.2.4另支持DashScope原生Qwen edit/max/plus、2.0/pro及日期快照。不提供mask、多参考图或连续编辑历史。
178
184
 
179
185
  ## 高级配置文件
180
186
 
@@ -381,11 +387,13 @@ npx @mokuyoaxis/dsh-iris doctor --json
381
387
 
382
388
  已安装包也可以直接运行 `dsh-iris doctor`。退出码为 `0`(正常)、`1`(警告)和 `2`(硬错误)。Doctor 会执行一个随后立即删除的本地写入探针,并检查配置、任务、能力分配、临时文件和产物引用;输出不含完整 API Key。离线模式无法检查 DSH 工具/客户端是否已经装载,也不会用付费请求验证 Provider。
383
389
 
384
- **0.2.0** 另提供 `doctor --data-root <绝对路径> --json`:只读盘点指定 Core 数据根的 Task、Artifact、Index 和租约,不执行上述写入探针,也不自动修复。`providers list` 与 `capabilities list` 可用 `--provider-config <私有配置绝对路径>` 只读查看脱敏供应商摘要和按配置排序的候选链;它们不代表远端能力已实测。异常退出留下租约时,先用显式 Doctor 查看 owner,再按 [Headless CLI 的恢复步骤](docs/HEADLESS_CLI.md#异常退出后的写者租约)人工确认并运行 `runtime recover`,不要手动删除租约。安装 Iris 后使用 `dsh-iris` 命令;临时运行可用 `npx --package @mokuyoaxis/dsh-iris@0.2.3 dsh-iris ...`。旧版 0.1.4 不包含这些 Core 命令。
390
+ **0.2.0** 另提供 `doctor --data-root <绝对路径> --json`:只读盘点指定 Core 数据根的 Task、Artifact、Index 和租约,不执行上述写入探针,也不自动修复。`providers list` 与 `capabilities list` 可用 `--provider-config <私有配置绝对路径>` 只读查看脱敏供应商摘要和按配置排序的候选链;它们不代表远端能力已实测。异常退出留下租约时,先用显式 Doctor 查看 owner,再按 [Headless CLI 的恢复步骤](docs/HEADLESS_CLI.md#异常退出后的写者租约)人工确认并运行 `runtime recover`,不要手动删除租约。安装 Iris 后使用 `dsh-iris` 命令;临时运行可用 `npx --package @mokuyoaxis/dsh-iris@0.2.4 dsh-iris ...`。旧版 0.1.4 不包含这些 Core 命令。
385
391
 
386
392
  ## Host Doctor
387
393
 
388
- DSH运行后打开“设置 → Iris工作台 → 宿主诊断”。卡片检查宿主/服务端/客户端版本、当前工具和随包Skill、四组路由与四个UI Slot。0.2.3 包含 16 个工具(含音乐)和三项 Skill;升级后需重启服务端、刷新页面,并为要使用的功能分配模型。点击“刷新”查看结果,原始JSON可在已认证的同一浏览器访问 `/iris/api/doctor`。
394
+ DSH运行后打开“设置 → Iris工作台 → 宿主诊断”。卡片检查宿主/服务端/客户端版本、当前工具和随包Skill、四组路由与四个UI Slot。0.2.4包含17个工具和三项Skill;升级后检查服务端实际装载、刷新页面,并为所需功能分配模型。点击“刷新”查看结果,原始JSON可在已认证的同一浏览器访问 `/iris/api/doctor`。
395
+
396
+ 0.2.4新增图片导入后检查17个工具,三项Skill保持;0.2.3为16个工具。
389
397
 
390
398
  Host Doctor 不打开 Browser、不读取附件或会话、不调用模型,也不请求供应商。报告中的 `exitCode` 是诊断严重度,不是 HTTP 状态:`0` 全部健康,`1` 有可降级警告,`2` 有硬错误。未安装 HTML Browser 时出现一条 Browser 警告是预期降级;如果提示尚未收到客户端握手,刷新 DSH 页面后再点“刷新”。若显示 `not found`,通常是前端已更新而 DSH 后端仍运行旧插件代码,应重启 DSH 后再刷新页面。
391
399
 
@@ -425,7 +433,7 @@ dsh plugin --profile web remove @mokuyoaxis/dsh-iris
425
433
  - **没有可用模型**:先确认供应商已启用且 Key 有效,再运行“发现模型”;未标注模型需通过“设置功能”选择用途,不支持 `/models` 时手动添加并标注。
426
434
  - **OpenAI 兼容图片请求走错协议**:核对模型的有效图片协议;`/images/generations` 使用 `openai-images`,网关聊天图片扩展使用 `openai-chat-images`,同步 `/responses` 生图使用 `openai-responses-images`。可在生图模型行或 CLI `models protocol` 设置模型级覆盖,不需要改变同账号其他模型的接口。
427
435
  - **反向代理访问返回 `untrusted host`**:把浏览器实际使用的域名或 `host:port` 加入 `IRIS_TRUSTED_HOSTS`,保留 Host,并确认代理层已经启用认证。
428
- - **看图可用但 Iris 模型池显示为空**:Agent 视觉工具可以回退到 DSH 全局视觉模型;需要在工作台操作时仍建议配置 Iris 的 `vision` 模型。
436
+ - **模型池为空时使用宿主看图**:0.2.4需在视觉分配中明确加入“DSH 当前默认模型”;宿主默认模型须声明图片输入。也可配置并分配自持 `vision` 模型。
429
437
  - **视频摘要提示 ffmpeg 不可用**:安装系统级 `ffmpeg` 和 `ffprobe`,并确保 DSH 进程的 PATH 能找到它们。
430
438
  - **本地文件不存在**:这里的“本地”指 DSH 宿主,不一定是浏览器所在设备;优先用“上传文件”。
431
439
  - **视频任务长时间运行**:不要重复提交。用 `iris_task_status` 或工作台任务详情查询原任务。