@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/plugins.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Experimental v0 public plugin author/embedding boundary. No automatic discovery or installation.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createPluginRegistry } from './plugin-registry.js';
|
|
6
|
+
import { createApplicationRuntime } from './application-runtime.js';
|
|
7
|
+
import { definePlugin, pluginFail, pluginFields } from './plugin-contract.js';
|
|
8
|
+
import { createCoreCapabilityCatalog } from './core-capability-catalog.js';
|
|
9
|
+
import { pinPluginWebAssets, registerPluginWebAssets } from './plugin-web-assets.js';
|
|
10
|
+
import { createLocalReadApi } from './local-api.js';
|
|
11
|
+
|
|
12
|
+
export { createCoreCapabilityCatalog } from './core-capability-catalog.js';
|
|
13
|
+
|
|
14
|
+
export { definePlugin, PluginContractError, PLUGIN_CONTRACT_VERSION, PLUGIN_KINDS, PLUGIN_CORE_COMMANDS,
|
|
15
|
+
checkedPluginDependency, comparePluginVersions } from './plugin-contract.js';
|
|
16
|
+
export { defineProviderAdapter, ProviderContractError, normalizeMediaEndpoint } from './provider-extensions.js';
|
|
17
|
+
export { PROVIDER_FACTORY_CONTEXT_VERSION } from './provider-context.js';
|
|
18
|
+
|
|
19
|
+
export function createCorePluginRegistry({ plugins = [], coreVersion } = {}) {
|
|
20
|
+
coreVersion ??= JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
21
|
+
return createPluginRegistry({ plugins, coreVersion });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Importing a module executes trusted code. Compatibility gates activate, not module evaluation. */
|
|
25
|
+
export async function loadCorePluginRegistry({ entries = [], coreVersion } = {}) {
|
|
26
|
+
if (!Array.isArray(entries)) pluginFail('IRIS_PLUGIN_INVALID', '入口须为明确文件列表');
|
|
27
|
+
const plugins = [], assets = new Map();
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (typeof entry !== 'string' || !path.isAbsolute(entry) || !/\.m?js$/.test(entry) || !fs.statSync(entry).isFile()) pluginFail('IRIS_PLUGIN_INVALID', '插件入口须为 JS/MJS 文件绝对路径');
|
|
30
|
+
const file = fs.realpathSync(entry), definition = definePlugin((await import(pathToFileURL(file).href)).default);
|
|
31
|
+
for (const [id, files] of pinPluginWebAssets(definition, file)) assets.set(id, files);
|
|
32
|
+
plugins.push(definition);
|
|
33
|
+
}
|
|
34
|
+
const registry = await createCorePluginRegistry({ plugins, coreVersion });
|
|
35
|
+
registerPluginWebAssets(registry, assets);
|
|
36
|
+
return registry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Trusted Core assembly. Grants are captured once; individual calls cannot enlarge them. */
|
|
40
|
+
export function createCorePluginHost({ dataRoot, registry, allowedOperations = [] } = {}) {
|
|
41
|
+
const selection = createCoreCapabilityCatalog({ registry }).selectOperations(allowedOperations);
|
|
42
|
+
const grants = Object.freeze(selection.map(item => item.id)), application = createApplicationRuntime({ dataRoot });
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
call: (id, input, { dependency, signal } = {}) => application.executePluginOperation(registry, id, input,
|
|
45
|
+
{ allowedOperations: grants, dependency, signal }),
|
|
46
|
+
snapshot: () => application.snapshot(),
|
|
47
|
+
dispose: () => application.dispose()
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Public, bounded Panel assembly. Media remains read-only; Provider editing is an explicit owner gate. */
|
|
52
|
+
export function createCorePluginWorkbench(options) {
|
|
53
|
+
if (!pluginFields(options, ['dataRoot', 'registry', 'panelContributions', 'panelManifestPath', 'providerConfigPath', 'allowProviderManagement', 'allowedOperations', 'connectionStore', 'allowMcp', 'workbenchAuth'], ['dataRoot'])) {
|
|
54
|
+
pluginFail('IRIS_PLUGIN_WEB_INVALID', '工作台只接受明确的数据根、注册器、Panel 选择及供应商管理设置');
|
|
55
|
+
}
|
|
56
|
+
const { dataRoot, registry: pluginRegistry, panelContributions, panelManifestPath, providerConfigPath, allowProviderManagement = false, allowedOperations, connectionStore, allowMcp = false, workbenchAuth } = options;
|
|
57
|
+
return createLocalReadApi({ dataRoot, serveWorkbench: true, pluginRegistry, panelContributions, panelManifestPath, providerConfigPath, allowProviderManagement,
|
|
58
|
+
allowedOperations, connectionStore, allowConnectionManagement: connectionStore !== undefined, allowMcp, workbenchAuth });
|
|
59
|
+
}
|
package/lib/provider-adapter.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
providerErrorRecord,
|
|
14
14
|
resultFromThrown
|
|
15
15
|
} from './provider-contract.js';
|
|
16
|
+
import { checkedPluginDependency } from './plugin-contract.js';
|
|
16
17
|
|
|
17
18
|
export const PROVIDER_ADAPTER_CONTRACT_VERSION = 0;
|
|
18
19
|
|
|
@@ -72,7 +73,7 @@ export function defineProviderAdapter(input) {
|
|
|
72
73
|
if (!isPlainObject(input)) throw new TypeError('Provider Adapter 必须是对象');
|
|
73
74
|
const allowed = new Set([
|
|
74
75
|
'contractVersion', 'id', 'protocol', 'capabilities', 'operations', 'unsupported',
|
|
75
|
-
'prepareInput', 'unsupportedPreparation'
|
|
76
|
+
'prepareInput', 'unsupportedPreparation', 'pluginDependency'
|
|
76
77
|
]);
|
|
77
78
|
for (const key of Object.keys(input)) {
|
|
78
79
|
if (!allowed.has(key)) throw new Error('Provider Adapter 不允许顶层字段:' + key);
|
|
@@ -129,6 +130,8 @@ export function defineProviderAdapter(input) {
|
|
|
129
130
|
}
|
|
130
131
|
const unsupportedPreparation = input.prepareInput ? undefined
|
|
131
132
|
: normalizedReason(input.unsupportedPreparation, '协议未实现临时输入上传');
|
|
133
|
+
const pluginDependency = input.pluginDependency === undefined ? undefined : checkedPluginDependency(input.pluginDependency);
|
|
134
|
+
if (pluginDependency && (pluginDependency.kind !== 'providers' || pluginDependency.contributionId !== protocol)) throw new TypeError('Provider Adapter 的插件依赖与协议不一致');
|
|
132
135
|
|
|
133
136
|
return Object.freeze({
|
|
134
137
|
contractVersion,
|
|
@@ -137,6 +140,7 @@ export function defineProviderAdapter(input) {
|
|
|
137
140
|
capabilities: Object.freeze(capabilities),
|
|
138
141
|
operations: Object.freeze(operations),
|
|
139
142
|
unsupported: Object.freeze(unsupported),
|
|
143
|
+
...(pluginDependency ? { pluginDependency } : {}),
|
|
140
144
|
prepareInput: input.prepareInput,
|
|
141
145
|
unsupportedPreparation
|
|
142
146
|
});
|
|
@@ -192,7 +196,7 @@ export function providerAdapterSnapshot(adapter) {
|
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
/** 提交前的可选上传能力;不创建 Task,也不属于六个生命周期操作。 */
|
|
195
|
-
export async function prepareProviderInput(adapter, input = {}) {
|
|
199
|
+
export async function prepareProviderInput(adapter, input = {}, operationContext = {}) {
|
|
196
200
|
if (typeof adapter?.prepareInput !== 'function') {
|
|
197
201
|
const error = new ProviderContractError(adapter?.unsupportedPreparation || '协议未实现临时输入上传', {
|
|
198
202
|
stage: 'upload', category: 'invalid_request', acceptance: 'not_accepted',
|
|
@@ -202,7 +206,7 @@ export async function prepareProviderInput(adapter, input = {}) {
|
|
|
202
206
|
throw error;
|
|
203
207
|
}
|
|
204
208
|
try {
|
|
205
|
-
const value = await adapter.prepareInput(input);
|
|
209
|
+
const value = await adapter.prepareInput(input, operationContext);
|
|
206
210
|
if (typeof value?.url !== 'string' || !value.url.trim()) {
|
|
207
211
|
throw new ProviderContractError('Provider 输入准备必须返回非空 url', {
|
|
208
212
|
stage: 'upload', category: 'protocol', acceptance: 'not_accepted'
|
package/lib/provider-adapters.js
CHANGED
|
@@ -10,10 +10,17 @@ import * as defaultTransport from './adapters.js';
|
|
|
10
10
|
import { ProviderContractError, providerErrorRecord } from './provider-contract.js';
|
|
11
11
|
import { writePrivateFile } from './private-storage.js';
|
|
12
12
|
import { defineProviderAdapter } from './provider-adapter.js';
|
|
13
|
-
import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError, supportsImageEdit, dashscopeTtsInputIssue } from './provider-protocol.js';
|
|
13
|
+
import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError, supportsImageEdit, dashscopeTtsInputIssue, BUILTIN_PROVIDER_PROTOCOLS } from './provider-protocol.js';
|
|
14
14
|
import { modelRateLimit, reportModelRateLimit, isFreeQuotaExhausted, recordModelAvailability } from './provider-health.js';
|
|
15
15
|
import { modelUnavailable, modelUnavailableReason, modelAvailabilityBinding } from './model-directory.js';
|
|
16
16
|
import { prepareVisionImage, visionInputLimits } from './vision-image-input.js';
|
|
17
|
+
import { createComfyUiProviderAdapter } from './comfyui-adapter.js';
|
|
18
|
+
import { PROVIDER_RESPONSE_LIMITS } from './provider-response.js';
|
|
19
|
+
import { resolveProviderCredential, assertDiscoveryCredentials } from './provider-credentials.js';
|
|
20
|
+
import { assertProviderNetwork } from './provider-network.js';
|
|
21
|
+
import { providerNetworkRequirement, sameNetworkRequirement, networkPolicyError } from './network-policy.js';
|
|
22
|
+
import { createProviderTransport } from './provider-transport.js';
|
|
23
|
+
import { hasProviderContext } from './provider-context.js';
|
|
17
24
|
|
|
18
25
|
const AMBIGUOUS_4XX = new Set([408, 409, 425, 499]);
|
|
19
26
|
|
|
@@ -26,7 +33,7 @@ function providerIdentity(provider) {
|
|
|
26
33
|
return { id, key, baseUrl };
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
function mappedCategory(error, status) {
|
|
36
|
+
function mappedCategory(error, status, stage) {
|
|
30
37
|
const raw = String(error?.category || '').toLowerCase();
|
|
31
38
|
if (error?.name === 'AbortError' || error?.code === 'ABORT_ERR') return 'aborted';
|
|
32
39
|
if (error?.name === 'TimeoutError' || raw === 'timeout'
|
|
@@ -37,7 +44,10 @@ function mappedCategory(error, status) {
|
|
|
37
44
|
if (status === 429 || raw === 'rate_limit') return 'rate_limit';
|
|
38
45
|
if (raw === 'invalid_parameter' || raw === 'invalid_request') return 'invalid_request';
|
|
39
46
|
if (raw === 'protocol') return 'protocol';
|
|
47
|
+
if (raw === 'local_io') return 'local_io';
|
|
40
48
|
if (raw === 'network' || error instanceof TypeError) return 'network';
|
|
49
|
+
if (!status && ['download', 'persist'].includes(stage)
|
|
50
|
+
&& ['ENOSPC', 'EDQUOT', 'EACCES', 'EPERM', 'ENOENT', 'EEXIST', 'EROFS', 'EIO'].includes(error?.code)) return 'local_io';
|
|
41
51
|
if (raw === 'server' || status >= 500) return 'provider';
|
|
42
52
|
if (!status && /未返回|响应|协议|结构|json/i.test(String(error?.message || ''))) return 'protocol';
|
|
43
53
|
return 'provider';
|
|
@@ -50,7 +60,7 @@ export function mapProviderAdapterError(error, context = {}) {
|
|
|
50
60
|
? statusValue
|
|
51
61
|
: undefined;
|
|
52
62
|
const stage = String(context.stage || error?.stage || 'submit');
|
|
53
|
-
const category = mappedCategory(error, httpStatus);
|
|
63
|
+
const category = mappedCategory(error, httpStatus, stage);
|
|
54
64
|
let acceptance = context.acceptance || error?.acceptance;
|
|
55
65
|
if (stage === 'validate' || stage === 'prepare' || stage === 'upload') {
|
|
56
66
|
acceptance = 'not_accepted';
|
|
@@ -117,8 +127,16 @@ function openAiImageArtifacts(items) {
|
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
function writeInlineArtifact(artifact, targetPath) {
|
|
130
|
+
const maxBytes = String(artifact.mediaType || '').startsWith('image/') ? PROVIDER_RESPONSE_LIMITS.image
|
|
131
|
+
: artifact.mediaType === 'text/plain' ? PROVIDER_RESPONSE_LIMITS.transcript : PROVIDER_RESPONSE_LIMITS.audio;
|
|
132
|
+
if (typeof artifact.data === 'string' && artifact.data.length > Math.ceil(maxBytes / 3) * 4) {
|
|
133
|
+
throw new ProviderContractError('内嵌媒体超过允许的大小', { stage: 'download', category: 'protocol', acceptance: 'accepted' });
|
|
134
|
+
}
|
|
120
135
|
const data = String(artifact?.data || '').trim();
|
|
121
136
|
const body = Buffer.from(data, 'base64');
|
|
137
|
+
if (body.length > maxBytes) {
|
|
138
|
+
throw new ProviderContractError('内嵌媒体超过允许的大小', { stage: 'download', category: 'protocol', acceptance: 'accepted' });
|
|
139
|
+
}
|
|
122
140
|
if (!data || body.length === 0 || body.toString('base64') !== data) {
|
|
123
141
|
throw new ProviderContractError('inline-base64 媒体数据无效', {
|
|
124
142
|
stage: 'download', category: 'protocol', acceptance: 'accepted'
|
|
@@ -262,6 +280,8 @@ function operationsForDashScope(provider, transport) {
|
|
|
262
280
|
return {
|
|
263
281
|
bytes: await transport.downloadTo(artifact.url, targetPath, {
|
|
264
282
|
signal: request.signal,
|
|
283
|
+
maxBytes: { image: PROVIDER_RESPONSE_LIMITS.image, video: PROVIDER_RESPONSE_LIMITS.video,
|
|
284
|
+
tts: PROVIDER_RESPONSE_LIMITS.audio, transcribe: PROVIDER_RESPONSE_LIMITS.transcript }[request.capability] || PROVIDER_RESPONSE_LIMITS.video,
|
|
265
285
|
...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
|
|
266
286
|
})
|
|
267
287
|
};
|
|
@@ -278,8 +298,8 @@ export function createDashScopeProviderAdapter(provider, { transport = defaultTr
|
|
|
278
298
|
protocol: 'dashscope',
|
|
279
299
|
capabilities: ['image', 'video', 'tts', 'transcribe'],
|
|
280
300
|
operations: operationsForDashScope(provider, transport),
|
|
281
|
-
async prepareInput({ model, filePath, signal, timeoutMs } = {}) {
|
|
282
|
-
return { url: await transport.uploadTempFile({ key, baseUrl, model, filePath, signal, timeoutMs }) };
|
|
301
|
+
async prepareInput({ model, filePath, bytes, filename, signal, timeoutMs } = {}) {
|
|
302
|
+
return { url: await transport.uploadTempFile({ key, baseUrl, model, filePath, ...(bytes !== undefined ? { bytes, filename } : {}), signal, timeoutMs }) };
|
|
283
303
|
},
|
|
284
304
|
unsupported: {
|
|
285
305
|
cancel: '当前 DashScope 媒体协议未提供经过验证的远端取消实现'
|
|
@@ -345,6 +365,7 @@ function operationsForOpenAiImages(provider, transport, protocol = 'openai-image
|
|
|
345
365
|
return {
|
|
346
366
|
bytes: await transport.downloadTo(artifact.url, targetPath, {
|
|
347
367
|
signal: request.signal,
|
|
368
|
+
maxBytes: PROVIDER_RESPONSE_LIMITS.image,
|
|
348
369
|
...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
|
|
349
370
|
})
|
|
350
371
|
};
|
|
@@ -418,38 +439,78 @@ export function createGeminiWebMusicProviderAdapter(provider, { transport = defa
|
|
|
418
439
|
});
|
|
419
440
|
}
|
|
420
441
|
|
|
421
|
-
const PROTOCOL_FACTORIES =
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
442
|
+
const PROTOCOL_FACTORIES = Object.freeze({
|
|
443
|
+
'gemini-web-music': createGeminiWebMusicProviderAdapter,
|
|
444
|
+
dashscope: createDashScopeProviderAdapter,
|
|
445
|
+
'openai-images': createOpenAiImagesProviderAdapter,
|
|
446
|
+
'openai-chat-images': createOpenAiChatImagesProviderAdapter,
|
|
447
|
+
'openai-responses-images': createOpenAiResponsesImagesProviderAdapter,
|
|
448
|
+
comfyui: createComfyUiProviderAdapter
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
export function builtinProviderExtension() {
|
|
452
|
+
return { contractVersion: 0, id: 'iris.builtin-media', version: '0.1.0',
|
|
453
|
+
protocols: BUILTIN_PROVIDER_PROTOCOLS.map(descriptor => ({ ...descriptor, createAdapter: PROTOCOL_FACTORIES[descriptor.id] })) };
|
|
454
|
+
}
|
|
428
455
|
|
|
429
456
|
export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
457
|
+
const network = options.network === undefined ? undefined : assertProviderNetwork(options.network);
|
|
458
|
+
const requirement = providerNetworkRequirement(provider);
|
|
459
|
+
if (requirement && (!network || !sameNetworkRequirement(requirement, network.networkRequirement) || network.providerId !== provider.id)) {
|
|
460
|
+
throw networkPolicyError('IRIS_NETWORK_CONTEXT_REQUIRED', '此供应商需要匹配的显式网络上下文,不会静默使用默认出口');
|
|
461
|
+
}
|
|
462
|
+
if (network && (options.transport !== undefined || options.fetchPort !== undefined)) {
|
|
463
|
+
throw new TypeError('network 不能与显式 transport 或 fetchPort 同时配置');
|
|
464
|
+
}
|
|
465
|
+
const factoryOptions = network ? { ...options, transport: createProviderTransport(network.fetch), fetchPort: network.fetch } : options;
|
|
466
|
+
provider = resolveProviderCredential(provider);
|
|
430
467
|
const selection = options.protocol
|
|
431
468
|
? selectMediaProtocol({ ...provider, mediaProtocol: options.protocol, protocolInferred: false })
|
|
432
469
|
: selectMediaProtocol(provider);
|
|
433
|
-
const factory =
|
|
434
|
-
if (!factory) throw unsupportedProtocolError(selection.mediaProtocol);
|
|
435
|
-
const adapter = factory(provider,
|
|
470
|
+
const factory = Object.hasOwn(PROTOCOL_FACTORIES, selection.mediaProtocol) ? PROTOCOL_FACTORIES[selection.mediaProtocol] : null;
|
|
471
|
+
if (!options.registry && !factory) throw unsupportedProtocolError(selection.mediaProtocol);
|
|
472
|
+
const adapter = options.registry ? options.registry.createAdapter(selection.mediaProtocol, provider, factoryOptions) : factory(provider, factoryOptions);
|
|
436
473
|
const operations = { ...adapter.operations };
|
|
474
|
+
const key = providerApiKey(provider);
|
|
475
|
+
const scrub = value => typeof value === 'string' && key ? value.split(key).join('[REDACTED]') : value;
|
|
476
|
+
if (operations.discover) {
|
|
477
|
+
const discover = operations.discover;
|
|
478
|
+
operations.discover = async (...args) => {
|
|
479
|
+
const result = await discover(...args);
|
|
480
|
+
assertDiscoveryCredentials(result, key);
|
|
481
|
+
return result;
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
operations.mapError = (error, context) => {
|
|
485
|
+
let mapped;
|
|
486
|
+
try { mapped = adapter.operations.mapError(error, context); } catch (failure) { mapped = failure; }
|
|
487
|
+
mapped ||= error;
|
|
488
|
+
const defaults = {};
|
|
489
|
+
for (const field of ['stage', 'category', 'acceptance', 'retryable']) {
|
|
490
|
+
if (mapped?.[field] === undefined) defaults[field] = context?.[field];
|
|
491
|
+
}
|
|
492
|
+
return providerErrorRecord(mapped, {
|
|
493
|
+
...defaults,
|
|
494
|
+
safeMessage: scrub(String(mapped?.safeMessage || mapped?.message || mapped)),
|
|
495
|
+
providerCode: scrub(mapped?.providerCode ?? mapped?.code)
|
|
496
|
+
});
|
|
497
|
+
};
|
|
437
498
|
for (const name of ['submit', 'poll']) {
|
|
438
499
|
if (!operations[name]) continue;
|
|
439
500
|
const run = operations[name];
|
|
440
|
-
operations[name] = async request => {
|
|
501
|
+
operations[name] = async (request, context) => {
|
|
441
502
|
const modelId = request.model || options.modelId;
|
|
442
503
|
const report = error => reportModelRateLimit(provider, modelId, {
|
|
443
504
|
httpStatus: error.httpStatus ?? error.status, category: error.category,
|
|
444
|
-
code: error.code, providerCode: error.providerCode, message: error.safeMessage || error.message,
|
|
505
|
+
code: scrub(error.code), providerCode: scrub(error.providerCode), message: scrub(error.safeMessage || error.message),
|
|
445
506
|
retryAfterMs: error.retryAfterMs,
|
|
446
507
|
source: options.allowRateLimited ? 'probe' : 'task'
|
|
447
508
|
}, options.onRateLimit, request.capability);
|
|
448
509
|
const availability = result => {
|
|
449
510
|
if (name !== 'submit') return;
|
|
450
511
|
const event = { ok: result.ok === true, category: result.category,
|
|
451
|
-
httpStatus: result.httpStatus ?? result.status, providerCode: result.providerCode || result.code,
|
|
452
|
-
note: result.safeMessage || result.message,
|
|
512
|
+
httpStatus: result.httpStatus ?? result.status, providerCode: scrub(result.providerCode || result.code),
|
|
513
|
+
note: scrub(result.safeMessage || result.message),
|
|
453
514
|
stage: 'submit', source: options.allowRateLimited ? 'probe' : 'task',
|
|
454
515
|
binding: modelAvailabilityBinding(provider, modelId, capability) };
|
|
455
516
|
if (!recordModelAvailability(provider, modelId, request.capability, event)) return;
|
|
@@ -464,12 +525,12 @@ export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
|
464
525
|
}
|
|
465
526
|
if (name === 'submit' && !options.allowRateLimited && modelRateLimit(provider, modelId)) {
|
|
466
527
|
const blocked = modelRateLimit(provider, modelId);
|
|
467
|
-
throw new ProviderContractError(blocked.until ? '该模型正在限流冷却,请稍后使用' : '该模型额度或预算耗尽,请手动实测成功后再使用', {
|
|
528
|
+
if (!(options.deferCooling && blocked.until)) throw new ProviderContractError(blocked.until ? '该模型正在限流冷却,请稍后使用' : '该模型额度或预算耗尽,请手动实测成功后再使用', {
|
|
468
529
|
stage: 'validate', category: 'rate_limit', acceptance: 'not_accepted'
|
|
469
530
|
});
|
|
470
531
|
}
|
|
471
532
|
try {
|
|
472
|
-
const result = await run(request);
|
|
533
|
+
const result = await run(request, context);
|
|
473
534
|
if (result.error) report(result.error);
|
|
474
535
|
availability(result.error || { ok: ['completed', 'accepted'].includes(result.kind) });
|
|
475
536
|
return result;
|
|
@@ -480,5 +541,14 @@ export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
|
480
541
|
}
|
|
481
542
|
};
|
|
482
543
|
}
|
|
483
|
-
|
|
544
|
+
if (network && !hasProviderContext(adapter)) {
|
|
545
|
+
for (const name of Object.keys(operations)) {
|
|
546
|
+
if (name === 'mapError') continue;
|
|
547
|
+
const operation = operations[name];
|
|
548
|
+
operations[name] = (...args) => network.run(() => operation(...args), { ...args[1], operation: name });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return Object.freeze({ ...adapter, ...(requirement ? { networkRequirement: requirement } : {}), operations: Object.freeze(operations),
|
|
552
|
+
...(options.deferCooling ? { schedulingReadyAt: modelId => Date.parse(modelRateLimit(provider, modelId)?.until || '') || 0 } : {}),
|
|
553
|
+
...(network && !hasProviderContext(adapter) && adapter.prepareInput ? { prepareInput: (...args) => network.run(() => adapter.prepareInput(...args), { ...args[1], operation: 'prepareInput' }) } : {}) });
|
|
484
554
|
}
|
package/lib/provider-catalog.js
CHANGED
|
@@ -5,11 +5,15 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { CAPABILITIES } from './capability.js';
|
|
7
7
|
import { modelPool, functionModelPool, functionPolicy, orderedModels, providerModels, parseModelRef, resolveModelRef, resolvePoolModel, selectionReasonForModel } from './models.js';
|
|
8
|
-
import {
|
|
8
|
+
import { selectMediaProtocol, providerForImageModel, providerForMusicModel, providerMediaBaseUrl, isConfiguredProvider, providerProtocolDescriptor } from './provider-protocol.js';
|
|
9
9
|
import { redactProviderMessage } from './provider-contract.js';
|
|
10
10
|
import { modelRateLimit } from './provider-health.js';
|
|
11
11
|
import { normalizeVisionInputLimits } from './vision-image-input.js';
|
|
12
12
|
import { modelDirectorySummary, modelUnavailableReason, validateModelDirectory } from './model-directory.js';
|
|
13
|
+
import { checkedComfyWorkflow, comfyWorkflowDigest } from './comfyui-workflow.js';
|
|
14
|
+
import { assertPluginDependencyMatches } from './plugin-contract.js';
|
|
15
|
+
import { validateProviderCredential, assertProviderCredential, providerCredentialStatus, createProviderSecretScrubber } from './provider-credentials.js';
|
|
16
|
+
import { decodeProviderConfig } from './provider-config-format.js';
|
|
13
17
|
|
|
14
18
|
export class ProviderCatalogError extends Error {
|
|
15
19
|
constructor(code, message) {
|
|
@@ -44,6 +48,7 @@ export function loadProviderCatalog(configPath) {
|
|
|
44
48
|
let parsed;
|
|
45
49
|
try { parsed = JSON.parse(source); }
|
|
46
50
|
catch (_) { fail('IRIS_PROVIDER_CONFIG_INVALID', 'provider-config 不是有效 JSON'); }
|
|
51
|
+
parsed = decodeProviderConfig(parsed);
|
|
47
52
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
|
|
48
53
|
|| !Array.isArray(parsed.providers)
|
|
49
54
|
|| (parsed.assignments !== undefined && (!parsed.assignments || typeof parsed.assignments !== 'object'
|
|
@@ -51,7 +56,11 @@ export function loadProviderCatalog(configPath) {
|
|
|
51
56
|
fail('IRIS_PROVIDER_CONFIG_INVALID', 'provider-config 结构无效');
|
|
52
57
|
}
|
|
53
58
|
for (const [capability, assignment] of Object.entries(parsed.assignments || {})) functionPolicy(assignment, capability);
|
|
54
|
-
for (const provider of parsed.providers)
|
|
59
|
+
for (const provider of parsed.providers) {
|
|
60
|
+
validateProviderCredential(provider);
|
|
61
|
+
validateModelDirectory(provider.modelDirectory);
|
|
62
|
+
for (const model of provider.models || []) if (model?.comfyWorkflow !== undefined) checkedComfyWorkflow(model.comfyWorkflow);
|
|
63
|
+
}
|
|
55
64
|
return parsed;
|
|
56
65
|
}
|
|
57
66
|
|
|
@@ -62,11 +71,16 @@ const CANDIDATE_CAP_LABEL = Object.freeze({
|
|
|
62
71
|
});
|
|
63
72
|
|
|
64
73
|
/** 与 DSH 共用功能策略;CLI 不附加宿主资源。 */
|
|
65
|
-
function candidatesFromCatalog(catalog, requestedModelRef, capability) {
|
|
74
|
+
function candidatesFromCatalog(catalog, requestedModelRef, capability, registry, { deferCooling = false } = {}) {
|
|
66
75
|
const label = CANDIDATE_CAP_LABEL[capability];
|
|
67
76
|
if (!label) fail('IRIS_PROVIDER_TASK_OBSERVE_UNSUPPORTED', '当前 Headless 候选链只开放图片、视频、语音与转写能力');
|
|
77
|
+
const requestedProvider = parseModelRef(String(requestedModelRef || '').trim())?.providerId;
|
|
78
|
+
if (requestedProvider) {
|
|
79
|
+
const provider = catalog.providers.find(item => item.id === requestedProvider && item.enabled !== false);
|
|
80
|
+
if (provider) assertProviderCredential(provider);
|
|
81
|
+
}
|
|
68
82
|
const providers = catalog.providers.filter(isConfiguredProvider);
|
|
69
|
-
const configuredPool = functionModelPool(providers, capability, catalog.assignments?.[capability]);
|
|
83
|
+
const configuredPool = functionModelPool(providers, capability, catalog.assignments?.[capability], { registry });
|
|
70
84
|
const pool = orderedModels(configuredPool, capability, catalog.assignments?.[capability], { includeRateLimited: true, includeUnavailable: true });
|
|
71
85
|
const requested = String(requestedModelRef || '').trim();
|
|
72
86
|
if (requested) {
|
|
@@ -76,7 +90,7 @@ function candidatesFromCatalog(catalog, requestedModelRef, capability) {
|
|
|
76
90
|
if (!exact) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `指定模型未分配给${label}功能、未启用或没有所需能力`);
|
|
77
91
|
const provider = providers.find((item) => item.id === exact.providerId);
|
|
78
92
|
if (exact.unavailableCapabilities?.includes(capability)) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', modelUnavailableReason());
|
|
79
|
-
if (exact.rateLimited) fail('IRIS_PROVIDER_MODEL_RATE_LIMITED', modelRateLimit(provider, exact.id)?.until
|
|
93
|
+
if (exact.rateLimited && !(deferCooling && modelRateLimit(provider, exact.id)?.until)) fail('IRIS_PROVIDER_MODEL_RATE_LIMITED', modelRateLimit(provider, exact.id)?.until
|
|
80
94
|
? '指定模型正在限流冷却,请稍后使用或手动实测'
|
|
81
95
|
: '指定模型额度或预算耗尽,请手动实测成功后再使用');
|
|
82
96
|
return Object.freeze([Object.freeze({
|
|
@@ -85,8 +99,14 @@ function candidatesFromCatalog(catalog, requestedModelRef, capability) {
|
|
|
85
99
|
})]);
|
|
86
100
|
}
|
|
87
101
|
const assignment = catalog.assignments?.[capability];
|
|
88
|
-
const ordered = pool.filter(model => !model.rateLimited &&
|
|
89
|
-
|
|
102
|
+
const ordered = pool.filter(model => (!model.rateLimited || deferCooling && modelRateLimit(providers.find(provider => provider.id === model.providerId), model.id)?.until)
|
|
103
|
+
&& !model.unavailableCapabilities?.includes(capability));
|
|
104
|
+
if (!ordered.length) {
|
|
105
|
+
const missing = catalog.providers.find(provider => provider.enabled !== false && provider.auth !== 'none' && provider.apiKeyEnv
|
|
106
|
+
&& providerCredentialStatus(provider) !== 'ready' && providerModels(provider).some(model => model.capabilities.includes(capability)));
|
|
107
|
+
if (missing) assertProviderCredential(missing);
|
|
108
|
+
fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `请检查${label}功能的启用方式、已确认模型、连接和协议;当前没有可用候选`);
|
|
109
|
+
}
|
|
90
110
|
return Object.freeze(ordered.map((item) => Object.freeze({
|
|
91
111
|
provider: providers.find((provider) => provider.id === item.providerId),
|
|
92
112
|
model: item.id,
|
|
@@ -95,37 +115,38 @@ function candidatesFromCatalog(catalog, requestedModelRef, capability) {
|
|
|
95
115
|
})));
|
|
96
116
|
}
|
|
97
117
|
|
|
98
|
-
export function imageCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
99
|
-
return Object.freeze(candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.IMAGE).map(route =>
|
|
100
|
-
Object.freeze({ ...route, provider: providerForImageModel(route.provider, route.model) })));
|
|
118
|
+
export function imageCandidatesFromCatalog(catalog, requestedModelRef = '', registry, options) {
|
|
119
|
+
return Object.freeze(candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.IMAGE, registry, options).map(route =>
|
|
120
|
+
Object.freeze({ ...route, provider: providerForImageModel(route.provider, route.model, registry) })));
|
|
101
121
|
}
|
|
102
122
|
|
|
103
|
-
export function videoCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
104
|
-
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.VIDEO);
|
|
123
|
+
export function videoCandidatesFromCatalog(catalog, requestedModelRef = '', registry, options) {
|
|
124
|
+
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.VIDEO, registry, options);
|
|
105
125
|
}
|
|
106
126
|
|
|
107
|
-
export function ttsCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
108
|
-
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TTS);
|
|
127
|
+
export function ttsCandidatesFromCatalog(catalog, requestedModelRef = '', registry, options) {
|
|
128
|
+
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TTS, registry, options);
|
|
109
129
|
}
|
|
110
130
|
|
|
111
|
-
export function musicCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
112
|
-
return Object.freeze(candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.MUSIC).map(route =>
|
|
113
|
-
Object.freeze({ ...route, provider: providerForMusicModel(route.provider, route.model) })));
|
|
131
|
+
export function musicCandidatesFromCatalog(catalog, requestedModelRef = '', registry) {
|
|
132
|
+
return Object.freeze(candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.MUSIC, registry).map(route =>
|
|
133
|
+
Object.freeze({ ...route, provider: providerForMusicModel(route.provider, route.model, registry) })));
|
|
114
134
|
}
|
|
115
135
|
|
|
116
|
-
export function transcribeCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
117
|
-
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TRANSCRIBE);
|
|
136
|
+
export function transcribeCandidatesFromCatalog(catalog, requestedModelRef = '', registry, options) {
|
|
137
|
+
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TRANSCRIBE, registry, options);
|
|
118
138
|
}
|
|
119
139
|
|
|
120
|
-
export function visionCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
121
|
-
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.VISION);
|
|
140
|
+
export function visionCandidatesFromCatalog(catalog, requestedModelRef = '', registry) {
|
|
141
|
+
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.VISION, registry);
|
|
122
142
|
}
|
|
123
143
|
|
|
124
144
|
/** 只读配置投影;不构造 Adapter、不发现模型、不暴露端点或凭据。 */
|
|
125
145
|
export function providerCatalogSnapshot(catalog) {
|
|
146
|
+
const scrub = createProviderSecretScrubber(catalog.providers);
|
|
126
147
|
const safeLabel = (value) => value
|
|
127
|
-
? redactProviderMessage(String(value).replace(/https?:\/\/[^\s<>"']+/gi, '[URL]')) : '';
|
|
128
|
-
return {
|
|
148
|
+
? redactProviderMessage(String(scrub(value)).replace(/https?:\/\/[^\s<>"']+/gi, '[URL]')) : '';
|
|
149
|
+
return scrub({
|
|
129
150
|
schemaVersion: 1,
|
|
130
151
|
providers: catalog.providers.filter((provider) => provider && typeof provider === 'object').map((provider) => {
|
|
131
152
|
const models = providerModels(provider);
|
|
@@ -140,16 +161,16 @@ export function providerCatalogSnapshot(catalog) {
|
|
|
140
161
|
modelCount: models.length, directory: modelDirectorySummary(provider)
|
|
141
162
|
};
|
|
142
163
|
})
|
|
143
|
-
};
|
|
164
|
+
});
|
|
144
165
|
}
|
|
145
166
|
|
|
146
|
-
export function catalogCapabilitySnapshot(catalog) {
|
|
167
|
+
export function catalogCapabilitySnapshot(catalog, registry) {
|
|
147
168
|
return {
|
|
148
169
|
schemaVersion: 1,
|
|
149
170
|
source: 'configuration',
|
|
150
171
|
capabilities: Object.values(CAPABILITIES).map((capability) => {
|
|
151
172
|
const policy = functionPolicy(catalog.assignments?.[capability], capability);
|
|
152
|
-
const pool = functionModelPool(catalog.providers.filter(isConfiguredProvider), capability, catalog.assignments?.[capability]);
|
|
173
|
+
const pool = functionModelPool(catalog.providers.filter(isConfiguredProvider), capability, catalog.assignments?.[capability], { registry });
|
|
153
174
|
const candidates = orderedModels(pool, capability, catalog.assignments?.[capability]).map((model) => model.ref);
|
|
154
175
|
return { capability, mode: policy.mode, candidates, ...(candidates.length ? {} : { gap: 'no_configured_model' }) };
|
|
155
176
|
})
|
|
@@ -157,21 +178,26 @@ export function catalogCapabilitySnapshot(catalog) {
|
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
/** 非敏感摘要把 Task 绑定到提交时实际使用的媒体端点与协议。 */
|
|
160
|
-
export function providerTaskBinding(provider) {
|
|
181
|
+
export function providerTaskBinding(provider, registry) {
|
|
161
182
|
const baseUrl = providerMediaBaseUrl(provider);
|
|
162
183
|
const protocol = selectMediaProtocol(provider).mediaProtocol;
|
|
163
|
-
let endpoint;
|
|
184
|
+
let endpoint, descriptor;
|
|
164
185
|
try {
|
|
165
186
|
const url = new URL(baseUrl);
|
|
166
187
|
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('unsafe endpoint');
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
descriptor = providerProtocolDescriptor(protocol, registry);
|
|
189
|
+
if (!descriptor) throw new Error('unknown protocol');
|
|
190
|
+
endpoint = descriptor.normalizeEndpoint(baseUrl);
|
|
191
|
+
if (typeof endpoint !== 'string' || !endpoint) throw new Error('invalid normalized endpoint');
|
|
169
192
|
} catch (_) { fail('IRIS_PROVIDER_CONFIG_INVALID', 'Provider 媒体端点或协议无效'); }
|
|
170
|
-
|
|
193
|
+
const identity = protocol === 'comfyui' ? [2, protocol, endpoint, comfyWorkflowDigest(provider.comfyWorkflow)] : [1, protocol, endpoint];
|
|
194
|
+
// Only the new unified plugin contract opts in. Legacy protocols retain their exact historical binding.
|
|
195
|
+
if (descriptor.dependency) identity.push(descriptor.dependency);
|
|
196
|
+
return 'sha256:' + crypto.createHash('sha256').update(JSON.stringify(identity)).digest('hex');
|
|
171
197
|
}
|
|
172
198
|
|
|
173
199
|
/** 恢复只使用 Task 已保存的 Provider/模型;忽略当前 assignment 与 failover 顺序。 */
|
|
174
|
-
export function providerForTaskFromCatalog(catalog, task) {
|
|
200
|
+
export function providerForTaskFromCatalog(catalog, task, registry) {
|
|
175
201
|
const capability = CAPABILITIES[
|
|
176
202
|
task?.capability === 'video' ? 'VIDEO'
|
|
177
203
|
: task?.capability === 'image' ? 'IMAGE'
|
|
@@ -186,11 +212,13 @@ export function providerForTaskFromCatalog(catalog, task) {
|
|
|
186
212
|
const matches = catalog.providers.filter((item) => item && item.id === task.providerId);
|
|
187
213
|
if (matches.length > 1) fail('IRIS_PROVIDER_CONFIG_AMBIGUOUS', 'provider-config 存在重复 Provider ID');
|
|
188
214
|
const configured = matches[0];
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
215
|
+
if (configured && configured.enabled !== false) assertProviderCredential(configured);
|
|
216
|
+
if (!configured || configured.enabled === false || (configured.auth !== 'none' && providerCredentialStatus(configured) !== 'ready')) fail('IRIS_PROVIDER_TASK_PROVIDER_UNAVAILABLE', 'Task 原 Provider 已移除、停用或缺少凭据');
|
|
217
|
+
const provider = task.capability === 'image' ? providerForImageModel(configured, parsed.modelId, registry)
|
|
218
|
+
: task.capability === 'music' ? providerForMusicModel(configured, parsed.modelId, registry) : configured;
|
|
219
|
+
assertPluginDependencyMatches(task.pluginDependency, providerProtocolDescriptor(selectMediaProtocol(provider).mediaProtocol, registry)?.dependency);
|
|
192
220
|
if (!task.providerBinding) fail('IRIS_PROVIDER_TASK_BINDING_MISSING', '旧 Task 缺少提交端点绑定;仅支持只读查看,不猜测恢复配置');
|
|
193
|
-
if (providerTaskBinding(provider) !== task.providerBinding) fail('IRIS_PROVIDER_TASK_BINDING_MISMATCH', 'Task
|
|
221
|
+
if (providerTaskBinding(provider, registry) !== task.providerBinding) fail('IRIS_PROVIDER_TASK_BINDING_MISMATCH', 'Task 提交端点、协议或插件依赖已改变;请恢复原配置和插件版本,不会重新提交');
|
|
194
222
|
const model = modelPool([provider]).find((item) => item.providerId === parsed.providerId && item.id === parsed.modelId && item.capabilities.includes(capability));
|
|
195
223
|
if (!model) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `Task 原模型已移除或不再具备${label}能力`);
|
|
196
224
|
return provider;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Network-enabled catalogs use an outer envelope which older loaders reject.
|
|
2
|
+
import { CATALOG_NETWORK, checkedNetworkRequirement, sameNetworkRequirement } from './network-policy.js';
|
|
3
|
+
|
|
4
|
+
const fail = message => { throw Object.assign(new Error(message), { code: 'IRIS_PROVIDER_CONFIG_INVALID' }); };
|
|
5
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
|
|
7
|
+
function markedCatalog(catalog, requirement) {
|
|
8
|
+
if (!object(catalog) || !Array.isArray(catalog.providers) || catalog.providers.some(provider => !object(provider))) {
|
|
9
|
+
fail('Provider 配置内容无效');
|
|
10
|
+
}
|
|
11
|
+
const ids = new Set();
|
|
12
|
+
for (const provider of catalog.providers) {
|
|
13
|
+
if (typeof provider.id !== 'string' || !provider.id.trim() || provider.id.length > 256 || /[\x00-\x1f\x7f]/.test(provider.id) || ids.has(provider.id)) fail('网络目录需要明确、唯一且不含控制字符的供应商身份');
|
|
14
|
+
ids.add(provider.id);
|
|
15
|
+
}
|
|
16
|
+
const identity = Object.freeze(checkedNetworkRequirement(requirement));
|
|
17
|
+
return { ...catalog, [CATALOG_NETWORK]: identity,
|
|
18
|
+
providers: catalog.providers.map(provider => ({ ...provider, [CATALOG_NETWORK]: identity })) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function decodeProviderConfig(parsed) {
|
|
22
|
+
if (object(parsed) && Object.hasOwn(parsed, 'providers') && Object.hasOwn(parsed, 'networkRequirement')) fail('网络目录不能与旧根 providers 字段混用,拒绝忽略网络要求');
|
|
23
|
+
if (!object(parsed) || Object.hasOwn(parsed, 'providers')) return parsed;
|
|
24
|
+
if (parsed.schemaVersion !== 1 || Object.keys(parsed).length !== 3
|
|
25
|
+
|| !Object.hasOwn(parsed, 'networkRequirement') || !Object.hasOwn(parsed, 'catalog')) {
|
|
26
|
+
fail('Provider 网络配置包装版本或字段无效;请使用兼容版本,原文件不会自动迁移');
|
|
27
|
+
}
|
|
28
|
+
return markedCatalog(parsed.catalog, parsed.networkRequirement);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function encodeProviderConfig(catalog) {
|
|
32
|
+
const requirement = catalog?.[CATALOG_NETWORK];
|
|
33
|
+
if (requirement === undefined) {
|
|
34
|
+
if (catalog?.providers?.some(provider => provider?.[CATALOG_NETWORK] !== undefined)) fail('Provider 网络配置身份丢失,拒绝写回旧格式');
|
|
35
|
+
return catalog;
|
|
36
|
+
}
|
|
37
|
+
const checked = checkedNetworkRequirement(requirement);
|
|
38
|
+
for (const provider of catalog.providers) {
|
|
39
|
+
const identity = provider?.[CATALOG_NETWORK];
|
|
40
|
+
if (identity !== undefined && !sameNetworkRequirement(checked, identity)) fail('Provider 与目录的网络配置身份不一致');
|
|
41
|
+
}
|
|
42
|
+
const { [CATALOG_NETWORK]: _identity, ...contents } = catalog;
|
|
43
|
+
return { schemaVersion: 1, networkRequirement: checked, catalog: contents };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Return a new marked runtime catalog; callers explicitly write a new file. */
|
|
47
|
+
export function wrapProviderCatalog(catalog, requirement) {
|
|
48
|
+
const original = encodeProviderConfig(catalog);
|
|
49
|
+
const contents = catalog?.[CATALOG_NETWORK] === undefined ? original : original.catalog;
|
|
50
|
+
return markedCatalog(JSON.parse(JSON.stringify(contents)), requirement);
|
|
51
|
+
}
|