@mokuyoaxis/dsh-iris 0.3.0-alpha.1 → 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 +63 -1
- package/README.en.md +20 -13
- package/README.md +20 -13
- package/bin/dsh-iris.js +143 -51
- package/docs/AGENT_CORE_QUICKSTART.md +2 -2
- package/docs/AGENT_QUICKSTART.md +1 -1
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +20 -4
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +1 -1
- package/docs/CORE_CONNECTIONS.md +3 -1
- package/docs/CORE_QUICKSTART.md +7 -5
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
- package/docs/CORE_SERVICE.md +9 -5
- package/docs/CORE_SKILLS.md +2 -2
- package/docs/DSH_COMPATIBILITY.md +11 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RUNTIME.md +2 -2
- package/docs/HEADLESS_CLI.md +29 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +2 -2
- package/docs/IMAGE_GENERATION.md +1 -1
- package/docs/LOCAL_API.md +2 -2
- package/docs/LOCAL_API_CONTRACT.md +17 -15
- package/docs/LOCAL_CREATION.md +2 -2
- package/docs/LOCAL_OPERATIONS.md +2 -2
- package/docs/LOCAL_SELECTION.md +1 -1
- package/docs/LOCAL_WEB.md +11 -5
- package/docs/MCP.md +30 -9
- package/docs/MEDIA_SCHEDULING.md +72 -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 +1 -1
- package/docs/PLUGINS.md +9 -5
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +15 -2
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +8 -4
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +13 -7
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +3 -3
- package/docs/WEB_RECOVERY.md +2 -2
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +2 -2
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +2 -2
- package/lib/actions.js +5 -4
- package/lib/adapters.js +58 -41
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +44 -15
- package/lib/application-writer.js +193 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +62 -25
- package/lib/comfyui-adapter.js +3 -2
- package/lib/comfyui-client.js +10 -1
- package/lib/command-service.js +18 -5
- package/lib/config.js +13 -9
- package/lib/configured-image-runtime.js +28 -9
- package/lib/connection-skill.js +9 -0
- package/lib/core-artifact-store.js +68 -26
- package/lib/core-bridge-tools.js +5 -3
- package/lib/core-capability-catalog.js +21 -11
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-service-client.js +6 -2
- package/lib/core-tasks.js +86 -11
- package/lib/creation-request-store.js +8 -5
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +1 -0
- package/lib/flow-store.js +64 -17
- package/lib/image-flow.js +39 -17
- package/lib/index.js +10 -5
- package/lib/local-api-projection.js +6 -2
- package/lib/local-api.js +137 -49
- package/lib/local-creation.js +12 -5
- package/lib/local-operations.js +2 -2
- package/lib/local-request-root.js +2 -2
- package/lib/local-web-assets.js +2 -0
- package/lib/mcp-http.js +82 -9
- package/lib/mcp-server.js +23 -1
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +25 -8
- package/lib/music-transport.js +4 -2
- 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 +83 -38
- package/lib/operation-store.js +6 -6
- package/lib/plugins.d.ts +25 -6
- package/lib/plugins.js +2 -1
- package/lib/provider-adapter.js +2 -2
- package/lib/provider-adapters.js +37 -5
- package/lib/provider-catalog.js +33 -16
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +52 -22
- 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 +5 -5
- package/lib/provider-health.js +10 -5
- package/lib/provider-monitor.js +5 -3
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +3 -2
- package/lib/provider-registry.js +15 -5
- package/lib/provider-response.js +10 -0
- package/lib/provider-settings.js +14 -12
- package/lib/provider-task-runner.js +113 -37
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- 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 +2 -0
- package/lib/web/app.js +107 -46
- package/lib/web/connections.js +151 -12
- package/lib/web/creation.js +29 -25
- package/lib/web/flow-view.js +4 -3
- package/lib/web/gallery.js +17 -16
- package/lib/web/index.html +58 -5
- package/lib/web/media-client.js +18 -4
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +4 -2
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/providers.js +34 -13
- package/lib/web/recovery.js +2 -2
- package/lib/web/styles.css +54 -1
- package/lib/web/transport.js +6 -1
- package/lib/workbench-artifacts.js +3 -2
- package/package.json +5 -2
- package/skills/iris-core/references/comfyui.md +1 -1
- 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 +3 -3
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Internal composition port. No proxy configuration, global mutation or automatic retry.
|
|
2
|
+
export function inheritedProviderFetch(...args) { return globalThis.fetch(...args); }
|
|
3
|
+
|
|
4
|
+
function closed() {
|
|
5
|
+
return Object.assign(new Error('Provider 网络客户端正在关闭或已关闭'), {
|
|
6
|
+
code: 'IRIS_PROVIDER_NETWORK_CLOSED', stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function assertProviderNetwork(network) {
|
|
11
|
+
if (!network || typeof network.fetch !== 'function' || typeof network.run !== 'function') {
|
|
12
|
+
throw new TypeError('network 需要实例级 fetch 和完整操作 run');
|
|
13
|
+
}
|
|
14
|
+
return network;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The owner drains Core/registry operations before disposing this client. */
|
|
18
|
+
export function createProviderNetworkClient({ fetchPort, close } = {}) {
|
|
19
|
+
if (fetchPort !== undefined && typeof fetchPort !== 'function') throw new TypeError('fetchPort 必须是函数');
|
|
20
|
+
if (close !== undefined && typeof close !== 'function') throw new TypeError('close 必须是函数');
|
|
21
|
+
const active = new Set();
|
|
22
|
+
let state = 'open', disposal, cleanupFailed = false;
|
|
23
|
+
const snapshot = () => Object.freeze({ state, activeOperations: active.size, cleanupFailed });
|
|
24
|
+
|
|
25
|
+
// Used inside run: closing must still permit later pages, uploads and body reads.
|
|
26
|
+
const fetch = (...args) => {
|
|
27
|
+
if (state === 'closed') return Promise.reject(closed());
|
|
28
|
+
return (fetchPort || inheritedProviderFetch)(...args);
|
|
29
|
+
};
|
|
30
|
+
function run(action) {
|
|
31
|
+
if (typeof action !== 'function') throw new TypeError('网络操作必须是函数');
|
|
32
|
+
if (state !== 'open') return Promise.reject(closed());
|
|
33
|
+
const operation = Promise.resolve().then(() => action(fetch));
|
|
34
|
+
active.add(operation);
|
|
35
|
+
void operation.then(() => active.delete(operation), () => active.delete(operation));
|
|
36
|
+
return operation;
|
|
37
|
+
}
|
|
38
|
+
function dispose() {
|
|
39
|
+
if (!disposal) {
|
|
40
|
+
state = 'closing';
|
|
41
|
+
disposal = (async () => {
|
|
42
|
+
await Promise.allSettled([...active]);
|
|
43
|
+
try { await close?.(); }
|
|
44
|
+
catch (error) { cleanupFailed = true; throw error; }
|
|
45
|
+
finally { state = 'closed'; }
|
|
46
|
+
return snapshot();
|
|
47
|
+
})();
|
|
48
|
+
}
|
|
49
|
+
return disposal;
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({ fetch, run, dispose, snapshot });
|
|
52
|
+
}
|
package/lib/provider-protocol.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/** 供应商媒体协议判断与 DashScope 凭据边界。 */
|
|
3
3
|
import { comfyWorkflowDigest } from './comfyui-workflow.js';
|
|
4
|
+
import { providerCredentialValue, providerCredentialStatus } from './provider-credentials.js';
|
|
4
5
|
|
|
5
6
|
const DASH_SCOPE_HOST = /^dashscope(?:-[a-z0-9-]+)?\.aliyuncs\.com$/i;
|
|
6
7
|
const REGIONAL_HOST = /^cn-hongkong\.dashscope\.aliyuncs\.com$/i;
|
|
@@ -114,7 +115,7 @@ export function dashscopeApiBase(baseUrl) {
|
|
|
114
115
|
|
|
115
116
|
/** 无认证模式不发送保留的旧凭据;未声明模式保持 bearer 兼容。 */
|
|
116
117
|
export function providerApiKey(provider) {
|
|
117
|
-
return provider?.auth === 'none' ? '' :
|
|
118
|
+
return provider?.auth === 'none' ? '' : providerCredentialValue(provider);
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
/** 配置管理与 headless 候选链共用可用性门,未声明 enabled 时默认启用。 */
|
|
@@ -122,7 +123,7 @@ export function isConfiguredProvider(provider) {
|
|
|
122
123
|
return Boolean(provider && provider.enabled !== false
|
|
123
124
|
&& typeof provider.id === 'string' && provider.id.trim()
|
|
124
125
|
&& providerMediaBaseUrl(provider)
|
|
125
|
-
&& (provider.auth === 'none' || (
|
|
126
|
+
&& (provider.auth === 'none' || providerCredentialStatus(provider) === 'ready'));
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/** 已知需要其他接口的语音系列;自定义模型仍以明确配置和实测为准。 */
|
package/lib/provider-registry.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { defineProviderAdapter } from './provider-adapter.js';
|
|
2
2
|
import { unsupportedProtocolError, providerApiKey } from './provider-protocol.js';
|
|
3
3
|
import { checkedPluginDependency, assertPluginDependencyMatches } from './plugin-contract.js';
|
|
4
|
+
import { createProviderNetworkClient } from './provider-network.js';
|
|
5
|
+
import { createProviderContext, markProviderContextAdapter } from './provider-context.js';
|
|
6
|
+
import { resolveProviderCredential } from './provider-credentials.js';
|
|
4
7
|
|
|
5
8
|
export const PROVIDER_EXTENSION_CONTRACT_VERSION = 0;
|
|
6
9
|
const ID = /^[a-z][a-z0-9._-]{0,79}$/;
|
|
@@ -40,7 +43,8 @@ function checkedProtocol(value) {
|
|
|
40
43
|
/** One registry owns its factory instances, active calls and serial asynchronous cleanup. */
|
|
41
44
|
export async function createExtensionRegistry(factories) {
|
|
42
45
|
const protocols = new Map(), extensions = [], active = new Set();
|
|
43
|
-
let state = 'loading', disposal, cleanupFailed = false;
|
|
46
|
+
let state = 'loading', disposal, cleanupFailed = false, inherited;
|
|
47
|
+
const inheritedNetwork = () => inherited ||= createProviderNetworkClient();
|
|
44
48
|
|
|
45
49
|
function dispose() {
|
|
46
50
|
if (!disposal) {
|
|
@@ -48,6 +52,7 @@ export async function createExtensionRegistry(factories) {
|
|
|
48
52
|
disposal = (async () => {
|
|
49
53
|
await Promise.allSettled([...active]);
|
|
50
54
|
const errors = [];
|
|
55
|
+
try { await inherited?.dispose(); } catch (error) { errors.push(error); }
|
|
51
56
|
for (const extension of [...extensions].reverse()) {
|
|
52
57
|
try { await extension.dispose?.(); } catch (error) { errors.push(error); }
|
|
53
58
|
}
|
|
@@ -101,7 +106,10 @@ export async function createExtensionRegistry(factories) {
|
|
|
101
106
|
if (state !== 'active') throw closed();
|
|
102
107
|
const descriptor = protocols.get(id);
|
|
103
108
|
if (!descriptor) throw unsupportedProtocolError(id);
|
|
104
|
-
const
|
|
109
|
+
const author = descriptor.dependency ? createProviderContext(provider, options, inheritedNetwork) : undefined;
|
|
110
|
+
const resolved = author ? resolveProviderCredential(provider) : provider;
|
|
111
|
+
const connection = { ...resolved, apiKey: providerApiKey(resolved) };
|
|
112
|
+
const value = descriptor.createAdapter(author ? Object.freeze(connection) : connection, author?.context || options);
|
|
105
113
|
if (!value || typeof value.then === 'function') throw invalid('createAdapter 必须同步返回 Adapter');
|
|
106
114
|
if (value.pluginDependency !== undefined) assertPluginDependencyMatches(descriptor.dependency, value.pluginDependency);
|
|
107
115
|
const adapter = defineProviderAdapter({ ...value,
|
|
@@ -109,9 +117,11 @@ export async function createExtensionRegistry(factories) {
|
|
|
109
117
|
unsupported: Object.fromEntries(Object.entries(value.unsupported || {}).map(([name, reason]) => [name, reason?.reason ?? reason])) });
|
|
110
118
|
if (adapter.id !== provider.id || adapter.protocol !== id || adapter.capabilities.some(cap =>
|
|
111
119
|
!descriptor.capabilities.some(known => CAPS[known] === cap))) throw invalid('Adapter 身份或能力与注册协议不一致');
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
const operation = (name, action) => wrap(author ? (...args) => author.invoke(name, action, args) : action);
|
|
121
|
+
const result = Object.freeze({ ...adapter, operations: Object.freeze(Object.fromEntries(Object.entries(adapter.operations)
|
|
122
|
+
.map(([name, action]) => [name, name === 'mapError' ? action : operation(name, action)]))),
|
|
123
|
+
...(adapter.prepareInput ? { prepareInput: operation('prepareInput', adapter.prepareInput) } : {}) });
|
|
124
|
+
return author ? markProviderContextAdapter(result) : result;
|
|
115
125
|
},
|
|
116
126
|
dispose
|
|
117
127
|
});
|
package/lib/provider-response.js
CHANGED
|
@@ -3,6 +3,16 @@ import { ProviderContractError } from './provider-contract.js';
|
|
|
3
3
|
export const PROVIDER_RESPONSE_LIMITS = Object.freeze({ json: 2 * 1024 * 1024, mediaJson: 128 * 1024 * 1024,
|
|
4
4
|
image: 64 * 1024 * 1024, audio: 128 * 1024 * 1024, transcript: 8 * 1024 * 1024, video: 1024 * 1024 * 1024 });
|
|
5
5
|
|
|
6
|
+
/** Core materialization uses the same limits as provider downloads, including cached bytes. */
|
|
7
|
+
export function assertProviderMediaSize(size, capability) {
|
|
8
|
+
const maxBytes = { image: PROVIDER_RESPONSE_LIMITS.image, video: PROVIDER_RESPONSE_LIMITS.video,
|
|
9
|
+
tts: PROVIDER_RESPONSE_LIMITS.audio, music: PROVIDER_RESPONSE_LIMITS.audio, transcribe: PROVIDER_RESPONSE_LIMITS.transcript }[capability];
|
|
10
|
+
if (maxBytes === undefined) throw new TypeError('未知的媒体交付能力:' + capability);
|
|
11
|
+
if (size > maxBytes) throw Object.assign(new ProviderContractError('供应商媒体超过允许的大小(' + maxBytes + ' 字节)', {
|
|
12
|
+
stage: 'download', category: 'protocol', acceptance: 'accepted', retryable: false
|
|
13
|
+
}), { code: 'IRIS_PROVIDER_RESPONSE_TOO_LARGE' });
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
/** Count decoded response bytes as well as Content-Length: chunked and compressed
|
|
7
17
|
* responses cannot bypass the limit. Iterator cancellation closes the body. */
|
|
8
18
|
export async function* boundedProviderBody(response, { maxBytes, stage = 'response', acceptance = 'unknown' }) {
|
package/lib/provider-settings.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Owner-only editing boundary. No I/O, secrets in projections, or implicit model authorization.
|
|
1
|
+
// Owner-only editing boundary. No file I/O, secrets in projections, or implicit model authorization.
|
|
2
2
|
import { CAPABILITIES } from './capability.js';
|
|
3
3
|
import { providerModels, modelPool, functionPolicy, savedAssignmentRefs, resolvePoolModel } from './models.js';
|
|
4
4
|
import { BUILTIN_PROVIDER_PROTOCOLS, normalizeMediaEndpoint, normalizeImageProtocol, normalizeMusicProtocol, selectMediaProtocol, providerForModelBinding, isConfiguredProvider } from './provider-protocol.js';
|
|
5
5
|
import { clearModelCapabilityHealth, resetHealthOnCriticalChange } from './provider-health.js';
|
|
6
6
|
import { checkedComfyWorkflow, projectComfyWorkflow } from './comfyui-workflow.js';
|
|
7
|
+
import { validateProviderCredential, providerCredentialStatus, scrubProviderSecrets } from './provider-credentials.js';
|
|
7
8
|
|
|
8
9
|
const CAPS = Object.values(CAPABILITIES);
|
|
9
10
|
const fail = message => { throw Object.assign(new Error(message), { code: 'IRIS_CONFIG_INPUT_INVALID' }); };
|
|
@@ -28,7 +29,8 @@ export function projectProviderSettings(catalog, revision, registry) {
|
|
|
28
29
|
providers: catalog.providers.map(provider => ({ id: provider.id, name: provider.name || provider.id,
|
|
29
30
|
baseUrl: provider.baseUrl || '', mediaBaseUrl: provider.mediaBaseUrl || '',
|
|
30
31
|
enabled: provider.enabled !== false, auth: provider.auth === 'none' ? 'none' : 'bearer',
|
|
31
|
-
hasCredential:
|
|
32
|
+
hasCredential: providerCredentialStatus(provider) === 'ready', configured: isConfiguredProvider(provider),
|
|
33
|
+
...(provider.apiKeyEnv !== undefined ? { apiKeyEnv: provider.apiKeyEnv, credentialStatus: providerCredentialStatus(provider) } : {}),
|
|
32
34
|
mediaProtocol: selectMediaProtocol(provider).mediaProtocol,
|
|
33
35
|
models: providerModels(provider).map(model => ({ id: model.id, ref: model.ref, capabilities: model.capabilities,
|
|
34
36
|
imageProtocol: model.imageProtocol || 'auto', musicProtocol: model.musicProtocol || '',
|
|
@@ -44,17 +46,13 @@ export function projectProviderSettings(catalog, revision, registry) {
|
|
|
44
46
|
return [cap, { ...policy, models: savedAssignmentRefs(pool, cap, policy.models) }];
|
|
45
47
|
})) };
|
|
46
48
|
// A mistakenly pasted key in a display field must not become a key read-back route.
|
|
47
|
-
|
|
48
|
-
const scrub = value => typeof value === 'string' ? keys.reduce((s, key) => s.split(key).join('[redacted]'), value)
|
|
49
|
-
: Array.isArray(value) ? value.map(scrub) : value && typeof value === 'object'
|
|
50
|
-
? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, scrub(item)])) : value;
|
|
51
|
-
return scrub(result);
|
|
49
|
+
return scrubProviderSecrets(result, catalog.providers);
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
export function applyProviderSettings(catalog, input, registry) {
|
|
55
53
|
object(input, ['expectedRevision', 'provider', 'credential', 'assignments']);
|
|
56
54
|
object(input.provider, ['id', 'name', 'baseUrl', 'mediaBaseUrl', 'mediaProtocol', 'enabled', 'auth', 'models']);
|
|
57
|
-
object(input.credential, ['action', 'value']);
|
|
55
|
+
object(input.credential, ['action', 'value', 'name']);
|
|
58
56
|
const value = input.provider, id = string(value.id, 160);
|
|
59
57
|
const matches = catalog.providers.filter(item => item.id === id);
|
|
60
58
|
if (matches.length > 1) fail('供应商 ID 重复,请在本机核对');
|
|
@@ -68,9 +66,13 @@ export function applyProviderSettings(catalog, input, registry) {
|
|
|
68
66
|
catch (_) { fail('地址必须是无凭据、查询或片段的 HTTP(S) URL'); }
|
|
69
67
|
if (!protocols(registry).some(item => item.id === next.mediaProtocol) && previous?.mediaProtocol !== next.mediaProtocol) fail('协议未注册,请先显式加载对应扩展');
|
|
70
68
|
const credential = input.credential;
|
|
71
|
-
if (!['keep', 'replace', 'clear'].includes(credential.action)
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
if (!['keep', 'replace', 'clear', 'env'].includes(credential.action)
|
|
70
|
+
|| credential.action !== 'replace' && Object.hasOwn(credential, 'value')
|
|
71
|
+
|| credential.action !== 'env' && Object.hasOwn(credential, 'name')) fail('密钥操作必须是 keep、replace、clear 或 env,并只提供对应字段');
|
|
72
|
+
if (credential.action === 'replace') { next.apiKey = string(credential.value, 8192); delete next.apiKeyEnv; }
|
|
73
|
+
if (credential.action === 'env') { next.apiKeyEnv = string(credential.name, 128); delete next.apiKey; }
|
|
74
|
+
if (credential.action === 'clear') { delete next.apiKey; delete next.apiKeyEnv; }
|
|
75
|
+
validateProviderCredential(next);
|
|
74
76
|
if (!Array.isArray(value.models) || value.models.length > 256) fail('单次最多保存 256 个模型');
|
|
75
77
|
const existing = providerModels(previous || {});
|
|
76
78
|
const entries = Array.isArray(previous?.models) ? previous.models.map(model => typeof model === 'string' ? { id: model } : { ...model })
|
|
@@ -102,7 +104,7 @@ export function applyProviderSettings(catalog, input, registry) {
|
|
|
102
104
|
resetHealthOnCriticalChange(previousConnection, next, next);
|
|
103
105
|
for (const model of existing) {
|
|
104
106
|
const updated = next.models.find(entry => entry.id === model.id);
|
|
105
|
-
const connectionChanged = ['apiKey', 'baseUrl', 'mediaBaseUrl', 'auth', 'mediaProtocol'].some(key => previousConnection[key] !== next[key]);
|
|
107
|
+
const connectionChanged = ['apiKey', 'apiKeyEnv', 'baseUrl', 'mediaBaseUrl', 'auth', 'mediaProtocol'].some(key => previousConnection[key] !== next[key]);
|
|
106
108
|
for (const cap of model.capabilities) if (connectionChanged || !updated.capabilities?.includes(cap)
|
|
107
109
|
|| cap === 'image-gen' && JSON.stringify(previous.models?.find(entry => entry?.id === model.id)?.comfyWorkflow) !== JSON.stringify(updated.comfyWorkflow)
|
|
108
110
|
|| cap === 'image-gen' && providerForModelBinding(previous, model.id, cap).mediaProtocol !== providerForModelBinding(next, model.id, cap).mediaProtocol
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
recordCoreAttemptResult,
|
|
21
21
|
recordCoreCancelResult,
|
|
22
22
|
recordCoreDownloadCompleted,
|
|
23
|
+
recordCoreNetworkRoute,
|
|
23
24
|
recordCorePollResult,
|
|
24
25
|
recordCoreWatchError,
|
|
25
26
|
recoverCoreTask,
|
|
@@ -38,6 +39,8 @@ import { createDeliveryCache, newDeliveryPayloadPath, readDeliveryCache,
|
|
|
38
39
|
import { readFlowRun, flowFail, flowStep } from './flow-store.js';
|
|
39
40
|
import { normalizeGenerationInput } from './generation-input.js';
|
|
40
41
|
import { assertPluginDependencyMatches } from './plugin-contract.js';
|
|
42
|
+
import { checkedNetworkRequirement, sameNetworkRequirement, createNetworkAttemptBudget } from './network-policy.js';
|
|
43
|
+
import { assertProviderMediaSize } from './provider-response.js';
|
|
41
44
|
|
|
42
45
|
export const PROVIDER_TASK_RUNNER_VERSION = 0;
|
|
43
46
|
|
|
@@ -48,7 +51,7 @@ export const PROVIDER_TASK_RUNNER_VERSION = 0;
|
|
|
48
51
|
*/
|
|
49
52
|
export const DELIVERY_PROFILES = Object.freeze({
|
|
50
53
|
image: Object.freeze({ kind: 'generated-image', mediaTypes: Object.freeze(['image/png', 'image/jpeg', 'image/webp']), defaultMediaType: 'image/png' }),
|
|
51
|
-
video: Object.freeze({ kind: 'generated-video', mediaTypes: Object.freeze(['video/mp4']), defaultMediaType: 'video/mp4' }),
|
|
54
|
+
video: Object.freeze({ kind: 'generated-video', mediaTypes: Object.freeze(['video/mp4', 'video/webm']), defaultMediaType: 'video/mp4' }),
|
|
52
55
|
// TTS 同步完成型:submit 内 completed → 同一次调用直接交付(无 remote 长轮询)。
|
|
53
56
|
tts: Object.freeze({ kind: 'generated-audio', mediaTypes: Object.freeze(['audio/mpeg', 'audio/wav']), defaultMediaType: 'audio/mpeg' }),
|
|
54
57
|
music: Object.freeze({ kind: 'generated-audio', mediaTypes: Object.freeze(['audio/mpeg', 'audio/wav']), defaultMediaType: 'audio/mpeg' }),
|
|
@@ -57,6 +60,20 @@ export const DELIVERY_PROFILES = Object.freeze({
|
|
|
57
60
|
});
|
|
58
61
|
const runtimeTaskGates = new WeakMap();
|
|
59
62
|
|
|
63
|
+
// The Core operation owns the root and route persistence, including when the
|
|
64
|
+
// network manager is supplied by a caller rather than created by CLI/Web.
|
|
65
|
+
function networkOperationContext(runtime, dataRoot, taskId, context) {
|
|
66
|
+
const task = inspectCoreTask(dataRoot, taskId);
|
|
67
|
+
return { ...context, taskId, modelRef: task.modelRef, providerId: task.providerId,
|
|
68
|
+
...(runtime.reportScheduling ? { onSchedulingState: runtime.reportScheduling } : {}),
|
|
69
|
+
recordNetworkRoute: fact => runtime.checkpoint ? runtime.checkpoint(() => recordCoreNetworkRoute(dataRoot, taskId, fact)) : recordCoreNetworkRoute(dataRoot, taskId, fact) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function invokeNetwork(runtime, adapter, name, request, context) {
|
|
73
|
+
const invoke = () => invokeProviderOperation(adapter, name, request, context);
|
|
74
|
+
return runtime.networkWait ? runtime.networkWait(invoke, name) : invoke();
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
export class ProviderTaskRunnerError extends Error {
|
|
61
78
|
constructor(code, message) {
|
|
62
79
|
super(message);
|
|
@@ -76,12 +93,16 @@ function checkAdapterForTask(task, adapter) {
|
|
|
76
93
|
throw new ProviderTaskRunnerError('IRIS_PROVIDER_TASK_IDENTITY_MISMATCH', 'Provider Adapter 与 Task 已持久化身份不一致');
|
|
77
94
|
}
|
|
78
95
|
assertPluginDependencyMatches(task.pluginDependency, adapter.pluginDependency);
|
|
96
|
+
const requirement = adapter.networkRequirement === undefined ? undefined : checkedNetworkRequirement(adapter.networkRequirement);
|
|
97
|
+
if (!sameNetworkRequirement(task.networkRequirement, requirement)) {
|
|
98
|
+
throw new ProviderTaskRunnerError('IRIS_NETWORK_REQUIREMENT_MISMATCH', 'Provider 网络要求与原 Task 不一致;不会隐式升级、解除或借用其他网络配置');
|
|
99
|
+
}
|
|
79
100
|
return adapter;
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
function normalizedCandidates(capability, candidates) {
|
|
83
104
|
if (!Array.isArray(candidates) || !candidates.length) throw new TypeError('至少需要一个 Provider 候选');
|
|
84
|
-
|
|
105
|
+
const normalized = candidates.map((candidate, index) => {
|
|
85
106
|
const adapter = candidate?.adapter;
|
|
86
107
|
const model = String(candidate?.model || '').trim();
|
|
87
108
|
const parsed = parseModelRef(model);
|
|
@@ -95,9 +116,18 @@ function normalizedCandidates(capability, candidates) {
|
|
|
95
116
|
}
|
|
96
117
|
if (candidate.prepareInput !== undefined && typeof candidate.prepareInput !== 'function') throw new TypeError('候选 prepareInput 必须是函数');
|
|
97
118
|
if (candidate.supportsImageEdit !== undefined && typeof candidate.supportsImageEdit !== 'boolean') throw new TypeError('候选 supportsImageEdit 必须是布尔值');
|
|
119
|
+
const networkRequirement = adapter.networkRequirement === undefined ? undefined : checkedNetworkRequirement(adapter.networkRequirement);
|
|
120
|
+
if (candidate.networkRequirement !== undefined && !sameNetworkRequirement(checkedNetworkRequirement(candidate.networkRequirement), networkRequirement)) {
|
|
121
|
+
throw new ProviderTaskRunnerError('IRIS_NETWORK_REQUIREMENT_MISMATCH', '候选与 Adapter 的网络要求不一致');
|
|
122
|
+
}
|
|
98
123
|
return { adapter, model, providerModel: parsed.modelId, providerBinding: candidate.providerBinding, pluginDependency: adapter.pluginDependency, selectionReason, prepareInput: candidate.prepareInput,
|
|
124
|
+
...(networkRequirement ? { networkRequirement } : {}),
|
|
99
125
|
...(candidate.supportsImageEdit !== undefined ? { supportsImageEdit: candidate.supportsImageEdit } : {}) };
|
|
100
126
|
});
|
|
127
|
+
if (normalized.some(candidate => !sameNetworkRequirement(candidate.networkRequirement, normalized[0].networkRequirement))) {
|
|
128
|
+
throw new ProviderTaskRunnerError('IRIS_NETWORK_REQUIREMENT_MISMATCH', '同一 Task 的所有模型候选必须属于同一网络要求');
|
|
129
|
+
}
|
|
130
|
+
return normalized;
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
function stagingFile(dataRoot) {
|
|
@@ -121,7 +151,20 @@ async function generatedImageMediaType(bytes) {
|
|
|
121
151
|
return mediaType;
|
|
122
152
|
}
|
|
123
153
|
|
|
124
|
-
|
|
154
|
+
/** Confirm the saved container, independently of an adapter's declared MIME; never buffer long audio. */
|
|
155
|
+
function generatedAudioMediaType(file) {
|
|
156
|
+
const header = Buffer.alloc(12), fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
157
|
+
let size;
|
|
158
|
+
try { size = fs.readSync(fd, header, 0, header.length, 0); }
|
|
159
|
+
finally { fs.closeSync(fd); }
|
|
160
|
+
if (size >= 12 && header.toString('ascii', 0, 4) === 'RIFF' && header.toString('ascii', 8, 12) === 'WAVE') return 'audio/wav';
|
|
161
|
+
if (size >= 10 && (header.toString('ascii', 0, 3) === 'ID3' || header[0] === 0xff && (header[1] & 0xe0) === 0xe0)) return 'audio/mpeg';
|
|
162
|
+
throw new ProviderContractError('供应商音频不是可识别的 WAV 或 MP3 容器', {
|
|
163
|
+
stage: 'download', category: 'protocol', acceptance: 'accepted'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function deliverArtifacts(runtime, dataRoot, taskId, adapter, remoteArtifacts, signal) {
|
|
125
168
|
beginCoreDelivery(dataRoot, taskId);
|
|
126
169
|
const artifactIds = [];
|
|
127
170
|
const stagedFiles = [];
|
|
@@ -147,22 +190,25 @@ async function deliverArtifacts(dataRoot, taskId, adapter, remoteArtifacts, sign
|
|
|
147
190
|
}
|
|
148
191
|
const target = stagingFile(dataRoot);
|
|
149
192
|
stagedFiles.push({ target, mediaType });
|
|
150
|
-
const downloaded = await
|
|
193
|
+
const downloaded = await invokeNetwork(runtime, adapter, 'download', {
|
|
151
194
|
capability: task.capability,
|
|
152
195
|
artifact: remote,
|
|
153
196
|
targetPath: target,
|
|
154
197
|
signal
|
|
155
|
-
}, { signal,
|
|
198
|
+
}, networkOperationContext(runtime, dataRoot, taskId, { signal, attemptId: task.activeAttemptId }));
|
|
156
199
|
const stat = fs.lstatSync(target);
|
|
157
200
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== downloaded.bytes) {
|
|
158
201
|
throw Object.assign(new Error('Provider 物化结果与文件证据不一致'), {
|
|
159
202
|
stage: 'download', category: 'local_io', acceptance: 'accepted'
|
|
160
203
|
});
|
|
161
204
|
}
|
|
205
|
+
assertProviderMediaSize(stat.size, task.capability);
|
|
162
206
|
const staged = stagedFiles.at(-1);
|
|
163
207
|
if (task.capability === 'image') {
|
|
164
208
|
staged.bytes = fs.readFileSync(target);
|
|
165
209
|
staged.mediaType = await generatedImageMediaType(staged.bytes);
|
|
210
|
+
} else if (['tts', 'music'].includes(task.capability)) {
|
|
211
|
+
staged.mediaType = generatedAudioMediaType(target);
|
|
166
212
|
}
|
|
167
213
|
}
|
|
168
214
|
recordCoreDownloadCompleted(dataRoot, taskId);
|
|
@@ -188,30 +234,34 @@ async function deliverArtifacts(dataRoot, taskId, adapter, remoteArtifacts, sign
|
|
|
188
234
|
}
|
|
189
235
|
}
|
|
190
236
|
|
|
191
|
-
async function observeWithinRoot(dataRoot, taskId, adapter, signal) {
|
|
237
|
+
async function observeWithinRoot(runtime, dataRoot, taskId, adapter, signal) {
|
|
192
238
|
const before = inspectCoreTask(dataRoot, taskId);
|
|
193
239
|
checkAdapterForTask(before, adapter);
|
|
194
240
|
activateCoreWatch(dataRoot, taskId);
|
|
195
241
|
let result;
|
|
196
242
|
try {
|
|
197
|
-
result = await
|
|
243
|
+
result = await invokeNetwork(runtime, adapter, 'poll', {
|
|
198
244
|
capability: before.capability,
|
|
199
245
|
remoteTaskId: before.remoteTaskId,
|
|
200
246
|
signal
|
|
201
|
-
}, { signal,
|
|
247
|
+
}, networkOperationContext(runtime, dataRoot, taskId, { signal, attemptId: before.activeAttemptId }));
|
|
202
248
|
} catch (error) {
|
|
203
249
|
return recordCoreWatchError(dataRoot, taskId, error);
|
|
204
250
|
}
|
|
205
|
-
|
|
206
|
-
if (
|
|
251
|
+
let cachedFlowResult = false, cacheError;
|
|
252
|
+
if (before.flowBinding && result.kind === 'succeeded') {
|
|
253
|
+
try { createDeliveryCache(dataRoot, before, result.artifacts); cachedFlowResult = true; }
|
|
254
|
+
catch (error) { cacheError = error; }
|
|
255
|
+
}
|
|
207
256
|
const recorded = recordCorePollResult(dataRoot, taskId, result, { deliveryCache: cachedFlowResult });
|
|
208
257
|
if (result.kind !== 'succeeded') return recorded;
|
|
209
|
-
if (
|
|
210
|
-
return
|
|
258
|
+
if (cacheError) return failCoreDelivery(dataRoot, taskId, cacheError);
|
|
259
|
+
if (cachedFlowResult) return deliverCachedArtifacts(runtime, dataRoot, taskId, adapter, signal);
|
|
260
|
+
return deliverArtifacts(runtime, dataRoot, taskId, adapter, result.artifacts, signal);
|
|
211
261
|
}
|
|
212
262
|
|
|
213
263
|
/** 同步结果按已提交缓存补交付;已下载项不再访问 Provider,稳定 Artifact ID 防重复。 */
|
|
214
|
-
async function deliverCachedArtifacts(dataRoot, taskId, adapter, signal) {
|
|
264
|
+
async function deliverCachedArtifacts(runtime, dataRoot, taskId, adapter, signal) {
|
|
215
265
|
beginCoreDelivery(dataRoot, taskId);
|
|
216
266
|
try {
|
|
217
267
|
const task = inspectCoreTask(dataRoot, taskId);
|
|
@@ -228,16 +278,17 @@ async function deliverCachedArtifacts(dataRoot, taskId, adapter, signal) {
|
|
|
228
278
|
}
|
|
229
279
|
checkAdapterForTask(task, adapter);
|
|
230
280
|
const target = newDeliveryPayloadPath(dataRoot, cache);
|
|
231
|
-
const downloaded = await
|
|
281
|
+
const downloaded = await invokeNetwork(runtime, adapter, 'download', {
|
|
232
282
|
capability: task.capability, artifact: item.source, targetPath: target, signal
|
|
233
|
-
}, { signal,
|
|
283
|
+
}, networkOperationContext(runtime, dataRoot, taskId, { signal, attemptId: task.activeAttemptId }));
|
|
234
284
|
const stat = fs.lstatSync(target);
|
|
235
285
|
if (!stat.isFile() || stat.isSymbolicLink() || !stat.size || stat.size !== downloaded.bytes) {
|
|
236
286
|
throw new Error('Provider 物化结果与文件证据不一致');
|
|
237
287
|
}
|
|
288
|
+
assertProviderMediaSize(stat.size, task.capability);
|
|
238
289
|
const mediaType = task.capability === 'image'
|
|
239
290
|
? await generatedImageMediaType(fs.readFileSync(target))
|
|
240
|
-
: declaredMediaType;
|
|
291
|
+
: ['tts', 'music'].includes(task.capability) ? generatedAudioMediaType(target) : declaredMediaType;
|
|
241
292
|
if (!profile.mediaTypes.includes(mediaType)) throw new Error('交付媒体类型不在 Profile 白名单');
|
|
242
293
|
recordDeliveryPayload(dataRoot, cache, index, target, mediaType);
|
|
243
294
|
}
|
|
@@ -250,7 +301,7 @@ async function deliverCachedArtifacts(dataRoot, taskId, adapter, signal) {
|
|
|
250
301
|
const sourcePath = readDeliveryPayload(dataRoot, cache, index);
|
|
251
302
|
const mediaType = task.capability === 'image'
|
|
252
303
|
? await generatedImageMediaType(fs.readFileSync(sourcePath))
|
|
253
|
-
: (item.payload.mediaType || profile.defaultMediaType);
|
|
304
|
+
: ['tts', 'music'].includes(task.capability) ? generatedAudioMediaType(sourcePath) : (item.payload.mediaType || profile.defaultMediaType);
|
|
254
305
|
if (!profile.mediaTypes.includes(mediaType)) throw new Error('交付媒体类型不在 Profile 白名单');
|
|
255
306
|
const artifact = adoptCoreArtifactFile(dataRoot, {
|
|
256
307
|
artifactId: item.artifactId, sourcePath, createdAt: cache.createdAt,
|
|
@@ -272,10 +323,11 @@ async function deliverCachedArtifacts(dataRoot, taskId, adapter, signal) {
|
|
|
272
323
|
*/
|
|
273
324
|
export function createProviderTaskRunner(runtime) {
|
|
274
325
|
checkRuntime(runtime);
|
|
275
|
-
|
|
326
|
+
const gate = runtime.taskGate || runtime;
|
|
327
|
+
let activeTasks = runtimeTaskGates.get(gate);
|
|
276
328
|
if (!activeTasks) {
|
|
277
329
|
activeTasks = new Set();
|
|
278
|
-
runtimeTaskGates.set(
|
|
330
|
+
runtimeTaskGates.set(gate, activeTasks);
|
|
279
331
|
}
|
|
280
332
|
|
|
281
333
|
async function runExclusive(taskId, operation, callback) {
|
|
@@ -299,6 +351,8 @@ export function createProviderTaskRunner(runtime) {
|
|
|
299
351
|
if (signal.aborted) throw new ProviderTaskRunnerError('IRIS_PROVIDER_ABORTED', '提交已取消');
|
|
300
352
|
const capability = String(input.capability || '').trim();
|
|
301
353
|
let candidates = normalizedCandidates(capability, input.candidates);
|
|
354
|
+
const networkRequirement = candidates[0].networkRequirement;
|
|
355
|
+
const networkBudget = createNetworkAttemptBudget();
|
|
302
356
|
let providerInput = input.providerInput ?? {};
|
|
303
357
|
if (providerInput.workflow_parameters !== undefined && candidates.some(candidate => candidate.adapter.protocol !== 'comfyui')) {
|
|
304
358
|
throw new ProviderTaskRunnerError('IRIS_COMFYUI_PARAMETERS_INVALID', '工作流参数仅用于明确的 ComfyUI 模板');
|
|
@@ -306,15 +360,16 @@ export function createProviderTaskRunner(runtime) {
|
|
|
306
360
|
let flow;
|
|
307
361
|
if (input.flowRunId !== undefined) {
|
|
308
362
|
flow = readFlowRun(dataRoot, input.flowRunId);
|
|
309
|
-
const audio = isAudioRecipe(flow.recipe),
|
|
310
|
-
const expected = audio ? Object.fromEntries(Object.entries(
|
|
311
|
-
|
|
363
|
+
const audio = isAudioRecipe(flow.recipe), video = flow.recipe === 'video-generate/v0';
|
|
364
|
+
const expected = audio ? Object.fromEntries(Object.entries(normalizeAudioFlowInput(flow.recipe, flow.input)).filter(([key]) => key !== 'model_ref'))
|
|
365
|
+
: normalizeGenerationInput(video ? 'video' : 'image', flow.input).providerInput;
|
|
366
|
+
const expectedCapability = video ? 'video' : audio ? flow.recipe === 'speech/v0' ? 'tts' : 'transcribe' : 'image';
|
|
312
367
|
if (capability !== expectedCapability || !flowStep(flow).claimed || input.sourceArtifactId !== flow.source?.artifactId
|
|
313
368
|
|| Object.keys(providerInput).length !== Object.keys(expected).length
|
|
314
369
|
|| Object.keys(expected).some(key => key === 'workflow_parameters'
|
|
315
370
|
? JSON.stringify(providerInput[key]) !== JSON.stringify(expected[key]) : providerInput[key] !== expected[key])
|
|
316
371
|
|| candidates.length !== 1 || candidates[0].model !== flow.input.model_ref
|
|
317
|
-
|| candidates[0].providerBinding !== flow.providerBinding) {
|
|
372
|
+
|| candidates[0].providerBinding !== flow.providerBinding || !sameNetworkRequirement(networkRequirement, flow.networkRequirement)) {
|
|
318
373
|
flowFail('IRIS_FLOW_INPUT_CONFLICT', '提交与流程已保存的输入或模型绑定冲突');
|
|
319
374
|
}
|
|
320
375
|
}
|
|
@@ -324,9 +379,15 @@ export function createProviderTaskRunner(runtime) {
|
|
|
324
379
|
providerInput = { ...providerInput, image };
|
|
325
380
|
}
|
|
326
381
|
if (input.sourceArtifactId !== undefined && !['image', 'transcribe'].includes(capability)) throw new TypeError('来源作品仅用于图片编辑或转写');
|
|
382
|
+
if (runtime.scheduler) await runtime.scheduler.beforeSubmit(runtime, dataRoot, { modelRef: candidates[0].model },
|
|
383
|
+
() => candidates[0].adapter.schedulingReadyAt?.(candidates[0].providerModel));
|
|
327
384
|
const task = flow ? claimFlowTask(dataRoot, flow.id)
|
|
328
|
-
: createCoreTask(dataRoot, { capability, retriedFrom: input.retriedFrom, sourceArtifactId: input.sourceArtifactId
|
|
385
|
+
: createCoreTask(dataRoot, { capability, retriedFrom: input.retriedFrom, sourceArtifactId: input.sourceArtifactId,
|
|
386
|
+
...(networkRequirement ? { networkRequirement } : {}) });
|
|
329
387
|
if (flow && task.attempts.length) flowFail('IRIS_FLOW_TASK_ALREADY_STARTED', '该流程 Task 已有提交意图,禁止重新 submit');
|
|
388
|
+
if (activeTasks.has(task.id)) throw new ProviderTaskRunnerError('IRIS_PROVIDER_TASK_BUSY', '该 Task 已有生命周期操作正在执行');
|
|
389
|
+
activeTasks.add(task.id);
|
|
390
|
+
try {
|
|
330
391
|
const completedResults = new Map();
|
|
331
392
|
const wrapped = candidates.map(({ adapter, model, providerModel, prepareInput }) => ({
|
|
332
393
|
id: adapter.id,
|
|
@@ -334,17 +395,22 @@ export function createProviderTaskRunner(runtime) {
|
|
|
334
395
|
adapter,
|
|
335
396
|
submit: async (providerInput, context) => {
|
|
336
397
|
if (prepareInput) {
|
|
337
|
-
try {
|
|
398
|
+
try {
|
|
399
|
+
const prepareContext = networkOperationContext(runtime, dataRoot, task.id, { providerInput, signal, attempt: context.attempt, attemptId: context.attempt.id, networkBudget });
|
|
400
|
+
const prepare = () => prepareInput(prepareContext);
|
|
401
|
+
providerInput = await (runtime.networkWait ? runtime.networkWait(prepare, 'prepareInput') : prepare());
|
|
402
|
+
}
|
|
338
403
|
catch (error) { throw new ProviderContractError((capability === 'video' ? '视频' : '音频') + '输入准备失败:' + (error.safeMessage || error.message), {
|
|
339
404
|
...providerErrorRecord(error), stage: error.stage || 'prepare', category: error.category || 'local_io', acceptance: 'not_accepted'
|
|
340
405
|
}); }
|
|
341
406
|
}
|
|
342
407
|
if (signal.aborted) throw new ProviderContractError('提交已取消', { stage: 'prepare', category: 'aborted', acceptance: 'not_accepted' });
|
|
343
|
-
const result = await
|
|
408
|
+
const result = await invokeNetwork(
|
|
409
|
+
runtime,
|
|
344
410
|
adapter,
|
|
345
411
|
'submit',
|
|
346
412
|
{ capability, model: providerModel, input: providerInput, signal },
|
|
347
|
-
{ ...context, signal,
|
|
413
|
+
networkOperationContext(runtime, dataRoot, task.id, { ...context, signal, attemptId: context.attempt.id, networkBudget })
|
|
348
414
|
);
|
|
349
415
|
if (result.kind === 'completed') completedResults.set(context.attempt.id, result.artifacts);
|
|
350
416
|
return result;
|
|
@@ -354,9 +420,12 @@ export function createProviderTaskRunner(runtime) {
|
|
|
354
420
|
signal,
|
|
355
421
|
beforeAttempt: async (attempt) => {
|
|
356
422
|
const candidate = candidates[attempt.ordinal - 1];
|
|
423
|
+
if (attempt.ordinal > 1 && runtime.scheduler) await runtime.scheduler.beforeSubmit(runtime, dataRoot, { modelRef: candidate.model },
|
|
424
|
+
() => candidate.adapter.schedulingReadyAt?.(candidate.providerModel));
|
|
357
425
|
return beginCoreAttempt(dataRoot, task.id, {
|
|
358
426
|
...attempt,
|
|
359
427
|
...(candidate.providerBinding !== undefined ? { providerBinding: candidate.providerBinding } : {}),
|
|
428
|
+
...(candidate.networkRequirement ? { networkRequirement: candidate.networkRequirement } : {}),
|
|
360
429
|
...(candidate.pluginDependency ? { pluginDependency: candidate.pluginDependency } : {}),
|
|
361
430
|
...(candidate.selectionReason !== undefined ? { selectionReason: candidate.selectionReason } : {})
|
|
362
431
|
});
|
|
@@ -385,7 +454,7 @@ export function createProviderTaskRunner(runtime) {
|
|
|
385
454
|
} else if (submitted.result.kind === 'completed') {
|
|
386
455
|
return Object.freeze({
|
|
387
456
|
taskId: task.id,
|
|
388
|
-
task: await deliverCachedArtifacts(dataRoot, task.id, submitted.candidate.adapter, signal),
|
|
457
|
+
task: await deliverCachedArtifacts(runtime, dataRoot, task.id, submitted.candidate.adapter, signal),
|
|
389
458
|
attempts: submitted.attempts
|
|
390
459
|
});
|
|
391
460
|
}
|
|
@@ -394,6 +463,7 @@ export function createProviderTaskRunner(runtime) {
|
|
|
394
463
|
task: inspectCoreTask(dataRoot, task.id),
|
|
395
464
|
attempts: submitted.attempts
|
|
396
465
|
});
|
|
466
|
+
} finally { activeTasks.delete(task.id); }
|
|
397
467
|
});
|
|
398
468
|
}
|
|
399
469
|
|
|
@@ -408,7 +478,7 @@ export function createProviderTaskRunner(runtime) {
|
|
|
408
478
|
checkAdapterForTask(current, adapter);
|
|
409
479
|
if (current.watchState === 'active') recoverCoreTask(dataRoot, taskId);
|
|
410
480
|
}
|
|
411
|
-
return observeWithinRoot(dataRoot, taskId, adapter, signal);
|
|
481
|
+
return observeWithinRoot(runtime, dataRoot, taskId, adapter, signal);
|
|
412
482
|
});
|
|
413
483
|
}
|
|
414
484
|
|
|
@@ -417,14 +487,14 @@ export function createProviderTaskRunner(runtime) {
|
|
|
417
487
|
const task = inspectCoreTask(dataRoot, taskId);
|
|
418
488
|
if (adapter || !task.deliveryCache) checkAdapterForTask(task, adapter);
|
|
419
489
|
prepareCoreRedelivery(dataRoot, taskId);
|
|
420
|
-
if (task.deliveryCache) return deliverCachedArtifacts(dataRoot, taskId, adapter, signal);
|
|
490
|
+
if (task.deliveryCache) return deliverCachedArtifacts(runtime, dataRoot, taskId, adapter, signal);
|
|
421
491
|
let result;
|
|
422
492
|
try {
|
|
423
|
-
result = await
|
|
493
|
+
result = await invokeNetwork(runtime, adapter, 'poll', {
|
|
424
494
|
capability: task.capability,
|
|
425
495
|
remoteTaskId: task.remoteTaskId,
|
|
426
496
|
signal
|
|
427
|
-
}, { signal,
|
|
497
|
+
}, networkOperationContext(runtime, dataRoot, taskId, { signal, attemptId: task.activeAttemptId, redelivery: true }));
|
|
428
498
|
} catch (error) {
|
|
429
499
|
return failCoreDelivery(dataRoot, taskId, error);
|
|
430
500
|
}
|
|
@@ -434,7 +504,7 @@ export function createProviderTaskRunner(runtime) {
|
|
|
434
504
|
{ stage: 'download', category: 'protocol', acceptance: 'accepted' }
|
|
435
505
|
));
|
|
436
506
|
}
|
|
437
|
-
return deliverArtifacts(dataRoot, taskId, adapter, result.artifacts, signal);
|
|
507
|
+
return deliverArtifacts(runtime, dataRoot, taskId, adapter, result.artifacts, signal);
|
|
438
508
|
});
|
|
439
509
|
}
|
|
440
510
|
|
|
@@ -446,23 +516,26 @@ export function createProviderTaskRunner(runtime) {
|
|
|
446
516
|
if (!hasProviderOperation(adapter, 'cancel')) {
|
|
447
517
|
return recordCoreCancelResult(dataRoot, taskId, { kind: 'not_supported' });
|
|
448
518
|
}
|
|
449
|
-
const result = await
|
|
519
|
+
const result = await invokeNetwork(runtime, adapter, 'cancel', {
|
|
450
520
|
capability: task.capability,
|
|
451
521
|
remoteTaskId: task.remoteTaskId,
|
|
452
522
|
signal
|
|
453
|
-
}, { signal,
|
|
523
|
+
}, networkOperationContext(runtime, dataRoot, taskId, { signal, attemptId: task.activeAttemptId }));
|
|
454
524
|
return recordCoreCancelResult(dataRoot, taskId, result);
|
|
455
525
|
});
|
|
456
526
|
}
|
|
457
527
|
|
|
458
528
|
function recover(taskId, adapter) {
|
|
459
529
|
return runExclusive(taskId, 'recover', async ({ dataRoot, signal }) => {
|
|
530
|
+
const before = inspectCoreTask(dataRoot, taskId);
|
|
531
|
+
if (before.acceptance === 'accepted' && before.remoteTaskId && before.outcome === 'none'
|
|
532
|
+
&& ['accepted', 'running'].includes(before.phase)) checkAdapterForTask(before, adapter);
|
|
460
533
|
const recovered = recoverCoreTask(dataRoot, taskId);
|
|
461
534
|
if (recovered.acceptance === 'accepted' && recovered.remoteTaskId
|
|
462
535
|
&& recovered.outcome === 'none'
|
|
463
536
|
&& ['accepted', 'running'].includes(recovered.phase)) {
|
|
464
537
|
checkAdapterForTask(recovered, adapter);
|
|
465
|
-
return observeWithinRoot(dataRoot, taskId, adapter, signal);
|
|
538
|
+
return observeWithinRoot(runtime, dataRoot, taskId, adapter, signal);
|
|
466
539
|
}
|
|
467
540
|
return recovered;
|
|
468
541
|
});
|
|
@@ -475,6 +548,9 @@ export function createProviderTaskRunner(runtime) {
|
|
|
475
548
|
observe,
|
|
476
549
|
redeliver,
|
|
477
550
|
cancel,
|
|
478
|
-
recover
|
|
551
|
+
recover,
|
|
552
|
+
assertIdle(taskId) {
|
|
553
|
+
if (activeTasks.has(taskId)) throw new ProviderTaskRunnerError('IRIS_PROVIDER_TASK_BUSY', '该 Task 已有生命周期操作正在执行');
|
|
554
|
+
}
|
|
479
555
|
});
|
|
480
556
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Bind all hops of an existing media transport to an instance-owned request function.
|
|
2
|
+
import * as transport from './adapters.js';
|
|
3
|
+
|
|
4
|
+
const REQUEST_METHODS = Object.freeze([
|
|
5
|
+
'generateImageMultimodal', 'startImageGeneration', 'submitImage', 'submitVideo', 'uploadTempFile',
|
|
6
|
+
'pollTask', 'synthesizeTts', 'openAiGenerateImage', 'openAiChatGenerateImage', 'openAiResponsesGenerateImage',
|
|
7
|
+
'listModels', 'visionStream', 'submitTranscription', 'pollTranscriptionTask', 'generateMusic'
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export function createProviderTransport(fetchPort) {
|
|
11
|
+
if (typeof fetchPort !== 'function') throw new TypeError('Provider transport 需要明确的 fetchPort');
|
|
12
|
+
const bound = { ...transport };
|
|
13
|
+
for (const name of REQUEST_METHODS) bound[name] = input => transport[name]({ ...input, fetchPort });
|
|
14
|
+
bound.downloadTo = (url, filePath, options) => transport.downloadTo(url, filePath, { ...options, fetchPort });
|
|
15
|
+
return Object.freeze(bound);
|
|
16
|
+
}
|