@mokuyoaxis/dsh-iris 0.2.4 → 0.2.6

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 (53) hide show
  1. package/.dsh/skills/iris-use/SKILL.md +2 -2
  2. package/.dsh/skills/iris-use/references/media-contract.md +2 -2
  3. package/CHANGELOG.md +38 -1
  4. package/README.en.md +34 -41
  5. package/README.md +35 -40
  6. package/bin/dsh-iris.js +29 -12
  7. package/docs/AGENT_QUICKSTART.md +11 -13
  8. package/docs/ARCHITECTURE.md +1 -1
  9. package/docs/CLI_MANAGEMENT.md +5 -1
  10. package/docs/DSH_COMPATIBILITY.md +38 -8
  11. package/docs/DSH_PROVIDER_CONNECTIONS.md +34 -0
  12. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  13. package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +2 -0
  14. package/docs/FUNCTION_MODEL_POOLS.md +84 -0
  15. package/docs/HEADLESS_CLI.md +7 -7
  16. package/docs/HOST_ADAPTER_CONTRACT.md +8 -2
  17. package/docs/HOST_DOCTOR.md +3 -3
  18. package/docs/MUSIC.md +2 -2
  19. package/docs/PROVIDER_HEALTH.md +26 -2
  20. package/docs/ROADMAP.md +8 -4
  21. package/docs/SECURITY.md +12 -2
  22. package/docs/VISION_CLI.md +2 -2
  23. package/docs/releases/0.2.5.md +33 -0
  24. package/docs/releases/0.2.6.md +50 -0
  25. package/lib/actions.js +137 -30
  26. package/lib/adapters.js +36 -9
  27. package/lib/api.js +2 -1
  28. package/lib/client.js +717 -180
  29. package/lib/composite-vision-routing.js +6 -6
  30. package/lib/config.js +140 -29
  31. package/lib/doctor.js +9 -2
  32. package/lib/dsh-compatibility.js +7 -1
  33. package/lib/dsh-core-adapter.js +22 -11
  34. package/lib/dsh-host-adapter.js +32 -29
  35. package/lib/dsh-provider-config.js +35 -0
  36. package/lib/dsh-provider-source.js +102 -0
  37. package/lib/headless-vision.js +5 -5
  38. package/lib/host-contract.js +1 -0
  39. package/lib/http-vision-model-adapter.js +5 -2
  40. package/lib/index.js +54 -35
  41. package/lib/model-directory-service.js +53 -0
  42. package/lib/model-directory.js +103 -0
  43. package/lib/models.js +71 -6
  44. package/lib/ocr-model-routing.js +2 -2
  45. package/lib/provider-adapters.js +42 -9
  46. package/lib/provider-catalog.js +14 -9
  47. package/lib/provider-config-scope.js +16 -0
  48. package/lib/provider-config-service.js +82 -13
  49. package/lib/provider-health.js +33 -0
  50. package/lib/provider-protocol.js +30 -0
  51. package/lib/vision-model-routing.js +25 -12
  52. package/package.json +9 -9
  53. package/user_guide.md +28 -29
@@ -3,14 +3,16 @@ 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, HOST_VISION_REF, capabilitiesOfDiscoveredModel, hasConfiguredModelCapabilities, resolvePoolModel } from './models.js';
6
+ import { providerModels, modelPool, functionPolicy, savedAssignmentRefs, 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
- import { selectImageProtocol, normalizeImageProtocol, normalizeMusicProtocol } from './provider-protocol.js';
9
+ import { selectImageProtocol, normalizeImageProtocol, normalizeMusicProtocol, isConfiguredProvider } from './provider-protocol.js';
10
10
  import { invokeProviderOperation } from './provider-adapter.js';
11
11
  import { redactProviderMessage } from './provider-contract.js';
12
- import { modelRateLimit, recordModelRateLimit, clearModelRateLimit, clearModelCapabilityHealth, isDefinitiveHealthFailure } from './provider-health.js';
12
+ import { modelRateLimit, recordModelRateLimit, clearModelRateLimit, clearModelCapabilityHealth, isDefinitiveHealthFailure, recordModelAvailability, resetHealthOnCriticalChange } from './provider-health.js';
13
13
  import { normalizeVisionInputLimits, visionInputLimits } from './vision-image-input.js';
14
+ import { currentModelDirectory, modelDirectoryBinding, modelAvailabilityBinding } from './model-directory.js';
15
+ import { refreshModelDirectory } from './model-directory-service.js';
14
16
 
15
17
  const fail = message => { throw Object.assign(new Error(message), { code: 'IRIS_CONFIG_INPUT_INVALID' }); };
16
18
  const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
@@ -24,14 +26,18 @@ const caps = value => {
24
26
  function assignmentSnapshot(catalog) {
25
27
  const pool = modelPool(catalog.providers, { includeHostVision: true });
26
28
  return Object.fromEntries(Object.entries(catalog.assignments || {}).map(([capability, value]) => {
27
- const refs = (Array.isArray(value) ? value : [value]).map(ref => resolvePoolModel(pool, ref, capability));
28
- return [safe(capability), { model_refs: refs.filter(Boolean).map(model => safe(model.ref)), invalidRefs: refs.filter(ref => !ref).length }];
29
+ const policy = functionPolicy(value, capability);
30
+ const refs = policy.models.map(ref => resolvePoolModel(pool, ref, capability));
31
+ return [safe(capability), { mode: policy.mode, hostVision: policy.hostVision,
32
+ model_refs: refs.filter(Boolean).map(model => safe(model.ref)), invalidRefs: refs.filter(ref => !ref).length }];
29
33
  }));
30
34
  }
31
35
 
32
36
  export function configSnapshot(catalog) {
33
37
  return { ...providerCatalogSnapshot(catalog), models: catalog.providers.flatMap(provider => providerModels(provider).map(model => ({
34
38
  ref: safe(model.ref), providerId: safe(provider.id), id: safe(model.id), capabilities: model.capabilities,
39
+ ...(model.catalogStatus ? { catalogStatus: model.catalogStatus } : {}),
40
+ ...(model.unavailableCapabilities ? { unavailableCapabilities: model.unavailableCapabilities } : {}),
35
41
  ...(model.imageProtocol !== undefined ? { imageProtocol: model.imageProtocol } : {}),
36
42
  ...(model.musicProtocol !== undefined ? { musicProtocol: model.musicProtocol } : {}),
37
43
  ...(model.visionInput !== undefined ? { visionInput: model.visionInput } : {}),
@@ -73,7 +79,8 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
73
79
  }
74
80
  for (const [capability, assignment] of Object.entries(catalog.assignments || {})) {
75
81
  if (!isKnownCapability(capability)) issues.push('unknown_assignment_capability');
76
- for (const ref of (Array.isArray(assignment) ? assignment : [assignment])) if (!resolvePoolModel(pool, ref, capability)) issues.push('unavailable_assignment_model');
82
+ const policy = functionPolicy(assignment, capability);
83
+ if (policy.mode === 'manual') for (const ref of policy.models) if (!resolvePoolModel(pool, ref, capability)) issues.push('unavailable_assignment_model');
77
84
  }
78
85
  return { valid: issues.length === 0, issues: [...new Set(issues)] };
79
86
  }
@@ -116,6 +123,7 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
116
123
  clearModelCapabilityHealth(updated, model.id, 'image-gen');
117
124
  }
118
125
  }
126
+ resetHealthOnCriticalChange(provider, updated, value);
119
127
  }
120
128
  if (provider) catalog.providers[catalog.providers.indexOf(provider)] = updated;
121
129
  else catalog.providers.push(updated);
@@ -134,6 +142,9 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
134
142
  const models = discovered.models.map(model => ({ id: text(typeof model === 'string' ? model : model.id), capabilities: capabilitiesOfDiscoveredModel(model), source: 'discovered' }));
135
143
  result = { command, changed: input.apply === true, total: models.length, mediaCount: models.filter(model => model.capabilities.length).length, models: models.map(model => ({ ...model, id: safe(model.id) })) };
136
144
  if (input.apply !== true) return result;
145
+ provider.modelDirectory = { version: 1, binding: modelDirectoryBinding(provider), checkedAt: new Date().toISOString(),
146
+ models: models.map(({ id, capabilities }) => ({ id, capabilities })),
147
+ added: models.filter(model => !entries.some(entry => entry.id === model.id)).map(model => model.id) };
137
148
  for (const model of models) {
138
149
  const old = entries.find(value => value.id === model.id);
139
150
  if (!old) entries.push(model);
@@ -181,12 +192,14 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
181
192
  const tested = await probe({ provider, modelId, capability: input.capability, signal });
182
193
  if (tested.skipped) return { command, changed: false, ...tested };
183
194
  const evidence = { at: new Date().toISOString(), source: 'probe', category: tested.category || (tested.ok ? 'success' : 'unknown'),
195
+ ...(tested.providerCode ? { providerCode: safe(tested.providerCode) } : {}),
184
196
  ...(tested.httpStatus || tested.status ? { httpStatus: tested.httpStatus || tested.status } : {}) };
185
197
  entry.verified = { ...(entry.verified || {}), [input.capability]: { ok: tested.ok === true, ...evidence } };
186
198
  provider.health ||= { version: 1, revision: 0, observations: [] };
187
199
  const observations = provider.health.observations;
188
200
  let observation = observations.find(value => value.modelId === modelId && value.capability === input.capability);
189
201
  if (!observation) { observation = { modelId, capability: input.capability }; observations.push(observation); }
202
+ recordModelAvailability(provider, modelId, input.capability, { ...tested, ...evidence });
190
203
  if (tested.ok === true) { observation.lastSuccess = evidence; clearModelRateLimit(provider, modelId); }
191
204
  else {
192
205
  observation[isDefinitiveHealthFailure(evidence) ? 'lastFailure' : 'lastTransient'] = evidence;
@@ -195,11 +208,34 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
195
208
  result = { command, changed: true, passed: tested.ok === true, ...(tested.taskId ? { taskId: tested.taskId } : {}), ...(tested.code ? { code: tested.code } : {}) };
196
209
  } else fail('未知模型命令');
197
210
  provider.models = entries;
211
+ if (['models.add', 'models.caps'].includes(command) && input.enableAutomatic !== undefined) {
212
+ if (typeof input.enableAutomatic !== 'boolean') fail('enableAutomatic 必须是布尔值');
213
+ if (input.enableAutomatic) for (const capability of caps(input.capabilities)) {
214
+ catalog.assignments ||= {};
215
+ const previous = functionPolicy(catalog.assignments[capability], capability);
216
+ catalog.assignments[capability] = { mode: 'auto', models: savedAssignmentRefs(modelPool(catalog.providers, { includeHostVision: true }), capability, previous.models),
217
+ ...(capability === 'vision' ? { hostVision: previous.hostVision } : {}) };
218
+ }
219
+ }
198
220
  } else if (command === 'assignments.set' || command === 'assignments.clear') {
199
221
  if (!isKnownCapability(input.capability)) fail('未知能力');
200
222
  catalog.assignments ||= {};
201
223
  if (command === 'assignments.clear') delete catalog.assignments[input.capability];
202
- else {
224
+ else if (input.mode !== undefined) {
225
+ const previous = functionPolicy(catalog.assignments[input.capability], input.capability);
226
+ const next = { mode: input.mode, models: savedAssignmentRefs(modelPool(catalog.providers, { includeHostVision: true }), input.capability, previous.models),
227
+ ...(input.capability === 'vision' ? { hostVision: input.hostVision ?? previous.hostVision } : {}) };
228
+ functionPolicy(next, input.capability);
229
+ if (input.hostVision && input.capability !== 'vision') fail('hostVision 仅用于视觉');
230
+ if (input.model_refs !== undefined) {
231
+ if (!Array.isArray(input.model_refs) || new Set(input.model_refs).size !== input.model_refs.length) fail('model_refs 必须是不重复的引用数组');
232
+ const pool = modelPool(catalog.providers, { includeHostVision: true });
233
+ for (const ref of input.model_refs) if ((typeof ref !== 'string' || (!parseModelRef(ref) && ref !== HOST_VISION_REF))
234
+ || !resolvePoolModel(pool, ref, input.capability)) fail('分配模型不存在或没有该能力');
235
+ next.models = input.model_refs;
236
+ }
237
+ catalog.assignments[input.capability] = next;
238
+ } else {
203
239
  if (!Array.isArray(input.model_refs) || new Set(input.model_refs).size !== input.model_refs.length) fail('model_refs 必须是不重复的复合引用数组');
204
240
  const pool = modelPool(catalog.providers, { includeHostVision: true });
205
241
  for (const ref of input.model_refs) if ((!parseModelRef(ref) && ref !== HOST_VISION_REF) || !resolvePoolModel(pool, ref, input.capability)) fail('分配模型不存在或没有该能力');
@@ -211,8 +247,9 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
211
247
  if (command === 'providers.remove' || command === 'models.remove' || command === 'models.caps') {
212
248
  const pool = modelPool(catalog.providers);
213
249
  for (const [capability, assignment] of Object.entries(catalog.assignments || {})) {
214
- const values = (Array.isArray(assignment) ? assignment : [assignment]).filter(ref => resolvePoolModel(pool, ref, capability) || !resolvePoolModel(initialPool, ref, capability));
215
- if (!values.length) delete catalog.assignments[capability];
250
+ const values = functionPolicy(assignment, capability).models.filter(ref => resolvePoolModel(pool, ref, capability) || !resolvePoolModel(initialPool, ref, capability));
251
+ if (assignment && typeof assignment === 'object' && !Array.isArray(assignment) && Object.hasOwn(assignment, 'mode')) catalog.assignments[capability] = { ...assignment, models: values };
252
+ else if (!values.length) delete catalog.assignments[capability];
216
253
  else catalog.assignments[capability] = Array.isArray(assignment) ? values : values[0];
217
254
  }
218
255
  }
@@ -230,7 +267,37 @@ export function recordCatalogRateLimit(configPath, event) {
230
267
  return writeConfigChange(configPath, catalog, before, { changed: true });
231
268
  }
232
269
 
233
- function writeConfigChange(configPath, catalog, before, result) {
270
+ /** CLI真实提交的可用性证据,复用同一配置锁;不为可重建元数据新增备份。 */
271
+ export function recordCatalogAvailability(configPath, { providerId, modelId, capability, ...event }) {
272
+ const catalog = loadProviderCatalog(configPath);
273
+ const before = fs.readFileSync(configPath);
274
+ const provider = catalog.providers.find(item => item.id === providerId);
275
+ const cap = { image: 'image-gen', video: 'video-gen', music: 'music-gen' }[capability] || capability;
276
+ if (!provider || (event.binding && modelAvailabilityBinding(provider, modelId, cap) !== event.binding)
277
+ || !recordModelAvailability(provider, modelId, capability, event)) return;
278
+ const directory = currentModelDirectory(provider);
279
+ if (directory && event.ok !== true) directory.invalidatedAt = event.at || new Date().toISOString();
280
+ return writeConfigChange(configPath, catalog, before, { changed: true }, { backup: false });
281
+ }
282
+
283
+ /** 新的自动请求按需读取目录;只读CLI命令与手选/停用不走这里。 */
284
+ export async function refreshCatalogDirectories(configPath, catalog, capability, { signal } = {}) {
285
+ if (functionPolicy(catalog.assignments?.[capability], capability).mode !== 'auto') return [];
286
+ return Promise.all(catalog.providers.filter(provider => isConfiguredProvider(provider)
287
+ && providerModels(provider).some(model => model.capabilities.includes(capability))).map(provider =>
288
+ refreshModelDirectory(provider, { signal, save: directory => {
289
+ const fresh = loadProviderCatalog(configPath);
290
+ const before = fs.readFileSync(configPath);
291
+ const current = fresh.providers.find(item => item.id === provider.id);
292
+ if (!current || modelDirectoryBinding(current) !== directory.binding) return false;
293
+ current.modelDirectory = directory;
294
+ writeConfigChange(configPath, fresh, before, { changed: true }, { backup: false });
295
+ provider.modelDirectory = directory;
296
+ return true;
297
+ } })));
298
+ }
299
+
300
+ function writeConfigChange(configPath, catalog, before, result, { backup = true } = {}) {
234
301
  const lock = configPath + '.lock';
235
302
  let fd;
236
303
  try { fd = fs.openSync(lock, 'wx', 0o600); }
@@ -238,10 +305,12 @@ function writeConfigChange(configPath, catalog, before, result) {
238
305
  try {
239
306
  loadProviderCatalog(configPath);
240
307
  if (hash(fs.readFileSync(configPath)) !== hash(before)) throw Object.assign(new Error('配置在操作期间发生变化,请重新读取后重试'), { code: 'IRIS_CONFIG_CHANGED' });
241
- const backupPath = privateSibling(configPath, 'backup');
242
- fs.writeFileSync(backupPath, before, { flag: 'wx', mode: 0o600 });
308
+ if (backup) {
309
+ const backupPath = privateSibling(configPath, 'backup');
310
+ fs.writeFileSync(backupPath, before, { flag: 'wx', mode: 0o600 });
311
+ result.backupPath = backupPath;
312
+ }
243
313
  atomicWritePrivate(configPath, JSON.stringify(catalog, null, 2) + '\n');
244
- result.backupPath = backupPath;
245
314
  return result;
246
315
  } finally { fs.closeSync(fd); fs.unlinkSync(lock); }
247
316
  }
@@ -1,4 +1,37 @@
1
1
  'use strict';
2
+ import { isKnownCapability } from './capability.js';
3
+ import { isModelNotFound, modelAvailabilityBinding, modelUnavailable } from './model-directory.js';
4
+
5
+ const HEALTH_SENSITIVE_FIELDS = new Set([
6
+ 'auth', 'type', 'baseUrl', 'mediaBaseUrl', 'apiKey', 'mediaProtocol',
7
+ 'imageModel', 'videoModel', 'ttsModel', 'transcribeModel', 'visionModel'
8
+ ]);
9
+
10
+ /** 明确修改连接后旧健康证据失效;额度停用仍需显式实测解除。 */
11
+ export function resetHealthOnCriticalChange(previous, next, patch) {
12
+ if (![...HEALTH_SENSITIVE_FIELDS].some(key => Object.hasOwn(patch, key) && previous[key] !== next[key])) return;
13
+ const current = previous.health;
14
+ next.health = { version: 1, revision: (Number.isSafeInteger(current?.revision) ? current.revision : 0) + 1,
15
+ observations: [], ...(current?.rateLimits?.length ? { rateLimits: current.rateLimits } : {}) };
16
+ }
17
+
18
+ /** 仅提交或显式实测更新模型可选性,远端旧任务的观察不解除阻断。 */
19
+ export function recordModelAvailability(provider, modelId, capability, result) {
20
+ const cap = { image: 'image-gen', video: 'video-gen', music: 'music-gen' }[capability] || capability;
21
+ if (!modelId || !isKnownCapability(cap)) return false;
22
+ let observation = provider.health?.observations?.find(item => item.modelId === modelId && item.capability === cap);
23
+ const blocked = isModelNotFound(result);
24
+ if (!blocked && !(result.ok === true && (result.source === 'probe' || result.stage === 'submit')
25
+ && (observation?.availability?.state === 'blocked' || modelUnavailable(provider, modelId, cap)))) return false;
26
+ provider.health ||= { version: 1, revision: 0, observations: [] };
27
+ provider.health.observations ||= [];
28
+ observation ||= provider.health.observations.find(item => item.modelId === modelId && item.capability === cap);
29
+ if (!observation) { observation = { modelId, capability: cap }; provider.health.observations.push(observation); }
30
+ observation.availability = { state: blocked ? 'blocked' : 'available', at: result.at || new Date().toISOString(),
31
+ binding: result.binding || modelAvailabilityBinding(provider, modelId, cap), source: result.source === 'probe' ? 'probe' : 'task',
32
+ ...(blocked ? { httpStatus: 404, reason: 'model_not_found' } : {}) };
33
+ return true;
34
+ }
2
35
 
3
36
  export const PROVIDER_HEALTH_SCHEMA_VERSION = 1;
4
37
  export const PROVIDER_HEALTH_FRESH_MS = 7 * 24 * 60 * 60 * 1000;
@@ -114,6 +114,36 @@ export function isConfiguredProvider(provider) {
114
114
  && (provider.auth === 'none' || (typeof provider.apiKey === 'string' && provider.apiKey.trim())));
115
115
  }
116
116
 
117
+ /** 已知需要其他接口的语音系列;自定义模型仍以明确配置和实测为准。 */
118
+ export function dashscopeTtsProtocolIssue(modelId) {
119
+ const model = String(modelId || '');
120
+ if (/^qwen(?:\d+(?:\.\d+)?)?-(?:tts|omni|audio)-.*realtime(?:-|$)/i.test(model)) {
121
+ return '该实时语音模型需要 WebSocket,Iris 尚未接入该调用方式';
122
+ }
123
+ if (/^(?:qwen-audio-|cosyvoice(?:-|$)|sambert-|MiniMax\/speech-)/i.test(model)) {
124
+ return 'Iris 尚未接入该语音系列的合成接口;当前使用 Qwen-TTS 普通 HTTP 接口';
125
+ }
126
+ return '';
127
+ }
128
+
129
+ /** 复刻/设计模型不能沿用普通模型的默认音色。 */
130
+ export function dashscopeTtsInputIssue(modelId, voice) {
131
+ return dashscopeTtsProtocolIssue(modelId)
132
+ || (/^qwen(?:\d+)?-tts-(?:vc|vd)(?:-|$)/i.test(String(modelId || '')) && !String(voice || '').trim()
133
+ ? '该模型需要先创建复刻或设计音色;请在语音功能填写与该模型匹配的音色 ID 后验证'
134
+ : '');
135
+ }
136
+
137
+ /** Protocol support is independent from model capability declarations and health. */
138
+ export function supportsModelCapability(provider, modelId, capability, { checkEndpoint = true } = {}) {
139
+ if (capability === 'vision') return true;
140
+ if (capability === 'music-gen') return provider?.models?.some(model => model?.id === modelId && model.musicProtocol === 'gemini-web-music') === true;
141
+ const protocol = capability === 'image-gen' ? selectImageProtocol(provider, modelId).mediaProtocol : selectMediaProtocol(provider).mediaProtocol;
142
+ if (protocol === 'dashscope') return (capability !== 'tts' || !dashscopeTtsProtocolIssue(modelId))
143
+ && (!checkEndpoint || isDashScopeBaseUrl(providerMediaBaseUrl(provider)));
144
+ return capability === 'image-gen' && ['openai-images', 'openai-chat-images', 'openai-responses-images'].includes(protocol);
145
+ }
146
+
117
147
  /**
118
148
  * 生图尺寸的协议差异集中在这里,调用点不再自带厂商假设。
119
149
  *
@@ -7,7 +7,8 @@ import { prepareDshVisionModelPort } from './dsh-vision-model-adapter.js';
7
7
  import { createModelOperation } from './model-invoker.js';
8
8
  import { ModelPortError, normalizeModelDescriptor, normalizeModelCall, modelErrorRecord } from './model-port-contract.js';
9
9
  import { waitForModelWork } from './model-call-runtime.js';
10
- import { modelRateLimit, reportModelRateLimit } from './provider-health.js';
10
+ import { modelRateLimit, reportModelRateLimit, recordModelAvailability } from './provider-health.js';
11
+ import { modelAvailabilityBinding, modelUnavailable } from './model-directory.js';
11
12
  import { completeVision, VISION_BUDGET } from './vision-core.js';
12
13
  import { open } from 'node:fs/promises';
13
14
  import { DEFAULT_VISION_INPUT, visionInputLimits } from './vision-image-input.js';
@@ -47,7 +48,7 @@ function deferredDshPort(host, { prepareImages, orientImages, prepareImageReques
47
48
  }
48
49
 
49
50
  /** 配置只绑定一个自持模型;CLI 不附加宿主候选。 */
50
- export function createConfiguredVisionModelPort(provider, model, { allowRateLimited = false, onRateLimit, prepareImages = false,
51
+ export function createConfiguredVisionModelPort(provider, model, { allowRateLimited = false, onRateLimit, onModelAvailability, prepareImages = false,
51
52
  orientImages = false, prepareImageRequest } = {}) {
52
53
  const parsed = parseModelRef(resolveModelRef(provider, 'vision', model));
53
54
  const id = 'iris-provider:' + encodeURIComponent(provider.id);
@@ -59,13 +60,25 @@ export function createConfiguredVisionModelPort(provider, model, { allowRateLimi
59
60
  baseUrl: provider.baseUrl, apiKey: providerApiKey(provider),
60
61
  orientImages, prepareImageRequest,
61
62
  ...(prepareImages ? { imageInput: visionInputLimits(provider, parsed.modelId) } : {}) });
63
+ const availability = result => {
64
+ const event = { ...result, stage: 'submit', source: allowRateLimited ? 'probe' : 'task',
65
+ binding: modelAvailabilityBinding(provider, parsed.modelId, 'vision') };
66
+ if (!recordModelAvailability(provider, parsed.modelId, 'vision', event)) return;
67
+ try { onModelAvailability?.({ providerId: provider.id, modelId: parsed.modelId, capability: 'vision', ...event }); }
68
+ catch (_) { console.warn('[iris] 模型可用性未能持久化,请检查配置写入权限'); }
69
+ };
62
70
  return Object.freeze({ describe: port.describe,
63
71
  async complete(request, options) {
64
- if (!allowRateLimited && modelRateLimit(provider, parsed.modelId)) {
72
+ if (!allowRateLimited && (modelRateLimit(provider, parsed.modelId) || modelUnavailable(provider, parsed.modelId, 'vision'))) {
65
73
  throw new ModelPortError('IRIS_MODEL_UNAVAILABLE', { stage: 'prepare', invocation: 'not_invoked' });
66
74
  }
67
- try { return await port.complete(request, options); }
75
+ try {
76
+ const result = await port.complete(request, options);
77
+ availability({ ok: true });
78
+ return result;
79
+ }
68
80
  catch (error) {
81
+ availability({ ok: false, httpStatus: error.status, providerCode: error.providerCode });
69
82
  reportModelRateLimit(provider, parsed.modelId, { status: error.status, code: error.code,
70
83
  providerCode: error.providerCode, retryAfterMs: error.retryAfterMs,
71
84
  ...(error.quotaExhausted ? { reason: 'free_quota' } : {}),
@@ -76,7 +89,7 @@ export function createConfiguredVisionModelPort(provider, model, { allowRateLimi
76
89
  });
77
90
  }
78
91
 
79
- export function buildVisionModelCandidates(host, { providers = [], model, allowRateLimited = false, onRateLimit, prepareImages = false,
92
+ export function buildVisionModelCandidates(host, { providers = [], model, allowRateLimited = false, onRateLimit, onModelAvailability, prepareImages = false,
80
93
  orientImages = false, prepareImageRequest } = {}) {
81
94
  const candidates = [], seen = new Set();
82
95
  let selected = providers.filter(Boolean);
@@ -100,7 +113,7 @@ export function buildVisionModelCandidates(host, { providers = [], model, allowR
100
113
  // 显式模型会把同账号的多份池记录解析为同一复合引用,只保留首次候选。
101
114
  if (parsed && seen.has(ref)) continue;
102
115
  if (parsed) seen.add(ref);
103
- const port = createConfiguredVisionModelPort(provider, provider.visionModel, { allowRateLimited, onRateLimit, prepareImages, orientImages, prepareImageRequest });
116
+ const port = createConfiguredVisionModelPort(provider, provider.visionModel, { allowRateLimited, onRateLimit, onModelAvailability, prepareImages, orientImages, prepareImageRequest });
104
117
  const id = port.describe().identity.backendId;
105
118
  candidates.push({ port, id, via: 'selfstack', backendId: provider.id, model: parsed?.modelId || '' });
106
119
  }
@@ -148,10 +161,10 @@ function projectError(record, candidates) {
148
161
  }
149
162
 
150
163
  /** prepareImage 的信号覆盖文件/会话读取;函数开始时即建立整体预算。 */
151
- export async function runVisionOperation(host, { providers, model, signal, budget = VISION_BUDGET, onRateLimit, prepareImages = false,
164
+ export async function runVisionOperation(host, { providers, model, signal, budget = VISION_BUDGET, onRateLimit, onModelAvailability, prepareImages = false,
152
165
  orientImages = false, prepareImageRequest }, consume) {
153
166
  if (signal?.aborted) throw new ModelPortError('IRIS_MODEL_ABORTED');
154
- const candidates = buildVisionModelCandidates(host, { providers, model, onRateLimit, prepareImages, orientImages, prepareImageRequest });
167
+ const candidates = buildVisionModelCandidates(host, { providers, model, onRateLimit, onModelAvailability, prepareImages, orientImages, prepareImageRequest });
155
168
  const operation = createModelOperation({ signal, budget: { timeoutMs: budget.timeoutMs, maxInvocations: candidates.length } });
156
169
  try {
157
170
  const { completion: result, ...details } = await consume(candidates.map(candidate => candidate.port), operation);
@@ -168,8 +181,8 @@ export async function runVisionOperation(host, { providers, model, signal, budge
168
181
  }
169
182
 
170
183
  /** 单图与复合业务共用同一候选、整体预算和结果投影。 */
171
- export async function runVisionRequest(host, { providers, model, question, image, dataUrl, prepareImage, signal, budget = VISION_BUDGET, onRateLimit }) {
172
- return runVisionOperation(host, { providers, model, signal, budget, onRateLimit, prepareImages: true }, async (ports, operation) => {
184
+ export async function runVisionRequest(host, { providers, model, question, image, dataUrl, prepareImage, signal, budget = VISION_BUDGET, onRateLimit, onModelAvailability }) {
185
+ return runVisionOperation(host, { providers, model, signal, budget, onRateLimit, onModelAvailability, prepareImages: true }, async (ports, operation) => {
173
186
  normalizeModelCall(ports[0].describe(), { prompt: question,
174
187
  image: { bytes: new Uint8Array([0]), mediaType: 'image/png' } }, { budget });
175
188
  const resolved = await waitForModelWork(() => prepareImage ? prepareImage(operation.signal)
@@ -179,9 +192,9 @@ export async function runVisionRequest(host, { providers, model, question, image
179
192
  }
180
193
 
181
194
  /** 付费确认在动作入口;红图实测只有一个供应商,不退到 Host 冒充其能力。 */
182
- export async function probeVisionModel(provider, model, image, { signal, timeoutMs = 15000 } = {}) {
195
+ export async function probeVisionModel(provider, model, image, { signal, timeoutMs = 15000, onModelAvailability } = {}) {
183
196
  try {
184
- const port = createConfiguredVisionModelPort(provider, model, { allowRateLimited: true });
197
+ const port = createConfiguredVisionModelPort(provider, model, { allowRateLimited: true, onModelAvailability });
185
198
  const result = await completeVision([port], { prompt: '这张图片是什么颜色?请只回答颜色名称(中文)。', image },
186
199
  { signal, budget: { ...VISION_BUDGET, timeoutMs } });
187
200
  return { ok: /红|red/i.test(result.text), answer: result.text };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mokuyoaxis/dsh-iris",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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 || 0.2.1-alpha.4"
37
+ "dsh": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4"
38
38
  },
39
39
  "bundle": {
40
40
  "patch": "./cordis.patch.yml"
@@ -66,7 +66,7 @@
66
66
  "license": "MIT",
67
67
  "engines": {
68
68
  "node": ">=22.0.0",
69
- "dsh": "0.2.0-rc.2 || 0.2.1-alpha.4"
69
+ "dsh": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4"
70
70
  },
71
71
  "scripts": {
72
72
  "test": "node scripts/test.mjs",
@@ -74,11 +74,11 @@
74
74
  "prepublishOnly": "npm test"
75
75
  },
76
76
  "peerDependencies": {
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"
77
+ "@deepseek-ai/dsh-client-locale": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4",
78
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4",
79
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4",
80
+ "@deepseek-ai/dsh-client-ui-session": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4",
81
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4"
82
82
  },
83
83
  "peerDependenciesMeta": {
84
84
  "@deepseek-ai/dsh-client-locale": {
@@ -98,7 +98,7 @@
98
98
  }
99
99
  },
100
100
  "dependencies": {
101
- "sharp": "^0.35.4"
101
+ "sharp": "^0.35.5"
102
102
  },
103
103
  "publishConfig": {
104
104
  "access": "public"
package/user_guide.md CHANGED
@@ -1,40 +1,37 @@
1
1
  # dsh-iris 用户指南
2
2
 
3
- 本指南对应Iris 0.2.4。发布状态见 [README](README.md),升级范围见 [CHANGELOG](CHANGELOG.md) 和 [发布说明](docs/releases/0.2.4.md)。
3
+ 本指南对应 Iris 0.2.6。安装要求见 [README](README.md),升级范围见 [CHANGELOG](CHANGELOG.md) 和 [发布说明](docs/releases/0.2.6.md)。
4
4
 
5
- 功能来自明确标签、旧能力字段绑定或供应商能力元数据,账号和名称不会自动赋能。0.2.4还要求把模型明确加入对应功能名单;空名单停止新调用。已有配置保留,详见 [升级说明](docs/FUNCTION_MODEL_ASSIGNMENTS.md)。
5
+ 功能来自明确标签、旧能力字段绑定或供应商能力元数据,账号和名称不会自动赋能。0.2.4/0.2.5 要求明确功能名单;0.2.6 保留旧名单和空配置行为,也可主动启用自动池。详见 [升级说明](docs/FUNCTION_MODEL_ASSIGNMENTS.md)。
6
+
7
+ 可 [引用 DSH 连接](docs/DSH_PROVIDER_CONNECTIONS.md),并在确认模型用途时启用 [功能自动池](docs/FUNCTION_MODEL_POOLS.md),无需二次分配;已有名单与空配置不会自动改变。
8
+
9
+ 供应商详情提供默认折叠的“模型目录”,可同步目录并查看新增、未列出及失败保留状态;自动请求按需刷新并缓存 10 分钟。当前连接明确返回模型不存在的条目会被隔离,新发现项仍须确认用途。目录同步不发起模型实测或生成。
6
10
 
7
11
  ## 安装
8
12
 
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。
13
+ 先运行`dsh --version`。Iris 0.2.6 沿用 0.2.5 的 12 个准确 DSH 版本,详见 [兼容矩阵](docs/DSH_COMPATIBILITY.md)。还需要 PATH 中的 pnpm 及至少一个所需功能的供应商。当前 Iris 最低 Node.js 为 22.0.0,即使保留旧 DSH 也需满足;宿主要求更高时以宿主为准。
10
14
 
11
- 在上述旧 DSH 范围内,从 npm 安装固定稳定版到 Web profile:
15
+ 在矩阵所列宿主中,将固定版本加入Web profile:
12
16
 
13
17
  ```bash
14
- dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.1.4
18
+ dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.6
15
19
  dsh web
16
20
  ```
17
21
 
18
22
  npm 上无 scope 的 `dsh-iris` 是另一款插件;安装或更新 Iris Media 时必须使用完整 scoped 包名。
19
23
 
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
-
22
- 在DSH rc.2或alpha.4上,固定安装0.2.4:
23
-
24
- ```bash
25
- dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.4
26
- dsh web
27
- ```
24
+ 只下载包可运行`npm pack @mokuyoaxis/dsh-iris@0.2.6`;普通`npm install`不会自动加入DSH profile。仍需保留历史Iris0.1.4时可以固定其版本,历史DSH/Node要求见兼容矩阵;0.1.4不包含当前Core媒体CLI与后续新功能。
28
25
 
29
26
  浏览器默认打开 `http://127.0.0.1:3080`。插件安装后只会在下一次 DSH 启动时装载;如果 DSH 已经运行,请先停止再启动。启动输出包含一次性认证参数时,必须完整打开该 URL,不能只输入裸地址。
30
27
 
31
28
  ### 独立 CLI
32
29
 
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)。
30
+ 无需DSH,在自己的项目安装 `npm install @mokuyoaxis/dsh-iris@0.2.6`,通过 `npx dsh-iris --help` 查看入口。发布状态见README,完整说明见 [Headless CLI](docs/HEADLESS_CLI.md) 和 [视觉CLI](docs/VISION_CLI.md)。
34
31
 
35
- 其他 DSH 版本,包括 `0.2.0` 正式版与其他 rc,须另行验收。让 Agent 协助安装时,可使用[简要 Prompt](docs/AGENT_QUICKSTART.md)。
32
+ 未列入对应Iris版本兼容矩阵的DSH版本须另行验收。让Agent协助安装时,可使用 [简要Prompt](docs/AGENT_QUICKSTART.md)。
36
33
 
37
- 通过 dsh-market 安装时也要核对显示的 Iris 版本、完整 scoped 包名和 DSH 支持范围。市场可能缓存 npm 信息;旧宿主仍按固定 `@0.1.4` 命令安装,不直接跟随 latest。
34
+ 通过dsh-market安装时也要核对显示的Iris版本、完整scoped包名和对应发行版的DSH支持范围。市场可能缓存npm信息;0.2.4及更早发行版仍按各自历史范围安装。
38
35
 
39
36
  可以在启动前检查组合配置:
40
37
 
@@ -58,10 +55,12 @@ dsh --profile web --dump-config
58
55
  2. 名称可填“阿里云百炼”。
59
56
  3. Base URL 填 `https://dashscope.aliyuncs.com/compatible-mode/v1`;如果账号使用百炼业务空间专属域名,再把“媒体 Base URL”填为同地域的 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1`(地域按账号实际情况替换)。
60
57
  4. 填入与模型、Endpoint 同地域的 DashScope API Key,点击“保存”。
61
- 5. 展开刚添加的供应商,点击“发现模型”。
62
- 6. 若模型显示“未标注功能”,点击其“设置功能”,勾选已确认支持的功能并保存。
58
+ 5. 展开供应商的“模型目录”,点击“同步目录”;也可手动填写已核对的模型 ID。
59
+ 6. 点击“设置功能”,勾选已确认支持的用途。首次配置可同时勾选“同时启用所选功能的自动池”,点击“保存模型用途”。
60
+
61
+ DSH 已配好同一媒体服务时,可在第 1 步选择连接来源,明确协议后直接复用地址与密钥,无需重复填写。连接支持范围见 [连接复用](docs/DSH_PROVIDER_CONNECTIONS.md)。
63
62
 
64
- 0.2.4中,单个供应商也需要在“功能分配(调用顺序)”为所需功能选择模型;未分配时不调用模型池。0.2.3仅Music采用严格名单,升级影响见 [功能模型分配](docs/FUNCTION_MODEL_ASSIGNMENTS.md)。
63
+ 选择自动池后,确认过用途的合格模型按登记顺序参与调用;保留“仅用所选模型”时,还需在“功能池与调用方式”选择模型。旧空名单不会自动变成自动池,升级差异见 [功能模型池](docs/FUNCTION_MODEL_POOLS.md)。
65
64
 
66
65
  保存供应商和修改能力分配都会立即生效,不需要重启 DSH。
67
66
 
@@ -103,7 +102,7 @@ Iris 当前支持两类调用路径:
103
102
 
104
103
  未提供标签的模型仍可在后续发现中接收供应商能力元数据;显式保存空标签表示当前不加入任何功能池,发现不会覆盖该选择。
105
104
 
106
- Iris 先按画图、视频、语音、转写和视觉功能组织候选池,再按该功能的能力分配顺序选模型;每次从首项开始,生成只有明确未受理才切换。标注功能不会发起实测、生成或改变分配顺序。
105
+ Iris 按六类功能的当前策略组织候选池:自动模式使用已确认用途的合格模型,手选模式按所选顺序,停用模式不发起新调用。每次从首项开始,媒体生成只有明确未受理才安全切换。确认用途不发起实测或生成;勾选同时启用自动池会明确改变所选功能策略。
107
106
 
108
107
  点击模型旁单项能力标记会进行验证。视觉、图片和 TTS 会先显示确认框,再发起真实供应商请求,不是本地校验:
109
108
 
@@ -135,16 +134,16 @@ Iris 先按画图、视频、语音、转写和视觉功能组织候选池,再
135
134
 
136
135
  每项能力都可以设置一个有序的“供应商 + 模型”列表。界面显示的是模型名和供应商,配置中保存为 `providerId::modelId` 复合引用,因此不同供应商的同名模型不会冲突。
137
136
 
138
- - 六类功能仅使用各自分配名单,每次按名单顺序选择,不补入其他模型。
139
- - 临时指定模型须已分配,本次只调用该项。
137
+ - 六类功能可选自动池、仅用所选模型或停用;手选不补池,自动池只纳入已确认用途的合格模型,每个请求从首项开始。
138
+ - 临时指定模型须在当前功能允许范围内,本次只调用该项;功能停用时不能绕过。
140
139
  - “清空分配”停止新调用,模型、任务与作品保留;已有任务按原绑定观察和补交付。
141
- - 视觉可明确加入“DSH 当前默认模型”并排序;未加入时不使用宿主视觉。
140
+ - 视觉可将“DSH 当前默认模型”加入手选名单,或在自动池中明确启用宿主视觉;默认不加入。
142
141
 
143
142
  生成类故障转移只覆盖上传、提交和同步生成阶段。远端一旦受理异步任务,Iris 就会继续跟踪这个任务,不会因轮询或下载失败自动重提,以免重复生成和计费。
144
143
 
145
144
  ## 音乐生成
146
145
 
147
- 为音乐服务添加实际模型,设置“音乐”标签,选择 **Chat Completions(内嵌音频)** 协议,再将模型加入音乐能力分配名单。音乐必须明确分配,不从其他模型池自动补齐;这个接口要求服务在聊天响应正文中返回MP3/WAV音频data URL。
146
+ 为音乐服务添加实际模型,确认“音乐”用途,选择 **Chat Completions(内嵌音频)** 协议,再启用音乐自动池或加入手选名单。这个接口要求服务在聊天响应正文中返回 MP3/WAV 音频 data URL;音乐与 TTS 的用途及协议分别确认。
148
147
 
149
148
  配置后可在DSH中说“用Iris生成一首复古摇滚配乐”,或使用工作台音乐卡片。生成结果可试听、下载与导出;音乐和TTS分别配置,配乐按需加入多步骤流程。
150
149
 
@@ -178,7 +177,7 @@ Core 图片卡可直接点击“看图”或“OCR”;结果可以复制、下
178
177
 
179
178
  ### 按 Artifact ID 改图
180
179
 
181
- Core 静态 PNG/JPEG/WebP 图片卡可点击“改图”,填写修改指令,再选择已分配的带图编辑模型。点击“生成修改版”才调用模型,修改版保存为关联原图的新作品,原图保留;可继续看图、OCR、下载或编辑。
180
+ Core 静态 PNG/JPEG/WebP 图片卡可点击“改图”,填写修改指令,再选择当前图片池允许的带图编辑模型。点击“生成修改版”才调用模型,修改版保存为关联原图的新作品,原图保留;可继续看图、OCR、下载或编辑。
182
181
 
183
182
  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、多参考图或连续编辑历史。
184
183
 
@@ -219,7 +218,7 @@ $DSH_HOME/iris/v1/providers.json
219
218
  }
220
219
  ```
221
220
 
222
- `mediaBaseUrl` 可以省略或留空,此时媒体调用沿用 `baseUrl`。`assignments` 可以省略;省略后使用模型池顺序。包含特殊字符的 provider 或模型引用会经过 URL 编码,复杂引用建议在工作台中生成,不要手写。
221
+ `mediaBaseUrl` 可以省略或留空,此时媒体调用沿用 `baseUrl`。`assignments` 可以省略,但从 0.2.4 起缺失分配不发起对应功能的新调用;0.2.6 也不会将其静默改为自动池。包含特殊字符的 provider 或模型引用会经过 URL 编码,复杂引用建议在工作台中生成,不要手写。
223
222
 
224
223
  可为明确不需要认证的本地端点设置 `"auth": "none"` 并省略 `apiKey`;缺省或 `"auth": "bearer"` 仍要求 Key,保持旧配置行为。无认证模式不会发送 Authorization 头,已有的旧 Key 也不会被带入该模式的媒体请求。工作台尚无专门的无认证开关,需要按上述停机原则编辑私有配置;不要对需要认证的公网端点使用 `auth:none`。
225
224
 
@@ -387,11 +386,11 @@ npx @mokuyoaxis/dsh-iris doctor --json
387
386
 
388
387
  已安装包也可以直接运行 `dsh-iris doctor`。退出码为 `0`(正常)、`1`(警告)和 `2`(硬错误)。Doctor 会执行一个随后立即删除的本地写入探针,并检查配置、任务、能力分配、临时文件和产物引用;输出不含完整 API Key。离线模式无法检查 DSH 工具/客户端是否已经装载,也不会用付费请求验证 Provider。
389
388
 
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 命令。
389
+ **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.6 dsh-iris ...`。旧版 0.1.4 不包含这些 Core 命令。
391
390
 
392
391
  ## Host Doctor
393
392
 
394
- DSH运行后打开“设置 → Iris工作台 → 宿主诊断”。卡片检查宿主/服务端/客户端版本、当前工具和随包Skill、四组路由与四个UI Slot。0.2.4包含17个工具和三项Skill;升级后检查服务端实际装载、刷新页面,并为所需功能分配模型。点击“刷新”查看结果,原始JSON可在已认证的同一浏览器访问 `/iris/api/doctor`。
393
+ DSH 运行后打开“设置 → Iris 工作台 → 宿主诊断”。卡片检查宿主/服务端/客户端版本、当前工具和随包 Skill、四组路由与四个 UI Slot。0.2.6 保留 17 个工具和三项 Skill;升级后检查服务端实际装载、刷新页面,并确认功能策略。点击“刷新”查看结果,原始 JSON 可在同一浏览器访问 `/iris/api/doctor`;该路由使用 Iris 的 Host/来源守卫,不提供独立身份认证。
395
394
 
396
395
  0.2.4新增图片导入后检查17个工具,三项Skill保持;0.2.3为16个工具。
397
396
 
@@ -433,7 +432,7 @@ dsh plugin --profile web remove @mokuyoaxis/dsh-iris
433
432
  - **没有可用模型**:先确认供应商已启用且 Key 有效,再运行“发现模型”;未标注模型需通过“设置功能”选择用途,不支持 `/models` 时手动添加并标注。
434
433
  - **OpenAI 兼容图片请求走错协议**:核对模型的有效图片协议;`/images/generations` 使用 `openai-images`,网关聊天图片扩展使用 `openai-chat-images`,同步 `/responses` 生图使用 `openai-responses-images`。可在生图模型行或 CLI `models protocol` 设置模型级覆盖,不需要改变同账号其他模型的接口。
435
434
  - **反向代理访问返回 `untrusted host`**:把浏览器实际使用的域名或 `host:port` 加入 `IRIS_TRUSTED_HOSTS`,保留 Host,并确认代理层已经启用认证。
436
- - **模型池为空时使用宿主看图**:0.2.4需在视觉分配中明确加入“DSH 当前默认模型”;宿主默认模型须声明图片输入。也可配置并分配自持 `vision` 模型。
435
+ - **模型池为空时使用宿主看图**:在视觉手选名单中加入“DSH 当前默认模型”,或在自动池中明确启用宿主视觉;宿主模型须声明图片输入。也可确认自持 `vision` 模型用途,再启用自动池或加入手选名单。
437
436
  - **视频摘要提示 ffmpeg 不可用**:安装系统级 `ffmpeg` 和 `ffprobe`,并确保 DSH 进程的 PATH 能找到它们。
438
437
  - **本地文件不存在**:这里的“本地”指 DSH 宿主,不一定是浏览器所在设备;优先用“上传文件”。
439
438
  - **视频任务长时间运行**:不要重复提交。用 `iris_task_status` 或工作台任务详情查询原任务。