@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -1
- package/README.en.md +109 -311
- package/README.md +106 -309
- package/bin/dsh-iris.js +452 -92
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +87 -0
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +101 -0
- package/docs/CORE_QUICKSTART.md +110 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
- package/docs/CORE_SERVICE.md +101 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +21 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +30 -5
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +4 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +4 -2
- package/docs/LOCAL_API_CONTRACT.md +42 -15
- package/docs/LOCAL_CREATION.md +7 -1
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +207 -0
- package/docs/MCP.md +212 -0
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/MUSIC.md +2 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +193 -0
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +113 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +82 -0
- package/docs/PROVIDER_HEALTH.md +5 -1
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +91 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +21 -6
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +56 -4
- package/lib/adapters.js +75 -49
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +73 -17
- package/lib/application-writer.js +193 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +402 -72
- package/lib/comfyui-adapter.js +95 -0
- package/lib/comfyui-client.js +60 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/command-service.js +18 -5
- package/lib/config.js +49 -10
- package/lib/configured-image-runtime.js +69 -0
- package/lib/connection-skill.js +98 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +84 -28
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +85 -0
- package/lib/core-capability-catalog.js +192 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-maintenance.js +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +174 -0
- package/lib/core-tasks.js +113 -21
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +41 -64
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +67 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +156 -14
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +97 -31
- package/lib/index.js +47 -22
- package/lib/local-api-projection.js +15 -6
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +861 -71
- package/lib/local-creation.js +96 -24
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +39 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +174 -0
- package/lib/mcp-server.js +69 -0
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +37 -10
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +6 -3
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +281 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +307 -0
- package/lib/plugins.js +59 -0
- package/lib/provider-adapter.js +7 -3
- package/lib/provider-adapters.js +92 -22
- package/lib/provider-catalog.js +64 -36
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +143 -31
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +18 -6
- package/lib/provider-monitor.js +81 -0
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +73 -22
- package/lib/provider-registry.js +128 -0
- package/lib/provider-response.js +38 -0
- package/lib/provider-settings.js +136 -0
- package/lib/provider-task-runner.js +132 -43
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/skill-installation.js +23 -11
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +5 -0
- package/lib/web/app.js +703 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +516 -0
- package/lib/web/creation.js +359 -0
- package/lib/web/flow-view.js +34 -0
- package/lib/web/gallery.js +133 -0
- package/lib/web/index.html +344 -0
- package/lib/web/media-client.js +218 -0
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +223 -0
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +513 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +571 -0
- package/lib/web/transport.js +101 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +3 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +24 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +8 -4
- package/skills/iris-core/references/cli.md +2 -2
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +2 -0
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +5 -1
package/lib/dsh-host-adapter.js
CHANGED
|
@@ -133,12 +133,15 @@ function dshVisionPort(llm, defaults) {
|
|
|
133
133
|
if (chunk?.type === 'finish') {
|
|
134
134
|
if (chunk.reason?.kind === 'error') throw visionRouteError('DSH 视觉调用失败;请检查宿主模型状态');
|
|
135
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
|
+
}
|
|
136
139
|
}
|
|
137
140
|
const text = typeof chunk?.delta === 'string'
|
|
138
141
|
? chunk.delta
|
|
139
142
|
: (chunk?.type === 'text-delta' ? String(chunk.text || '') : '');
|
|
140
143
|
if (text) chunks.push(text);
|
|
141
|
-
if (chunks.reduce((sum, item) => sum + item.length, 0) > 6000)
|
|
144
|
+
if (chunks.reduce((sum, item) => sum + item.length, 0) > 6000) throw visionRouteError('DSH 视觉结果超过长度限制,未返回部分结果');
|
|
142
145
|
}
|
|
143
146
|
return chunks.join('').trim();
|
|
144
147
|
}
|
|
@@ -24,7 +24,7 @@ export async function withDshProviderConfig(host, callback, { signal, fresh = fa
|
|
|
24
24
|
throw new DshProviderSourceError('当前宿主无法解析 DSH 连接,请检查原连接或改用手动供应商');
|
|
25
25
|
}
|
|
26
26
|
const value = await host.ports.providerConnections.resolve(provider.dshProvider, { signal });
|
|
27
|
-
return { ...provider, baseUrl: value.baseUrl, apiKey: value.apiKey };
|
|
27
|
+
return { ...provider, mediaBaseUrl: undefined, baseUrl: value.baseUrl, apiKey: value.apiKey };
|
|
28
28
|
} catch (error) {
|
|
29
29
|
if (!(error instanceof DshProviderSourceError)) throw error;
|
|
30
30
|
return { ...provider, enabled: false, connectionError: error.message };
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
if (selected.capabilities.includes('video-generate')) throw Object.assign(new Error('DSH 原生共享桥尚未接入视频资源交付;请为此桥选择图片功能,视频使用通用 MCP 或 Core 工作台'), { code: 'IRIS_DSH_VIDEO_NOT_SUPPORTED' });
|
|
21
|
+
const tools = requireHostPort(getHost(), 'tools', '注册 Core 服务工具');
|
|
22
|
+
for (const tool of bridgeToolsForScopes(selected.scopes, selected.capabilities, selected.tools, selected.pluginOperations)) {
|
|
23
|
+
const remove = tools.register({ name: tool.name, description: tool.description, parameters: tool.parameters,
|
|
24
|
+
isConcurrencySafe: () => tool.scope === 'read' || tool.scope === 'stop',
|
|
25
|
+
output: { schema: { type: 'object' }, render: (_args, result) => result.blocks },
|
|
26
|
+
async execute(args, exec = {}) {
|
|
27
|
+
const signal = AbortSignal.any([lifetime.signal, ...(exec.signal ? [exec.signal] : [])]);
|
|
28
|
+
const job = (async () => {
|
|
29
|
+
signal.throwIfAborted();
|
|
30
|
+
const result = await callCoreBridgeTool(client, tool.name, args, { signal });
|
|
31
|
+
if (!result.image) return { value: result.value, blocks: [{ type: 'text', text: JSON.stringify(result.value) }] };
|
|
32
|
+
const attachments = requireHostPort(getHost(), 'attachments', '显示 Core 图片');
|
|
33
|
+
const attachment = await attachments.saveImage({ data: new Uint8Array(result.image.bytes), mediaType: result.image.mediaType,
|
|
34
|
+
name: 'iris-core-' + result.image.artifactId + '.' + (result.image.mediaType === 'image/jpeg' ? 'jpg' : result.image.mediaType.slice(6)) });
|
|
35
|
+
signal.throwIfAborted();
|
|
36
|
+
if (!attachment?.attachmentId) throw Object.assign(new Error('DSH 没有返回图片附件身份'), { code: 'IRIS_DSH_PROJECTION_FAILED' });
|
|
37
|
+
return { artifactId: result.image.artifactId, blocks: [{ type: 'image', attachment }] };
|
|
38
|
+
})();
|
|
39
|
+
jobs.add(job); try { return await job; } finally { jobs.delete(job); }
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
if (typeof remove === 'function') toolDisposers.push(remove);
|
|
43
|
+
}
|
|
44
|
+
state = 'open'; return snapshot();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
try { await dispose(); }
|
|
47
|
+
catch (cleanup) { throw new AggregateError([error, cleanup], 'DSH 服务桥启动与释放均失败'); }
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function snapshot() { return { state, connection: client.snapshot(), operations: jobs.size, remoteDisconnect }; }
|
|
52
|
+
function dispose() {
|
|
53
|
+
if (disposal) return disposal;
|
|
54
|
+
state = 'closing'; lifetime.abort();
|
|
55
|
+
disposal = (async () => {
|
|
56
|
+
const errors = [];
|
|
57
|
+
for (const remove of toolDisposers.splice(0).reverse()) { try { await remove(); } catch (error) { errors.push(error); } }
|
|
58
|
+
await Promise.allSettled([...jobs]);
|
|
59
|
+
try { remoteDisconnect = (await client.dispose()).remoteDisconnect; } catch (error) { errors.push(error); }
|
|
60
|
+
state = 'closed';
|
|
61
|
+
if (errors.length) throw new AggregateError(errors, 'DSH 服务桥释放失败');
|
|
62
|
+
return snapshot();
|
|
63
|
+
})();
|
|
64
|
+
return disposal;
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze({ start, snapshot, dispose });
|
|
67
|
+
}
|
|
@@ -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
|
+
}
|
package/lib/flow-store.js
CHANGED
|
@@ -4,9 +4,14 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
|
|
7
|
-
import { WRITER_LEASE_DIR, WRITER_INTENT_FILE } from './core-writer-lease.js';
|
|
7
|
+
import { WRITER_LEASE_DIR, WRITER_INTENT_FILE, isAbandonedWriterPreparation } from './core-writer-lease.js';
|
|
8
8
|
import { normalizeGenerationInput } from './generation-input.js';
|
|
9
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
|
+
import { VIDEO_RECIPE, normalizeVideoFlowInput } from './video-flow-input.js';
|
|
14
|
+
import { checkedNetworkRequirement } from './network-policy.js';
|
|
10
15
|
|
|
11
16
|
const RUN = /^run_[a-f0-9]{24}$/;
|
|
12
17
|
const TASK = /^task_[a-f0-9]{24}$/;
|
|
@@ -14,6 +19,7 @@ const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
|
|
|
14
19
|
const SHA = /^[a-f0-9]{64}$/;
|
|
15
20
|
const BINDING = /^sha256:[a-f0-9]{64}$/;
|
|
16
21
|
const RECIPE = 'image-edit/v0';
|
|
22
|
+
export const IMAGE_GENERATE_RECIPE = 'image-generate/v0';
|
|
17
23
|
|
|
18
24
|
export class FlowError extends Error {
|
|
19
25
|
constructor(code, message) { super(message); this.name = 'FlowError'; this.code = code; }
|
|
@@ -28,25 +34,114 @@ function iso(value) { return typeof value === 'string' && Number.isFinite(Date.p
|
|
|
28
34
|
const copy = value => Object.freeze(structuredClone(value));
|
|
29
35
|
export const flowId = prefix => prefix + '_' + crypto.randomBytes(12).toString('hex');
|
|
30
36
|
export const flowSha256 = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
37
|
+
export const flowStepId = run => run.recipe === VIDEO_RECIPE ? 'video' : isAudioRecipe(run.recipe) ? AUDIO_RECIPES[run.recipe].step : run.recipe === IMAGE_GENERATE_RECIPE ? 'generate' : 'edit';
|
|
38
|
+
export const flowCapability = run => run.recipe === VIDEO_RECIPE ? 'video' : isAudioRecipe(run.recipe) ? AUDIO_RECIPES[run.recipe].capability : 'image';
|
|
39
|
+
export const flowStep = run => run[flowStepId(run)];
|
|
40
|
+
export function requireImageGenerationRoot(root) {
|
|
41
|
+
if (![2, 3, 4].includes(inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion)) {
|
|
42
|
+
flowFail('IRIS_FLOW_GENERATION_ROOT_REQUIRED', '文生图需要新的专用执行根:execution init --image-generation;不迁移已有根');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function requireAudioRoot(root) {
|
|
46
|
+
if (![3, 4].includes(inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion)) {
|
|
47
|
+
flowFail('IRIS_FLOW_AUDIO_ROOT_REQUIRED', '音频需要新的专用执行根:execution init --audio;不迁移已有根');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function requireVideoRoot(root) {
|
|
52
|
+
if (inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion !== 4) {
|
|
53
|
+
flowFail('IRIS_FLOW_VIDEO_ROOT_REQUIRED', '文生视频需要新的专用 execution init --video 根;原根不迁移');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
31
56
|
|
|
32
57
|
export function validFlowBinding(value) {
|
|
33
58
|
return keys(value, ['runId', 'stepId', 'inputDigest']) && RUN.test(value.runId)
|
|
34
|
-
&&
|
|
59
|
+
&& ['edit', 'generate', 'speak', 'transcribe', 'video'].includes(value.stepId) && BINDING.test(value.inputDigest);
|
|
35
60
|
}
|
|
36
|
-
export function flowTaskBinding(run) { return { runId: run.id, stepId:
|
|
61
|
+
export function flowTaskBinding(run) { return { runId: run.id, stepId: flowStepId(run), inputDigest: run.inputDigest }; }
|
|
37
62
|
|
|
38
|
-
function
|
|
63
|
+
function legacyInputDigest(run) {
|
|
64
|
+
if (run.recipe === VIDEO_RECIPE) return 'sha256:' + flowSha256(JSON.stringify([VIDEO_RECIPE, run.providerBinding, normalizeVideoFlowInput(run.input)]));
|
|
65
|
+
if (isAudioRecipe(run.recipe)) return 'sha256:' + flowSha256(JSON.stringify([run.recipe, run.providerBinding,
|
|
66
|
+
normalizeAudioFlowInput(run.recipe, run.input), run.source?.sha256 ?? null]));
|
|
67
|
+
if (run.recipe === IMAGE_GENERATE_RECIPE) return 'sha256:' + flowSha256(JSON.stringify([IMAGE_GENERATE_RECIPE,
|
|
68
|
+
run.input.model_ref, run.providerBinding, run.input.prompt, run.input.size ?? null, 1,
|
|
69
|
+
...(run.input.workflow_parameters !== undefined ? [['comfyui-parameters/v0', normalizeComfyParameters(run.input.workflow_parameters)]] : [])]));
|
|
39
70
|
return 'sha256:' + flowSha256(JSON.stringify([RECIPE, run.source.artifactId, run.source.sha256,
|
|
40
71
|
run.input.model_ref, run.providerBinding, run.input.prompt, run.input.size ?? null, 1]));
|
|
41
72
|
}
|
|
42
73
|
|
|
74
|
+
function networkPart(value) {
|
|
75
|
+
if (value?.networkRequirement === undefined) return {};
|
|
76
|
+
try { return { networkRequirement: checkedNetworkRequirement(value.networkRequirement) }; }
|
|
77
|
+
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '流程网络要求身份或版本无效'); }
|
|
78
|
+
}
|
|
79
|
+
function inputDigest(run) {
|
|
80
|
+
const base = legacyInputDigest(run);
|
|
81
|
+
return run.networkRequirement === undefined ? base
|
|
82
|
+
: 'sha256:' + flowSha256(JSON.stringify(['network-flow/v1', base, networkPart(run).networkRequirement]));
|
|
83
|
+
}
|
|
84
|
+
|
|
43
85
|
function reference(value) { return keys(value, ['artifactId', 'sha256']) && ARTIFACT.test(value.artifactId) && SHA.test(value.sha256); }
|
|
86
|
+
function validateAudio(run, expectedId) {
|
|
87
|
+
let normalized;
|
|
88
|
+
try { normalized = normalizeAudioFlowInput(run.recipe, run.input); }
|
|
89
|
+
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '音频流程输入无效'); }
|
|
90
|
+
const step = flowStepId(run), transcribe = run.recipe === 'transcribe/v0';
|
|
91
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', step, 'source', 'output', 'networkRequirement'],
|
|
92
|
+
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', step, ...(transcribe ? ['source'] : [])])
|
|
93
|
+
|| run.schemaVersion !== 3 || !RUN.test(run.id) || run.id !== expectedId || !Number.isSafeInteger(run.revision) || run.revision < 1
|
|
94
|
+
|| !iso(run.createdAt) || !iso(run.updatedAt) || !BINDING.test(run.providerBinding)
|
|
95
|
+
|| JSON.stringify(run.input) !== JSON.stringify(normalized)
|
|
96
|
+
|| transcribe && (!reference(run.source) || run.source.artifactId !== run.input.source_artifact_id)
|
|
97
|
+
|| !transcribe && run.source !== undefined
|
|
98
|
+
|| !keys(run[step], ['taskId', 'claimed']) || !TASK.test(run[step].taskId) || typeof run[step].claimed !== 'boolean'
|
|
99
|
+
|| run.inputDigest !== inputDigest(run) || run.output !== undefined && (!reference(run.output) || !run[step].claimed)) {
|
|
100
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '音频流程记录结构或绑定摘要冲突');
|
|
101
|
+
}
|
|
102
|
+
return run;
|
|
103
|
+
}
|
|
104
|
+
function validateGeneration(run, expectedId, normalized) {
|
|
105
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding',
|
|
106
|
+
'inputDigest', 'generate', 'output', 'selection', 'networkRequirement'],
|
|
107
|
+
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', 'generate'])
|
|
108
|
+
|| run.schemaVersion !== (run.input?.workflow_parameters === undefined ? 1 : 2) || run.recipe !== IMAGE_GENERATE_RECIPE || !RUN.test(run.id) || run.id !== expectedId
|
|
109
|
+
|| !Number.isSafeInteger(run.revision) || run.revision < 1 || !iso(run.createdAt) || !iso(run.updatedAt)
|
|
110
|
+
|| !BINDING.test(run.providerBinding) || !parseModelRef(run.input.model_ref)
|
|
111
|
+
|| !keys(run.input, ['prompt', 'model_ref', 'n', 'size', 'workflow_parameters'], ['prompt', 'model_ref', 'n'])
|
|
112
|
+
|| normalized.modelRef !== run.input.model_ref || normalized.sourceArtifactId !== undefined
|
|
113
|
+
|| run.input.prompt !== normalized.providerInput.prompt || run.input.n !== 1
|
|
114
|
+
|| run.input.size !== normalized.providerInput.size
|
|
115
|
+
|| !keys(run.generate, ['taskId', 'claimed']) || !TASK.test(run.generate.taskId) || typeof run.generate.claimed !== 'boolean'
|
|
116
|
+
|| run.inputDigest !== inputDigest(run)
|
|
117
|
+
|| run.output !== undefined && (!reference(run.output) || !run.generate.claimed)
|
|
118
|
+
|| run.selection !== undefined && (!reference(run.selection) || !run.output
|
|
119
|
+
|| run.selection.artifactId !== run.output.artifactId || run.selection.sha256 !== run.output.sha256)) {
|
|
120
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '文生图记录结构或绑定摘要冲突');
|
|
121
|
+
}
|
|
122
|
+
return run;
|
|
123
|
+
}
|
|
44
124
|
function validate(run, expectedId) {
|
|
125
|
+
networkPart(run);
|
|
126
|
+
if (run?.recipe === VIDEO_RECIPE) {
|
|
127
|
+
let normalized;
|
|
128
|
+
try { normalized = normalizeVideoFlowInput(run.input); } catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '视频流程输入无效'); }
|
|
129
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', 'video', 'output', 'networkRequirement'],
|
|
130
|
+
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', 'video'])
|
|
131
|
+
|| run.schemaVersion !== 4 || run.id !== expectedId || !RUN.test(run.id) || !Number.isSafeInteger(run.revision) || run.revision < 1
|
|
132
|
+
|| !iso(run.createdAt) || !iso(run.updatedAt) || !BINDING.test(run.providerBinding)
|
|
133
|
+
|| JSON.stringify(run.input) !== JSON.stringify(normalized) || run.inputDigest !== inputDigest(run)
|
|
134
|
+
|| !keys(run.video, ['taskId', 'claimed']) || !TASK.test(run.video.taskId) || typeof run.video.claimed !== 'boolean'
|
|
135
|
+
|| run.output !== undefined && (!reference(run.output) || !run.video.claimed)) flowFail('IRIS_FLOW_RECORD_INVALID', '视频流程记录或绑定摘要冲突');
|
|
136
|
+
return run;
|
|
137
|
+
}
|
|
138
|
+
if (isAudioRecipe(run?.recipe)) return validateAudio(run, expectedId);
|
|
45
139
|
let normalized;
|
|
46
140
|
try { normalized = normalizeGenerationInput('image', run?.input); }
|
|
47
141
|
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '流程创作输入无效'); }
|
|
142
|
+
if (run.recipe === IMAGE_GENERATE_RECIPE) return validateGeneration(run, expectedId, normalized);
|
|
48
143
|
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'source', 'input', 'providerBinding',
|
|
49
|
-
'inputDigest', 'edit', 'compare', 'output', 'comparison', 'selection'],
|
|
144
|
+
'inputDigest', 'edit', 'compare', 'output', 'comparison', 'selection', 'networkRequirement'],
|
|
50
145
|
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'source', 'input', 'providerBinding', 'inputDigest', 'edit', 'compare'])
|
|
51
146
|
|| run.schemaVersion !== 0 || run.recipe !== RECIPE || !RUN.test(run.id) || run.id !== expectedId
|
|
52
147
|
|| !Number.isSafeInteger(run.revision) || run.revision < 1 || !iso(run.createdAt) || !iso(run.updatedAt)
|
|
@@ -101,7 +196,7 @@ export function inspectFlowRoot(root) {
|
|
|
101
196
|
export function initializeFlowRoot(root) {
|
|
102
197
|
try { fs.lstatSync(path.join(root, 'flow-store')); return inspectFlowRoot(root); }
|
|
103
198
|
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
104
|
-
if (fs.readdirSync(root).some(name => ![WRITER_LEASE_DIR, WRITER_INTENT_FILE].includes(name))) {
|
|
199
|
+
if (fs.readdirSync(root).some(name => ![WRITER_LEASE_DIR, WRITER_INTENT_FILE].includes(name) && !isAbandonedWriterPreparation(root, name))) {
|
|
105
200
|
flowFail('IRIS_FLOW_ROOT_NOT_EMPTY', '流程只在新的专用数据根启用;不迁移已有 Task/Artifact 数据根');
|
|
106
201
|
}
|
|
107
202
|
directory(root, 'runs', true);
|
|
@@ -122,20 +217,61 @@ export function readFlowRun(root, id) {
|
|
|
122
217
|
if (error.code === 'ENOENT') flowFail('IRIS_FLOW_NOT_FOUND', '流程记录不存在');
|
|
123
218
|
throw error;
|
|
124
219
|
}
|
|
125
|
-
|
|
220
|
+
validate(record, id);
|
|
221
|
+
if (record.recipe === VIDEO_RECIPE) requireVideoRoot(root);
|
|
222
|
+
if (isAudioRecipe(record.recipe)) requireAudioRoot(root);
|
|
223
|
+
if (record.recipe === IMAGE_GENERATE_RECIPE) requireImageGenerationRoot(root);
|
|
224
|
+
return copy(record);
|
|
126
225
|
}
|
|
127
226
|
|
|
128
227
|
/** Internal immutable creation intent; HTTP callers cannot choose these identities. */
|
|
129
|
-
export function prepareFlowCreation({ source, input, providerBinding }) {
|
|
130
|
-
|
|
228
|
+
export function prepareFlowCreation({ source, input, providerBinding, recipe, networkRequirement }) {
|
|
229
|
+
const network = networkPart({ networkRequirement });
|
|
230
|
+
if (recipe === VIDEO_RECIPE) return { ...network, recipe, runId: flowId('run'), taskId: flowId('task'), createdAt: new Date().toISOString(), input, providerBinding };
|
|
231
|
+
if (isAudioRecipe(recipe)) return { ...network, recipe, runId: flowId('run'), taskId: flowId('task'), createdAt: new Date().toISOString(),
|
|
232
|
+
...(source ? { source } : {}), input, providerBinding };
|
|
233
|
+
if (!source) return { ...network, recipe: IMAGE_GENERATE_RECIPE, runId: flowId('run'), taskId: flowId('task'),
|
|
234
|
+
createdAt: new Date().toISOString(), input, providerBinding };
|
|
235
|
+
return { ...network, runId: flowId('run'), taskId: flowId('task'), comparisonArtifactId: flowId('artifact'),
|
|
131
236
|
createdAt: new Date().toISOString(), source, input, providerBinding };
|
|
132
237
|
}
|
|
133
238
|
|
|
134
239
|
export function validateFlowCreation(intent) {
|
|
240
|
+
if (!intent || typeof intent !== 'object' || Array.isArray(intent)) flowFail('IRIS_FLOW_RECORD_INVALID', '流程创建意图无效');
|
|
241
|
+
const network = networkPart(intent);
|
|
242
|
+
const { networkRequirement, ...payload } = intent;
|
|
243
|
+
intent = payload;
|
|
244
|
+
if (intent?.recipe === VIDEO_RECIPE) {
|
|
245
|
+
if (!keys(intent, ['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding'])) flowFail('IRIS_FLOW_RECORD_INVALID', '视频创建意图无效');
|
|
246
|
+
const record = { ...network, schemaVersion: 4, recipe: VIDEO_RECIPE, id: intent.runId, revision: 1, createdAt: intent.createdAt, updatedAt: intent.createdAt,
|
|
247
|
+
input: intent.input, providerBinding: intent.providerBinding, video: { taskId: intent.taskId, claimed: false } };
|
|
248
|
+
record.inputDigest = inputDigest(record); return validate(record, record.id);
|
|
249
|
+
}
|
|
250
|
+
if (isAudioRecipe(intent?.recipe)) {
|
|
251
|
+
if (!keys(intent, ['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding', 'source'],
|
|
252
|
+
['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding', ...(intent.recipe === 'transcribe/v0' ? ['source'] : [])])) {
|
|
253
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '音频创建意图无效');
|
|
254
|
+
}
|
|
255
|
+
const record = { ...network, schemaVersion: 3, recipe: intent.recipe, id: intent.runId, revision: 1,
|
|
256
|
+
createdAt: intent.createdAt, updatedAt: intent.createdAt, input: intent.input, providerBinding: intent.providerBinding,
|
|
257
|
+
...(intent.source ? { source: intent.source } : {}), [AUDIO_RECIPES[intent.recipe].step]: { taskId: intent.taskId, claimed: false } };
|
|
258
|
+
record.inputDigest = inputDigest(record);
|
|
259
|
+
return validate(record, record.id);
|
|
260
|
+
}
|
|
261
|
+
if (intent?.recipe === IMAGE_GENERATE_RECIPE) {
|
|
262
|
+
if (!keys(intent, ['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding'])) {
|
|
263
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '文生图创建意图无效');
|
|
264
|
+
}
|
|
265
|
+
const record = { ...network, schemaVersion: intent.input?.workflow_parameters === undefined ? 1 : 2, recipe: IMAGE_GENERATE_RECIPE, id: intent.runId, revision: 1,
|
|
266
|
+
createdAt: intent.createdAt, updatedAt: intent.createdAt, input: intent.input,
|
|
267
|
+
providerBinding: intent.providerBinding, generate: { taskId: intent.taskId, claimed: false } };
|
|
268
|
+
record.inputDigest = inputDigest(record);
|
|
269
|
+
return validate(record, record.id);
|
|
270
|
+
}
|
|
135
271
|
if (!keys(intent, ['runId', 'taskId', 'comparisonArtifactId', 'createdAt', 'source', 'input', 'providerBinding'])) {
|
|
136
272
|
flowFail('IRIS_FLOW_RECORD_INVALID', '流程创建意图无效');
|
|
137
273
|
}
|
|
138
|
-
const record = { schemaVersion: 0, recipe: RECIPE, id: intent.runId, revision: 1,
|
|
274
|
+
const record = { ...network, schemaVersion: 0, recipe: RECIPE, id: intent.runId, revision: 1,
|
|
139
275
|
createdAt: intent.createdAt, updatedAt: intent.createdAt, source: intent.source, input: intent.input,
|
|
140
276
|
providerBinding: intent.providerBinding, edit: { taskId: intent.taskId, claimed: false },
|
|
141
277
|
compare: { artifactId: intent.comparisonArtifactId } };
|
|
@@ -146,12 +282,16 @@ export function validateFlowCreation(intent) {
|
|
|
146
282
|
export function commitFlowCreation(root, intent, { allowCreate = true } = {}) {
|
|
147
283
|
inspectFlowRoot(root);
|
|
148
284
|
const record = validateFlowCreation(intent);
|
|
285
|
+
if (record.recipe === VIDEO_RECIPE) requireVideoRoot(root);
|
|
286
|
+
if (isAudioRecipe(record.recipe)) requireAudioRoot(root);
|
|
287
|
+
if (record.recipe === IMAGE_GENERATE_RECIPE) requireImageGenerationRoot(root);
|
|
149
288
|
let current;
|
|
150
289
|
try { current = readFlowRun(root, record.id); }
|
|
151
290
|
catch (error) { if (error.code !== 'IRIS_FLOW_NOT_FOUND') throw error; }
|
|
152
291
|
if (current) {
|
|
153
292
|
if (current.createdAt !== record.createdAt || current.inputDigest !== record.inputDigest
|
|
154
|
-
|| current.
|
|
293
|
+
|| current.recipe !== record.recipe || flowStep(current).taskId !== flowStep(record).taskId
|
|
294
|
+
|| current.compare?.artifactId !== record.compare?.artifactId) {
|
|
155
295
|
flowFail('IRIS_FLOW_INPUT_CONFLICT', '原流程与预留创建身份不一致');
|
|
156
296
|
}
|
|
157
297
|
return current;
|
|
@@ -168,7 +308,9 @@ export function createFlowRun(root, input) {
|
|
|
168
308
|
export function updateFlowRun(root, previous, patch) {
|
|
169
309
|
const current = readFlowRun(root, previous.id);
|
|
170
310
|
if (current.revision !== previous.revision) flowFail('IRIS_FLOW_REVISION_CONFLICT', '流程在本次操作期间已改变');
|
|
171
|
-
|
|
311
|
+
const allowed = isAudioRecipe(current.recipe) || current.recipe === VIDEO_RECIPE ? [flowStepId(current), 'output']
|
|
312
|
+
: current.recipe === IMAGE_GENERATE_RECIPE ? ['generate', 'output', 'selection'] : ['edit', 'output', 'comparison', 'selection'];
|
|
313
|
+
if (!keys(patch, allowed, [])) throw new TypeError('未知流程更新字段');
|
|
172
314
|
const record = { ...current, ...patch, revision: current.revision + 1, updatedAt: new Date().toISOString() };
|
|
173
315
|
validate(record, current.id);
|
|
174
316
|
atomicWritePrivate(recordFile(root, record.id), JSON.stringify(record, null, 2) + '\n');
|
|
@@ -190,6 +332,6 @@ export function listFlowRuns(root) {
|
|
|
190
332
|
export function flowReferences(root) {
|
|
191
333
|
try { fs.lstatSync(path.join(root, 'flow-store')); }
|
|
192
334
|
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
193
|
-
return listFlowRuns(root).map(run => ({ run, taskIds: [run.
|
|
194
|
-
artifactIds: [...new Set([run.source
|
|
335
|
+
return listFlowRuns(root).map(run => ({ run, taskIds: [flowStep(run).taskId],
|
|
336
|
+
artifactIds: [...new Set([run.source?.artifactId, run.compare?.artifactId, run.output?.artifactId, run.selection?.artifactId].filter(Boolean))] }));
|
|
195
337
|
}
|
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 原生改图模型');
|