@mokuyoaxis/dsh-iris 0.2.1 → 0.2.3
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.
- package/.dsh/skills/iris-compose-media/SKILL.md +23 -73
- package/.dsh/skills/iris-compose-media/agents/openai.yaml +1 -1
- package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +23 -22
- package/.dsh/skills/iris-compose-media/references/workflows.md +25 -30
- package/.dsh/skills/iris-use/SKILL.md +41 -0
- package/.dsh/skills/iris-use/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-use/references/media-contract.md +73 -0
- package/.dsh/skills/iris-verify-ui/SKILL.md +9 -7
- package/.dsh/skills/iris-verify-ui/agents/openai.yaml +1 -1
- package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +3 -0
- package/CHANGELOG.md +48 -2
- package/README.en.md +47 -40
- package/README.md +45 -39
- package/bin/dsh-iris.js +27 -6
- package/docs/AGENT_QUICKSTART.md +9 -9
- package/docs/ARCHITECTURE.md +11 -3
- package/docs/ARTIFACT_LIBRARY.md +1 -1
- package/docs/CLI_MANAGEMENT.md +5 -3
- package/docs/COMPOSITE_VISION.md +1 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +12 -2
- package/docs/DSH_CORE_MIGRATION.md +7 -3
- package/docs/FAULT_INJECTION.md +11 -1
- package/docs/HEADLESS_CLI.md +10 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +1 -1
- package/docs/HOST_DOCTOR.md +3 -3
- package/docs/MODEL_PORT_CONTRACT.md +9 -3
- package/docs/MUSIC.md +77 -0
- package/docs/OCR_MODEL.md +1 -1
- package/docs/PROMPT_OPTIMIZER.md +74 -6
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +12 -2
- package/docs/PROVIDER_HEALTH.md +1 -1
- package/docs/ROADMAP.md +15 -18
- package/docs/SECURITY.md +4 -3
- package/docs/SYNC_DELIVERY_RECOVERY.md +39 -0
- package/docs/TASK_SEMANTICS.md +5 -5
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +1 -1
- package/docs/WORKBENCH_ARTIFACTS.md +1 -1
- package/docs/releases/0.2.2.md +31 -0
- package/docs/releases/0.2.3.md +54 -0
- package/lib/actions.js +52 -19
- package/lib/adapters.js +89 -24
- package/lib/api.js +14 -5
- package/lib/bundled-skills.js +1 -1
- package/lib/capability.js +25 -16
- package/lib/client.js +374 -43
- package/lib/command-service.js +12 -3
- package/lib/config.js +39 -6
- package/lib/core-artifact-store.js +44 -6
- package/lib/core-delivery-cache.js +145 -0
- package/lib/core-lease-recovery.js +64 -92
- package/lib/core-maintenance.js +12 -1
- package/lib/core-runtime.js +45 -47
- package/lib/core-tasks.js +19 -5
- package/lib/core-user-projection.js +2 -2
- package/lib/core-writer-lease.js +134 -0
- package/lib/dsh-core-adapter.js +14 -4
- package/lib/dsh-text-model-adapter.js +15 -2
- package/lib/generation-input.js +10 -0
- package/lib/host-runtime.js +4 -2
- package/lib/index.js +23 -4
- package/lib/models.js +23 -38
- package/lib/music-generation.js +21 -0
- package/lib/music-options.js +18 -0
- package/lib/music-transport.js +46 -0
- package/lib/prompt-optimizer-config.js +111 -30
- package/lib/prompt-optimizer-core.js +183 -12
- package/lib/prompt-optimizer.js +20 -14
- package/lib/provider-adapter.js +1 -0
- package/lib/provider-adapters.js +26 -2
- package/lib/provider-catalog.js +18 -7
- package/lib/provider-config-service.js +16 -5
- package/lib/provider-protocol.js +73 -0
- package/lib/provider-task-runner.js +86 -9
- package/package.json +4 -2
- package/user_guide.md +52 -24
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, 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';
|
|
@@ -32,7 +32,7 @@ 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
34
|
import { selectMediaProtocol, selectImageProtocol, providerForImageModel } from './provider-protocol.js';
|
|
35
|
-
import { providerTaskBinding } from './provider-catalog.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';
|
|
@@ -136,7 +136,8 @@ function imageRoute(provider, args) {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function taskActionError(taskId, message, details) {
|
|
139
|
-
const
|
|
139
|
+
const diagnostic = details?.safeMessage ? ':' + details.safeMessage : '';
|
|
140
|
+
const error = new Error('iris: ' + message + '(task: ' + taskId + ')' + diagnostic);
|
|
140
141
|
error.taskId = taskId;
|
|
141
142
|
if (details) error.details = details;
|
|
142
143
|
return error;
|
|
@@ -563,6 +564,33 @@ async function submitCoreTts(routes, normalizedInput, signal) {
|
|
|
563
564
|
}
|
|
564
565
|
|
|
565
566
|
/* ---------- 🔊 语音合成 ---------- */
|
|
567
|
+
register('music', async (host, args, { signal }) => {
|
|
568
|
+
const normalized = normalizeGenerationInput('music', {
|
|
569
|
+
prompt: args.prompt,
|
|
570
|
+
...(optionalActionText(args.timeout_ms) !== undefined ? { timeout_ms: args.timeout_ms } : {}),
|
|
571
|
+
...(optionalActionText(args.retry_502) !== undefined ? { retry_502: args.retry_502 } : {}),
|
|
572
|
+
...(optionalActionText(args.retry_delay_ms) !== undefined ? { retry_delay_ms: args.retry_delay_ms } : {})
|
|
573
|
+
});
|
|
574
|
+
const routes = musicCandidatesFromCatalog({ providers: store.allProviders(), assignments: store.assignments() }, args.model);
|
|
575
|
+
const submitted = await submitMusicForDsh({
|
|
576
|
+
candidates: routes.map(route => ({ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
|
|
577
|
+
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
|
|
578
|
+
providerInput: normalized.providerInput, musicOptions: normalized.musicOptions, signal
|
|
579
|
+
});
|
|
580
|
+
const task = submitted.task;
|
|
581
|
+
recordCoreTaskAttemptHealth(task, cap.CAPABILITIES.MUSIC);
|
|
582
|
+
if (submitted.localError) throw taskActionError(task.id, 'Music 结果未能可靠落盘', submitted.localError);
|
|
583
|
+
if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready') {
|
|
584
|
+
throw taskActionError(task.id, task.outcome === 'succeeded'
|
|
585
|
+
? '音乐已返回,但交付失败;可从缓存重新交付'
|
|
586
|
+
: `音乐未取得产物(已重试${submitted.retries}次)`, task.lastError);
|
|
587
|
+
}
|
|
588
|
+
return { ok: true, storage: 'core', taskId: task.id, taskIds: submitted.taskIds, retries: submitted.retries,
|
|
589
|
+
model: task.modelRef, providerId: task.providerId, artifactIds: task.artifactIds,
|
|
590
|
+
audioUrl: '/iris/api/core/artifact/' + encodeURIComponent(task.artifactIds[0]) + '/media',
|
|
591
|
+
text: `[iris] 音乐已生成(${task.modelRef},重试${submitted.retries}次,Core task: ${task.id})` };
|
|
592
|
+
});
|
|
593
|
+
|
|
566
594
|
register('tts', async (host, args, { signal }) => {
|
|
567
595
|
const { providerInput } = normalizeGenerationInput('tts', {
|
|
568
596
|
text: args.text, voice: optionalActionText(args.voice)
|
|
@@ -1007,6 +1035,7 @@ register('providers_list', async (host, args) => {
|
|
|
1007
1035
|
id: m.id,
|
|
1008
1036
|
capabilities: m.capabilities,
|
|
1009
1037
|
...(m.imageProtocol !== undefined ? { imageProtocol: m.imageProtocol } : {}),
|
|
1038
|
+
...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
|
|
1010
1039
|
...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}),
|
|
1011
1040
|
...(m.capabilities.includes(cap.CAPABILITIES.VISION) ? { visionInputEffective: visionInputLimits(p, m.id) } : {}),
|
|
1012
1041
|
...(m.capabilities.includes(cap.CAPABILITIES.IMAGE) ? { imageRouting: selectImageProtocol(p, m.id) } : {}),
|
|
@@ -1049,7 +1078,7 @@ register('providers_set_models', async (host, args) => {
|
|
|
1049
1078
|
});
|
|
1050
1079
|
|
|
1051
1080
|
/**
|
|
1052
|
-
* 模型发现:读取供应商目录 →
|
|
1081
|
+
* 模型发现:读取供应商目录 → 明确能力元数据 → 合并模型池。
|
|
1053
1082
|
* 免费只读;拉不到/不支持则原样保留现有池(优雅降级),报人话错误。
|
|
1054
1083
|
*/
|
|
1055
1084
|
register('providers_discover', async (host, args, { signal }) => {
|
|
@@ -1063,16 +1092,11 @@ register('providers_discover', async (host, args, { signal }) => {
|
|
|
1063
1092
|
} catch (err) {
|
|
1064
1093
|
throw new Error('iris: 模型发现失败(保留现有池):' + String(err.message || err));
|
|
1065
1094
|
}
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (!media.length) {
|
|
1072
|
-
return { ok: true, total: discoveredModels.length, media: 0, text: `[iris] ${p.name}:拉到 ${discoveredModels.length} 个模型,无媒体能力匹配——可在模型池区手动添加并标能力` };
|
|
1073
|
-
}
|
|
1074
|
-
store.mergeDiscoveredModels(p.id, media);
|
|
1075
|
-
return { ok: true, total: discoveredModels.length, media: media.length, text: `[iris] ${p.name}:发现 ${discoveredModels.length} 个模型,${media.length} 个媒体能力模型已入池(官方元数据优先)` };
|
|
1095
|
+
const entries = discoveredModels.map((model) => ({ id: model.id, capabilities: models.capabilitiesOfDiscoveredModel(model) }));
|
|
1096
|
+
const mediaCount = entries.filter((model) => model.capabilities.length).length;
|
|
1097
|
+
store.mergeDiscoveredModels(p.id, entries);
|
|
1098
|
+
return { ok: true, total: discoveredModels.length, media: mediaCount,
|
|
1099
|
+
text: `[iris] ${p.name}:发现 ${entries.length} 个模型,${mediaCount} 个有能力声明;未标注模型请在模型池中设置能力` };
|
|
1076
1100
|
});
|
|
1077
1101
|
|
|
1078
1102
|
/** 能力实测(红色测试图):对某供应商的 vision 模型跑 testVisionCapability */
|
|
@@ -1104,6 +1128,9 @@ register('providers_test_vision', async (host, args, { signal }) => {
|
|
|
1104
1128
|
|
|
1105
1129
|
/** 按能力实测单个模型;返回 {ok, note}。真实调用仅在 UI 明确确认后执行。 */
|
|
1106
1130
|
async function probeModel(host, p, modelId, capability, signal) {
|
|
1131
|
+
if (capability === cap.CAPABILITIES.MUSIC) {
|
|
1132
|
+
return { skipped: true, ok: false, note: 'Music 不发起小样本探测;请通过音乐功能提交实际创作要求' };
|
|
1133
|
+
}
|
|
1107
1134
|
if (capability === cap.CAPABILITIES.VISION) {
|
|
1108
1135
|
const r = await probeVisionModel(p, modelId, visionImageFromDataUrl(RED_TEST_IMAGE), { timeoutMs: 15000, signal });
|
|
1109
1136
|
return {
|
|
@@ -1177,17 +1204,17 @@ async function probeModel(host, p, modelId, capability, signal) {
|
|
|
1177
1204
|
return { ok: false, note: '未知能力' };
|
|
1178
1205
|
}
|
|
1179
1206
|
|
|
1180
|
-
/**
|
|
1207
|
+
/** 手动添加模型到池;省略或清空能力时保留未标注模型。 */
|
|
1181
1208
|
register('providers_add_model', async (host, args) => {
|
|
1182
1209
|
const id = String(args.id || '');
|
|
1183
1210
|
const modelId = String(args.model_id || '').trim();
|
|
1184
1211
|
if (!modelId) throw new Error('iris: model_id 不能为空');
|
|
1185
|
-
const caps = Array.isArray(args.capabilities)
|
|
1212
|
+
const caps = Array.isArray(args.capabilities)
|
|
1186
1213
|
? args.capabilities.filter((c) => cap.isKnownCapability(c))
|
|
1187
|
-
:
|
|
1214
|
+
: undefined;
|
|
1188
1215
|
const entry = store.addProviderModel(id, modelId, caps);
|
|
1189
1216
|
if (!entry) throw new Error('iris: 供应商不存在');
|
|
1190
|
-
return { ok: true, text: `[iris] 已添加模型 ${modelId}(能力:${caps.join(',') || '未标注'})` };
|
|
1217
|
+
return { ok: true, text: `[iris] 已添加模型 ${modelId}(能力:${(caps || []).join(',') || '未标注'})` };
|
|
1191
1218
|
});
|
|
1192
1219
|
|
|
1193
1220
|
/** 从池移除模型 */
|
|
@@ -1211,6 +1238,12 @@ register('providers_set_model_image_protocol', async (host, args) => {
|
|
|
1211
1238
|
return { ok: true, text: '[iris] 模型图片协议已更新' };
|
|
1212
1239
|
});
|
|
1213
1240
|
|
|
1241
|
+
register('providers_set_model_music_protocol', async (host, args) => {
|
|
1242
|
+
const ok = store.setModelMusicProtocol(String(args.id || ''), String(args.model_id || ''), args.musicProtocol);
|
|
1243
|
+
if (!ok) throw new Error('iris: 模型不在池中');
|
|
1244
|
+
return { ok: true, text: '[iris] 模型音乐协议已更新' };
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1214
1247
|
register('providers_set_model_vision_input', async (host, args) => {
|
|
1215
1248
|
if (!Object.hasOwn(args, 'visionInput')) throw new Error('iris: 需要 visionInput 对象或 null');
|
|
1216
1249
|
const ok = store.setModelVisionInput(String(args.id || ''), String(args.model_id || ''), args.visionInput);
|
|
@@ -1225,7 +1258,7 @@ register('providers_test_model', async (host, args, { signal }) => {
|
|
|
1225
1258
|
const modelId = String(args.model_id || '').trim();
|
|
1226
1259
|
const capability = String(args.capability || '').trim();
|
|
1227
1260
|
if (!modelId || !cap.isKnownCapability(capability)) throw new Error('iris: 需 model_id 与合法 capability');
|
|
1228
|
-
if (![cap.CAPABILITIES.VIDEO, cap.CAPABILITIES.TRANSCRIBE].includes(capability) && args.confirm_paid !== true) {
|
|
1261
|
+
if (![cap.CAPABILITIES.VIDEO, cap.CAPABILITIES.TRANSCRIBE, cap.CAPABILITIES.MUSIC].includes(capability) && args.confirm_paid !== true) {
|
|
1229
1262
|
throw new Error('iris: 模型实测会发起真实供应商请求,请先明确确认');
|
|
1230
1263
|
}
|
|
1231
1264
|
let result;
|
package/lib/adapters.js
CHANGED
|
@@ -11,10 +11,11 @@ import fs from 'node:fs';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { Readable } from 'node:stream';
|
|
13
13
|
import { pipeline } from 'node:stream/promises';
|
|
14
|
-
import { dashscopeApiBase, isDashScopeBaseUrl } from './provider-protocol.js';
|
|
14
|
+
import { dashscopeApiBase, imageSizeViolation, isDashScopeBaseUrl, normalizeImageSize } from './provider-protocol.js';
|
|
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——
|
|
@@ -95,17 +96,65 @@ function classifyHttpError(status, body, fallback, headers) {
|
|
|
95
96
|
...(status === 429 ? { retryAfterMs: retryAfterDelay(headers?.get('retry-after')) } : {}) });
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
/** 包装 fetch 层的网络异常(无 HTTP
|
|
99
|
-
export function networkError(err) {
|
|
99
|
+
/** 包装 fetch 层的网络异常(无 HTTP 状态)。hint 补一句可执行的排查方向。 */
|
|
100
|
+
export function networkError(err, hint) {
|
|
101
|
+
const code = String(err?.cause?.code || err?.code || '');
|
|
100
102
|
const e = new ProviderError({
|
|
101
103
|
category: 'network',
|
|
102
104
|
status: 0,
|
|
103
|
-
message: '网络错误:' + String((err && err.message) || err),
|
|
104
|
-
rootCause: err
|
|
105
|
+
message: '网络错误:' + String((err && err.message) || err) + (hint ? '(' + hint + ')' : ''),
|
|
106
|
+
rootCause: err,
|
|
107
|
+
...(/^[A-Z][A-Z0-9_]{1,63}$/.test(code) ? { providerCode: code } : {})
|
|
105
108
|
});
|
|
106
109
|
return e;
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
/** 超时/取消必须保持原分类:轮询 errStreak 容错与 submitGuard 都依赖 network 与 timeout 的区分。 */
|
|
113
|
+
function isAbortLike(err) {
|
|
114
|
+
return err?.name === 'AbortError' || err?.name === 'TimeoutError' || err?.code === 'ABORT_ERR';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function transportFetchError(err, response) {
|
|
118
|
+
if (isAbortLike(err)) return err;
|
|
119
|
+
const code = String(err?.cause?.code || err?.code || '');
|
|
120
|
+
const detail = {
|
|
121
|
+
ENOTFOUND: 'DNS 未找到目标主机',
|
|
122
|
+
EAI_AGAIN: 'DNS 解析暂时失败',
|
|
123
|
+
ECONNREFUSED: '目标连接被拒绝',
|
|
124
|
+
UND_ERR_CONNECT_TIMEOUT: '连接建立超时',
|
|
125
|
+
UND_ERR_HEADERS_TIMEOUT: '等待响应头超时',
|
|
126
|
+
UND_ERR_BODY_TIMEOUT: '响应正文读取超时',
|
|
127
|
+
ETIMEDOUT: '网络操作超时',
|
|
128
|
+
ECONNRESET: '连接被重置',
|
|
129
|
+
UND_ERR_SOCKET: '底层连接中断',
|
|
130
|
+
CERT_HAS_EXPIRED: 'TLS 证书已过期',
|
|
131
|
+
DEPTH_ZERO_SELF_SIGNED_CERT: 'TLS 证书不受信任',
|
|
132
|
+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'TLS 证书链验证失败'
|
|
133
|
+
}[code];
|
|
134
|
+
const phase = response
|
|
135
|
+
? `已收到 HTTP ${response.status} 响应头,读取响应正文失败`
|
|
136
|
+
: '请求传输失败,尚未收到响应头';
|
|
137
|
+
const error = networkError(err, phase + (detail ? ';' + detail : '') +
|
|
138
|
+
(code && /^[A-Z][A-Z0-9_]{1,63}$/.test(code) ? ';' + code : '') + ';受理结果未知,禁止自动重提');
|
|
139
|
+
if (['UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'ETIMEDOUT'].includes(code)) {
|
|
140
|
+
error.category = 'timeout';
|
|
141
|
+
}
|
|
142
|
+
error.stage = response ? 'response' : 'submit';
|
|
143
|
+
error.acceptance = 'unknown';
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 只包装网络读取;HTTP 业务错误和图片协议校验由调用方处理。 */
|
|
148
|
+
async function fetchImageJson(url, options) {
|
|
149
|
+
let response;
|
|
150
|
+
try {
|
|
151
|
+
response = await fetch(url, options);
|
|
152
|
+
return { res: response, body: await readJson(response) };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
throw transportFetchError(err, response);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
109
158
|
function providerError(status, body, fallback, headers) {
|
|
110
159
|
return classifyHttpError(status, body, fallback, headers);
|
|
111
160
|
}
|
|
@@ -125,20 +174,34 @@ export function dashscopeImageMode(model) {
|
|
|
125
174
|
return 'legacy-async';
|
|
126
175
|
}
|
|
127
176
|
|
|
177
|
+
/**
|
|
178
|
+
* 尺寸在进入请求体前统一「归一 + 校验」:
|
|
179
|
+
* 协议差异(分隔符、数值边界)全部来自 provider-protocol.js,本地违规明确标记为未受理。
|
|
180
|
+
* 归一的意义是跨协议可用 —— 例如把 `1024x1536` 送给 DashScope 多模态(服务端要求 `*`),
|
|
181
|
+
* 或把 `1024*1536` 送给 OpenAI 兼容端点(服务端要求 `x`),都不再需要调用方自己记格式。
|
|
182
|
+
*/
|
|
183
|
+
export function applyImageSize({ protocol, mode, size }) {
|
|
184
|
+
const violation = imageSizeViolation({ protocol, mode, size });
|
|
185
|
+
if (violation) throw new ProviderContractError(violation, {
|
|
186
|
+
stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
|
|
187
|
+
});
|
|
188
|
+
return normalizeImageSize({ protocol, mode, size });
|
|
189
|
+
}
|
|
190
|
+
|
|
128
191
|
/** 新版多模态同步协议:响应中直接返回图片 URL。 */
|
|
129
192
|
export async function generateImageMultimodal({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
130
193
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
131
|
-
const
|
|
194
|
+
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'multimodal-sync', size });
|
|
195
|
+
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/multimodal-generation/generation`, {
|
|
132
196
|
method: 'POST',
|
|
133
197
|
headers: authHeaders(key),
|
|
134
198
|
signal: timeoutSignal(timeoutMs, signal),
|
|
135
199
|
body: JSON.stringify({
|
|
136
200
|
model,
|
|
137
201
|
input: { messages: [{ role: 'user', content: [{ text: prompt }] }] },
|
|
138
|
-
parameters: { ...(
|
|
202
|
+
parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 }
|
|
139
203
|
})
|
|
140
204
|
});
|
|
141
|
-
const body = await readJson(res);
|
|
142
205
|
if (!res.ok) throw providerError(res.status, body, '生成失败', res.headers);
|
|
143
206
|
const urls = [];
|
|
144
207
|
for (const choice of (body && body.output && body.output.choices) || []) {
|
|
@@ -160,13 +223,13 @@ export async function startImageGeneration(opts) {
|
|
|
160
223
|
}
|
|
161
224
|
export async function submitImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SUBMIT, signal }) {
|
|
162
225
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
163
|
-
const
|
|
226
|
+
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'legacy-async', size });
|
|
227
|
+
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/text2image/image-synthesis`, {
|
|
164
228
|
method: 'POST',
|
|
165
229
|
headers: { ...authHeaders(key), 'X-DashScope-Async': 'enable' },
|
|
166
230
|
signal: timeoutSignal(timeoutMs, signal),
|
|
167
|
-
body: JSON.stringify({ model, input: { prompt }, parameters: { ...(
|
|
231
|
+
body: JSON.stringify({ model, input: { prompt }, parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 } })
|
|
168
232
|
});
|
|
169
|
-
const body = await readJson(res);
|
|
170
233
|
if (!res.ok) throw providerError(res.status, body, '提交失败', res.headers);
|
|
171
234
|
const taskId = body && body.output && body.output.task_id;
|
|
172
235
|
if (!taskId) throw new Error('提交成功但未返回 task_id');
|
|
@@ -312,13 +375,13 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
|
|
|
312
375
|
/* ---------------- OpenAI Images 兼容(同步 base64/url) ---------------- */
|
|
313
376
|
export async function openAiGenerateImage({ key, baseUrl, model, prompt, size = '1024x1024', n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
314
377
|
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
315
|
-
const
|
|
378
|
+
const requestSize = applyImageSize({ protocol: 'openai-images', size });
|
|
379
|
+
const { res, body } = await fetchImageJson(`${base}/images/generations`, {
|
|
316
380
|
method: 'POST',
|
|
317
381
|
headers: authHeaders(key),
|
|
318
382
|
signal: timeoutSignal(timeoutMs, signal),
|
|
319
|
-
body: JSON.stringify({ model, prompt, size, n: Number(n) || 1 })
|
|
383
|
+
body: JSON.stringify({ model, prompt, size: requestSize, n: Number(n) || 1 })
|
|
320
384
|
});
|
|
321
|
-
const body = await readJson(res);
|
|
322
385
|
if (!res.ok) throw providerError(res.status, body, '生成失败', res.headers);
|
|
323
386
|
const items = (body && Array.isArray(body.data) && body.data) || [];
|
|
324
387
|
const out = [];
|
|
@@ -389,12 +452,11 @@ export async function openAiChatGenerateImage({ key, baseUrl, model, prompt, ima
|
|
|
389
452
|
{ type: 'text', text: prompt },
|
|
390
453
|
{ type: 'image_url', image_url: { url: 'data:' + image.mediaType + ';base64,' + Buffer.from(image.bytes).toString('base64') } }
|
|
391
454
|
] : prompt;
|
|
392
|
-
const res = await
|
|
455
|
+
const { res, body } = await fetchImageJson(`${base}/chat/completions`, {
|
|
393
456
|
method: 'POST', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
394
457
|
body: JSON.stringify({ model, stream: false, messages: [{ role: 'user', content }],
|
|
395
458
|
...(Number(n) > 1 ? { n: Number(n) } : {}) })
|
|
396
459
|
});
|
|
397
|
-
const body = await readJson(res);
|
|
398
460
|
if (!res.ok) throw providerError(res.status, body, '聊天生图失败', res.headers);
|
|
399
461
|
return chatImageItems(body);
|
|
400
462
|
}
|
|
@@ -407,13 +469,13 @@ export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt
|
|
|
407
469
|
});
|
|
408
470
|
}
|
|
409
471
|
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
410
|
-
const
|
|
472
|
+
const requestSize = applyImageSize({ protocol: 'openai-responses-images', size });
|
|
473
|
+
const { res, body } = await fetchImageJson(`${base}/responses`, {
|
|
411
474
|
method: 'POST', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
412
475
|
body: JSON.stringify({ model, input: prompt, stream: false, store: false, background: false,
|
|
413
|
-
tools: [{ type: 'image_generation', action: 'generate', ...(
|
|
476
|
+
tools: [{ type: 'image_generation', action: 'generate', ...(requestSize ? { size: requestSize } : {}) }],
|
|
414
477
|
tool_choice: { type: 'image_generation' } })
|
|
415
478
|
});
|
|
416
|
-
const body = await readJson(res);
|
|
417
479
|
if (!res.ok) throw providerError(res.status, body, 'Responses 生图失败', res.headers);
|
|
418
480
|
if (body?.status !== 'completed') throw imageResponseError('Responses 生图未返回同步完成结果;禁止自动重提');
|
|
419
481
|
const output = Array.isArray(body.output) ? body.output : [];
|
|
@@ -469,11 +531,14 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
469
531
|
? item.inference_metadata.request_modality.map((value) => String(value).toLowerCase()) : [];
|
|
470
532
|
const responseModalities = Array.isArray(item?.inference_metadata?.response_modality)
|
|
471
533
|
? item.inference_metadata.response_modality.map((value) => String(value).toLowerCase()) : [];
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
534
|
+
// 明确能力代码(含空数组/未知新能力)优先,不能被通用模态推断追加用途。
|
|
535
|
+
if (!Array.isArray(item?.capabilities)) {
|
|
536
|
+
if (responseModalities.includes('image')) capabilities.push('IG');
|
|
537
|
+
if (responseModalities.includes('video')) capabilities.push('VG');
|
|
538
|
+
if (requestModalities.includes('audio') && responseModalities.includes('text')) capabilities.push('ASR');
|
|
539
|
+
if (requestModalities.includes('text') && responseModalities.includes('audio')) capabilities.push('TTS');
|
|
540
|
+
if (requestModalities.includes('image') && responseModalities.includes('text')) capabilities.push('VU');
|
|
541
|
+
}
|
|
477
542
|
const normalizedCapabilities = [...new Set(capabilities)];
|
|
478
543
|
found.set(id, normalizedCapabilities.length ? { id, capabilities: normalizedCapabilities } : id);
|
|
479
544
|
}
|
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:
|
|
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
|
-
|
|
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:
|
|
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 === '
|
|
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
|
}
|
package/lib/bundled-skills.js
CHANGED
|
@@ -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-
|
|
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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - 能力常量:image-gen / video-gen / tts / transcribe / vision
|
|
7
7
|
* - 严格选择:不具备某能力的 Provider 不得被兜底选中
|
|
8
8
|
* - 有序 failover:同一能力按 providers.json 声明顺序返回,调用方按序尝试
|
|
9
|
-
* -
|
|
9
|
+
* - 能力只来自明确标签或旧模型字段绑定,不按供应商端点赋能
|
|
10
10
|
*
|
|
11
11
|
* 本模块是纯函数,不读写磁盘;只吃 provider 记录、吐能力判断。
|
|
12
12
|
*/
|
|
@@ -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,40 +27,48 @@ 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
|
};
|
|
32
34
|
|
|
35
|
+
/** 模型级明确标签优先;旧能力字段只为同名模型提供明确绑定。 */
|
|
36
|
+
export function configuredModelCapabilities(provider, model) {
|
|
37
|
+
if (Array.isArray(model?.capabilities)) {
|
|
38
|
+
return [...new Set(model.capabilities.filter(isKnownCapability))];
|
|
39
|
+
}
|
|
40
|
+
const id = typeof model === 'string' ? model.trim() : String(model?.id || '').trim();
|
|
41
|
+
if (!id) return [];
|
|
42
|
+
const bound = Object.entries(INFER_FIELD)
|
|
43
|
+
.filter(([, field]) => typeof provider?.[field] === 'string' && provider[field].trim() === id)
|
|
44
|
+
.map(([capability]) => capability);
|
|
45
|
+
return Array.isArray(provider?.capabilities) ? bound.filter((capability) => provider.capabilities.includes(capability)) : bound;
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
export function isKnownCapability(capability) {
|
|
34
49
|
return KNOWN.has(capability);
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
/**
|
|
38
53
|
* 一个 provider 的能力列表(去重、只留已知能力)。
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* ② 否则按模型字段推断(imageModel→image-gen 等);
|
|
42
|
-
* ③ 仍为空且是 DashScope 端点 → 全部媒体能力
|
|
43
|
-
* (百炼单一 API 服务 t2i/t2v/tts/compatible-vision,裸账号常见于迁移期)。
|
|
54
|
+
* 显式模型池优先,包括空池;其次是账号明确标签(含空数组),
|
|
55
|
+
* 最后兼容旧能力字段绑定。账号地址和模型名称都不证明能力。
|
|
44
56
|
* @param {object} p provider 记录
|
|
45
57
|
* @returns {string[]}
|
|
46
58
|
*/
|
|
47
59
|
export function capabilitiesOf(p) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
if (Array.isArray(p?.models)) {
|
|
61
|
+
return [...new Set(p.models.flatMap((model) => configuredModelCapabilities(p, model)))];
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(p?.capabilities)) {
|
|
64
|
+
return [...new Set(p.capabilities.filter(isKnownCapability))];
|
|
51
65
|
}
|
|
52
66
|
// 旧配置迁移:按模型字段推断(有该能力模型才认为具备该能力)
|
|
53
67
|
const inferred = [];
|
|
54
68
|
for (const [cap, field] of Object.entries(INFER_FIELD)) {
|
|
55
|
-
if (p && p[field]) inferred.push(cap);
|
|
56
|
-
}
|
|
57
|
-
if (inferred.length) return inferred;
|
|
58
|
-
// 裸 DashScope 账号兜底迁移
|
|
59
|
-
if (p && /dashscope/i.test(String(p.baseUrl || ''))) {
|
|
60
|
-
return Object.values(CAPABILITIES);
|
|
69
|
+
if (typeof p?.[field] === 'string' && p[field].trim()) inferred.push(cap);
|
|
61
70
|
}
|
|
62
|
-
return
|
|
71
|
+
return inferred;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
/**
|