@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-beta.1
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/CHANGELOG.md +143 -1
- package/README.en.md +109 -311
- package/README.md +106 -309
- package/bin/dsh-iris.js +452 -92
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +87 -0
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +101 -0
- package/docs/CORE_QUICKSTART.md +110 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
- package/docs/CORE_SERVICE.md +101 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +21 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +30 -5
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +4 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +4 -2
- package/docs/LOCAL_API_CONTRACT.md +42 -15
- package/docs/LOCAL_CREATION.md +7 -1
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +207 -0
- package/docs/MCP.md +212 -0
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/MUSIC.md +2 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +193 -0
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +113 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +82 -0
- package/docs/PROVIDER_HEALTH.md +5 -1
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +91 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +21 -6
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +56 -4
- package/lib/adapters.js +75 -49
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +73 -17
- package/lib/application-writer.js +193 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +402 -72
- package/lib/comfyui-adapter.js +95 -0
- package/lib/comfyui-client.js +60 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/command-service.js +18 -5
- package/lib/config.js +49 -10
- package/lib/configured-image-runtime.js +69 -0
- package/lib/connection-skill.js +98 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +84 -28
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +85 -0
- package/lib/core-capability-catalog.js +192 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-maintenance.js +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +174 -0
- package/lib/core-tasks.js +113 -21
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +41 -64
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +67 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +156 -14
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +97 -31
- package/lib/index.js +47 -22
- package/lib/local-api-projection.js +15 -6
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +861 -71
- package/lib/local-creation.js +96 -24
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +39 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +174 -0
- package/lib/mcp-server.js +69 -0
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +37 -10
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +6 -3
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +281 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +307 -0
- package/lib/plugins.js +59 -0
- package/lib/provider-adapter.js +7 -3
- package/lib/provider-adapters.js +92 -22
- package/lib/provider-catalog.js +64 -36
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +143 -31
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +18 -6
- package/lib/provider-monitor.js +81 -0
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +73 -22
- package/lib/provider-registry.js +128 -0
- package/lib/provider-response.js +38 -0
- package/lib/provider-settings.js +136 -0
- package/lib/provider-task-runner.js +132 -43
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/skill-installation.js +23 -11
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +5 -0
- package/lib/web/app.js +703 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +516 -0
- package/lib/web/creation.js +359 -0
- package/lib/web/flow-view.js +34 -0
- package/lib/web/gallery.js +133 -0
- package/lib/web/index.html +344 -0
- package/lib/web/media-client.js +218 -0
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +223 -0
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +513 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +571 -0
- package/lib/web/transport.js +101 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +3 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +24 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +8 -4
- package/skills/iris-core/references/cli.md +2 -2
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +2 -0
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +5 -1
package/lib/actions.js
CHANGED
|
@@ -30,7 +30,7 @@ import { formatOcrResult } from './ocr.js';
|
|
|
30
30
|
import { runOcrRequest } from './ocr-model-routing.js';
|
|
31
31
|
import { ffmpegAvailable, extractFrames, probeVideo } from './media-probe.js';
|
|
32
32
|
import { ensurePrivateDir, writePrivateFile } from './private-storage.js';
|
|
33
|
-
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, supportsImageEdit, supportsModelCapability, dashscopeTtsProtocolIssue, dashscopeTtsInputIssue } from './provider-protocol.js';
|
|
33
|
+
import { selectMediaProtocol, selectImageProtocol, providerForImageModel, providerApiKey, supportsImageEdit, supportsModelCapability, dashscopeTtsProtocolIssue, dashscopeTtsInputIssue } from './provider-protocol.js';
|
|
34
34
|
import { musicCandidatesFromCatalog, providerTaskBinding } from './provider-catalog.js';
|
|
35
35
|
import { invokeProviderOperation, providerPollTaskView, prepareProviderInput } from './provider-adapter.js';
|
|
36
36
|
import { createConfiguredProviderAdapter as createProviderAdapter } from './provider-adapters.js';
|
|
@@ -40,10 +40,14 @@ import { videoTaskCandidates } from './video-input.js';
|
|
|
40
40
|
import { normalizeDshProvider, withDshProviderConfig } from './dsh-provider-config.js';
|
|
41
41
|
import { modelDirectorySummary, modelUnavailableReason } from './model-directory.js';
|
|
42
42
|
import { ModelPortError } from './model-port-contract.js';
|
|
43
|
+
import { compileComfyWorkflow, projectComfyWorkflow } from './comfyui-workflow.js';
|
|
44
|
+
import { createComfyUiClient } from './comfyui-client.js';
|
|
45
|
+
import { inspectComfyWorkflow } from './comfyui-inspection.js';
|
|
46
|
+
import { createProviderSecretScrubber } from './provider-credentials.js';
|
|
43
47
|
|
|
44
48
|
const PROVIDER_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
45
49
|
'locate', 'ocr', 'media_summarize', 'task_reobserve', 'task_redeliver', 'task_manual_retry',
|
|
46
|
-
'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model']);
|
|
50
|
+
'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model', 'providers_check_comfy_workflow']);
|
|
47
51
|
const AUTO_DIRECTORY_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
48
52
|
'locate', 'ocr', 'media_summarize']);
|
|
49
53
|
const VISION_ACTIONS = new Set(['look', 'relook', 'locate', 'ocr', 'media_summarize']);
|
|
@@ -64,6 +68,12 @@ function imageRoutingForModel(provider, modelId) {
|
|
|
64
68
|
return { ...routing, editSupported: supportsImageEdit(routing.mediaProtocol, modelId) };
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
function comfySummaryForModel(provider, modelId) {
|
|
72
|
+
const route = providerForImageModel(provider, modelId);
|
|
73
|
+
return route.mediaProtocol === 'comfyui' && route.comfyWorkflow !== undefined
|
|
74
|
+
? { comfyWorkflow: projectComfyWorkflow(route.comfyWorkflow) } : {};
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
const EMPTY_ACTION_HOST = defineHostAdapter({
|
|
68
78
|
id: 'iris-hostless',
|
|
69
79
|
ports: {},
|
|
@@ -512,11 +522,22 @@ async function imageAction(host, args, { signal, core }, editing = false) {
|
|
|
512
522
|
if (editing && !args.source_artifact_id) throw new Error('iris: 改图必须提供 source_artifact_id');
|
|
513
523
|
const { providerInput, sourceArtifactId } = normalizeGenerationInput('image', {
|
|
514
524
|
prompt: args.prompt, size: optionalActionText(args.size), n: args.n,
|
|
525
|
+
...(args.workflow_parameters !== undefined ? { model_ref: args.model, workflow_parameters: args.workflow_parameters } : {}),
|
|
515
526
|
...(editing ? { source_artifact_id: args.source_artifact_id } : {})
|
|
516
527
|
});
|
|
517
528
|
|
|
518
529
|
const imageProviders = selectProviders(cap.CAPABILITIES.IMAGE, '图像', args.model);
|
|
519
530
|
const routes = imageProviders.map((provider) => imageRoute(provider, args));
|
|
531
|
+
if (args.workflow_sha !== undefined && (typeof args.workflow_sha !== 'string' || routes.length !== 1
|
|
532
|
+
|| projectComfyWorkflow(routes[0].provider.comfyWorkflow)?.sha256 !== args.workflow_sha)) {
|
|
533
|
+
throw Object.assign(new Error('工作流已变化,请重新选择并核对参数'), { code: 'IRIS_CONFIG_CHANGED' });
|
|
534
|
+
}
|
|
535
|
+
if (providerInput.workflow_parameters !== undefined) {
|
|
536
|
+
if (!models.parseModelRef(args.model) || routes.length !== 1 || routes[0].protocol !== 'comfyui') {
|
|
537
|
+
throw new TypeError('工作流参数需要明确选择一个 ComfyUI 工作流');
|
|
538
|
+
}
|
|
539
|
+
compileComfyWorkflow(routes[0].provider.comfyWorkflow, providerInput);
|
|
540
|
+
}
|
|
520
541
|
const submitted = await submitCoreImage(core, routes, providerInput, signal, sourceArtifactId);
|
|
521
542
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
522
543
|
const ready = submitted.task.outcome === 'succeeded'
|
|
@@ -1084,13 +1105,14 @@ register('providers_list', async (host, args) => {
|
|
|
1084
1105
|
...selectMediaProtocol(p),
|
|
1085
1106
|
visionInput: p.visionInput || null,
|
|
1086
1107
|
directory: modelDirectorySummary(p),
|
|
1087
|
-
apiKeyHint: p.dshProvider ? '由 DSH 管理' : String(p.apiKey || '').length <= 8 ? '****' : String(p.apiKey).slice(0, 3) + '****' + String(p.apiKey).slice(-4),
|
|
1108
|
+
apiKeyHint: p.dshProvider ? '由 DSH 管理' : p.apiKeyEnv ? '由环境变量提供' : String(p.apiKey || '').length <= 8 ? '****' : String(p.apiKey).slice(0, 3) + '****' + String(p.apiKey).slice(-4),
|
|
1088
1109
|
models: models.providerModelsWithDirectory(p).map((m) => ({
|
|
1089
1110
|
id: m.id,
|
|
1090
1111
|
capabilities: m.capabilities,
|
|
1091
1112
|
...(m.directoryOnly ? { directoryOnly: true } : {}),
|
|
1092
1113
|
...(m.catalogStatus ? { catalogStatus: m.catalogStatus } : {}),
|
|
1093
1114
|
...(m.imageProtocol !== undefined ? { imageProtocol: m.imageProtocol } : {}),
|
|
1115
|
+
...comfySummaryForModel(p, m.id),
|
|
1094
1116
|
...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
|
|
1095
1117
|
...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}),
|
|
1096
1118
|
...(m.capabilities.includes(cap.CAPABILITIES.VISION) ? { visionInputEffective: visionInputLimits(p, m.id) } : {}),
|
|
@@ -1112,6 +1134,34 @@ register('providers_host_connections', async host => {
|
|
|
1112
1134
|
return { ok: true, connections: await host.ports.providerConnections.list() };
|
|
1113
1135
|
});
|
|
1114
1136
|
|
|
1137
|
+
/** Explicit template save; no provider request or model execution. */
|
|
1138
|
+
register('providers_save_comfy_workflow', async (_host, args) => {
|
|
1139
|
+
if (Object.keys(args).some(key => !['id', 'model_id', 'workflow', 'expectedSha', 'enableAutomatic'].includes(key))) throw new TypeError('工作流保存包含不支持的字段');
|
|
1140
|
+
const entry = store.setComfyWorkflow(args.id, args.model_id, args.workflow, {
|
|
1141
|
+
expectedSha: args.expectedSha, enableAutomatic: args.enableAutomatic
|
|
1142
|
+
});
|
|
1143
|
+
return { ok: true, modelId: entry.id, comfyWorkflow: projectComfyWorkflow(entry.comfyWorkflow),
|
|
1144
|
+
text: '工作流已保存;未执行生成' + (args.enableAutomatic ? ',图片功能已使用自动池' : ',功能池设置保持') };
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
/** Uses only an already saved connection. /object_info never enqueues a prompt. */
|
|
1148
|
+
register('providers_check_comfy_workflow', async (_host, args, { signal }) => {
|
|
1149
|
+
if (Object.keys(args).some(key => !['id', 'model_id', 'workflow', 'workflow_parameters'].includes(key))
|
|
1150
|
+
|| (args.workflow !== undefined) === (args.model_id !== undefined)) throw new TypeError('请选择已保存工作流或导入一份草稿进行检查');
|
|
1151
|
+
const provider = store.providerById(args.id);
|
|
1152
|
+
if (!provider) throw new TypeError('请先保存并启用 ComfyUI 连接');
|
|
1153
|
+
const route = args.workflow !== undefined ? provider : providerForImageModel(provider, args.model_id);
|
|
1154
|
+
if (route.mediaProtocol !== 'comfyui') throw new TypeError('此检查仅用于 ComfyUI 连接');
|
|
1155
|
+
const file = path.join(store.irisHome(), 'providers.json'), before = fs.readFileSync(file);
|
|
1156
|
+
const workflow = compileComfyWorkflow(args.workflow !== undefined ? args.workflow : route.comfyWorkflow,
|
|
1157
|
+
{ workflow_parameters: args.workflow_parameters });
|
|
1158
|
+
const scrub = createProviderSecretScrubber([route]);
|
|
1159
|
+
const report = await inspectComfyWorkflow(createComfyUiClient(route), workflow, { signal });
|
|
1160
|
+
signal?.throwIfAborted();
|
|
1161
|
+
if (!fs.readFileSync(file).equals(before)) throw Object.assign(new Error('检查期间配置已变化,请重新检查'), { code: 'IRIS_CONFIG_CHANGED' });
|
|
1162
|
+
return { ok: true, report: scrub(report) };
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1115
1165
|
/** 新增/更新供应商 */
|
|
1116
1166
|
register('providers_upsert', async (host, args) => {
|
|
1117
1167
|
const id = args.id || undefined;
|
|
@@ -1121,6 +1171,7 @@ register('providers_upsert', async (host, args) => {
|
|
|
1121
1171
|
for (const key of allowed) {
|
|
1122
1172
|
if (Object.prototype.hasOwnProperty.call(args, key)) patch[key] = args[key];
|
|
1123
1173
|
}
|
|
1174
|
+
if (Object.hasOwn(patch, 'auth') && !['none', 'bearer'].includes(patch.auth)) throw new TypeError('认证方式必须为 bearer 或 none');
|
|
1124
1175
|
if (Object.hasOwn(args, 'dshProvider')) {
|
|
1125
1176
|
patch.dshProvider = normalizeDshProvider(args.dshProvider);
|
|
1126
1177
|
const source = requireHostPort(host, 'providerConnections', '复用 DSH 供应商连接');
|
|
@@ -1235,6 +1286,7 @@ async function probeModel(host, p, modelId, capability, signal) {
|
|
|
1235
1286
|
}
|
|
1236
1287
|
if (capability === cap.CAPABILITIES.IMAGE) {
|
|
1237
1288
|
p = providerForImageModel(p, modelId);
|
|
1289
|
+
if (p.mediaProtocol === 'comfyui') return { skipped: true, ok: false, note: 'ComfyUI 请在画图表单选择工作流验证,以持久 Task 保留受理与结果;只读工作流检查不生成图片' };
|
|
1238
1290
|
const providerAdapter = createConfiguredProviderAdapter(p, { allowRateLimited: true, modelId });
|
|
1239
1291
|
const submitted = await invokeProviderOperation(providerAdapter, 'submit', {
|
|
1240
1292
|
capability: 'image', model: modelId,
|
|
@@ -1407,7 +1459,7 @@ register('assignments_get', async (host, args) => {
|
|
|
1407
1459
|
? (c === cap.CAPABILITIES.TTS && selectMediaProtocol(provider).mediaProtocol === 'dashscope'
|
|
1408
1460
|
&& dashscopeTtsProtocolIssue(m.id)) || '当前接口协议不支持此功能'
|
|
1409
1461
|
: modelUnavailable ? modelUnavailableReason() : m.rateLimited ? '限流冷却或额度不可用,请稍后或检查账号' : m.source === 'discovered' ? '已发现,待确认模型用途' : '',
|
|
1410
|
-
...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id) } : {}) });
|
|
1462
|
+
...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id), ...comfySummaryForModel(provider, m.id) } : {}) });
|
|
1411
1463
|
}
|
|
1412
1464
|
}
|
|
1413
1465
|
// order:归一化的有序分配列表(GUI 直接渲染 failover 顺序,无需处理单字符串旧格式)
|
package/lib/adapters.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 所有函数只收显式 key;错误统一 throw Error(人话)。
|
|
9
9
|
*/
|
|
10
10
|
import fs from 'node:fs';
|
|
11
|
+
import { inheritedProviderFetch } from './provider-network.js';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
import { Readable } from 'node:stream';
|
|
13
14
|
import { pipeline } from 'node:stream/promises';
|
|
@@ -15,6 +16,7 @@ import { dashscopeApiBase, imageSizeViolation, isDashScopeBaseUrl, normalizeImag
|
|
|
15
16
|
import { chmodPrivateFile, ensurePrivateDir, privateSibling } from './private-storage.js';
|
|
16
17
|
import { isFreeQuotaExhausted, retryAfterDelay } from './provider-health.js';
|
|
17
18
|
import { ProviderContractError } from './provider-contract.js';
|
|
19
|
+
import { boundedProviderBody, readProviderText, PROVIDER_RESPONSE_LIMITS } from './provider-response.js';
|
|
18
20
|
export { generateMusic } from './music-transport.js';
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -42,8 +44,8 @@ function authHeaders(key) {
|
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
async function readJson(res) {
|
|
46
|
-
const text = await res
|
|
47
|
+
async function readJson(res, maxBytes = PROVIDER_RESPONSE_LIMITS.json) {
|
|
48
|
+
const text = await readProviderText(res, { maxBytes });
|
|
47
49
|
try {
|
|
48
50
|
return text ? JSON.parse(text) : null;
|
|
49
51
|
} catch (_) {
|
|
@@ -145,12 +147,13 @@ function transportFetchError(err, response) {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
/** 只包装网络读取;HTTP 业务错误和图片协议校验由调用方处理。 */
|
|
148
|
-
async function fetchImageJson(url, options) {
|
|
150
|
+
async function fetchImageJson(url, options, fetchPort) {
|
|
149
151
|
let response;
|
|
150
152
|
try {
|
|
151
|
-
response = await
|
|
152
|
-
return { res: response, body: await readJson(response) };
|
|
153
|
+
response = await fetchPort(url, options);
|
|
154
|
+
return { res: response, body: await readJson(response, response.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json) };
|
|
153
155
|
} catch (err) {
|
|
156
|
+
if (err instanceof ProviderContractError) throw err;
|
|
154
157
|
throw transportFetchError(err, response);
|
|
155
158
|
}
|
|
156
159
|
}
|
|
@@ -189,11 +192,12 @@ export function applyImageSize({ protocol, mode, size }) {
|
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
/** 新版多模态同步协议:响应中直接返回图片 URL。 */
|
|
192
|
-
export async function generateImageMultimodal({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
195
|
+
export async function generateImageMultimodal({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
193
196
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
194
197
|
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'multimodal-sync', size });
|
|
195
198
|
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/multimodal-generation/generation`, {
|
|
196
199
|
method: 'POST',
|
|
200
|
+
redirect: 'error',
|
|
197
201
|
headers: authHeaders(key),
|
|
198
202
|
signal: timeoutSignal(timeoutMs, signal),
|
|
199
203
|
body: JSON.stringify({
|
|
@@ -204,7 +208,7 @@ export async function generateImageMultimodal({ key, baseUrl, model, prompt, ima
|
|
|
204
208
|
] }] },
|
|
205
209
|
parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 }
|
|
206
210
|
})
|
|
207
|
-
});
|
|
211
|
+
}, fetchPort);
|
|
208
212
|
if (!res.ok) throw providerError(res.status, body, '生成失败', res.headers);
|
|
209
213
|
const urls = [];
|
|
210
214
|
for (const choice of (body && body.output && body.output.choices) || []) {
|
|
@@ -224,15 +228,16 @@ export async function startImageGeneration(opts) {
|
|
|
224
228
|
}
|
|
225
229
|
return { remoteTaskId: await submitImage(opts), urls: [] };
|
|
226
230
|
}
|
|
227
|
-
export async function submitImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SUBMIT, signal }) {
|
|
231
|
+
export async function submitImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SUBMIT, signal, fetchPort = inheritedProviderFetch }) {
|
|
228
232
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
229
233
|
const requestSize = applyImageSize({ protocol: 'dashscope', mode: 'legacy-async', size });
|
|
230
234
|
const { res, body } = await fetchImageJson(`${apiBase}/services/aigc/text2image/image-synthesis`, {
|
|
231
235
|
method: 'POST',
|
|
236
|
+
redirect: 'error',
|
|
232
237
|
headers: { ...authHeaders(key), 'X-DashScope-Async': 'enable' },
|
|
233
238
|
signal: timeoutSignal(timeoutMs, signal),
|
|
234
239
|
body: JSON.stringify({ model, input: { prompt }, parameters: { ...(requestSize ? { size: requestSize } : {}), n: Number(n) || 1 } })
|
|
235
|
-
});
|
|
240
|
+
}, fetchPort);
|
|
236
241
|
if (!res.ok) throw providerError(res.status, body, '提交失败', res.headers);
|
|
237
242
|
const taskId = body && body.output && body.output.task_id;
|
|
238
243
|
if (!taskId) throw new Error('提交成功但未返回 task_id');
|
|
@@ -248,7 +253,7 @@ export async function submitImage({ key, baseUrl, model, prompt, size, n = 1, ti
|
|
|
248
253
|
* - 其他(t2v/i2v):POST /services/aigc/video-generation/video-synthesis,
|
|
249
254
|
* imgDataUrl 支持公网 URL 或 data:image base64。
|
|
250
255
|
*/
|
|
251
|
-
export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, size = '1280*720', duration, audioUrl, resolution, timeoutMs = T_SUBMIT, signal }) {
|
|
256
|
+
export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, size = '1280*720', duration, audioUrl, resolution, timeoutMs = T_SUBMIT, signal, fetchPort = inheritedProviderFetch }) {
|
|
252
257
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
253
258
|
const isS2V = /s2v/i.test(model || '');
|
|
254
259
|
let url, input, parameters;
|
|
@@ -265,11 +270,12 @@ export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, siz
|
|
|
265
270
|
url = `${apiBase}/services/aigc/video-generation/video-synthesis`;
|
|
266
271
|
input = { prompt };
|
|
267
272
|
if (imgDataUrl) input.img_url = imgDataUrl;
|
|
268
|
-
parameters = { size };
|
|
273
|
+
parameters = { size: typeof size === 'string' ? size.replace(/^(\d+)[xX](\d+)$/, '$1*$2') : size };
|
|
269
274
|
if (duration) parameters.duration = Number(duration);
|
|
270
275
|
}
|
|
271
|
-
const res = await
|
|
276
|
+
const res = await fetchPort(url, {
|
|
272
277
|
method: 'POST',
|
|
278
|
+
redirect: 'error',
|
|
273
279
|
headers,
|
|
274
280
|
signal: timeoutSignal(timeoutMs, signal),
|
|
275
281
|
body: JSON.stringify({ model, input, parameters })
|
|
@@ -287,16 +293,19 @@ export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, siz
|
|
|
287
293
|
* 返回 oss:// 形式 URL;调用模型时请求头须带 X-DashScope-OssResourceResolve: enable
|
|
288
294
|
* (submitVideo 的 s2v 分支已自动处理)。
|
|
289
295
|
*/
|
|
290
|
-
export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs = T_UPLOAD, signal }) {
|
|
296
|
+
export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, filename, timeoutMs = T_UPLOAD, signal, fetchPort = inheritedProviderFetch }) {
|
|
297
|
+
if ((filePath === undefined) === (bytes === undefined)) throw new TypeError('上传须且只能提供本地路径或明确字节');
|
|
298
|
+
const name = filePath !== undefined ? path.basename(filePath) : filename;
|
|
299
|
+
if (typeof name !== 'string' || !name || name.length > 128 || /[\/\\\0]/.test(name)
|
|
300
|
+
|| bytes !== undefined && (!(bytes instanceof Uint8Array) || !bytes.length)) throw new TypeError('上传字节或文件名无效');
|
|
291
301
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
292
302
|
const u = new URL(`${apiBase}/uploads`);
|
|
293
303
|
u.searchParams.set('action', 'getPolicy');
|
|
294
304
|
u.searchParams.set('model', model);
|
|
295
|
-
const pres = await
|
|
305
|
+
const pres = await fetchPort(u, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
|
|
296
306
|
const pbody = await readJson(pres);
|
|
297
307
|
if (!pres.ok || !pbody || !pbody.data) throw providerError(pres.status, pbody, '获取上传凭证失败', pres.headers);
|
|
298
308
|
const pol = pbody.data;
|
|
299
|
-
const name = path.basename(filePath);
|
|
300
309
|
const keyPath = `${pol.upload_dir}/${Date.now()}-${name}`;
|
|
301
310
|
const fd = new FormData();
|
|
302
311
|
fd.append('OSSAccessKeyId', pol.oss_access_key_id);
|
|
@@ -306,8 +315,9 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
|
|
|
306
315
|
fd.append('x-oss-forbid-overwrite', pol.x_oss_forbid_overwrite);
|
|
307
316
|
fd.append('key', keyPath);
|
|
308
317
|
fd.append('success_action_status', '200');
|
|
309
|
-
fd.append('file', new Blob([fs.readFileSync(filePath)]), name);
|
|
310
|
-
const up = await
|
|
318
|
+
fd.append('file', new Blob([bytes ?? fs.readFileSync(filePath)]), name);
|
|
319
|
+
const up = await fetchPort(pol.upload_host, { method: 'POST', redirect: 'error', body: fd, signal: timeoutSignal(timeoutMs, signal) });
|
|
320
|
+
await up.body?.cancel();
|
|
311
321
|
if (up.status !== 200) throw new Error(`临时上传失败:HTTP ${up.status}`);
|
|
312
322
|
return 'oss://' + keyPath;
|
|
313
323
|
}
|
|
@@ -315,9 +325,9 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
|
|
|
315
325
|
const TERMINAL = new Set(['SUCCEEDED', 'FAILED', 'CANCELED', 'UNKNOWN']);
|
|
316
326
|
|
|
317
327
|
/** @returns {{done:boolean, ok:boolean, urls:string[], message?:string}} */
|
|
318
|
-
export async function pollTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal }) {
|
|
328
|
+
export async function pollTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal, fetchPort = inheritedProviderFetch }) {
|
|
319
329
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
320
|
-
const res = await
|
|
330
|
+
const res = await fetchPort(`${apiBase}/tasks/${encodeURIComponent(remoteTaskId)}`, {
|
|
321
331
|
headers: authHeaders(key),
|
|
322
332
|
signal: timeoutSignal(timeoutMs, signal)
|
|
323
333
|
});
|
|
@@ -358,15 +368,16 @@ export async function pollTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL,
|
|
|
358
368
|
}
|
|
359
369
|
|
|
360
370
|
/* ---------------- DashScope:qwen-tts(同步) ---------------- */
|
|
361
|
-
export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', text, voice = 'Cherry', timeoutMs = T_SYNC_GEN, signal }) {
|
|
371
|
+
export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', text, voice = 'Cherry', timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
362
372
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
363
|
-
const res = await
|
|
373
|
+
const res = await fetchPort(`${apiBase}/services/aigc/multimodal-generation/generation`, {
|
|
364
374
|
method: 'POST',
|
|
375
|
+
redirect: 'error',
|
|
365
376
|
headers: authHeaders(key),
|
|
366
377
|
signal: timeoutSignal(timeoutMs, signal),
|
|
367
378
|
body: JSON.stringify({ model, input: { text, voice } })
|
|
368
379
|
});
|
|
369
|
-
const body = await readJson(res);
|
|
380
|
+
const body = await readJson(res, res.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json);
|
|
370
381
|
if (!res.ok) {
|
|
371
382
|
const error = providerError(res.status, body, '合成失败', res.headers);
|
|
372
383
|
if (res.status === 404 && (/NotFound\.Model|model_not_found/i.test(error.providerCode)
|
|
@@ -384,15 +395,16 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
|
|
|
384
395
|
}
|
|
385
396
|
|
|
386
397
|
/* ---------------- OpenAI Images 兼容(同步 base64/url) ---------------- */
|
|
387
|
-
export async function openAiGenerateImage({ key, baseUrl, model, prompt, size = '1024x1024', n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
398
|
+
export async function openAiGenerateImage({ key, baseUrl, model, prompt, size = '1024x1024', n = 1, timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
388
399
|
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
389
400
|
const requestSize = applyImageSize({ protocol: 'openai-images', size });
|
|
390
401
|
const { res, body } = await fetchImageJson(`${base}/images/generations`, {
|
|
391
402
|
method: 'POST',
|
|
403
|
+
redirect: 'error',
|
|
392
404
|
headers: authHeaders(key),
|
|
393
405
|
signal: timeoutSignal(timeoutMs, signal),
|
|
394
406
|
body: JSON.stringify({ model, prompt, size: requestSize, n: Number(n) || 1 })
|
|
395
|
-
});
|
|
407
|
+
}, fetchPort);
|
|
396
408
|
if (!res.ok) throw providerError(res.status, body, '生成失败', res.headers);
|
|
397
409
|
const items = (body && Array.isArray(body.data) && body.data) || [];
|
|
398
410
|
const out = [];
|
|
@@ -452,7 +464,7 @@ function chatImageItems(body, label = '聊天生图') {
|
|
|
452
464
|
return items;
|
|
453
465
|
}
|
|
454
466
|
|
|
455
|
-
export async function openAiChatGenerateImage({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
467
|
+
export async function openAiChatGenerateImage({ key, baseUrl, model, prompt, image, size, n = 1, timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
456
468
|
if (typeof size === 'string' && size.trim()) {
|
|
457
469
|
throw new ProviderContractError('聊天生图协议不支持 size 参数;请在提示词中描述所需尺寸', {
|
|
458
470
|
stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
|
|
@@ -464,16 +476,16 @@ export async function openAiChatGenerateImage({ key, baseUrl, model, prompt, ima
|
|
|
464
476
|
{ type: 'image_url', image_url: { url: 'data:' + image.mediaType + ';base64,' + Buffer.from(image.bytes).toString('base64') } }
|
|
465
477
|
] : prompt;
|
|
466
478
|
const { res, body } = await fetchImageJson(`${base}/chat/completions`, {
|
|
467
|
-
method: 'POST', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
479
|
+
method: 'POST', redirect: 'error', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
468
480
|
body: JSON.stringify({ model, stream: false, messages: [{ role: 'user', content }],
|
|
469
481
|
...(Number(n) > 1 ? { n: Number(n) } : {}) })
|
|
470
|
-
});
|
|
482
|
+
}, fetchPort);
|
|
471
483
|
if (!res.ok) throw providerError(res.status, body, '聊天生图失败', res.headers);
|
|
472
484
|
return chatImageItems(body);
|
|
473
485
|
}
|
|
474
486
|
|
|
475
487
|
/** 同步 Responses 生图:主模型由 model 指定,图片工具模型沿用服务端默认。 */
|
|
476
|
-
export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SYNC_GEN, signal }) {
|
|
488
|
+
export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt, size, n = 1, timeoutMs = T_SYNC_GEN, signal, fetchPort = inheritedProviderFetch }) {
|
|
477
489
|
if (Number(n) !== 1) {
|
|
478
490
|
throw new ProviderContractError('Responses 生图当前只支持 n=1', {
|
|
479
491
|
stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
|
|
@@ -482,11 +494,11 @@ export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt
|
|
|
482
494
|
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
483
495
|
const requestSize = applyImageSize({ protocol: 'openai-responses-images', size });
|
|
484
496
|
const { res, body } = await fetchImageJson(`${base}/responses`, {
|
|
485
|
-
method: 'POST', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
497
|
+
method: 'POST', redirect: 'error', headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal),
|
|
486
498
|
body: JSON.stringify({ model, input: prompt, stream: false, store: false, background: false,
|
|
487
499
|
tools: [{ type: 'image_generation', action: 'generate', ...(requestSize ? { size: requestSize } : {}) }],
|
|
488
500
|
tool_choice: { type: 'image_generation' } })
|
|
489
|
-
});
|
|
501
|
+
}, fetchPort);
|
|
490
502
|
if (!res.ok) throw providerError(res.status, body, 'Responses 生图失败', res.headers);
|
|
491
503
|
if (body?.status !== 'completed') throw imageResponseError('Responses 生图未返回同步完成结果;禁止自动重提');
|
|
492
504
|
const output = Array.isArray(body.output) ? body.output : [];
|
|
@@ -510,7 +522,7 @@ export async function openAiResponsesGenerateImage({ key, baseUrl, model, prompt
|
|
|
510
522
|
* 普通 OpenAI 兼容端点维持 GET /models 与字符串 id 返回形状。
|
|
511
523
|
* @returns {Promise<Array<string|{id:string,capabilities?:string[]}>>}
|
|
512
524
|
*/
|
|
513
|
-
export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal }) {
|
|
525
|
+
export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal, fetchPort = inheritedProviderFetch }) {
|
|
514
526
|
const base = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
515
527
|
if (!base) throw new Error('iris: 供应商缺 baseUrl,无法发现模型');
|
|
516
528
|
const dashscope = isDashScopeBaseUrl(base);
|
|
@@ -525,7 +537,7 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
525
537
|
url.searchParams.set('page_no', String(page));
|
|
526
538
|
url.searchParams.set('page_size', '100');
|
|
527
539
|
}
|
|
528
|
-
const res = await
|
|
540
|
+
const res = await fetchPort(url, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
|
|
529
541
|
const body = await readJson(res);
|
|
530
542
|
if (!res.ok) throw providerError(res.status, body, '模型发现失败', res.headers);
|
|
531
543
|
if (!body || body.success === false) throw new ProviderContractError('模型目录响应无效,已保留上次完整目录', {
|
|
@@ -588,14 +600,17 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
|
|
|
588
600
|
* @param {AbortSignal} [signal] 取消传播
|
|
589
601
|
* @returns {Promise<string>} 完整回答文本
|
|
590
602
|
*/
|
|
591
|
-
export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', prompt, imageDataUrl, signal, timeoutMs = T_VISION }) {
|
|
603
|
+
export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', prompt, imageDataUrl, signal, timeoutMs = T_VISION, fetchPort = inheritedProviderFetch }) {
|
|
592
604
|
const base = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
605
|
+
const requestSignal = timeoutSignal(timeoutMs, signal);
|
|
606
|
+
requestSignal.throwIfAborted();
|
|
593
607
|
let res;
|
|
594
608
|
try {
|
|
595
|
-
res = await
|
|
609
|
+
res = await fetchPort(`${base}/chat/completions`, {
|
|
596
610
|
method: 'POST',
|
|
611
|
+
redirect: 'error',
|
|
597
612
|
headers: authHeaders(key),
|
|
598
|
-
signal:
|
|
613
|
+
signal: requestSignal,
|
|
599
614
|
body: JSON.stringify({
|
|
600
615
|
model,
|
|
601
616
|
stream: true,
|
|
@@ -611,10 +626,12 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
|
|
|
611
626
|
})
|
|
612
627
|
});
|
|
613
628
|
} catch (err) {
|
|
629
|
+
requestSignal.throwIfAborted();
|
|
614
630
|
throw networkError(err); // 网络层失败归类为 network
|
|
615
631
|
}
|
|
632
|
+
if (requestSignal.aborted) { await res.body?.cancel(); requestSignal.throwIfAborted(); }
|
|
616
633
|
if (!res.ok) {
|
|
617
|
-
const text = await res.
|
|
634
|
+
const text = await readProviderText(res, { maxBytes: PROVIDER_RESPONSE_LIMITS.json, stage: 'response', acceptance: 'unknown' });
|
|
618
635
|
let body = null;
|
|
619
636
|
try { body = JSON.parse(text); } catch (_) { /* keep null */ }
|
|
620
637
|
throw providerError(res.status, body, '视觉理解失败', res.headers);
|
|
@@ -622,8 +639,8 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
|
|
|
622
639
|
let full = '';
|
|
623
640
|
const decoder = new TextDecoder();
|
|
624
641
|
let buffer = '';
|
|
625
|
-
for await (const chunk of res.
|
|
626
|
-
|
|
642
|
+
for await (const chunk of boundedProviderBody(res, { maxBytes: PROVIDER_RESPONSE_LIMITS.json, stage: 'response', acceptance: 'unknown' })) {
|
|
643
|
+
requestSignal.throwIfAborted();
|
|
627
644
|
buffer += decoder.decode(chunk, { stream: true });
|
|
628
645
|
let idx;
|
|
629
646
|
while ((idx = buffer.indexOf('\n')) >= 0) {
|
|
@@ -641,6 +658,7 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
|
|
|
641
658
|
}
|
|
642
659
|
}
|
|
643
660
|
}
|
|
661
|
+
requestSignal.throwIfAborted();
|
|
644
662
|
return full.trim();
|
|
645
663
|
}
|
|
646
664
|
|
|
@@ -655,15 +673,19 @@ export function extFromUrl(url, fallback = 'bin') {
|
|
|
655
673
|
return fallback;
|
|
656
674
|
}
|
|
657
675
|
|
|
658
|
-
export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal } = {}) {
|
|
659
|
-
const res = await fetch(url, { signal: timeoutSignal(timeoutMs, signal) });
|
|
660
|
-
if (!res.ok) throw new Error(`下载结果失败:HTTP ${res.status}`);
|
|
661
|
-
if (!res.body) throw new Error('下载结果失败:响应没有内容');
|
|
676
|
+
export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal, maxBytes = PROVIDER_RESPONSE_LIMITS.video, fetchPort = inheritedProviderFetch } = {}) {
|
|
662
677
|
ensurePrivateDir(path.dirname(filePath));
|
|
663
678
|
const tmp = privateSibling(filePath, 'download');
|
|
679
|
+
const res = await fetchPort(url, { signal: timeoutSignal(timeoutMs, signal) });
|
|
680
|
+
if (!res.ok) {
|
|
681
|
+
await res.body?.cancel();
|
|
682
|
+
throw Object.assign(new Error(`下载结果失败:HTTP ${res.status}`), { stage: 'download', category: 'provider', httpStatus: res.status, acceptance: 'accepted' });
|
|
683
|
+
}
|
|
684
|
+
if (!res.body) throw new Error('下载结果失败:响应没有内容');
|
|
664
685
|
try {
|
|
665
686
|
const output = fs.createWriteStream(tmp, { flags: 'wx', mode: 0o600 });
|
|
666
|
-
|
|
687
|
+
const closed = new Promise(resolve => output.once('close', resolve));
|
|
688
|
+
await pipeline(Readable.from(boundedProviderBody(res, { maxBytes, stage: 'download', acceptance: 'accepted' })), output).finally(() => closed);
|
|
667
689
|
const bytes = fs.statSync(tmp).size;
|
|
668
690
|
fs.renameSync(tmp, filePath);
|
|
669
691
|
chmodPrivateFile(filePath);
|
|
@@ -679,7 +701,7 @@ export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal
|
|
|
679
701
|
* @param {{key:string, model:string, audioUrl:string}} opts audioUrl 为 oss:// 或公网 URL
|
|
680
702
|
* @returns {Promise<string>} remoteTaskId
|
|
681
703
|
*/
|
|
682
|
-
export async function submitTranscription({ key, baseUrl, model = 'qwen-audio-3.0-asr-flash-filetrans', audioUrl, timeoutMs = T_SUBMIT, signal }) {
|
|
704
|
+
export async function submitTranscription({ key, baseUrl, model = 'qwen-audio-3.0-asr-flash-filetrans', audioUrl, timeoutMs = T_SUBMIT, signal, fetchPort = inheritedProviderFetch }) {
|
|
683
705
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
684
706
|
const headers = { ...authHeaders(key), 'X-DashScope-Async': 'enable' };
|
|
685
707
|
if (/^oss:\/\//.test(audioUrl || '')) headers['X-DashScope-OssResourceResolve'] = 'enable';
|
|
@@ -687,8 +709,9 @@ export async function submitTranscription({ key, baseUrl, model = 'qwen-audio-3.
|
|
|
687
709
|
const input = /^qwen3-asr-.*filetrans/i.test(model || '')
|
|
688
710
|
? { file_url: audioUrl }
|
|
689
711
|
: { file_urls: [audioUrl] };
|
|
690
|
-
const res = await
|
|
712
|
+
const res = await fetchPort(`${apiBase}/services/audio/asr/transcription`, {
|
|
691
713
|
method: 'POST',
|
|
714
|
+
redirect: 'error',
|
|
692
715
|
headers,
|
|
693
716
|
signal: timeoutSignal(timeoutMs, signal),
|
|
694
717
|
body: JSON.stringify({ model, input })
|
|
@@ -719,9 +742,9 @@ export function transcriptionText(payload) {
|
|
|
719
742
|
}
|
|
720
743
|
|
|
721
744
|
/** 轮询 ASR 任务;正式结果位于 results[].transcription_url 指向的 JSON。 */
|
|
722
|
-
export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal }) {
|
|
745
|
+
export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal, fetchPort = inheritedProviderFetch }) {
|
|
723
746
|
const apiBase = dashscopeApiBase(baseUrl);
|
|
724
|
-
const res = await
|
|
747
|
+
const res = await fetchPort(`${apiBase}/tasks/${encodeURIComponent(remoteTaskId)}`, {
|
|
725
748
|
headers: authHeaders(key),
|
|
726
749
|
signal: timeoutSignal(timeoutMs, signal)
|
|
727
750
|
});
|
|
@@ -742,8 +765,11 @@ export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeou
|
|
|
742
765
|
if (typeof result.transcription_text === 'string') text += result.transcription_text;
|
|
743
766
|
else if (typeof result.text === 'string') text += result.text;
|
|
744
767
|
if (status === 'SUCCEEDED' && result.subtask_status !== 'FAILED' && typeof result.transcription_url === 'string') {
|
|
745
|
-
const transcriptRes = await
|
|
746
|
-
if (!transcriptRes.ok)
|
|
768
|
+
const transcriptRes = await fetchPort(result.transcription_url, { signal: timeoutSignal(timeoutMs, signal) });
|
|
769
|
+
if (!transcriptRes.ok) {
|
|
770
|
+
await transcriptRes.body?.cancel();
|
|
771
|
+
throw new Error(`下载转写结果失败:HTTP ${transcriptRes.status}`);
|
|
772
|
+
}
|
|
747
773
|
const transcriptBody = await readJson(transcriptRes);
|
|
748
774
|
if (!transcriptBody) throw new Error('下载转写结果失败:响应不是 JSON');
|
|
749
775
|
text += transcriptionText(transcriptBody);
|
package/lib/api.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import crypto from 'node:crypto';
|
|
16
|
+
import { pipeline } from 'node:stream/promises';
|
|
16
17
|
import { irisHome, capabilitiesOf, onChange as onConfigChange, providerHealthSnapshot } from './config.js';
|
|
17
18
|
import { mimeOf, mediaUrl } from './media.js';
|
|
18
19
|
import * as tasks from './tasks.js';
|
|
@@ -65,7 +66,7 @@ function providerPublic(p) {
|
|
|
65
66
|
mediaBaseUrl: p.mediaBaseUrl || '',
|
|
66
67
|
enabled: p.enabled !== false,
|
|
67
68
|
...selectMediaProtocol(p),
|
|
68
|
-
apiKeyHint: p.dshProvider ? '由 DSH 管理' : keyHint(p.apiKey),
|
|
69
|
+
apiKeyHint: p.dshProvider ? '由 DSH 管理' : p.apiKeyEnv ? '由环境变量提供' : keyHint(p.apiKey),
|
|
69
70
|
...(p.dshProvider ? { dshProvider: p.dshProvider } : {}),
|
|
70
71
|
capabilities: capabilitiesOf(p),
|
|
71
72
|
// 迁移/推断结果是否与原始声明不同(UI 提示旧配置已推断)
|
|
@@ -445,19 +446,22 @@ function handleWorkbenchPost(req, res, execute, download = false, core) {
|
|
|
445
446
|
|
|
446
447
|
async function serveCoreArtifactMedia(artifactId, method, res, core) {
|
|
447
448
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
449
|
+
await core.streamCoreArtifactMediaForDsh(artifactId, async result => {
|
|
450
|
+
if (res.writableEnded || res.destroyed) return;
|
|
451
|
+
res.writeHead(200, {
|
|
452
|
+
'Content-Type': result.artifact.mediaType,
|
|
453
|
+
'Content-Length': result.artifact.size,
|
|
454
|
+
'Content-Disposition': 'inline',
|
|
455
|
+
'Cache-Control': 'private, max-age=3600',
|
|
456
|
+
'X-Content-Type-Options': 'nosniff',
|
|
457
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
458
|
+
'Referrer-Policy': 'no-referrer'
|
|
459
|
+
});
|
|
460
|
+
if (method === 'HEAD') res.end();
|
|
461
|
+
else await pipeline(result.stream, res);
|
|
462
|
+
}, { headOnly: method === 'HEAD' });
|
|
460
463
|
} catch (_) {
|
|
464
|
+
if (res.headersSent || res.destroyed) { res.destroy(); return; }
|
|
461
465
|
return sendJson(res, 404, { error: 'not found' });
|
|
462
466
|
}
|
|
463
467
|
}
|
|
@@ -567,10 +571,16 @@ function handleCoreTaskRetry(taskId, req, res, core) {
|
|
|
567
571
|
error.statusCode = 400;
|
|
568
572
|
throw error;
|
|
569
573
|
}
|
|
574
|
+
if (body.video_mode !== undefined && ['img_data_url', 'first_frame_path', 'audio_path'].some(field => Object.hasOwn(body, field))) {
|
|
575
|
+
throw Object.assign(new Error('DSH 视频重试仅提供明确的文生入口;图生/数字人视频请通过视频工具或 CLI 重新提供首帧和音频'), {
|
|
576
|
+
code: 'IRIS_COMMAND_INPUT_INVALID'
|
|
577
|
+
});
|
|
578
|
+
}
|
|
570
579
|
const prompt = String(body.prompt || '');
|
|
571
580
|
const result = await core.retryProviderTaskForDsh({
|
|
572
581
|
taskId,
|
|
573
582
|
prompt,
|
|
583
|
+
...(body.video_mode !== undefined ? { videoMode: body.video_mode } : {}),
|
|
574
584
|
...(body.source_artifact_id !== undefined ? { sourceArtifactId: body.source_artifact_id } : {}),
|
|
575
585
|
...(body.text !== undefined ? { text: String(body.text) } : {}),
|
|
576
586
|
...(body.voice !== undefined ? { voice: String(body.voice) } : {}),
|