@mokuyoaxis/dsh-iris 0.2.6 → 0.3.0-alpha.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 +103 -1
- package/README.en.md +104 -304
- package/README.md +101 -302
- package/bin/dsh-iris.js +446 -64
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +7 -5
- package/docs/ARCHITECTURE.md +20 -4
- package/docs/AUDIO_FLOWS.md +71 -0
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +99 -0
- package/docs/CORE_QUICKSTART.md +108 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
- package/docs/CORE_SERVICE.md +97 -0
- package/docs/CORE_SKILLS.md +94 -0
- package/docs/DSH_COMPATIBILITY.md +14 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +40 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +12 -4
- package/docs/HOST_DOCTOR.md +13 -1
- package/docs/IMAGE_FLOW.md +75 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +62 -0
- package/docs/LOCAL_API_CONTRACT.md +140 -0
- package/docs/LOCAL_CREATION.md +56 -0
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +201 -0
- package/docs/MCP.md +191 -0
- package/docs/MUSIC.md +2 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +189 -0
- package/docs/PLUGIN_STARTUP.md +100 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
- package/docs/PROVIDER_EXTENSIONS.md +78 -0
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_SETTINGS.md +85 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
- package/docs/ROADMAP.md +28 -6
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +62 -0
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.2.7.md +62 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +132 -70
- package/lib/adapters.js +20 -11
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +213 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.js +434 -55
- package/lib/comfyui-adapter.js +94 -0
- package/lib/comfyui-client.js +51 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/config.js +36 -1
- package/lib/configured-image-runtime.js +50 -0
- package/lib/connection-skill.js +89 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +21 -2
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +83 -0
- package/lib/core-capability-catalog.js +182 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-maintenance.js +31 -2
- package/lib/core-runtime.js +20 -0
- package/lib/core-service-client.js +170 -0
- package/lib/core-tasks.js +84 -13
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +133 -0
- package/lib/doctor.js +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +8 -2
- package/lib/dsh-service-bridge.js +66 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +290 -0
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +232 -0
- package/lib/index.js +94 -68
- package/lib/local-api-projection.js +32 -0
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +1083 -0
- package/lib/local-creation.js +150 -0
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +37 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +101 -0
- package/lib/mcp-server.js +47 -0
- package/lib/model-directory-service.js +16 -6
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +2 -1
- package/lib/operation-runner.js +236 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +288 -0
- package/lib/plugins.js +58 -0
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapter.js +5 -1
- package/lib/provider-adapters.js +56 -18
- package/lib/provider-catalog.js +40 -29
- package/lib/provider-config-service.js +105 -23
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +8 -1
- package/lib/provider-monitor.js +79 -0
- package/lib/provider-protocol.js +70 -20
- package/lib/provider-registry.js +118 -0
- package/lib/provider-response.js +28 -0
- package/lib/provider-settings.js +134 -0
- package/lib/provider-task-runner.js +40 -7
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +211 -0
- package/lib/vision-model-routing.js +3 -0
- package/lib/web/app.js +642 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +377 -0
- package/lib/web/creation.js +355 -0
- package/lib/web/flow-view.js +33 -0
- package/lib/web/gallery.js +132 -0
- package/lib/web/index.html +291 -0
- package/lib/web/media-client.js +204 -0
- package/lib/web/plugin-panel.js +221 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +492 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +518 -0
- package/lib/web/transport.js +96 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/lib/workbench-management.js +8 -9
- package/package.json +22 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +44 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +60 -0
- package/user_guide.md +19 -9
package/lib/dsh-host-adapter.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { defineHostAdapter } from './host-contract.js';
|
|
7
7
|
import { BrowserHtmlRenderer } from './render.js';
|
|
8
|
+
import { resolveRenderAddress } from './render-address.js';
|
|
8
9
|
import { createDshProviderSource } from './dsh-provider-source.js';
|
|
9
10
|
|
|
10
11
|
export function detectDshVersion({ entry = process.argv[1] } = {}) {
|
|
@@ -132,12 +133,15 @@ function dshVisionPort(llm, defaults) {
|
|
|
132
133
|
if (chunk?.type === 'finish') {
|
|
133
134
|
if (chunk.reason?.kind === 'error') throw visionRouteError('DSH 视觉调用失败;请检查宿主模型状态');
|
|
134
135
|
if (chunk.reason?.kind === 'aborted') throw visionRouteError('DSH 视觉调用已取消');
|
|
136
|
+
if (['length', 'max-tokens', 'tool-calls', 'content-blocked'].includes(chunk.reason?.kind)) {
|
|
137
|
+
throw visionRouteError('DSH 视觉结果未完整结束:' + chunk.reason.kind);
|
|
138
|
+
}
|
|
135
139
|
}
|
|
136
140
|
const text = typeof chunk?.delta === 'string'
|
|
137
141
|
? chunk.delta
|
|
138
142
|
: (chunk?.type === 'text-delta' ? String(chunk.text || '') : '');
|
|
139
143
|
if (text) chunks.push(text);
|
|
140
|
-
if (chunks.reduce((sum, item) => sum + item.length, 0) > 6000)
|
|
144
|
+
if (chunks.reduce((sum, item) => sum + item.length, 0) > 6000) throw visionRouteError('DSH 视觉结果超过长度限制,未返回部分结果');
|
|
141
145
|
}
|
|
142
146
|
return chunks.join('').trim();
|
|
143
147
|
}
|
|
@@ -160,8 +164,10 @@ export function createDshHostAdapter(ctx, { version = detectDshVersion() } = {})
|
|
|
160
164
|
|
|
161
165
|
const browser = service(ctx, 'browser');
|
|
162
166
|
if (browser && ['open', 'openUrl', 'screenshot', 'close'].every((name) => typeof browser[name] === 'function')) {
|
|
163
|
-
const
|
|
167
|
+
const getAddress = () => resolveRenderAddress(service(ctx, 'webServer') || service(ctx, 'httpServer'));
|
|
168
|
+
const renderer = new BrowserHtmlRenderer({ browser, getAddress });
|
|
164
169
|
ports.browser = Object.freeze({
|
|
170
|
+
get address() { return getAddress(); },
|
|
165
171
|
async renderHtml(input) {
|
|
166
172
|
const { png } = await renderer.render(input);
|
|
167
173
|
return { bytes: new Uint8Array(png), mediaType: 'image/png' };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createCoreServiceClient } from './core-service-client.js';
|
|
2
|
+
import { bridgeToolsForScopes, callCoreBridgeTool } from './core-bridge-tools.js';
|
|
3
|
+
import { requireHostPort } from './host-contract.js';
|
|
4
|
+
|
|
5
|
+
/** Optional shared-Flow connection. Native DSH ports stay in the host; no remote ctx or local execution fallback. */
|
|
6
|
+
export function createDshServiceBridge({ connectionFile, requestedScopes = ['read'], getHost, harnessVersion = 'unknown', fetch } = {}) {
|
|
7
|
+
if (typeof getHost !== 'function') throw new TypeError('DSH 服务桥需要真实 Host 工厂');
|
|
8
|
+
const client = createCoreServiceClient({ connectionFile, requestedScopes, harnessVersion, fetch });
|
|
9
|
+
const lifetime = new AbortController(), jobs = new Set();
|
|
10
|
+
const toolDisposers = [];
|
|
11
|
+
let state = 'new', disposal, remoteDisconnect = 'not_connected';
|
|
12
|
+
async function start() {
|
|
13
|
+
if (state !== 'new') throw new Error('DSH Core 服务桥只能启动一次');
|
|
14
|
+
state = 'connecting';
|
|
15
|
+
try {
|
|
16
|
+
await client.connect();
|
|
17
|
+
if (client.snapshot().adapterId !== 'iris.dsh-service') throw Object.assign(new Error('连接文件不是 DSH 适配授权'), { code: 'IRIS_CONNECTION_INPUT_INVALID' });
|
|
18
|
+
const selected = client.snapshot();
|
|
19
|
+
if (selected.capabilities.some(id => ['tts', 'transcribe'].includes(id))) throw Object.assign(new Error('DSH 原生共享桥尚未接入音频附件交付;请为此桥选择图片功能,音频使用通用 MCP 或 Core 工作台'), { code: 'IRIS_DSH_AUDIO_NOT_SUPPORTED' });
|
|
20
|
+
const tools = requireHostPort(getHost(), 'tools', '注册 Core 服务工具');
|
|
21
|
+
for (const tool of bridgeToolsForScopes(selected.scopes, selected.capabilities, selected.tools, selected.pluginOperations)) {
|
|
22
|
+
const remove = tools.register({ name: tool.name, description: tool.description, parameters: tool.parameters,
|
|
23
|
+
isConcurrencySafe: () => tool.scope === 'read' || tool.scope === 'stop',
|
|
24
|
+
output: { schema: { type: 'object' }, render: (_args, result) => result.blocks },
|
|
25
|
+
async execute(args, exec = {}) {
|
|
26
|
+
const signal = AbortSignal.any([lifetime.signal, ...(exec.signal ? [exec.signal] : [])]);
|
|
27
|
+
const job = (async () => {
|
|
28
|
+
signal.throwIfAborted();
|
|
29
|
+
const result = await callCoreBridgeTool(client, tool.name, args, { signal });
|
|
30
|
+
if (!result.image) return { value: result.value, blocks: [{ type: 'text', text: JSON.stringify(result.value) }] };
|
|
31
|
+
const attachments = requireHostPort(getHost(), 'attachments', '显示 Core 图片');
|
|
32
|
+
const attachment = await attachments.saveImage({ data: new Uint8Array(result.image.bytes), mediaType: result.image.mediaType,
|
|
33
|
+
name: 'iris-core-' + result.image.artifactId + '.' + (result.image.mediaType === 'image/jpeg' ? 'jpg' : result.image.mediaType.slice(6)) });
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
if (!attachment?.attachmentId) throw Object.assign(new Error('DSH 没有返回图片附件身份'), { code: 'IRIS_DSH_PROJECTION_FAILED' });
|
|
36
|
+
return { artifactId: result.image.artifactId, blocks: [{ type: 'image', attachment }] };
|
|
37
|
+
})();
|
|
38
|
+
jobs.add(job); try { return await job; } finally { jobs.delete(job); }
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
if (typeof remove === 'function') toolDisposers.push(remove);
|
|
42
|
+
}
|
|
43
|
+
state = 'open'; return snapshot();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
try { await dispose(); }
|
|
46
|
+
catch (cleanup) { throw new AggregateError([error, cleanup], 'DSH 服务桥启动与释放均失败'); }
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function snapshot() { return { state, connection: client.snapshot(), operations: jobs.size, remoteDisconnect }; }
|
|
51
|
+
function dispose() {
|
|
52
|
+
if (disposal) return disposal;
|
|
53
|
+
state = 'closing'; lifetime.abort();
|
|
54
|
+
disposal = (async () => {
|
|
55
|
+
const errors = [];
|
|
56
|
+
for (const remove of toolDisposers.splice(0).reverse()) { try { await remove(); } catch (error) { errors.push(error); } }
|
|
57
|
+
await Promise.allSettled([...jobs]);
|
|
58
|
+
try { remoteDisconnect = (await client.dispose()).remoteDisconnect; } catch (error) { errors.push(error); }
|
|
59
|
+
state = 'closed';
|
|
60
|
+
if (errors.length) throw new AggregateError(errors, 'DSH 服务桥释放失败');
|
|
61
|
+
return snapshot();
|
|
62
|
+
})();
|
|
63
|
+
return disposal;
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze({ start, snapshot, dispose });
|
|
66
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Runner ownership uses the existing Core lease protocol in its own private directory.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readWriterLease, WRITER_LEASE_DIR, WRITER_INTENT_FILE } from './core-writer-lease.js';
|
|
5
|
+
|
|
6
|
+
const UUID = '[a-f0-9-]{36}';
|
|
7
|
+
const FILE = new RegExp('^(?:' + WRITER_INTENT_FILE.replaceAll('.', '\\.')
|
|
8
|
+
+ '(?:\\.prepare-[1-9][0-9]*-[a-f0-9]{12})?|' + WRITER_LEASE_DIR.replaceAll('.', '\\.')
|
|
9
|
+
+ '\\.recovery-' + UUID + '\\.json)$');
|
|
10
|
+
const RETIRED = new RegExp('^' + WRITER_LEASE_DIR.replaceAll('.', '\\.') + '\\.released-' + UUID + '$');
|
|
11
|
+
const invalid = () => { throw Object.assign(new Error('执行所有权目录未知或损坏;已保留现场'), { code: 'IRIS_EXECUTION_LEASE_INVALID' }); };
|
|
12
|
+
|
|
13
|
+
export function inspectRunnerArea(directory) {
|
|
14
|
+
function regular(file) {
|
|
15
|
+
const stat = fs.lstatSync(file);
|
|
16
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) invalid();
|
|
17
|
+
}
|
|
18
|
+
function ownerDirectory(file, audit = false) {
|
|
19
|
+
const stat = fs.lstatSync(file);
|
|
20
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
|
|
21
|
+
for (const name of fs.readdirSync(file)) {
|
|
22
|
+
if (name !== 'owner.json' && !/^\.owner\.stale-[a-f0-9-]{36}\.json$/.test(name)
|
|
23
|
+
&& !(audit && new RegExp('^' + WRITER_LEASE_DIR.replaceAll('.', '\\.') + '\\.recovery-' + UUID + '\\.json$').test(name))) invalid();
|
|
24
|
+
regular(path.join(file, name));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const name of fs.readdirSync(directory)) {
|
|
28
|
+
const file = path.join(directory, name);
|
|
29
|
+
if (name === WRITER_LEASE_DIR || RETIRED.test(name)) ownerDirectory(file);
|
|
30
|
+
else if (FILE.test(name)) regular(file);
|
|
31
|
+
else if (name === '.iris-runtime-recovery-audit-v0') {
|
|
32
|
+
const stat = fs.lstatSync(file);
|
|
33
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
|
|
34
|
+
for (const entry of fs.readdirSync(file)) {
|
|
35
|
+
if (/^lease_recovery_[a-f0-9]{24}\.json$/.test(entry)) regular(path.join(file, entry));
|
|
36
|
+
else if (/^lease_recovery_[a-f0-9]{24}\.lease$/.test(entry)) ownerDirectory(path.join(file, entry), true);
|
|
37
|
+
else invalid();
|
|
38
|
+
}
|
|
39
|
+
} else invalid();
|
|
40
|
+
}
|
|
41
|
+
try { return readWriterLease(directory); }
|
|
42
|
+
catch (_) { invalid(); }
|
|
43
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
|
|
7
|
+
import { WRITER_LEASE_DIR, WRITER_INTENT_FILE, isAbandonedWriterPreparation } from './core-writer-lease.js';
|
|
8
|
+
import { normalizeGenerationInput } from './generation-input.js';
|
|
9
|
+
import { parseModelRef } from './models.js';
|
|
10
|
+
import { inspectLocalRequestRoot } from './local-request-root.js';
|
|
11
|
+
import { normalizeComfyParameters } from './comfyui-workflow.js';
|
|
12
|
+
import { AUDIO_RECIPES, isAudioRecipe, normalizeAudioFlowInput } from './audio-flow-input.js';
|
|
13
|
+
|
|
14
|
+
const RUN = /^run_[a-f0-9]{24}$/;
|
|
15
|
+
const TASK = /^task_[a-f0-9]{24}$/;
|
|
16
|
+
const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
|
|
17
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
18
|
+
const BINDING = /^sha256:[a-f0-9]{64}$/;
|
|
19
|
+
const RECIPE = 'image-edit/v0';
|
|
20
|
+
export const IMAGE_GENERATE_RECIPE = 'image-generate/v0';
|
|
21
|
+
|
|
22
|
+
export class FlowError extends Error {
|
|
23
|
+
constructor(code, message) { super(message); this.name = 'FlowError'; this.code = code; }
|
|
24
|
+
}
|
|
25
|
+
export function flowFail(code, message) { throw new FlowError(code, message); }
|
|
26
|
+
|
|
27
|
+
function keys(value, allowed, required = allowed) {
|
|
28
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value)
|
|
29
|
+
&& Object.keys(value).every(key => allowed.includes(key)) && required.every(key => Object.hasOwn(value, key)));
|
|
30
|
+
}
|
|
31
|
+
function iso(value) { return typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value; }
|
|
32
|
+
const copy = value => Object.freeze(structuredClone(value));
|
|
33
|
+
export const flowId = prefix => prefix + '_' + crypto.randomBytes(12).toString('hex');
|
|
34
|
+
export const flowSha256 = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
35
|
+
export const flowStepId = run => isAudioRecipe(run.recipe) ? AUDIO_RECIPES[run.recipe].step : run.recipe === IMAGE_GENERATE_RECIPE ? 'generate' : 'edit';
|
|
36
|
+
export const flowCapability = run => isAudioRecipe(run.recipe) ? AUDIO_RECIPES[run.recipe].capability : 'image';
|
|
37
|
+
export const flowStep = run => run[flowStepId(run)];
|
|
38
|
+
export function requireImageGenerationRoot(root) {
|
|
39
|
+
if (![2, 3].includes(inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion)) {
|
|
40
|
+
flowFail('IRIS_FLOW_GENERATION_ROOT_REQUIRED', '文生图需要新的专用执行根:execution init --image-generation;不迁移已有根');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function requireAudioRoot(root) {
|
|
44
|
+
if (inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion !== 3) {
|
|
45
|
+
flowFail('IRIS_FLOW_AUDIO_ROOT_REQUIRED', '音频需要新的专用执行根:execution init --audio;不迁移已有根');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validFlowBinding(value) {
|
|
50
|
+
return keys(value, ['runId', 'stepId', 'inputDigest']) && RUN.test(value.runId)
|
|
51
|
+
&& ['edit', 'generate', 'speak', 'transcribe'].includes(value.stepId) && BINDING.test(value.inputDigest);
|
|
52
|
+
}
|
|
53
|
+
export function flowTaskBinding(run) { return { runId: run.id, stepId: flowStepId(run), inputDigest: run.inputDigest }; }
|
|
54
|
+
|
|
55
|
+
function inputDigest(run) {
|
|
56
|
+
if (isAudioRecipe(run.recipe)) return 'sha256:' + flowSha256(JSON.stringify([run.recipe, run.providerBinding,
|
|
57
|
+
normalizeAudioFlowInput(run.recipe, run.input), run.source?.sha256 ?? null]));
|
|
58
|
+
if (run.recipe === IMAGE_GENERATE_RECIPE) return 'sha256:' + flowSha256(JSON.stringify([IMAGE_GENERATE_RECIPE,
|
|
59
|
+
run.input.model_ref, run.providerBinding, run.input.prompt, run.input.size ?? null, 1,
|
|
60
|
+
...(run.input.workflow_parameters !== undefined ? [['comfyui-parameters/v0', normalizeComfyParameters(run.input.workflow_parameters)]] : [])]));
|
|
61
|
+
return 'sha256:' + flowSha256(JSON.stringify([RECIPE, run.source.artifactId, run.source.sha256,
|
|
62
|
+
run.input.model_ref, run.providerBinding, run.input.prompt, run.input.size ?? null, 1]));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function reference(value) { return keys(value, ['artifactId', 'sha256']) && ARTIFACT.test(value.artifactId) && SHA.test(value.sha256); }
|
|
66
|
+
function validateAudio(run, expectedId) {
|
|
67
|
+
let normalized;
|
|
68
|
+
try { normalized = normalizeAudioFlowInput(run.recipe, run.input); }
|
|
69
|
+
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '音频流程输入无效'); }
|
|
70
|
+
const step = flowStepId(run), transcribe = run.recipe === 'transcribe/v0';
|
|
71
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', step, 'source', 'output'],
|
|
72
|
+
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', step, ...(transcribe ? ['source'] : [])])
|
|
73
|
+
|| run.schemaVersion !== 3 || !RUN.test(run.id) || run.id !== expectedId || !Number.isSafeInteger(run.revision) || run.revision < 1
|
|
74
|
+
|| !iso(run.createdAt) || !iso(run.updatedAt) || !BINDING.test(run.providerBinding)
|
|
75
|
+
|| JSON.stringify(run.input) !== JSON.stringify(normalized)
|
|
76
|
+
|| transcribe && (!reference(run.source) || run.source.artifactId !== run.input.source_artifact_id)
|
|
77
|
+
|| !transcribe && run.source !== undefined
|
|
78
|
+
|| !keys(run[step], ['taskId', 'claimed']) || !TASK.test(run[step].taskId) || typeof run[step].claimed !== 'boolean'
|
|
79
|
+
|| run.inputDigest !== inputDigest(run) || run.output !== undefined && (!reference(run.output) || !run[step].claimed)) {
|
|
80
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '音频流程记录结构或绑定摘要冲突');
|
|
81
|
+
}
|
|
82
|
+
return run;
|
|
83
|
+
}
|
|
84
|
+
function validateGeneration(run, expectedId, normalized) {
|
|
85
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding',
|
|
86
|
+
'inputDigest', 'generate', 'output', 'selection'],
|
|
87
|
+
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', 'generate'])
|
|
88
|
+
|| run.schemaVersion !== (run.input?.workflow_parameters === undefined ? 1 : 2) || run.recipe !== IMAGE_GENERATE_RECIPE || !RUN.test(run.id) || run.id !== expectedId
|
|
89
|
+
|| !Number.isSafeInteger(run.revision) || run.revision < 1 || !iso(run.createdAt) || !iso(run.updatedAt)
|
|
90
|
+
|| !BINDING.test(run.providerBinding) || !parseModelRef(run.input.model_ref)
|
|
91
|
+
|| !keys(run.input, ['prompt', 'model_ref', 'n', 'size', 'workflow_parameters'], ['prompt', 'model_ref', 'n'])
|
|
92
|
+
|| normalized.modelRef !== run.input.model_ref || normalized.sourceArtifactId !== undefined
|
|
93
|
+
|| run.input.prompt !== normalized.providerInput.prompt || run.input.n !== 1
|
|
94
|
+
|| run.input.size !== normalized.providerInput.size
|
|
95
|
+
|| !keys(run.generate, ['taskId', 'claimed']) || !TASK.test(run.generate.taskId) || typeof run.generate.claimed !== 'boolean'
|
|
96
|
+
|| run.inputDigest !== inputDigest(run)
|
|
97
|
+
|| run.output !== undefined && (!reference(run.output) || !run.generate.claimed)
|
|
98
|
+
|| run.selection !== undefined && (!reference(run.selection) || !run.output
|
|
99
|
+
|| run.selection.artifactId !== run.output.artifactId || run.selection.sha256 !== run.output.sha256)) {
|
|
100
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '文生图记录结构或绑定摘要冲突');
|
|
101
|
+
}
|
|
102
|
+
return run;
|
|
103
|
+
}
|
|
104
|
+
function validate(run, expectedId) {
|
|
105
|
+
if (isAudioRecipe(run?.recipe)) return validateAudio(run, expectedId);
|
|
106
|
+
let normalized;
|
|
107
|
+
try { normalized = normalizeGenerationInput('image', run?.input); }
|
|
108
|
+
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '流程创作输入无效'); }
|
|
109
|
+
if (run.recipe === IMAGE_GENERATE_RECIPE) return validateGeneration(run, expectedId, normalized);
|
|
110
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'source', 'input', 'providerBinding',
|
|
111
|
+
'inputDigest', 'edit', 'compare', 'output', 'comparison', 'selection'],
|
|
112
|
+
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'source', 'input', 'providerBinding', 'inputDigest', 'edit', 'compare'])
|
|
113
|
+
|| run.schemaVersion !== 0 || run.recipe !== RECIPE || !RUN.test(run.id) || run.id !== expectedId
|
|
114
|
+
|| !Number.isSafeInteger(run.revision) || run.revision < 1 || !iso(run.createdAt) || !iso(run.updatedAt)
|
|
115
|
+
|| !reference(run.source) || !BINDING.test(run.providerBinding)
|
|
116
|
+
|| !parseModelRef(run.input.model_ref) || normalized.modelRef !== run.input.model_ref
|
|
117
|
+
|| run.input.prompt !== normalized.providerInput.prompt || run.input.n !== 1
|
|
118
|
+
|| (run.input.size !== undefined && run.input.size !== normalized.providerInput.size)
|
|
119
|
+
|| normalized.sourceArtifactId !== run.source.artifactId
|
|
120
|
+
|| !keys(run.edit, ['taskId', 'claimed']) || !TASK.test(run.edit.taskId) || typeof run.edit.claimed !== 'boolean'
|
|
121
|
+
|| !keys(run.compare, ['artifactId']) || !ARTIFACT.test(run.compare.artifactId)
|
|
122
|
+
|| run.compare.artifactId === run.source.artifactId || run.inputDigest !== inputDigest(run)
|
|
123
|
+
|| (run.output !== undefined && (!reference(run.output) || !run.edit.claimed
|
|
124
|
+
|| [run.source.artifactId, run.compare.artifactId].includes(run.output.artifactId)))
|
|
125
|
+
|| (run.comparison !== undefined && (!reference(run.comparison) || !run.output || run.comparison.artifactId !== run.compare.artifactId))
|
|
126
|
+
|| (run.selection !== undefined && (!reference(run.selection) || !run.comparison
|
|
127
|
+
|| ![run.source, run.output].some(item => item.artifactId === run.selection.artifactId && item.sha256 === run.selection.sha256)))) {
|
|
128
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '流程记录结构或绑定摘要冲突');
|
|
129
|
+
}
|
|
130
|
+
return run;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function directory(root, leaf, create = false) {
|
|
134
|
+
let current = root;
|
|
135
|
+
for (const segment of ['flow-store', 'v0', ...(leaf ? [leaf] : [])]) {
|
|
136
|
+
current = path.join(current, segment);
|
|
137
|
+
if (create && !fs.existsSync(current)) fs.mkdirSync(current, { mode: 0o700 });
|
|
138
|
+
const stat = fs.lstatSync(current);
|
|
139
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) flowFail('IRIS_FLOW_STORE_UNSAFE', '流程存储目录不安全');
|
|
140
|
+
}
|
|
141
|
+
return current;
|
|
142
|
+
}
|
|
143
|
+
function jsonFile(file) {
|
|
144
|
+
const stat = fs.lstatSync(file);
|
|
145
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) flowFail('IRIS_FLOW_RECORD_INVALID', '流程记录不是受支持的普通文件');
|
|
146
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
147
|
+
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '流程记录不是有效 JSON'); }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function inspectFlowRoot(root) {
|
|
151
|
+
let marker;
|
|
152
|
+
try { marker = jsonFile(path.join(directory(root), 'root.json')); }
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code === 'ENOENT') flowFail('IRIS_FLOW_ROOT_REQUIRED', '请先在新的专用数据根执行 flow init,再导入图片');
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
if (!keys(marker, ['schemaVersion', 'kind', 'createdAt']) || marker.schemaVersion !== 0
|
|
158
|
+
|| marker.kind !== 'iris-image-flow-root' || !iso(marker.createdAt)) flowFail('IRIS_FLOW_RECORD_INVALID', '流程根声明无效或版本不受支持');
|
|
159
|
+
directory(root, 'runs');
|
|
160
|
+
return copy(marker);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function initializeFlowRoot(root) {
|
|
164
|
+
try { fs.lstatSync(path.join(root, 'flow-store')); return inspectFlowRoot(root); }
|
|
165
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
166
|
+
if (fs.readdirSync(root).some(name => ![WRITER_LEASE_DIR, WRITER_INTENT_FILE].includes(name) && !isAbandonedWriterPreparation(root, name))) {
|
|
167
|
+
flowFail('IRIS_FLOW_ROOT_NOT_EMPTY', '流程只在新的专用数据根启用;不迁移已有 Task/Artifact 数据根');
|
|
168
|
+
}
|
|
169
|
+
directory(root, 'runs', true);
|
|
170
|
+
const marker = { schemaVersion: 0, kind: 'iris-image-flow-root', createdAt: new Date().toISOString() };
|
|
171
|
+
atomicCreatePrivate(path.join(directory(root), 'root.json'), JSON.stringify(marker, null, 2) + '\n');
|
|
172
|
+
return copy(marker);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function recordFile(root, id) {
|
|
176
|
+
if (typeof id !== 'string' || !RUN.test(id)) flowFail('IRIS_FLOW_ID_INVALID', 'Run ID 格式无效');
|
|
177
|
+
return path.join(directory(root, 'runs'), id + '.json');
|
|
178
|
+
}
|
|
179
|
+
export function readFlowRun(root, id) {
|
|
180
|
+
inspectFlowRoot(root);
|
|
181
|
+
let record;
|
|
182
|
+
try { record = jsonFile(recordFile(root, id)); }
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error.code === 'ENOENT') flowFail('IRIS_FLOW_NOT_FOUND', '流程记录不存在');
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
validate(record, id);
|
|
188
|
+
if (isAudioRecipe(record.recipe)) requireAudioRoot(root);
|
|
189
|
+
if (record.recipe === IMAGE_GENERATE_RECIPE) requireImageGenerationRoot(root);
|
|
190
|
+
return copy(record);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Internal immutable creation intent; HTTP callers cannot choose these identities. */
|
|
194
|
+
export function prepareFlowCreation({ source, input, providerBinding, recipe }) {
|
|
195
|
+
if (isAudioRecipe(recipe)) return { recipe, runId: flowId('run'), taskId: flowId('task'), createdAt: new Date().toISOString(),
|
|
196
|
+
...(source ? { source } : {}), input, providerBinding };
|
|
197
|
+
if (!source) return { recipe: IMAGE_GENERATE_RECIPE, runId: flowId('run'), taskId: flowId('task'),
|
|
198
|
+
createdAt: new Date().toISOString(), input, providerBinding };
|
|
199
|
+
return { runId: flowId('run'), taskId: flowId('task'), comparisonArtifactId: flowId('artifact'),
|
|
200
|
+
createdAt: new Date().toISOString(), source, input, providerBinding };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function validateFlowCreation(intent) {
|
|
204
|
+
if (isAudioRecipe(intent?.recipe)) {
|
|
205
|
+
if (!keys(intent, ['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding', 'source'],
|
|
206
|
+
['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding', ...(intent.recipe === 'transcribe/v0' ? ['source'] : [])])) {
|
|
207
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '音频创建意图无效');
|
|
208
|
+
}
|
|
209
|
+
const record = { schemaVersion: 3, recipe: intent.recipe, id: intent.runId, revision: 1,
|
|
210
|
+
createdAt: intent.createdAt, updatedAt: intent.createdAt, input: intent.input, providerBinding: intent.providerBinding,
|
|
211
|
+
...(intent.source ? { source: intent.source } : {}), [AUDIO_RECIPES[intent.recipe].step]: { taskId: intent.taskId, claimed: false } };
|
|
212
|
+
record.inputDigest = inputDigest(record);
|
|
213
|
+
return validate(record, record.id);
|
|
214
|
+
}
|
|
215
|
+
if (intent?.recipe === IMAGE_GENERATE_RECIPE) {
|
|
216
|
+
if (!keys(intent, ['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding'])) {
|
|
217
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '文生图创建意图无效');
|
|
218
|
+
}
|
|
219
|
+
const record = { schemaVersion: intent.input?.workflow_parameters === undefined ? 1 : 2, recipe: IMAGE_GENERATE_RECIPE, id: intent.runId, revision: 1,
|
|
220
|
+
createdAt: intent.createdAt, updatedAt: intent.createdAt, input: intent.input,
|
|
221
|
+
providerBinding: intent.providerBinding, generate: { taskId: intent.taskId, claimed: false } };
|
|
222
|
+
record.inputDigest = inputDigest(record);
|
|
223
|
+
return validate(record, record.id);
|
|
224
|
+
}
|
|
225
|
+
if (!keys(intent, ['runId', 'taskId', 'comparisonArtifactId', 'createdAt', 'source', 'input', 'providerBinding'])) {
|
|
226
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '流程创建意图无效');
|
|
227
|
+
}
|
|
228
|
+
const record = { schemaVersion: 0, recipe: RECIPE, id: intent.runId, revision: 1,
|
|
229
|
+
createdAt: intent.createdAt, updatedAt: intent.createdAt, source: intent.source, input: intent.input,
|
|
230
|
+
providerBinding: intent.providerBinding, edit: { taskId: intent.taskId, claimed: false },
|
|
231
|
+
compare: { artifactId: intent.comparisonArtifactId } };
|
|
232
|
+
record.inputDigest = inputDigest(record);
|
|
233
|
+
return validate(record, record.id);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function commitFlowCreation(root, intent, { allowCreate = true } = {}) {
|
|
237
|
+
inspectFlowRoot(root);
|
|
238
|
+
const record = validateFlowCreation(intent);
|
|
239
|
+
if (isAudioRecipe(record.recipe)) requireAudioRoot(root);
|
|
240
|
+
if (record.recipe === IMAGE_GENERATE_RECIPE) requireImageGenerationRoot(root);
|
|
241
|
+
let current;
|
|
242
|
+
try { current = readFlowRun(root, record.id); }
|
|
243
|
+
catch (error) { if (error.code !== 'IRIS_FLOW_NOT_FOUND') throw error; }
|
|
244
|
+
if (current) {
|
|
245
|
+
if (current.createdAt !== record.createdAt || current.inputDigest !== record.inputDigest
|
|
246
|
+
|| current.recipe !== record.recipe || flowStep(current).taskId !== flowStep(record).taskId
|
|
247
|
+
|| current.compare?.artifactId !== record.compare?.artifactId) {
|
|
248
|
+
flowFail('IRIS_FLOW_INPUT_CONFLICT', '原流程与预留创建身份不一致');
|
|
249
|
+
}
|
|
250
|
+
return current;
|
|
251
|
+
}
|
|
252
|
+
if (!allowCreate) flowFail('IRIS_FLOW_NOT_FOUND', '原流程记录已不可用');
|
|
253
|
+
atomicCreatePrivate(recordFile(root, record.id), JSON.stringify(record, null, 2) + '\n');
|
|
254
|
+
return copy(record);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function createFlowRun(root, input) {
|
|
258
|
+
return commitFlowCreation(root, prepareFlowCreation(input));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function updateFlowRun(root, previous, patch) {
|
|
262
|
+
const current = readFlowRun(root, previous.id);
|
|
263
|
+
if (current.revision !== previous.revision) flowFail('IRIS_FLOW_REVISION_CONFLICT', '流程在本次操作期间已改变');
|
|
264
|
+
const allowed = isAudioRecipe(current.recipe) ? [flowStepId(current), 'output']
|
|
265
|
+
: current.recipe === IMAGE_GENERATE_RECIPE ? ['generate', 'output', 'selection'] : ['edit', 'output', 'comparison', 'selection'];
|
|
266
|
+
if (!keys(patch, allowed, [])) throw new TypeError('未知流程更新字段');
|
|
267
|
+
const record = { ...current, ...patch, revision: current.revision + 1, updatedAt: new Date().toISOString() };
|
|
268
|
+
validate(record, current.id);
|
|
269
|
+
atomicWritePrivate(recordFile(root, record.id), JSON.stringify(record, null, 2) + '\n');
|
|
270
|
+
return copy(record);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function listFlowRuns(root) {
|
|
274
|
+
inspectFlowRoot(root);
|
|
275
|
+
const records = [];
|
|
276
|
+
for (const name of fs.readdirSync(directory(root, 'runs'))) {
|
|
277
|
+
if (/^run_[a-f0-9]{24}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name)) continue; // 保留未发布的原子写现场。
|
|
278
|
+
if (!/^run_[a-f0-9]{24}\.json$/.test(name)) flowFail('IRIS_FLOW_RECORD_INVALID', '流程目录包含未知记录,停止盘点');
|
|
279
|
+
records.push(readFlowRun(root, name.slice(0, -5)));
|
|
280
|
+
}
|
|
281
|
+
return records.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 管理入口不能忽略损坏/未知的流程根,从而丢掉引用。普通 Core 根保持零写。 */
|
|
285
|
+
export function flowReferences(root) {
|
|
286
|
+
try { fs.lstatSync(path.join(root, 'flow-store')); }
|
|
287
|
+
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
288
|
+
return listFlowRuns(root).map(run => ({ run, taskIds: [flowStep(run).taskId],
|
|
289
|
+
artifactIds: [...new Set([run.source?.artifactId, run.compare?.artifactId, run.output?.artifactId, run.selection?.artifactId].filter(Boolean))] }));
|
|
290
|
+
}
|
package/lib/generation-input.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { normalizeMusicOptions } from './music-options.js';
|
|
6
|
+
import { normalizeComfyParameters } from './comfyui-workflow.js';
|
|
6
7
|
|
|
7
8
|
const FIELDS = Object.freeze({
|
|
8
|
-
image: ['prompt', 'size', 'n', 'source_artifact_id'],
|
|
9
|
+
image: ['prompt', 'size', 'n', 'source_artifact_id', 'workflow_parameters'],
|
|
9
10
|
video: ['prompt', 'size', 'duration', 'img_data_url', 'first_frame_path', 'audio_path', 'resolution'],
|
|
10
11
|
tts: ['text', 'voice'],
|
|
11
12
|
music: ['prompt', 'timeout_ms', 'retry_502', 'retry_delay_ms'],
|
|
@@ -86,6 +87,14 @@ export function normalizeGenerationInput(capability, input, { allowAudioPath = f
|
|
|
86
87
|
const n = input.n === undefined ? 1 : Number(input.n);
|
|
87
88
|
if (!Number.isSafeInteger(n) || n < 1 || n > 4) fail('n 必须为 1–4 的整数');
|
|
88
89
|
providerInput.n = n;
|
|
90
|
+
if (input.workflow_parameters !== undefined) {
|
|
91
|
+
if (!modelRef || sourceArtifactId || n !== 1) fail('工作流参数需要明确 model_ref、n=1 且不带来源图片');
|
|
92
|
+
try { providerInput.workflow_parameters = Object.freeze(normalizeComfyParameters(input.workflow_parameters)); }
|
|
93
|
+
catch (error) { fail(error.message); }
|
|
94
|
+
if (Buffer.byteLength(JSON.stringify({ model_ref: modelRef, ...providerInput }, null, 2)) > 96 * 1024) {
|
|
95
|
+
fail('参数化流程输入最多 96 KiB;请缩短提示词或参数值');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
89
98
|
}
|
|
90
99
|
if (capability === 'video') {
|
|
91
100
|
if (input.duration !== undefined) {
|
package/lib/headless-vision.js
CHANGED
|
@@ -70,10 +70,10 @@ function completionResult(completion, routes) {
|
|
|
70
70
|
return { ...result, ...selectedModel(completion, routes) };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
async function transcribeVideo({ catalog, input, runtime, operation, onRateLimit, onModelAvailability }) {
|
|
74
|
-
const routes = transcribeCandidatesFromCatalog(catalog, input.transcribe_model_ref);
|
|
75
|
-
const candidates = routes.map(route => ({ adapter: createConfiguredProviderAdapter(route.provider, { onRateLimit, onModelAvailability }),
|
|
76
|
-
model: route.modelRef, selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) }));
|
|
73
|
+
async function transcribeVideo({ catalog, input, runtime, operation, onRateLimit, onModelAvailability, registry }) {
|
|
74
|
+
const routes = transcribeCandidatesFromCatalog(catalog, input.transcribe_model_ref, registry);
|
|
75
|
+
const candidates = routes.map(route => ({ adapter: createConfiguredProviderAdapter(route.provider, { onRateLimit, onModelAvailability, registry }),
|
|
76
|
+
model: route.modelRef, selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider, registry) }));
|
|
77
77
|
const audio = await waitForModelWork(() => extractAudioTrack({ inputPath: input.video_path, signal: operation.signal }), operation.signal);
|
|
78
78
|
const abortRuntime = () => { void runtime.dispose().catch(() => {}); };
|
|
79
79
|
operation.signal.addEventListener('abort', abortRuntime, { once: true });
|
|
@@ -87,7 +87,7 @@ async function transcribeVideo({ catalog, input, runtime, operation, onRateLimit
|
|
|
87
87
|
providerInput: { audioUrl: prepared.url } }), operation.signal, 'invoke', 'unknown');
|
|
88
88
|
task = submitted.task;
|
|
89
89
|
if (task.acceptance === 'accepted' && task.outcome === 'none') {
|
|
90
|
-
const adapter = createConfiguredProviderAdapter(providerForTaskFromCatalog(catalog, task), { onRateLimit,
|
|
90
|
+
const adapter = createConfiguredProviderAdapter(providerForTaskFromCatalog(catalog, task, registry), { onRateLimit, registry,
|
|
91
91
|
modelId: routes.find(route => route.modelRef === task.modelRef)?.model });
|
|
92
92
|
while (task.outcome === 'none') {
|
|
93
93
|
task = await waitForModelWork(() => runner.observe(task.id, adapter), operation.signal, 'read', 'responded');
|
|
@@ -111,7 +111,7 @@ async function transcribeVideo({ catalog, input, runtime, operation, onRateLimit
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export async function executeVisionCommand({ command, input, catalog, modelRef = '', runtime, signal, timeoutMs = VISION_BUDGET.timeoutMs, onRateLimit, onModelAvailability }) {
|
|
114
|
+
export async function executeVisionCommand({ command, input, catalog, modelRef = '', runtime, signal, timeoutMs = VISION_BUDGET.timeoutMs, onRateLimit, onModelAvailability, registry }) {
|
|
115
115
|
normalizeVisionInput(command, input);
|
|
116
116
|
if (modelRef && !/^[^:]+::.+$/.test(modelRef)) invalid('--model-ref 必须是 providerId::modelId');
|
|
117
117
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > VISION_BUDGET.timeoutMs) invalid('--timeout-ms 必须是 1–120000 的整数');
|
|
@@ -140,7 +140,7 @@ export async function executeVisionCommand({ command, input, catalog, modelRef =
|
|
|
140
140
|
}
|
|
141
141
|
let transcript = input.transcribe_text || '', transcription = { status: transcript.trim() ? 'provided' : 'disabled' };
|
|
142
142
|
if (input.transcribe) {
|
|
143
|
-
const asr = meta.hasAudio ? await transcribeVideo({ catalog, input, runtime, operation, onRateLimit, onModelAvailability }) : { report: { status: 'no_audio' }, text: '' };
|
|
143
|
+
const asr = meta.hasAudio ? await transcribeVideo({ catalog, input, runtime, operation, onRateLimit, onModelAvailability, registry }) : { report: { status: 'no_audio' }, text: '' };
|
|
144
144
|
transcript = asr.text; transcription = asr.report;
|
|
145
145
|
}
|
|
146
146
|
const completion = await summarizeMedia({ ports, frames, question: input.question, transcript, operation, budget });
|
package/lib/image-edit-input.js
CHANGED
|
@@ -10,7 +10,7 @@ function invalid(message) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function imageEditCandidates(candidates) {
|
|
13
|
-
const supported = candidate => supportsImageEdit(candidate.adapter.protocol, candidate.providerModel);
|
|
13
|
+
const supported = candidate => candidate.supportsImageEdit ?? supportsImageEdit(candidate.adapter.protocol, candidate.providerModel);
|
|
14
14
|
if (candidates.some(candidate => candidate.selectionReason === 'explicit'
|
|
15
15
|
&& !supported(candidate))) {
|
|
16
16
|
throw invalid('指定模型的图片协议不支持此类改图,请选择聊天生图模型或 DashScope Qwen 原生改图模型');
|