@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
package/lib/local-api.js
CHANGED
|
@@ -8,7 +8,8 @@ import { createCoreRuntime } from './core-runtime.js';
|
|
|
8
8
|
import { inspectFlowRoot, listFlowRuns, readFlowRun } from './flow-store.js';
|
|
9
9
|
import { inspectCoreArtifactMetadata, readCoreArtifactBytes, listCoreArtifacts } from './core-artifacts.js';
|
|
10
10
|
import { isPluginSourceImage } from './plugin-source-image.js';
|
|
11
|
-
import { imageCandidatesFromCatalog, ttsCandidatesFromCatalog, transcribeCandidatesFromCatalog, loadProviderCatalog, providerTaskBinding } from './provider-catalog.js';
|
|
11
|
+
import { imageCandidatesFromCatalog, videoCandidatesFromCatalog, ttsCandidatesFromCatalog, transcribeCandidatesFromCatalog, loadProviderCatalog, providerTaskBinding } from './provider-catalog.js';
|
|
12
|
+
import { videoModelSupportsMode } from './video-input.js';
|
|
12
13
|
import { AUDIO_TYPES, MAX_AUDIO_INPUT_BYTES, isAudioRecipe } from './audio-flow-input.js';
|
|
13
14
|
import { CREATION_KINDS } from './creation-request-store.js';
|
|
14
15
|
import { supportsImageEdit } from './provider-protocol.js';
|
|
@@ -33,9 +34,15 @@ import { CONNECTION_TEMPLATE_EXAMPLE } from './connection-template.js';
|
|
|
33
34
|
import { checkedBridgeCapabilities, DEFAULT_BRIDGE_CAPABILITIES } from './core-bridge-selection.js';
|
|
34
35
|
import { createCoreCapabilityCatalog, projectCoreSessionOperations, CORE_CAPABILITY_GROUPS } from './core-capability-catalog.js';
|
|
35
36
|
import { MCP_CONNECTION_PRESETS, MCP_CONFIG_FORMATS } from './mcp-delivery.js';
|
|
37
|
+
import { VIDEO_RECIPE, VIDEO_TYPES, MAX_VIDEO_BYTES } from './video-flow-input.js';
|
|
36
38
|
import { checkedPluginSelection, hasPluginSelection } from './plugin-selection.js';
|
|
37
39
|
import { readAllOperations, projectOperation } from './operation-store.js';
|
|
38
40
|
import { createWorkbenchAuth, WorkbenchAuthError } from './workbench-auth.js';
|
|
41
|
+
import { createProviderNetworkManager } from './network-routing.js';
|
|
42
|
+
import { providerNetworkRequirement } from './network-policy.js';
|
|
43
|
+
import { networkManagementSettings, manageNetworkResources, NETWORK_MANAGEMENT_ACTIONS } from './network-management.js';
|
|
44
|
+
import { networkDoctor, probeNetworkRoute } from './network-doctor.js';
|
|
45
|
+
import { createMediaScheduler } from './media-scheduler.js';
|
|
39
46
|
|
|
40
47
|
export const MAX_LOCAL_API_IMAGE_BYTES = 64 * 1024 * 1024;
|
|
41
48
|
const RUN = /^run_[a-f0-9]{24}$/;
|
|
@@ -54,16 +61,25 @@ function fail(code, message, status) { throw new LocalApiError(code, message, st
|
|
|
54
61
|
function httpError(error) {
|
|
55
62
|
if (error instanceof LocalApiError) return error;
|
|
56
63
|
if (error instanceof WorkbenchAuthError) return new LocalApiError(error.code, error.message, error.status);
|
|
64
|
+
if (error instanceof ProviderContractError && error.category === 'invalid_request') return new LocalApiError('IRIS_CONFIG_INPUT_INVALID',
|
|
65
|
+
'供应商配置或参数无效,请核对所选协议要求的 API 根地址、模型和参数', 400);
|
|
66
|
+
if (/^IRIS_NETWORK_/.test(error?.code || '')) return new LocalApiError(error.code, error.message, 409);
|
|
57
67
|
if (/^IRIS_PLUGIN_/.test(error?.code || '')) return new LocalApiError(error.code,
|
|
58
68
|
error.code === 'IRIS_PLUGIN_INPUT_INVALID' ? '业务输入不符合当前插件参数要求' : '插件业务未授权、缺失或版本不一致;请读取原 Operation 并核对准确插件版本',
|
|
59
69
|
error.code === 'IRIS_PLUGIN_INPUT_INVALID' ? 400 : error.code === 'IRIS_PLUGIN_OPERATION_DENIED' ? 403 : 409);
|
|
60
70
|
if (['IRIS_CONFIG_CHANGED', 'IRIS_CONFIG_BUSY'].includes(error?.code)) return new LocalApiError(error.code,
|
|
61
71
|
'配置已变化或有其他写入,请保留草稿并重新读取后核对', 409);
|
|
72
|
+
if (error?.code === 'IRIS_PROVIDER_CREDENTIAL_UNAVAILABLE') return new LocalApiError(error.code,
|
|
73
|
+
'环境凭据未就绪,请由所有者检查 Core 进程中的指定变量;不会改绑或重新提交任务', 409);
|
|
74
|
+
if (error?.providerCode === 'IRIS_PROVIDER_DIRECTORY_CREDENTIALS') return new LocalApiError(error.providerCode,
|
|
75
|
+
'上游模型目录包含认证内容,已拒绝读取与保存;请检查该服务的目录响应', 502);
|
|
62
76
|
if (error?.code === 'IRIS_CONFIG_INPUT_INVALID' || error?.code === 'IRIS_FUNCTION_POLICY_INVALID') return new LocalApiError('IRIS_CONFIG_INPUT_INVALID',
|
|
63
77
|
'设置无效,请检查地址、协议、模型用途和密钥动作', 400);
|
|
64
78
|
if (error?.code === 'IRIS_OPERATION_NOT_FOUND') return new LocalApiError(error.code, 'Operation 不存在', 404);
|
|
65
79
|
if (error?.code === 'IRIS_OPERATION_KEY_CONFLICT') return new LocalApiError(error.code, '该 UUID 已绑定其他业务、输入或依赖,请先查询原操作', 409);
|
|
66
80
|
if (error?.code === 'IRIS_OPERATION_QUEUE_FULL') return new LocalApiError(error.code, '活动操作已达上限,本次未接收', 429);
|
|
81
|
+
if (/^IRIS_SCHEDULER_(?:QUEUE_FULL|QUEUE_TIMEOUT)$/.test(error?.code || '')) return new LocalApiError(error.code, error.message, 429);
|
|
82
|
+
if (['IRIS_APP_OPERATION_BUSY', 'IRIS_PROVIDER_TASK_BUSY'].includes(error?.code)) return new LocalApiError(error.code, '原 Run 或 Task 正在执行,请查询原操作', 409);
|
|
67
83
|
if (error?.code === 'IRIS_OPERATION_INPUT_INVALID' || error?.code === 'IRIS_OPERATION_KEY_INVALID') return new LocalApiError(error.code, 'Operation 输入无效', 400);
|
|
68
84
|
if (/^IRIS_CONNECTION_/.test(error?.code || '')) {
|
|
69
85
|
const status = error.code === 'IRIS_CONNECTION_AUTH_REQUIRED' ? 401 : error.code === 'IRIS_CONNECTION_CAPABILITY_DENIED' ? 403
|
|
@@ -84,7 +100,7 @@ function httpError(error) {
|
|
|
84
100
|
if (error?.code === 'IRIS_CREATION_KEY_CONFLICT') return new LocalApiError('IRIS_API_KEY_CONFLICT', '该 Key 已绑定其他输入', 409);
|
|
85
101
|
if (error?.code === 'IRIS_CREATION_NOT_FOUND') return new LocalApiError('IRIS_API_NOT_FOUND', '该实例中没有此创建请求', 404);
|
|
86
102
|
if (['IRIS_CREATION_KEY_INVALID', 'IRIS_CREATION_INPUT_INVALID', 'IRIS_GENERATION_INPUT_INVALID',
|
|
87
|
-
'IRIS_IMAGE_IMPORT_INVALID', 'IRIS_IMAGE_EDIT_INPUT_INVALID', 'IRIS_FLOW_INPUT_INVALID', 'IRIS_COMFYUI_PARAMETERS_INVALID'].includes(error?.code)) {
|
|
103
|
+
'IRIS_IMAGE_IMPORT_INVALID', 'IRIS_IMAGE_EDIT_INPUT_INVALID', 'IRIS_FLOW_INPUT_INVALID', 'IRIS_VIDEO_FLOW_INPUT_INVALID', 'IRIS_COMFYUI_PARAMETERS_INVALID'].includes(error?.code)) {
|
|
88
104
|
return new LocalApiError('IRIS_API_INPUT_INVALID', '创作输入或静态图片无效,请检查来源、模型和参数', 400);
|
|
89
105
|
}
|
|
90
106
|
if (error?.code === 'IRIS_CREATION_MEDIA_MISMATCH') return new LocalApiError('IRIS_API_MEDIA_UNSUPPORTED', '图片真实格式与 Content-Type 不一致', 415);
|
|
@@ -114,7 +130,8 @@ function reportUnexpected(error, requestId) {
|
|
|
114
130
|
// Keep call sites for diagnosis, without the potentially private exception message or directory names.
|
|
115
131
|
const frames = String(error?.stack || '').split('\n').slice(1, 7)
|
|
116
132
|
.map(line => line.replace(/(?:file:\/\/)?(?:\/[^\s()]+)+(?=:\d+:\d+)/g, value => path.basename(value)));
|
|
117
|
-
|
|
133
|
+
const code = ['IRIS_SCHEDULER_QUEUE_TIMEOUT', 'IRIS_SCHEDULER_QUEUE_FULL', 'IRIS_SCHEDULER_CLOSED'].includes(error?.code) ? error.code : undefined;
|
|
134
|
+
console.error(JSON.stringify({ event: 'iris_local_api_error', requestId, ...(code ? { code } : {}), frames }));
|
|
118
135
|
}
|
|
119
136
|
|
|
120
137
|
function headerCount(request, name) {
|
|
@@ -160,7 +177,8 @@ const json = (request, response, value, status = 200) => send(request, response,
|
|
|
160
177
|
/** Internal Host, read-only by default. Construction performs no I/O and never binds a port. */
|
|
161
178
|
export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWrites = false, serveWorkbench = false,
|
|
162
179
|
allowExecution = false, allowSelection = false, allowMcp = false, connectionsFile, executionPorts, allowConnectionManagement = false, connectionStore, providerRegistry, pluginRegistry,
|
|
163
|
-
allowProviderManagement = false, panelManifestPath, panelContributions, allowedOperations, workbenchAuth
|
|
180
|
+
allowProviderManagement = false, allowNetworkManagement = false, panelManifestPath, panelContributions, allowedOperations, workbenchAuth, networkConfigPath, networkManager,
|
|
181
|
+
scheduling, scheduler: suppliedScheduler } = {}) {
|
|
164
182
|
if (typeof allowExecution !== 'boolean') throw new TypeError('allowExecution 必须是布尔值');
|
|
165
183
|
if (typeof allowSelection !== 'boolean') throw new TypeError('allowSelection 必须是布尔值');
|
|
166
184
|
if (typeof allowMcp !== 'boolean') throw new TypeError('allowMcp 必须是布尔值');
|
|
@@ -175,8 +193,13 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
175
193
|
fail('IRIS_API_CONFIG_INVALID', '工作台口令认证只接受明确的私有 directory,且仅用于 web serve');
|
|
176
194
|
}
|
|
177
195
|
if (typeof allowProviderManagement !== 'boolean') throw new TypeError('allowProviderManagement 必须是布尔值');
|
|
196
|
+
if (typeof allowNetworkManagement !== 'boolean') throw new TypeError('allowNetworkManagement 必须是布尔值');
|
|
197
|
+
if (allowNetworkManagement && !networkConfigPath) fail('IRIS_API_CONFIG_INVALID', '网络管理需要显式network-config路径');
|
|
178
198
|
if (pluginRegistry && providerRegistry && providerRegistry !== pluginRegistry.providers) fail('IRIS_API_CONFIG_INVALID', '插件与 Provider 注册器须来自同一装配实例');
|
|
179
199
|
const suppliedRegistry = pluginRegistry?.providers ?? providerRegistry;
|
|
200
|
+
if (networkConfigPath !== undefined && (typeof networkConfigPath !== 'string' || !path.isAbsolute(networkConfigPath))) fail('IRIS_API_CONFIG_INVALID', '网络文件必须是显式绝对路径');
|
|
201
|
+
if (networkManager && (typeof networkManager.resolveProviderNetwork !== 'function' || typeof networkManager.dispose !== 'function')) throw new TypeError('networkManager 需要实例解析与释放方法');
|
|
202
|
+
if ((networkConfigPath || scheduling || suppliedScheduler) && suppliedRegistry && !networkManager) fail('IRIS_API_CONFIG_INVALID', '借用 Provider 注册器并使用网络或调度配置时,请同时传入由调用者持有的网络管理实例,按顺序统一释放');
|
|
180
203
|
const capabilityCatalog = createCoreCapabilityCatalog({ registry: pluginRegistry });
|
|
181
204
|
const pluginExecution = allowedOperations !== undefined;
|
|
182
205
|
const selectedOperations = pluginExecution ? capabilityCatalog.selectOperations(allowedOperations) : [];
|
|
@@ -191,11 +214,18 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
191
214
|
if (allowExecution && !providerConfigPath && !executionPorts) fail('IRIS_API_CONFIG_INVALID', '执行模式需要显式 Provider 配置');
|
|
192
215
|
if (connectionsFile !== undefined && (typeof connectionsFile !== 'string' || !path.isAbsolute(connectionsFile))) fail('IRIS_API_CONFIG_INVALID', '连接授权须为显式根外文件');
|
|
193
216
|
let state = 'new', origin = null, tokenDigest = null, application, server, physicalRoot;
|
|
217
|
+
const scheduler = suppliedScheduler || networkManager?.scheduler || (scheduling ? createMediaScheduler(scheduling) : undefined);
|
|
218
|
+
if (scheduler && networkManager && networkManager.scheduler !== scheduler) fail('IRIS_API_CONFIG_INVALID', '应用与网络管理必须共用同一调度实例');
|
|
219
|
+
const ownedScheduler = Boolean(scheduler && !suppliedScheduler && !networkManager?.scheduler);
|
|
220
|
+
const ownedNetwork = !networkManager && (networkConfigPath !== undefined || Boolean(scheduler));
|
|
221
|
+
const networks = networkManager || (ownedNetwork ? createProviderNetworkManager({ networkConfigPath, scheduler }) : undefined);
|
|
222
|
+
const resolveProviderNetwork = ({ provider, ...context }) => networks?.resolveProviderNetwork({ ...context,
|
|
223
|
+
networkRequirement: context.networkRequirement || providerNetworkRequirement(provider) });
|
|
194
224
|
let browserAuth;
|
|
195
225
|
let providerMonitor;
|
|
196
|
-
let runner, runnerStarted = false, connections, mcp, managed, imageGenerationEnabled = false, audioEnabled = false;
|
|
226
|
+
let runner, runnerStarted = false, connections, mcp, managed, imageGenerationEnabled = false, audioEnabled = false, videoEnabled = false;
|
|
197
227
|
let registry = suppliedRegistry, panel = DEFAULT_PANEL, pluginPanel, providerManagementBusy = false;
|
|
198
|
-
const capabilityFacts = () => capabilityCatalog.snapshot({ audioEnabled, panelContributions: pluginPanel?.selection, allowedOperations: allowedPluginIds });
|
|
228
|
+
const capabilityFacts = () => capabilityCatalog.snapshot({ audioEnabled, videoEnabled, panelContributions: pluginPanel?.selection, allowedOperations: allowedPluginIds });
|
|
199
229
|
const changedAuthorizations = new Set();
|
|
200
230
|
let loginTicket = null;
|
|
201
231
|
const browserSessions = new Set();
|
|
@@ -214,7 +244,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
214
244
|
return [...facts.tools, ...facts.pluginTools].filter(tool => tool.scope !== 'execute'
|
|
215
245
|
|| (tool.name === 'iris_core_plugin_call' ? pluginExecution : !pluginExecution || allowExecution)).map(tool => tool.name);
|
|
216
246
|
};
|
|
217
|
-
const availableCapabilities = () => capabilityCatalog.snapshot({ audioEnabled }).groups;
|
|
247
|
+
const availableCapabilities = () => capabilityCatalog.snapshot({ audioEnabled, videoEnabled }).groups;
|
|
218
248
|
|
|
219
249
|
function issueLoginLink() {
|
|
220
250
|
const ticket = crypto.randomBytes(32).toString('base64url');
|
|
@@ -287,6 +317,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
287
317
|
async function requireArtifactCapability(caller, artifactId) {
|
|
288
318
|
if (caller.owner) return;
|
|
289
319
|
const metadata = await application.run('reader', runtime => runtime.run('inspect', ({ dataRoot }) => inspectCoreArtifactMetadata(dataRoot, artifactId)));
|
|
320
|
+
scope(caller, 'read'); // A revocation during the metadata read invalidates the original handshake too.
|
|
290
321
|
const selection = connections.selection(caller.connectionId, caller.runtimeInstanceId);
|
|
291
322
|
if (isPluginSourceImage(metadata) && (selection.pluginOperations || []).some(pin => selectedOperations.some(item => hasPluginSelection([pin], item.dependency) && item.artifactInputs?.length))) return;
|
|
292
323
|
if (metadata.metadata?.pluginOperationId) {
|
|
@@ -308,7 +339,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
308
339
|
// alone only grant the declared eligible source images handled above.
|
|
309
340
|
if (CORE_CAPABILITY_GROUPS.some(group => selection.capabilities.includes(group.id)
|
|
310
341
|
&& (metadata.mediaType.startsWith('image/') ? group.media === 'image'
|
|
311
|
-
: metadata.mediaType.startsWith('audio/') ? group.requiresAudio
|
|
342
|
+
: metadata.mediaType.startsWith('video/') ? group.media === 'video' : metadata.mediaType.startsWith('audio/') ? group.requiresAudio
|
|
312
343
|
: metadata.mediaType === 'text/plain' && group.media === 'transcribe'))) return;
|
|
313
344
|
fail('IRIS_CONNECTION_CAPABILITY_DENIED', '此连接没有选择该作品的媒体功能或图片来源用途', 403);
|
|
314
345
|
}
|
|
@@ -344,6 +375,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
344
375
|
}
|
|
345
376
|
if (url.pathname.startsWith('/api/v0/connection-management')) return manage(request, response, url, caller);
|
|
346
377
|
if (url.pathname.startsWith('/api/v0/provider-management')) return manageProviders(request, response, url, caller);
|
|
378
|
+
if (url.pathname.startsWith('/api/v0/network-management')) return manageNetworks(request, response, url, caller);
|
|
347
379
|
const selecting = /^\/api\/v0\/runs\/([^/]+)\/selection$/.exec(url.pathname);
|
|
348
380
|
if (selecting) return selectRun(request, response, selecting[1], caller);
|
|
349
381
|
const connection = /^\/api\/v0\/connections\/([a-f0-9-]{36})\/(handshake|disconnect|client-info)$/.exec(url.pathname);
|
|
@@ -380,10 +412,11 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
380
412
|
const result = await runner.submit({ key: request.headers['idempotency-key'], runId: continuing[1], expectedRunRevision: value.expectedRevision });
|
|
381
413
|
return json(request, response, result, result.idempotentReplay ? 200 : 202);
|
|
382
414
|
}
|
|
383
|
-
|
|
415
|
+
const creationTool = ({ '/api/v0/images': 'iris_core_import_image', '/api/v0/runs': 'iris_core_create_run',
|
|
384
416
|
'/api/v0/generations': 'iris_core_create_generation', '/api/v0/audio': 'iris_core_import_audio',
|
|
385
|
-
'/api/v0/speeches': 'iris_core_create_speech', '/api/v0/transcriptions': 'iris_core_create_transcription' })[url.pathname]
|
|
386
|
-
|
|
417
|
+
'/api/v0/speeches': 'iris_core_create_speech', '/api/v0/transcriptions': 'iris_core_create_transcription', '/api/v0/videos': 'iris_core_create_video' })[url.pathname];
|
|
418
|
+
scope(caller, 'create', creationTool);
|
|
419
|
+
return create(request, response, url, caller, creationTool);
|
|
387
420
|
}
|
|
388
421
|
|
|
389
422
|
async function selectRun(request, response, runId, caller) {
|
|
@@ -429,11 +462,11 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
429
462
|
if (['GET', 'HEAD'].includes(request.method) && url.pathname === '/api/v0/provider-management/settings') {
|
|
430
463
|
return json(request, response, await executeConfigCommand(providerConfigPath, 'settings.show', {}, { registry }));
|
|
431
464
|
}
|
|
432
|
-
const monitor = () => providerMonitor ||= createProviderMonitor({ configPath: providerConfigPath, registry });
|
|
465
|
+
const monitor = () => providerMonitor ||= createProviderMonitor({ configPath: providerConfigPath, registry, resolveProviderNetwork });
|
|
433
466
|
if (['GET', 'HEAD'].includes(request.method) && url.pathname === '/api/v0/provider-management/diagnostics') {
|
|
434
467
|
const settings = await executeConfigCommand(providerConfigPath, 'settings.show', {}, { registry });
|
|
435
468
|
return json(request, response, { ...projectProviderDiagnostics(loadProviderCatalog(providerConfigPath), registry), revision: settings.revision,
|
|
436
|
-
monitors: monitor().snapshot(), execution: allowExecution, imageGeneration: imageGenerationEnabled, audio: audioEnabled });
|
|
469
|
+
monitors: monitor().snapshot(), execution: allowExecution, imageGeneration: imageGenerationEnabled, audio: audioEnabled, video: videoEnabled });
|
|
437
470
|
}
|
|
438
471
|
const command = { '/api/v0/provider-management/save': 'settings.save', '/api/v0/provider-management/discover': 'settings.discover',
|
|
439
472
|
'/api/v0/provider-management/policies': 'settings.policies', '/api/v0/provider-management/enabled': 'settings.enabled',
|
|
@@ -466,11 +499,11 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
466
499
|
const result = await application.run('writer', runtime => runtime.run('execute', async ({ dataRoot }) => {
|
|
467
500
|
const dependencies = providerDependencies(dataRoot, value.providerId);
|
|
468
501
|
if (dependencies.blockingTasks.length || dependencies.blockingRuns.length) fail('IRIS_PROVIDER_REFERENCED', '当前工作区仍有未完成或可恢复流程依赖此供应商,请先处理原任务', 409);
|
|
469
|
-
return executeConfigCommand(providerConfigPath, command, value, { registry, signal: controller.signal });
|
|
502
|
+
return executeConfigCommand(providerConfigPath, command, value, { registry, signal: controller.signal, resolveProviderNetwork });
|
|
470
503
|
}), { signal: controller.signal });
|
|
471
504
|
providerMonitor?.snapshot(); return json(request, response, result);
|
|
472
505
|
}
|
|
473
|
-
const result = await executeConfigCommand(providerConfigPath, command, value, { registry, signal: controller.signal });
|
|
506
|
+
const result = await executeConfigCommand(providerConfigPath, command, value, { registry, signal: controller.signal, resolveProviderNetwork });
|
|
474
507
|
if (command === 'settings.enabled' || command === 'settings.save') providerMonitor?.snapshot();
|
|
475
508
|
return await json(request, response, result);
|
|
476
509
|
} catch (error) {
|
|
@@ -481,6 +514,28 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
481
514
|
throw error;
|
|
482
515
|
} finally { response.off('close', aborted); providerManagementBusy = false; }
|
|
483
516
|
}
|
|
517
|
+
async function manageNetworks(request, response, url, caller) {
|
|
518
|
+
if (!caller.owner) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '只有工作台所有者可以管理和检测网络出口', 403);
|
|
519
|
+
if (!allowNetworkManagement) fail('IRIS_API_READ_ONLY', '此服务未开启网络出口管理', 405);
|
|
520
|
+
query(url);
|
|
521
|
+
const options = { networkConfigPath, providerConfigPath, registry, resolveProviderNetwork };
|
|
522
|
+
const action = url.pathname.slice('/api/v0/network-management/'.length);
|
|
523
|
+
if (['GET', 'HEAD'].includes(request.method) && ['settings', 'doctor'].includes(action)) {
|
|
524
|
+
return json(request, response, action === 'settings' ? networkManagementSettings(options) : { ...networkDoctor(options), instanceId });
|
|
525
|
+
}
|
|
526
|
+
if (request.method !== 'POST' || ![...NETWORK_MANAGEMENT_ACTIONS, 'probe'].includes(action)) fail('IRIS_API_NOT_FOUND', '没有此网络管理操作', 404);
|
|
527
|
+
if (providerManagementBusy) fail('IRIS_API_BUSY', '供应商或出口设置正在处理,请等待完成', 429);
|
|
528
|
+
providerManagementBusy = true;
|
|
529
|
+
const controller = new AbortController(), aborted = () => { if (!response.writableFinished) controller.abort(); };
|
|
530
|
+
response.once('close', aborted);
|
|
531
|
+
try {
|
|
532
|
+
const input = await smallJson(request, 64 * 1024);
|
|
533
|
+
controller.signal.throwIfAborted();
|
|
534
|
+
const result = action === 'probe' ? await probeNetworkRoute(options, input, { signal: controller.signal })
|
|
535
|
+
: manageNetworkResources(options, action, input, { signal: controller.signal });
|
|
536
|
+
return await json(request, response, { ...result, instanceId });
|
|
537
|
+
} finally { response.off('close', aborted); providerManagementBusy = false; }
|
|
538
|
+
}
|
|
484
539
|
async function manage(request, response, url, caller) {
|
|
485
540
|
if (!caller.owner) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '只有工作台所有者可以管理连接', 403);
|
|
486
541
|
if (!allowConnectionManagement) fail('IRIS_API_READ_ONLY', '此服务未开启连接管理', 405);
|
|
@@ -536,12 +591,12 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
536
591
|
reader.start();
|
|
537
592
|
return await reader.run('inspect', ({ dataRoot: physicalRoot }) => {
|
|
538
593
|
const metadata = inspectCoreArtifactMetadata(physicalRoot, artifactId);
|
|
539
|
-
if (!(kind === 'image' ? IMAGE_TYPES.has(metadata.mediaType) : kind === 'audio' ? AUDIO_MEDIA_TYPES.has(metadata.mediaType) : metadata.mediaType === 'text/plain')) {
|
|
594
|
+
if (!(kind === 'image' ? IMAGE_TYPES.has(metadata.mediaType) : kind === 'video' ? Object.hasOwn(VIDEO_TYPES, metadata.mediaType) : kind === 'audio' ? AUDIO_MEDIA_TYPES.has(metadata.mediaType) : metadata.mediaType === 'text/plain')) {
|
|
540
595
|
fail('IRIS_API_MEDIA_UNSUPPORTED', '此入口不支持该作品类型', 415);
|
|
541
596
|
}
|
|
542
|
-
if (metadata.size > (kind === 'text' ? 1024 * 1024 : MAX_LOCAL_API_IMAGE_BYTES)) fail('IRIS_API_MEDIA_TOO_LARGE', '作品超过此读取入口的大小上限', 413);
|
|
597
|
+
if (metadata.size > (kind === 'text' ? 1024 * 1024 : kind === 'video' ? MAX_VIDEO_BYTES : MAX_LOCAL_API_IMAGE_BYTES)) fail('IRIS_API_MEDIA_TOO_LARGE', '作品超过此读取入口的大小上限', 413);
|
|
543
598
|
const result = readCoreArtifactBytes(physicalRoot, artifactId);
|
|
544
|
-
if (result.bytes.length > MAX_LOCAL_API_IMAGE_BYTES || result.bytes.length !== result.artifact.size
|
|
599
|
+
if (result.bytes.length > (kind === 'video' ? MAX_VIDEO_BYTES : MAX_LOCAL_API_IMAGE_BYTES) || result.bytes.length !== result.artifact.size
|
|
545
600
|
|| digest(result.bytes).toString('hex') !== result.artifact.digest.value) {
|
|
546
601
|
fail('IRIS_API_RECORD_CONFLICT', '图片字节与已保存记录不一致', 409);
|
|
547
602
|
}
|
|
@@ -559,6 +614,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
559
614
|
if (error.code === 'IRIS_PROVIDER_IMAGE_PROTOCOL_INVALID') {
|
|
560
615
|
return { contractVersion: 0, configured: true, models: [], total: 0, gap: 'protocol_unavailable' };
|
|
561
616
|
}
|
|
617
|
+
if (error.code === 'IRIS_PROVIDER_CREDENTIAL_UNAVAILABLE') return { contractVersion: 0, configured: true, models: [], total: 0, gap: 'credential_unavailable' };
|
|
562
618
|
if (error.code !== 'IRIS_PROVIDER_MODEL_UNAVAILABLE') throw error;
|
|
563
619
|
candidates = [];
|
|
564
620
|
}
|
|
@@ -576,15 +632,25 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
576
632
|
if (!capabilities.includes(capability)) continue;
|
|
577
633
|
let candidates;
|
|
578
634
|
try { candidates = select(catalog, '', registry); }
|
|
579
|
-
catch (error) { if (error.code
|
|
635
|
+
catch (error) { if (!['IRIS_PROVIDER_MODEL_UNAVAILABLE', 'IRIS_PROVIDER_CREDENTIAL_UNAVAILABLE'].includes(error.code)) throw error; candidates = []; }
|
|
580
636
|
entries.push(...candidates.map(route => ({ capability, modelRef: route.modelRef, displayName: route.model })));
|
|
581
637
|
}
|
|
582
638
|
return { contractVersion: 0, configured: true, models: entries, total: entries.length };
|
|
583
639
|
}
|
|
584
640
|
|
|
585
|
-
|
|
641
|
+
function videoModels() {
|
|
642
|
+
if (!providerConfigPath) return { contractVersion: 0, configured: false, models: [], total: 0 };
|
|
643
|
+
let candidates;
|
|
644
|
+
try { candidates = videoCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), '', registry); }
|
|
645
|
+
catch (error) { if (!['IRIS_PROVIDER_MODEL_UNAVAILABLE', 'IRIS_PROVIDER_CREDENTIAL_UNAVAILABLE'].includes(error.code)) throw error; candidates = []; }
|
|
646
|
+
const entries = candidates.filter(route => videoModelSupportsMode(route.model, 't2v')).map(route => ({ modelRef: route.modelRef, displayName: route.model }));
|
|
647
|
+
return { contractVersion: 0, configured: true, models: entries, total: entries.length };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function create(request, response, url, caller, creationTool) {
|
|
586
651
|
query(url);
|
|
587
|
-
if (!['/api/v0/images', '/api/v0/runs', '/api/v0/generations', '/api/v0/audio', '/api/v0/speeches', '/api/v0/transcriptions'].includes(url.pathname)) fail('IRIS_API_NOT_FOUND', '没有此本地创建接口', 404);
|
|
652
|
+
if (!['/api/v0/images', '/api/v0/runs', '/api/v0/generations', '/api/v0/audio', '/api/v0/speeches', '/api/v0/transcriptions', '/api/v0/videos'].includes(url.pathname)) fail('IRIS_API_NOT_FOUND', '没有此本地创建接口', 404);
|
|
653
|
+
if (url.pathname === '/api/v0/videos' && !videoEnabled) fail('IRIS_FLOW_VIDEO_ROOT_REQUIRED', '需要新的 execution init --video 根;原根不迁移', 409);
|
|
588
654
|
const isImage = url.pathname === '/api/v0/images', isAudio = url.pathname === '/api/v0/audio', upload = isImage || isAudio;
|
|
589
655
|
if (!audioEnabled && ['/api/v0/audio', '/api/v0/speeches', '/api/v0/transcriptions'].includes(url.pathname)) fail('IRIS_FLOW_AUDIO_ROOT_REQUIRED', '请使用新的音频执行根;原根不迁移', 409);
|
|
590
656
|
if (headerCount(request, 'idempotency-key') !== 1 || !KEY.test(request.headers['idempotency-key'] || '')) {
|
|
@@ -624,8 +690,18 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
624
690
|
try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
|
|
625
691
|
catch (_) { fail('IRIS_API_INPUT_INVALID', '需要有效的 UTF-8 JSON'); }
|
|
626
692
|
}
|
|
693
|
+
if (url.pathname === '/api/v0/runs' || url.pathname === '/api/v0/transcriptions') {
|
|
694
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
695
|
+
|| typeof value.sourceArtifactId !== 'string' || !ARTIFACT.test(value.sourceArtifactId)) {
|
|
696
|
+
fail('IRIS_API_INPUT_INVALID', '需要当前数据根中的有效来源 Artifact ID');
|
|
697
|
+
}
|
|
698
|
+
// Input reuse follows the existing media/shared-source grant. Knowing an ID
|
|
699
|
+
// does not authorize a new Run to read or upload another capability's media.
|
|
700
|
+
await requireArtifactCapability(caller, value.sourceArtifactId);
|
|
701
|
+
scope(caller, 'create', creationTool);
|
|
702
|
+
}
|
|
627
703
|
const command = { '/api/v0/images': 'creation.image', '/api/v0/audio': 'creation.audio', '/api/v0/runs': 'creation.flow',
|
|
628
|
-
'/api/v0/generations': 'creation.generate', '/api/v0/speeches': 'creation.speech', '/api/v0/transcriptions': 'creation.transcribe' }[url.pathname];
|
|
704
|
+
'/api/v0/generations': 'creation.generate', '/api/v0/speeches': 'creation.speech', '/api/v0/transcriptions': 'creation.transcribe', '/api/v0/videos': 'creation.video' }[url.pathname];
|
|
629
705
|
const result = await application.execute(command, {
|
|
630
706
|
key: request.headers['idempotency-key'], ...(upload ? { bytes, mediaType } : { value })
|
|
631
707
|
}, { signal: controller.signal, onPrepared: () => { prepared = true; } });
|
|
@@ -720,8 +796,9 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
720
796
|
const connectionPost = connections && /^\/api\/v0\/connections\/[a-f0-9-]{36}\/(?:handshake|disconnect|client-info)$/.test(request.url.split('?')[0]);
|
|
721
797
|
const managementPost = allowConnectionManagement && request.url.split('?')[0].startsWith('/api/v0/connection-management/');
|
|
722
798
|
const providerPost = allowProviderManagement && request.url.split('?')[0].startsWith('/api/v0/provider-management/');
|
|
799
|
+
const networkPost = allowNetworkManagement && request.url.split('?')[0].startsWith('/api/v0/network-management/');
|
|
723
800
|
const pluginPost = pluginExecution && (request.url.split('?')[0] === '/api/v0/plugin-operations' || /^\/api\/v0\/operations\/op_[a-f0-9]{64}\/cancel$/.test(request.url.split('?')[0]));
|
|
724
|
-
if (!['GET', 'HEAD'].includes(request.method) && !((allowLocalWrites || allowSelection || connectionPost || managementPost || providerPost || pluginPost) && request.method === 'POST')) {
|
|
801
|
+
if (!['GET', 'HEAD'].includes(request.method) && !((allowLocalWrites || allowSelection || connectionPost || managementPost || providerPost || networkPost || pluginPost) && request.method === 'POST')) {
|
|
725
802
|
fail('IRIS_API_READ_ONLY', allowLocalWrites || allowSelection ? '仅支持 GET/HEAD 与明确开启的 POST 操作' : '当前接口仅支持只读 GET/HEAD', 405);
|
|
726
803
|
}
|
|
727
804
|
if (request.method !== 'POST' && (request.headers['transfer-encoding'] !== undefined || (request.headers['content-length'] !== undefined && request.headers['content-length'] !== '0'))) {
|
|
@@ -733,10 +810,12 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
733
810
|
if (request.method === 'POST') return specialPost(request, response, url, caller);
|
|
734
811
|
if (url.pathname.startsWith('/api/v0/connection-management')) return manage(request, response, url, caller);
|
|
735
812
|
if (url.pathname.startsWith('/api/v0/provider-management')) return manageProviders(request, response, url, caller);
|
|
813
|
+
if (url.pathname.startsWith('/api/v0/network-management')) return manageNetworks(request, response, url, caller);
|
|
736
814
|
const callerScopes = scope(caller, 'read');
|
|
737
815
|
const matchRun = /^\/api\/v0\/runs\/([^/]+)(\/download)?$/.exec(url.pathname);
|
|
738
816
|
const matchImage = /^\/api\/v0\/artifacts\/([^/]+)\/content$/.exec(url.pathname);
|
|
739
|
-
const
|
|
817
|
+
const matchMedia = audioEnabled && /^\/api\/v0\/artifacts\/([^/]+)\/(audio|text)$/.exec(url.pathname)
|
|
818
|
+
|| videoEnabled && /^\/api\/v0\/artifacts\/([^/]+)\/(video)$/.exec(url.pathname);
|
|
740
819
|
const operation = (allowExecution || pluginExecution) && /^\/api\/v0\/operations\/(op_[a-f0-9]{64})$/.exec(url.pathname);
|
|
741
820
|
const runOperations = allowExecution && /^\/api\/v0\/runs\/(run_[a-f0-9]{24})\/operations$/.exec(url.pathname);
|
|
742
821
|
const operationLookup = (allowExecution || pluginExecution) && url.pathname === '/api/v0/operations';
|
|
@@ -746,16 +825,16 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
746
825
|
if (creationLookup && !caller.owner) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '只有工作台所有者可以定位原创建请求', 403);
|
|
747
826
|
query(url, pluginCatalog ? ['limit', 'offset'] : pluginHistory ? ['limit', 'cursor'] : url.pathname === '/api/v0/runs' ? ['limit', 'cursor', 'selection', 'kind'] : runOperations ? ['limit', 'cursor']
|
|
748
827
|
: creationLookup ? ['kind', 'key'] : operationLookup ? ['key'] : []);
|
|
749
|
-
if (matchRun && !RUN.test(matchRun[1]) || matchImage && !ARTIFACT.test(matchImage[1]) ||
|
|
828
|
+
if (matchRun && !RUN.test(matchRun[1]) || matchImage && !ARTIFACT.test(matchImage[1]) || matchMedia && !ARTIFACT.test(matchMedia[1])) {
|
|
750
829
|
fail('IRIS_API_INPUT_INVALID', '流程或图片 ID 格式无效');
|
|
751
830
|
}
|
|
752
831
|
const readTool = pluginCatalog ? 'iris_core_plugin_catalog' : pluginHistory ? 'iris_core_operation' : operation ? 'iris_core_operation' : runOperations ? 'iris_core_operation' : operationLookup ? 'iris_core_lookup'
|
|
753
|
-
:
|
|
832
|
+
: matchMedia ? matchMedia[2] === 'video' ? 'iris_core_video' : matchMedia[2] === 'audio' ? 'iris_core_audio' : 'iris_core_transcript'
|
|
754
833
|
: matchImage ? 'iris_core_image' : matchRun ? matchRun[2] ? null : 'iris_core_run'
|
|
755
|
-
: ({ '/api/v0/models': 'iris_core_models', '/api/v0/audio-models': 'iris_core_audio_models', '/api/v0/runs': 'iris_core_runs', '/api/v0/connections': 'iris_core_connections', '/api/v0/adapters': 'iris_core_connection' })[url.pathname];
|
|
834
|
+
: ({ '/api/v0/models': 'iris_core_models', '/api/v0/video-models': 'iris_core_video_models', '/api/v0/audio-models': 'iris_core_audio_models', '/api/v0/runs': 'iris_core_runs', '/api/v0/connections': 'iris_core_connections', '/api/v0/adapters': 'iris_core_connection' })[url.pathname];
|
|
756
835
|
if (readTool) scope(caller, 'read', readTool);
|
|
757
836
|
if (matchRun?.[2] && !caller.owner && !connections.selection(caller.connectionId, caller.runtimeInstanceId).tools
|
|
758
|
-
.some(name => ['iris_core_image', 'iris_core_audio', 'iris_core_transcript'].includes(name))) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '未选择作品交付工具', 403);
|
|
837
|
+
.some(name => ['iris_core_image', 'iris_core_audio', 'iris_core_transcript', 'iris_core_video'].includes(name))) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '未选择作品交付工具', 403);
|
|
759
838
|
if (activeReads >= 2) fail('IRIS_API_BUSY', '读取繁忙,请稍后再试', 429);
|
|
760
839
|
activeReads++;
|
|
761
840
|
try {
|
|
@@ -800,15 +879,17 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
800
879
|
return await json(request, response, { contractVersion: 0, instanceId, readOnly: mediaReadOnly,
|
|
801
880
|
...(identity ? { rootId: identity.rootId } : {}),
|
|
802
881
|
connectionManagement: Boolean(caller.owner && allowConnectionManagement), workbenchMediaReadOnly: serveWorkbench && mediaReadOnly,
|
|
803
|
-
providerManagement: Boolean(caller.owner && allowProviderManagement),
|
|
882
|
+
providerManagement: Boolean(caller.owner && allowProviderManagement), networkManagement: Boolean(caller.owner && allowNetworkManagement),
|
|
883
|
+
...(serveWorkbench && caller.owner ? { panel: panel.identity } : {}),
|
|
804
884
|
...(pluginPanel && caller.owner ? { panelContributions: pluginPanel.manifest } : {}),
|
|
805
885
|
...(pluginExecution ? { pluginExecution: true, pluginOperations: caller.owner ? selectedOperations.map(item => item.dependency) : selection?.pluginOperations || [] } : {}),
|
|
806
|
-
...(audioEnabled ? { audio: true } : {}),
|
|
886
|
+
...(audioEnabled ? { audio: true } : {}), ...(videoEnabled ? { video: true } : {}),
|
|
807
887
|
operations: projectCoreSessionOperations({ scopes: callerScopes, ...(selection ? { tools: selection.tools } : {}),
|
|
808
|
-
imageGeneration: imageGenerationEnabled, audio: audioEnabled, execution: allowExecution, pluginExecution,
|
|
888
|
+
imageGeneration: imageGenerationEnabled, audio: audioEnabled, video: videoEnabled, execution: allowExecution, pluginExecution,
|
|
809
889
|
creationLookup: Boolean(identity?.rootId), selection: canSelect }),
|
|
810
890
|
...(selection || {}),
|
|
811
891
|
...(allowExecution || pluginExecution ? { executionEnabled: runner.snapshot().executionEnabled } : {}),
|
|
892
|
+
...(caller.owner && scheduler ? { scheduling: scheduler.snapshot(physicalRoot) } : {}),
|
|
812
893
|
limits: { runPageSize: 30, imageBytes: MAX_LOCAL_API_IMAGE_BYTES, activeReads: 2,
|
|
813
894
|
...(allowLocalWrites ? { uploadBytes: MAX_IMPORT_IMAGE_BYTES, jsonBytes: 64 * 1024, activeUploads: 1, localWrites: 9 } : {}) } });
|
|
814
895
|
}
|
|
@@ -819,6 +900,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
819
900
|
if (operation) {
|
|
820
901
|
const result = await application.execute('operation.inspect', { id: operation[1] });
|
|
821
902
|
await requireOperationCapability(caller, result.operation);
|
|
903
|
+
if (runner?.scheduling(operation[1])) result.operation.scheduling = runner.scheduling(operation[1]);
|
|
822
904
|
return await json(request, response, result);
|
|
823
905
|
}
|
|
824
906
|
if (creationLookup) {
|
|
@@ -830,6 +912,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
830
912
|
if (!KEY.test(url.searchParams.get('key') || '')) fail('IRIS_API_INPUT_INVALID', '需要原 Operation Key');
|
|
831
913
|
const result = await application.execute('operation.lookup', { key: url.searchParams.get('key') });
|
|
832
914
|
await requireOperationCapability(caller, result.operation);
|
|
915
|
+
if (runner?.scheduling(result.operation.id)) result.operation.scheduling = runner.scheduling(result.operation.id);
|
|
833
916
|
return await json(request, response, result);
|
|
834
917
|
}
|
|
835
918
|
if (runOperations) {
|
|
@@ -840,6 +923,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
840
923
|
...(limit !== null ? { limit: Number(limit) } : {}), ...(cursor !== null ? { cursor } : {}) }));
|
|
841
924
|
}
|
|
842
925
|
if (url.pathname === '/api/v0/models') return await json(request, response, models());
|
|
926
|
+
if (url.pathname === '/api/v0/video-models' && videoEnabled) return await json(request, response, videoModels());
|
|
843
927
|
if (url.pathname === '/api/v0/audio-models' && audioEnabled) return await json(request, response, audioModels(caller));
|
|
844
928
|
if (url.pathname === '/api/v0/runs') {
|
|
845
929
|
const limitText = url.searchParams.get('limit') ?? '20';
|
|
@@ -849,10 +933,10 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
849
933
|
const selection = url.searchParams.get('selection') ?? 'all';
|
|
850
934
|
if (!['all', 'selected', 'unselected'].includes(selection)) fail('IRIS_API_INPUT_INVALID', '选择筛选无效');
|
|
851
935
|
const kind = url.searchParams.get('kind') ?? 'all';
|
|
852
|
-
if (!['all', 'image', 'speech', 'transcribe'].includes(kind)) fail('IRIS_API_INPUT_INVALID', '作品类别无效');
|
|
936
|
+
if (!['all', 'image', 'speech', 'transcribe', 'video'].includes(kind)) fail('IRIS_API_INPUT_INVALID', '作品类别无效');
|
|
853
937
|
const records = (await application.run('reader', runtime => runtime.run('inspect', ({ dataRoot }) => listFlowRuns(dataRoot))))
|
|
854
|
-
.filter(run => allowsRun(caller, run) && (kind === 'all' || kind === 'image' && !isAudioRecipe(run.recipe)
|
|
855
|
-
|| kind === 'speech' && run.recipe === 'speech/v0' || kind === 'transcribe' && run.recipe === 'transcribe/v0'));
|
|
938
|
+
.filter(run => allowsRun(caller, run) && (kind === 'all' || kind === 'image' && !isAudioRecipe(run.recipe) && run.recipe !== VIDEO_RECIPE
|
|
939
|
+
|| kind === 'video' && run.recipe === VIDEO_RECIPE || kind === 'speech' && run.recipe === 'speech/v0' || kind === 'transcribe' && run.recipe === 'transcribe/v0'));
|
|
856
940
|
const selected = records.filter(run => Boolean(run.selection)).length;
|
|
857
941
|
const counts = { all: records.length, selected, unselected: records.length - selected };
|
|
858
942
|
const ordered = records.filter(run => selection === 'all' || Boolean(run.selection) === (selection === 'selected'))
|
|
@@ -863,15 +947,15 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
863
947
|
return await json(request, response, { contractVersion: 0, total: ordered.length, counts,
|
|
864
948
|
runs: page.map(projectLocalApiRunSummary), nextCursor: index + page.length < ordered.length ? page.at(-1).id : null });
|
|
865
949
|
}
|
|
866
|
-
let artifactId, mediaKind =
|
|
950
|
+
let artifactId, mediaKind = matchMedia ? matchMedia[2] : 'image';
|
|
867
951
|
if (matchRun) {
|
|
868
952
|
await requireFlowCapability(caller, matchRun[1]);
|
|
869
953
|
const view = projectLocalApiFlow(await application.execute('flow.inspect', { run_id: matchRun[1] }));
|
|
870
954
|
if (!matchRun[2]) return await json(request, response, view);
|
|
871
|
-
const audio = isAudioRecipe(view.recipe);
|
|
872
|
-
if (audio) {
|
|
873
|
-
mediaKind = view.recipe === 'speech/v0' ? 'audio' : 'text';
|
|
874
|
-
scope(caller, 'read', mediaKind === 'audio' ? 'iris_core_audio' : 'iris_core_transcript');
|
|
955
|
+
const audio = isAudioRecipe(view.recipe), video = view.recipe === VIDEO_RECIPE;
|
|
956
|
+
if (audio || video) {
|
|
957
|
+
mediaKind = video ? 'video' : view.recipe === 'speech/v0' ? 'audio' : 'text';
|
|
958
|
+
scope(caller, 'read', mediaKind === 'video' ? 'iris_core_video' : mediaKind === 'audio' ? 'iris_core_audio' : 'iris_core_transcript');
|
|
875
959
|
if (!view.output) fail('IRIS_API_SELECTION_REQUIRED', '请先接回原任务结果', 409);
|
|
876
960
|
artifactId = view.output.artifactId;
|
|
877
961
|
} else {
|
|
@@ -880,12 +964,12 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
880
964
|
artifactId = view.selection.artifactId;
|
|
881
965
|
}
|
|
882
966
|
} else if (matchImage) artifactId = matchImage[1];
|
|
883
|
-
else if (
|
|
967
|
+
else if (matchMedia) artifactId = matchMedia[1];
|
|
884
968
|
else fail('IRIS_API_NOT_FOUND', '没有此只读接口', 404);
|
|
885
969
|
await requireArtifactCapability(caller, artifactId);
|
|
886
970
|
const result = await image(artifactId, mediaKind);
|
|
887
971
|
return await send(request, response, 200, result.bytes, { 'Content-Type': result.artifact.mediaType,
|
|
888
|
-
...(matchRun ? { 'Content-Disposition': `attachment; filename="iris-${artifactId}.${IMAGE_TYPES.get(result.artifact.mediaType) || AUDIO_TYPES[result.artifact.mediaType] || 'txt'}"` } : {}) });
|
|
972
|
+
...(matchRun ? { 'Content-Disposition': `attachment; filename="iris-${artifactId}.${IMAGE_TYPES.get(result.artifact.mediaType) || AUDIO_TYPES[result.artifact.mediaType] || VIDEO_TYPES[result.artifact.mediaType] || 'txt'}"` } : {}) });
|
|
889
973
|
} finally { activeReads--; }
|
|
890
974
|
}
|
|
891
975
|
|
|
@@ -899,7 +983,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
899
983
|
error: { code: safe.code, message: safe.message, requestId } })), {
|
|
900
984
|
Connection: 'close', ...(safe.status === 401 ? { 'WWW-Authenticate': 'Bearer realm="iris-local"' } : {}),
|
|
901
985
|
...(safe.status === 405 ? { Allow: safe.allow || (safe.code === 'IRIS_API_METHOD_DENIED' ? 'POST'
|
|
902
|
-
: allowLocalWrites || allowSelection || allowConnectionManagement || allowProviderManagement ? 'GET, HEAD, POST' : 'GET, HEAD') } : {}) });
|
|
986
|
+
: allowLocalWrites || allowSelection || allowConnectionManagement || allowProviderManagement || allowNetworkManagement ? 'GET, HEAD, POST' : 'GET, HEAD') } : {}) });
|
|
903
987
|
}
|
|
904
988
|
}
|
|
905
989
|
|
|
@@ -918,7 +1002,9 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
918
1002
|
['application', () => application?.dispose()], ['requests', () => Promise.all([...jobs])],
|
|
919
1003
|
['connections-store', () => managed?.dispose()], ['connections', () => connections?.dispose()],
|
|
920
1004
|
['workbench-auth', () => browserAuth?.dispose()],
|
|
921
|
-
['providers', () => suppliedRegistry ? undefined : registry?.dispose()]
|
|
1005
|
+
['providers', () => suppliedRegistry ? undefined : registry?.dispose()],
|
|
1006
|
+
['network', () => ownedNetwork ? networks.dispose() : undefined],
|
|
1007
|
+
['scheduler', () => ownedScheduler ? scheduler.dispose() : undefined]
|
|
922
1008
|
]) {
|
|
923
1009
|
try { await close(); } catch (error) { failures.push({ stage, error }); }
|
|
924
1010
|
}
|
|
@@ -946,7 +1032,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
946
1032
|
physicalRoot = await reader.run('inspect', ({ dataRoot: root }) => {
|
|
947
1033
|
inspectFlowRoot(root);
|
|
948
1034
|
const version = inspectCreationRoot(root, { allowAbsent: !allowLocalWrites })?.schemaVersion;
|
|
949
|
-
imageGenerationEnabled = [2, 3].includes(version); audioEnabled = version ===
|
|
1035
|
+
imageGenerationEnabled = [2, 3, 4].includes(version); audioEnabled = [3, 4].includes(version); videoEnabled = version === 4;
|
|
950
1036
|
return root;
|
|
951
1037
|
});
|
|
952
1038
|
} finally { await reader.dispose(); }
|
|
@@ -955,7 +1041,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
955
1041
|
let ports = {};
|
|
956
1042
|
if (allowExecution) {
|
|
957
1043
|
if (!executionPorts) loadProviderCatalog(providerConfigPath); // Static validation only; no discovery or model call.
|
|
958
|
-
ports = executionPorts || configuredImagePorts(providerConfigPath, registry);
|
|
1044
|
+
ports = executionPorts || configuredImagePorts(providerConfigPath, registry, { resolveProviderNetwork, deferCooling: Boolean(scheduler) });
|
|
959
1045
|
}
|
|
960
1046
|
let grants = [];
|
|
961
1047
|
if (connectionsFile) {
|
|
@@ -983,11 +1069,13 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
983
1069
|
}
|
|
984
1070
|
for (const grant of dynamic) connections.setGrant(grant);
|
|
985
1071
|
}
|
|
986
|
-
application = createApplicationRuntime({ dataRoot: physicalRoot, ...ports,
|
|
987
|
-
...(!allowExecution && allowLocalWrites && providerConfigPath ? {
|
|
1072
|
+
application = createApplicationRuntime({ dataRoot: physicalRoot, ...ports, scheduler,
|
|
1073
|
+
...(!allowExecution && allowLocalWrites && providerConfigPath ? { resolveVideoCandidates: ({ modelRef }) => videoCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), modelRef, registry).map(route => ({
|
|
1074
|
+
adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol, networkRequirement: providerNetworkRequirement(route.provider) }, model: route.modelRef,
|
|
1075
|
+
providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit' })), resolveAudioCandidates: ({ capability, modelRef }) => {
|
|
988
1076
|
const select = capability === 'tts' ? ttsCandidatesFromCatalog : transcribeCandidatesFromCatalog;
|
|
989
1077
|
return select(loadProviderCatalog(providerConfigPath), modelRef, registry).map(route => ({
|
|
990
|
-
adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol }, model: route.modelRef,
|
|
1078
|
+
adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol, networkRequirement: providerNetworkRequirement(route.provider) }, model: route.modelRef,
|
|
991
1079
|
providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
|
|
992
1080
|
}));
|
|
993
1081
|
}, resolveImageCandidates: ({ modelRef, input }) =>
|
|
@@ -997,7 +1085,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
|
|
|
997
1085
|
compileComfyWorkflow(route.provider.comfyWorkflow, input);
|
|
998
1086
|
}
|
|
999
1087
|
return {
|
|
1000
|
-
adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol }, model: route.modelRef,
|
|
1088
|
+
adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol, networkRequirement: providerNetworkRequirement(route.provider) }, model: route.modelRef,
|
|
1001
1089
|
supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, registry),
|
|
1002
1090
|
providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
|
|
1003
1091
|
};
|
package/lib/local-creation.js
CHANGED
|
@@ -6,6 +6,7 @@ import { readImportImage } from './image-import.js';
|
|
|
6
6
|
import { normalizeGenerationInput } from './generation-input.js';
|
|
7
7
|
import { readFlowRun, requireAudioRoot } from './flow-store.js';
|
|
8
8
|
import { readImportAudio, normalizeAudioFlowInput } from './audio-flow-input.js';
|
|
9
|
+
import { VIDEO_RECIPE, normalizeVideoFlowInput } from './video-flow-input.js';
|
|
9
10
|
import { initializeCreationRoot, inspectCreationRoot, creationRequestId, creationInputDigest,
|
|
10
11
|
readCreationRequest, publishCreationRequest, completeCreationRequest, creationFail, CREATION_KINDS, isImportCreation } from './creation-request-store.js';
|
|
11
12
|
|
|
@@ -32,8 +33,8 @@ function flowInput(input, generate = false) {
|
|
|
32
33
|
return { ...(normalized.sourceArtifactId ? { source_artifact_id: normalized.sourceArtifactId } : {}), model_ref: normalized.modelRef,
|
|
33
34
|
...normalized.providerInput };
|
|
34
35
|
}
|
|
35
|
-
export function createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, onPrepared } = {}) {
|
|
36
|
-
const flows = createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates });
|
|
36
|
+
export function createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, onPrepared } = {}) {
|
|
37
|
+
const flows = createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates });
|
|
37
38
|
function execute(name, input) {
|
|
38
39
|
if (name === 'creation.root' || name === 'creation.lookup') {
|
|
39
40
|
return runtime.run('inspect', ({ dataRoot, signal }) => {
|
|
@@ -46,7 +47,8 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, re
|
|
|
46
47
|
}
|
|
47
48
|
const marker = inspectCreationRoot(dataRoot, { allowAbsent: true });
|
|
48
49
|
if (name === 'creation.root') return { contractVersion: 0, rootId: marker?.rootId ?? null,
|
|
49
|
-
...([2, 3].includes(marker?.schemaVersion) ? { imageGeneration: true } : {}), ...(marker?.schemaVersion
|
|
50
|
+
...([2, 3, 4].includes(marker?.schemaVersion) ? { imageGeneration: true } : {}), ...([3, 4].includes(marker?.schemaVersion) ? { audio: true } : {}),
|
|
51
|
+
...(marker?.schemaVersion === 4 ? { video: true } : {}) };
|
|
50
52
|
if (!marker) creationFail('IRIS_CREATION_NOT_FOUND', '此数据根尚无创建请求区');
|
|
51
53
|
const id = creationRequestId(marker, input.kind, input.key);
|
|
52
54
|
const request = readCreationRequest(dataRoot, id, marker);
|
|
@@ -86,7 +88,7 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, re
|
|
|
86
88
|
return runtime.run('execute', async ({ dataRoot, signal }) => {
|
|
87
89
|
if (name === 'creation.init') return { contractVersion: 0, root: initializeCreationRoot(dataRoot) };
|
|
88
90
|
const kinds = { 'creation.image': 'image-import', 'creation.flow': 'flow-create', 'creation.generate': 'generation-create',
|
|
89
|
-
'creation.audio': 'audio-import', 'creation.speech': 'speech-create', 'creation.transcribe': 'transcription-create' };
|
|
91
|
+
'creation.audio': 'audio-import', 'creation.speech': 'speech-create', 'creation.transcribe': 'transcription-create', 'creation.video': 'video-create' };
|
|
90
92
|
const kind = kinds[name];
|
|
91
93
|
if (!kind) throw new TypeError('未知本地创建操作');
|
|
92
94
|
if (['audio-import', 'speech-create', 'transcription-create'].includes(kind)) requireAudioRoot(dataRoot);
|
|
@@ -102,6 +104,11 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, re
|
|
|
102
104
|
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !allowed.includes(key))) creationFail('IRIS_CREATION_INPUT_INVALID', '音频创建输入无效');
|
|
103
105
|
owned = normalizeAudioFlowInput(speech ? 'speech/v0' : 'transcribe/v0', { model_ref: value.modelRef,
|
|
104
106
|
...(speech ? { text: value.text, ...(value.voice !== undefined ? { voice: value.voice } : {}) } : { source_artifact_id: value.sourceArtifactId }) });
|
|
107
|
+
} else if (kind === 'video-create') {
|
|
108
|
+
const value = input.value;
|
|
109
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['modelRef', 'prompt', 'size', 'duration'].includes(key))) creationFail('IRIS_CREATION_INPUT_INVALID', '文生视频输入无效');
|
|
110
|
+
owned = normalizeVideoFlowInput({ model_ref: value.modelRef, prompt: value.prompt,
|
|
111
|
+
...(value.size !== undefined ? { size: value.size } : {}), ...(value.duration !== undefined ? { duration: value.duration } : {}) });
|
|
105
112
|
} else owned = flowInput(input.value, kind === 'generation-create');
|
|
106
113
|
const inputDigest = creationInputDigest(kind, owned);
|
|
107
114
|
let request = readCreationRequest(dataRoot, id, marker);
|
|
@@ -112,7 +119,7 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, re
|
|
|
112
119
|
? { artifactId: 'artifact_' + crypto.randomBytes(12).toString('hex'), ...owned,
|
|
113
120
|
metadata: source.metadata, createdAt: new Date().toISOString() }
|
|
114
121
|
: await flows.prepareCreation(dataRoot, signal, { ...owned, ...(kind === 'speech-create' ? { recipe: 'speech/v0' }
|
|
115
|
-
: kind === 'transcription-create' ? { recipe: 'transcribe/v0' } : {}) });
|
|
122
|
+
: kind === 'transcription-create' ? { recipe: 'transcribe/v0' } : kind === 'video-create' ? { recipe: VIDEO_RECIPE } : {}) });
|
|
116
123
|
signal.throwIfAborted();
|
|
117
124
|
request = publishCreationRequest(dataRoot, { schemaVersion: 0, id, rootId: marker.rootId, kind,
|
|
118
125
|
inputDigest, createdAt: intent.createdAt, intent, state: 'prepared' });
|
package/lib/local-operations.js
CHANGED
|
@@ -8,8 +8,8 @@ export function createLocalOperationService(runtime) {
|
|
|
8
8
|
return runtime.run(readOnly ? 'inspect' : 'execute', ({ dataRoot, signal }) => {
|
|
9
9
|
signal.throwIfAborted();
|
|
10
10
|
if (name === 'execution.init') {
|
|
11
|
-
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !['imageGeneration', 'audio'].includes(key))
|
|
12
|
-
|| ['imageGeneration', 'audio'].some(key => input[key] !== undefined && typeof input[key] !== 'boolean')) operationFail('IRIS_OPERATION_INPUT_INVALID', 'execution.init 只接受可选 imageGeneration/audio 开关');
|
|
11
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !['imageGeneration', 'audio', 'video'].includes(key))
|
|
12
|
+
|| ['imageGeneration', 'audio', 'video'].some(key => input[key] !== undefined && typeof input[key] !== 'boolean')) operationFail('IRIS_OPERATION_INPUT_INVALID', 'execution.init 只接受可选 imageGeneration/audio/video 开关');
|
|
13
13
|
return { contractVersion: 0, root: initializeExecutionRoot(dataRoot, input), executionEnabled: false };
|
|
14
14
|
}
|
|
15
15
|
if (name === 'operation.accept') {
|
|
@@ -36,7 +36,7 @@ export function inspectLocalRequestRoot(root, { allowAbsent = false } = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
try {
|
|
38
38
|
const versions = fs.readdirSync(base);
|
|
39
|
-
if (versions.length !== 1 || !['v0', 'v1', 'v2', 'v3'].includes(versions[0])) invalid();
|
|
39
|
+
if (versions.length !== 1 || !['v0', 'v1', 'v2', 'v3', 'v4'].includes(versions[0])) invalid();
|
|
40
40
|
const schemaVersion = Number(versions[0].slice(1)), version = directory(root, ['creation-requests', versions[0]]);
|
|
41
41
|
const allowed = schemaVersion === 0 ? ['root.json', 'requests'] : ['root.json', 'requests', 'operations', 'runner'];
|
|
42
42
|
if (fs.readdirSync(version).some(name => !allowed.includes(name) && !/^root\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name))) invalid();
|
|
@@ -62,7 +62,7 @@ export function inspectLocalRequestRoot(root, { allowAbsent = false } = {}) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function initializeLocalRequestRoot(root, schemaVersion) {
|
|
65
|
-
if (![0, 1, 2, 3].includes(schemaVersion)) throw new TypeError('未知请求容器版本');
|
|
65
|
+
if (![0, 1, 2, 3, 4].includes(schemaVersion)) throw new TypeError('未知请求容器版本');
|
|
66
66
|
const existing = inspectLocalRequestRoot(root, { allowAbsent: true });
|
|
67
67
|
if (existing) {
|
|
68
68
|
if (existing.schemaVersion !== schemaVersion) requestRootFail('IRIS_CREATION_ROOT_VERSION_CONFLICT', '请求根版本不同;不自动升级、降级或迁移,请使用新的专用根');
|