@mokuyoaxis/dsh-iris 0.2.2 → 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 (79) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +23 -73
  2. package/.dsh/skills/iris-compose-media/agents/openai.yaml +1 -1
  3. package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +23 -22
  4. package/.dsh/skills/iris-compose-media/references/workflows.md +25 -30
  5. package/.dsh/skills/iris-use/SKILL.md +42 -0
  6. package/.dsh/skills/iris-use/agents/openai.yaml +4 -0
  7. package/.dsh/skills/iris-use/references/media-contract.md +83 -0
  8. package/.dsh/skills/iris-verify-ui/SKILL.md +9 -7
  9. package/.dsh/skills/iris-verify-ui/agents/openai.yaml +1 -1
  10. package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +3 -0
  11. package/CHANGELOG.md +41 -2
  12. package/README.en.md +50 -38
  13. package/README.md +48 -37
  14. package/bin/dsh-iris.js +35 -6
  15. package/docs/AGENT_QUICKSTART.md +9 -9
  16. package/docs/ARCHITECTURE.md +4 -2
  17. package/docs/CLI_MANAGEMENT.md +11 -3
  18. package/docs/CORE_RUNTIME_CONTRACT.md +3 -1
  19. package/docs/DSH_COMPATIBILITY.md +43 -0
  20. package/docs/DSH_CORE_MIGRATION.md +6 -2
  21. package/docs/DSH_RC2_ADAPTATION.md +4 -2
  22. package/docs/FAULT_INJECTION.md +1 -0
  23. package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +63 -0
  24. package/docs/HEADLESS_CLI.md +14 -8
  25. package/docs/HOST_ADAPTER_CONTRACT.md +10 -7
  26. package/docs/HOST_DOCTOR.md +6 -4
  27. package/docs/IMAGE_IMPORT.md +49 -0
  28. package/docs/MODEL_PORT_CONTRACT.md +6 -2
  29. package/docs/MUSIC.md +77 -0
  30. package/docs/PROMPT_OPTIMIZER.md +48 -10
  31. package/docs/PROVIDER_ADAPTER_CONTRACT.md +2 -0
  32. package/docs/ROADMAP.md +16 -25
  33. package/docs/SECURITY.md +3 -2
  34. package/docs/SYNC_DELIVERY_RECOVERY.md +39 -0
  35. package/docs/TASK_SEMANTICS.md +4 -4
  36. package/docs/VISION_CLI.md +3 -3
  37. package/docs/VISION_MODEL.md +1 -1
  38. package/docs/WORKBENCH_ARTIFACTS.md +4 -2
  39. package/docs/releases/0.2.3.md +54 -0
  40. package/docs/releases/0.2.4.md +27 -0
  41. package/lib/actions.js +71 -14
  42. package/lib/adapters.js +6 -2
  43. package/lib/api.js +14 -5
  44. package/lib/bundled-skills.js +1 -1
  45. package/lib/capability.js +2 -0
  46. package/lib/client.js +131 -47
  47. package/lib/command-service.js +28 -4
  48. package/lib/config.js +41 -23
  49. package/lib/core-artifact-store.js +44 -6
  50. package/lib/core-delivery-cache.js +145 -0
  51. package/lib/core-maintenance.js +12 -1
  52. package/lib/core-tasks.js +19 -5
  53. package/lib/core-user-projection.js +2 -2
  54. package/lib/doctor.js +4 -10
  55. package/lib/dsh-compatibility.js +10 -0
  56. package/lib/dsh-core-adapter.js +20 -4
  57. package/lib/dsh-text-model-adapter.js +15 -2
  58. package/lib/generation-input.js +10 -0
  59. package/lib/host-runtime.js +5 -2
  60. package/lib/image-edit-input.js +7 -5
  61. package/lib/image-import.js +61 -0
  62. package/lib/index.js +60 -17
  63. package/lib/models.js +12 -3
  64. package/lib/music-generation.js +21 -0
  65. package/lib/music-options.js +18 -0
  66. package/lib/music-transport.js +46 -0
  67. package/lib/ocr-model-routing.js +1 -0
  68. package/lib/prompt-optimizer-config.js +92 -21
  69. package/lib/prompt-optimizer-core.js +51 -6
  70. package/lib/prompt-optimizer.js +20 -14
  71. package/lib/provider-adapter.js +1 -0
  72. package/lib/provider-adapters.js +35 -3
  73. package/lib/provider-catalog.js +20 -11
  74. package/lib/provider-config-service.js +18 -6
  75. package/lib/provider-protocol.js +18 -0
  76. package/lib/provider-task-runner.js +80 -5
  77. package/lib/vision-model-routing.js +22 -8
  78. package/package.json +12 -9
  79. package/user_guide.md +45 -23
@@ -0,0 +1,27 @@
1
+ # Iris 0.2.4:图片续作、统一分配与新版宿主兼容
2
+
3
+ 0.2.4让已有图片可以直接进入作品库并继续编辑,统一六类功能的模型选择规则,同时支持DSH rc.2与alpha.4。
4
+
5
+ ## 升级前检查功能分配
6
+
7
+ 图片、视频、TTS、Music、转写和视觉只使用各自明确分配的有序名单。名单为空时停止新调用,临时指定的模型也必须已分配,且只调用该项。以前依赖自动补入模型池的功能,需要在升级后明确分配模型。
8
+
9
+ 供应商、模型池和已有名单保留,不自动迁移个人配置。旧任务仍绑定原模型查询和补交付;手动模型实测独立于生产名单。DSH默认视觉改为需要明确加入名单的“DSH 当前默认模型”,独立CLI不能调用该宿主资源。见 [完整升级说明](../FUNCTION_MODEL_ASSIGNMENTS.md)。
10
+
11
+ ## 图片导入与原生Qwen改图
12
+
13
+ 工作台“导入图片”、`iris_import_image` 和CLI `artifact import` 保存静态PNG/JPEG/WebP图片,最多20 MiB,返回持久作品ID。保留原字节和源文件,不调用模型;已有Core作品无需重复导入。见 [图片导入](../IMAGE_IMPORT.md)。
14
+
15
+ `iris_edit_image`、CLI和工作台改图支持DashScope原生Qwen edit/max/plus、2.0/pro系列及日期快照,继续支持聊天生图协议。模型还须具备image-gen能力并加入生图名单,具体账号与地域是否可用由服务端决定。`qwen-image-2.0-pro` 已通过真实CLI带图生成、取回与保存来源关系的实测。
16
+
17
+ 原图只读,发送副本按账号/模型预算处理;修改版保存为独立作品并关联来源。原生Qwen输入副本另受10 MiB上限约束。编辑精度、输出尺寸由模型决定,不提供蒙版、多参考图或像素级保真保证。见 [Artifact改图](../CLI_MANAGEMENT.md#artifact-聊天改图)。
18
+
19
+ ## DSH双版本兼容
20
+
21
+ 支持准确版本 `0.2.0-rc.2` 和 `0.2.1-alpha.4`,manifest、可选客户端peer及Doctor同步。不要求已有rc.2用户升级宿主,也不据此支持整个0.2.x。Iris最低Node.js为22.0.0;DSH若要求更高,以宿主为准。见 [兼容矩阵与验证范围](../DSH_COMPATIBILITY.md)。
22
+
23
+ ## 使用与验证范围
24
+
25
+ Agent工具增至17项,三项Skills的共享说明包含图片导入、严格分配和Qwen改图。Music仍是可选功能,本版未更换音乐来源或增加新的音乐协议。
26
+
27
+ 宿主隔离验收覆盖实际官方SDK服务、工具/附件交付、HTTP与界面插槽;React/DOM和部分会话数据使用fixture,完整浏览器/移动端交互与热更新仍需实际使用验证。未增加Core数据格式迁移或生产依赖。
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, 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,8 +31,8 @@ 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';
35
- import { providerTaskBinding } from './provider-catalog.js';
34
+ import { selectMediaProtocol, selectImageProtocol, providerForImageModel, supportsImageEdit } from './provider-protocol.js';
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';
38
38
  import { deriveUserState } from './task-semantics.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');
@@ -564,6 +578,33 @@ async function submitCoreTts(routes, normalizedInput, signal) {
564
578
  }
565
579
 
566
580
  /* ---------- 🔊 语音合成 ---------- */
581
+ register('music', async (host, args, { signal }) => {
582
+ const normalized = normalizeGenerationInput('music', {
583
+ prompt: args.prompt,
584
+ ...(optionalActionText(args.timeout_ms) !== undefined ? { timeout_ms: args.timeout_ms } : {}),
585
+ ...(optionalActionText(args.retry_502) !== undefined ? { retry_502: args.retry_502 } : {}),
586
+ ...(optionalActionText(args.retry_delay_ms) !== undefined ? { retry_delay_ms: args.retry_delay_ms } : {})
587
+ });
588
+ const routes = musicCandidatesFromCatalog({ providers: store.allProviders(), assignments: store.assignments() }, args.model);
589
+ const submitted = await submitMusicForDsh({
590
+ candidates: routes.map(route => ({ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
591
+ selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
592
+ providerInput: normalized.providerInput, musicOptions: normalized.musicOptions, signal
593
+ });
594
+ const task = submitted.task;
595
+ recordCoreTaskAttemptHealth(task, cap.CAPABILITIES.MUSIC);
596
+ if (submitted.localError) throw taskActionError(task.id, 'Music 结果未能可靠落盘', submitted.localError);
597
+ if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready') {
598
+ throw taskActionError(task.id, task.outcome === 'succeeded'
599
+ ? '音乐已返回,但交付失败;可从缓存重新交付'
600
+ : `音乐未取得产物(已重试${submitted.retries}次)`, task.lastError);
601
+ }
602
+ return { ok: true, storage: 'core', taskId: task.id, taskIds: submitted.taskIds, retries: submitted.retries,
603
+ model: task.modelRef, providerId: task.providerId, artifactIds: task.artifactIds,
604
+ audioUrl: '/iris/api/core/artifact/' + encodeURIComponent(task.artifactIds[0]) + '/media',
605
+ text: `[iris] 音乐已生成(${task.modelRef},重试${submitted.retries}次,Core task: ${task.id})` };
606
+ });
607
+
567
608
  register('tts', async (host, args, { signal }) => {
568
609
  const { providerInput } = normalizeGenerationInput('tts', {
569
610
  text: args.text, voice: optionalActionText(args.voice)
@@ -1008,9 +1049,10 @@ register('providers_list', async (host, args) => {
1008
1049
  id: m.id,
1009
1050
  capabilities: m.capabilities,
1010
1051
  ...(m.imageProtocol !== undefined ? { imageProtocol: m.imageProtocol } : {}),
1052
+ ...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
1011
1053
  ...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}),
1012
1054
  ...(m.capabilities.includes(cap.CAPABILITIES.VISION) ? { visionInputEffective: visionInputLimits(p, m.id) } : {}),
1013
- ...(m.capabilities.includes(cap.CAPABILITIES.IMAGE) ? { imageRouting: selectImageProtocol(p, m.id) } : {}),
1055
+ ...(m.capabilities.includes(cap.CAPABILITIES.IMAGE) ? { imageRouting: imageRoutingForModel(p, m.id) } : {}),
1014
1056
  verified: m.verified || null,
1015
1057
  health: Object.fromEntries(m.capabilities.map((capability) => [
1016
1058
  capability, store.modelHealth(p.id, m.id, capability)
@@ -1075,11 +1117,11 @@ register('providers_discover', async (host, args, { signal }) => {
1075
1117
  register('providers_test_vision', async (host, args, { signal }) => {
1076
1118
  const p = store.providerById(String(args.id || ''));
1077
1119
  if (!p) throw new Error('iris: 供应商不存在');
1078
- const visionProviders = store.pickAllFor(cap.CAPABILITIES.VISION, { includeRateLimited: true }).filter((x) => x.id === p.id);
1079
- if (!visionProviders.length) return { ok: true, tested: false, text: '该供应商未配置 vision 模型,跳过' };
1080
- 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;
1081
1123
  if (args.confirm_paid !== true) throw new Error('iris: 视觉实测会发起真实供应商请求,请先明确确认');
1082
- 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 });
1083
1125
  store.setModelVerified(p.id, model, cap.CAPABILITIES.VISION, {
1084
1126
  ok: r.ok,
1085
1127
  note: r.ok ? '认出红色测试图' : (r.error || '未识别出红色'),
@@ -1100,6 +1142,9 @@ register('providers_test_vision', async (host, args, { signal }) => {
1100
1142
 
1101
1143
  /** 按能力实测单个模型;返回 {ok, note}。真实调用仅在 UI 明确确认后执行。 */
1102
1144
  async function probeModel(host, p, modelId, capability, signal) {
1145
+ if (capability === cap.CAPABILITIES.MUSIC) {
1146
+ return { skipped: true, ok: false, note: 'Music 不发起小样本探测;请通过音乐功能提交实际创作要求' };
1147
+ }
1103
1148
  if (capability === cap.CAPABILITIES.VISION) {
1104
1149
  const r = await probeVisionModel(p, modelId, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal });
1105
1150
  return {
@@ -1207,6 +1252,12 @@ register('providers_set_model_image_protocol', async (host, args) => {
1207
1252
  return { ok: true, text: '[iris] 模型图片协议已更新' };
1208
1253
  });
1209
1254
 
1255
+ register('providers_set_model_music_protocol', async (host, args) => {
1256
+ const ok = store.setModelMusicProtocol(String(args.id || ''), String(args.model_id || ''), args.musicProtocol);
1257
+ if (!ok) throw new Error('iris: 模型不在池中');
1258
+ return { ok: true, text: '[iris] 模型音乐协议已更新' };
1259
+ });
1260
+
1210
1261
  register('providers_set_model_vision_input', async (host, args) => {
1211
1262
  if (!Object.hasOwn(args, 'visionInput')) throw new Error('iris: 需要 visionInput 对象或 null');
1212
1263
  const ok = store.setModelVisionInput(String(args.id || ''), String(args.model_id || ''), args.visionInput);
@@ -1221,7 +1272,7 @@ register('providers_test_model', async (host, args, { signal }) => {
1221
1272
  const modelId = String(args.model_id || '').trim();
1222
1273
  const capability = String(args.capability || '').trim();
1223
1274
  if (!modelId || !cap.isKnownCapability(capability)) throw new Error('iris: 需 model_id 与合法 capability');
1224
- if (![cap.CAPABILITIES.VIDEO, cap.CAPABILITIES.TRANSCRIBE].includes(capability) && args.confirm_paid !== true) {
1275
+ if (![cap.CAPABILITIES.VIDEO, cap.CAPABILITIES.TRANSCRIBE, cap.CAPABILITIES.MUSIC].includes(capability) && args.confirm_paid !== true) {
1225
1276
  throw new Error('iris: 模型实测会发起真实供应商请求,请先明确确认');
1226
1277
  }
1227
1278
  let result;
@@ -1250,17 +1301,23 @@ register('providers_test_model', async (host, args, { signal }) => {
1250
1301
 
1251
1302
  /** 查询:全部分配 + 每个能力可用的模型池(client 渲染下拉/排序) */
1252
1303
  register('assignments_get', async (host, args) => {
1253
- 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 });
1254
1307
  const byCap = {};
1255
1308
  for (const m of pool) {
1256
1309
  for (const c of m.capabilities) {
1257
1310
  if (!byCap[c]) byCap[c] = [];
1258
- 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) } : {}) });
1259
1316
  }
1260
1317
  }
1261
1318
  // order:归一化的有序分配列表(GUI 直接渲染 failover 顺序,无需处理单字符串旧格式)
1262
1319
  const order = {};
1263
- 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);
1264
1321
  return { ok: true, assignments: store.assignments(), order, poolByCapability: byCap };
1265
1322
  });
1266
1323
 
package/lib/adapters.js CHANGED
@@ -15,6 +15,7 @@ import { dashscopeApiBase, imageSizeViolation, isDashScopeBaseUrl, normalizeImag
15
15
  import { chmodPrivateFile, ensurePrivateDir, privateSibling } from './private-storage.js';
16
16
  import { isFreeQuotaExhausted, retryAfterDelay } from './provider-health.js';
17
17
  import { ProviderContractError } from './provider-contract.js';
18
+ export { generateMusic } from './music-transport.js';
18
19
 
19
20
  /**
20
21
  * 网络调用超时档位(2026-09-03 健康检查):挂死的连接绝不能冻住盯守 tick——
@@ -188,7 +189,7 @@ export function applyImageSize({ protocol, mode, size }) {
188
189
  }
189
190
 
190
191
  /** 新版多模态同步协议:响应中直接返回图片 URL。 */
191
- 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 }) {
192
193
  const apiBase = dashscopeApiBase(baseUrl);
193
194
  const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'multimodal-sync', size });
194
195
  const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/multimodal-generation/generation`, {
@@ -197,7 +198,10 @@ export async function generateImageMultimodal({ key, baseUrl, model, prompt, siz
197
198
  signal: timeoutSignal(timeoutMs, signal),
198
199
  body: JSON.stringify({
199
200
  model,
200
- 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
+ ] }] },
201
205
  parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 }
202
206
  })
203
207
  });
package/lib/api.js CHANGED
@@ -33,9 +33,12 @@ import {
33
33
  importPromptOptimizerConfig,
34
34
  loadPromptOptimizerConfig,
35
35
  resetPromptOptimizerConfig,
36
+ resetPromptOptimizerInstructions,
36
37
  setPromptOptimizerEnabled
37
38
  } from './prompt-optimizer-config.js';
38
39
 
40
+ const PROMPT_CONFIG_CAPABILITIES = Object.freeze({ ...PROMPT_OPTIMIZER_CAPABILITIES, resetInstructions: true });
41
+
39
42
  function readJsonSafe(file) {
40
43
  try {
41
44
  const raw = fs.readFileSync(file, 'utf8');
@@ -369,7 +372,7 @@ export function serveApi(req, res, host) {
369
372
  return serveSse(res);
370
373
  }
371
374
  if (urlPath === '/iris/api/prompt-optimizer/config') {
372
- return sendJson(res, 200, method === 'HEAD' ? {} : { ...loadPromptOptimizerConfig(), capabilities: PROMPT_OPTIMIZER_CAPABILITIES });
375
+ return sendJson(res, 200, method === 'HEAD' ? {} : { ...loadPromptOptimizerConfig(), capabilities: PROMPT_CONFIG_CAPABILITIES });
373
376
  }
374
377
  if (urlPath === '/iris/api/doctor') {
375
378
  if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
@@ -506,7 +509,9 @@ function handleJsonPost(req, res, execute, respond = sendJson) {
506
509
  if (overflow) return finish(413, { error: 'request body too large (limit 1MB)' });
507
510
  try {
508
511
  const raw = Buffer.concat(chunks).toString('utf8');
509
- const body = raw ? JSON.parse(raw) : {};
512
+ let body;
513
+ try { body = raw ? JSON.parse(raw) : {}; }
514
+ catch (_) { return finish(controller.signal.aborted ? 499 : 400, { error: controller.signal.aborted ? 'request canceled' : 'invalid JSON' }); }
510
515
  const result = await execute(body, controller.signal);
511
516
  if (controller.signal.aborted) return finish(499, { error: 'request canceled' });
512
517
  finish(200, result);
@@ -630,10 +635,14 @@ function handlePromptOptimizer(host, operation, req, res) {
630
635
  if (operation === 'optimize') return optimizePrompt(host, body, { signal });
631
636
  if (operation === 'import') {
632
637
  const imported = body && body.config !== undefined ? body.config : body;
633
- return { ok: true, ...importPromptOptimizerConfig(imported), capabilities: PROMPT_OPTIMIZER_CAPABILITIES };
638
+ return { ok: true, ...importPromptOptimizerConfig(imported), capabilities: PROMPT_CONFIG_CAPABILITIES };
639
+ }
640
+ if (operation === 'reset') {
641
+ const scope = body?.scope === undefined ? 'all' : body.scope;
642
+ if (!['all', 'instructions'].includes(scope)) throw new Error('scope 只能是 all 或 instructions');
643
+ return { ok: true, ...(scope === 'instructions' ? resetPromptOptimizerInstructions() : resetPromptOptimizerConfig()), capabilities: PROMPT_CONFIG_CAPABILITIES };
634
644
  }
635
- if (operation === 'reset') return { ok: true, ...resetPromptOptimizerConfig(), capabilities: PROMPT_OPTIMIZER_CAPABILITIES };
636
- if (operation === 'enabled') return { ok: true, ...setPromptOptimizerEnabled(body && body.enabled), capabilities: PROMPT_OPTIMIZER_CAPABILITIES };
645
+ if (operation === 'enabled') return { ok: true, ...setPromptOptimizerEnabled(body && body.enabled), capabilities: PROMPT_CONFIG_CAPABILITIES };
637
646
  throw new Error('unknown prompt optimizer operation');
638
647
  });
639
648
  }
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { requireHostPort } from './host-contract.js';
7
7
 
8
- const SKILL_NAMES = ['iris-verify-ui', 'iris-compose-media'];
8
+ const SKILL_NAMES = ['iris-use', 'iris-compose-media', 'iris-verify-ui'];
9
9
  const SKILL_ROOT = fileURLToPath(new URL('../.dsh/skills/', import.meta.url));
10
10
 
11
11
  function parseScalar(value, file) {
package/lib/capability.js CHANGED
@@ -14,6 +14,7 @@ export const CAPABILITIES = Object.freeze({
14
14
  IMAGE: 'image-gen',
15
15
  VIDEO: 'video-gen',
16
16
  TTS: 'tts',
17
+ MUSIC: 'music-gen',
17
18
  TRANSCRIBE: 'transcribe',
18
19
  VISION: 'vision'
19
20
  });
@@ -26,6 +27,7 @@ const INFER_FIELD = {
26
27
  [CAPABILITIES.IMAGE]: 'imageModel',
27
28
  [CAPABILITIES.VIDEO]: 'videoModel',
28
29
  [CAPABILITIES.TTS]: 'ttsModel',
30
+ [CAPABILITIES.MUSIC]: 'musicModel',
29
31
  [CAPABILITIES.TRANSCRIBE]: 'transcribeModel',
30
32
  [CAPABILITIES.VISION]: 'visionModel'
31
33
  };